// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
#if !WINDOWS_UWP
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices;
using DebuggerLaunchMode = Microsoft.VisualStudio.TestTools.UnitTesting.DebuggerLaunchMode;
using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope;
#endif
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface;
using MessageLevel = Microsoft.VisualStudio.TestTools.UnitTesting.MessageLevel;
using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx;
namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
internal sealed partial class MSTestSettings
{
///
/// Populate adapter settings from the run settings XML.
///
/// The run settings XML, or when none was provided.
/// The logger for messages.
/// The configuration.
#if !WINDOWS_UWP
internal static void PopulateSettings(string? settingsXml, IAdapterMessageLogger? logger, IConfiguration? configuration)
{
try
{
PopulateSettingsCore(settingsXml, logger, configuration);
}
catch (XmlException ex)
{
throw new AdapterSettingsException(Resource.InvalidSettingsXml, ex);
}
}
#else
internal static void PopulateSettings(string? settingsXml, IAdapterMessageLogger? logger, IConfiguration? configuration)
=> PopulateSettingsCore(settingsXml, logger, configuration);
#endif
private static void PopulateSettingsCore(string? settingsXml, IAdapterMessageLogger? logger, IConfiguration? configuration)
{
#if !WINDOWS_UWP
if (configuration?["mstest"] is not null
&& settingsXml is not null
&& RunSettingsFileHasMSTestSettings(settingsXml))
{
throw new InvalidOperationException(Resource.DuplicateConfigurationError);
}
#endif
var settings = new MSTestSettings();
var runConfigurationSettings = RunConfigurationSettings.GetSettings(settingsXml);
#if !WINDOWS_UWP
if (!StringEx.IsNullOrEmpty(settingsXml) && configuration?["mstest"] is null)
#else
if (!StringEx.IsNullOrEmpty(settingsXml))
#endif
{
MSTestSettings? aliasSettings = GetSettings(settingsXml, SettingsNameAlias, logger);
settings = aliasSettings ?? GetSettings(settingsXml, SettingsName, logger) ?? new MSTestSettings();
SetGlobalSettings(settingsXml, settings, logger);
}
#if !WINDOWS_UWP
else if (configuration?["mstest"] is not null)
{
RunConfigurationSettings.SetRunConfigurationSettingsFromConfig(configuration, runConfigurationSettings);
SetSettingsFromConfig(configuration, logger, settings);
}
#endif
CurrentSettings = settings;
RunConfigurationSettings = runConfigurationSettings;
if (settings.RandomizeTestOrder && settings.OrderTestsByNameInClass)
{
logger?.SendMessage(MessageLevel.Warning, Resource.RandomTestOrderAndOrderTestsByNameInClassConflict);
}
// Track configuration source for telemetry.
#if !WINDOWS_UWP && !WIN_UI
if (MSTestTelemetryDataCollector.Current is { } telemetry)
{
telemetry.ConfigurationSource = configuration?["mstest"] is not null
? "testconfig.json"
: !StringEx.IsNullOrEmpty(settingsXml)
? "runsettings"
: "none";
}
#endif
}
private static void SetGlobalSettings(string runsettingsXml, MSTestSettings settings, IAdapterMessageLogger? logger)
{
XElement? runConfigElement = XDocument.Parse(runsettingsXml).Element("RunSettings")?.Element("RunConfiguration");
if (runConfigElement == null)
{
return;
}
string? disableParallelizationString = runConfigElement.Element("DisableParallelization")?.Value;
if (bool.TryParse(disableParallelizationString, out bool disableParallelization))
{
settings.DisableParallelization = disableParallelization;
}
else if (disableParallelizationString is not null)
{
logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, disableParallelizationString, "DisableParallelization"));
}
}
#if !WINDOWS_UWP
private static void ParseBooleanSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting)
{
if (configuration[$"mstest:{key}"] is not string value)
{
return;
}
if (bool.TryParse(value, out bool result))
{
setSetting(result);
}
else
{
logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key));
}
}
private static void ParseCaptureTraceOutputSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting)
{
if (configuration[$"mstest:{key}"] is not string value)
{
return;
}
// Accept the legacy boolean spelling (true -> Result, false -> None) as well as the
// TestOutputCaptureMode enum names (None/Result/Live) so existing configs keep working.
if (bool.TryParse(value, out bool boolResult))
{
setSetting(boolResult ? TestOutputCaptureMode.Result : TestOutputCaptureMode.None);
}
else if (TryParseCaptureMode(value, out TestOutputCaptureMode mode))
{
setSetting(mode);
}
else
{
logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key));
}
}
private static void ParseDebuggerLaunchModeSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting)
{
if (configuration[$"mstest:{key}"] is not string value)
{
return;
}
if (TryParseEnum(value, out DebuggerLaunchMode mode))
{
setSetting(mode);
}
else
{
logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key));
}
}
///
/// Reads the mstest:execution:dependencies section, which declares test dependencies outside the
/// test source as an alternative to [DependsOn]. Two shapes are supported: chains, where
/// each entry is an ordered list and every element waits for the one before it (the flat case), and
/// nodes, where an entry names one test and the prerequisites it waits for (the tree case:
/// fan-in, fan-out and per-node options).
///
///
/// The configuration model flattens arrays to indexed keys (...:nodes:0:test), so the indices are
/// probed until a key is absent. That is the same convention Microsoft.Extensions.Configuration uses.
///
private static void ParseDependencySettings(IConfiguration configuration, IAdapterMessageLogger? logger, MSTestSettings settings)
{
const string root = "mstest:execution:dependencies";
List? declarations = null;
// chains: [ [ "A.B.First", "A.B.Second" ], ... ] - each element waits for the previous one.
//
// The configuration model flattens an array to indexed keys and stores nothing under an *empty*
// element, and IConfiguration's indexer cannot tell an absent key from one present with a null value.
// A stray empty chain would therefore look exactly like the end of the outer array and silently
// discard every chain after it - dependencies vanishing without a diagnostic, which is the failure
// mode this feature must never have. The schema forbids chains shorter than two entries (a shorter
// one declares no edge anyway); the single-index lookahead below keeps one authoring slip from
// costing the declarations that follow it.
for (int chainIndex = 0; ; chainIndex++)
{
string? firstOfChain = configuration[$"{root}:chains:{chainIndex}:0"];
if (firstOfChain is null)
{
if (configuration[$"{root}:chains:{chainIndex + 1}:0"] is null)
{
break;
}
logger?.SendMessage(
MessageLevel.Warning,
string.Format(CultureInfo.CurrentCulture, Resource.DependencyConfigurationEmptyChain, chainIndex));
continue;
}
string previous = firstOfChain;
for (int testIndex = 1; ; testIndex++)
{
if (configuration[$"{root}:chains:{chainIndex}:{testIndex}"] is not { } current)
{
break;
}
(declarations ??= []).Add(new TestDependencyDeclaration(current, previous, proceedOnFailure: false));
previous = current;
}
}
// nodes: [ { "test": "...", "dependsOn": [ "..." ], "proceedOnFailure": true }, ... ]
//
// As for chains, a node missing its "test" key is indistinguishable from the end of the array through
// the indexer, so the same single-index lookahead keeps one malformed entry from silently discarding
// every node after it.
for (int nodeIndex = 0; ; nodeIndex++)
{
string? test = configuration[$"{root}:nodes:{nodeIndex}:test"];
if (test is null)
{
if (configuration[$"{root}:nodes:{nodeIndex + 1}:test"] is null)
{
break;
}
logger?.SendMessage(
MessageLevel.Warning,
string.Format(CultureInfo.CurrentCulture, Resource.DependencyConfigurationNodeWithoutTest, nodeIndex));
continue;
}
bool proceedOnFailure = false;
if (configuration[$"{root}:nodes:{nodeIndex}:proceedOnFailure"] is { } rawProceedOnFailure)
{
if (bool.TryParse(rawProceedOnFailure, out bool parsed))
{
proceedOnFailure = parsed;
}
else
{
logger?.SendMessage(
MessageLevel.Warning,
string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, rawProceedOnFailure, $"{root}:nodes:{nodeIndex}:proceedOnFailure"));
}
}
bool anyPrerequisite = false;
for (int prerequisiteIndex = 0; ; prerequisiteIndex++)
{
if (configuration[$"{root}:nodes:{nodeIndex}:dependsOn:{prerequisiteIndex}"] is not { } prerequisite)
{
break;
}
anyPrerequisite = true;
(declarations ??= []).Add(new TestDependencyDeclaration(test, prerequisite, proceedOnFailure));
}
if (!anyPrerequisite)
{
logger?.SendMessage(
MessageLevel.Warning,
string.Format(CultureInfo.CurrentCulture, Resource.DependencyConfigurationNodeWithoutPrerequisites, test));
}
}
if (declarations is not null)
{
settings.DeclaredDependencies = [.. declarations];
}
}
private static void ParseTimeoutSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting)
{
if (configuration[$"mstest:{key}"] is not string value)
{
return;
}
// This helper is only used for timeout settings, which must be strictly positive (in milliseconds).
// A value of 0 (or less) is rejected on purpose: omitting the key already means "no timeout" (the
// default is 0 internally), so accepting an explicit 0 here would be redundant and ambiguous. Invalid
// values are ignored with a warning rather than throwing.
if (int.TryParse(value, out int result) && result > 0)
{
setSetting(result);
}
else
{
logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, value, key));
}
}
private static void ParseSignedIntegerSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting)
{
if (configuration[$"mstest:{key}"] is not string value)
{
return;
}
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int result))
{
setSetting(result);
}
else
{
logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key));
}
}
///
/// Convert the parameter xml to TestSettings.
///
/// Configuration to load the settings from.
/// The logger for messages.
/// The MSTest settings.
internal static void SetSettingsFromConfig(IConfiguration configuration, IAdapterMessageLogger? logger, MSTestSettings settings)
{
// 'orderTestsByNameInClass' has moved under 'execution:' for consistency with the other execution settings.
// Prefer the new 'mstest:execution:orderTestsByNameInClass' key. Only fall back to the deprecated flat
// 'mstest:orderTestsByNameInClass' key (emitting a deprecation warning) when the new key is absent, so users
// can keep both keys for cross-version compatibility without being warned and without spurious parse warnings.
if (configuration["mstest:execution:orderTestsByNameInClass"] is not null)
{
ParseBooleanSetting(configuration, "execution:orderTestsByNameInClass", logger, value => settings.OrderTestsByNameInClass = value);
}
else if (configuration["mstest:orderTestsByNameInClass"] is not null)
{
logger?.SendMessage(MessageLevel.Warning, Resource.DeprecatedFlatOrderTestsByNameInClassKey);
ParseBooleanSetting(configuration, "orderTestsByNameInClass", logger, value => settings.OrderTestsByNameInClass = value);
}
ParseBooleanSetting(configuration, "execution:randomizeTestOrder", logger, value => settings.RandomizeTestOrder = value);
ParseSignedIntegerSetting(configuration, "execution:randomTestOrderSeed", logger, value => settings.RandomTestOrderSeed = value);
ParseDependencySettings(configuration, logger, settings);
ParseCaptureTraceOutputSetting(configuration, "output:captureTrace", logger, value => settings.OutputCaptureMode = value);
ParseBooleanSetting(configuration, "parallelism:enabled", logger, value => settings.DisableParallelization = !value);
ParseBooleanSetting(configuration, "execution:mapInconclusiveToFailed", logger, value => settings.MapInconclusiveToFailed = value);
ParseBooleanSetting(configuration, "execution:mapNotRunnableToFailed", logger, value => settings.MapNotRunnableToFailed = value);
ParseBooleanSetting(configuration, "execution:treatDiscoveryWarningsAsErrors", logger, value => settings.TreatDiscoveryWarningsAsErrors = value);
ParseBooleanSetting(configuration, "execution:considerEmptyDataSourceAsInconclusive", logger, value => settings.ConsiderEmptyDataSourceAsInconclusive = value);
ParseDebuggerLaunchModeSetting(configuration, "execution:launchDebuggerOnAssertionFailure", logger, value => settings.LaunchDebuggerOnAssertionFailure = value);
ParseBooleanSetting(configuration, "execution:captureAssertionFailureDiagnostics", logger, value => settings.CaptureAssertionFailureDiagnostics = value);
ParseBooleanSetting(configuration, "timeout:useCooperativeCancellation", logger, value => settings.CooperativeCancellationTimeout = value);
ParseTimeoutSetting(configuration, "timeout:test", logger, value => settings.TestTimeout = value);
ParseTimeoutSetting(configuration, "timeout:assemblyCleanup", logger, value => settings.AssemblyCleanupTimeout = value);
ParseTimeoutSetting(configuration, "timeout:assemblyInitialize", logger, value => settings.AssemblyInitializeTimeout = value);
ParseTimeoutSetting(configuration, "timeout:classInitialize", logger, value => settings.ClassInitializeTimeout = value);
ParseTimeoutSetting(configuration, "timeout:classCleanup", logger, value => settings.ClassCleanupTimeout = value);
ParseTimeoutSetting(configuration, "timeout:testInitialize", logger, value => settings.TestInitializeTimeout = value);
ParseTimeoutSetting(configuration, "timeout:testCleanup", logger, value => settings.TestCleanupTimeout = value);
ParseTimeoutSetting(configuration, "timeout:globalTestInitialize", logger, value => settings.GlobalTestInitializeTimeout = value);
ParseTimeoutSetting(configuration, "timeout:globalTestCleanup", logger, value => settings.GlobalTestCleanupTimeout = value);
if (configuration["mstest:parallelism:workers"] is string workers)
{
if (!int.TryParse(workers, out int parallelWorkers) || parallelWorkers < 0)
{
throw new AdapterSettingsException(string.Format(CultureInfo.CurrentCulture, Resource.InvalidParallelWorkersValue, workers));
}
settings.ParallelizationWorkers = parallelWorkers == 0 ? Environment.ProcessorCount : parallelWorkers;
}
if (configuration["mstest:parallelism:scope"] is string value)
{
value = value.Equals("class", StringComparison.OrdinalIgnoreCase) ? "ClassLevel"
: value.Equals("method", StringComparison.OrdinalIgnoreCase) ? "MethodLevel"
: value;
if (!TryParseEnum(value, out ExecutionScope scope))
{
throw new AdapterSettingsException(string.Format(CultureInfo.CurrentCulture, Resource.InvalidParallelScopeValue, value, string.Join(", ", Enum.GetNames(typeof(ExecutionScope)))));
}
settings.ParallelizationScope = scope;
}
MSTestSettingsProvider.Load(configuration);
}
#endif
}