File: OptProf\GetRunSettingsSessionConfiguration.cs
Web Access
Project: Microsoft.DotNet.Build.Tasks.VisualStudio.csproj (Microsoft.DotNet.Build.Tasks.VisualStudio)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Newtonsoft.Json.Linq;

namespace Microsoft.DotNet.Build.Tasks.VisualStudio;

/// <summary>
/// Calculates the SessionConfiguration to be used in .runsettings for OptProf training 
/// based on given OptProf.json configuration and VS bootstrapper information.
/// </summary>
[MSBuildMultiThreadableTask]
public sealed class GetRunSettingsSessionConfiguration : Task, IMultiThreadableTask
{
    /// <summary>Injected by MSBuild so paths resolve against the project directory in multithreaded builds.</summary>
    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

    /// <summary>
    /// Absolute path to the OptProf.json config file.
    /// </summary>
    [Required]
    public string ConfigurationFile { get; set; }

    /// <summary>
    /// Product drop name, e.g. 'Products/$(System.TeamProject)/$(Build.Repository.Name)/$(Build.SourceBranchName)/$(Build.BuildNumber)'
    /// </summary>
    [Required]
    public string ProductDropName { get; set; }

    /// <summary>
    /// Path to the BootstrapperInfo.json
    /// </summary>
    [Required]
    public string BootstrapperInfoPath { get; set; }

    /// <summary>
    /// Contents of SessionConfiguration node of the .runsettings file.
    /// </summary>
    [Output]
    public string SessionConfiguration { get; private set; }

    public override bool Execute()
    {
        try
        {
            var profilingInputsDropName = GetProfilingInputsDropName(ProductDropName);
            var buildDropName = GetTestsDropName(File.ReadAllText(TaskEnvironment.GetAbsolutePath(BootstrapperInfoPath), Encoding.UTF8));
            var (testContainersString, testCaseFilterString) = GetTestContainersAndFilters(File.ReadAllText(TaskEnvironment.GetAbsolutePath(ConfigurationFile), Encoding.UTF8), ConfigurationFile);

            SessionConfiguration = 
$@"<TestStores>
  <TestStore Uri=""vstsdrop:{profilingInputsDropName}"" />
  <TestStore Uri=""vstsdrop:{buildDropName}"" />
</TestStores>
<TestContainers>
{testContainersString}
</TestContainers>
<TestCaseFilter>{testCaseFilterString}</TestCaseFilter>";
        }
        catch (ApplicationException e)
        {
            Log.LogError(e.Message);
        }

        return !Log.HasLoggedErrors;
    }

    internal static string GetTestsDropName(string bootstrapperInfoJson)
    {
        try
        {
            var jsonContent = JToken.Parse(bootstrapperInfoJson);
            var dropUrl = (string)((JArray)jsonContent).First["BuildDrop"];

            const string prefix = "https://vsdrop.corp.microsoft.com/file/v1/Products/";
            if (!dropUrl.StartsWith(prefix, StringComparison.Ordinal))
            {
                throw new ApplicationException($"Invalid drop URL: '{dropUrl}'");
            }

            return $"Tests/{dropUrl.Substring(prefix.Length)}";
        }
        catch (Exception e)
        {                
            throw new InvalidDataException(
                $"Unable to read bootstrapper info: {e.Message}{Environment.NewLine}" +
                $"Content of BootstrapperInfo.json:{Environment.NewLine}" +
                $"{bootstrapperInfoJson}");
        }
    }

    private static string GetProfilingInputsDropName(string vsDropName)
    {
        const string prefix = "Products/";
        if (!vsDropName.StartsWith(prefix, StringComparison.Ordinal))
        {
            throw new ApplicationException("Invalid value of vsDropName argument: must start with 'Products/'.");
        }

        return "ProfilingInputs/" + vsDropName.Substring(prefix.Length);
    }

    internal static (string containers, string filters) GetTestContainersAndFilters(string configJson, string configPath)
    {
        try
        {
            var config = OptProfTrainingConfiguration.Deserialize(configJson);
            return (GetTestContainers(config), GetTestFilters(config));
        }
        catch (Exception e)
        {
            throw new ApplicationException($"Unable to read config file '{configPath}': {e.Message}");
        }
    }

    private static string GetTestContainers(OptProfTrainingConfiguration config)
    {
        var productContainers = config.Products?.Any() == true
          ? config.Products.SelectMany(x => x.Tests.Select(y => y.Container + ".dll"))
          : Enumerable.Empty<string>();

        var assemblyContainers = config.Assemblies?.Any() == true
            ? config.Assemblies.SelectMany(x => x.Tests.Select(y => y.Container + ".dll"))
            : Enumerable.Empty<string>();

        return string.Join(
            Environment.NewLine,
            productContainers
                .Concat(assemblyContainers)
                .Distinct()
                .Select(x => $@"  <TestContainer FileName=""{x}"" />"));
    }

    private static string GetTestFilters(OptProfTrainingConfiguration config)
    {
        var productTests = config.Products?.Any() == true
            ? config.Products.SelectMany(x => x.Tests.SelectMany(y => y.TestCases ?? y.FilteredTestCases.SelectMany(z => z.TestCases)))
            : Enumerable.Empty<string>();

        var assemblyTests = config.Assemblies?.Any() == true
            ? config.Assemblies.SelectMany(x => x.Tests.SelectMany(y => y.TestCases))
            : Enumerable.Empty<string>();

        return string.Join(
            "|",
            productTests
                .Concat(assemblyTests)
                .Distinct()
                .Select(x => $"FullyQualifiedName={x}"));
    }
}