File: Utilities\FakesUtilities.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.Common\Microsoft.TestPlatform.Common.csproj (Microsoft.VisualStudio.TestPlatform.Common)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.XPath;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;

namespace Microsoft.VisualStudio.TestPlatform.Common.Utilities;

/// <summary>
/// Provides helper to configure run settings for Fakes. Works even when Fakes are not installed on the machine.
/// </summary>
public static class FakesUtilities
{
    private const string ConfiguratorAssemblyQualifiedName = "Microsoft.VisualStudio.TestPlatform.Fakes.FakesDataCollectorConfiguration";

    private const string NetFrameworkConfiguratorMethodName = "GetDataCollectorSettingsOrDefault";

    private const string CrossPlatformConfiguratorMethodName = "GetCrossPlatformDataCollectorSettings";

    // Must be kept in sync with version being generated by VSUnitTesting
    private const string AssemblyVersion = ExternalAssemblyVersions.MicrosoftFakesAssemblyVersion;
    private const string FakesConfiguratorAssembly = "Microsoft.VisualStudio.TestPlatform.Fakes, Version=" + AssemblyVersion + ", Culture=neutral";

    /// <summary>
    /// Dynamically compute the Fakes data collector settings, given a set of test assemblies
    /// </summary>
    /// <param name="sources">test sources</param>
    /// <param name="runSettingsXml">runsettings</param>
    /// <returns>updated runsettings for fakes</returns>
    public static string GenerateFakesSettingsForRunConfiguration(string[] sources, string runSettingsXml)
    {
        ValidateArg.NotNull(sources, nameof(sources));
        ValidateArg.NotNull(runSettingsXml, nameof(runSettingsXml));

        var doc = new XmlDocument();
        using (var xmlReader = XmlReader.Create(
                   new StringReader(runSettingsXml),
                   new XmlReaderSettings() { CloseInput = true }))
        {
            doc.Load(xmlReader);
        }

        var frameworkVersion = GetFramework(runSettingsXml);
        return frameworkVersion == null
            ? runSettingsXml
            : TryAddFakesDataCollectorSettings(doc, sources, (FrameworkVersion)frameworkVersion)
                ? doc.OuterXml
                : runSettingsXml;
    }

    /// <summary>
    /// returns FrameworkVersion contained in the runsettingsXML
    /// </summary>
    /// <param name="runSettingsXml"></param>
    /// <returns></returns>
    private static FrameworkVersion? GetFramework(string runSettingsXml)
    {
        // We assume that only .NET Core, .NET Standard, or .NET Framework projects can have fakes.
        var targetFramework = XmlRunSettingsUtilities.GetRunConfigurationNode(runSettingsXml)?.TargetFramework;

        if (targetFramework == null)
        {
            return null;
        }

        // Since there are no FrameworkVersion values for .Net Core 2.0 +, we check TargetFramework instead
        // and default to FrameworkCore10 for .Net Core
        if (targetFramework.Name.IndexOf("netstandard", StringComparison.OrdinalIgnoreCase) >= 0 ||
            targetFramework.Name.IndexOf("netcoreapp", StringComparison.OrdinalIgnoreCase) >= 0 ||
            targetFramework.Name.IndexOf("net5", StringComparison.OrdinalIgnoreCase) >= 0)
        {
            return FrameworkVersion.FrameworkCore10;
        }

        // Since the Datacollector is separated on the NetFramework/NetCore line, any value of NETFramework
        // can be passed along to the fakes data collector configuration creator.
        // We default to Framework40 to preserve back compat
        return FrameworkVersion.Framework40;
    }

    /// <summary>
    /// Tries to embed the Fakes data collector settings for the given run settings.
    /// </summary>
    /// <param name="runSettings">runsettings</param>
    /// <param name="sources">test sources</param>
    /// <param name="framework">version of the framework</param>
    /// <returns>true if runSettings was modified; false otherwise.</returns>
    private static bool TryAddFakesDataCollectorSettings(
        XmlDocument runSettings,
        IEnumerable<string> sources,
        FrameworkVersion framework)
    {
        // Only cross-platform (v2) Fakes is supported. Fallback to v1 is removed.
        var crossPlatformConfigurator = TryGetFakesCrossPlatformDataCollectorConfigurator();
        if (crossPlatformConfigurator != null)
        {
            var sourceTfmMap = CreateDictionary(sources, framework);
            var fakesSettings = crossPlatformConfigurator(sourceTfmMap);

            // if no fakes, return settings unchanged
            if (fakesSettings == null)
            {
                return false;
            }

            InsertOrReplaceFakesDataCollectorNode(runSettings, fakesSettings);
            return true;
        }

        // Fakes v1 fallback support removed.
        return false;
    }

    internal static void InsertOrReplaceFakesDataCollectorNode(XmlDocument runSettings, DataCollectorSettings settings)
    {
        // override current settings
        var navigator = runSettings.CreateNavigator();
        var nodes = navigator!.Select("/RunSettings/DataCollectionRunSettings/DataCollectors/DataCollector");

        foreach (XPathNavigator dataCollectorNavigator in nodes)
        {
            var uri = dataCollectorNavigator.GetAttribute("uri", string.Empty);
            // We assume that only one uri can exist in a given runsettings
            if (string.Equals(FakesMetadata.DataCollectorUriV1, uri, StringComparison.OrdinalIgnoreCase) ||
                string.Equals(FakesMetadata.DataCollectorUriV2, uri, StringComparison.OrdinalIgnoreCase))
            {
                dataCollectorNavigator.ReplaceSelf(settings.ToXml().CreateNavigator()!);
                return;
            }
        }

        // insert new node
        XmlRunSettingsUtilities.InsertDataCollectorsNode(runSettings.CreateNavigator()!, settings);
    }

    private static IDictionary<string, FrameworkVersion> CreateDictionary(IEnumerable<string> sources, FrameworkVersion framework)
    {
        var dict = new Dictionary<string, FrameworkVersion>();
        foreach (var source in sources)
        {
            if (!dict.ContainsKey(source))
            {
                dict.Add(source, framework);
            }
        }

        return dict;
    }

    /// <summary>
    /// Ensures that an xml element corresponding to the test run settings exists in the setting document.
    /// </summary>
    /// <param name="settings">settings</param>
    /// <param name="settingsNode">settingsNode</param>
    private static void EnsureSettingsNode(XmlDocument settings, TestRunSettings settingsNode)
    {
        TPDebug.Assert(settingsNode != null, "Invalid Settings Node");
        TPDebug.Assert(settings != null, "Invalid Settings");

        var root = settings.DocumentElement!;
        if (root[settingsNode.Name] == null)
        {
            var newElement = settingsNode.ToXml();
            XmlNode newNode = settings.ImportNode(newElement, true);
            root.AppendChild(newNode);
        }
    }

    private static Func<IDictionary<string, FrameworkVersion>, DataCollectorSettings>? TryGetFakesCrossPlatformDataCollectorConfigurator()
    {
        try
        {
            var assembly = LoadTestPlatformAssembly();
            var type = assembly?.GetType(ConfiguratorAssemblyQualifiedName, false, false);
            var method = type?.GetMethod(CrossPlatformConfiguratorMethodName, [typeof(IDictionary<string, FrameworkVersion>)]);
            if (method != null)
            {
                return (Func<IDictionary<string, FrameworkVersion>, DataCollectorSettings>)method.CreateDelegate(typeof(Func<IDictionary<string, FrameworkVersion>, DataCollectorSettings>));
            }
        }
        catch (Exception ex)
        {
            EqtTrace.Info("Failed to create newly implemented Fakes Configurator. Reason: {0} ", ex);
        }

        return null;
    }

    private static Assembly? LoadTestPlatformAssembly()
    {
        try
        {
            return Assembly.Load(new AssemblyName(FakesConfiguratorAssembly));
        }
        catch (Exception ex)
        {
            EqtTrace.Info("Failed to load assembly {0}. Reason:{1}", FakesConfiguratorAssembly, ex);
        }
        return null;
    }

    internal static class FakesMetadata
    {
        /// <summary>
        /// Friendly name of the data collector
        /// </summary>
        public const string FriendlyName = "UnitTestIsolation";

        /// <summary>
        /// Gets the URI of the data collector (V1, deprecated and removed)
        /// </summary>
        public const string DataCollectorUriV1 = "datacollector://microsoft/unittestisolation/1.0";

        /// <summary>
        /// Gets the URI of the data collector (V2)
        /// </summary>
        public const string DataCollectorUriV2 = "datacollector://microsoft/unittestisolation/2.0";

        /// <summary>
        /// Gets the assembly qualified name of the data collector type
        /// </summary>
        public const string DataCollectorAssemblyQualifiedName = "Microsoft.VisualStudio.TraceCollector.UnitTestIsolationDataCollector, Microsoft.VisualStudio.TraceCollector, Version=" + AssemblyVersion + ", Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a";
    }
}