|
// 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";
}
}
|