From 5b07073be7bbcd8bab86ecc85f6f05a530884f0d Mon Sep 17 00:00:00 2001 From: copilot Date: Wed, 1 Jul 2026 16:44:51 +0200 Subject: [PATCH 01/24] Abstract VSTest message logging in PlatformServices (Phase 0/1) Introduce a platform-agnostic IAdapterMessageLogger abstraction (reusing the existing MessageLevel enum) so the platform services layer no longer depends on the VSTest IMessageLogger/TestMessageLevel for the standalone message-logger role. The VSTest bridge (ToAdapterMessageLogger) lives in the adapter-facing extension and is applied at the MSTestDiscoverer/MSTestExecutor boundary and at the two execution sites that reuse the framework handle as a logger. The recorder's dual logger role (IFrameworkHandle/ITestExecutionRecorder) is intentionally left for the later recorder phase, since logger and recorder are the same object there. Tests keep their Mock and wrap with .ToAdapterMessageLogger() at migrated call sites, so TestMessageLevel Verify assertions are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../VSTestAdapter/MSTestDiscoverer.cs | 5 +- .../VSTestAdapter/MSTestExecutor.cs | 4 +- .../Discovery/UnitTestDiscoverer.cs | 9 +- .../TestExecutionManager.Parallelization.cs | 2 +- .../Execution/TestExecutionManager.cs | 6 +- .../Helpers/MSTestDiscovererHelpers.cs | 6 +- .../Interfaces/IAdapterMessageLogger.cs | 25 ++++ .../MSTestSettings.Configuration.cs | 30 ++--- .../MSTestSettings.RunSettingsXml.cs | 31 ++--- .../AdapterMessageLoggerExtensions.cs | 39 ++++++ .../TestMethodFilter.cs | 10 +- .../Utilities/CLITestBase.discovery.cs | 2 +- .../Discovery/AssemblyEnumeratorTests.cs | 5 +- .../Discovery/TypeEnumeratorTests.cs | 7 +- .../Discovery/UnitTestDiscovererTests.cs | 32 ++--- .../Execution/TestExecutionManagerTests.cs | 22 ++-- .../Execution/TestMethodFilterTests.cs | 14 +- .../Execution/TypeCacheTests.cs | 9 +- .../Execution/UnitTestRunnerTests.cs | 7 +- .../Helpers/UnitTestOutcomeHelperTests.cs | 5 +- .../MSTestSettingsTests.cs | 121 +++++++++--------- .../ObjectModel/UnitTestResultTests.cs | 27 ++-- .../RunConfigurationSettingsTests.cs | 13 +- 23 files changed, 252 insertions(+), 179 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IAdapterMessageLogger.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs index a98e00c83a..c3dd955f99 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs @@ -84,9 +84,10 @@ internal async Task DiscoverTestsAsync(IEnumerable sources, IDiscoveryCo try { - if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, logger, configuration, _testSourceHandler)) + IAdapterMessageLogger adapterLogger = logger.ToAdapterMessageLogger(); + if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, adapterLogger, configuration, _testSourceHandler)) { - new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, logger, discoverySink, discoveryContext, isMTP); + new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink, discoveryContext, isMTP); } } finally diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs index 7518f455a7..a8ada1235f 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs @@ -139,7 +139,7 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run try { - if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext, frameworkHandle, configuration, new TestSourceHandler())) + if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext, frameworkHandle.ToAdapterMessageLogger(), configuration, new TestSourceHandler())) { return; } @@ -183,7 +183,7 @@ internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? run try { TestSourceHandler testSourceHandler = new(); - if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext, frameworkHandle, configuration, testSourceHandler)) + if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext, frameworkHandle.ToAdapterMessageLogger(), configuration, testSourceHandler)) { return; } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs index fad95daf23..d7e3cd213e 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs @@ -5,7 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -31,7 +30,7 @@ internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) /// Flag set to true when the platform running discovery is MTP. internal void DiscoverTests( IEnumerable sources, - IMessageLogger logger, + IAdapterMessageLogger logger, ITestCaseDiscoverySink discoverySink, IDiscoveryContext discoveryContext, bool isMTP) @@ -52,7 +51,7 @@ internal void DiscoverTests( /// Flag set to true when the platform running discovery is MTP. internal virtual void DiscoverTestsInSource( string source, - IMessageLogger logger, + IAdapterMessageLogger logger, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, bool isMTP) @@ -86,7 +85,7 @@ internal virtual void DiscoverTestsInSource( } string message = string.Format(CultureInfo.CurrentCulture, Resource.DiscoveryWarning, source, warning); - logger.SendMessage(TestMessageLevel.Warning, message); + logger.SendMessage(MessageLevel.Warning, message); } } @@ -109,7 +108,7 @@ internal virtual void DiscoverTestsInSource( private readonly ITestSourceHandler _testSource; - internal void SendTestCases(IEnumerable testElements, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, IMessageLogger logger) + internal void SendTestCases(IEnumerable testElements, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, IAdapterMessageLogger logger) { // Get filter expression and skip discovery in case filter expression has parsing error. ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, logger, out bool filterHasError); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 90d4be897c..5e5d2ed142 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -72,7 +72,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo } // Default test set is filtered tests based on user provided filter criteria - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, frameworkHandle, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, frameworkHandle.ToAdapterMessageLogger(), out bool filterHasError); if (filterHasError) { // Bail out without processing everything else below. diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index 83e42fe0c8..8db6351aea 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -124,13 +124,13 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC var tests = new List(); + IAdapterMessageLogger logger = frameworkHandle.ToAdapterMessageLogger(); + // deploy everything first. foreach (string source in sources) { _testRunCancellationToken?.ThrowIfCancellationRequested(); - var logger = (IMessageLogger)frameworkHandle; - // discover the tests GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, runContext, isMTP); tests.AddRange(discoverySink.Tests); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs index 593c6b73f5..76ea3316ec 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs @@ -4,7 +4,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -20,7 +20,7 @@ internal static class MSTestDiscovererHelpers internal static bool AreValidSources(IEnumerable sources, ITestSourceHandler testSourceHandler) => sources.Any(source => testSourceHandler.ValidSourceExtensions.Any(extension => string.Equals(Path.GetExtension(source), extension, StringComparison.OrdinalIgnoreCase))); - internal static bool InitializeDiscovery(IEnumerable sources, IDiscoveryContext? discoveryContext, IMessageLogger messageLogger, IConfiguration? configuration, ITestSourceHandler testSourceHandler) + internal static bool InitializeDiscovery(IEnumerable sources, IDiscoveryContext? discoveryContext, IAdapterMessageLogger messageLogger, IConfiguration? configuration, ITestSourceHandler testSourceHandler) { if (!AreValidSources(sources, testSourceHandler)) { @@ -35,7 +35,7 @@ internal static bool InitializeDiscovery(IEnumerable sources, IDiscovery } catch (AdapterSettingsException ex) { - messageLogger.SendMessage(TestMessageLevel.Error, ex.Message); + messageLogger.SendMessage(MessageLevel.Error, ex.Message); return false; } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IAdapterMessageLogger.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IAdapterMessageLogger.cs new file mode 100644 index 0000000000..fd894a8ad9 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IAdapterMessageLogger.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic sink for diagnostic messages (informational / warning / error) that the adapter +/// surfaces to whichever test host is running the tests. +/// +/// +/// This abstraction lets the platform services layer report messages without taking a dependency on a +/// specific test platform's object model. The mapping to a concrete host logger (for example the VSTest +/// IMessageLogger) is provided by the adapter layer. +/// +internal interface IAdapterMessageLogger +{ + /// + /// Reports a diagnostic message to the running test host. + /// + /// The severity of the message. + /// The message text. + void SendMessage(MessageLevel level, string message); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs index 6768b0b290..724ab28194 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs @@ -11,8 +11,8 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using MessageLevel = Microsoft.VisualStudio.TestTools.UnitTesting.MessageLevel; using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -25,7 +25,7 @@ internal sealed partial class MSTestSettings /// The discovery context. /// The logger for messages. /// The configuration. - internal static void PopulateSettings(IDiscoveryContext? context, IMessageLogger? logger, IConfiguration? configuration) + internal static void PopulateSettings(IDiscoveryContext? context, IAdapterMessageLogger? logger, IConfiguration? configuration) { #if !WINDOWS_UWP if (configuration?["mstest"] is not null @@ -62,7 +62,7 @@ internal static void PopulateSettings(IDiscoveryContext? context, IMessageLogger if (settings.RandomizeTestOrder && settings.OrderTestsByNameInClass) { - logger?.SendMessage(TestMessageLevel.Warning, Resource.RandomTestOrderAndOrderTestsByNameInClassConflict); + logger?.SendMessage(MessageLevel.Warning, Resource.RandomTestOrderAndOrderTestsByNameInClassConflict); } // Track configuration source for telemetry. @@ -78,7 +78,7 @@ internal static void PopulateSettings(IDiscoveryContext? context, IMessageLogger #endif } - private static void SetGlobalSettings(string runsettingsXml, MSTestSettings settings, IMessageLogger? logger) + private static void SetGlobalSettings(string runsettingsXml, MSTestSettings settings, IAdapterMessageLogger? logger) { XElement? runConfigElement = XDocument.Parse(runsettingsXml).Element("RunSettings")?.Element("RunConfiguration"); if (runConfigElement == null) @@ -93,12 +93,12 @@ private static void SetGlobalSettings(string runsettingsXml, MSTestSettings sett } else if (disableParallelizationString is not null) { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, disableParallelizationString, "DisableParallelization")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, disableParallelizationString, "DisableParallelization")); } } #if !WINDOWS_UWP - private static void ParseBooleanSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseBooleanSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -111,11 +111,11 @@ private static void ParseBooleanSetting(IConfiguration configuration, string key } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); } } - private static void ParseDebuggerLaunchModeSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseDebuggerLaunchModeSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -128,11 +128,11 @@ private static void ParseDebuggerLaunchModeSetting(IConfiguration configuration, } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); } } - private static void ParseTimeoutSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseTimeoutSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -149,11 +149,11 @@ private static void ParseTimeoutSetting(IConfiguration configuration, string key } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, value, key)); } } - private static void ParseSignedIntegerSetting(IConfiguration configuration, string key, IMessageLogger? logger, Action setSetting) + private static void ParseSignedIntegerSetting(IConfiguration configuration, string key, IAdapterMessageLogger? logger, Action setSetting) { if (configuration[$"mstest:{key}"] is not string value) { @@ -166,7 +166,7 @@ private static void ParseSignedIntegerSetting(IConfiguration configuration, stri } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, value, key)); } } @@ -176,7 +176,7 @@ private static void ParseSignedIntegerSetting(IConfiguration configuration, stri /// Configuration to load the settings from. /// The logger for messages. /// The MSTest settings. - internal static void SetSettingsFromConfig(IConfiguration configuration, IMessageLogger? logger, MSTestSettings 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 @@ -188,7 +188,7 @@ internal static void SetSettingsFromConfig(IConfiguration configuration, IMessag } else if (configuration["mstest:orderTestsByNameInClass"] is not null) { - logger?.SendMessage(TestMessageLevel.Warning, Resource.DeprecatedFlatOrderTestsByNameInClassKey); + logger?.SendMessage(MessageLevel.Warning, Resource.DeprecatedFlatOrderTestsByNameInClassKey); ParseBooleanSetting(configuration, "orderTestsByNameInClass", logger, value => settings.OrderTestsByNameInClass = value); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs index 73dd94d50b..a306cff8be 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using DebuggerLaunchMode = Microsoft.VisualStudio.TestTools.UnitTesting.DebuggerLaunchMode; +using MessageLevel = Microsoft.VisualStudio.TestTools.UnitTesting.MessageLevel; using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -60,7 +61,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) } #endif - internal static MSTestSettings? GetSettings(string? runSettingsXml, string settingName, IMessageLogger? logger) + internal static MSTestSettings? GetSettings(string? runSettingsXml, string settingName, IAdapterMessageLogger? logger) { if (StringEx.IsNullOrWhiteSpace(runSettingsXml)) { @@ -81,7 +82,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) return !reader.EOF ? ToSettings(reader.ReadSubtree(), logger) : null; } - private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logger) + private static MSTestSettings ToSettings(XmlReader reader, IAdapterMessageLogger? logger) { if (reader is null) { @@ -108,7 +109,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, captureTraceOutput, "CaptureTraceOutput")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, captureTraceOutput, "CaptureTraceOutput")); } break; @@ -120,7 +121,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapInconclusiveToFailed, "MapInconclusiveToFailed")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapInconclusiveToFailed, "MapInconclusiveToFailed")); } break; @@ -132,7 +133,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapNotRunnableToFailed, "MapNotRunnableToFailed")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, mapNotRunnableToFailed, "MapNotRunnableToFailed")); } break; @@ -144,7 +145,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, treatDiscoveryWarningsAsErrors, "TreatDiscoveryWarningsAsErrors")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, treatDiscoveryWarningsAsErrors, "TreatDiscoveryWarningsAsErrors")); } break; @@ -166,7 +167,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, considerEmptyDataSourceAsInconclusive, "ConsiderEmptyDataSourceAsInconclusive")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, considerEmptyDataSourceAsInconclusive, "ConsiderEmptyDataSourceAsInconclusive")); } break; @@ -193,7 +194,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, cooperativeCancellationTimeout, "CooperativeCancellationTimeout")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, cooperativeCancellationTimeout, "CooperativeCancellationTimeout")); } break; @@ -205,7 +206,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, orderTestsByNameInClass, "OrderTestsByNameInClass")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, orderTestsByNameInClass, "OrderTestsByNameInClass")); } break; @@ -217,7 +218,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomizeTestOrder, "RandomizeTestOrder")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomizeTestOrder, "RandomizeTestOrder")); } break; @@ -229,7 +230,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomTestOrderSeed, "RandomTestOrderSeed")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, randomTestOrderSeed, "RandomTestOrderSeed")); } break; @@ -241,7 +242,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, launchDebuggerOnAssertionFailure, "LaunchDebuggerOnAssertionFailure")); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, launchDebuggerOnAssertionFailure, "LaunchDebuggerOnAssertionFailure")); } break; @@ -256,7 +257,7 @@ private static MSTestSettings ToSettings(XmlReader reader, IMessageLogger? logge return settings; } - private static void ParseTimeoutSetting(string rawValue, string settingName, IMessageLogger? logger, Action setSetting) + private static void ParseTimeoutSetting(string rawValue, string settingName, IAdapterMessageLogger? logger, Action setSetting) { if (int.TryParse(rawValue, out int result) && result > 0) { @@ -264,7 +265,7 @@ private static void ParseTimeoutSetting(string rawValue, string settingName, IMe } else { - logger?.SendMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, rawValue, settingName)); + logger?.SendMessage(MessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, Resource.InvalidTimeoutValue, rawValue, settingName)); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs new file mode 100644 index 0000000000..c7dec256bb --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +/// +/// Bridges a VSTest to the platform-agnostic . +/// +/// +/// This is the single translation point between the VSTest object model and the platform services +/// message-logging abstraction. It is expected to move entirely into the adapter layer once the +/// execution pipeline no longer flows VSTest handles through the platform services (see the tracking +/// issue linked in the pull request that removes the VSTest object model from platform services). +/// +internal static class AdapterMessageLoggerExtensions +{ + /// + /// Wraps a VSTest as an . + /// + /// The host message logger to wrap. + /// A platform-agnostic logger that forwards to . + public static IAdapterMessageLogger ToAdapterMessageLogger(this IMessageLogger messageLogger) + => new HostMessageLogger(messageLogger); + + private sealed class HostMessageLogger : IAdapterMessageLogger + { + private readonly IMessageLogger _messageLogger; + + public HostMessageLogger(IMessageLogger messageLogger) + => _messageLogger = messageLogger; + + public void SendMessage(MessageLevel level, string message) + => _messageLogger.SendMessage(level.ToTestMessageLevel(), message); + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs b/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs index 481220ad0c..2ab18dfb0f 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -33,7 +33,7 @@ internal sealed class TestMethodFilter /// Handler to report test messages/start/end and results. /// Indicates that the filter is unsupported/has an error. /// A filter expression. - internal ITestCaseFilterExpression? GetFilterExpression(IDiscoveryContext? context, IMessageLogger logger, out bool filterHasError) + internal ITestCaseFilterExpression? GetFilterExpression(IDiscoveryContext? context, IAdapterMessageLogger logger, out bool filterHasError) { filterHasError = false; if (context == null) @@ -51,7 +51,7 @@ internal sealed class TestMethodFilter catch (TestPlatformFormatException ex) { filterHasError = true; - logger.SendMessage(TestMessageLevel.Error, ex.Message); + logger.SendMessage(MessageLevel.Error, ex.Message); } return filter; @@ -118,7 +118,7 @@ internal TestProperty PropertyProvider(string propertyName) /// The logger to log exception messages too. /// Filter expression. [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072:'target parameter' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", Justification = "GetTestCaseFilter is part of the VSTest discovery contract on the concrete DiscoveryContext type; the runtime guarantees the method exists on supported hosts.")] - private ITestCaseFilterExpression? GetTestCaseFilterFromDiscoveryContext(IDiscoveryContext context, IMessageLogger logger) + private ITestCaseFilterExpression? GetTestCaseFilterFromDiscoveryContext(IDiscoveryContext context, IAdapterMessageLogger logger) { try { @@ -134,7 +134,7 @@ internal TestProperty PropertyProvider(string propertyName) } catch (Exception ex) { - logger.SendMessage(TestMessageLevel.Warning, ex.Message); + logger.SendMessage(MessageLevel.Warning, ex.Message); } return null; diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index c1d2b1fad9..4696a25c42 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -27,7 +27,7 @@ internal static ImmutableArray DiscoverTests(string assemblyPath, stri string runSettingsXml = GetRunSettingsXml(string.Empty); var context = new InternalDiscoveryContext(runSettingsXml, testCaseFilter); - unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger, sink, context, false); + unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink, context, false); return sink.DiscoveredTests; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs index 2766c1f9da..6fd99105f0 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/AssemblyEnumeratorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.Collections.ObjectModel; @@ -9,6 +9,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -64,7 +65,7 @@ public void ConstructorShouldPopulateSettings() actualReader.ReadInnerXml(); }); var mockMessageLogger = new Mock(); - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, mockMessageLogger.Object); + var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, mockMessageLogger.Object.ToAdapterMessageLogger()); adapterSettings.Should().NotBeNull(); // Constructor has the side effect of populating the passed settings to MSTestSettings.CurrentSettings diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs index 73c2af6afc..ed5bdcead3 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -8,6 +8,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -120,7 +121,7 @@ public void GetTestsShouldReturnBaseTestMethodsFromAnotherAssemblyByDefault() mockRunContext.Setup(dc => dc.RunSettings).Returns(mockRunSettings.Object); mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true); TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DummyDerivedTestClass), Assembly.GetExecutingAssembly().FullName!); @@ -150,7 +151,7 @@ public void GetTestsShouldReturnBaseTestMethodsFromAnotherAssemblyByConfiguratio mockRunContext.Setup(dc => dc.RunSettings).Returns(mockRunSettings.Object); mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true); TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DummyDerivedTestClass), Assembly.GetExecutingAssembly().FullName!); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs index 1cb7bcd452..559d1b06e3 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -80,7 +80,7 @@ public void DiscoverTestsShouldThrowOnFileNotFound() .Returns(false); } - Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, sources[0])); } @@ -93,7 +93,7 @@ public void DiscoverTestsInSourceShouldThrowOnFileNotFound() _testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.DoesFileExist(Source)) .Returns(false); - Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, Source)); } @@ -122,10 +122,10 @@ public void DiscoverTestsInSourceShouldSendBackTestCasesDiscovered() """); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.AtLeastOnce); @@ -157,10 +157,10 @@ public void DiscoverTestsInSourceShouldThrowWhenTreatDiscoveryWarningsAsErrorsIs """; _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(settingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); // Assert action.Should().Throw() @@ -195,11 +195,11 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError """); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act & Assert // Should not throw an exception - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); // Verify warning message was sent to logger (not error) _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.AtLeastOnce); @@ -209,7 +209,7 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError public void SendTestCasesShouldNotSendAnyTestCasesIfThereAreNoTestElements() { // There is a null check for testElements in the code flow before this function call. So not adding a unit test for that. - _unitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object); + _unitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.Never); @@ -221,7 +221,7 @@ public void SendTestCasesShouldSendAllTestCaseData() var test2 = new UnitTestElement(CreateTestMethod("M2", "C", "A", displayName: null)); var testElements = new List { test1, test2 }; - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -240,7 +240,7 @@ public void SendTestCasesShouldSendFilteredTestCasesIfValidFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -259,7 +259,7 @@ public void SendTestCasesShouldSendAllTestCasesIfNullFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -278,7 +278,7 @@ public void SendTestCasesShouldSendAllTestCasesIfGetTestCaseFilterNotPresent() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -297,7 +297,7 @@ public void SendTestCasesShouldNotSendAnyTestCasesIfFilterError() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Never); @@ -325,7 +325,7 @@ public DummyNavigationData(string fileName, int minLineNumber, int maxLineNumber { internal override void DiscoverTestsInSource( string source, - IMessageLogger logger, + IAdapterMessageLogger logger, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, bool isMTP) diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 18cfc4734a..bb25174dae 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -461,7 +461,7 @@ public async Task RunTestsForTestShouldRunTestsInParallelWhenEnabledInRunsetting try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); @@ -498,7 +498,7 @@ public async Task RunTestsForTestShouldRunTestsByMethodLevelWhenSpecified() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -535,7 +535,7 @@ public async Task RunTestsForTestShouldRunTestsWithSpecifiedNumberOfWorkers() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); @@ -570,7 +570,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -629,7 +629,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -696,7 +696,7 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -730,7 +730,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -802,7 +802,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.Count.Should().Be(1); @@ -842,7 +842,7 @@ static TestCase[] BuildTests() => """); - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); var firstHandle = new TestableFrameworkHandle(); var firstManager = new TestExecutionManager(EnvironmentWrapper.Instance, task => task()); @@ -885,7 +885,7 @@ public async Task RunTestsWithoutRandomizeTestOrderShouldPreserveInputOrder() """); - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs index 990bc3ddb1..1be1b3a932 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -89,7 +89,7 @@ public void PropertyProviderValueForValidTestAndSupportedPropertyNameReturnsValu public void GetFilterExpressionForNullRunContextReturnsNull() { TestableTestExecutionRecorder recorder = new(); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(null, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(null, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeFalse(); @@ -100,7 +100,7 @@ public void GetFilterExpressionForValidRunContextReturnsValidTestCaseFilterExpre TestableTestExecutionRecorder recorder = new(); var dummyFilterExpression = new TestableTestCaseFilterExpression(); TestableRunContext runContext = new(() => dummyFilterExpression); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().Be(dummyFilterExpression); filterHasError.Should().BeFalse(); @@ -114,7 +114,7 @@ public void GetFilterExpressionForDiscoveryContextWithGetTestCaseFilterReturnsVa TestableTestExecutionRecorder recorder = new(); var dummyFilterExpression = new TestableTestCaseFilterExpression(); TestableDiscoveryContextWithGetTestCaseFilter discoveryContext = new(() => dummyFilterExpression); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().Be(dummyFilterExpression); filterHasError.Should().BeFalse(); @@ -127,7 +127,7 @@ public void GetFilterExpressionForDiscoveryContextWithoutGetTestCaseFilterReturn { TestableTestExecutionRecorder recorder = new(); TestableDiscoveryContextWithoutGetTestCaseFilter discoveryContext = new(); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeFalse(); @@ -137,7 +137,7 @@ public void GetFilterExpressionForRunContextGetTestCaseFilterThrowingExceptionRe { TestableTestExecutionRecorder recorder = new(); TestableRunContext runContext = new(() => throw new TestPlatformFormatException("DummyException")); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeTrue(); @@ -152,7 +152,7 @@ public void GetFilterExpressionForDiscoveryContextWithGetTestCaseFilterThrowingE { TestableTestExecutionRecorder recorder = new(); TestableDiscoveryContextWithGetTestCaseFilter discoveryContext = new(() => throw new TestPlatformFormatException("DummyException")); - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder, out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); filterExpression.Should().BeNull(); filterHasError.Should().BeTrue(); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs index 97ec776f20..91615b5c93 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TypeCacheTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -7,6 +7,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -817,7 +818,7 @@ public void GetTestMethodInfoWhenTimeoutAttributeNotSetShouldReturnTestMethodInf """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); @@ -841,7 +842,7 @@ public void GetTestMethodInfoWhenTimeoutAttributeSetShouldReturnTimeoutBasedOnAt """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); @@ -870,7 +871,7 @@ public void GetTestMethodInfoForInvalidGlobalTimeoutShouldReturnTestMethodInfoWi """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs index bfa71fc029..cb9b662fb9 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/UnitTestRunnerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -7,6 +7,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -65,7 +66,7 @@ public void ConstructorShouldPopulateSettings() actualReader.ReadInnerXml(); }); - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); adapterSettings.Should().NotBeNull(); var assemblyEnumerator = new UnitTestRunner(adapterSettings, []); @@ -449,7 +450,7 @@ private MSTestSettings GetSettingsWithDebugTrace(bool captureDebugTraceValue) actualReader.ReadInnerXml(); }); - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); return settings; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs index e256a8db07..27ba637848 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/UnitTestOutcomeHelperTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -28,7 +29,7 @@ public UnitTestOutcomeHelperTests() """; var mockMessageLogger = new Mock(); - _adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, mockMessageLogger.Object)!; + _adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, mockMessageLogger.Object.ToAdapterMessageLogger())!; } public void UniTestHelperToTestOutcomeForUnitTestOutcomePassedShouldReturnTestOutcomePassed() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs index 7f3a0cae03..cde5b91378 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; @@ -57,7 +58,7 @@ public void MapInconclusiveToFailedIsByDefaultFalseWhenNotSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapInconclusiveToFailed.Should().BeFalse(); } @@ -72,7 +73,7 @@ public void MapNotRunnableToFailedIsByDefaultTrueWhenNotSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapNotRunnableToFailed.Should().BeTrue(); } @@ -88,7 +89,7 @@ public void MapInconclusiveToFailedShouldBeConsumedFromRunSettingsWhenSpecified( """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapInconclusiveToFailed.Should().BeTrue(); } @@ -116,7 +117,7 @@ public void RunSettings_WithInvalidValues_GettingAWarningForEachInvalidSetting() """; - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Exactly(13)); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'CooperativeCancellationTimeout', setting will be ignored."), Times.Once); @@ -146,7 +147,7 @@ public void RunSettings_WithZeroTimeout_GettingAWarningAndTimeoutIsNotSet() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(0); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '0' for runsettings entry 'TestTimeout'. The timeout must be a strictly positive integer (in milliseconds); a value of 0 or less is not allowed. Omit the entry to use the default (no timeout). The setting will be ignored."), Times.Once); @@ -163,7 +164,7 @@ public void MapNotRunnableToFailedShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.MapNotRunnableToFailed.Should().BeTrue(); } @@ -178,7 +179,7 @@ public void CaptureDebugTracesShouldBeTrueByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.CaptureDebugTraces.Should().BeTrue(); } @@ -194,7 +195,7 @@ public void CaptureDebugTracesShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.CaptureDebugTraces.Should().BeFalse(); } @@ -210,7 +211,7 @@ public void TestTimeoutShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(4000); } @@ -225,7 +226,7 @@ public void TestTimeoutShouldBeSetToZeroIfNotSpecifiedInRunSettings() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(0); } @@ -240,7 +241,7 @@ public void TreatDiscoveryWarningsAsErrorsShouldBeTrueByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TreatDiscoveryWarningsAsErrors.Should().BeTrue(); } @@ -256,7 +257,7 @@ public void TreatDiscoveryWarningsAsErrorsShouldBeConsumedFromRunSettingsWhenSpe """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TreatDiscoveryWarningsAsErrors.Should().BeTrue(); } @@ -271,7 +272,7 @@ public void ParallelizationSettingsShouldNotBeSetByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.HasValue.Should().BeFalse(); adapterSettings.ParallelizationScope.HasValue.Should().BeFalse(); @@ -287,7 +288,7 @@ public void RandomizeTestOrderShouldBeFalseByDefault() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomizeTestOrder.Should().BeFalse(); adapterSettings.RandomTestOrderSeed.Should().BeNull(); @@ -305,7 +306,7 @@ public void RandomizeTestOrderShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomizeTestOrder.Should().BeTrue(); adapterSettings.RandomTestOrderSeed.Should().Be(42); @@ -322,7 +323,7 @@ public void RandomTestOrderSeedShouldAcceptNegativeValues() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomTestOrderSeed.Should().Be(-7); } @@ -338,7 +339,7 @@ public void RandomizeTestOrderShouldWarnWhenValueIsInvalid() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomizeTestOrder.Should().BeFalse(); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value 'notabool' for runsettings entry 'RandomizeTestOrder', setting will be ignored."), Times.Once); @@ -355,7 +356,7 @@ public void RandomTestOrderSeedShouldWarnWhenValueIsInvalid() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.RandomTestOrderSeed.Should().BeNull(); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value 'notanint' for runsettings entry 'RandomTestOrderSeed', setting will be ignored."), Times.Once); @@ -375,7 +376,7 @@ public void PopulateSettingsShouldWarnWhenRandomizeTestOrderAndOrderTestsByNameI _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.RandomizeTestOrder.Should().BeTrue(); MSTestSettings.CurrentSettings.OrderTestsByNameInClass.Should().BeTrue(); @@ -399,7 +400,7 @@ public void PopulateSettingsShouldNotWarnWhenOnlyRandomizeTestOrderIsEnabled() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.RandomizeTestOrder.Should().BeTrue(); MSTestSettings.CurrentSettings.OrderTestsByNameInClass.Should().BeFalse(); @@ -419,7 +420,7 @@ public void GetSettingsShouldThrowIfParallelizationWorkersIsNotInt() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("Invalid value 'GoneFishing' specified for 'Workers'. The value should be a non-negative integer."); @@ -438,7 +439,7 @@ public void GetSettingsShouldThrowIfParallelizationWorkersIsNegative() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("Invalid value '-1' specified for 'Workers'. The value should be a non-negative integer."); } @@ -456,7 +457,7 @@ public void ParallelizationWorkersShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(2); } @@ -474,7 +475,7 @@ public void ParallelizationWorkersShouldBeSetToProcessorCountWhenSetToZero() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(Environment.ProcessorCount); } @@ -491,7 +492,7 @@ public void ParallelizationSettingsShouldBeSetToDefaultsWhenNotSet() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(Environment.ProcessorCount); adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.ClassLevel); @@ -508,7 +509,7 @@ public void ParallelizationSettingsShouldBeSetToDefaultsOnAnEmptyParalleizeSetti """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(Environment.ProcessorCount); adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.ClassLevel); @@ -528,7 +529,7 @@ public void ParallelizationSettingsShouldBeConsumedFromRunSettingsWhenSpecified( """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationWorkers.Should().Be(127); adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.MethodLevel); @@ -547,7 +548,7 @@ public void GetSettingsShouldThrowIfParallelizationScopeIsNotValid() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("Invalid value 'JustParallelizeWillYou' specified for 'Scope'. Supported scopes are ClassLevel, MethodLevel."); } @@ -565,7 +566,7 @@ public void ParallelizationScopeShouldBeConsumedFromRunSettingsWhenSpecified() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.ParallelizationScope.Should().Be(ExecutionScope.MethodLevel); } @@ -583,7 +584,7 @@ public void GetSettingsShouldThrowWhenParallelizeHasInvalidElements() """; - Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object); + Action act = () => MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); AdapterSettingsException exception = act.Should().Throw().Which; exception.Message.Should().Contain("MSTestAdapter encountered an unexpected element 'Hola' in its settings 'Parallelize'. Remove this element and try again."); } @@ -601,7 +602,7 @@ public void GetSettingsShouldBeAbleToReadAfterParallelizationSettings() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(12); } @@ -621,7 +622,7 @@ public void GetSettingsShouldBeAbleToReadAfterParallelizationSettingsWithData() """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(12); adapterSettings.ParallelizationWorkers.Should().Be(127); @@ -640,7 +641,7 @@ public void GetSettingsShouldBeAbleToReadAfterParallelizationSettingsOnEmptyPara """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; adapterSettings.TestTimeout.Should().Be(12); } @@ -655,7 +656,7 @@ public void DisableParallelizationShouldBeFalseByDefault() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.DisableParallelization.Should().BeFalse(); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Never); @@ -674,7 +675,7 @@ public void DisableParallelizationShouldBeConsumedFromRunSettingsWhenSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.DisableParallelization.Should().BeTrue(); } @@ -692,7 +693,7 @@ public void DisableParallelization_WithInvalidValue_GettingAWarning() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'DisableParallelization', setting will be ignored."), Times.Once); } @@ -718,7 +719,7 @@ public void GetSettingsShouldProbePlatformSpecificSettingsAlso() actualReader.ReadInnerXml(); }); - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); _testablePlatformServiceProvider.MockSettingsProvider.Verify(sp => sp.Load(It.IsAny()), Times.Once); } @@ -744,7 +745,7 @@ public void GetSettingsShouldOnlyPassTheElementSubTreeToPlatformService() observedXml = actualReader.ReadOuterXml(); }); - MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); (expectedRunSettingXml == observedXml).Should().BeTrue(); } @@ -791,7 +792,7 @@ public void GetSettingsShouldBeAbleToReadSettingsAfterThePlatformServiceReadsIts } }); - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger())!; // Assert. dummyPlatformSpecificSetting.Should().BeTrue(); @@ -844,7 +845,7 @@ public void GetSettingsShouldBeAbleToReadSettingsIfThePlatformServiceDoesNotUnde } }); - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger())!; // Assert. dummyPlatformSpecificSetting.Should().BeTrue(); @@ -893,7 +894,7 @@ public void GetSettingsShouldOnlyReadTheAdapterSection() } }); - MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. outOfScopeCall.Should().BeFalse(); @@ -944,7 +945,7 @@ public void GetSettingsShouldWorkIfThereAreCommentsInTheXML() } }); - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger())!; // Assert. dummyPlatformSpecificSetting.Should().BeTrue(); @@ -980,7 +981,7 @@ public void CurrentSettingShouldReturnCachedLoadedSettings() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; MSTestSettings adapterSettings2 = MSTestSettings.CurrentSettings; @@ -1009,7 +1010,7 @@ public void PopulateSettingsShouldFillInSettingsFromSettingsObject() """; - var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object); + var settings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); settings.Should().NotBeNull(); MSTestSettings.PopulateSettings(settings); @@ -1021,7 +1022,7 @@ public void PopulateSettingsShouldFillInSettingsFromSettingsObject() public void PopulateSettingsShouldInitializeDefaultAdapterSettingsWhenDiscoveryContextIsNull() { - MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1032,7 +1033,7 @@ public void PopulateSettingsShouldInitializeDefaultAdapterSettingsWhenDiscoveryC public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull() { - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1044,7 +1045,7 @@ public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsXmlIsEmpty() { _mockDiscoveryContext.Setup(md => md.RunSettings!.SettingsXml).Returns(string.Empty); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1066,7 +1067,7 @@ public void PopulateSettingsShouldInitializeSettingsToDefaultIfNotSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1091,7 +1092,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestSection() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1117,7 +1118,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestV2Section() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1146,7 +1147,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestV2OverMSTestV1Sect _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1185,7 +1186,7 @@ public void ConfigJson_WithInvalidValues_GettingAWarningForEachInvalidSetting() .Returns((string key) => configDictionary.TryGetValue(key, out string? value) ? value : null); var settings = new MSTestSettings(); - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Exactly(12)); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'timeout:useCooperativeCancellation', setting will be ignored."), Times.Once); @@ -1216,7 +1217,7 @@ public void ConfigJson_WithZeroTimeout_GettingAWarningAndTimeoutIsNotSet() .Returns((string key) => configDictionary.TryGetValue(key, out string? value) ? value : null); var settings = new MSTestSettings(); - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); settings.TestTimeout.Should().Be(0); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '0' for runsettings entry 'timeout:test'. The timeout must be a strictly positive integer (in milliseconds); a value of 0 or less is not allowed. Omit the entry to use the default (no timeout). The setting will be ignored."), Times.Once); @@ -1255,7 +1256,7 @@ public void ConfigJson_WithValidValues_ValuesAreSetCorrectly() var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1296,7 +1297,7 @@ public void ConfigJson_WithDeprecatedFlatOrderTestsByNameInClassKey_ValueIsSetAn var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1318,7 +1319,7 @@ public void ConfigJson_WithExecutionOrderTestsByNameInClassKey_ValueIsSetAndNoWa var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1342,7 +1343,7 @@ public void ConfigJson_WithBothOrderTestsByNameInClassKeys_NewExecutionKeyTakesP var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.OrderTestsByNameInClass.Should().BeTrue(); @@ -1370,7 +1371,7 @@ private void ConfigJson_Parllelism_Enabled_Core(bool parallelismEnabled) var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.DisableParallelization.Should().Be(!parallelismEnabled); @@ -1390,7 +1391,7 @@ public void ConfigJson_WithValidValues_MethodScope() var settings = new MSTestSettings(); // Act - MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object, settings); + MSTestSettings.SetSettingsFromConfig(mockConfig.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), settings); // Assert settings.ParallelizationScope.Should().Be(ExecutionScope.MethodLevel); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs index d2fcb3feeb..806e904a6a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestResultTests.cs @@ -1,10 +1,11 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -28,7 +29,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomePassedShouldReturnTestOu """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Passed, adapterSettings); resultOutcome.Should().Be(TestOutcome.Passed); @@ -44,7 +45,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeFailedShouldReturnTestOu """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Failed, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -60,7 +61,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeErrorShouldReturnTestOut """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Error, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -77,7 +78,7 @@ public void UnitTestHelperToTestOutcomeForUnitTestOutcomeNotRunnableShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotRunnable, adapterSettings); resultOutcome.Should().Be(TestOutcome.None); @@ -93,7 +94,7 @@ public void UnitTestHelperToTestOutcomeForUnitTestOutcomeNotRunnableShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotRunnable, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -109,7 +110,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeTimeoutShouldReturnTestO """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Timeout, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -125,7 +126,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeIgnoredShouldReturnTestO """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Ignored, adapterSettings); resultOutcome.Should().Be(TestOutcome.Skipped); @@ -141,7 +142,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeInconclusiveShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Inconclusive, adapterSettings); resultOutcome.Should().Be(TestOutcome.Skipped); @@ -158,7 +159,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeInconclusiveShouldReturn """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.Inconclusive, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); @@ -174,7 +175,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeNotFoundShouldReturnTest """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotFound, adapterSettings); resultOutcome.Should().Be(TestOutcome.NotFound); @@ -190,7 +191,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeInProgressShouldReturnTe """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.InProgress, adapterSettings); resultOutcome.Should().Be(TestOutcome.None); @@ -207,7 +208,7 @@ public void UniTestHelperToTestOutcomeForUnitTestOutcomeNotRunnableShouldReturnT """; - MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object)!; + MSTestSettings adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger())!; var resultOutcome = UnitTestOutcomeHelper.ToTestOutcome(UnitTestOutcome.NotRunnable, adapterSettings); resultOutcome.Should().Be(TestOutcome.Failed); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs index 9e3d878be2..15e1c5c839 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -60,7 +61,7 @@ public void ConfigurationSettingsShouldReturnDefaultSettingsIfNotSet() public void PopulateSettingsShouldInitializeDefaultConfigurationSettingsWhenDiscoveryContextIsNull() { - MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(null, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -68,7 +69,7 @@ public void PopulateSettingsShouldInitializeDefaultConfigurationSettingsWhenDisc public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull() { - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -77,7 +78,7 @@ public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsXmlIsEmpty() { _mockDiscoveryContext.Setup(md => md.RunSettings!.SettingsXml).Returns(string.Empty); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -96,7 +97,7 @@ public void PopulateSettingsShouldInitializeSettingsToDefaultIfNotSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.Should().NotBeNull(); @@ -119,7 +120,7 @@ public void PopulateSettingsShouldInitializeSettingsFromRunConfigurationSection( _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object, null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.Should().NotBeNull(); From 40cabbb15bf0690661f5071bc2db958cc5f6a54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 1 Jul 2026 17:41:27 +0200 Subject: [PATCH 02/24] Abstract VSTest result reporting in PlatformServices (Phase 5) Introduce a platform-agnostic `ITestResultRecorder` so the execution result path in PlatformServices no longer constructs VSTest result-side types (`TestResult`, `TestOutcome`, `TestResultMessage`, `AttachmentSet`, `UriDataAttachment`). The single VSTest translation point now lives in the adapter-facing bridge `HostTestResultRecorder` (`Services/TestResultRecorderExtensions.ToTestResultRecorder`), mirroring the Phase 1 `IAdapterMessageLogger` + `AdapterMessageLoggerExtensions` pattern. `TestExecutionManager.Runner.cs` routes start/empty/result reporting through the neutral recorder. `TestResultExtensions.ToTestResult` and `UnitTestOutcomeHelper.ToTestOutcome` are unchanged and are now called from the bridge. This is a pure refactor with no behavior change: the outcome mapping, assembled `TestResult`, and the trace / `_hasAnyTestFailed` / NotFound+HotReload branches are preserved. Independent of and parallel to PR #9548 (Phase 1). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Execution/TestExecutionManager.Runner.cs | 50 ++++------- .../Interfaces/ITestResultRecorder.cs | 43 ++++++++++ .../Services/TestResultRecorderExtensions.cs | 86 +++++++++++++++++++ .../Execution/TestExecutionManagerTests.cs | 2 +- 4 files changed, 145 insertions(+), 36 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/TestResultRecorderExtensions.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs index dd43d20b47..1814e96eb4 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs @@ -2,8 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -17,11 +18,11 @@ internal void SendTestResults( TestTools.UnitTesting.TestResult[] unitTestResults, DateTimeOffset startTime, DateTimeOffset endTime, - ITestExecutionRecorder testExecutionRecorder) + ITestResultRecorder testResultRecorder) { if (unitTestResults.Length == 0) { - testExecutionRecorder.RecordEnd(test, TestOutcome.None); + testResultRecorder.RecordEmptyResult(test); return; } @@ -29,39 +30,14 @@ internal void SendTestResults( { _testRunCancellationToken?.ThrowIfCancellationRequested(); - var testResult = unitTestResult.ToTestResult( - test, - startTime, - endTime, - _environment.MachineName, - MSTestSettings.CurrentSettings); - - testExecutionRecorder.RecordEnd(test, testResult.Outcome); - - if (testResult.Outcome == TestOutcome.Failed) - { - if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) - { - PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor:Test {0} failed. ErrorMessage:{1}, ErrorStackTrace:{2}.", testResult.TestCase.FullyQualifiedName, testResult.ErrorMessage, testResult.ErrorStackTrace); - } - #if !WINDOWS_UWP && !WIN_UI - _hasAnyTestFailed = true; -#endif - } - - try - { - if (testResult.Outcome != TestOutcome.NotFound - || !RuntimeContext.IsHotReloadEnabled) - { - testExecutionRecorder.RecordResult(testResult); - } - } - catch (TestCanceledException) + if (testResultRecorder.RecordResult(test, unitTestResult, startTime, endTime)) { - // Ignore this exception + _hasAnyTestFailed = true; } +#else + testResultRecorder.RecordResult(test, unitTestResult, startTime, endTime); +#endif } } @@ -95,6 +71,10 @@ private async Task ExecuteTestsWithTestRunnerAsync( ? new RemotingMessageLogger(testExecutionRecorder) : testExecutionRecorder; + // Translate the VSTest recorder into the platform-agnostic result recorder a single time for this + // test set. This is the boundary at which VSTest result construction is applied. + ITestResultRecorder testResultRecorder = testExecutionRecorder.ToTestResultRecorder(_environment.MachineName, MSTestSettings.CurrentSettings); + foreach (TestCase currentTest in orderedTests) { _testRunCancellationToken?.ThrowIfCancellationRequested(); @@ -105,7 +85,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( UnitTestElement unitTestElement = currentTest.ToUnitTestElementWithUpdatedSource(source); - testExecutionRecorder.RecordStart(currentTest); + testResultRecorder.RecordStart(currentTest); DateTimeOffset startTime = DateTimeOffset.Now; @@ -143,7 +123,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( DateTimeOffset endTime = DateTimeOffset.Now; - SendTestResults(currentTest, unitTestResult, startTime, endTime, testExecutionRecorder); + SendTestResults(currentTest, unitTestResult, startTime, endTime, testResultRecorder); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs new file mode 100644 index 0000000000..157075ecdd --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic sink for reporting the lifecycle and results of test execution to whichever +/// test host is running the tests. +/// +/// +/// This abstraction lets the platform services layer report test start/end and results without taking a +/// dependency on a specific test platform's result object model (for example the VSTest TestResult, +/// TestOutcome and attachment types). The mapping to a concrete host recorder (for example the +/// VSTest ITestExecutionRecorder) is provided by the adapter layer. +/// +internal interface ITestResultRecorder +{ + /// + /// Signals that execution of the given test case has started. + /// + /// The test case whose execution is starting. + void RecordStart(TestCase testCase); + + /// + /// Signals that execution of the given test case ended without producing any result. + /// + /// The test case whose execution ended. + void RecordEmptyResult(TestCase testCase); + + /// + /// Reports a single framework for the given test case to the test host. + /// + /// The test case the result belongs to. + /// The framework result produced by executing the test. + /// The time at which the test started executing. + /// The time at which the test finished executing. + /// if the result was reported as a failure; otherwise, . + bool RecordResult(TestCase testCase, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestResultRecorderExtensions.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestResultRecorderExtensions.cs new file mode 100644 index 0000000000..911f563609 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestResultRecorderExtensions.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +/// +/// Bridges a VSTest to the platform-agnostic . +/// +/// +/// This is the single translation point between the VSTest result object model (TestResult, +/// TestOutcome, attachments, ...) and the platform services result-reporting abstraction. It is +/// expected to move entirely into the adapter layer once the execution pipeline no longer flows VSTest +/// recorders through the platform services. +/// +internal static class TestResultRecorderExtensions +{ + /// + /// Wraps a VSTest as an . + /// + /// The host recorder to wrap. + /// The computer name stamped on reported results. + /// The current MSTest settings used to map framework outcomes to host outcomes. + /// A platform-agnostic recorder that forwards to . + public static ITestResultRecorder ToTestResultRecorder(this ITestExecutionRecorder testExecutionRecorder, string computerName, MSTestSettings settings) + => new HostTestResultRecorder(testExecutionRecorder, computerName, settings); + + private sealed class HostTestResultRecorder : ITestResultRecorder + { + private readonly ITestExecutionRecorder _testExecutionRecorder; + private readonly string _computerName; + private readonly MSTestSettings _settings; + + public HostTestResultRecorder(ITestExecutionRecorder testExecutionRecorder, string computerName, MSTestSettings settings) + { + _testExecutionRecorder = testExecutionRecorder; + _computerName = computerName; + _settings = settings; + } + + public void RecordStart(TestCase testCase) + => _testExecutionRecorder.RecordStart(testCase); + + public void RecordEmptyResult(TestCase testCase) + => _testExecutionRecorder.RecordEnd(testCase, TestOutcome.None); + + public bool RecordResult(TestCase testCase, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime) + { + var testResult = unitTestResult.ToTestResult(testCase, startTime, endTime, _computerName, _settings); + + _testExecutionRecorder.RecordEnd(testCase, testResult.Outcome); + + bool isFailed = testResult.Outcome == TestOutcome.Failed; + if (isFailed && PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) + { + PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor:Test {0} failed. ErrorMessage:{1}, ErrorStackTrace:{2}.", testResult.TestCase.FullyQualifiedName, testResult.ErrorMessage, testResult.ErrorStackTrace); + } + + try + { + if (testResult.Outcome != TestOutcome.NotFound + || !RuntimeContext.IsHotReloadEnabled) + { + _testExecutionRecorder.RecordResult(testResult); + } + } + catch (TestCanceledException) + { + // Ignore this exception + } + + // A failure is reported only once RecordResult has completed (a swallowed TestCanceledException + // still counts as completed). This mirrors the original inline flow where the failure was + // observed as part of reporting the result. + return isFailed; + } + } +} diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 18cfc4734a..b9bb6963f4 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -125,7 +125,7 @@ public void SendTestResults_WhenUnitTestResultsIsEmpty_RecordsEndWithoutResult() TestCase testCase = GetTestCase(typeof(DummyTestClass), "PassingTest"); Microsoft.VisualStudio.TestTools.UnitTesting.TestResult[] unitTestResults = []; - _testExecutionManager.SendTestResults(testCase, unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle); + _testExecutionManager.SendTestResults(testCase, unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings)); _frameworkHandle.TestCaseEndList.Should().Equal("PassingTest:None"); _frameworkHandle.ResultsList.Should().BeEmpty(); From f05621da5a89f7b6136240977ee25b5ec67b0159 Mon Sep 17 00:00:00 2001 From: copilot Date: Thu, 2 Jul 2026 20:05:37 +0200 Subject: [PATCH 03/24] Address review comments on Phase 1 logging PR - AdapterMessageLoggerExtensions: validate the logger argument (throw ArgumentNullException instead of a later NullReferenceException) and make ToAdapterMessageLogger internal to match the containing internal class. - TestExecutionManager.Parallelization: cache a single IAdapterMessageLogger per source instead of allocating a wrapper per call, and route the parallelization banner and error SendMessage calls through it (removing the file's remaining TestMessageLevel usage). - MSTestSettingsTests: drop two dead-store local assignments flagged by CodeQL; the GetSettings calls remain as statements so logging side effects and Verify assertions are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Execution/TestExecutionManager.Parallelization.cs | 11 ++++++----- .../Services/AdapterMessageLoggerExtensions.cs | 4 ++-- .../MSTestSettingsTests.cs | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 5e5d2ed142..f4b12f1e51 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -8,7 +8,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; @@ -63,6 +62,8 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo } #endif + IAdapterMessageLogger adapterMessageLogger = frameworkHandle.ToAdapterMessageLogger(); + using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, runContext?.RunSettings); bool usesAppDomains = isolationHost is TestSourceHost { UsesAppDomain: true }; @@ -72,7 +73,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo } // Default test set is filtered tests based on user provided filter criteria - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, frameworkHandle.ToAdapterMessageLogger(), out bool filterHasError); + ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, adapterMessageLogger, out bool filterHasError); if (filterHasError) { // Bail out without processing everything else below. @@ -142,8 +143,8 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo if (!MSTestSettings.CurrentSettings.DisableParallelization && sourceSettings.CanParallelizeAssembly && parallelWorkers > 0) { // Parallelization is enabled. Let's do further classification for sets. - frameworkHandle.SendMessage( - TestMessageLevel.Informational, + adapterMessageLogger.SendMessage( + MessageLevel.Informational, string.Format(CultureInfo.CurrentCulture, Resource.TestParallelizationBanner, source, parallelWorkers, parallelScope)); // Create test sets for execution, we can execute them in parallel based on parallel settings @@ -248,7 +249,7 @@ .. parallelizableTestSet PlatformServiceProvider.Instance.AdapterTraceLogger.Error("Error occurred while executing tests in parallel{0}{1}", Environment.NewLine, exceptionToString); } - frameworkHandle.SendMessage(TestMessageLevel.Error, exceptionToString); + adapterMessageLogger.SendMessage(MessageLevel.Error, exceptionToString); throw; } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs index c7dec256bb..b8d4ca1b42 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs @@ -23,8 +23,8 @@ internal static class AdapterMessageLoggerExtensions /// /// The host message logger to wrap. /// A platform-agnostic logger that forwards to . - public static IAdapterMessageLogger ToAdapterMessageLogger(this IMessageLogger messageLogger) - => new HostMessageLogger(messageLogger); + internal static IAdapterMessageLogger ToAdapterMessageLogger(this IMessageLogger messageLogger) + => new HostMessageLogger(messageLogger ?? throw new ArgumentNullException(nameof(messageLogger))); private sealed class HostMessageLogger : IAdapterMessageLogger { diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs index cde5b91378..a38cfe178b 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs @@ -117,7 +117,7 @@ public void RunSettings_WithInvalidValues_GettingAWarningForEachInvalidSetting() """; - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsNameAlias, _mockMessageLogger.Object.ToAdapterMessageLogger()); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Exactly(13)); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'CooperativeCancellationTimeout', setting will be ignored."), Times.Once); @@ -719,7 +719,7 @@ public void GetSettingsShouldProbePlatformSpecificSettingsAlso() actualReader.ReadInnerXml(); }); - var adapterSettings = MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); + MSTestSettings.GetSettings(runSettingsXml, MSTestSettings.SettingsName, _mockMessageLogger.Object.ToAdapterMessageLogger()); _testablePlatformServiceProvider.MockSettingsProvider.Verify(sp => sp.Load(It.IsAny()), Times.Once); } From 3f85af01627d6949cf55d4c0f371b275613dfb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 2 Jul 2026 20:09:46 +0200 Subject: [PATCH 04/24] Clarify ITestResultRecorder remarks re: platform boundary Address PR review: the concrete recorder is provided at the platform boundary by a wrapper over the host's ITestExecutionRecorder (currently TestResultRecorderExtensions in PlatformServices/Services), rather than by the 'adapter layer'. Doc-only change; no behavior change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Interfaces/ITestResultRecorder.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs index 157075ecdd..d944cd33ff 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs @@ -14,8 +14,10 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Int /// /// This abstraction lets the platform services layer report test start/end and results without taking a /// dependency on a specific test platform's result object model (for example the VSTest TestResult, -/// TestOutcome and attachment types). The mapping to a concrete host recorder (for example the -/// VSTest ITestExecutionRecorder) is provided by the adapter layer. +/// TestOutcome and attachment types). The concrete recorder is provided at the platform boundary by a +/// wrapper over the host's result recorder (currently TestResultRecorderExtensions, which wraps the +/// VSTest ITestExecutionRecorder), and is expected to move fully out of the platform services layer in +/// a later phase. /// internal interface ITestResultRecorder { From f1e1f3621257dfe50a270cb7b70a24e7ce8c34d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 11:19:39 +0200 Subject: [PATCH 05/24] Abstract VSTest test filtering in PlatformServices (Phase 2 of platform-agnostic effort) (#9555) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Discovery/UnitTestDiscoverer.cs | 12 +- .../TestExecutionManager.Parallelization.cs | 4 +- .../Execution/TestExecutionManager.Runner.cs | 6 +- .../Interfaces/ITestElementFilter.cs | 29 +++++ .../TestMethodFilter.cs | 42 +++++++ .../TestCaseFilteringTests.cs | 107 ++++++++++++++++++ .../Utilities/CLITestBase.discovery.cs | 53 ++++++++- .../Execution/TestMethodFilterTests.cs | 62 ++++++++++ 8 files changed, 298 insertions(+), 17 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilter.cs create mode 100644 test/IntegrationTests/MSTest.IntegrationTests/TestCaseFilteringTests.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs index d7e3cd213e..eec21b8710 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs @@ -110,8 +110,8 @@ internal virtual void DiscoverTestsInSource( internal void SendTestCases(IEnumerable testElements, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, IAdapterMessageLogger logger) { - // Get filter expression and skip discovery in case filter expression has parsing error. - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(discoveryContext, logger, out bool filterHasError); + // Get filter and skip discovery in case filter expression has parsing error. + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(discoveryContext, logger, out bool filterHasError); if (filterHasError) { return; @@ -119,15 +119,13 @@ internal void SendTestCases(IEnumerable testElements, ITestCase foreach (UnitTestElement testElement in testElements) { - var testCase = testElement.ToTestCase(); - - // Filter tests based on test case filters - if (filterExpression != null && !filterExpression.MatchTestCase(testCase, p => _testMethodFilter.PropertyValueProvider(testCase, p))) + // Filter tests based on test case filters. + if (filter is not null && !filter.Matches(testElement)) { continue; } - discoverySink.SendTestCase(testCase); + discoverySink.SendTestCase(testElement.ToTestCase()); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index f4b12f1e51..71451b676a 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -73,7 +73,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo } // Default test set is filtered tests based on user provided filter criteria - ITestCaseFilterExpression? filterExpression = _testMethodFilter.GetFilterExpression(runContext, adapterMessageLogger, out bool filterHasError); + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(runContext, adapterMessageLogger, out bool filterHasError); if (filterHasError) { // Bail out without processing everything else below. @@ -113,7 +113,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo int parallelWorkers = sourceSettings.Workers; ExecutionScope parallelScope = sourceSettings.Scope; - TestCase[] testsToRun = [.. tests.Where(t => MatchTestFilter(filterExpression, t, _testMethodFilter))]; + TestCase[] testsToRun = [.. tests.Where(t => MatchTestFilter(filter, t, source))]; if (_testOrderRandom is { } sourceRandom) { Shuffle(sourceRandom, testsToRun); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs index 1814e96eb4..4a15c30509 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs @@ -41,10 +41,10 @@ internal void SendTestResults( } } - private static bool MatchTestFilter(ITestCaseFilterExpression? filterExpression, TestCase test, TestMethodFilter testMethodFilter) + private static bool MatchTestFilter(ITestElementFilter? filter, TestCase test, string source) { - if (filterExpression != null - && !filterExpression.MatchTestCase(test, p => testMethodFilter.PropertyValueProvider(test, p))) + if (filter is not null + && !filter.Matches(test.ToUnitTestElementWithUpdatedSource(source))) { // Skip test if not fitting filter criteria. return false; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilter.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilter.cs new file mode 100644 index 0000000000..357f8762e4 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilter.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic predicate that decides whether a discovered or executed +/// should be included in the current test run, based on the user-provided test-case filter. +/// +/// +/// This abstraction lets the platform services discovery and execution pipelines apply filtering over the +/// neutral model without taking a dependency on a specific test platform's +/// filter object model (for example the VSTest ITestCaseFilterExpression, TestProperty and +/// IRunContext.GetTestCaseFilter types). The concrete filter is produced at the platform boundary by a +/// wrapper that parses the host's filter (currently TestMethodFilter, which wraps the VSTest filter +/// expression), and is expected to move fully out of the platform services layer in a later phase. +/// A filter means "no filter" and every element is included. +/// +internal interface ITestElementFilter +{ + /// + /// Determines whether the given matches the filter and should be included. + /// + /// The test element to evaluate. + /// if the element matches the filter; otherwise, . + bool Matches(UnitTestElement testElement); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs b/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs index 2ab18dfb0f..b10d7d9130 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -57,6 +58,24 @@ internal sealed class TestMethodFilter return filter; } + /// + /// Builds a platform-agnostic for the properties supported by the adapter. + /// + /// The current context of the run. + /// Handler to report test messages/start/end and results. + /// Indicates that the filter is unsupported/has an error. + /// + /// A filter that evaluates instances, or when no + /// filter applies (in which case every element is included). + /// + internal ITestElementFilter? GetTestElementFilter(IDiscoveryContext? context, IAdapterMessageLogger logger, out bool filterHasError) + { + ITestCaseFilterExpression? filterExpression = GetFilterExpression(context, logger, out filterHasError); + return filterExpression is null + ? null + : new TestElementFilter(this, filterExpression); + } + /// /// Provides TestProperty for property name 'propertyName' as used in filter. /// @@ -139,4 +158,27 @@ internal TestProperty PropertyProvider(string propertyName) return null; } + + /// + /// Platform-agnostic filter that evaluates a against a VSTest + /// . This is the single point where the neutral element model is + /// translated to the VSTest test case in order to reuse the host's filter matching. + /// + private sealed class TestElementFilter : ITestElementFilter + { + private readonly TestMethodFilter _testMethodFilter; + private readonly ITestCaseFilterExpression _filterExpression; + + public TestElementFilter(TestMethodFilter testMethodFilter, ITestCaseFilterExpression filterExpression) + { + _testMethodFilter = testMethodFilter; + _filterExpression = filterExpression; + } + + public bool Matches(UnitTestElement testElement) + { + var testCase = testElement.ToTestCase(); + return _filterExpression.MatchTestCase(testCase, propertyName => _testMethodFilter.PropertyValueProvider(testCase, propertyName)); + } + } } diff --git a/test/IntegrationTests/MSTest.IntegrationTests/TestCaseFilteringTests.cs b/test/IntegrationTests/MSTest.IntegrationTests/TestCaseFilteringTests.cs new file mode 100644 index 0000000000..11d9b30662 --- /dev/null +++ b/test/IntegrationTests/MSTest.IntegrationTests/TestCaseFilteringTests.cs @@ -0,0 +1,107 @@ +// 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.Collections.Immutable; + +using Microsoft.MSTestV2.CLIAutomation; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +using TestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult; + +namespace MSTest.IntegrationTests; + +/// +/// Regression guard for the platform-agnostic filtering refactor (neutral ITestElementFilter). +/// Filtering is now expressed over the neutral UnitTestElement: discovery matches +/// element.ToTestCase(), and execution matches +/// incomingTestCase.ToUnitTestElementWithUpdatedSource(source).ToTestCase(). These tests lock the +/// end-to-end discover-then-run behavior for FullyQualifiedName and Id filters — the two +/// properties most sensitive to the TestCaseUnitTestElement round-trip that the refactor +/// re-routes — so a future change cannot silently break --filter. +/// +[TestClass] +public class TestCaseFilteringTests : CLITestBase +{ + private const string TestAsset = "DiscoverInternalsProject"; + + // A non-parameterized test with a stable, filter-safe fully qualified name (no filter operator chars). + private const string TargetDisplayName = "TopLevelInternalClass_TestMethod1"; + + [TestMethod] + public void FilterByFullyQualifiedNameSelectsExactlyTheMatchingTestDuringDiscovery() + { + string assemblyPath = GetAssetFullPath(TestAsset); + TestCase target = GetTargetTestCase(assemblyPath); + + ImmutableArray filtered = DiscoverTests(assemblyPath, $"FullyQualifiedName={target.FullyQualifiedName}"); + + Assert.HasCount(1, filtered); + Assert.AreEqual(target.FullyQualifiedName, filtered[0].FullyQualifiedName); + Assert.AreEqual(target.Id, filtered[0].Id); + } + + [TestMethod] + public void FilterByIdSelectsExactlyTheMatchingTestDuringDiscovery() + { + string assemblyPath = GetAssetFullPath(TestAsset); + TestCase target = GetTargetTestCase(assemblyPath); + + ImmutableArray filtered = DiscoverTests(assemblyPath, $"Id={target.Id}"); + + Assert.HasCount(1, filtered); + Assert.AreEqual(target.FullyQualifiedName, filtered[0].FullyQualifiedName); + Assert.AreEqual(target.Id, filtered[0].Id); + } + + [TestMethod] + public async Task FilterByFullyQualifiedNameSelectsExactlyTheMatchingTestDuringExecution() + { + string assemblyPath = GetAssetFullPath(TestAsset); + ImmutableArray allTests = DiscoverTests(assemblyPath); + TestCase target = allTests.First(t => t.DisplayName == TargetDisplayName); + + SimulateCrossProcessTestCases(allTests); + + ImmutableArray results = await RunTestsAsync(allTests, $"FullyQualifiedName={target.FullyQualifiedName}"); + + // HasCount(1) is the load-bearing assertion: it fails if the filter is ignored (all tests run) or if + // the FQN no longer matches after reconstruction (no test runs). The outcome check proves the target + // was actually executed via the reconstructed element, not merely selected. + Assert.HasCount(1, results); + Assert.AreEqual(target.FullyQualifiedName, results[0].TestCase.FullyQualifiedName); + Assert.AreEqual(TestOutcome.Passed, results[0].Outcome); + } + + [TestMethod] + public async Task FilterByIdSelectsExactlyTheMatchingTestDuringExecution() + { + string assemblyPath = GetAssetFullPath(TestAsset); + ImmutableArray allTests = DiscoverTests(assemblyPath); + Guid targetId = allTests.First(t => t.DisplayName == TargetDisplayName).Id; + + SimulateCrossProcessTestCases(allTests); + + ImmutableArray results = await RunTestsAsync(allTests, $"Id={targetId}"); + + // HasCount(1) is the load-bearing assertion: filtering by Id after the TestCase -> UnitTestElement -> + // TestCase reconstruction must still recompute the same Id and select exactly the target. The outcome + // check proves the target was actually executed via the reconstructed element. + Assert.HasCount(1, results); + Assert.AreEqual(targetId, results[0].TestCase.Id); + Assert.AreEqual(TestOutcome.Passed, results[0].Outcome); + } + + private static TestCase GetTargetTestCase(string assemblyPath) + => DiscoverTests(assemblyPath).First(t => t.DisplayName == TargetDisplayName); + + // Clears LocalExtensionData so the execution pipeline rebuilds the UnitTestElement (and its regenerated + // TestCase/Id) from the test-case properties, mirroring the out-of-proc VSTest path where deserialized + // test cases carry no LocalExtensionData. This is the round-trip the filtering refactor must preserve. + private static void SimulateCrossProcessTestCases(ImmutableArray testCases) + { + foreach (TestCase testCase in testCases) + { + testCase.LocalExtensionData = null; + } + } +} diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index 4696a25c42..638c50b607 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -41,6 +41,18 @@ internal static async Task> RunTestsAsync(IEnumerable return frameworkHandle.GetFlattenedTestResults(); } + internal static async Task> RunTestsAsync(IEnumerable testCases, string? testCaseFilter) + { + var testExecutionManager = new TestExecutionManager(); + var frameworkHandle = new InternalFrameworkHandle(); + + string runSettingsXml = GetRunSettingsXml(string.Empty); + var runContext = new InternalRunContext(runSettingsXml, testCaseFilter); + + await testExecutionManager.ExecuteTestsAsync(testCases, runContext, frameworkHandle, false); + return frameworkHandle.GetFlattenedTestResults(); + } + #region Helper classes private class InternalLogger : IMessageLogger { @@ -73,15 +85,46 @@ public InternalDiscoveryContext(string runSettings, string? testCaseFilter) public IRunSettings? RunSettings { get; } public ITestCaseFilterExpression? GetTestCaseFilter(IEnumerable supportedProperties, Func propertyProvider) => _filter; + } - private class InternalRunSettings : IRunSettings - { - public InternalRunSettings(string runSettings) => SettingsXml = runSettings; + private sealed class InternalRunContext : IRunContext + { + private readonly ITestCaseFilterExpression? _filter; - public string SettingsXml { get; } + public InternalRunContext(string runSettings, string? testCaseFilter) + { + RunSettings = new InternalRunSettings(runSettings); - public ISettingsProvider? GetSettings(string? settingsName) => throw new NotImplementedException(); + if (testCaseFilter != null) + { + _filter = TestCaseFilterFactory.ParseTestFilter(testCaseFilter); + } } + + public IRunSettings? RunSettings { get; } + + public bool KeepAlive => false; + + public bool InIsolation => false; + + public bool IsDataCollectionEnabled => false; + + public bool IsBeingDebugged => false; + + public string? TestRunDirectory => null; + + public string? SolutionDirectory => null; + + public ITestCaseFilterExpression? GetTestCaseFilter(IEnumerable? supportedProperties, Func propertyProvider) => _filter; + } + + private sealed class InternalRunSettings : IRunSettings + { + public InternalRunSettings(string runSettings) => SettingsXml = runSettings; + + public string SettingsXml { get; } + + public ISettingsProvider? GetSettings(string? settingsName) => throw new NotImplementedException(); } private sealed class InternalFrameworkHandle : IFrameworkHandle diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs index 1be1b3a932..4ac15c0473 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestMethodFilterTests.cs @@ -4,7 +4,9 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -160,6 +162,55 @@ public void GetFilterExpressionForDiscoveryContextWithGetTestCaseFilterThrowingE recorder.TestMessageLevel.Should().Be(TestMessageLevel.Error); } + public void GetTestElementFilterForNullContextReturnsNull() + { + TestableTestExecutionRecorder recorder = new(); + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(null, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().BeNull(); + filterHasError.Should().BeFalse(); + } + + public void GetTestElementFilterForContextWithoutFilterReturnsNull() + { + TestableTestExecutionRecorder recorder = new(); + TestableDiscoveryContextWithoutGetTestCaseFilter discoveryContext = new(); + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(discoveryContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().BeNull(); + filterHasError.Should().BeFalse(); + } + + public void GetTestElementFilterMatchesElementsUsingUnderlyingFilterExpression() + { + TestableTestExecutionRecorder recorder = new(); + MatchingTestCaseFilterExpression matchingFilterExpression = new(testCase => testCase.DisplayName == "M1"); + TestableRunContext runContext = new(() => matchingFilterExpression); + + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().NotBeNull(); + filterHasError.Should().BeFalse(); + + var matching = new UnitTestElement(new TestMethod("M1", "C", "A", displayName: "M1")); + var nonMatching = new UnitTestElement(new TestMethod("M2", "C", "A", displayName: "M2")); + + filter!.Matches(matching).Should().BeTrue(); + filter.Matches(nonMatching).Should().BeFalse(); + } + + public void GetTestElementFilterForFilterErrorReturnsNullWithFilterHasErrorTrue() + { + TestableTestExecutionRecorder recorder = new(); + TestableRunContext runContext = new(() => throw new TestPlatformFormatException("DummyException")); + ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(runContext, recorder.ToAdapterMessageLogger(), out bool filterHasError); + + filter.Should().BeNull(); + filterHasError.Should().BeTrue(); + recorder.Message.Should().Be("DummyException"); + recorder.TestMessageLevel.Should().Be(TestMessageLevel.Error); + } + [DummyTestClass] internal class DummyTestClassWithTestMethods { @@ -232,5 +283,16 @@ private sealed class TestableTestCaseFilterExpression : ITestCaseFilterExpressio public bool MatchTestCase(TestCase testCase, Func propertyValueProvider) => throw new NotImplementedException(); } + private sealed class MatchingTestCaseFilterExpression : ITestCaseFilterExpression + { + private readonly Func _matchTestCase; + + public MatchingTestCaseFilterExpression(Func matchTestCase) => _matchTestCase = matchTestCase; + + public string TestCaseFilterValue => null!; + + public bool MatchTestCase(TestCase testCase, Func propertyValueProvider) => _matchTestCase(testCase); + } + private class DummyTestClassAttribute : TestClassAttribute; } From f84600875085efabb187f37136d0cad6fab0a3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 12:19:16 +0200 Subject: [PATCH 06/24] Abstract VSTest discovery sink in PlatformServices (Phase 3 of platform-agnostic effort) (#9566) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../VSTestAdapter/MSTestDiscoverer.cs | 2 +- .../Discovery/UnitTestDiscoverer.cs | 8 ++-- .../Execution/TestCaseDiscoverySink.cs | 26 ++++++------ .../Interfaces/IUnitTestElementSink.cs | 28 +++++++++++++ .../Services/UnitTestElementSinkExtensions.cs | 42 +++++++++++++++++++ .../Utilities/CLITestBase.discovery.cs | 2 +- .../Discovery/UnitTestDiscovererTests.cs | 32 +++++++------- .../Execution/TestCaseDiscoverySinkTests.cs | 26 +++++++----- 8 files changed, 122 insertions(+), 44 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IUnitTestElementSink.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Services/UnitTestElementSinkExtensions.cs diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs index c3dd955f99..b57182075e 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs @@ -87,7 +87,7 @@ internal async Task DiscoverTestsAsync(IEnumerable sources, IDiscoveryCo IAdapterMessageLogger adapterLogger = logger.ToAdapterMessageLogger(); if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, adapterLogger, configuration, _testSourceHandler)) { - new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink, discoveryContext, isMTP); + new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext, isMTP); } } finally diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs index eec21b8710..05ce24f292 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs @@ -31,7 +31,7 @@ internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) internal void DiscoverTests( IEnumerable sources, IAdapterMessageLogger logger, - ITestCaseDiscoverySink discoverySink, + IUnitTestElementSink discoverySink, IDiscoveryContext discoveryContext, bool isMTP) { @@ -52,7 +52,7 @@ internal void DiscoverTests( internal virtual void DiscoverTestsInSource( string source, IAdapterMessageLogger logger, - ITestCaseDiscoverySink discoverySink, + IUnitTestElementSink discoverySink, IDiscoveryContext? discoveryContext, bool isMTP) { @@ -108,7 +108,7 @@ internal virtual void DiscoverTestsInSource( private readonly ITestSourceHandler _testSource; - internal void SendTestCases(IEnumerable testElements, ITestCaseDiscoverySink discoverySink, IDiscoveryContext? discoveryContext, IAdapterMessageLogger logger) + internal void SendTestCases(IEnumerable testElements, IUnitTestElementSink discoverySink, IDiscoveryContext? discoveryContext, IAdapterMessageLogger logger) { // Get filter and skip discovery in case filter expression has parsing error. ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(discoveryContext, logger, out bool filterHasError); @@ -125,7 +125,7 @@ internal void SendTestCases(IEnumerable testElements, ITestCase continue; } - discoverySink.SendTestCase(testElement.ToTestCase()); + discoverySink.SendTestElement(testElement); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs index 8ae3f43e28..47d5156839 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs @@ -1,15 +1,22 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// -/// The test case discovery sink. +/// The test case discovery sink used internally by execution to collect the discovered tests. /// -internal sealed class TestCaseDiscoverySink : ITestCaseDiscoverySink +/// +/// It implements the platform-agnostic but still materializes a VSTest +/// for every discovered element, because the execution pipeline currently consumes +/// instances. That materialization is expected to disappear once execution flows the +/// neutral model end-to-end. +/// +internal sealed class TestCaseDiscoverySink : IUnitTestElementSink { /// /// Gets the tests. @@ -17,14 +24,9 @@ internal sealed class TestCaseDiscoverySink : ITestCaseDiscoverySink public ICollection Tests { get; } = []; /// - /// Sends the test case. + /// Collects the discovered test, materializing it as a VSTest . /// - /// The discovered test. - public void SendTestCase(TestCase? discoveredTest) - { - if (discoveredTest != null) - { - Tests.Add(discoveredTest); - } - } + /// The discovered test element. + public void SendTestElement(UnitTestElement testElement) + => Tests.Add(testElement.ToTestCase()); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IUnitTestElementSink.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IUnitTestElementSink.cs new file mode 100644 index 0000000000..7f52cf48f8 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/IUnitTestElementSink.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic sink that receives the tests discovered by the adapter and hands them to the +/// discovery consumer (a test host during discovery, or the execution pipeline during a run). +/// +/// +/// This abstraction lets the platform services discovery pipeline emit the neutral +/// model without taking a dependency on a specific test platform's +/// discovery object model (for example the VSTest TestCase and ITestCaseDiscoverySink +/// types). The concrete sink is produced at the platform boundary by a wrapper over the host's +/// discovery sink (currently UnitTestElementSinkExtensions, which wraps the VSTest +/// ITestCaseDiscoverySink and materializes a TestCase for each element), and is expected +/// to move fully out of the platform services layer in a later phase. +/// +internal interface IUnitTestElementSink +{ + /// + /// Reports a discovered to the running test host. + /// + /// The discovered test element. + void SendTestElement(UnitTestElement testElement); +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/UnitTestElementSinkExtensions.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/UnitTestElementSinkExtensions.cs new file mode 100644 index 0000000000..0d22dab757 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/UnitTestElementSinkExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +/// +/// Bridges a VSTest to the platform-agnostic +/// . +/// +/// +/// This is the single translation point between the neutral model and the +/// VSTest discovery object model (TestCase, ITestCaseDiscoverySink). It materializes a +/// VSTest TestCase for each discovered element via . It is +/// expected to move entirely into the adapter layer once discovery no longer flows VSTest discovery sinks +/// through the platform services (see the tracking issue linked in the pull request that removes the VSTest +/// object model from platform services). +/// +internal static class UnitTestElementSinkExtensions +{ + /// + /// Wraps a VSTest as an . + /// + /// The host discovery sink to wrap. + /// A platform-agnostic sink that forwards to . + internal static IUnitTestElementSink ToUnitTestElementSink(this ITestCaseDiscoverySink discoverySink) + => new HostDiscoverySink(discoverySink ?? throw new ArgumentNullException(nameof(discoverySink))); + + private sealed class HostDiscoverySink : IUnitTestElementSink + { + private readonly ITestCaseDiscoverySink _discoverySink; + + public HostDiscoverySink(ITestCaseDiscoverySink discoverySink) + => _discoverySink = discoverySink; + + public void SendTestElement(UnitTestElement testElement) + => _discoverySink.SendTestCase(testElement.ToTestCase()); + } +} diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index 638c50b607..55d5b3adc5 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -27,7 +27,7 @@ internal static ImmutableArray DiscoverTests(string assemblyPath, stri string runSettingsXml = GetRunSettingsXml(string.Empty); var context = new InternalDiscoveryContext(runSettingsXml, testCaseFilter); - unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink, context, false); + unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink.ToUnitTestElementSink(), context, false); return sink.DiscoveredTests; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs index 559d1b06e3..b15605e885 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs @@ -80,7 +80,7 @@ public void DiscoverTestsShouldThrowOnFileNotFound() .Returns(false); } - Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, sources[0])); } @@ -93,7 +93,7 @@ public void DiscoverTestsInSourceShouldThrowOnFileNotFound() _testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.DoesFileExist(Source)) .Returns(false); - Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, Source)); } @@ -125,7 +125,7 @@ public void DiscoverTestsInSourceShouldSendBackTestCasesDiscovered() MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.AtLeastOnce); @@ -160,7 +160,7 @@ public void DiscoverTestsInSourceShouldThrowWhenTreatDiscoveryWarningsAsErrorsIs MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); // Assert action.Should().Throw() @@ -199,7 +199,7 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError // Act & Assert // Should not throw an exception - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); // Verify warning message was sent to logger (not error) _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.AtLeastOnce); @@ -209,7 +209,7 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError public void SendTestCasesShouldNotSendAnyTestCasesIfThereAreNoTestElements() { // There is a null check for testElements in the code flow before this function call. So not adding a unit test for that. - _unitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); + _unitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.Never); @@ -221,7 +221,7 @@ public void SendTestCasesShouldSendAllTestCaseData() var test2 = new UnitTestElement(CreateTestMethod("M2", "C", "A", displayName: null)); var testElements = new List { test1, test2 }; - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -240,7 +240,7 @@ public void SendTestCasesShouldSendFilteredTestCasesIfValidFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -259,7 +259,7 @@ public void SendTestCasesShouldSendAllTestCasesIfNullFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -278,7 +278,7 @@ public void SendTestCasesShouldSendAllTestCasesIfGetTestCaseFilterNotPresent() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -297,7 +297,7 @@ public void SendTestCasesShouldNotSendAnyTestCasesIfFilterError() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object, discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Never); @@ -326,14 +326,14 @@ public DummyNavigationData(string fileName, int minLineNumber, int maxLineNumber internal override void DiscoverTestsInSource( string source, IAdapterMessageLogger logger, - ITestCaseDiscoverySink discoverySink, + IUnitTestElementSink discoverySink, IDiscoveryContext? discoveryContext, bool isMTP) { - var testCase1 = new TestCase("A", new Uri("executor://testExecutor"), source); - var testCase2 = new TestCase("B", new Uri("executor://testExecutor"), source); - discoverySink.SendTestCase(testCase1); - discoverySink.SendTestCase(testCase2); + var testElement1 = new UnitTestElement(new TestMethod("A", "C", source, displayName: null)); + var testElement2 = new UnitTestElement(new TestMethod("B", "C", source, displayName: null)); + discoverySink.SendTestElement(testElement1); + discoverySink.SendTestElement(testElement2); } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs index 8eb4ab7011..a675af493d 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs @@ -4,7 +4,7 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using TestFramework.ForTestingMSTest; @@ -22,21 +22,27 @@ public void TestCaseDiscoverySinkConstructorShouldInitializeTests() _testCaseDiscoverySink.Tests.Count.Should().Be(0); } - public void SendTestCaseShouldNotAddTestIfTestCaseIsNull() + public void SendTestElementShouldAddTheMaterializedTestCaseToTests() { - _testCaseDiscoverySink.SendTestCase(null); + var testElement = new UnitTestElement(new TestMethod("M", "C", "A", displayName: null)); + + _testCaseDiscoverySink.SendTestElement(testElement); _testCaseDiscoverySink.Tests.Should().NotBeNull(); - _testCaseDiscoverySink.Tests.Count.Should().Be(0); + _testCaseDiscoverySink.Tests.Count.Should().Be(1); + _testCaseDiscoverySink.Tests.ToArray()[0].FullyQualifiedName.Should().Be("C.M"); } - public void SendTestCaseShouldAddTheTestCaseToTests() + public void SendTestElementShouldAddEachTestCaseInOrder() { - TestCase tc = new("TAttribute", new Uri("executor://TestExecutorUri"), "A"); - _testCaseDiscoverySink.SendTestCase(tc); + var testElement1 = new UnitTestElement(new TestMethod("M1", "C", "A", displayName: null)); + var testElement2 = new UnitTestElement(new TestMethod("M2", "C", "A", displayName: null)); - _testCaseDiscoverySink.Tests.Should().NotBeNull(); - _testCaseDiscoverySink.Tests.Count.Should().Be(1); - _testCaseDiscoverySink.Tests.ToArray()[0].Should().Be(tc); + _testCaseDiscoverySink.SendTestElement(testElement1); + _testCaseDiscoverySink.SendTestElement(testElement2); + + _testCaseDiscoverySink.Tests.Count.Should().Be(2); + _testCaseDiscoverySink.Tests.ToArray()[0].FullyQualifiedName.Should().Be("C.M1"); + _testCaseDiscoverySink.Tests.ToArray()[1].FullyQualifiedName.Should().Be("C.M2"); } } From 4f02b6e3f18ba71038d5c0e637af1a8a9501603e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 13:41:56 +0200 Subject: [PATCH 07/24] Abstract VSTest execution input in PlatformServices (Phase 4 of platform-agnostic effort) (#9572) Co-authored-by: Amaury Leveque Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../VSTestAdapter/MSTestExecutor.cs | 18 ++++- .../Execution/TcmTestPropertiesProvider.cs | 48 +++++++----- .../Execution/TestCaseDiscoverySink.cs | 17 ++--- .../TestExecutionManager.Parallelization.cs | 40 +++++----- .../Execution/TestExecutionManager.Runner.cs | 30 +++++--- .../TestExecutionManager.TestContext.cs | 6 +- .../Execution/TestExecutionManager.cs | 27 +++++-- .../Interfaces/ITestResultRecorder.cs | 4 +- .../ObjectModel/TestMethod.cs | 16 ++++ .../ObjectModel/UnitTestElement.cs | 71 +++++++++++++++++ .../Utilities/CLITestBase.discovery.cs | 18 ++++- .../TcmTestPropertiesProviderTests.cs | 22 +++--- .../Execution/TestCaseDiscoverySinkTests.cs | 20 ++--- .../Execution/TestExecutionManagerTests.cs | 76 +++++++++++-------- .../ObjectModel/UnitTestElementTests.cs | 38 ++++++++++ 15 files changed, 325 insertions(+), 126 deletions(-) diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs index a8ada1235f..a58d362ec4 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.VSTestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; @@ -144,7 +146,13 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run return; } - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(tests, runContext, frameworkHandle, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + // Convert the host test cases into the neutral model at this boundary so the execution engine runs + // on UnitTestElement end-to-end. Each element keeps its originating test case as an opaque handle + // (for full-fidelity result recording and deployment) plus the host execution-context (TCM) + // properties surfaced through TestContext. + UnitTestElement[] testElements = [.. tests.Select(ToUnitTestElement)]; + + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, runContext, frameworkHandle, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { @@ -152,6 +160,14 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run } } + private static UnitTestElement ToUnitTestElement(TestCase testCase) + { + UnitTestElement testElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); + testElement.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + testElement.HostRecordingHandle = testCase; + return testElement; + } + internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? runContext, IFrameworkHandle? frameworkHandle, IConfiguration? configuration, bool isMTP) { if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs index 92d8534c6b..30c8af1f17 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs @@ -8,16 +8,24 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// -/// Reads and parses the TcmTestProperties in order to populate them in TestRunParameters. +/// Reads and parses the test-case-management (TCM) properties supplied by a host on a test case, in order to +/// surface them to the running test through TestContext. /// +/// +/// This is a translation point between the VSTest test-case object model and the neutral execution context bag +/// consumed by the platform services engine. It runs at the adapter boundary (when a host hands the adapter +/// concrete test cases to run) and produces a string-keyed dictionary so the engine never depends on the VSTest +/// TestProperty object model. It is expected to move entirely into the adapter layer once the platform +/// services no longer reference the VSTest object model. +/// internal static class TcmTestPropertiesProvider { /// - /// Gets tcm properties from test case. + /// Gets the host execution context properties from a test case, keyed by the host property identifier. /// /// Test case. - /// Tcm properties. - public static IDictionary? GetTcmProperties(TestPlatformObjectModel.TestCase? testCase) + /// The properties, or when the test case is null or carries no test case id. + public static IReadOnlyDictionary? GetTcmProperties(TestPlatformObjectModel.TestCase? testCase) { // Return empty properties when testCase is null or when test case id is zero. if (testCase == null || @@ -26,26 +34,26 @@ internal static class TcmTestPropertiesProvider return null; } - var tcmProperties = new Dictionary(capacity: 15) + var tcmProperties = new Dictionary(capacity: 15) { // Step 1: Add common properties. - [EngineConstants.TestRunIdProperty] = testCase.GetPropertyValue(EngineConstants.TestRunIdProperty, default), - [EngineConstants.TestPlanIdProperty] = testCase.GetPropertyValue(EngineConstants.TestPlanIdProperty, default), - [EngineConstants.BuildConfigurationIdProperty] = testCase.GetPropertyValue(EngineConstants.BuildConfigurationIdProperty, default), - [EngineConstants.BuildDirectoryProperty] = testCase.GetPropertyValue(EngineConstants.BuildDirectoryProperty, default), - [EngineConstants.BuildFlavorProperty] = testCase.GetPropertyValue(EngineConstants.BuildFlavorProperty, default), - [EngineConstants.BuildNumberProperty] = testCase.GetPropertyValue(EngineConstants.BuildNumberProperty, default), - [EngineConstants.BuildPlatformProperty] = testCase.GetPropertyValue(EngineConstants.BuildPlatformProperty, default), - [EngineConstants.BuildUriProperty] = testCase.GetPropertyValue(EngineConstants.BuildUriProperty, default), - [EngineConstants.TfsServerCollectionUrlProperty] = testCase.GetPropertyValue(EngineConstants.TfsServerCollectionUrlProperty, default), - [EngineConstants.TfsTeamProjectProperty] = testCase.GetPropertyValue(EngineConstants.TfsTeamProjectProperty, default), - [EngineConstants.IsInLabEnvironmentProperty] = testCase.GetPropertyValue(EngineConstants.IsInLabEnvironmentProperty, default), + [EngineConstants.TestRunIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestRunIdProperty, default), + [EngineConstants.TestPlanIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestPlanIdProperty, default), + [EngineConstants.BuildConfigurationIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildConfigurationIdProperty, default), + [EngineConstants.BuildDirectoryProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildDirectoryProperty, default), + [EngineConstants.BuildFlavorProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildFlavorProperty, default), + [EngineConstants.BuildNumberProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildNumberProperty, default), + [EngineConstants.BuildPlatformProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildPlatformProperty, default), + [EngineConstants.BuildUriProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildUriProperty, default), + [EngineConstants.TfsServerCollectionUrlProperty.Id] = testCase.GetPropertyValue(EngineConstants.TfsServerCollectionUrlProperty, default), + [EngineConstants.TfsTeamProjectProperty.Id] = testCase.GetPropertyValue(EngineConstants.TfsTeamProjectProperty, default), + [EngineConstants.IsInLabEnvironmentProperty.Id] = testCase.GetPropertyValue(EngineConstants.IsInLabEnvironmentProperty, default), // Step 2: Add test case specific properties. - [EngineConstants.TestCaseIdProperty] = testCase.GetPropertyValue(EngineConstants.TestCaseIdProperty, default), - [EngineConstants.TestConfigurationIdProperty] = testCase.GetPropertyValue(EngineConstants.TestConfigurationIdProperty, default), - [EngineConstants.TestConfigurationNameProperty] = testCase.GetPropertyValue(EngineConstants.TestConfigurationNameProperty, default), - [EngineConstants.TestPointIdProperty] = testCase.GetPropertyValue(EngineConstants.TestPointIdProperty, default), + [EngineConstants.TestCaseIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestCaseIdProperty, default), + [EngineConstants.TestConfigurationIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestConfigurationIdProperty, default), + [EngineConstants.TestConfigurationNameProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestConfigurationNameProperty, default), + [EngineConstants.TestPointIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestPointIdProperty, default), }; return tcmProperties; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs index 47d5156839..d76e64a597 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestCaseDiscoverySink.cs @@ -3,30 +3,27 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// -/// The test case discovery sink used internally by execution to collect the discovered tests. +/// The discovery sink used internally by execution to collect the tests discovered from a source. /// /// -/// It implements the platform-agnostic but still materializes a VSTest -/// for every discovered element, because the execution pipeline currently consumes -/// instances. That materialization is expected to disappear once execution flows the -/// neutral model end-to-end. +/// It collects the neutral model directly, so the execution pipeline can flow +/// discovered tests without round-tripping them through a VSTest TestCase. /// internal sealed class TestCaseDiscoverySink : IUnitTestElementSink { /// - /// Gets the tests. + /// Gets the discovered tests. /// - public ICollection Tests { get; } = []; + public ICollection TestElements { get; } = []; /// - /// Collects the discovered test, materializing it as a VSTest . + /// Collects the discovered test element. /// /// The discovered test element. public void SendTestElement(UnitTestElement testElement) - => Tests.Add(testElement.ToTestCase()); + => TestElements.Add(testElement); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 71451b676a..7d4ba27de1 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -23,13 +21,13 @@ internal partial class TestExecutionManager /// The run context. /// Handle to record test start/end/results. /// Indicates if deployment is done. - internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) + internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) { InitializeRandomTestOrder(frameworkHandle); var testsBySource = (from test in tests - group test by test.Source into testGroup - select new { Source = testGroup.Key, Tests = (IEnumerable)testGroup }).ToArray(); + group test by test.TestMethod.AssemblyName into testGroup + select new { Source = testGroup.Key, Tests = (IEnumerable)testGroup }).ToArray(); if (_testOrderRandom is { } random) { @@ -51,7 +49,7 @@ group test by test.Source into testGroup /// Handle to record test start/end/results. /// The test container for the tests. /// Indicates if deployment is done. - private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, string source, bool isDeploymentDone) + private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, string source, bool isDeploymentDone) { DebugEx.Assert(!StringEx.IsNullOrEmpty(source), "Source cannot be empty"); @@ -113,13 +111,13 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo int parallelWorkers = sourceSettings.Workers; ExecutionScope parallelScope = sourceSettings.Scope; - TestCase[] testsToRun = [.. tests.Where(t => MatchTestFilter(filter, t, source))]; + UnitTestElement[] testsToRun = [.. tests.Where(t => MatchTestFilter(filter, t, source))]; if (_testOrderRandom is { } sourceRandom) { Shuffle(sourceRandom, testsToRun); } - UnitTestElement[] unitTestElements = [.. testsToRun.Select(e => e.ToUnitTestElementWithUpdatedSource(source))]; + UnitTestElement[] unitTestElements = [.. testsToRun.Select(e => e.WithUpdatedSource(source))]; // Create an instance of a type defined in adapter so that adapter gets loaded in the child app domain var testRunner = (UnitTestRunner)isolationHost.CreateInstanceForType( typeof(UnitTestRunner), @@ -151,9 +149,9 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo // Parallel and not parallel sets. // Single-pass partition: enumerate testsToRun once via GroupBy (lazy GroupBy evaluated twice // — once per FirstOrDefault — caused 2 full passes over testsToRun). - IEnumerable? parallelizableTestSet = null; - IEnumerable? nonParallelizableTestSet = null; - foreach (IGrouping group in testsToRun.GroupBy(t => t.GetPropertyValue(EngineConstants.DoNotParallelizeProperty, false))) + IEnumerable? parallelizableTestSet = null; + IEnumerable? nonParallelizableTestSet = null; + foreach (IGrouping group in testsToRun.GroupBy(t => t.DoNotParallelize)) { if (group.Key) { @@ -167,7 +165,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo if (parallelizableTestSet != null) { - ConcurrentQueue>? queue = null; + ConcurrentQueue>? queue = null; // Chunk the sets into further groups based on parallel level switch (parallelScope) @@ -175,13 +173,13 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo case ExecutionScope.MethodLevel: if (_testOrderRandom is { } methodRandom) { - IEnumerable[] methodChunks = [.. parallelizableTestSet.Select(t => (IEnumerable)[t])]; + IEnumerable[] methodChunks = [.. parallelizableTestSet.Select(t => (IEnumerable)[t])]; Shuffle(methodRandom, methodChunks); - queue = new ConcurrentQueue>(methodChunks); + queue = new ConcurrentQueue>(methodChunks); } else { - queue = new ConcurrentQueue>(parallelizableTestSet.Select(t => new[] { t })); + queue = new ConcurrentQueue>(parallelizableTestSet.Select(t => new[] { t })); } break; @@ -189,18 +187,18 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunCo case ExecutionScope.ClassLevel: if (_testOrderRandom is { } classRandom) { - IEnumerable[] classChunks = + IEnumerable[] classChunks = [ .. parallelizableTestSet - .GroupBy(t => t.GetPropertyValue(EngineConstants.TestClassNameProperty) as string) - .Select(g => (IEnumerable)g.ToArray()), + .GroupBy(t => t.TestMethod.FullClassName) + .Select(g => (IEnumerable)g.ToArray()), ]; Shuffle(classRandom, classChunks); - queue = new ConcurrentQueue>(classChunks); + queue = new ConcurrentQueue>(classChunks); } else { - queue = new ConcurrentQueue>(parallelizableTestSet.GroupBy(t => t.GetPropertyValue(EngineConstants.TestClassNameProperty) as string)); + queue = new ConcurrentQueue>(parallelizableTestSet.GroupBy(t => t.TestMethod.FullClassName)); } break; @@ -220,7 +218,7 @@ .. parallelizableTestSet { _testRunCancellationToken?.ThrowIfCancellationRequested(); - if (queue.TryDequeue(out IEnumerable? testSet)) + if (queue.TryDequeue(out IEnumerable? testSet)) { await ExecuteTestsWithTestRunnerAsync(testSet, frameworkHandle, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs index 4a15c30509..aa1d11bef0 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; @@ -41,10 +40,10 @@ internal void SendTestResults( } } - private static bool MatchTestFilter(ITestElementFilter? filter, TestCase test, string source) + private static bool MatchTestFilter(ITestElementFilter? filter, UnitTestElement test, string source) { if (filter is not null - && !filter.Matches(test.ToUnitTestElementWithUpdatedSource(source))) + && !filter.Matches(test.WithUpdatedSource(source))) { // Skip test if not fitting filter criteria. return false; @@ -54,15 +53,18 @@ private static bool MatchTestFilter(ITestElementFilter? filter, TestCase test, s } private async Task ExecuteTestsWithTestRunnerAsync( - IEnumerable tests, + IEnumerable tests, ITestExecutionRecorder testExecutionRecorder, string source, IDictionary sourceLevelParameters, UnitTestRunner testRunner, bool usesAppDomains) { - IEnumerable orderedTests = MSTestSettings.CurrentSettings.OrderTestsByNameInClass && !MSTestSettings.CurrentSettings.RandomizeTestOrder - ? tests.OrderBy(t => t.GetManagedType()).ThenBy(t => t.GetManagedMethod()) + // Ordering keys mirror the historical VSTest ManagedType/ManagedMethod test-case properties, which are + // only populated when the test method carries managed method metadata (see UnitTestElement.ToTestCase). + IEnumerable orderedTests = MSTestSettings.CurrentSettings.OrderTestsByNameInClass && !MSTestSettings.CurrentSettings.RandomizeTestOrder + ? tests.OrderBy(t => t.TestMethod.HasManagedMethodAndTypeProperties ? t.TestMethod.ManagedTypeName : null) + .ThenBy(t => t.TestMethod.HasManagedMethodAndTypeProperties ? t.TestMethod.ManagedMethodName : null) : tests; // If testRunner is in a different AppDomain, we cannot pass the testExecutionRecorder directly. @@ -75,7 +77,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( // test set. This is the boundary at which VSTest result construction is applied. ITestResultRecorder testResultRecorder = testExecutionRecorder.ToTestResultRecorder(_environment.MachineName, MSTestSettings.CurrentSettings); - foreach (TestCase currentTest in orderedTests) + foreach (UnitTestElement currentTest in orderedTests) { _testRunCancellationToken?.ThrowIfCancellationRequested(); if (PlatformServiceProvider.Instance.IsGracefulStopRequested) @@ -83,9 +85,15 @@ private async Task ExecuteTestsWithTestRunnerAsync( break; } - UnitTestElement unitTestElement = currentTest.ToUnitTestElementWithUpdatedSource(source); + UnitTestElement unitTestElement = currentTest.WithUpdatedSource(source); - testResultRecorder.RecordStart(currentTest); + // Obtain the host's test case for this test as an OPAQUE handle: the engine passes it verbatim to + // the (still VSTest-based) result recorder and reads nothing VSTest-specific off it. This preserves + // byte-for-byte reporting fidelity — including any host-injected (TCM / data-collector) properties — + // for the recorded results. Neutralizing the recorder is deferred to a later boundary phase. + TestCase currentTestCase = currentTest.GetOrCreateHostTestCase(); + + testResultRecorder.RecordStart(currentTestCase); DateTimeOffset startTime = DateTimeOffset.Now; @@ -95,7 +103,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( } // Run single test passing test context properties to it. - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(currentTest); + IReadOnlyDictionary? tcmProperties = currentTest.ExecutionContextProperties; Dictionary testContextProperties = GetTestContextProperties(tcmProperties, sourceLevelParameters, unitTestElement); TestTools.UnitTesting.TestResult[] unitTestResult; @@ -123,7 +131,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( DateTimeOffset endTime = DateTimeOffset.Now; - SendTestResults(currentTest, unitTestResult, startTime, endTime, testResultRecorder); + SendTestResults(currentTestCase, unitTestResult, startTime, endTime, testResultRecorder); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs index 7db20bc62a..541881f6fb 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs @@ -20,7 +20,7 @@ internal partial class TestExecutionManager /// The unit test element to get properties from. /// Test context properties. private static Dictionary GetTestContextProperties( - IDictionary? tcmProperties, + IReadOnlyDictionary? tcmProperties, IDictionary sourceLevelParameters, UnitTestElement unitTestElement) { @@ -38,9 +38,9 @@ internal partial class TestExecutionManager // Add tcm properties. if (tcmProperties is not null) { - foreach (KeyValuePair kvp in tcmProperties) + foreach (KeyValuePair kvp in tcmProperties) { - testContextProperties[kvp.Key.Id] = kvp.Value; + testContextProperties[kvp.Key] = kvp.Value; } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index 8db6351aea..69a6651586 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +#if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.ObjectModel; +#endif using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -85,7 +88,7 @@ private static Task DefaultFactoryAsync(Func taskGetter) /// Context to use when executing the tests. /// Handle to the framework to record results and to do framework operations. /// Test run cancellation token. - internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, TestRunCancellationToken runCancellationToken) + internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, TestRunCancellationToken runCancellationToken) { DebugEx.Assert(tests != null, "tests"); DebugEx.Assert(runContext != null, "runContext"); @@ -96,7 +99,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runC PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, runContext, frameworkHandle); + bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(ToHostTestCasesForDeployment(tests), runContext, frameworkHandle); #else const bool isDeploymentDone = false; #endif @@ -122,7 +125,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC var discoverySink = new TestCaseDiscoverySink(); - var tests = new List(); + var tests = new List(); IAdapterMessageLogger logger = frameworkHandle.ToAdapterMessageLogger(); @@ -133,14 +136,14 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC // discover the tests GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, runContext, isMTP); - tests.AddRange(discoverySink.Tests); + tests.AddRange(discoverySink.TestElements); // Clear discoverSinksTests so that it just stores test for one source at one point of time - discoverySink.Tests.Clear(); + discoverySink.TestElements.Clear(); } #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, runContext, frameworkHandle); + bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(ToHostTestCasesForDeployment(tests), runContext, frameworkHandle); #else const bool isDeploymentDone = false; #endif @@ -159,5 +162,17 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC #endif } +#if !WINDOWS_UWP && !WIN_UI + /// + /// Materializes the VSTest test cases required by the (still VSTest-based) deployment service. Reuses each + /// element's host test case when present (tests handed to the adapter to run) and otherwise materializes + /// and caches one (tests discovered internally), so deployment and result recording share a single test + /// case per test. This is the single remaining place where execution touches the VSTest test case type, + /// kept until the deployment service itself is made platform-agnostic in a later phase. + /// + private static TestCase[] ToHostTestCasesForDeployment(IEnumerable tests) + => [.. tests.Select(static e => e.GetOrCreateHostTestCase())]; +#endif + internal virtual UnitTestDiscoverer GetUnitTestDiscoverer(ITestSourceHandler testSourceHandler) => new(testSourceHandler); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs index d944cd33ff..1a24a89772 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs @@ -17,7 +17,9 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Int /// TestOutcome and attachment types). The concrete recorder is provided at the platform boundary by a /// wrapper over the host's result recorder (currently TestResultRecorderExtensions, which wraps the /// VSTest ITestExecutionRecorder), and is expected to move fully out of the platform services layer in -/// a later phase. +/// a later phase. The execution engine passes the test's TestCase as an opaque handle to this recorder +/// (it reads nothing VSTest-specific off it); reporting fidelity — including host-injected properties — is the +/// recorder's responsibility. /// internal interface ITestResultRecorder { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs index f108598c65..a95cc80dcd 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestMethod.cs @@ -135,6 +135,11 @@ private static string GetManagedTypeName(string fullClassName) internal TestMethod Clone() => (TestMethod)MemberwiseClone(); + // NOTE: This method has a latent bug (it assigns AssemblyName/MethodInfo on `this` instead of on the + // returned clone), tracked by https://github.com/microsoft/testfx/issues/9573. It is intentionally left + // untouched here so the existing ToTestCase / test-case filter bridge callers keep their exact current + // behavior; the execution engine uses the correct CloneWithSource below instead. The two are unified once + // #9573 is addressed. internal TestMethod CloneWithUpdatedSource(string source) { var clone = (TestMethod)MemberwiseClone(); @@ -142,4 +147,15 @@ internal TestMethod CloneWithUpdatedSource(string source) MethodInfo = null; return clone; } + + // Correct counterpart of CloneWithUpdatedSource: assigns the updated source and clears the cached + // MethodInfo on the returned clone (leaving `this` untouched). Used by the execution engine's source + // resolution. See https://github.com/microsoft/testfx/issues/9573. + internal TestMethod CloneWithSource(string source) + { + var clone = (TestMethod)MemberwiseClone(); + clone.AssemblyName = source; + clone.MethodInfo = null; + return clone; + } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs index fb20da9858..c61f2e04d6 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs @@ -81,6 +81,51 @@ public UnitTestElement(TestMethod testMethod) /// internal string[]? WorkItemIds { get; set; } + /// + /// Gets or sets host-provided execution context properties (for example the values historically read + /// from a test-case-management host: run id, plan id, build configuration, test point id, ...) that are + /// surfaced to the running test through TestContext. Keyed by the host property identifier so the + /// platform services layer does not depend on a specific test platform's property object model. This is + /// only populated when a host supplies such values (for internally discovered tests it stays null). + /// +#if NETFRAMEWORK + // Consumed on the source-processing side before execution; the isolation host copy never reads it. + [field: NonSerialized] +#endif + internal IReadOnlyDictionary? ExecutionContextProperties { get; set; } + + /// + /// Gets or sets the host's test case for this test, used to report its lifecycle and results back to the + /// host with full fidelity (preserving any host-injected data — such as test-case-management or + /// data-collector properties — that the neutral model does not otherwise carry) and to describe it to the + /// (still VSTest-based) deployment service. For tests handed to the adapter by a host it is that original + /// test case; for tests discovered internally by the platform services it is until + /// materialized on demand by . The execution engine treats it as an + /// opaque handle — it reads nothing VSTest-specific off it and only threads it into the result recorder and + /// the deployment boundary. + /// +#if NETFRAMEWORK + // Result recording and deployment happen on the source-processing side; the isolation host copy never reads it. + [field: NonSerialized] +#endif + internal object? HostRecordingHandle { get; set; } + + /// + /// Returns the host test case for this test, reusing the host-provided one when present and otherwise + /// materializing a single VSTest test case on demand and caching it (so deployment, test-start and every + /// reported result share one instance, matching the historical "one test case per discovered test"). + /// + internal TestCase GetOrCreateHostTestCase() + { + if (HostRecordingHandle is not TestCase testCase) + { + testCase = ToTestCase(); + HostRecordingHandle = testCase; + } + + return testCase; + } + internal UnitTestElement Clone() { var clone = (UnitTestElement)MemberwiseClone(); @@ -88,6 +133,11 @@ internal UnitTestElement Clone() return clone; } + // Legacy source-updating clone used only by the ToTestCase / test-case filter bridge + // (TestCaseExtensions.ToUnitTestElementWithUpdatedSource). It delegates to the buggy + // TestMethod.CloneWithUpdatedSource and is retained to preserve that path's exact current behavior; the + // execution engine uses WithUpdatedSource / CloneWithSource instead. Tracked by + // https://github.com/microsoft/testfx/issues/9573. internal UnitTestElement CloneWithUpdatedSource(string source) { var clone = (UnitTestElement)MemberwiseClone(); @@ -95,6 +145,27 @@ internal UnitTestElement CloneWithUpdatedSource(string source) return clone; } + // Correct source-updating clone: the returned clone (only) targets the new source. Used by the execution + // engine via WithUpdatedSource. See https://github.com/microsoft/testfx/issues/9573. + internal UnitTestElement CloneWithSource(string source) + { + var clone = (UnitTestElement)MemberwiseClone(); + clone.TestMethod = TestMethod.CloneWithSource(source); + return clone; + } + + /// + /// Returns this element when it already targets , otherwise a clone whose test + /// method points at . This mirrors the source resolution the adapter previously + /// performed while converting a host test case, so a deployed source (relocated to the deployment + /// directory) is honored without reloading the assembly from its original location (see + /// https://github.com/microsoft/testfx/issues/6713). + /// + /// The (possibly deployment-relocated) source of the test. + /// An element whose is . + internal UnitTestElement WithUpdatedSource(string source) + => TestMethod.AssemblyName == source ? this : CloneWithSource(source); + /// /// Convert the UnitTestElement instance to an Object Model testCase instance. /// diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index 55d5b3adc5..19a72cac8f 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -7,6 +7,8 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -37,7 +39,7 @@ internal static async Task> RunTestsAsync(IEnumerable var testExecutionManager = new TestExecutionManager(); var frameworkHandle = new InternalFrameworkHandle(); - await testExecutionManager.ExecuteTestsAsync(testCases, null, frameworkHandle, false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), null, frameworkHandle, false); return frameworkHandle.GetFlattenedTestResults(); } @@ -49,10 +51,22 @@ internal static async Task> RunTestsAsync(IEnumerable string runSettingsXml = GetRunSettingsXml(string.Empty); var runContext = new InternalRunContext(runSettingsXml, testCaseFilter); - await testExecutionManager.ExecuteTestsAsync(testCases, runContext, frameworkHandle, false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), runContext, frameworkHandle, false); return frameworkHandle.GetFlattenedTestResults(); } + // Mirrors the adapter's execution boundary (see MSTestExecutor): each host test case becomes a neutral + // UnitTestElement carrying its execution-context (TCM) properties and the originating test case as an + // opaque recording handle, so results are recorded against the exact same TestCase instances. + private static IEnumerable ToUnitTestElements(IEnumerable testCases) + => testCases.Select(static testCase => + { + UnitTestElement element = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); + element.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + element.HostRecordingHandle = testCase; + return element; + }); + #region Helper classes private class InternalLogger : IMessageLogger { diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs index 64e70e9b80..69fe685e75 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -34,7 +34,7 @@ public class TcmTestPropertiesProviderTests : TestContainer public void GetTcmPropertiesShouldReturnEmptyDictionaryIfTestCaseIsNull() { - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(null); + IReadOnlyDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(null); tcmProperties.Should().BeNull(); } @@ -61,7 +61,7 @@ public void GetTcmPropertiesShouldReturnEmptyDictionaryIfTestCaseIdIsZero() ]; SetTestCaseProperties(testCase, propertiesValue); - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + IReadOnlyDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); tcmProperties.Should().BeNull(); } @@ -88,7 +88,7 @@ public void GetTcmPropertiesShouldGetAllPropertiesFromTestCase() ]; SetTestCaseProperties(testCase, propertiesValue); - IDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + IReadOnlyDictionary? tcmProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); VerifyTcmProperties(tcmProperties, testCase); } @@ -116,7 +116,7 @@ public void GetTcmPropertiesShouldCopyMultiplePropertiesCorrectlyFromTestCase() 345 ]; SetTestCaseProperties(testCase1, propertiesValue1); - IDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); + IReadOnlyDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); VerifyTcmProperties(tcmProperties1, testCase1); // Verify 2nd call. @@ -140,7 +140,7 @@ public void GetTcmPropertiesShouldCopyMultiplePropertiesCorrectlyFromTestCase() 346 ]; SetTestCaseProperties(testCase2, propertiesValue2); - IDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); + IReadOnlyDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); VerifyTcmProperties(tcmProperties2, testCase2); } @@ -167,7 +167,7 @@ public void GetTcmPropertiesShouldHandleDuplicateTestsProperlyFromTestCase() 345 ]; SetTestCaseProperties(testCase1, propertiesValue1); - IDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); + IReadOnlyDictionary? tcmProperties1 = TcmTestPropertiesProvider.GetTcmProperties(testCase1); VerifyTcmProperties(tcmProperties1, testCase1); // Verify 2nd call. @@ -191,7 +191,7 @@ public void GetTcmPropertiesShouldHandleDuplicateTestsProperlyFromTestCase() 346 ]; SetTestCaseProperties(testCase2, propertiesValue2); - IDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); + IReadOnlyDictionary? tcmProperties2 = TcmTestPropertiesProvider.GetTcmProperties(testCase2); VerifyTcmProperties(tcmProperties2, testCase2); // Verify 3rd call. @@ -215,7 +215,7 @@ public void GetTcmPropertiesShouldHandleDuplicateTestsProperlyFromTestCase() 347 ]; SetTestCaseProperties(testCase3, propertiesValue3); - IDictionary? tcmProperties3 = TcmTestPropertiesProvider.GetTcmProperties(testCase3); + IReadOnlyDictionary? tcmProperties3 = TcmTestPropertiesProvider.GetTcmProperties(testCase3); VerifyTcmProperties(tcmProperties3, testCase3); } @@ -232,12 +232,12 @@ private void SetTestCaseProperties(TestCase testCase, object[] propertiesValue) } } - private void VerifyTcmProperties(IDictionary? tcmProperties, TestCase testCase) + private void VerifyTcmProperties(IReadOnlyDictionary? tcmProperties, TestCase testCase) { tcmProperties.Should().NotBeNull(); foreach (TestProperty property in _tcmKnownProperties) { - testCase.GetPropertyValue(property)!.Equals(tcmProperties[property]).Should().BeTrue(); + testCase.GetPropertyValue(property)!.Equals(tcmProperties[property.Id]).Should().BeTrue(); } } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs index a675af493d..07f916bd40 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestCaseDiscoverySinkTests.cs @@ -18,22 +18,22 @@ public class TestCaseDiscoverySinkTests : TestContainer public void TestCaseDiscoverySinkConstructorShouldInitializeTests() { - _testCaseDiscoverySink.Tests.Should().NotBeNull(); - _testCaseDiscoverySink.Tests.Count.Should().Be(0); + _testCaseDiscoverySink.TestElements.Should().NotBeNull(); + _testCaseDiscoverySink.TestElements.Count.Should().Be(0); } - public void SendTestElementShouldAddTheMaterializedTestCaseToTests() + public void SendTestElementShouldAddTheTestElement() { var testElement = new UnitTestElement(new TestMethod("M", "C", "A", displayName: null)); _testCaseDiscoverySink.SendTestElement(testElement); - _testCaseDiscoverySink.Tests.Should().NotBeNull(); - _testCaseDiscoverySink.Tests.Count.Should().Be(1); - _testCaseDiscoverySink.Tests.ToArray()[0].FullyQualifiedName.Should().Be("C.M"); + _testCaseDiscoverySink.TestElements.Should().NotBeNull(); + _testCaseDiscoverySink.TestElements.Count.Should().Be(1); + _testCaseDiscoverySink.TestElements.ToArray()[0].Should().BeSameAs(testElement); } - public void SendTestElementShouldAddEachTestCaseInOrder() + public void SendTestElementShouldAddEachTestElementInOrder() { var testElement1 = new UnitTestElement(new TestMethod("M1", "C", "A", displayName: null)); var testElement2 = new UnitTestElement(new TestMethod("M2", "C", "A", displayName: null)); @@ -41,8 +41,8 @@ public void SendTestElementShouldAddEachTestCaseInOrder() _testCaseDiscoverySink.SendTestElement(testElement1); _testCaseDiscoverySink.SendTestElement(testElement2); - _testCaseDiscoverySink.Tests.Count.Should().Be(2); - _testCaseDiscoverySink.Tests.ToArray()[0].FullyQualifiedName.Should().Be("C.M1"); - _testCaseDiscoverySink.Tests.ToArray()[1].FullyQualifiedName.Should().Be("C.M2"); + _testCaseDiscoverySink.TestElements.Count.Should().Be(2); + _testCaseDiscoverySink.TestElements.ToArray()[0].Should().BeSameAs(testElement1); + _testCaseDiscoverySink.TestElements.ToArray()[1].Should().BeSameAs(testElement2); } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 2a18bb5757..274857661a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; @@ -92,7 +93,7 @@ public async Task RunTestsForTestWithFilterErrorShouldSendZeroResults() // Causing the FilterExpressionError _runContext = new TestableRunContextTestExecutionTests(() => throw new TestPlatformFormatException()); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); // No Results _frameworkHandle.TestCaseStartList.Count.Should().Be(0); @@ -108,7 +109,7 @@ public async Task RunTestsForTestWithFilterShouldSendResultsForFilteredTests() _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(p => p.DisplayName == "PassingTest")); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); // FailingTest should be skipped because it does not match the filter criteria. List expectedTestCaseStartList = ["PassingTest"]; @@ -136,7 +137,7 @@ public async Task RunTestsForIgnoredTestShouldSendResultsMarkingIgnoredTestsAsSk TestCase testCase = GetTestCase(typeof(DummyTestClass), "IgnoredTest"); TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); _frameworkHandle.TestCaseStartList[0].Should().Be("IgnoredTest"); _frameworkHandle.TestCaseEndList[0].Should().Be("IgnoredTest:Skipped"); @@ -149,7 +150,7 @@ public async Task RunTestsForASingleTestShouldSendSingleResult() TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); List expectedTestCaseStartList = ["PassingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed"]; @@ -166,7 +167,7 @@ public async Task RunTestsForMultipleTestShouldSendMultipleResults() TestCase failingTestCase = GetTestCase(typeof(DummyTestClass), "FailingTest"); TestCase[] tests = [testCase, failingTestCase]; - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); List expectedTestCaseStartList = ["PassingTest", "FailingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed", "FailingTest:Failed"]; @@ -186,7 +187,7 @@ public async Task RunTestsForCancellationTokenCanceledSetToTrueShouldSendZeroRes // Cancel the test run _cancellationToken.Cancel(); - Func func = () => _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, _cancellationToken); + Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); await func.Should().ThrowAsync(); // No Results @@ -204,10 +205,10 @@ public async Task RunTestsForTestShouldDeployBeforeExecution() // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.MockTestDeployment.Setup( - td => td.Deploy(tests, _runContext, _frameworkHandle)).Callback(() => SetCaller("Deploy")); + td => td.Deploy(It.Is>(t => t.SequenceEqual(tests)), _runContext, _frameworkHandle)).Callback(() => SetCaller("Deploy")); await _testExecutionManager.RunTestsAsync( - tests, + ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); @@ -230,7 +231,7 @@ public async Task RunTestsForTestShouldCleanupAfterExecution() td => td.Cleanup()).Callback(() => SetCaller("Cleanup")); #endif - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); _callers[0].Should().Be("LoadAssembly", "Cleanup should be called after execution."); @@ -247,7 +248,7 @@ public async Task RunTestsForTestShouldNotCleanupOnTestFailure() TestCase[] tests = [testCase, failingTestCase]; TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); testablePlatformService.MockTestDeployment.Verify(td => td.Cleanup(), Times.Never); } @@ -262,11 +263,11 @@ public async Task RunTestsForTestShouldLoadSourceFromDeploymentDirectoryIfDeploy // Setup mocks. testablePlatformService.MockTestDeployment.Setup( - td => td.Deploy(tests, _runContext, _frameworkHandle)).Returns(true); + td => td.Deploy(It.Is>(t => t.SequenceEqual(tests)), _runContext, _frameworkHandle)).Returns(true); testablePlatformService.MockTestDeployment.Setup(td => td.GetDeploymentDirectory()) .Returns(@"C:\temp"); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); testablePlatformService.MockFileOperations.Verify( fo => fo.LoadAssembly(It.Is(s => s.StartsWith("C:\\temp"))), @@ -292,7 +293,7 @@ public async Task RunTestsForTestShouldPassInTestRunParametersInformationAsPrope """); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -314,7 +315,7 @@ public async Task RunTestsForTestShouldPassInTcmPropertiesAsPropertiesToTheTest( """); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); VerifyTcmProperties(DummyTestClass.TestContextProperties, testCase); } @@ -327,7 +328,7 @@ public async Task RunTestsForTestShouldPassInDeploymentInformationAsPropertiesTo // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); testablePlatformService.MockSettingsProvider.Verify(sp => sp.GetProperties(It.IsAny()), Times.Once); } @@ -351,7 +352,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger First Run - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); // Update runsettings to have different values for similar keys _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -368,7 +369,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger another Run - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); "http://updatedLocalHost".Equals(DummyTestClass.TestContextProperties!["webAppUrl"]).Should().BeTrue(); } @@ -462,7 +463,7 @@ public async Task RunTestsForTestShouldRunTestsInParallelWhenEnabledInRunsetting try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -499,7 +500,7 @@ public async Task RunTestsForTestShouldRunTestsByMethodLevelWhenSpecified() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -536,7 +537,7 @@ public async Task RunTestsForTestShouldRunTestsWithSpecifiedNumberOfWorkers() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -599,7 +600,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -658,7 +659,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -697,7 +698,7 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); DummyTestClassWithDoNotParallelizeMethods.ParallelizableTestsThreadIds.Count.Should().BeOneOf(1, 2); @@ -763,7 +764,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -803,7 +804,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.Count.Should().Be(1); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.ToArray()[0].Should().Be(Thread.CurrentThread.GetApartmentState()); @@ -846,11 +847,11 @@ static TestCase[] BuildTests() => var firstHandle = new TestableFrameworkHandle(); var firstManager = new TestExecutionManager(EnvironmentWrapper.Instance, task => task()); - await firstManager.RunTestsAsync(BuildTests(), _runContext, firstHandle, new TestRunCancellationToken()); + await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, firstHandle, new TestRunCancellationToken()); var secondHandle = new TestableFrameworkHandle(); var secondManager = new TestExecutionManager(EnvironmentWrapper.Instance, task => task()); - await secondManager.RunTestsAsync(BuildTests(), _runContext, secondHandle, new TestRunCancellationToken()); + await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, secondHandle, new TestRunCancellationToken()); // Same seed must produce the same order across separate runs. firstHandle.TestCaseStartList.Should().Equal(secondHandle.TestCaseStartList); @@ -887,7 +888,7 @@ public async Task RunTestsWithoutRandomizeTestOrderShouldPreserveInputOrder() MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(tests, _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); _frameworkHandle.TestCaseStartList.Should().Equal("TestA", "TestB", "TestC"); } @@ -904,6 +905,21 @@ private static TestCase GetTestCase(Type typeOfClass, string testName) return element.ToTestCase(); } + // Mirrors the conversion the adapter performs at the execution boundary (see MSTestExecutor): each host + // test case becomes a neutral UnitTestElement carrying the host execution-context (TCM) properties and the + // originating test case as an opaque recording handle, so recorded results are reported against the exact + // same TestCase instances the tests build (keeping the framework-handle Verify assertions meaningful). + private static UnitTestElement[] ToUnitTestElements(params TestCase[] tests) + => [.. tests.Select(ToUnitTestElement)]; + + private static UnitTestElement ToUnitTestElement(TestCase testCase) + { + UnitTestElement element = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); + element.ExecutionContextProperties = TcmTestPropertiesProvider.GetTcmProperties(testCase); + element.HostRecordingHandle = testCase; + return element; + } + private TestablePlatformServiceProvider SetupTestablePlatformService() { var testablePlatformService = new TestablePlatformServiceProvider(); @@ -1248,9 +1264,9 @@ internal sealed class TestableTestCaseFilterExpression : ITestCaseFilterExpressi internal class TestableTestExecutionManager : TestExecutionManager { - internal Action, IRunContext?, IFrameworkHandle, bool> ExecuteTestsWrapper { get; set; } = null!; + internal Action, IRunContext?, IFrameworkHandle, bool> ExecuteTestsWrapper { get; set; } = null!; - internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) + internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) { ExecuteTestsWrapper?.Invoke(tests, runContext, frameworkHandle, isDeploymentDone); return Task.CompletedTask; diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs index f8cd836e35..634b2a4f7a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs @@ -33,6 +33,44 @@ public void UnitTestElementConstructorShouldThrowIfTestMethodIsNull() #endregion + #region Source resolution / host test case tests + + public void WithUpdatedSourceShouldReturnSameInstanceWhenSourceUnchanged() + => _unitTestElement.WithUpdatedSource("A").Should().BeSameAs(_unitTestElement); + + public void WithUpdatedSourceShouldReturnCloneWithNewSourceAndLeaveOriginalUnchanged() + { + UnitTestElement clone = _unitTestElement.WithUpdatedSource("B"); + + clone.Should().NotBeSameAs(_unitTestElement); + clone.TestMethod.AssemblyName.Should().Be("B"); + // The original element (and its test method) must not be mutated by the clone. + _unitTestElement.TestMethod.AssemblyName.Should().Be("A"); + clone.TestMethod.Should().NotBeSameAs(_testMethod); + } + + public void GetOrCreateHostTestCaseShouldReturnHostHandleWhenPresent() + { + var hostTestCase = new TestCase("C.M", EngineConstants.ExecutorUri, "A"); + _unitTestElement.HostRecordingHandle = hostTestCase; + + _unitTestElement.GetOrCreateHostTestCase().Should().BeSameAs(hostTestCase); + } + + public void GetOrCreateHostTestCaseShouldMaterializeAndCacheWhenNoHandle() + { + _unitTestElement.HostRecordingHandle.Should().BeNull(); + + TestCase materialized = _unitTestElement.GetOrCreateHostTestCase(); + + materialized.Should().NotBeNull(); + // Subsequent calls (deployment, test-start, each reported result) reuse the same instance. + _unitTestElement.GetOrCreateHostTestCase().Should().BeSameAs(materialized); + _unitTestElement.HostRecordingHandle.Should().BeSameAs(materialized); + } + + #endregion + #region ToTestCase tests public void ToTestCaseShouldSetFullyQualifiedName() From 98ec16e356353f37b35e79fdaed61177c4e9c309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 15:16:47 +0200 Subject: [PATCH 08/24] Neutralize deployment input in PlatformServices (Phase 6a of platform-agnostic effort) (#9576) Co-authored-by: Amaury Leveque Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Deployment/DeploymentContext.cs | 34 ++++++ .../Execution/TestExecutionManager.cs | 25 ++-- .../Interfaces/ITestDeployment.cs | 14 +-- .../Services/TestDeployment.cs | 32 ++--- .../Utilities/DeploymentItemUtility.cs | 26 ++-- .../Utilities/DeploymentUtilityBase.cs | 38 +++--- .../Execution/TestExecutionManagerTests.cs | 5 +- .../Services/DesktopTestDeploymentTests.cs | 32 ++--- .../Services/TestDeploymentTests.cs | 71 +++++------ .../Utilities/DeploymentItemUtilityTests.cs | 19 ++- .../Utilities/DeploymentUtilityTests.cs | 113 +++++++++--------- 11 files changed, 201 insertions(+), 208 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs new file mode 100644 index 0000000000..b8a40efd53 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs @@ -0,0 +1,34 @@ +// 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 && !WIN_UI + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; + +/// +/// Platform-agnostic inputs the deployment pipeline needs from the running test host: the test-run/results +/// directory and the run settings XML. This lets the platform services deployment layer run without taking a +/// dependency on a specific test platform's run-context object model (for example the VSTest +/// IRunContext / IRunSettings types). It is populated at the adapter boundary. +/// +internal sealed class DeploymentContext +{ + public DeploymentContext(string? testRunDirectory, string? runSettingsXml) + { + TestRunDirectory = testRunDirectory; + RunSettingsXml = runSettingsXml; + } + + /// + /// Gets the host-provided test-run directory (the root under which results/deployment directories are + /// created), or to fall back to a temp directory. + /// + public string? TestRunDirectory { get; } + + /// + /// Gets the run settings XML supplied by the host, or when none was provided. + /// + public string? RunSettingsXml { get; } +} + +#endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index 69a6651586..669a01073c 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -3,11 +3,11 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; #if !WINDOWS_UWP && !WIN_UI -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; #endif +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -99,7 +99,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(ToHostTestCasesForDeployment(tests), runContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, runContext, frameworkHandle); #else const bool isDeploymentDone = false; #endif @@ -143,7 +143,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC } #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = PlatformServiceProvider.Instance.TestDeployment.Deploy(ToHostTestCasesForDeployment(tests), runContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, runContext, frameworkHandle); #else const bool isDeploymentDone = false; #endif @@ -164,14 +164,15 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC #if !WINDOWS_UWP && !WIN_UI /// - /// Materializes the VSTest test cases required by the (still VSTest-based) deployment service. Reuses each - /// element's host test case when present (tests handed to the adapter to run) and otherwise materializes - /// and caches one (tests discovered internally), so deployment and result recording share a single test - /// case per test. This is the single remaining place where execution touches the VSTest test case type, - /// kept until the deployment service itself is made platform-agnostic in a later phase. + /// Runs deployment for the given tests via the platform-agnostic deployment service, translating the host + /// run context into the neutral at this call site. This is the single place + /// execution reads the VSTest run context for deployment (removed once the run context itself is neutralized). /// - private static TestCase[] ToHostTestCasesForDeployment(IEnumerable tests) - => [.. tests.Select(static e => e.GetOrCreateHostTestCase())]; + private static bool Deploy(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle) + { + var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); + return PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, deploymentContext, frameworkHandle.ToAdapterMessageLogger()); + } #endif internal virtual UnitTestDiscoverer GetUnitTestDiscoverer(ITestSourceHandler testSourceHandler) => new(testSourceHandler); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs index 1aabea8f1c..a3215764fb 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestDeployment.cs @@ -3,8 +3,8 @@ #if !WINDOWS_UWP && !WIN_UI -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; @@ -14,13 +14,13 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Int internal interface ITestDeployment { /// - /// Deploy deployment items for the specified test cases. + /// Deploy deployment items for the specified tests. /// - /// The test cases. - /// The run context. - /// The framework handle. + /// The tests. + /// The host deployment inputs (test-run directory, run settings XML). + /// The logger used to surface deployment warnings. /// True if deployment is done. - bool Deploy(IEnumerable testCases, IRunContext? runContext, IFrameworkHandle frameworkHandle); + bool Deploy(IEnumerable testElements, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger); /// /// Gets the set of deployment items on a method and its corresponding class. diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs index 11a4cd49cb..5c17c97d43 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs @@ -4,12 +4,14 @@ #if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; #if NETFRAMEWORK +// SuspendCodeCoverage (used below to pause dynamic code coverage while deployment copies files) is a VSTest +// object-model type. Its neutralization is deferred to a later platform-services decoupling step; the rest of +// the deployment input is already platform-agnostic. using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; #endif using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -104,13 +106,13 @@ public void Cleanup() /// /// Deploy files related to the list of tests specified. /// - /// The tests. - /// The run context. - /// The framework handle. + /// The tests. + /// The host deployment inputs. + /// The logger used to surface deployment warnings. /// Return true if deployment is done. - public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFrameworkHandle frameworkHandle) + public bool Deploy(IEnumerable testElements, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger) { - DebugEx.Assert(testCases != null, "tests"); + DebugEx.Assert(testElements != null, "tests"); // Reset runDirectories before doing deployment, so that older values of runDirectories is not picked // even if test host is kept alive. @@ -118,7 +120,7 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr _adapterSettings = MSTestSettingsProvider.Settings; bool canDeploy = CanDeploy(); - bool hasDeploymentItems = testCases.Any(DeploymentItemUtility.HasDeploymentItems); + bool hasDeploymentItems = testElements.Any(DeploymentItemUtility.HasDeploymentItems); // deployment directories should not be created in this case,simply return if (!canDeploy && hasDeploymentItems) @@ -127,8 +129,8 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr } #if NETFRAMEWORK - string? firstTestSource = testCases.FirstOrDefault()?.Source; - RunDirectories = _deploymentUtility.CreateDeploymentDirectories(runContext, firstTestSource); + string? firstTestSource = testElements.FirstOrDefault()?.TestMethod.AssemblyName; + RunDirectories = _deploymentUtility.CreateDeploymentDirectories(deploymentContext, firstTestSource); // Deployment directories are created but deployment will not happen. // This is added just to keep consistency with MSTest v1 behavior. @@ -143,8 +145,8 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr return false; } - string? firstTestSource = testCases.FirstOrDefault()?.Source; - RunDirectories = _deploymentUtility.CreateDeploymentDirectories(runContext, firstTestSource); + string? firstTestSource = testElements.FirstOrDefault()?.TestMethod.AssemblyName; + RunDirectories = _deploymentUtility.CreateDeploymentDirectories(deploymentContext, firstTestSource); #endif // Object model currently does not have support for SuspendCodeCoverage. We can remove this once support is added @@ -153,15 +155,15 @@ public bool Deploy(IEnumerable testCases, IRunContext? runContext, IFr #endif { // Group the tests by source - var testsBySource = from test in testCases - group test by test.Source into testGroup + var testsBySource = from test in testElements + group test by test.TestMethod.AssemblyName into testGroup select new { Source = testGroup.Key, Tests = testGroup }; TestRunDirectories runDirectories = RunDirectories; foreach (var group in testsBySource) { // do the deployment - _deploymentUtility.Deploy(@group.Tests, @group.Source, runContext, frameworkHandle, RunDirectories); + _deploymentUtility.Deploy(@group.Tests, @group.Source, deploymentContext, messageLogger, RunDirectories); } // Update the runDirectories diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs index 50546a66e2..c371e13baa 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentItemUtility.cs @@ -3,9 +3,9 @@ #if !WINDOWS_UWP && !WIN_UI +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; @@ -105,21 +105,17 @@ internal static bool IsValidDeploymentItem([NotNullWhen(true)] string? sourcePat /// /// Returns whether there are any deployment items defined on the test. /// - /// The test Case. + /// The test. /// True if has deployment items. - internal static bool HasDeploymentItems(TestCase testCase) - { - KeyValuePair[]? deploymentItems = GetDeploymentItems(testCase); - - return deploymentItems is { Length: > 0 }; - } + internal static bool HasDeploymentItems(UnitTestElement testElement) + => testElement.DeploymentItems is { Length: > 0 }; - internal static IList GetDeploymentItems(IEnumerable tests) + internal static IList GetDeploymentItems(IEnumerable tests) { List allDeploymentItems = []; - foreach (TestCase test in tests) + foreach (UnitTestElement test in tests) { - KeyValuePair[]? items = GetDeploymentItems(test); + KeyValuePair[]? items = test.DeploymentItems; if (items == null || items.Length == 0) { continue; @@ -220,14 +216,6 @@ private static List GetDeploymentItems(IEnumerable - /// Returns the deployment items defined on the test. - /// - /// The test Case. - /// The . - private static KeyValuePair[]? GetDeploymentItems(TestCase testCase) => testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty) as - KeyValuePair[]; - private static KeyValuePair[]? ToKeyValuePairs(IEnumerable deploymentItemList) { if (!deploymentItemList.Any()) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs index be6335ba69..47713d16af 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/DeploymentUtilityBase.cs @@ -4,13 +4,11 @@ #if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; - -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; @@ -43,31 +41,31 @@ public DeploymentUtilityBase(DeploymentItemUtility deploymentItemUtility, Assemb protected AssemblyUtility AssemblyUtility { get; set; } - public bool Deploy(IEnumerable tests, string source, IRunContext? runContext, ITestExecutionRecorder testExecutionRecorder, TestRunDirectories runDirectories) + public bool Deploy(IEnumerable tests, string source, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, TestRunDirectories runDirectories) { IList deploymentItems = DeploymentItemUtility.GetDeploymentItems(tests); // we just deploy source if there are no deployment items for current source but there are deployment items for other sources - return Deploy(source, runContext, testExecutionRecorder, deploymentItems, runDirectories); + return Deploy(source, deploymentContext, messageLogger, deploymentItems, runDirectories); } /// /// Create deployment directories. /// - /// The run context. + /// The host deployment inputs. /// /// The path to the test assembly of the first test case. In most cases, all /// test cases belong to the same assembly, but not guaranteed. We are using the path from /// the first test case as a "best effort" implementation. DeploymentItem isn't correctly designed and should be deprecated in future. /// /// TestRunDirectories instance. - public TestRunDirectories CreateDeploymentDirectories(IRunContext? runContext, string? firstTestSource) + public TestRunDirectories CreateDeploymentDirectories(DeploymentContext deploymentContext, string? firstTestSource) { - string resultsDirectory = GetTestResultsDirectory(runContext); + string resultsDirectory = GetTestResultsDirectory(deploymentContext); string rootDeploymentDirectory = GetRootDeploymentDirectory(resultsDirectory); #if NETFRAMEWORK - bool isAppDomainCreationDisabled = runContext?.RunSettings != null && MSTestAdapterSettings.IsAppDomainCreationDisabled(runContext.RunSettings.SettingsXml); + bool isAppDomainCreationDisabled = deploymentContext.RunSettingsXml is not null && MSTestAdapterSettings.IsAppDomainCreationDisabled(deploymentContext.RunSettingsXml); #else // AppDomains are only supported in .NET Framework. const bool isAppDomainCreationDisabled = true; @@ -94,10 +92,10 @@ public TestRunDirectories CreateDeploymentDirectories(IRunContext? runContext, s /// /// Get the parent test results directory where deployment will be done. /// - /// The run context. + /// The host deployment inputs. /// The test results directory. - public static string GetTestResultsDirectory(IRunContext? runContext) => !StringEx.IsNullOrEmpty(runContext?.TestRunDirectory) - ? runContext.TestRunDirectory + public static string GetTestResultsDirectory(DeploymentContext deploymentContext) => !StringEx.IsNullOrEmpty(deploymentContext.TestRunDirectory) + ? deploymentContext.TestRunDirectory : Path.GetFullPath(Path.Combine(Path.GetTempPath(), TestRunDirectories.DefaultDeploymentRootDirectory)); /// @@ -397,20 +395,20 @@ protected static bool IsOutputDirectoryValid(DeploymentItem deploymentItem, stri /// /// Log the parameter warnings on the parameter logger. /// - /// Execution recorder. + /// Message logger. /// Warnings. - private static void LogWarnings(ITestExecutionRecorder testExecutionRecorder, IEnumerable warnings) + private static void LogWarnings(IAdapterMessageLogger messageLogger, IEnumerable warnings) { - DebugEx.Assert(testExecutionRecorder != null, "Logger should not be null"); + DebugEx.Assert(messageLogger != null, "Logger should not be null"); // log the warnings foreach (string warning in warnings) { - testExecutionRecorder.SendMessage(TestMessageLevel.Warning, warning); + messageLogger.SendMessage(MessageLevel.Warning, warning); } } - private bool Deploy(string source, IRunContext? runContext, ITestExecutionRecorder testExecutionRecorder, IList deploymentItems, TestRunDirectories runDirectories) + private bool Deploy(string source, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, IList deploymentItems, TestRunDirectories runDirectories) { if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) { @@ -427,10 +425,10 @@ private bool Deploy(string source, IRunContext? runContext, ITestExecutionRecord PlatformServiceProvider.Instance.AdapterTraceLogger.Info("MSTestExecutor: Using deployment directory {0} for source {1}.", runDirectories.OutDirectory, source); } - IEnumerable warnings = Deploy([.. deploymentItems], source, runDirectories.OutDirectory, GetTestResultsDirectory(runContext)); + IEnumerable warnings = Deploy([.. deploymentItems], source, runDirectories.OutDirectory, GetTestResultsDirectory(deploymentContext)); // Log warnings - LogWarnings(testExecutionRecorder, warnings); + LogWarnings(messageLogger, warnings); return deploymentItems is { Count: > 0 }; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 274857661a..e228371479 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -8,6 +8,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Discovery; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.TestableImplementations; @@ -205,7 +206,7 @@ public async Task RunTestsForTestShouldDeployBeforeExecution() // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.MockTestDeployment.Setup( - td => td.Deploy(It.Is>(t => t.SequenceEqual(tests)), _runContext, _frameworkHandle)).Callback(() => SetCaller("Deploy")); + td => td.Deploy(It.IsAny>(), It.IsAny(), It.IsAny())).Callback(() => SetCaller("Deploy")); await _testExecutionManager.RunTestsAsync( ToUnitTestElements(tests), @@ -263,7 +264,7 @@ public async Task RunTestsForTestShouldLoadSourceFromDeploymentDirectoryIfDeploy // Setup mocks. testablePlatformService.MockTestDeployment.Setup( - td => td.Deploy(It.Is>(t => t.SequenceEqual(tests)), _runContext, _frameworkHandle)).Returns(true); + td => td.Deploy(It.IsAny>(), It.IsAny(), It.IsAny())).Returns(true); testablePlatformService.MockTestDeployment.Setup(td => td.GetDeploymentDirectory()) .Returns(@"C:\temp"); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs index ad2b3917b3..b154dfc5c1 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestDeploymentTests.cs @@ -4,12 +4,11 @@ #if NETFRAMEWORK using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Moq; @@ -45,15 +44,12 @@ public DesktopTestDeploymentTests() public void DeployShouldDeployFilesInASourceAndReturnTrue() { - TestCase testCase = GetTestCase(Assembly.GetExecutingAssembly().Location); + UnitTestElement testCase = GetTestCase(Assembly.GetExecutingAssembly().Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); string? warning; string sourceFile = Assembly.GetExecutingAssembly().GetName().Name + ".exe"; @@ -68,17 +64,14 @@ public void DeployShouldDeployFilesInASourceAndReturnTrue() public void DeployShouldDeployFilesInMultipleSourcesAndReturnTrue() { - TestCase testCase1 = GetTestCase(Assembly.GetExecutingAssembly().Location); + UnitTestElement testCase1 = GetTestCase(Assembly.GetExecutingAssembly().Location); string sourceFile2 = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "a.dll"); - TestCase testCase2 = GetTestCase(sourceFile2); + UnitTestElement testCase2 = GetTestCase(sourceFile2); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase1, testCase2 }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase1, testCase2 }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); string? warning; string sourceFile1 = Assembly.GetExecutingAssembly().GetName().Name + ".exe"; @@ -100,15 +93,12 @@ public void DeployShouldDeployFilesInMultipleSourcesAndReturnTrue() public void DeployShouldCreateDeploymentDirectories() { - TestCase testCase = GetTestCase(typeof(DesktopTestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(DesktopTestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // matched twice because root deployment and out directory are same in net core _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(testRunDirectories.RootDeploymentDirectory), Times.Once); @@ -118,16 +108,16 @@ public void DeployShouldCreateDeploymentDirectories() #region private methods - private TestCase GetTestCase(string source) + private static UnitTestElement GetTestCase(string source) { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), source); + var testCase = new UnitTestElement(new TestMethod("M", "C", source, displayName: null)); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; return testCase; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs index c88f094732..97f30a2bd2 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestDeploymentTests.cs @@ -4,12 +4,11 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Moq; @@ -111,15 +110,12 @@ public void CleanupShouldNotDeleteDirectoriesIfRunSettingsSpecifiesSo() MSTestSettingsProvider mstestSettingsProvider = new(); mstestSettingsProvider.Load(reader); - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); testDeployment.Cleanup(); @@ -128,15 +124,12 @@ public void CleanupShouldNotDeleteDirectoriesIfRunSettingsSpecifiesSo() public void CleanupShouldDeleteRootDeploymentDirectory() { - TestCase testCase = GetTestCase(typeof(DeploymentUtilityTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(DeploymentUtilityTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. testDeployment.Cleanup(); @@ -152,15 +145,12 @@ public void CleanupShouldDeleteRootDeploymentDirectory() public void GetDeploymentDirectoryShouldReturnDeploymentOutputDirectory() { - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. testDeployment.GetDeploymentDirectory().Should().Be(testRunDirectories.OutDirectory); @@ -172,14 +162,14 @@ public void GetDeploymentDirectoryShouldReturnDeploymentOutputDirectory() public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseButHasDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); + UnitTestElement testCase = CreateTestElement("path/to/asm.dll"); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), @@ -194,7 +184,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseButHasDeployme mstestSettingsProvider.Load(reader); // Deployment should not happen - testDeployment.Deploy(new List { testCase }, null, null!).Should().BeFalse(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeFalse(); // Deployment directories should not be created testDeployment.GetDeploymentDirectory().Should().BeNull(); @@ -202,8 +192,8 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseButHasDeployme public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseAndHasNoDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); + UnitTestElement testCase = CreateTestElement("path/to/asm.dll"); + testCase.DeploymentItems = null; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), @@ -217,7 +207,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseAndHasNoDeploy mstestSettingsProvider.Load(reader); // Deployment should not happen - testDeployment.Deploy(new List { testCase }, null, null!).Should().BeFalse(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeFalse(); // Deployment directories should get created on .NET Framework for compat, but not on .NET Core #if NETFRAMEWORK @@ -229,8 +219,8 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToFalseAndHasNoDeploy public void DeployShouldReturnFalseWhenDeploymentEnabledSetToTrueButHasNoDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), "path/to/asm.dll"); - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); + UnitTestElement testCase = CreateTestElement("path/to/asm.dll"); + testCase.DeploymentItems = null; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), @@ -244,7 +234,7 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToTrueButHasNoDeploym mstestSettingsProvider.Load(reader); // Deployment should not happen - testDeployment.Deploy(new List { testCase }, null, null!).Should().BeFalse(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeFalse(); // Deployment directories should get created on .NET Framework for compat, but not on .NET Core #if NETFRAMEWORK @@ -257,14 +247,14 @@ public void DeployShouldReturnFalseWhenDeploymentEnabledSetToTrueButHasNoDeploym // TODO: This test has to have mocks (tracked by https://github.com/microsoft/testfx/issues/8086). It actually deploys stuff and we cannot assume that all the dependencies get copied over to bin\debug. internal void DeployShouldReturnTrueWhenDeploymentEnabledSetToTrueAndHasDeploymentItems() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = CreateTestElement(typeof(TestDeploymentTests).Assembly.Location); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; var testDeployment = new TestDeployment( new DeploymentItemUtility(_mockReflectionOperations.Object), new DeploymentUtility(), @@ -278,7 +268,7 @@ internal void DeployShouldReturnTrueWhenDeploymentEnabledSetToTrueAndHasDeployme mstestSettingsProvider.Load(reader); // Deployment should happen - testDeployment.Deploy(new List { testCase }, null, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(null, null), new Mock().Object).Should().BeTrue(); // Deployment directories should get created testDeployment.GetDeploymentDirectory().Should().NotBeNull(); @@ -309,15 +299,12 @@ public void GetDeploymentInformationShouldReturnAppBaseDirectoryIfRunDirectoryIs public void GetDeploymentInformationShouldReturnRunDirectoryInformationIfSourceIsNull() { // Arrange. - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. IDictionary properties = TestDeployment.GetDeploymentInformation(null); @@ -339,15 +326,12 @@ public void GetDeploymentInformationShouldReturnRunDirectoryInformationIfSourceI public void GetDeploymentInformationShouldReturnRunDirectoryInformationIfSourceIsNotNull() { // Arrange. - TestCase testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); + UnitTestElement testCase = GetTestCase(typeof(TestDeploymentTests).Assembly.Location); // Setup mocks. TestDeployment testDeployment = CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories); - var mockRunContext = new Mock(); - mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(testRunDirectories.RootDeploymentDirectory); - - testDeployment.Deploy(new List { testCase }, mockRunContext.Object, new Mock().Object).Should().BeTrue(); + testDeployment.Deploy(new List { testCase }, new DeploymentContext(testRunDirectories.RootDeploymentDirectory, null), new Mock().Object).Should().BeTrue(); // Act. IDictionary properties = TestDeployment.GetDeploymentInformation(typeof(TestDeploymentTests).Assembly.Location); @@ -383,20 +367,23 @@ private void SetupDeploymentItems(ICustomAttributeProvider attributeProvider, Ke ru => ru.GetAttributes(attributeProvider)).Returns(deploymentItemAttributes); } - private static TestCase GetTestCase(string source) + private static UnitTestElement GetTestCase(string source) { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), source); + UnitTestElement testCase = CreateTestElement(source); KeyValuePair[] kvpArray = [ new KeyValuePair( DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; return testCase; } + private static UnitTestElement CreateTestElement(string source) + => new(new TestMethod("M", "C", source, displayName: null)); + private TestDeployment CreateAndSetupDeploymentRelatedUtilities(out TestRunDirectories testRunDirectories) { string currentExecutingFolder = Path.GetDirectoryName(typeof(TestDeploymentTests).Assembly.Location)!; diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs index c1a11d0c2b..10086d9e2d 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentItemUtilityTests.cs @@ -4,12 +4,12 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Moq; @@ -19,13 +19,6 @@ namespace MSTestAdapter.PlatformServices.UnitTests.Utilities; public class DeploymentItemUtilityTests : TestContainer { - internal static readonly TestProperty DeploymentItemsProperty = TestProperty.Register( - "MSTestDiscoverer.DeploymentItems", - "DeploymentItems", - typeof(KeyValuePair[]), - TestPropertyAttributes.Hidden, - typeof(TestCase)); - private readonly Mock _mockReflectionOperations; private readonly DeploymentItemUtility _deploymentItemUtility; private readonly ICollection _warnings; @@ -391,22 +384,24 @@ public void IsValidDeploymentItemShouldReturnTrueForAValidDeploymentItem() public void HasDeployItemsShouldReturnFalseForNoDeploymentItems() { - TestCase testCase = new("A.C.M", new Uri("executor://testExecutor"), "A"); - testCase.SetPropertyValue(DeploymentItemsProperty, null); + var testCase = new UnitTestElement(new TestMethod("M", "C", "A", displayName: null)) + { + DeploymentItems = null, + }; DeploymentItemUtility.HasDeploymentItems(testCase).Should().BeFalse(); } public void HasDeployItemsShouldReturnTrueWhenDeploymentItemsArePresent() { - TestCase testCase = new("A.C.M", new Uri("executor://testExecutor"), "A"); + var testCase = new UnitTestElement(new TestMethod("M", "C", "A", displayName: null)); KeyValuePair[] kvpArray = [ new KeyValuePair( _defaultDeploymentItemPath, _defaultDeploymentItemOutputDirectory) ]; - testCase.SetPropertyValue(DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; DeploymentItemUtility.HasDeploymentItems(testCase).Should().BeTrue(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs index c66fcba062..4f7d0d9e59 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Utilities/DeploymentUtilityTests.cs @@ -4,14 +4,12 @@ #if !WINDOWS_UWP && !WIN_UI using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Moq; @@ -29,8 +27,7 @@ public class DeploymentUtilityTests : TestContainer private readonly Mock _mockReflectionOperations; private readonly Mock _mockFileUtility; private readonly Mock _mockAssemblyUtility; - private readonly Mock _mockRunContext; - private readonly Mock _mockTestExecutionRecorder; + private readonly Mock _mockMessageLogger; private readonly DeploymentUtility _deploymentUtility; @@ -52,16 +49,15 @@ public DeploymentUtilityTests() _mockAssemblyUtility.Object, _mockFileUtility.Object); - _mockRunContext = new Mock(); - _mockTestExecutionRecorder = new Mock(); + _mockMessageLogger = new Mock(); } #region Deploy tests public void DeployShouldReturnFalseWhenNoDeploymentItemsOnTestCase() { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), Path.Combine(RootDeploymentDirectory, "asm.dll")); - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, null); + UnitTestElement testCase = CreateTestElement(Path.Combine(RootDeploymentDirectory, "asm.dll")); + testCase.DeploymentItems = null; var testRunDirectories = new TestRunDirectories(RootDeploymentDirectory, Path.Combine(RootDeploymentDirectory, "asm.dll"), isAppDomainCreationDisabled: true); _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -77,17 +73,17 @@ public void DeployShouldReturnFalseWhenNoDeploymentItemsOnTestCase() #endif _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeFalse(); } #if NETFRAMEWORK public void DeployShouldDeploySourceAndItsConfigFile() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -102,10 +98,10 @@ public void DeployShouldDeploySourceAndItsConfigFile() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -132,7 +128,7 @@ public void DeployShouldDeployDependentFiles() { string dependencyFile = "C:\\temp\\dependency.dll"; - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -147,10 +143,10 @@ public void DeployShouldDeployDependentFiles() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -167,7 +163,7 @@ public void DeployShouldDeployDependentFiles() public void DeployShouldDeploySatelliteAssemblies() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); string assemblyFullPath = Assembly.GetExecutingAssembly().Location; string satelliteFullPath = Path.Combine(Path.GetDirectoryName(assemblyFullPath), "de", "satellite.dll"); @@ -184,10 +180,10 @@ public void DeployShouldDeploySatelliteAssemblies() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -208,7 +204,7 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() string deploymentItemPath = "C:\\temp\\sample.dll"; string deploymentItemOutputDirectory = "..\\..\\out"; - TestCase testCase = GetTestCaseAndTestRunDirectories(deploymentItemPath, deploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(deploymentItemPath, deploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -225,10 +221,10 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -243,10 +239,10 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() Times.Never); // Verify the warning. - _mockTestExecutionRecorder.Verify( + _mockMessageLogger.Verify( ter => ter.SendMessage( - TestMessageLevel.Warning, + MessageLevel.Warning, string.Format( CultureInfo.InvariantCulture, Resource.DeploymentErrorBadDeploymentItem, @@ -257,7 +253,7 @@ public void DeployShouldNotDeployIfOutputDirectoryIsInvalid() public void DeployShouldDeployContentsOfADirectoryIfSpecified() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); string content1 = Path.Combine(DefaultDeploymentItemPath, "directoryContents.dll"); var directoryContentFiles = new List { content1 }; @@ -280,10 +276,10 @@ public void DeployShouldDeployContentsOfADirectoryIfSpecified() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -301,7 +297,7 @@ public void DeployShouldDeployContentsOfADirectoryIfSpecified() #if NETFRAMEWORK public void DeployShouldDeployPdbWithSourceIfPdbFileIsPresentInSourceDirectory() { - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -324,10 +320,10 @@ public void DeployShouldDeployPdbWithSourceIfPdbFileIsPresentInSourceDirectory() // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -356,7 +352,7 @@ public void DeployShouldNotDeployPdbFileOfAssemblyIfPdbFileIsNotPresentInAssembl // Path for pdb file of dependent assembly if pdb file is present. string pdbFile = Path.ChangeExtension(dependencyFile, "pdb"); - TestCase testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); + UnitTestElement testCase = GetTestCaseAndTestRunDirectories(DefaultDeploymentItemPath, DefaultDeploymentItemOutputDirectory, out TestRunDirectories testRunDirectories); // Setup mocks. _mockFileUtility.Setup(fu => fu.DoesDirectoryExist(It.Is(s => !s.EndsWith(".dll") && !s.EndsWith(".exe") && !s.EndsWith(".config")))) @@ -379,10 +375,10 @@ public void DeployShouldNotDeployPdbFileOfAssemblyIfPdbFileIsNotPresentInAssembl // Act. _deploymentUtility.Deploy( - new List { testCase }, - testCase.Source, - _mockRunContext.Object, - _mockTestExecutionRecorder.Object, + new List { testCase }, + testCase.TestMethod.AssemblyName, + new DeploymentContext(null, null), + _mockMessageLogger.Object, testRunDirectories).Should().BeTrue(); // Assert. @@ -411,12 +407,11 @@ public void DeployShouldNotDeployPdbFileOfAssemblyIfPdbFileIsNotPresentInAssembl public void CreateDeploymentDirectoriesShouldCreateDeploymentDirectoryFromRunContext() { // Setup mocks - _mockRunContext.Setup(rc => rc.TestRunDirectory).Returns(TestRunDirectory); _mockFileUtility.Setup(fu => fu.GetNextIterationDirectoryName(TestRunDirectory, It.IsAny())) .Returns(RootDeploymentDirectory); // Act. - _deploymentUtility.CreateDeploymentDirectories(_mockRunContext.Object, null); + _deploymentUtility.CreateDeploymentDirectories(new DeploymentContext(TestRunDirectory, null), null); // Assert. _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(RootDeploymentDirectory), Times.Once); @@ -428,12 +423,11 @@ public void CreateDeploymentDirectoriesShouldCreateDeploymentDirectoryFromRunCon public void CreateDeploymentDirectoriesShouldCreateDefaultDeploymentDirectoryIfTestRunDirectoryIsNull() { // Setup mocks - _mockRunContext.Setup(rc => rc.TestRunDirectory).Returns((string)null!); _mockFileUtility.Setup(fu => fu.GetNextIterationDirectoryName(It.Is(s => s.Contains(TestRunDirectories.DefaultDeploymentRootDirectory)), It.IsAny())) .Returns(RootDeploymentDirectory); // Act. - _deploymentUtility.CreateDeploymentDirectories(_mockRunContext.Object, null); + _deploymentUtility.CreateDeploymentDirectories(new DeploymentContext(null, null), null); // Assert. _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(RootDeploymentDirectory), Times.Once); @@ -449,7 +443,7 @@ public void CreateDeploymentDirectoriesShouldCreateDefaultDeploymentDirectoryIfR .Returns(RootDeploymentDirectory); // Act. - _deploymentUtility.CreateDeploymentDirectories(null, null); + _deploymentUtility.CreateDeploymentDirectories(new DeploymentContext(null, null), null); // Assert. _mockFileUtility.Verify(fu => fu.CreateDirectoryIfNotExists(RootDeploymentDirectory), Times.Once); @@ -462,16 +456,16 @@ public void CreateDeploymentDirectoriesShouldCreateDefaultDeploymentDirectoryIfR #region private methods - private static TestCase GetTestCaseAndTestRunDirectories(string deploymentItemPath, string defaultDeploymentItemOutputDirectoryOut, out TestRunDirectories testRunDirectories) + private static UnitTestElement GetTestCaseAndTestRunDirectories(string deploymentItemPath, string defaultDeploymentItemOutputDirectoryOut, out TestRunDirectories testRunDirectories) { - var testCase = new TestCase("A.C.M", new Uri("executor://testExecutor"), Assembly.GetExecutingAssembly().Location); + UnitTestElement testCase = CreateTestElement(Assembly.GetExecutingAssembly().Location); KeyValuePair[] kvpArray = [ new KeyValuePair( deploymentItemPath, defaultDeploymentItemOutputDirectoryOut) ]; - testCase.SetPropertyValue(DeploymentItemUtilityTests.DeploymentItemsProperty, kvpArray); + testCase.DeploymentItems = kvpArray; string currentExecutingFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; const bool isAppDomainCreationDisabled = @@ -486,6 +480,9 @@ private static TestCase GetTestCaseAndTestRunDirectories(string deploymentItemPa return testCase; } + private static UnitTestElement CreateTestElement(string source) + => new(new TestMethod("M", "C", source, displayName: null)); + #endregion } #endif From 371124a41901c10c52d90db5cf0b723f46b39547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 16:22:44 +0200 Subject: [PATCH 09/24] Neutralize test result recording in PlatformServices (Phase 6b) (#9579) Co-authored-by: Amaury Leveque Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Extensions/TestResultExtensions.cs | 1 + .../Helpers/UnitTestOutcomeHelper.cs | 0 .../MSTest.TestAdapter.csproj | 1 + .../Services/TestResultRecorderExtensions.cs | 25 +++--- .../VSTestAdapter/MSTestExecutor.cs | 13 ++- .../TestExecutionManager.Parallelization.cs | 5 +- .../Execution/TestExecutionManager.Runner.cs | 20 ++--- .../Execution/TestExecutionManager.cs | 21 +++-- .../Interfaces/ITestResultRecorder.cs | 34 ++++---- .../TestFramework/TestFramework.csproj | 1 + .../Utilities/CLITestBase.discovery.cs | 7 +- .../Execution/TestExecutionManagerTests.cs | 82 ++++++++++--------- ...tAdapter.PlatformServices.UnitTests.csproj | 1 + 13 files changed, 116 insertions(+), 95 deletions(-) rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/Extensions/TestResultExtensions.cs (98%) rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/Helpers/UnitTestOutcomeHelper.cs (100%) rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/Services/TestResultRecorderExtensions.cs (71%) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestResultExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs similarity index 98% rename from src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestResultExtensions.cs rename to src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs index 0eac8b99a2..0a4824f828 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestResultExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; using VSTestAttachmentSet = Microsoft.VisualStudio.TestPlatform.ObjectModel.AttachmentSet; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/UnitTestOutcomeHelper.cs b/src/Adapter/MSTest.TestAdapter/Helpers/UnitTestOutcomeHelper.cs similarity index 100% rename from src/Adapter/MSTestAdapter.PlatformServices/Helpers/UnitTestOutcomeHelper.cs rename to src/Adapter/MSTest.TestAdapter/Helpers/UnitTestOutcomeHelper.cs diff --git a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj index 20616c2118..b4792d9cad 100644 --- a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj +++ b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj @@ -193,6 +193,7 @@ + diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestResultRecorderExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs similarity index 71% rename from src/Adapter/MSTestAdapter.PlatformServices/Services/TestResultRecorderExtensions.cs rename to src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs index 911f563609..d0eba71eb2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestResultRecorderExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs @@ -1,25 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; -namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; /// /// Bridges a VSTest to the platform-agnostic . /// /// -/// This is the single translation point between the VSTest result object model (TestResult, -/// TestOutcome, attachments, ...) and the platform services result-reporting abstraction. It is -/// expected to move entirely into the adapter layer once the execution pipeline no longer flows VSTest -/// recorders through the platform services. +/// This is the single translation point between the neutral execution model ( and +/// the framework ) and the VSTest result object model (TestResult, +/// TestOutcome, attachments, ...). It lives in the adapter layer; the platform services engine reports +/// through the neutral only. The element's host test case is resolved (and +/// materialized/cached on demand for internally discovered tests) via +/// so recorded results preserve host-injected data (test-case-management / data-collector properties) with full fidelity. /// internal static class TestResultRecorderExtensions { @@ -46,14 +48,15 @@ public HostTestResultRecorder(ITestExecutionRecorder testExecutionRecorder, stri _settings = settings; } - public void RecordStart(TestCase testCase) - => _testExecutionRecorder.RecordStart(testCase); + public void RecordStart(UnitTestElement testElement) + => _testExecutionRecorder.RecordStart(testElement.GetOrCreateHostTestCase()); - public void RecordEmptyResult(TestCase testCase) - => _testExecutionRecorder.RecordEnd(testCase, TestOutcome.None); + public void RecordEmptyResult(UnitTestElement testElement) + => _testExecutionRecorder.RecordEnd(testElement.GetOrCreateHostTestCase(), TestOutcome.None); - public bool RecordResult(TestCase testCase, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime) + public bool RecordResult(UnitTestElement testElement, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime) { + TestCase testCase = testElement.GetOrCreateHostTestCase(); var testResult = unitTestResult.ToTestResult(testCase, startTime, endTime, _computerName, _settings); _testExecutionRecorder.RecordEnd(testCase, testResult.Outcome); diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs index a58d362ec4..e8ce1faca9 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs @@ -152,7 +152,11 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run // properties surfaced through TestContext. UnitTestElement[] testElements = [.. tests.Select(ToUnitTestElement)]; - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, runContext, frameworkHandle, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + // Translate the VSTest recorder into the platform-agnostic result recorder at this boundary; the + // execution engine reports results through this neutral recorder and never constructs VSTest results. + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, runContext, frameworkHandle, testResultRecorder, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { @@ -205,7 +209,12 @@ internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? run } sources = testSourceHandler.GetTestSources(sources); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + + // Translate the VSTest recorder into the platform-agnostic result recorder at this boundary; the + // execution engine reports results through this neutral recorder and never constructs VSTest results. + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testResultRecorder, testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 7d4ba27de1..cbad57db82 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -20,9 +20,12 @@ internal partial class TestExecutionManager /// Tests to execute. /// The run context. /// Handle to record test start/end/results. + /// Recorder used to report test results back to the host. /// Indicates if deployment is done. - internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) + internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, bool isDeploymentDone) { + _testResultRecorder = testResultRecorder; + InitializeRandomTestOrder(frameworkHandle); var testsBySource = (from test in tests diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs index aa1d11bef0..ab1b641a63 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -13,7 +11,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; internal partial class TestExecutionManager { internal void SendTestResults( - TestCase test, + UnitTestElement test, TestTools.UnitTesting.TestResult[] unitTestResults, DateTimeOffset startTime, DateTimeOffset endTime, @@ -73,10 +71,6 @@ private async Task ExecuteTestsWithTestRunnerAsync( ? new RemotingMessageLogger(testExecutionRecorder) : testExecutionRecorder; - // Translate the VSTest recorder into the platform-agnostic result recorder a single time for this - // test set. This is the boundary at which VSTest result construction is applied. - ITestResultRecorder testResultRecorder = testExecutionRecorder.ToTestResultRecorder(_environment.MachineName, MSTestSettings.CurrentSettings); - foreach (UnitTestElement currentTest in orderedTests) { _testRunCancellationToken?.ThrowIfCancellationRequested(); @@ -87,13 +81,9 @@ private async Task ExecuteTestsWithTestRunnerAsync( UnitTestElement unitTestElement = currentTest.WithUpdatedSource(source); - // Obtain the host's test case for this test as an OPAQUE handle: the engine passes it verbatim to - // the (still VSTest-based) result recorder and reads nothing VSTest-specific off it. This preserves - // byte-for-byte reporting fidelity — including any host-injected (TCM / data-collector) properties — - // for the recorded results. Neutralizing the recorder is deferred to a later boundary phase. - TestCase currentTestCase = currentTest.GetOrCreateHostTestCase(); - - testResultRecorder.RecordStart(currentTestCase); + // Report through the neutral recorder using the element itself; the adapter-side recorder resolves + // the host test case (preserving host-injected TCM / data-collector properties) with full fidelity. + _testResultRecorder.RecordStart(currentTest); DateTimeOffset startTime = DateTimeOffset.Now; @@ -131,7 +121,7 @@ private async Task ExecuteTestsWithTestRunnerAsync( DateTimeOffset endTime = DateTimeOffset.Now; - SendTestResults(currentTestCase, unitTestResult, startTime, endTime, testResultRecorder); + SendTestResults(currentTest, unitTestResult, startTime, endTime, _testResultRecorder); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index 669a01073c..2546567638 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -24,7 +24,6 @@ internal partial class TestExecutionManager /// Dictionary for test run parameters. /// private readonly IDictionary _sessionParameters; - private readonly IEnvironment _environment; private readonly Func, Task> _taskFactory; /// @@ -38,19 +37,24 @@ internal partial class TestExecutionManager /// private Random? _testOrderRandom; + /// + /// Recorder used to report test start/end/results back to the host. Supplied at the adapter boundary and set + /// once per run in before any source is processed. + /// + private ITestResultRecorder _testResultRecorder = null!; + /// /// Initializes a new instance of the class. /// public TestExecutionManager() - : this(EnvironmentWrapper.Instance) + : this(null) { } - internal TestExecutionManager(IEnvironment environment, Func, Task>? taskFactory = null) + internal TestExecutionManager(Func, Task>? taskFactory = null) { _testMethodFilter = new TestMethodFilter(); _sessionParameters = new Dictionary(); - _environment = environment; _taskFactory = taskFactory ?? DefaultFactoryAsync; } @@ -87,8 +91,9 @@ private static Task DefaultFactoryAsync(Func taskGetter) /// Tests to be run. /// Context to use when executing the tests. /// Handle to the framework to record results and to do framework operations. + /// Recorder used to report test results back to the host. /// Test run cancellation token. - internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, TestRunCancellationToken runCancellationToken) + internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, TestRunCancellationToken runCancellationToken) { DebugEx.Assert(tests != null, "tests"); DebugEx.Assert(runContext != null, "runContext"); @@ -108,7 +113,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex CacheSessionParameters(runContext, frameworkHandle); // Execute the tests - await ExecuteTestsAsync(tests, runContext, frameworkHandle, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -118,7 +123,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex #endif } - internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) + internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) { _testRunCancellationToken = cancellationToken; PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; @@ -152,7 +157,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC CacheSessionParameters(runContext, frameworkHandle); // Run tests. - await ExecuteTestsAsync(tests, runContext, frameworkHandle, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs index 1a24a89772..7887c7df51 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestResultRecorder.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; @@ -12,36 +12,34 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Int /// test host is running the tests. /// /// -/// This abstraction lets the platform services layer report test start/end and results without taking a -/// dependency on a specific test platform's result object model (for example the VSTest TestResult, -/// TestOutcome and attachment types). The concrete recorder is provided at the platform boundary by a -/// wrapper over the host's result recorder (currently TestResultRecorderExtensions, which wraps the -/// VSTest ITestExecutionRecorder), and is expected to move fully out of the platform services layer in -/// a later phase. The execution engine passes the test's TestCase as an opaque handle to this recorder -/// (it reads nothing VSTest-specific off it); reporting fidelity — including host-injected properties — is the -/// recorder's responsibility. +/// This abstraction lets the platform services execution engine report test start/end and results using only +/// the neutral and framework models, without +/// taking a dependency on a specific test platform's result object model (for example the VSTest +/// TestResult, TestOutcome and attachment types). The concrete recorder — which builds the host +/// result representation — is provided at the adapter boundary (see the VSTest implementation in +/// TestResultRecorderExtensions in the adapter layer). /// internal interface ITestResultRecorder { /// - /// Signals that execution of the given test case has started. + /// Signals that execution of the given test has started. /// - /// The test case whose execution is starting. - void RecordStart(TestCase testCase); + /// The test whose execution is starting. + void RecordStart(UnitTestElement testElement); /// - /// Signals that execution of the given test case ended without producing any result. + /// Signals that execution of the given test ended without producing any result. /// - /// The test case whose execution ended. - void RecordEmptyResult(TestCase testCase); + /// The test whose execution ended. + void RecordEmptyResult(UnitTestElement testElement); /// - /// Reports a single framework for the given test case to the test host. + /// Reports a single framework for the given test to the test host. /// - /// The test case the result belongs to. + /// The test the result belongs to. /// The framework result produced by executing the test. /// The time at which the test started executing. /// The time at which the test finished executing. /// if the result was reported as a failure; otherwise, . - bool RecordResult(TestCase testCase, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime); + bool RecordResult(UnitTestElement testElement, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime); } diff --git a/src/TestFramework/TestFramework/TestFramework.csproj b/src/TestFramework/TestFramework/TestFramework.csproj index f7e4b961f1..759d85cc85 100644 --- a/src/TestFramework/TestFramework/TestFramework.csproj +++ b/src/TestFramework/TestFramework/TestFramework.csproj @@ -58,6 +58,7 @@ + diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index 19a72cac8f..4ac356c178 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -14,6 +14,7 @@ using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using ITestResultRecorder = Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ITestResultRecorder; using TestResult = Microsoft.VisualStudio.TestPlatform.ObjectModel.TestResult; namespace Microsoft.MSTestV2.CLIAutomation; @@ -39,7 +40,8 @@ internal static async Task> RunTestsAsync(IEnumerable var testExecutionManager = new TestExecutionManager(); var frameworkHandle = new InternalFrameworkHandle(); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), null, frameworkHandle, false); + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), null, frameworkHandle, testResultRecorder, false); return frameworkHandle.GetFlattenedTestResults(); } @@ -51,7 +53,8 @@ internal static async Task> RunTestsAsync(IEnumerable string runSettingsXml = GetRunSettingsXml(string.Empty); var runContext = new InternalRunContext(runSettingsXml, testCaseFilter); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), runContext, frameworkHandle, false); + ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), runContext, frameworkHandle, testResultRecorder, false); return frameworkHandle.GetFlattenedTestResults(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index e228371479..57341c0d04 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -56,6 +56,13 @@ public class TestExecutionManagerTests : TestContainer private List _callers = []; private int _enqueuedParallelTestsCount; + /// + /// Builds the VSTest-backed result recorder the execution engine now receives at its boundary, recording into + /// exactly as the adapter does in production. + /// + private ITestResultRecorder TestResultRecorder + => _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings); + public TestExecutionManagerTests() { _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(_ => true)); @@ -65,7 +72,6 @@ public TestExecutionManagerTests() _mockTestSourceHandler = new Mock(); _testExecutionManager = new TestExecutionManager( - EnvironmentWrapper.Instance, task => { _enqueuedParallelTestsCount++; @@ -94,7 +100,7 @@ public async Task RunTestsForTestWithFilterErrorShouldSendZeroResults() // Causing the FilterExpressionError _runContext = new TestableRunContextTestExecutionTests(() => throw new TestPlatformFormatException()); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); // No Results _frameworkHandle.TestCaseStartList.Count.Should().Be(0); @@ -110,7 +116,7 @@ public async Task RunTestsForTestWithFilterShouldSendResultsForFilteredTests() _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(p => p.DisplayName == "PassingTest")); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); // FailingTest should be skipped because it does not match the filter criteria. List expectedTestCaseStartList = ["PassingTest"]; @@ -127,7 +133,7 @@ public void SendTestResults_WhenUnitTestResultsIsEmpty_RecordsEndWithoutResult() TestCase testCase = GetTestCase(typeof(DummyTestClass), "PassingTest"); Microsoft.VisualStudio.TestTools.UnitTesting.TestResult[] unitTestResults = []; - _testExecutionManager.SendTestResults(testCase, unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings)); + _testExecutionManager.SendTestResults(ToUnitTestElement(testCase), unitTestResults, DateTimeOffset.Now, DateTimeOffset.Now, _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings)); _frameworkHandle.TestCaseEndList.Should().Equal("PassingTest:None"); _frameworkHandle.ResultsList.Should().BeEmpty(); @@ -138,7 +144,7 @@ public async Task RunTestsForIgnoredTestShouldSendResultsMarkingIgnoredTestsAsSk TestCase testCase = GetTestCase(typeof(DummyTestClass), "IgnoredTest"); TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); _frameworkHandle.TestCaseStartList[0].Should().Be("IgnoredTest"); _frameworkHandle.TestCaseEndList[0].Should().Be("IgnoredTest:Skipped"); @@ -151,7 +157,7 @@ public async Task RunTestsForASingleTestShouldSendSingleResult() TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); List expectedTestCaseStartList = ["PassingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed"]; @@ -168,7 +174,7 @@ public async Task RunTestsForMultipleTestShouldSendMultipleResults() TestCase failingTestCase = GetTestCase(typeof(DummyTestClass), "FailingTest"); TestCase[] tests = [testCase, failingTestCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); List expectedTestCaseStartList = ["PassingTest", "FailingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed", "FailingTest:Failed"]; @@ -188,7 +194,7 @@ public async Task RunTestsForCancellationTokenCanceledSetToTrueShouldSendZeroRes // Cancel the test run _cancellationToken.Cancel(); - Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, _cancellationToken); + Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); await func.Should().ThrowAsync(); // No Results @@ -212,7 +218,7 @@ await _testExecutionManager.RunTestsAsync( ToUnitTestElements(tests), _runContext, _frameworkHandle, - new TestRunCancellationToken()); + TestResultRecorder, new TestRunCancellationToken()); _callers[0].Should().Be("Deploy", "Deploy should be called before execution."); _callers[1].Should().Be("LoadAssembly", "Deploy should be called before execution."); @@ -232,7 +238,7 @@ public async Task RunTestsForTestShouldCleanupAfterExecution() td => td.Cleanup()).Callback(() => SetCaller("Cleanup")); #endif - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); _callers[0].Should().Be("LoadAssembly", "Cleanup should be called after execution."); @@ -249,7 +255,7 @@ public async Task RunTestsForTestShouldNotCleanupOnTestFailure() TestCase[] tests = [testCase, failingTestCase]; TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); testablePlatformService.MockTestDeployment.Verify(td => td.Cleanup(), Times.Never); } @@ -268,7 +274,7 @@ public async Task RunTestsForTestShouldLoadSourceFromDeploymentDirectoryIfDeploy testablePlatformService.MockTestDeployment.Setup(td => td.GetDeploymentDirectory()) .Returns(@"C:\temp"); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); testablePlatformService.MockFileOperations.Verify( fo => fo.LoadAssembly(It.Is(s => s.StartsWith("C:\\temp"))), @@ -294,7 +300,7 @@ public async Task RunTestsForTestShouldPassInTestRunParametersInformationAsPrope """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -316,7 +322,7 @@ public async Task RunTestsForTestShouldPassInTcmPropertiesAsPropertiesToTheTest( """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); VerifyTcmProperties(DummyTestClass.TestContextProperties, testCase); } @@ -329,7 +335,7 @@ public async Task RunTestsForTestShouldPassInDeploymentInformationAsPropertiesTo // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); testablePlatformService.MockSettingsProvider.Verify(sp => sp.GetProperties(It.IsAny()), Times.Once); } @@ -353,7 +359,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger First Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); // Update runsettings to have different values for similar keys _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -370,7 +376,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger another Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); "http://updatedLocalHost".Equals(DummyTestClass.TestContextProperties!["webAppUrl"]).Should().BeTrue(); } @@ -384,7 +390,7 @@ private async Task RunTestsForSourceShouldRunTestsInASource() { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); _frameworkHandle.TestCaseStartList.Contains("PassingTest").Should().BeTrue(); _frameworkHandle.TestCaseEndList.Contains("PassingTest:Passed").Should().BeTrue(); @@ -406,7 +412,7 @@ private async Task RunTestsForSourceShouldPassInTestRunParametersInformationAsPr """); - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -417,7 +423,7 @@ private async Task RunTestsForSourceShouldPassInDeploymentInformationAsPropertie { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties.Should().NotBeNull(); } @@ -428,10 +434,10 @@ public async Task RunTestsForMultipleSourcesShouldRunEachTestJustOnce() var sources = new List { Assembly.GetExecutingAssembly().Location, Assembly.GetExecutingAssembly().Location }; TestableTestExecutionManager testableTestExecutionManager = new() { - ExecuteTestsWrapper = (tests, runContext, frameworkHandle, isDeploymentDone) => testsCount += tests.Count(), + ExecuteTestsWrapper = (tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone) => testsCount += tests.Count(), }; - await testableTestExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, _mockTestSourceHandler.Object, false, _cancellationToken); + await testableTestExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); testsCount.Should().Be(4); } @@ -464,7 +470,7 @@ public async Task RunTestsForTestShouldRunTestsInParallelWhenEnabledInRunsetting try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -501,7 +507,7 @@ public async Task RunTestsForTestShouldRunTestsByMethodLevelWhenSpecified() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -538,7 +544,7 @@ public async Task RunTestsForTestShouldRunTestsWithSpecifiedNumberOfWorkers() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -601,7 +607,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -660,7 +666,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -699,7 +705,7 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); DummyTestClassWithDoNotParallelizeMethods.ParallelizableTestsThreadIds.Count.Should().BeOneOf(1, 2); @@ -765,7 +771,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -805,7 +811,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.Count.Should().Be(1); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.ToArray()[0].Should().Be(Thread.CurrentThread.GetApartmentState()); @@ -847,12 +853,12 @@ static TestCase[] BuildTests() => MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); var firstHandle = new TestableFrameworkHandle(); - var firstManager = new TestExecutionManager(EnvironmentWrapper.Instance, task => task()); - await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, firstHandle, new TestRunCancellationToken()); + var firstManager = new TestExecutionManager(task => task()); + await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, firstHandle, firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestRunCancellationToken()); var secondHandle = new TestableFrameworkHandle(); - var secondManager = new TestExecutionManager(EnvironmentWrapper.Instance, task => task()); - await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, secondHandle, new TestRunCancellationToken()); + var secondManager = new TestExecutionManager(task => task()); + await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, secondHandle, secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestRunCancellationToken()); // Same seed must produce the same order across separate runs. firstHandle.TestCaseStartList.Should().Equal(secondHandle.TestCaseStartList); @@ -889,7 +895,7 @@ public async Task RunTestsWithoutRandomizeTestOrderShouldPreserveInputOrder() MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); _frameworkHandle.TestCaseStartList.Should().Equal("TestA", "TestB", "TestC"); } @@ -1265,11 +1271,11 @@ internal sealed class TestableTestCaseFilterExpression : ITestCaseFilterExpressi internal class TestableTestExecutionManager : TestExecutionManager { - internal Action, IRunContext?, IFrameworkHandle, bool> ExecuteTestsWrapper { get; set; } = null!; + internal Action, IRunContext?, IFrameworkHandle, ITestResultRecorder, bool> ExecuteTestsWrapper { get; set; } = null!; - internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, bool isDeploymentDone) + internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, bool isDeploymentDone) { - ExecuteTestsWrapper?.Invoke(tests, runContext, frameworkHandle, isDeploymentDone); + ExecuteTestsWrapper?.Invoke(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone); return Task.CompletedTask; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj index f2fa7a0cdd..b265512c2a 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestAdapter.PlatformServices.UnitTests.csproj @@ -25,6 +25,7 @@ + From 274e479f023199f08f9828d4af7d2a3cfe3b9597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 3 Jul 2026 17:05:50 +0200 Subject: [PATCH 10/24] Neutralize test message logging in PlatformServices execution engine (Phase 6c) (#9585) Co-authored-by: Amaury Leveque Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Execution/ClassCleanupManager.cs | 4 +-- .../TestExecutionManager.Parallelization.cs | 10 ++++---- .../Execution/TestExecutionManager.Runner.cs | 12 ++++----- .../TestExecutionManager.TestContext.cs | 6 ++--- .../TestExecutionManager.Utilities.cs | 25 ++++++++++++------- .../Execution/TestExecutionManager.cs | 4 +-- .../Execution/UnitTestRunner.RunSingleTest.cs | 7 +++--- .../Execution/UnitTestRunner.TestFilter.cs | 3 +-- .../IPlatformServiceProvider.cs | 3 +-- .../PlatformServiceProvider.cs | 3 +-- .../Services/TestContextImplementation.cs | 7 +++--- .../TestContextImplementationTests.cs | 20 +++++++-------- .../TestablePlatformServiceProvider.cs | 3 +-- 13 files changed, 53 insertions(+), 54 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs index a41268c299..16081a0f4b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/ClassCleanupManager.cs @@ -3,7 +3,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using AdapterApplicationStateGuard = Microsoft.VisualStudio.TestPlatform.MSTestAdapter.ApplicationStateGuard; @@ -53,7 +53,7 @@ public void MarkClassComplete(string fullClassName) } } - internal static void ForceCleanup(TypeCache typeCache, IDictionary sourceLevelParameters, IMessageLogger logger) + internal static void ForceCleanup(TypeCache typeCache, IDictionary sourceLevelParameters, IAdapterMessageLogger logger) { IEnumerable classInfoCache = typeCache.ClassInfoListWithExecutableCleanupMethods; foreach (TestClassInfo classInfo in classInfoCache) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index cbad57db82..8d9590224a 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -26,7 +26,7 @@ internal virtual async Task ExecuteTestsAsync(IEnumerable tests { _testResultRecorder = testResultRecorder; - InitializeRandomTestOrder(frameworkHandle); + InitializeRandomTestOrder(frameworkHandle.ToAdapterMessageLogger()); var testsBySource = (from test in tests group test by test.TestMethod.AssemblyName into testGroup @@ -223,7 +223,7 @@ .. parallelizableTestSet if (queue.TryDequeue(out IEnumerable? testSet)) { - await ExecuteTestsWithTestRunnerAsync(testSet, frameworkHandle, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); + await ExecuteTestsWithTestRunnerAsync(testSet, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } } } @@ -258,17 +258,17 @@ .. parallelizableTestSet // Queue the non parallel set if (nonParallelizableTestSet != null) { - await ExecuteTestsWithTestRunnerAsync(nonParallelizableTestSet, frameworkHandle, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); + await ExecuteTestsWithTestRunnerAsync(nonParallelizableTestSet, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } } else { - await ExecuteTestsWithTestRunnerAsync(testsToRun, frameworkHandle, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); + await ExecuteTestsWithTestRunnerAsync(testsToRun, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } if (PlatformServiceProvider.Instance.IsGracefulStopRequested) { - testRunner.ForceCleanup(sourceLevelParameters!, new RemotingMessageLogger(frameworkHandle)); + testRunner.ForceCleanup(sourceLevelParameters!, new RemotingMessageLogger(adapterMessageLogger)); } if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs index ab1b641a63..67de43cfe2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Runner.cs @@ -3,8 +3,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -52,7 +50,7 @@ private static bool MatchTestFilter(ITestElementFilter? filter, UnitTestElement private async Task ExecuteTestsWithTestRunnerAsync( IEnumerable tests, - ITestExecutionRecorder testExecutionRecorder, + IAdapterMessageLogger adapterMessageLogger, string source, IDictionary sourceLevelParameters, UnitTestRunner testRunner, @@ -65,11 +63,11 @@ private async Task ExecuteTestsWithTestRunnerAsync( .ThenBy(t => t.TestMethod.HasManagedMethodAndTypeProperties ? t.TestMethod.ManagedMethodName : null) : tests; - // If testRunner is in a different AppDomain, we cannot pass the testExecutionRecorder directly. + // If testRunner is in a different AppDomain, we cannot pass the message logger directly. // Instead, we pass a proxy (remoting object) that is marshallable by ref. - IMessageLogger remotingMessageLogger = usesAppDomains - ? new RemotingMessageLogger(testExecutionRecorder) - : testExecutionRecorder; + IAdapterMessageLogger remotingMessageLogger = usesAppDomains + ? new RemotingMessageLogger(adapterMessageLogger) + : adapterMessageLogger; foreach (UnitTestElement currentTest in orderedTests) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs index 541881f6fb..bbcc7ab9b8 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs @@ -3,9 +3,9 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -99,7 +99,7 @@ private static void ValidateAndAssignTestProperty( } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle errors in user specified run parameters")] - private void CacheSessionParameters(IRunContext? runContext, ITestExecutionRecorder testExecutionRecorder) + private void CacheSessionParameters(IRunContext? runContext, IAdapterMessageLogger messageLogger) { if (StringEx.IsNullOrEmpty(runContext?.RunSettings?.SettingsXml)) { @@ -122,7 +122,7 @@ private void CacheSessionParameters(IRunContext? runContext, ITestExecutionRecor } catch (Exception ex) { - testExecutionRecorder.SendMessage(TestMessageLevel.Error, ex.Message); + messageLogger.SendMessage(MessageLevel.Error, ex.Message); } } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs index c21f1add17..d23abdff7b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Utilities.cs @@ -1,28 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; internal partial class TestExecutionManager { + /// + /// A message logger that can be marshaled into the isolation (child app-domain) host so log messages + /// raised while running a test cross back to the parent-domain . + /// Marshaling is by reference (the wrapped logger stays in the parent domain), so no VSTest object-model + /// type is required in the child domain. + /// private sealed class RemotingMessageLogger : #if NETFRAMEWORK MarshalByRefObject, #endif - IMessageLogger + IAdapterMessageLogger { - private readonly IMessageLogger _realMessageLogger; + private readonly IAdapterMessageLogger _realMessageLogger; - public RemotingMessageLogger(IMessageLogger messageLogger) + public RemotingMessageLogger(IAdapterMessageLogger messageLogger) => _realMessageLogger = messageLogger; - public void SendMessage(TestMessageLevel testMessageLevel, string message) - => _realMessageLogger.SendMessage(testMessageLevel, message); + public void SendMessage(MessageLevel level, string message) + => _realMessageLogger.SendMessage(level, message); } - private void InitializeRandomTestOrder(IMessageLogger frameworkHandle) + private void InitializeRandomTestOrder(IAdapterMessageLogger messageLogger) { if (!MSTestSettings.CurrentSettings.RandomizeTestOrder) { @@ -36,8 +43,8 @@ private void InitializeRandomTestOrder(IMessageLogger frameworkHandle) int seed = MSTestSettings.CurrentSettings.RandomTestOrderSeed ?? Guid.NewGuid().GetHashCode(); _testOrderRandom = new Random(seed); - frameworkHandle.SendMessage( - TestMessageLevel.Informational, + messageLogger.SendMessage( + MessageLevel.Informational, string.Format(CultureInfo.CurrentCulture, Resource.RandomTestOrderBanner, seed)); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index 2546567638..c67e7f8973 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -110,7 +110,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext, frameworkHandle); + CacheSessionParameters(runContext, frameworkHandle.ToAdapterMessageLogger()); // Execute the tests await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); @@ -154,7 +154,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext, frameworkHandle); + CacheSessionParameters(runContext, frameworkHandle.ToAdapterMessageLogger()); // Run tests. await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs index 8537205e91..396541bad2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.RunSingleTest.cs @@ -5,7 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -14,7 +13,7 @@ internal sealed partial class UnitTestRunner { // Task cannot cross app domains. // For now, TestExecutionManager will call this sync method which is hacky. - internal TestResult[] RunSingleTest(UnitTestElement unitTestElement, IDictionary testContextProperties, IMessageLogger messageLogger) + internal TestResult[] RunSingleTest(UnitTestElement unitTestElement, IDictionary testContextProperties, IAdapterMessageLogger messageLogger) => RunSingleTestAsync(unitTestElement, testContextProperties, messageLogger).GetAwaiter().GetResult(); /// @@ -24,7 +23,7 @@ internal TestResult[] RunSingleTest(UnitTestElement unitTestElement, IDictionary /// The test context properties. /// The message logger. /// The . - internal async Task RunSingleTestAsync(UnitTestElement unitTestElement, IDictionary testContextProperties, IMessageLogger messageLogger) + internal async Task RunSingleTestAsync(UnitTestElement unitTestElement, IDictionary testContextProperties, IAdapterMessageLogger messageLogger) { if (unitTestElement is null) { @@ -288,5 +287,5 @@ private static bool IsTestMethodRunnable( return true; } - internal void ForceCleanup(IDictionary sourceLevelParameters, IMessageLogger logger) => ClassCleanupManager.ForceCleanup(_typeCache, sourceLevelParameters, logger); + internal void ForceCleanup(IDictionary sourceLevelParameters, IAdapterMessageLogger logger) => ClassCleanupManager.ForceCleanup(_typeCache, sourceLevelParameters, logger); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs index 8b0ba506c4..89f2a214eb 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/UnitTestRunner.TestFilter.cs @@ -6,7 +6,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; // The programmatic test-filter types (ITestFilter, TestFilterContext, TestFilterResult, @@ -150,7 +149,7 @@ private static TestFilterContext CreateFilterContext(UnitTestElement element) private async Task FinishFilteredOutTestAsync( TestMethod testMethod, IDictionary testContextProperties, - IMessageLogger messageLogger, + IAdapterMessageLogger messageLogger, TestResult[] filterResult, ITestContext testContextForTestExecution) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs index 25761fb0a8..315567e42b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs @@ -3,7 +3,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; @@ -97,5 +96,5 @@ ITestSourceHost CreateTestSourceHost( /// /// This was required for compatibility reasons since the TestContext object that the V1 adapter had for desktop is not .Net Core compliant. /// - ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger messageLogger, UTF.UnitTestOutcome outcome); + ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger messageLogger, UTF.UnitTestOutcome outcome); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs index 81c975c823..be07d3c0ad 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; @@ -166,7 +165,7 @@ public ITestSourceHost CreateTestSourceHost( /// /// This was required for compatibility reasons since the TestContext object that the V1 adapter had for desktop is not .Net Core compliant. /// - public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger messageLogger, UTF.UnitTestOutcome outcome) + public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger messageLogger, UTF.UnitTestOutcome outcome) { var testContextImplementation = new TestContextImplementation(testMethod, testClassFullName, properties, messageLogger, TestRunCancellationToken); testContextImplementation.SetOutcome(outcome); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs index 5efc98a041..9cfb5f78d4 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestContextImplementation.cs @@ -10,7 +10,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using ITestMethod = Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel.ITestMethod; @@ -71,7 +70,7 @@ public override string ToString() #else private readonly object _propertiesLock = new(); #endif - private readonly IMessageLogger? _messageLogger; + private readonly IAdapterMessageLogger? _messageLogger; private readonly TestRunCancellationToken? _testRunCancellationToken; private CancellationTokenRegistration? _cancellationTokenRegistration; @@ -113,7 +112,7 @@ public override string ToString() /// Properties/configuration passed in. /// The message logger to use. /// The global test run cancellation token. - internal TestContextImplementation(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger? messageLogger, TestRunCancellationToken? testRunCancellationToken) + internal TestContextImplementation(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger? messageLogger, TestRunCancellationToken? testRunCancellationToken) { // testMethod can be null when running ForceCleanup (done when reaching --maximum-failed-tests. DebugEx.Assert(properties != null, "properties is not null"); @@ -428,7 +427,7 @@ public void SetDisplayName(string? displayName) /// public override void DisplayMessage(MessageLevel messageLevel, string message) - => _messageLogger?.SendMessage(messageLevel.ToTestMessageLevel(), message); + => _messageLogger?.SendMessage(messageLevel, message); #endregion /// diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs index 6062851605..3a6bf32c98 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/TestContextImplementationTests.cs @@ -9,8 +9,8 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Moq; @@ -28,7 +28,7 @@ public class TestContextImplementationTests : TestContainer private TestContextImplementation _testContextImplementation = null!; - private TestContextImplementation CreateTestContextImplementation(IMessageLogger? messageLogger = null) + private TestContextImplementation CreateTestContextImplementation(IAdapterMessageLogger? messageLogger = null) => new(_testMethod.Object, null, _properties, messageLogger, null); public void TestContextConstructorShouldInitializeProperties() @@ -346,19 +346,19 @@ public void GetResultFilesShouldReturnListOfAddedResultFiles() public void DisplayMessageShouldForwardToIMessageLogger() { - var messageLoggerMock = new Mock(MockBehavior.Strict); + var messageLoggerMock = new Mock(MockBehavior.Strict); messageLoggerMock - .Setup(l => l.SendMessage(It.IsAny(), It.IsAny())); + .Setup(l => l.SendMessage(It.IsAny(), It.IsAny())); _testContextImplementation = CreateTestContextImplementation(messageLoggerMock.Object); _testContextImplementation.DisplayMessage(MessageLevel.Informational, "InfoMessage"); _testContextImplementation.DisplayMessage(MessageLevel.Warning, "WarningMessage"); _testContextImplementation.DisplayMessage(MessageLevel.Error, "ErrorMessage"); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Informational, "InfoMessage"), Times.Once); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Warning, "WarningMessage"), Times.Once); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Error, "ErrorMessage"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Informational, "InfoMessage"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Warning, "WarningMessage"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Error, "ErrorMessage"), Times.Once); } public void GetAndClearOutput_ShouldReturnContentThenClearBuffer() @@ -399,7 +399,7 @@ public void GetAndClearTrace_ShouldReturnContentThenClearBuffer() public void WritesFromBackgroundThreadShouldNotThrow() { - TestContextImplementation testContextImplementation = CreateTestContextImplementation(new Mock().Object); + TestContextImplementation testContextImplementation = CreateTestContextImplementation(new Mock().Object); var t = new Thread(() => { for (int i = 0; i < 100; i++) @@ -704,12 +704,12 @@ public void CloneForDataDrivenIterationShouldCopyTestRunCount() public void CloneForDataDrivenIterationShouldShareMessageLogger() { - var messageLoggerMock = new Mock(); + var messageLoggerMock = new Mock(); _testContextImplementation = CreateTestContextImplementation(messageLoggerMock.Object); TestContextImplementation clone = _testContextImplementation.CloneForDataDrivenIteration(); clone.DisplayMessage(MessageLevel.Informational, "from-clone"); - messageLoggerMock.Verify(x => x.SendMessage(TestMessageLevel.Informational, "from-clone"), Times.Once); + messageLoggerMock.Verify(x => x.SendMessage(MessageLevel.Informational, "from-clone"), Times.Once); } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs index 82126a12c3..729d98e436 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; using Moq; @@ -69,7 +68,7 @@ public IReflectionOperations ReflectionOperations public int GetTestContextCallCount { get; private set; } - public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IMessageLogger messageLogger, UnitTestOutcome outcome) + public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFullName, IDictionary properties, IAdapterMessageLogger messageLogger, UnitTestOutcome outcome) { GetTestContextCallCount++; var testContextImpl = new TestContextImplementation(testMethod, testClassFullName, properties, messageLogger, testRunCancellationToken: null); From 1a37ffa502fd9e7881629277dea6f72855509746 Mon Sep 17 00:00:00 2001 From: Amaury Leveque Date: Fri, 3 Jul 2026 18:39:37 +0200 Subject: [PATCH 11/24] Neutralize run-settings input in PlatformServices host layer (Phase 6c2) Replace the VSTest IRunSettings with a neutral settings-XML string through the isolation-host layer, removing IRunSettings from MSTestAdapter.PlatformServices. - IPlatformServiceProvider.CreateTestSourceHost, TestSourceHost (both ctors) and AssemblyEnumeratorWrapper.GetTests/GetTestsInIsolation now take string? settingsXml. - TestExecutionManager.CacheSessionParameters takes the settings-XML string directly. - Callers extract runContext?.RunSettings?.SettingsXml / discoveryContext?.RunSettings?.SettingsXml at the point they already had the (still VSTest) run/discovery context; only .SettingsXml (a string) was ever read off IRunSettings, so this is byte-for-byte. The remaining IRunContext/IDiscoveryContext usage is the test-case filter (deferred to the filter sub-phase). No behavior change: the appdomain DisableAppDomain decision and the run-parameter caching read the same settings XML as before. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Discovery/AssemblyEnumeratorWrapper.cs | 9 ++++----- .../Discovery/UnitTestDiscoverer.cs | 2 +- .../TestExecutionManager.Parallelization.cs | 2 +- .../TestExecutionManager.TestContext.cs | 7 +++---- .../Execution/TestExecutionManager.cs | 4 ++-- .../IPlatformServiceProvider.cs | 4 ++-- .../PlatformServiceProvider.cs | 6 +++--- .../Services/TestSourceHost.cs | 11 +++++------ .../DesktopTestSourceHostTests.cs | 16 +++++----------- .../Services/DesktopTestSourceHostTests.cs | 16 +++------------- .../TestablePlatformServiceProvider.cs | 2 +- 11 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs index 16e83134c8..454373159d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/AssemblyEnumeratorWrapper.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; @@ -23,7 +22,7 @@ internal sealed class AssemblyEnumeratorWrapper /// Gets test elements from an assembly. /// /// A collection of test elements. - internal static ICollection? GetTests(string? assemblyFileName, IRunSettings? runSettings, ITestSourceHandler testSourceHandler, bool isMTP, out List warnings) + internal static ICollection? GetTests(string? assemblyFileName, string? settingsXml, ITestSourceHandler testSourceHandler, bool isMTP, out List warnings) { warnings = []; @@ -48,7 +47,7 @@ internal sealed class AssemblyEnumeratorWrapper try { // Load the assembly in isolation if required. - AssemblyEnumerationResult result = GetTestsInIsolation(fullFilePath, runSettings, isMTP); + AssemblyEnumerationResult result = GetTestsInIsolation(fullFilePath, settingsXml, isMTP); warnings.AddRange(result.Warnings); return result.TestElements; } @@ -86,9 +85,9 @@ internal sealed class AssemblyEnumeratorWrapper } } - private static AssemblyEnumerationResult GetTestsInIsolation(string fullFilePath, IRunSettings? runSettings, bool isMTP) + private static AssemblyEnumerationResult GetTestsInIsolation(string fullFilePath, string? settingsXml, bool isMTP) { - using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(fullFilePath, runSettings); + using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(fullFilePath, settingsXml); // Create an instance of a type defined in adapter so that adapter gets loaded in the child app domain var assemblyEnumerator = (AssemblyEnumerator)isolationHost.CreateInstanceForType(typeof(AssemblyEnumerator), [MSTestSettings.CurrentSettings])!; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs index 05ce24f292..caa53d2aad 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs @@ -56,7 +56,7 @@ internal virtual void DiscoverTestsInSource( IDiscoveryContext? discoveryContext, bool isMTP) { - ICollection? testElements = AssemblyEnumeratorWrapper.GetTests(source, discoveryContext?.RunSettings, _testSource, isMTP, out List warnings); + ICollection? testElements = AssemblyEnumeratorWrapper.GetTests(source, discoveryContext?.RunSettings?.SettingsXml, _testSource, isMTP, out List warnings); if (MSTestSettings.CurrentSettings.TreatDiscoveryWarningsAsErrors) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 8d9590224a..06f1cb5f5f 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -65,7 +65,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IAdapterMessageLogger adapterMessageLogger = frameworkHandle.ToAdapterMessageLogger(); - using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, runContext?.RunSettings); + using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, runContext?.RunSettings?.SettingsXml); bool usesAppDomains = isolationHost is TestSourceHost { UsesAppDomain: true }; if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs index bbcc7ab9b8..1f2f0677c0 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs @@ -5,7 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -99,16 +98,16 @@ private static void ValidateAndAssignTestProperty( } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle errors in user specified run parameters")] - private void CacheSessionParameters(IRunContext? runContext, IAdapterMessageLogger messageLogger) + private void CacheSessionParameters(string? settingsXml, IAdapterMessageLogger messageLogger) { - if (StringEx.IsNullOrEmpty(runContext?.RunSettings?.SettingsXml)) + if (StringEx.IsNullOrEmpty(settingsXml)) { return; } try { - Dictionary? testRunParameters = RunSettingsUtilities.GetTestRunParameters(runContext.RunSettings.SettingsXml); + Dictionary? testRunParameters = RunSettingsUtilities.GetTestRunParameters(settingsXml); if (testRunParameters != null) { // Clear sessionParameters to prevent key collisions of test run parameters in case diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index c67e7f8973..f0722e7cb5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -110,7 +110,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext, frameworkHandle.ToAdapterMessageLogger()); + CacheSessionParameters(runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger()); // Execute the tests await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); @@ -154,7 +154,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext, frameworkHandle.ToAdapterMessageLogger()); + CacheSessionParameters(runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger()); // Run tests. await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs index 315567e42b..8537dd59e7 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/IPlatformServiceProvider.cs @@ -66,7 +66,7 @@ internal interface IPlatformServiceProvider /// /// The source. /// - /// + /// /// The run Settings for the session. /// /// @@ -74,7 +74,7 @@ internal interface IPlatformServiceProvider /// ITestSourceHost CreateTestSourceHost( string source, - TestPlatform.ObjectModel.Adapter.IRunSettings? runSettings); + string? settingsXml); /// /// Gets the TestContext object for a platform. diff --git a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs index be07d3c0ad..aa460a8231 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/PlatformServiceProvider.cs @@ -129,7 +129,7 @@ internal static IPlatformServiceProvider Instance /// /// The source. /// - /// + /// /// The run Settings for the session. /// /// @@ -137,9 +137,9 @@ internal static IPlatformServiceProvider Instance /// public ITestSourceHost CreateTestSourceHost( string source, - TestPlatform.ObjectModel.Adapter.IRunSettings? runSettings) + string? settingsXml) { - var testSourceHost = new TestSourceHost(source, runSettings); + var testSourceHost = new TestSourceHost(source, settingsXml); testSourceHost.SetupHost(); return testSourceHost; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs index 7ff1be7380..d2e5960d17 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs @@ -11,7 +11,6 @@ #if NETFRAMEWORK using Microsoft.VisualStudio.TestPlatform.ObjectModel; #endif -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; #if NETFRAMEWORK || (NET && !WINDOWS_UWP) using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; #endif @@ -66,10 +65,10 @@ internal class TestSourceHost : ITestSourceHost /// Initializes a new instance of the class. /// /// The source file name. - /// The run-settings provided for this session. - public TestSourceHost(string sourceFileName, IRunSettings? runSettings) + /// The run-settings XML provided for this session. + public TestSourceHost(string sourceFileName, string? settingsXml) #if NETFRAMEWORK - : this(sourceFileName, runSettings, new AppDomainWrapper()) + : this(sourceFileName, settingsXml, new AppDomainWrapper()) #endif { #if !WINDOWS_UWP && !NETFRAMEWORK @@ -81,7 +80,7 @@ public TestSourceHost(string sourceFileName, IRunSettings? runSettings) } #if NETFRAMEWORK - internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDomain appDomain) + internal TestSourceHost(string sourceFileName, string? settingsXml, IAppDomain appDomain) { _sourceFileName = sourceFileName; _appDomain = appDomain; @@ -90,7 +89,7 @@ internal TestSourceHost(string sourceFileName, IRunSettings? runSettings, IAppDo SetContext(sourceFileName); // Set isAppDomainCreationDisabled flag - _isAppDomainCreationDisabled = runSettings != null && MSTestAdapterSettings.IsAppDomainCreationDisabled(runSettings.SettingsXml); + _isAppDomainCreationDisabled = settingsXml is not null && MSTestAdapterSettings.IsAppDomainCreationDisabled(settingsXml); } /// diff --git a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs index f052814199..aafd28b3a1 100644 --- a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs +++ b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/DesktopTestSourceHostTests.cs @@ -5,11 +5,8 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -using Moq; - using TestFramework.ForTestingMSTest; namespace PlatformServices.Desktop.ComponentTests; @@ -37,9 +34,10 @@ public void ParentDomainShouldHonorSearchDirectoriesSpecifiedInRunsettings() """; + LoadMSTestSettings(runSettingsXml); _testSourceHost = new TestSourceHost( GetTestAssemblyPath("DesktopTestProjectx86Debug"), - GetMockedIRunSettings(runSettingsXml).Object); + runSettingsXml); _testSourceHost.SetupHost(); // Loading SampleProjectForAssemblyResolution.dll should not throw. @@ -67,9 +65,10 @@ public void ChildDomainResolutionPathsShouldHaveSearchDirectoriesSpecifiedInRuns """; + LoadMSTestSettings(runSettingsXml); _testSourceHost = new TestSourceHost( GetTestAssemblyPath("DesktopTestProjectx86Debug"), - GetMockedIRunSettings(runSettingsXml).Object); + runSettingsXml); _testSourceHost.SetupHost(); var asm = Assembly.LoadFrom(sampleProjectPath); @@ -125,17 +124,12 @@ private static string GetTestAssemblyPath(string assetName) return testAssetPath; } - private static Mock GetMockedIRunSettings(string runSettingsXml) + private static void LoadMSTestSettings(string runSettingsXml) { - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - StringReader stringReader = new(runSettingsXml); var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); MSTestSettingsProvider mstestSettingsProvider = new(); reader.ReadToFollowing("MSTestV2"); mstestSettingsProvider.Load(reader); - - return mockRunSettings; } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs index c30b8edf73..e40ca50f4d 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/DesktopTestSourceHostTests.cs @@ -8,7 +8,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Moq; @@ -135,10 +134,7 @@ public void SetupHostShouldHaveParentDomainsAppBaseSetToTestSourceLocation() """; string location = typeof(TestSourceHost).Assembly.Location; - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - - TestSourceHost sourceHost = new(location, mockRunSettings.Object); + TestSourceHost sourceHost = new(location, runSettingsXml); try { @@ -189,10 +185,7 @@ public void NoAppDomainShouldGetCreatedWhenDisableAppDomainIsSetToTrue() "; string location = typeof(TestSourceHost).Assembly.Location; - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - - Mock testSourceHost = new(location, mockRunSettings.Object) { CallBase = true }; + Mock testSourceHost = new(location, runSettingsXml) { CallBase = true }; try { @@ -220,10 +213,7 @@ public void AppDomainShouldGetCreatedWhenDisableAppDomainIsSetToFalse() """; string location = typeof(TestSourceHost).Assembly.Location; - var mockRunSettings = new Mock(); - mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - - Mock testSourceHost = new(location, mockRunSettings.Object) { CallBase = true }; + Mock testSourceHost = new(location, runSettingsXml) { CallBase = true }; try { diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs index 729d98e436..57abe65278 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs @@ -76,7 +76,7 @@ public ITestContext GetTestContext(ITestMethod? testMethod, string? testClassFul return testContextImpl; } - public ITestSourceHost CreateTestSourceHost(string source, TestPlatform.ObjectModel.Adapter.IRunSettings? runSettings) => MockTestSourceHost.Object; + public ITestSourceHost CreateTestSourceHost(string source, string? settingsXml) => MockTestSourceHost.Object; public void SetupMockReflectionOperations() { From dc4c4d8f63d6e7e7baab5ba1861ce2e565b3755d Mon Sep 17 00:00:00 2001 From: Amaury Leveque Date: Fri, 3 Jul 2026 19:14:27 +0200 Subject: [PATCH 12/24] Move test-case filter parsing to adapter boundary (Phase 6d-1) Move TestMethodFilter (and its nested TestElementFilter) out of MSTestAdapter.PlatformServices up into MSTest.TestAdapter, and inject the neutral ITestElementFilter into the engine and discoverer via a new ITestElementFilterProvider abstraction. - New neutral ITestElementFilterProvider (PlatformServices.Interface): the boundary builds it (TestElementFilterProvider, closing over the VSTest IRunContext/IDiscoveryContext) and passes it into TestExecutionManager.RunTestsAsync/ExecuteTestsAsync and UnitTestDiscoverer.DiscoverTests. - The engine/discoverer invoke the provider at the EXACT points they previously built the filter (per source), so filter parse-error reporting keeps the same timing and per-source semantics; TestElementFilter.Matches still does element.ToTestCase() (byte-for-byte; #9568 deferred). - This removes ITestCaseFilterExpression / GetTestCaseFilter / MatchTestCase / the VSTest TestProperty filter set from PlatformServices code. IRunContext/IDiscoveryContext remain only for deployment + settings extraction (removed in a follow-up). No behavior change: filtered set/order and the discovery/execution filterHasError bail-out are identical; TestCaseFilteringTests (out-of-proc filter regression net) stays green. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TestMethodFilter.cs | 17 +++++ .../VSTestAdapter/MSTestDiscoverer.cs | 2 +- .../VSTestAdapter/MSTestExecutor.cs | 4 +- .../Discovery/UnitTestDiscoverer.cs | 21 +++---- .../TestExecutionManager.Parallelization.cs | 7 ++- .../Execution/TestExecutionManager.cs | 18 +++--- .../Interfaces/ITestElementFilterProvider.cs | 31 ++++++++++ .../Utilities/CLITestBase.discovery.cs | 6 +- .../Discovery/UnitTestDiscovererTests.cs | 25 ++++---- .../Execution/TestExecutionManagerTests.cs | 62 +++++++++---------- 10 files changed, 123 insertions(+), 70 deletions(-) rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/TestMethodFilter.cs (90%) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilterProvider.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs b/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs similarity index 90% rename from src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs rename to src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs index b10d7d9130..a2d0f20162 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/TestMethodFilter.cs +++ b/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs @@ -182,3 +182,20 @@ public bool Matches(UnitTestElement testElement) } } } + +/// +/// Adapter-boundary that closes over the VSTest discovery/run context +/// and produces the neutral via . Created at the +/// boundary and passed into the platform services engine/discoverer so the filter (and any parse-error report) +/// is built at the exact point the engine needs it, preserving the original per-source timing. +/// +internal sealed class TestElementFilterProvider : ITestElementFilterProvider +{ + private readonly TestMethodFilter _testMethodFilter = new(); + private readonly IDiscoveryContext? _context; + + public TestElementFilterProvider(IDiscoveryContext? context) => _context = context; + + public ITestElementFilter? GetTestElementFilter(IAdapterMessageLogger logger, out bool filterHasError) + => _testMethodFilter.GetTestElementFilter(_context, logger, out filterHasError); +} diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs index b57182075e..8bb3548db8 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs @@ -87,7 +87,7 @@ internal async Task DiscoverTestsAsync(IEnumerable sources, IDiscoveryCo IAdapterMessageLogger adapterLogger = logger.ToAdapterMessageLogger(); if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, adapterLogger, configuration, _testSourceHandler)) { - new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext, isMTP); + new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext, new TestElementFilterProvider(discoveryContext), isMTP); } } finally diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs index e8ce1faca9..9f718aefd6 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs @@ -156,7 +156,7 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run // execution engine reports results through this neutral recorder and never constructs VSTest results. ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, runContext, frameworkHandle, testResultRecorder, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, runContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { @@ -214,7 +214,7 @@ internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? run // execution engine reports results through this neutral recorder and never constructs VSTest results. ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testResultRecorder, testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs index caa53d2aad..11c94f1180 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs @@ -12,13 +12,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; [SuppressMessage("Performance", "CA1852: Seal internal types", Justification = "Overrides required for testability")] internal class UnitTestDiscoverer { - private readonly TestMethodFilter _testMethodFilter; - - internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) - { - _testMethodFilter = new TestMethodFilter(); - _testSource = testSourceHandler; - } + internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) => _testSource = testSourceHandler; /// /// Discovers the tests available from the provided sources. @@ -27,17 +21,19 @@ internal UnitTestDiscoverer(ITestSourceHandler testSourceHandler) /// The logger. /// The discovery Sink. /// The discovery context. + /// Provider for the test filter, or for no filter. /// Flag set to true when the platform running discovery is MTP. internal void DiscoverTests( IEnumerable sources, IAdapterMessageLogger logger, IUnitTestElementSink discoverySink, IDiscoveryContext discoveryContext, + ITestElementFilterProvider? filterProvider, bool isMTP) { foreach (string source in sources) { - DiscoverTestsInSource(source, logger, discoverySink, discoveryContext, isMTP); + DiscoverTestsInSource(source, logger, discoverySink, discoveryContext, filterProvider, isMTP); } } @@ -48,12 +44,14 @@ internal void DiscoverTests( /// The logger. /// The discovery Sink. /// The discovery context. + /// Provider for the test filter, or for no filter. /// Flag set to true when the platform running discovery is MTP. internal virtual void DiscoverTestsInSource( string source, IAdapterMessageLogger logger, IUnitTestElementSink discoverySink, IDiscoveryContext? discoveryContext, + ITestElementFilterProvider? filterProvider, bool isMTP) { ICollection? testElements = AssemblyEnumeratorWrapper.GetTests(source, discoveryContext?.RunSettings?.SettingsXml, _testSource, isMTP, out List warnings); @@ -103,15 +101,16 @@ internal virtual void DiscoverTestsInSource( source); } - SendTestCases(testElements, discoverySink, discoveryContext, logger); + SendTestCases(testElements, discoverySink, filterProvider, logger); } private readonly ITestSourceHandler _testSource; - internal void SendTestCases(IEnumerable testElements, IUnitTestElementSink discoverySink, IDiscoveryContext? discoveryContext, IAdapterMessageLogger logger) + internal static void SendTestCases(IEnumerable testElements, IUnitTestElementSink discoverySink, ITestElementFilterProvider? filterProvider, IAdapterMessageLogger logger) { // Get filter and skip discovery in case filter expression has parsing error. - ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(discoveryContext, logger, out bool filterHasError); + bool filterHasError = false; + ITestElementFilter? filter = filterProvider?.GetTestElementFilter(logger, out filterHasError); if (filterHasError) { return; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 06f1cb5f5f..f9c6925112 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -21,10 +21,12 @@ internal partial class TestExecutionManager /// The run context. /// Handle to record test start/end/results. /// Recorder used to report test results back to the host. + /// Provider for the test filter, or for no filter. /// Indicates if deployment is done. - internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, bool isDeploymentDone) + internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { _testResultRecorder = testResultRecorder; + _testElementFilterProvider = filterProvider; InitializeRandomTestOrder(frameworkHandle.ToAdapterMessageLogger()); @@ -74,7 +76,8 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, } // Default test set is filtered tests based on user provided filter criteria - ITestElementFilter? filter = _testMethodFilter.GetTestElementFilter(runContext, adapterMessageLogger, out bool filterHasError); + bool filterHasError = false; + ITestElementFilter? filter = _testElementFilterProvider?.GetTestElementFilter(adapterMessageLogger, out filterHasError); if (filterHasError) { // Bail out without processing everything else below. diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index f0722e7cb5..e5ea313a5d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -53,7 +53,6 @@ public TestExecutionManager() internal TestExecutionManager(Func, Task>? taskFactory = null) { - _testMethodFilter = new TestMethodFilter(); _sessionParameters = new Dictionary(); _taskFactory = taskFactory ?? DefaultFactoryAsync; } @@ -74,9 +73,11 @@ private static Task DefaultFactoryAsync(Func taskGetter) } /// - /// Gets or sets method filter for filtering tests. + /// Provider for the test filter, supplied at the adapter boundary and set once per run in + /// so the filter is built at the same point (and with the same per-source + /// timing) as before. /// - private readonly TestMethodFilter _testMethodFilter; + private ITestElementFilterProvider? _testElementFilterProvider; #if !WINDOWS_UWP && !WIN_UI /// @@ -92,8 +93,9 @@ private static Task DefaultFactoryAsync(Func taskGetter) /// Context to use when executing the tests. /// Handle to the framework to record results and to do framework operations. /// Recorder used to report test results back to the host. + /// Provider for the test filter, or for no filter. /// Test run cancellation token. - internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, TestRunCancellationToken runCancellationToken) + internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, TestRunCancellationToken runCancellationToken) { DebugEx.Assert(tests != null, "tests"); DebugEx.Assert(runContext != null, "runContext"); @@ -113,7 +115,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex CacheSessionParameters(runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger()); // Execute the tests - await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -123,7 +125,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex #endif } - internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) + internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) { _testRunCancellationToken = cancellationToken; PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; @@ -140,7 +142,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC _testRunCancellationToken?.ThrowIfCancellationRequested(); // discover the tests - GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, runContext, isMTP); + GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, runContext, filterProvider, isMTP); tests.AddRange(discoverySink.TestElements); // Clear discoverSinksTests so that it just stores test for one source at one point of time @@ -157,7 +159,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC CacheSessionParameters(runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger()); // Run tests. - await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilterProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilterProvider.cs new file mode 100644 index 0000000000..202b395383 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Interfaces/ITestElementFilterProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +/// +/// Platform-agnostic factory that produces the in effect for the current +/// run or discovery. +/// +/// +/// This abstraction lets the platform services discovery and execution pipelines obtain the active filter +/// without taking a dependency on a specific test platform's filter object model (for example the VSTest +/// ITestCaseFilterExpression parsed from an IRunContext / IDiscoveryContext). The concrete +/// provider is created at the adapter boundary, closing over the host filter context; the engine invokes it at +/// the exact point it needs the filter so parse-error reporting keeps the same timing and per-source semantics. +/// +internal interface ITestElementFilterProvider +{ + /// + /// Builds the for the current run/discovery. + /// + /// Logger used to report a filter parsing error. + /// + /// Set to when the filter is unsupported or failed to parse; in that case the caller + /// should report no tests for the affected source. + /// + /// + /// The filter to apply, or when no filter applies (every element is included). + /// + ITestElementFilter? GetTestElementFilter(IAdapterMessageLogger logger, out bool filterHasError); +} diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index 4ac356c178..72d364219b 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -30,7 +30,7 @@ internal static ImmutableArray DiscoverTests(string assemblyPath, stri string runSettingsXml = GetRunSettingsXml(string.Empty); var context = new InternalDiscoveryContext(runSettingsXml, testCaseFilter); - unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink.ToUnitTestElementSink(), context, false); + unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink.ToUnitTestElementSink(), context, new TestElementFilterProvider(context), false); return sink.DiscoveredTests; } @@ -41,7 +41,7 @@ internal static async Task> RunTestsAsync(IEnumerable var frameworkHandle = new InternalFrameworkHandle(); ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), null, frameworkHandle, testResultRecorder, false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), null, frameworkHandle, testResultRecorder, filterProvider: null, false); return frameworkHandle.GetFlattenedTestResults(); } @@ -54,7 +54,7 @@ internal static async Task> RunTestsAsync(IEnumerable var runContext = new InternalRunContext(runSettingsXml, testCaseFilter); ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), runContext, frameworkHandle, testResultRecorder, false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), runContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), false); return frameworkHandle.GetFlattenedTestResults(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs index b15605e885..0febd7b5ef 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -80,7 +80,7 @@ public void DiscoverTestsShouldThrowOnFileNotFound() .Returns(false); } - Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, sources[0])); } @@ -93,7 +93,7 @@ public void DiscoverTestsInSourceShouldThrowOnFileNotFound() _testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.DoesFileExist(Source)) .Returns(false); - Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); + Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, Source)); } @@ -125,7 +125,7 @@ public void DiscoverTestsInSourceShouldSendBackTestCasesDiscovered() MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.AtLeastOnce); @@ -160,7 +160,7 @@ public void DiscoverTestsInSourceShouldThrowWhenTreatDiscoveryWarningsAsErrorsIs MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); + Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Assert action.Should().Throw() @@ -199,7 +199,7 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError // Act & Assert // Should not throw an exception - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Verify warning message was sent to logger (not error) _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.AtLeastOnce); @@ -209,7 +209,7 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError public void SendTestCasesShouldNotSendAnyTestCasesIfThereAreNoTestElements() { // There is a null check for testElements in the code flow before this function call. So not adding a unit test for that. - _unitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); + UnitTestDiscoverer.SendTestCases(new List { }, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(_mockDiscoveryContext.Object), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.Never); @@ -221,7 +221,7 @@ public void SendTestCasesShouldSendAllTestCaseData() var test2 = new UnitTestElement(CreateTestMethod("M2", "C", "A", displayName: null)); var testElements = new List { test1, test2 }; - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger()); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(_mockDiscoveryContext.Object), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -240,7 +240,7 @@ public void SendTestCasesShouldSendFilteredTestCasesIfValidFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -259,7 +259,7 @@ public void SendTestCasesShouldSendAllTestCasesIfNullFilterExpression() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -278,7 +278,7 @@ public void SendTestCasesShouldSendAllTestCasesIfGetTestCaseFilterNotPresent() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Once); @@ -297,7 +297,7 @@ public void SendTestCasesShouldNotSendAnyTestCasesIfFilterError() var testElements = new List { test1, test2 }; // Action - _unitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), discoveryContext, _mockMessageLogger.Object.ToAdapterMessageLogger()); + UnitTestDiscoverer.SendTestCases(testElements, _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), new TestElementFilterProvider(discoveryContext), _mockMessageLogger.Object.ToAdapterMessageLogger()); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.Is(tc => tc.FullyQualifiedName == "C.M1")), Times.Never); @@ -328,6 +328,7 @@ internal override void DiscoverTestsInSource( IAdapterMessageLogger logger, IUnitTestElementSink discoverySink, IDiscoveryContext? discoveryContext, + ITestElementFilterProvider? filterProvider, bool isMTP) { var testElement1 = new UnitTestElement(new TestMethod("A", "C", source, displayName: null)); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 57341c0d04..b7a9d9fe3d 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -100,7 +100,7 @@ public async Task RunTestsForTestWithFilterErrorShouldSendZeroResults() // Causing the FilterExpressionError _runContext = new TestableRunContextTestExecutionTests(() => throw new TestPlatformFormatException()); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // No Results _frameworkHandle.TestCaseStartList.Count.Should().Be(0); @@ -116,7 +116,7 @@ public async Task RunTestsForTestWithFilterShouldSendResultsForFilteredTests() _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(p => p.DisplayName == "PassingTest")); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // FailingTest should be skipped because it does not match the filter criteria. List expectedTestCaseStartList = ["PassingTest"]; @@ -144,7 +144,7 @@ public async Task RunTestsForIgnoredTestShouldSendResultsMarkingIgnoredTestsAsSk TestCase testCase = GetTestCase(typeof(DummyTestClass), "IgnoredTest"); TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); _frameworkHandle.TestCaseStartList[0].Should().Be("IgnoredTest"); _frameworkHandle.TestCaseEndList[0].Should().Be("IgnoredTest:Skipped"); @@ -157,7 +157,7 @@ public async Task RunTestsForASingleTestShouldSendSingleResult() TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); List expectedTestCaseStartList = ["PassingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed"]; @@ -174,7 +174,7 @@ public async Task RunTestsForMultipleTestShouldSendMultipleResults() TestCase failingTestCase = GetTestCase(typeof(DummyTestClass), "FailingTest"); TestCase[] tests = [testCase, failingTestCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); List expectedTestCaseStartList = ["PassingTest", "FailingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed", "FailingTest:Failed"]; @@ -194,7 +194,7 @@ public async Task RunTestsForCancellationTokenCanceledSetToTrueShouldSendZeroRes // Cancel the test run _cancellationToken.Cancel(); - Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, _cancellationToken); + Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); await func.Should().ThrowAsync(); // No Results @@ -218,7 +218,7 @@ await _testExecutionManager.RunTestsAsync( ToUnitTestElements(tests), _runContext, _frameworkHandle, - TestResultRecorder, new TestRunCancellationToken()); + TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _callers[0].Should().Be("Deploy", "Deploy should be called before execution."); _callers[1].Should().Be("LoadAssembly", "Deploy should be called before execution."); @@ -238,7 +238,7 @@ public async Task RunTestsForTestShouldCleanupAfterExecution() td => td.Cleanup()).Callback(() => SetCaller("Cleanup")); #endif - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _callers[0].Should().Be("LoadAssembly", "Cleanup should be called after execution."); @@ -255,7 +255,7 @@ public async Task RunTestsForTestShouldNotCleanupOnTestFailure() TestCase[] tests = [testCase, failingTestCase]; TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockTestDeployment.Verify(td => td.Cleanup(), Times.Never); } @@ -274,7 +274,7 @@ public async Task RunTestsForTestShouldLoadSourceFromDeploymentDirectoryIfDeploy testablePlatformService.MockTestDeployment.Setup(td => td.GetDeploymentDirectory()) .Returns(@"C:\temp"); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockFileOperations.Verify( fo => fo.LoadAssembly(It.Is(s => s.StartsWith("C:\\temp"))), @@ -300,7 +300,7 @@ public async Task RunTestsForTestShouldPassInTestRunParametersInformationAsPrope """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -322,7 +322,7 @@ public async Task RunTestsForTestShouldPassInTcmPropertiesAsPropertiesToTheTest( """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); VerifyTcmProperties(DummyTestClass.TestContextProperties, testCase); } @@ -335,7 +335,7 @@ public async Task RunTestsForTestShouldPassInDeploymentInformationAsPropertiesTo // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockSettingsProvider.Verify(sp => sp.GetProperties(It.IsAny()), Times.Once); } @@ -359,7 +359,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger First Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Update runsettings to have different values for similar keys _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -376,7 +376,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger another Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); "http://updatedLocalHost".Equals(DummyTestClass.TestContextProperties!["webAppUrl"]).Should().BeTrue(); } @@ -390,7 +390,7 @@ private async Task RunTestsForSourceShouldRunTestsInASource() { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); _frameworkHandle.TestCaseStartList.Contains("PassingTest").Should().BeTrue(); _frameworkHandle.TestCaseEndList.Contains("PassingTest:Passed").Should().BeTrue(); @@ -412,7 +412,7 @@ private async Task RunTestsForSourceShouldPassInTestRunParametersInformationAsPr """); - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -423,7 +423,7 @@ private async Task RunTestsForSourceShouldPassInDeploymentInformationAsPropertie { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties.Should().NotBeNull(); } @@ -437,7 +437,7 @@ public async Task RunTestsForMultipleSourcesShouldRunEachTestJustOnce() ExecuteTestsWrapper = (tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone) => testsCount += tests.Count(), }; - await testableTestExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, _mockTestSourceHandler.Object, false, _cancellationToken); + await testableTestExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); testsCount.Should().Be(4); } @@ -470,7 +470,7 @@ public async Task RunTestsForTestShouldRunTestsInParallelWhenEnabledInRunsetting try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -507,7 +507,7 @@ public async Task RunTestsForTestShouldRunTestsByMethodLevelWhenSpecified() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -544,7 +544,7 @@ public async Task RunTestsForTestShouldRunTestsWithSpecifiedNumberOfWorkers() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -607,7 +607,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -666,7 +666,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -705,7 +705,7 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); DummyTestClassWithDoNotParallelizeMethods.ParallelizableTestsThreadIds.Count.Should().BeOneOf(1, 2); @@ -771,7 +771,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -811,7 +811,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState try { MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.Count.Should().Be(1); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.ToArray()[0].Should().Be(Thread.CurrentThread.GetApartmentState()); @@ -854,11 +854,11 @@ static TestCase[] BuildTests() => var firstHandle = new TestableFrameworkHandle(); var firstManager = new TestExecutionManager(task => task()); - await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, firstHandle, firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestRunCancellationToken()); + await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, firstHandle, firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); var secondHandle = new TestableFrameworkHandle(); var secondManager = new TestExecutionManager(task => task()); - await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, secondHandle, secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestRunCancellationToken()); + await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, secondHandle, secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Same seed must produce the same order across separate runs. firstHandle.TestCaseStartList.Should().Equal(secondHandle.TestCaseStartList); @@ -895,7 +895,7 @@ public async Task RunTestsWithoutRandomizeTestOrderShouldPreserveInputOrder() MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _frameworkHandle.TestCaseStartList.Should().Equal("TestA", "TestB", "TestC"); } @@ -1273,7 +1273,7 @@ internal class TestableTestExecutionManager : TestExecutionManager { internal Action, IRunContext?, IFrameworkHandle, ITestResultRecorder, bool> ExecuteTestsWrapper { get; set; } = null!; - internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, bool isDeploymentDone) + internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { ExecuteTestsWrapper?.Invoke(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone); return Task.CompletedTask; From 7caa937f3e486fd894b68c0ec2dcc5d5d25097fa Mon Sep 17 00:00:00 2001 From: Amaury Leveque Date: Sun, 5 Jul 2026 11:43:56 +0200 Subject: [PATCH 13/24] Remove VSTest run/discovery context from PlatformServices (Phase 6d-2) Replace the VSTest IRunContext/IDiscoveryContext with neutral primitives extracted at the adapter boundary, so MSTestAdapter.PlatformServices no longer references either type. - Execution: MSTestExecutor builds the neutral DeploymentContext (test-run directory + run settings XML) from the host run context and injects it into TestExecutionManager.RunTestsAsync/ ExecuteTestsAsync/ExecuteTestsInSourceAsync/Deploy (DeploymentContext un-guarded so it is the single execution-inputs carrier on all TFMs). - Discovery: MSTestDiscoverer passes the run settings XML string into UnitTestDiscoverer. DiscoverTests/DiscoverTestsInSource; MSTestDiscovererHelpers.InitializeDiscovery and MSTestSettings.PopulateSettings take string? settingsXml. - Only .SettingsXml + .TestRunDirectory were ever read off the contexts, so this is byte-for-byte. IRunContext/IDiscoveryContext are now absent from PlatformServices code (doc comments only); the remaining ObjectModel.Adapter surface is the IFrameworkHandle-backed deploy/recorder/logger handles. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../VSTestAdapter/MSTestDiscoverer.cs | 4 +- .../VSTestAdapter/MSTestExecutor.cs | 17 +++- .../Deployment/DeploymentContext.cs | 10 +- .../Discovery/UnitTestDiscoverer.cs | 13 ++- .../TestExecutionManager.Parallelization.cs | 13 +-- .../Execution/TestExecutionManager.cs | 36 +++---- .../Helpers/MSTestDiscovererHelpers.cs | 5 +- .../MSTestSettings.Configuration.cs | 25 +++-- .../Utilities/CLITestBase.discovery.cs | 7 +- .../Discovery/TypeEnumeratorTests.cs | 6 +- .../Discovery/UnitTestDiscovererTests.cs | 18 ++-- .../Execution/TestExecutionManagerTests.cs | 95 ++++++++++--------- .../MSTestSettingsTests.cs | 26 ++--- .../RunConfigurationSettingsTests.cs | 10 +- 14 files changed, 145 insertions(+), 140 deletions(-) diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs index 8bb3548db8..ebe26f397c 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs @@ -85,9 +85,9 @@ internal async Task DiscoverTestsAsync(IEnumerable sources, IDiscoveryCo try { IAdapterMessageLogger adapterLogger = logger.ToAdapterMessageLogger(); - if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, adapterLogger, configuration, _testSourceHandler)) + if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext?.RunSettings?.SettingsXml, adapterLogger, configuration, _testSourceHandler)) { - new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext, new TestElementFilterProvider(discoveryContext), isMTP); + new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, adapterLogger, discoverySink.ToUnitTestElementSink(), discoveryContext?.RunSettings?.SettingsXml, new TestElementFilterProvider(discoveryContext), isMTP); } } finally diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs index 9f718aefd6..0db5ce45f8 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs @@ -6,6 +6,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.VSTestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Resources; @@ -141,7 +142,7 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run try { - if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext, frameworkHandle.ToAdapterMessageLogger(), configuration, new TestSourceHandler())) + if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger(), configuration, new TestSourceHandler())) { return; } @@ -156,7 +157,11 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run // execution engine reports results through this neutral recorder and never constructs VSTest results. ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, runContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + // Extract the neutral run inputs (test-run directory + run settings XML) from the host run context at + // this boundary so the execution engine no longer depends on the VSTest run context. + var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); + + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, deploymentContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { @@ -203,7 +208,7 @@ internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? run try { TestSourceHandler testSourceHandler = new(); - if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext, frameworkHandle.ToAdapterMessageLogger(), configuration, testSourceHandler)) + if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger(), configuration, testSourceHandler)) { return; } @@ -214,7 +219,11 @@ internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? run // execution engine reports results through this neutral recorder and never constructs VSTest results. ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + // Extract the neutral run inputs (test-run directory + run settings XML) from the host run context at + // this boundary so the execution engine no longer depends on the VSTest run context. + var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); + + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, deploymentContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs index b8a40efd53..e6c35de10c 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Deployment/DeploymentContext.cs @@ -1,14 +1,12 @@ // 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 && !WIN_UI - namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; /// -/// Platform-agnostic inputs the deployment pipeline needs from the running test host: the test-run/results -/// directory and the run settings XML. This lets the platform services deployment layer run without taking a -/// dependency on a specific test platform's run-context object model (for example the VSTest +/// Platform-agnostic inputs the execution pipeline needs from the running test host: the test-run/results +/// directory and the run settings XML. This lets the platform services execution and deployment layers run +/// without taking a dependency on a specific test platform's run-context object model (for example the VSTest /// IRunContext / IRunSettings types). It is populated at the adapter boundary. /// internal sealed class DeploymentContext @@ -30,5 +28,3 @@ public DeploymentContext(string? testRunDirectory, string? runSettingsXml) /// public string? RunSettingsXml { get; } } - -#endif diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs index 11c94f1180..1ae8056ed7 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Discovery/UnitTestDiscoverer.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -20,20 +19,20 @@ internal class UnitTestDiscoverer /// The sources. /// The logger. /// The discovery Sink. - /// The discovery context. + /// The run settings XML, or when none was provided. /// Provider for the test filter, or for no filter. /// Flag set to true when the platform running discovery is MTP. internal void DiscoverTests( IEnumerable sources, IAdapterMessageLogger logger, IUnitTestElementSink discoverySink, - IDiscoveryContext discoveryContext, + string? settingsXml, ITestElementFilterProvider? filterProvider, bool isMTP) { foreach (string source in sources) { - DiscoverTestsInSource(source, logger, discoverySink, discoveryContext, filterProvider, isMTP); + DiscoverTestsInSource(source, logger, discoverySink, settingsXml, filterProvider, isMTP); } } @@ -43,18 +42,18 @@ internal void DiscoverTests( /// The source. /// The logger. /// The discovery Sink. - /// The discovery context. + /// The run settings XML, or when none was provided. /// Provider for the test filter, or for no filter. /// Flag set to true when the platform running discovery is MTP. internal virtual void DiscoverTestsInSource( string source, IAdapterMessageLogger logger, IUnitTestElementSink discoverySink, - IDiscoveryContext? discoveryContext, + string? settingsXml, ITestElementFilterProvider? filterProvider, bool isMTP) { - ICollection? testElements = AssemblyEnumeratorWrapper.GetTests(source, discoveryContext?.RunSettings?.SettingsXml, _testSource, isMTP, out List warnings); + ICollection? testElements = AssemblyEnumeratorWrapper.GetTests(source, settingsXml, _testSource, isMTP, out List warnings); if (MSTestSettings.CurrentSettings.TreatDiscoveryWarningsAsErrors) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index f9c6925112..379f5308b8 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -18,12 +19,12 @@ internal partial class TestExecutionManager /// Execute the parameter tests. /// /// Tests to execute. - /// The run context. + /// Host-provided test-run directory and run settings XML. /// Handle to record test start/end/results. /// Recorder used to report test results back to the host. /// Provider for the test filter, or for no filter. /// Indicates if deployment is done. - internal virtual async Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) + internal virtual async Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { _testResultRecorder = testResultRecorder; _testElementFilterProvider = filterProvider; @@ -42,7 +43,7 @@ group test by test.TestMethod.AssemblyName into testGroup foreach (var group in testsBySource) { _testRunCancellationToken?.ThrowIfCancellationRequested(); - await ExecuteTestsInSourceAsync(group.Tests, runContext, frameworkHandle, group.Source, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsInSourceAsync(group.Tests, deploymentContext, frameworkHandle, group.Source, isDeploymentDone).ConfigureAwait(false); } } @@ -50,11 +51,11 @@ group test by test.TestMethod.AssemblyName into testGroup /// Execute the parameter tests present in parameter source. /// /// Tests to execute. - /// The run context. + /// Host-provided test-run directory and run settings XML. /// Handle to record test start/end/results. /// The test container for the tests. /// Indicates if deployment is done. - private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, string source, bool isDeploymentDone) + private async Task ExecuteTestsInSourceAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, string source, bool isDeploymentDone) { DebugEx.Assert(!StringEx.IsNullOrEmpty(source), "Source cannot be empty"); @@ -67,7 +68,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, IAdapterMessageLogger adapterMessageLogger = frameworkHandle.ToAdapterMessageLogger(); - using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, runContext?.RunSettings?.SettingsXml); + using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, deploymentContext.RunSettingsXml); bool usesAppDomains = isolationHost is TestSourceHost { UsesAppDomain: true }; if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index e5ea313a5d..f08ddb0a52 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -3,9 +3,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -#if !WINDOWS_UWP && !WIN_UI using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; -#endif using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -90,15 +88,15 @@ private static Task DefaultFactoryAsync(Func taskGetter) /// Runs the tests. /// /// Tests to be run. - /// Context to use when executing the tests. + /// Host-provided test-run directory and run settings XML. /// Handle to the framework to record results and to do framework operations. /// Recorder used to report test results back to the host. /// Provider for the test filter, or for no filter. /// Test run cancellation token. - internal async Task RunTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, TestRunCancellationToken runCancellationToken) + internal async Task RunTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, TestRunCancellationToken runCancellationToken) { DebugEx.Assert(tests != null, "tests"); - DebugEx.Assert(runContext != null, "runContext"); + DebugEx.Assert(deploymentContext != null, "deploymentContext"); DebugEx.Assert(frameworkHandle != null, "frameworkHandle"); DebugEx.Assert(runCancellationToken != null, "runCancellationToken"); @@ -106,16 +104,16 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = Deploy(tests, runContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, deploymentContext, frameworkHandle); #else const bool isDeploymentDone = false; #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger()); + CacheSessionParameters(deploymentContext.RunSettingsXml, frameworkHandle.ToAdapterMessageLogger()); // Execute the tests - await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, deploymentContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -125,7 +123,7 @@ internal async Task RunTestsAsync(IEnumerable tests, IRunContex #endif } - internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) + internal async Task RunTestsAsync(IEnumerable sources, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) { _testRunCancellationToken = cancellationToken; PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; @@ -142,7 +140,7 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC _testRunCancellationToken?.ThrowIfCancellationRequested(); // discover the tests - GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, runContext, filterProvider, isMTP); + GetUnitTestDiscoverer(testSourceHandler).DiscoverTestsInSource(source, logger, discoverySink, deploymentContext.RunSettingsXml, filterProvider, isMTP); tests.AddRange(discoverySink.TestElements); // Clear discoverSinksTests so that it just stores test for one source at one point of time @@ -150,16 +148,16 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC } #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = Deploy(tests, runContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, deploymentContext, frameworkHandle); #else const bool isDeploymentDone = false; #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(runContext?.RunSettings?.SettingsXml, frameworkHandle.ToAdapterMessageLogger()); + CacheSessionParameters(deploymentContext.RunSettingsXml, frameworkHandle.ToAdapterMessageLogger()); // Run tests. - await ExecuteTestsAsync(tests, runContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, deploymentContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -171,15 +169,11 @@ internal async Task RunTestsAsync(IEnumerable sources, IRunContext? runC #if !WINDOWS_UWP && !WIN_UI /// - /// Runs deployment for the given tests via the platform-agnostic deployment service, translating the host - /// run context into the neutral at this call site. This is the single place - /// execution reads the VSTest run context for deployment (removed once the run context itself is neutralized). + /// Runs deployment for the given tests via the platform-agnostic deployment service using the neutral + /// supplied by the adapter boundary. /// - private static bool Deploy(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle) - { - var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); - return PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, deploymentContext, frameworkHandle.ToAdapterMessageLogger()); - } + private static bool Deploy(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle) + => PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, deploymentContext, frameworkHandle.ToAdapterMessageLogger()); #endif internal virtual UnitTestDiscoverer GetUnitTestDiscoverer(ITestSourceHandler testSourceHandler) => new(testSourceHandler); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs index 76ea3316ec..20b5ef982d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/MSTestDiscovererHelpers.cs @@ -3,7 +3,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -20,7 +19,7 @@ internal static class MSTestDiscovererHelpers internal static bool AreValidSources(IEnumerable sources, ITestSourceHandler testSourceHandler) => sources.Any(source => testSourceHandler.ValidSourceExtensions.Any(extension => string.Equals(Path.GetExtension(source), extension, StringComparison.OrdinalIgnoreCase))); - internal static bool InitializeDiscovery(IEnumerable sources, IDiscoveryContext? discoveryContext, IAdapterMessageLogger messageLogger, IConfiguration? configuration, ITestSourceHandler testSourceHandler) + internal static bool InitializeDiscovery(IEnumerable sources, string? settingsXml, IAdapterMessageLogger messageLogger, IConfiguration? configuration, ITestSourceHandler testSourceHandler) { if (!AreValidSources(sources, testSourceHandler)) { @@ -30,7 +29,7 @@ internal static bool InitializeDiscovery(IEnumerable sources, IDiscovery // Populate the runsettings. try { - MSTestSettings.PopulateSettings(discoveryContext, messageLogger, configuration); + MSTestSettings.PopulateSettings(settingsXml, messageLogger, configuration); return true; } catch (AdapterSettingsException ex) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs index 724ab28194..ee399d9ab5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.Configuration.cs @@ -10,7 +10,6 @@ #endif using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using MessageLevel = Microsoft.VisualStudio.TestTools.UnitTesting.MessageLevel; using StringEx = Microsoft.VisualStudio.TestTools.UnitTesting.StringEx; @@ -20,34 +19,34 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; internal sealed partial class MSTestSettings { /// - /// Populate adapter settings from the context. + /// Populate adapter settings from the run settings XML. /// - /// The discovery context. + /// The run settings XML, or when none was provided. /// The logger for messages. /// The configuration. - internal static void PopulateSettings(IDiscoveryContext? context, IAdapterMessageLogger? logger, IConfiguration? configuration) + internal static void PopulateSettings(string? settingsXml, IAdapterMessageLogger? logger, IConfiguration? configuration) { #if !WINDOWS_UWP if (configuration?["mstest"] is not null - && context?.RunSettings is not null - && RunSettingsFileHasMSTestSettings(context.RunSettings.SettingsXml)) + && settingsXml is not null + && RunSettingsFileHasMSTestSettings(settingsXml)) { throw new InvalidOperationException(Resource.DuplicateConfigurationError); } #endif var settings = new MSTestSettings(); - var runConfigurationSettings = RunConfigurationSettings.GetSettings(context?.RunSettings?.SettingsXml); + var runConfigurationSettings = RunConfigurationSettings.GetSettings(settingsXml); #if !WINDOWS_UWP - if (!StringEx.IsNullOrEmpty(context?.RunSettings?.SettingsXml) && configuration?["mstest"] is null) + if (!StringEx.IsNullOrEmpty(settingsXml) && configuration?["mstest"] is null) #else - if (!StringEx.IsNullOrEmpty(context?.RunSettings?.SettingsXml)) + if (!StringEx.IsNullOrEmpty(settingsXml)) #endif { - MSTestSettings? aliasSettings = GetSettings(context.RunSettings.SettingsXml, SettingsNameAlias, logger); - settings = aliasSettings ?? GetSettings(context.RunSettings.SettingsXml, SettingsName, logger) ?? new MSTestSettings(); - SetGlobalSettings(context.RunSettings.SettingsXml, settings, logger); + 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) @@ -71,7 +70,7 @@ internal static void PopulateSettings(IDiscoveryContext? context, IAdapterMessag { telemetry.ConfigurationSource = configuration?["mstest"] is not null ? "testconfig.json" - : !StringEx.IsNullOrEmpty(context?.RunSettings?.SettingsXml) + : !StringEx.IsNullOrEmpty(settingsXml) ? "runsettings" : "none"; } diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index 72d364219b..f80f61eeba 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -10,6 +10,7 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; @@ -30,7 +31,7 @@ internal static ImmutableArray DiscoverTests(string assemblyPath, stri string runSettingsXml = GetRunSettingsXml(string.Empty); var context = new InternalDiscoveryContext(runSettingsXml, testCaseFilter); - unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink.ToUnitTestElementSink(), context, new TestElementFilterProvider(context), false); + unitTestDiscoverer.DiscoverTestsInSource(assemblyPath, logger.ToAdapterMessageLogger(), sink.ToUnitTestElementSink(), runSettingsXml, new TestElementFilterProvider(context), false); return sink.DiscoveredTests; } @@ -41,7 +42,7 @@ internal static async Task> RunTestsAsync(IEnumerable var frameworkHandle = new InternalFrameworkHandle(); ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), null, frameworkHandle, testResultRecorder, filterProvider: null, false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(null, null), frameworkHandle, testResultRecorder, filterProvider: null, false); return frameworkHandle.GetFlattenedTestResults(); } @@ -54,7 +55,7 @@ internal static async Task> RunTestsAsync(IEnumerable var runContext = new InternalRunContext(runSettingsXml, testCaseFilter); ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), runContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(runContext.TestRunDirectory, runContext.RunSettings?.SettingsXml), frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), false); return frameworkHandle.GetFlattenedTestResults(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs index ed5bdcead3..a45bce3a57 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/TypeEnumeratorTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -121,7 +121,7 @@ public void GetTestsShouldReturnBaseTestMethodsFromAnotherAssemblyByDefault() mockRunContext.Setup(dc => dc.RunSettings).Returns(mockRunSettings.Object); mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(mockRunContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true); TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DummyDerivedTestClass), Assembly.GetExecutingAssembly().FullName!); @@ -151,7 +151,7 @@ public void GetTestsShouldReturnBaseTestMethodsFromAnotherAssemblyByConfiguratio mockRunContext.Setup(dc => dc.RunSettings).Returns(mockRunSettings.Object); mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(mockRunContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(mockRunContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); SetupTestClassAndTestMethods(isValidTestClass: true, isValidTestMethod: true); TypeEnumerator typeEnumerator = GetTypeEnumeratorInstance(typeof(DummyDerivedTestClass), Assembly.GetExecutingAssembly().FullName!); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs index 0febd7b5ef..361eb07e69 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Discovery/UnitTestDiscovererTests.cs @@ -80,7 +80,7 @@ public void DiscoverTestsShouldThrowOnFileNotFound() .Returns(false); } - Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); + Action act = () => _unitTestDiscoverer.DiscoverTests(sources, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, sources[0])); } @@ -93,7 +93,7 @@ public void DiscoverTestsInSourceShouldThrowOnFileNotFound() _testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.DoesFileExist(Source)) .Returns(false); - Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); + Action act = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); act.Should().Throw() .WithMessage(string.Format(CultureInfo.CurrentCulture, Resource.TestAssembly_FileDoesNotExist, Source)); } @@ -122,10 +122,10 @@ public void DiscoverTestsInSourceShouldSendBackTestCasesDiscovered() """); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Assert. _mockTestCaseDiscoverySink.Verify(ds => ds.SendTestCase(It.IsAny()), Times.AtLeastOnce); @@ -157,10 +157,10 @@ public void DiscoverTestsInSourceShouldThrowWhenTreatDiscoveryWarningsAsErrorsIs """; _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(settingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act - Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); + Action action = () => _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Assert action.Should().Throw() @@ -195,11 +195,11 @@ public void DiscoverTestsInSourceShouldNotThrowWhenTreatDiscoveryWarningsAsError """); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); // Act & Assert // Should not throw an exception - _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); + _unitTestDiscoverer.DiscoverTestsInSource(Source, _mockMessageLogger.Object.ToAdapterMessageLogger(), _mockTestCaseDiscoverySink.Object.ToUnitTestElementSink(), _mockDiscoveryContext.Object.RunSettings?.SettingsXml, new TestElementFilterProvider(_mockDiscoveryContext.Object), false); // Verify warning message was sent to logger (not error) _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.AtLeastOnce); @@ -327,7 +327,7 @@ internal override void DiscoverTestsInSource( string source, IAdapterMessageLogger logger, IUnitTestElementSink discoverySink, - IDiscoveryContext? discoveryContext, + string? settingsXml, ITestElementFilterProvider? filterProvider, bool isMTP) { diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index b7a9d9fe3d..12395a78d3 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -63,6 +63,13 @@ public class TestExecutionManagerTests : TestContainer private ITestResultRecorder TestResultRecorder => _frameworkHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings); + /// + /// Builds the neutral run inputs (test-run directory + run settings XML) the execution engine now receives at + /// its boundary, extracted from exactly as the adapter does in production. + /// + private DeploymentContext CurrentDeploymentContext + => new(_runContext.TestRunDirectory, _runContext.RunSettings?.SettingsXml); + public TestExecutionManagerTests() { _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(_ => true)); @@ -100,7 +107,7 @@ public async Task RunTestsForTestWithFilterErrorShouldSendZeroResults() // Causing the FilterExpressionError _runContext = new TestableRunContextTestExecutionTests(() => throw new TestPlatformFormatException()); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // No Results _frameworkHandle.TestCaseStartList.Count.Should().Be(0); @@ -116,7 +123,7 @@ public async Task RunTestsForTestWithFilterShouldSendResultsForFilteredTests() _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(p => p.DisplayName == "PassingTest")); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // FailingTest should be skipped because it does not match the filter criteria. List expectedTestCaseStartList = ["PassingTest"]; @@ -144,7 +151,7 @@ public async Task RunTestsForIgnoredTestShouldSendResultsMarkingIgnoredTestsAsSk TestCase testCase = GetTestCase(typeof(DummyTestClass), "IgnoredTest"); TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); _frameworkHandle.TestCaseStartList[0].Should().Be("IgnoredTest"); _frameworkHandle.TestCaseEndList[0].Should().Be("IgnoredTest:Skipped"); @@ -157,7 +164,7 @@ public async Task RunTestsForASingleTestShouldSendSingleResult() TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); List expectedTestCaseStartList = ["PassingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed"]; @@ -174,7 +181,7 @@ public async Task RunTestsForMultipleTestShouldSendMultipleResults() TestCase failingTestCase = GetTestCase(typeof(DummyTestClass), "FailingTest"); TestCase[] tests = [testCase, failingTestCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); List expectedTestCaseStartList = ["PassingTest", "FailingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed", "FailingTest:Failed"]; @@ -194,7 +201,7 @@ public async Task RunTestsForCancellationTokenCanceledSetToTrueShouldSendZeroRes // Cancel the test run _cancellationToken.Cancel(); - Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); await func.Should().ThrowAsync(); // No Results @@ -216,7 +223,7 @@ public async Task RunTestsForTestShouldDeployBeforeExecution() await _testExecutionManager.RunTestsAsync( ToUnitTestElements(tests), - _runContext, + CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); @@ -238,7 +245,7 @@ public async Task RunTestsForTestShouldCleanupAfterExecution() td => td.Cleanup()).Callback(() => SetCaller("Cleanup")); #endif - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _callers[0].Should().Be("LoadAssembly", "Cleanup should be called after execution."); @@ -255,7 +262,7 @@ public async Task RunTestsForTestShouldNotCleanupOnTestFailure() TestCase[] tests = [testCase, failingTestCase]; TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockTestDeployment.Verify(td => td.Cleanup(), Times.Never); } @@ -274,7 +281,7 @@ public async Task RunTestsForTestShouldLoadSourceFromDeploymentDirectoryIfDeploy testablePlatformService.MockTestDeployment.Setup(td => td.GetDeploymentDirectory()) .Returns(@"C:\temp"); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockFileOperations.Verify( fo => fo.LoadAssembly(It.Is(s => s.StartsWith("C:\\temp"))), @@ -300,7 +307,7 @@ public async Task RunTestsForTestShouldPassInTestRunParametersInformationAsPrope """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -322,7 +329,7 @@ public async Task RunTestsForTestShouldPassInTcmPropertiesAsPropertiesToTheTest( """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); VerifyTcmProperties(DummyTestClass.TestContextProperties, testCase); } @@ -335,7 +342,7 @@ public async Task RunTestsForTestShouldPassInDeploymentInformationAsPropertiesTo // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockSettingsProvider.Verify(sp => sp.GetProperties(It.IsAny()), Times.Once); } @@ -359,7 +366,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger First Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Update runsettings to have different values for similar keys _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -376,7 +383,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger another Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); "http://updatedLocalHost".Equals(DummyTestClass.TestContextProperties!["webAppUrl"]).Should().BeTrue(); } @@ -390,7 +397,7 @@ private async Task RunTestsForSourceShouldRunTestsInASource() { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); _frameworkHandle.TestCaseStartList.Contains("PassingTest").Should().BeTrue(); _frameworkHandle.TestCaseEndList.Contains("PassingTest:Passed").Should().BeTrue(); @@ -412,7 +419,7 @@ private async Task RunTestsForSourceShouldPassInTestRunParametersInformationAsPr """); - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -423,7 +430,7 @@ private async Task RunTestsForSourceShouldPassInDeploymentInformationAsPropertie { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties.Should().NotBeNull(); } @@ -437,7 +444,7 @@ public async Task RunTestsForMultipleSourcesShouldRunEachTestJustOnce() ExecuteTestsWrapper = (tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone) => testsCount += tests.Count(), }; - await testableTestExecutionManager.RunTestsAsync(sources, _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await testableTestExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); testsCount.Should().Be(4); } @@ -469,8 +476,8 @@ public async Task RunTestsForTestShouldRunTestsInParallelWhenEnabledInRunsetting try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -506,8 +513,8 @@ public async Task RunTestsForTestShouldRunTestsByMethodLevelWhenSpecified() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -543,8 +550,8 @@ public async Task RunTestsForTestShouldRunTestsWithSpecifiedNumberOfWorkers() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -578,7 +585,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -607,7 +614,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -637,7 +644,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -666,7 +673,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -704,8 +711,8 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); DummyTestClassWithDoNotParallelizeMethods.ParallelizableTestsThreadIds.Count.Should().BeOneOf(1, 2); @@ -738,7 +745,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); testablePlatformService.SetupMockReflectionOperations(); @@ -771,7 +778,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -810,8 +817,8 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState try { - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.Count.Should().Be(1); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.ToArray()[0].Should().Be(Thread.CurrentThread.GetApartmentState()); @@ -850,15 +857,15 @@ static TestCase[] BuildTests() => """); - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); var firstHandle = new TestableFrameworkHandle(); var firstManager = new TestExecutionManager(task => task()); - await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, firstHandle, firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, firstHandle, firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); var secondHandle = new TestableFrameworkHandle(); var secondManager = new TestExecutionManager(task => task()); - await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), _runContext, secondHandle, secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, secondHandle, secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Same seed must produce the same order across separate runs. firstHandle.TestCaseStartList.Should().Equal(secondHandle.TestCaseStartList); @@ -893,9 +900,9 @@ public async Task RunTestsWithoutRandomizeTestOrderShouldPreserveInputOrder() """); - MSTestSettings.PopulateSettings(_runContext, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), _runContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _frameworkHandle.TestCaseStartList.Should().Equal("TestA", "TestB", "TestC"); } @@ -1271,11 +1278,11 @@ internal sealed class TestableTestCaseFilterExpression : ITestCaseFilterExpressi internal class TestableTestExecutionManager : TestExecutionManager { - internal Action, IRunContext?, IFrameworkHandle, ITestResultRecorder, bool> ExecuteTestsWrapper { get; set; } = null!; + internal Action, DeploymentContext, IFrameworkHandle, ITestResultRecorder, bool> ExecuteTestsWrapper { get; set; } = null!; - internal override Task ExecuteTestsAsync(IEnumerable tests, IRunContext? runContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) + internal override Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { - ExecuteTestsWrapper?.Invoke(tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone); + ExecuteTestsWrapper?.Invoke(tests, deploymentContext, frameworkHandle, testResultRecorder, isDeploymentDone); return Task.CompletedTask; } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs index a38cfe178b..fb598c3b15 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestSettingsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -376,7 +376,7 @@ public void PopulateSettingsShouldWarnWhenRandomizeTestOrderAndOrderTestsByNameI _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.RandomizeTestOrder.Should().BeTrue(); MSTestSettings.CurrentSettings.OrderTestsByNameInClass.Should().BeTrue(); @@ -400,7 +400,7 @@ public void PopulateSettingsShouldNotWarnWhenOnlyRandomizeTestOrderIsEnabled() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.RandomizeTestOrder.Should().BeTrue(); MSTestSettings.CurrentSettings.OrderTestsByNameInClass.Should().BeFalse(); @@ -656,7 +656,7 @@ public void DisableParallelizationShouldBeFalseByDefault() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.DisableParallelization.Should().BeFalse(); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, It.IsAny()), Times.Never); @@ -675,7 +675,7 @@ public void DisableParallelizationShouldBeConsumedFromRunSettingsWhenSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings.CurrentSettings.DisableParallelization.Should().BeTrue(); } @@ -693,7 +693,7 @@ public void DisableParallelization_WithInvalidValue_GettingAWarning() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); _mockMessageLogger.Verify(lm => lm.SendMessage(TestMessageLevel.Warning, "Invalid value '3' for runsettings entry 'DisableParallelization', setting will be ignored."), Times.Once); } @@ -981,7 +981,7 @@ public void CurrentSettingShouldReturnCachedLoadedSettings() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; MSTestSettings adapterSettings2 = MSTestSettings.CurrentSettings; @@ -1033,7 +1033,7 @@ public void PopulateSettingsShouldInitializeDefaultAdapterSettingsWhenDiscoveryC public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull() { - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1045,7 +1045,7 @@ public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsXmlIsEmpty() { _mockDiscoveryContext.Setup(md => md.RunSettings!.SettingsXml).Returns(string.Empty); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; adapterSettings.CaptureDebugTraces.Should().BeTrue(); @@ -1067,7 +1067,7 @@ public void PopulateSettingsShouldInitializeSettingsToDefaultIfNotSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1092,7 +1092,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestSection() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1118,7 +1118,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestV2Section() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; @@ -1147,7 +1147,7 @@ public void PopulateSettingsShouldInitializeSettingsFromMSTestV2OverMSTestV1Sect _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); MSTestSettings adapterSettings = MSTestSettings.CurrentSettings; diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs index 15e1c5c839..94e060c056 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/RunConfigurationSettingsTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -69,7 +69,7 @@ public void PopulateSettingsShouldInitializeDefaultConfigurationSettingsWhenDisc public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull() { - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -78,7 +78,7 @@ public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsIsNull public void PopulateSettingsShouldInitializeDefaultSettingsWhenRunSettingsXmlIsEmpty() { _mockDiscoveryContext.Setup(md => md.RunSettings!.SettingsXml).Returns(string.Empty); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.ExecutionApartmentState.Should().BeNull(); @@ -97,7 +97,7 @@ public void PopulateSettingsShouldInitializeSettingsToDefaultIfNotSpecified() _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.Should().NotBeNull(); @@ -120,7 +120,7 @@ public void PopulateSettingsShouldInitializeSettingsFromRunConfigurationSection( _mockDiscoveryContext.Setup(dc => dc.RunSettings).Returns(_mockRunSettings.Object); _mockRunSettings.Setup(rs => rs.SettingsXml).Returns(runSettingsXml); - MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); + MSTestSettings.PopulateSettings(_mockDiscoveryContext.Object.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); RunConfigurationSettings settings = MSTestSettings.RunConfigurationSettings; settings.Should().NotBeNull(); From 668335abc02abd9fd073e5c46d6c3223a1c914fa Mon Sep 17 00:00:00 2001 From: Amaury Leveque Date: Sun, 5 Jul 2026 12:03:37 +0200 Subject: [PATCH 14/24] Remove IFrameworkHandle from the PlatformServices execution engine (Phase 6e-1) The execution engine used the VSTest IFrameworkHandle exclusively to obtain an IAdapterMessageLogger via ToAdapterMessageLogger(). Replace the IFrameworkHandle parameter with the neutral IAdapterMessageLogger throughout TestExecutionManager (RunTestsAsync both overloads, ExecuteTestsAsync, ExecuteTestsInSourceAsync, Deploy); the adapter boundary (MSTestExecutor) now calls frameworkHandle.ToAdapterMessageLogger() once and injects the result. This removes the last VSTest ObjectModel.Adapter reference from the execution engine. No behavior change: the logger wrapper is stateless, so injecting one instance is identical to building one per call site. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../VSTestAdapter/MSTestExecutor.cs | 4 +- .../TestExecutionManager.Parallelization.cs | 15 ++--- .../Execution/TestExecutionManager.cs | 28 ++++---- .../Utilities/CLITestBase.discovery.cs | 4 +- .../Execution/TestExecutionManagerTests.cs | 66 +++++++++---------- 5 files changed, 57 insertions(+), 60 deletions(-) diff --git a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs index 0db5ce45f8..f1fd235958 100644 --- a/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs +++ b/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs @@ -161,7 +161,7 @@ internal async Task RunTestsAsync(IEnumerable? tests, IRunContext? run // this boundary so the execution engine no longer depends on the VSTest run context. var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, deploymentContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(testElements, deploymentContext, frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { @@ -223,7 +223,7 @@ internal async Task RunTestsAsync(IEnumerable? sources, IRunContext? run // this boundary so the execution engine no longer depends on the VSTest run context. var deploymentContext = new DeploymentContext(runContext?.TestRunDirectory, runContext?.RunSettings?.SettingsXml); - await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, deploymentContext, frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); + await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, deploymentContext, frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs index 379f5308b8..9f7b4ffdd3 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.Parallelization.cs @@ -6,7 +6,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; @@ -20,16 +19,16 @@ internal partial class TestExecutionManager /// /// Tests to execute. /// Host-provided test-run directory and run settings XML. - /// Handle to record test start/end/results. + /// Logger used to report test messages back to the host. /// Recorder used to report test results back to the host. /// Provider for the test filter, or for no filter. /// Indicates if deployment is done. - internal virtual async Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) + internal virtual async Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { _testResultRecorder = testResultRecorder; _testElementFilterProvider = filterProvider; - InitializeRandomTestOrder(frameworkHandle.ToAdapterMessageLogger()); + InitializeRandomTestOrder(messageLogger); var testsBySource = (from test in tests group test by test.TestMethod.AssemblyName into testGroup @@ -43,7 +42,7 @@ group test by test.TestMethod.AssemblyName into testGroup foreach (var group in testsBySource) { _testRunCancellationToken?.ThrowIfCancellationRequested(); - await ExecuteTestsInSourceAsync(group.Tests, deploymentContext, frameworkHandle, group.Source, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsInSourceAsync(group.Tests, deploymentContext, messageLogger, group.Source, isDeploymentDone).ConfigureAwait(false); } } @@ -52,10 +51,10 @@ group test by test.TestMethod.AssemblyName into testGroup /// /// Tests to execute. /// Host-provided test-run directory and run settings XML. - /// Handle to record test start/end/results. + /// Logger used to report test messages back to the host. /// The test container for the tests. /// Indicates if deployment is done. - private async Task ExecuteTestsInSourceAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, string source, bool isDeploymentDone) + private async Task ExecuteTestsInSourceAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, string source, bool isDeploymentDone) { DebugEx.Assert(!StringEx.IsNullOrEmpty(source), "Source cannot be empty"); @@ -66,7 +65,7 @@ private async Task ExecuteTestsInSourceAsync(IEnumerable tests, } #endif - IAdapterMessageLogger adapterMessageLogger = frameworkHandle.ToAdapterMessageLogger(); + IAdapterMessageLogger adapterMessageLogger = messageLogger; using ITestSourceHost isolationHost = PlatformServiceProvider.Instance.CreateTestSourceHost(source, deploymentContext.RunSettingsXml); bool usesAppDomains = isolationHost is TestSourceHost { UsesAppDomain: true }; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs index f08ddb0a52..d8858cdaea 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.cs @@ -2,11 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -89,31 +87,31 @@ private static Task DefaultFactoryAsync(Func taskGetter) /// /// Tests to be run. /// Host-provided test-run directory and run settings XML. - /// Handle to the framework to record results and to do framework operations. + /// Logger used to report test messages back to the host. /// Recorder used to report test results back to the host. /// Provider for the test filter, or for no filter. /// Test run cancellation token. - internal async Task RunTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, TestRunCancellationToken runCancellationToken) + internal async Task RunTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, TestRunCancellationToken runCancellationToken) { DebugEx.Assert(tests != null, "tests"); DebugEx.Assert(deploymentContext != null, "deploymentContext"); - DebugEx.Assert(frameworkHandle != null, "frameworkHandle"); + DebugEx.Assert(messageLogger != null, "messageLogger"); DebugEx.Assert(runCancellationToken != null, "runCancellationToken"); _testRunCancellationToken = runCancellationToken; PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = Deploy(tests, deploymentContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, deploymentContext, messageLogger); #else const bool isDeploymentDone = false; #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(deploymentContext.RunSettingsXml, frameworkHandle.ToAdapterMessageLogger()); + CacheSessionParameters(deploymentContext.RunSettingsXml, messageLogger); // Execute the tests - await ExecuteTestsAsync(tests, deploymentContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, deploymentContext, messageLogger, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -123,7 +121,7 @@ internal async Task RunTestsAsync(IEnumerable tests, Deployment #endif } - internal async Task RunTestsAsync(IEnumerable sources, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) + internal async Task RunTestsAsync(IEnumerable sources, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, ITestSourceHandler testSourceHandler, bool isMTP, TestRunCancellationToken cancellationToken) { _testRunCancellationToken = cancellationToken; PlatformServiceProvider.Instance.TestRunCancellationToken = _testRunCancellationToken; @@ -132,7 +130,7 @@ internal async Task RunTestsAsync(IEnumerable sources, DeploymentContext var tests = new List(); - IAdapterMessageLogger logger = frameworkHandle.ToAdapterMessageLogger(); + IAdapterMessageLogger logger = messageLogger; // deploy everything first. foreach (string source in sources) @@ -148,16 +146,16 @@ internal async Task RunTestsAsync(IEnumerable sources, DeploymentContext } #if !WINDOWS_UWP && !WIN_UI - bool isDeploymentDone = Deploy(tests, deploymentContext, frameworkHandle); + bool isDeploymentDone = Deploy(tests, deploymentContext, messageLogger); #else const bool isDeploymentDone = false; #endif // Placing this after deployment since we need information post deployment that we pass in as properties. - CacheSessionParameters(deploymentContext.RunSettingsXml, frameworkHandle.ToAdapterMessageLogger()); + CacheSessionParameters(deploymentContext.RunSettingsXml, messageLogger); // Run tests. - await ExecuteTestsAsync(tests, deploymentContext, frameworkHandle, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); + await ExecuteTestsAsync(tests, deploymentContext, messageLogger, testResultRecorder, filterProvider, isDeploymentDone).ConfigureAwait(false); #if !WINDOWS_UWP && !WIN_UI if (!_hasAnyTestFailed) @@ -172,8 +170,8 @@ internal async Task RunTestsAsync(IEnumerable sources, DeploymentContext /// Runs deployment for the given tests via the platform-agnostic deployment service using the neutral /// supplied by the adapter boundary. /// - private static bool Deploy(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle) - => PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, deploymentContext, frameworkHandle.ToAdapterMessageLogger()); + private static bool Deploy(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger) + => PlatformServiceProvider.Instance.TestDeployment.Deploy(tests, deploymentContext, messageLogger); #endif internal virtual UnitTestDiscoverer GetUnitTestDiscoverer(ITestSourceHandler testSourceHandler) => new(testSourceHandler); diff --git a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs index f80f61eeba..40326955f6 100644 --- a/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs +++ b/test/IntegrationTests/MSTest.IntegrationTests/Utilities/CLITestBase.discovery.cs @@ -42,7 +42,7 @@ internal static async Task> RunTestsAsync(IEnumerable var frameworkHandle = new InternalFrameworkHandle(); ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(null, null), frameworkHandle, testResultRecorder, filterProvider: null, false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(null, null), frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, filterProvider: null, false); return frameworkHandle.GetFlattenedTestResults(); } @@ -55,7 +55,7 @@ internal static async Task> RunTestsAsync(IEnumerable var runContext = new InternalRunContext(runSettingsXml, testCaseFilter); ITestResultRecorder testResultRecorder = frameworkHandle.ToTestResultRecorder(Environment.MachineName, MSTestSettings.CurrentSettings); - await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(runContext.TestRunDirectory, runContext.RunSettings?.SettingsXml), frameworkHandle, testResultRecorder, new TestElementFilterProvider(runContext), false); + await testExecutionManager.ExecuteTestsAsync(ToUnitTestElements(testCases), new DeploymentContext(runContext.TestRunDirectory, runContext.RunSettings?.SettingsXml), frameworkHandle.ToAdapterMessageLogger(), testResultRecorder, new TestElementFilterProvider(runContext), false); return frameworkHandle.GetFlattenedTestResults(); } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 12395a78d3..4e15c282a4 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -107,7 +107,7 @@ public async Task RunTestsForTestWithFilterErrorShouldSendZeroResults() // Causing the FilterExpressionError _runContext = new TestableRunContextTestExecutionTests(() => throw new TestPlatformFormatException()); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // No Results _frameworkHandle.TestCaseStartList.Count.Should().Be(0); @@ -123,7 +123,7 @@ public async Task RunTestsForTestWithFilterShouldSendResultsForFilteredTests() _runContext = new TestableRunContextTestExecutionTests(() => new TestableTestCaseFilterExpression(p => p.DisplayName == "PassingTest")); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); // FailingTest should be skipped because it does not match the filter criteria. List expectedTestCaseStartList = ["PassingTest"]; @@ -151,7 +151,7 @@ public async Task RunTestsForIgnoredTestShouldSendResultsMarkingIgnoredTestsAsSk TestCase testCase = GetTestCase(typeof(DummyTestClass), "IgnoredTest"); TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); _frameworkHandle.TestCaseStartList[0].Should().Be("IgnoredTest"); _frameworkHandle.TestCaseEndList[0].Should().Be("IgnoredTest:Skipped"); @@ -164,7 +164,7 @@ public async Task RunTestsForASingleTestShouldSendSingleResult() TestCase[] tests = [testCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); List expectedTestCaseStartList = ["PassingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed"]; @@ -181,7 +181,7 @@ public async Task RunTestsForMultipleTestShouldSendMultipleResults() TestCase failingTestCase = GetTestCase(typeof(DummyTestClass), "FailingTest"); TestCase[] tests = [testCase, failingTestCase]; - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); List expectedTestCaseStartList = ["PassingTest", "FailingTest"]; List expectedTestCaseEndList = ["PassingTest:Passed", "FailingTest:Failed"]; @@ -201,7 +201,7 @@ public async Task RunTestsForCancellationTokenCanceledSetToTrueShouldSendZeroRes // Cancel the test run _cancellationToken.Cancel(); - Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); + Func func = () => _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _cancellationToken); await func.Should().ThrowAsync(); // No Results @@ -224,7 +224,7 @@ public async Task RunTestsForTestShouldDeployBeforeExecution() await _testExecutionManager.RunTestsAsync( ToUnitTestElements(tests), CurrentDeploymentContext, - _frameworkHandle, + _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _callers[0].Should().Be("Deploy", "Deploy should be called before execution."); @@ -245,7 +245,7 @@ public async Task RunTestsForTestShouldCleanupAfterExecution() td => td.Cleanup()).Callback(() => SetCaller("Cleanup")); #endif - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _callers[0].Should().Be("LoadAssembly", "Cleanup should be called after execution."); @@ -262,7 +262,7 @@ public async Task RunTestsForTestShouldNotCleanupOnTestFailure() TestCase[] tests = [testCase, failingTestCase]; TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockTestDeployment.Verify(td => td.Cleanup(), Times.Never); } @@ -281,7 +281,7 @@ public async Task RunTestsForTestShouldLoadSourceFromDeploymentDirectoryIfDeploy testablePlatformService.MockTestDeployment.Setup(td => td.GetDeploymentDirectory()) .Returns(@"C:\temp"); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockFileOperations.Verify( fo => fo.LoadAssembly(It.Is(s => s.StartsWith("C:\\temp"))), @@ -307,7 +307,7 @@ public async Task RunTestsForTestShouldPassInTestRunParametersInformationAsPrope """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -329,7 +329,7 @@ public async Task RunTestsForTestShouldPassInTcmPropertiesAsPropertiesToTheTest( """); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); VerifyTcmProperties(DummyTestClass.TestContextProperties, testCase); } @@ -342,7 +342,7 @@ public async Task RunTestsForTestShouldPassInDeploymentInformationAsPropertiesTo // Setup mocks. TestablePlatformServiceProvider testablePlatformService = SetupTestablePlatformService(); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); testablePlatformService.MockSettingsProvider.Verify(sp => sp.GetProperties(It.IsAny()), Times.Once); } @@ -366,7 +366,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger First Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Update runsettings to have different values for similar keys _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -383,7 +383,7 @@ public async Task RunTestsShouldClearSessionParametersAcrossRuns() """); // Trigger another Run - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); "http://updatedLocalHost".Equals(DummyTestClass.TestContextProperties!["webAppUrl"]).Should().BeTrue(); } @@ -397,7 +397,7 @@ private async Task RunTestsForSourceShouldRunTestsInASource() { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); _frameworkHandle.TestCaseStartList.Contains("PassingTest").Should().BeTrue(); _frameworkHandle.TestCaseEndList.Contains("PassingTest:Passed").Should().BeTrue(); @@ -419,7 +419,7 @@ private async Task RunTestsForSourceShouldPassInTestRunParametersInformationAsPr """); - await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties!.Contains( new KeyValuePair("webAppUrl", "http://localhost")).Should().BeTrue(); @@ -430,7 +430,7 @@ private async Task RunTestsForSourceShouldPassInDeploymentInformationAsPropertie { var sources = new List { Assembly.GetExecutingAssembly().Location }; - await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await _testExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); DummyTestClass.TestContextProperties.Should().NotBeNull(); } @@ -444,7 +444,7 @@ public async Task RunTestsForMultipleSourcesShouldRunEachTestJustOnce() ExecuteTestsWrapper = (tests, runContext, frameworkHandle, testResultRecorder, isDeploymentDone) => testsCount += tests.Count(), }; - await testableTestExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); + await testableTestExecutionManager.RunTestsAsync(sources, CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), _mockTestSourceHandler.Object, false, _cancellationToken); testsCount.Should().Be(4); } @@ -477,7 +477,7 @@ public async Task RunTestsForTestShouldRunTestsInParallelWhenEnabledInRunsetting try { MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -514,7 +514,7 @@ public async Task RunTestsForTestShouldRunTestsByMethodLevelWhenSpecified() try { MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -551,7 +551,7 @@ public async Task RunTestsForTestShouldRunTestsWithSpecifiedNumberOfWorkers() try { MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); DummyTestClassForParallelize2.ThreadIds.Count.Should().Be(1); @@ -614,7 +614,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromRuns testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -673,7 +673,7 @@ public async Task RunTestsForTestShouldNotRunTestsInParallelWhenDisabledFromSour testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassForParallelize.ThreadIds.Count.Should().Be(1); } @@ -712,7 +712,7 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() try { MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); DummyTestClassWithDoNotParallelizeMethods.ParallelizableTestsThreadIds.Count.Should().BeOneOf(1, 2); @@ -778,7 +778,7 @@ public async Task RunTestsForTestShouldPreferParallelSettingsFromRunSettingsOver testablePlatformService.MockReflectionOperations.Setup(fo => fo.GetRuntimeMethods(It.IsAny())) .Returns((Type t) => originalReflectionOperation.GetRuntimeMethods(t)); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _enqueuedParallelTestsCount.Should().Be(2); @@ -818,7 +818,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState try { MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.Count.Should().Be(1); DummyTestClassWithDoNotParallelizeMethods.ThreadApartmentStates.ToArray()[0].Should().Be(Thread.CurrentThread.GetApartmentState()); @@ -861,11 +861,11 @@ static TestCase[] BuildTests() => var firstHandle = new TestableFrameworkHandle(); var firstManager = new TestExecutionManager(task => task()); - await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, firstHandle, firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await firstManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, firstHandle.ToAdapterMessageLogger(), firstHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); var secondHandle = new TestableFrameworkHandle(); var secondManager = new TestExecutionManager(task => task()); - await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, secondHandle, secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await secondManager.RunTestsAsync(ToUnitTestElements(BuildTests()), CurrentDeploymentContext, secondHandle.ToAdapterMessageLogger(), secondHandle.ToTestResultRecorder(EnvironmentWrapper.Instance.MachineName, MSTestSettings.CurrentSettings), new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); // Same seed must produce the same order across separate runs. firstHandle.TestCaseStartList.Should().Equal(secondHandle.TestCaseStartList); @@ -902,7 +902,7 @@ public async Task RunTestsWithoutRandomizeTestOrderShouldPreserveInputOrder() MSTestSettings.PopulateSettings(_runContext.RunSettings?.SettingsXml, _mockMessageLogger.Object.ToAdapterMessageLogger(), null); - await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle, TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); + await _testExecutionManager.RunTestsAsync(ToUnitTestElements(tests), CurrentDeploymentContext, _frameworkHandle.ToAdapterMessageLogger(), TestResultRecorder, new TestElementFilterProvider(_runContext), new TestRunCancellationToken()); _frameworkHandle.TestCaseStartList.Should().Equal("TestA", "TestB", "TestC"); } @@ -1278,11 +1278,11 @@ internal sealed class TestableTestCaseFilterExpression : ITestCaseFilterExpressi internal class TestableTestExecutionManager : TestExecutionManager { - internal Action, DeploymentContext, IFrameworkHandle, ITestResultRecorder, bool> ExecuteTestsWrapper { get; set; } = null!; + internal Action, DeploymentContext, IAdapterMessageLogger, ITestResultRecorder, bool> ExecuteTestsWrapper { get; set; } = null!; - internal override Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IFrameworkHandle frameworkHandle, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) + internal override Task ExecuteTestsAsync(IEnumerable tests, DeploymentContext deploymentContext, IAdapterMessageLogger messageLogger, ITestResultRecorder testResultRecorder, ITestElementFilterProvider? filterProvider, bool isDeploymentDone) { - ExecuteTestsWrapper?.Invoke(tests, deploymentContext, frameworkHandle, testResultRecorder, isDeploymentDone); + ExecuteTestsWrapper?.Invoke(tests, deploymentContext, messageLogger, testResultRecorder, isDeploymentDone); return Task.CompletedTask; } From 1e61c137f12ce415ccd2aa479d877f69297bedfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 12:30:12 +0200 Subject: [PATCH 15/24] Relocate VSTest logger/sink bridges to the adapter (Phase 6e-2) Move the three remaining VSTest-object-model bridge helpers out of MSTestAdapter.PlatformServices and into MSTest.TestAdapter: AdapterMessageLoggerExtensions, MessageLevel (ToTestMessageLevel), and UnitTestElementSinkExtensions. These are the last code references to Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging / ITestCaseDiscoverySink in PlatformServices; only doc comments now mention the VSTest types. The logical namespace is unchanged so callers at the adapter boundary and the integration harness are unaffected. PlatformServices.UnitTests calls the ToAdapterMessageLogger bridge, which now lives in MSTest.TestAdapter; touching that module runs its [ModuleInitializer] (MSTestExecutor.SetPlatformLogger), which assigns PlatformServiceProvider.Instance.AdapterTraceLogger. Make the test double's setter tolerate the assignment (as the real PlatformServiceProvider does) instead of throwing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Services/AdapterMessageLoggerExtensions.cs | 0 .../Services/MessageLevel.cs | 0 .../Services/UnitTestElementSinkExtensions.cs | 0 .../TestablePlatformServiceProvider.cs | 7 ++++++- 4 files changed, 6 insertions(+), 1 deletion(-) rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/Services/AdapterMessageLoggerExtensions.cs (100%) rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/Services/MessageLevel.cs (100%) rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/Services/UnitTestElementSinkExtensions.cs (100%) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/AdapterMessageLoggerExtensions.cs similarity index 100% rename from src/Adapter/MSTestAdapter.PlatformServices/Services/AdapterMessageLoggerExtensions.cs rename to src/Adapter/MSTest.TestAdapter/Services/AdapterMessageLoggerExtensions.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/MessageLevel.cs b/src/Adapter/MSTest.TestAdapter/Services/MessageLevel.cs similarity index 100% rename from src/Adapter/MSTestAdapter.PlatformServices/Services/MessageLevel.cs rename to src/Adapter/MSTest.TestAdapter/Services/MessageLevel.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/UnitTestElementSinkExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs similarity index 100% rename from src/Adapter/MSTestAdapter.PlatformServices/Services/UnitTestElementSinkExtensions.cs rename to src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs index 57abe65278..baf709a8e7 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/TestableImplementations/TestablePlatformServiceProvider.cs @@ -40,7 +40,12 @@ internal class TestablePlatformServiceProvider : IPlatformServiceProvider public IFileOperations FileOperations => MockFileOperations.Object; - public ITraceLogger AdapterTraceLogger { get => MockTraceLogger.Object; set => throw new NotSupportedException(); } + // The getter always returns the mock so tests can verify trace logging. The setter is a no-op: + // MSTest.TestAdapter's [ModuleInitializer] (MSTestExecutor.SetPlatformLogger) legitimately assigns + // PlatformServiceProvider.Instance.AdapterTraceLogger when that module is first touched (e.g. via the + // ToAdapterMessageLogger bridge), so this double must tolerate the assignment exactly as the real + // PlatformServiceProvider does, rather than throwing. + public ITraceLogger AdapterTraceLogger { get => MockTraceLogger.Object; set => _ = value; } #if !WINDOWS_UWP && !WIN_UI public ITestDeployment TestDeployment => MockTestDeployment.Object; From ab2cc59ee95a6984d68aa898faee57dcd8db9fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 12:52:16 +0200 Subject: [PATCH 16/24] Neutralize the trait type on UnitTestElement (Phase 6e-3a) Replace the VSTest object-model Trait type carried on UnitTestElement.Traits with a neutral, platform-agnostic TestTrait { Name, Value } struct. The engine-side producers and consumers (ReflectHelper/ReflectionHelper GetTestPropertiesAsTraits, TypeEnumerator, TestExecutionManager TestContext building, TestRunInfo, the test-filter context) now operate on TestTrait, so five files stop referencing Microsoft.VisualStudio.TestPlatform.ObjectModel. The VSTest Trait only survives at the adapter conversion boundary (TestCaseExtensions and UnitTestElement.ToTestCase), which convert between TestTrait and the host trait type. TestTrait is [Serializable] on .NET Framework because UnitTestElement is serialized across app domains during isolated discovery/execution; order and Name/Value are preserved, so trait -> TestContext reporting and the produced host test case are byte-for-byte identical. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TestExecutionManager.TestContext.cs | 3 +- .../Execution/TestRunInfo.cs | 3 +- .../Extensions/TestCaseExtensions.cs | 2 +- .../Helpers/ReflectHelper.cs | 4 +-- .../Helpers/ReflectionHelper.cs | 10 +++--- .../ObjectModel/TestTrait.cs | 33 +++++++++++++++++++ .../ObjectModel/UnitTestElement.cs | 7 ++-- .../Execution/TestPropertyAttributeTests.cs | 6 ++-- .../Execution/TestRunInfoTests.cs | 2 +- .../ObjectModel/UnitTestElementTests.cs | 2 +- .../Services/ReflectionOperationsTests.cs | 21 ++++++------ 11 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestTrait.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs index 1f2f0677c0..e465af8e72 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestExecutionManager.TestContext.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -51,7 +50,7 @@ internal partial class TestExecutionManager if (unitTestElement.Traits is { Length: > 0 }) { - foreach (Trait trait in unitTestElement.Traits) + foreach (TestTrait trait in unitTestElement.Traits) { ValidateAndAssignTestProperty(testContextProperties, trait.Name, trait.Value); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs index 5999529378..90e7c4698b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TestRunInfo.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; @@ -39,7 +38,7 @@ public static TestRunInfo CreateFrom(IReadOnlyList testsToRun) private static PlannedTest ToPlannedTest(UnitTestElement element) { - Trait[]? traits = element.Traits; + TestTrait[]? traits = element.Traits; KeyValuePair[] testProperties; if (traits is { Length: > 0 }) { diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs b/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs index 4c69d171ee..9d2636d2d2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs @@ -118,7 +118,7 @@ internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase if (testCase.Traits.Any()) { - testElement.Traits = [.. testCase.Traits]; + testElement.Traits = [.. testCase.Traits.Select(t => new TestTrait(t.Name, t.Value))]; } string[]? workItemIds = testCase.GetPropertyValue(EngineConstants.WorkItemIdsProperty, null); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs index cd6c40f80c..518667da1d 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectHelper.cs @@ -5,9 +5,9 @@ using System.Security; #endif +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; @@ -198,7 +198,7 @@ internal static bool IsDoNotParallelizeSet(Assembly assembly) /// /// The member to inspect. /// List of traits. - internal static Trait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider) + internal static TestTrait[] GetTestPropertiesAsTraits(MethodInfo testPropertyProvider) => PlatformServiceProvider.Instance.ReflectionOperations.GetTestPropertiesAsTraits(testPropertyProvider); /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs index 721f5afd7a..b85063c118 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/ReflectionHelper.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; @@ -62,7 +62,7 @@ public static string[] GetTestCategories(this IReflectionOperations reflectionOp /// The reflection operations to use. /// The member to inspect. /// List of traits. - public static Trait[] GetTestPropertiesAsTraits(this IReflectionOperations reflectionOperations, MethodInfo testPropertyProvider) + public static TestTrait[] GetTestPropertiesAsTraits(this IReflectionOperations reflectionOperations, MethodInfo testPropertyProvider) { Attribute[] attributesFromMethod = reflectionOperations.GetCustomAttributesCached(testPropertyProvider); Attribute[] attributesFromClass = testPropertyProvider.ReflectedType is { } testClass ? reflectionOperations.GetCustomAttributesCached(testClass) : []; @@ -89,13 +89,13 @@ public static Trait[] GetTestPropertiesAsTraits(this IReflectionOperations refle return []; } - var traits = new Trait[countTestPropertyAttribute]; + var traits = new TestTrait[countTestPropertyAttribute]; int index = 0; foreach (Attribute attribute in attributesFromMethod) { if (attribute is TestPropertyAttribute testProperty) { - traits[index++] = new Trait(testProperty.Name, testProperty.Value); + traits[index++] = new TestTrait(testProperty.Name, testProperty.Value); } } @@ -103,7 +103,7 @@ public static Trait[] GetTestPropertiesAsTraits(this IReflectionOperations refle { if (attribute is TestPropertyAttribute testProperty) { - traits[index++] = new Trait(testProperty.Name, testProperty.Value); + traits[index++] = new TestTrait(testProperty.Name, testProperty.Value); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestTrait.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestTrait.cs new file mode 100644 index 0000000000..44c4799091 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/TestTrait.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +/// +/// A platform-agnostic name/value trait associated with a test (for example the values produced by +/// [TestProperty] attributes). This mirrors the shape historically carried by the test platform's +/// trait type but does not depend on any specific test platform's object model, so the platform services +/// layer can describe test traits without referencing that object model. +/// +#if NETFRAMEWORK +// A (which carries these) is serialized across app domains on .NET Framework. +[Serializable] +#endif +internal readonly struct TestTrait +{ + public TestTrait(string name, string value) + { + Name = name; + Value = value; + } + + /// + /// Gets the trait name. + /// + public string Name { get; } + + /// + /// Gets the trait value. + /// + public string Value { get; } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs index c61f2e04d6..56c2fb4710 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs @@ -53,7 +53,7 @@ public UnitTestElement(TestMethod testMethod) /// /// Gets or sets the traits for test method. /// - public Trait[]? Traits { get; set; } + public TestTrait[]? Traits { get; set; } /// /// Gets or sets the priority of the test method, if any. @@ -217,7 +217,10 @@ internal TestCase ToTestCase() if (Traits is { Length: > 0 }) { - testCase.Traits.AddRange(Traits); + foreach (TestTrait trait in Traits) + { + testCase.Traits.Add(new Trait(trait.Name, trait.Value)); + } } if (WorkItemIds != null) diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs index e5cb2d9162..3395bd01e3 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestPropertyAttributeTests.cs @@ -39,7 +39,7 @@ protected override void Dispose(bool disposing) public void GetTestMethodInfoShouldAddPropertiesFromContainingClassCorrectly() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassBase).GetMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived))!)]; + TestTrait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassBase).GetMethod(nameof(DummyTestClassBase.VirtualTestMethodInBaseAndDerived))!)]; traits.Length.Should().Be(3); traits[0].Name.Should().Be("TestMethodKeyFromBase"); traits[0].Value.Should().Be("TestMethodValueFromBase"); @@ -51,7 +51,7 @@ public void GetTestMethodInfoShouldAddPropertiesFromContainingClassCorrectly() public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsTestMethod() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived))!)]; + TestTrait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInBaseAndDerived))!)]; traits.Length.Should().Be(6); traits[0].Name.Should().Be("DerivedMethod1Key"); traits[0].Value.Should().Be("DerivedMethod1Value"); @@ -69,7 +69,7 @@ public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClasse public void GetTestMethodInfoShouldAddPropertiesFromContainingClassAndBaseClassesAndOverriddenMethodsCorrectly_OverriddenIsNotTestMethod() { - TestPlatform.ObjectModel.Trait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase))!)]; + TestTrait[] traits = [.. ReflectHelper.GetTestPropertiesAsTraits(typeof(DummyTestClassDerived).GetMethod(nameof(DummyTestClassDerived.VirtualTestMethodInDerivedButNotTestMethodInBase))!)]; traits.Length.Should().Be(6); traits[0].Name.Should().Be("DerivedMethod2Key"); traits[0].Value.Should().Be("DerivedMethod2Value"); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs index e787279850..2dbfe026bf 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestRunInfoTests.cs @@ -54,7 +54,7 @@ public void CreateFromPopulatesCategoriesAndProperties() var element = new UnitTestElement(testMethod) { TestCategory = ["Smoke", "Compatibility"], - Traits = [new Trait("Owner", "alice"), new Trait("Owner", "bob"), new Trait("Priority", "1")], + Traits = [new TestTrait("Owner", "alice"), new TestTrait("Owner", "bob"), new TestTrait("Priority", "1")], }; var info = TestRunInfo.CreateFrom([element]); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs index 634b2a4f7a..45ce00f498 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs @@ -164,7 +164,7 @@ public void ToTestCaseShouldSetTraitsIfPresent() testCase.Traits.Count().Should().Be(0); #pragma warning restore CA1827 // Do not use Count() or LongCount() when Any() can be used - var trait = new Trait("trait", "value"); + var trait = new TestTrait("trait", "value"); _unitTestElement.Traits = [trait]; testCase = _unitTestElement.ToTestCase(); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs index 568ee31d94..f87ab95723 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/ReflectionOperationsTests.cs @@ -4,6 +4,7 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; @@ -237,9 +238,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtMethodLevel() { _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("MethodKey", "MethodValue")], MemberTypes.Method); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("MethodKey", "MethodValue") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("MethodKey", "MethodValue") }, options => options.WithStrictOrdering()); } /// @@ -249,9 +250,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtClassLevel() { _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); } /// @@ -262,9 +263,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeTestPropertiesAtAllLevels() _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("MethodKey", "MethodValue")], MemberTypes.Method); _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("MethodKey", "MethodValue"), new Trait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("MethodKey", "MethodValue"), new TestTrait("ClassKey", "ClassValue") }, options => options.WithStrictOrdering()); } /// @@ -274,9 +275,9 @@ public void GetTestPropertiesAsTraitsShouldIncludeMultipleTestPropertiesAtMethod { _attributeMockingHelper.AddCustomAttribute(typeof(TestPropertyAttribute), [new TestPropertyAttribute("Key1", "Value1"), new TestPropertyAttribute("Key2", "Value2")], MemberTypes.Method); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); - actual.Should().BeEquivalentTo(new[] { new Trait("Key1", "Value1"), new Trait("Key2", "Value2") }, options => options.WithStrictOrdering()); + actual.Should().BeEquivalentTo(new[] { new TestTrait("Key1", "Value1"), new TestTrait("Key2", "Value2") }, options => options.WithStrictOrdering()); } /// @@ -286,7 +287,7 @@ public void GetTestPropertiesAsTraitsShouldReturnEmptyWhenNoTestProperties() { _attributeMockingHelper.AddCustomAttribute(typeof(TestCategoryBaseAttribute), [new TestCategoryAttribute("SomeCategory")], MemberTypes.Method); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(_method.Object); actual.Should().BeEmpty(); } @@ -577,7 +578,7 @@ public void GetTestPropertiesAsTraitsShouldNotIncludeClassLevelAttributesWhenRef [new TestPropertyAttribute("ClassKey", "ClassValue")], MemberTypes.TypeInfo); - Trait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(methodWithNullReflectedType.Object); + TestTrait[] actual = _reflectionOperations.GetTestPropertiesAsTraits(methodWithNullReflectedType.Object); actual.Should().BeEmpty(); } From 1ffa5fa898a0576c3664b37f9e993a34f39ae279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 13:23:02 +0200 Subject: [PATCH 17/24] Relocate the UnitTestElement<->TestCase conversion to the adapter (Phase 6e-3b) Move the deep VSTest-object-model conversion out of MSTestAdapter.PlatformServices and into MSTest.TestAdapter, as a pure relocation (no behavior change): - UnitTestElement.ToTestCase()/GetOrCreateHostTestCase() and the test-case Id hashing (GenerateSerializedDataStrategyTestId / VersionedGuidFromHash) become UnitTestElementExtensions in the adapter. The Id hashing moves byte-identical (VersionedGuidFromHash verbatim), preserving cross-version discovery->execution test-id correlation. - The EngineConstants '#region Test Property registration' (every TestProperty id/label/valueType/attribute, plus the TCM/TFS label constants) moves verbatim into a new adapter AdapterTestProperties class. EngineConstants keeps only its neutral members (extensions, fixture traits, executor uri) and no longer references the VSTest object model. - TestCaseExtensions and TcmTestPropertiesProvider (already adapter-namespaced) move physically into MSTest.TestAdapter. UnitTestElement, EngineConstants, TestCaseExtensions and TcmTestPropertiesProvider all stop referencing Microsoft.VisualStudio.TestPlatform.ObjectModel, dropping the PlatformServices coupling from 13 to 10 files. The conversion is still invoked only at the adapter boundary (executor/discoverer/recorder/filter). The single ToTestCase in the test-case filter and CloneWithUpdatedSource are left as-is to keep this change byte-for-byte (#9568 and #9573 remain open). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AdapterTestProperties.cs | 116 +++++++++ .../Execution/TcmTestPropertiesProvider.cs | 59 +++++ .../Extensions/TestCaseExtensions.cs | 25 +- .../Extensions/TestResultExtensions.cs | 4 +- .../Extensions/UnitTestElementExtensions.cs | 231 ++++++++++++++++++ .../Services/TestResultRecorderExtensions.cs | 2 +- .../Services/UnitTestElementSinkExtensions.cs | 3 +- .../MSTest.TestAdapter/TestMethodFilter.cs | 8 +- .../EngineConstants.cs | 103 -------- .../Execution/TcmTestPropertiesProvider.cs | 61 ----- .../ObjectModel/UnitTestElement.cs | 216 +--------------- .../TcmTestPropertiesProviderTests.cs | 31 +-- .../Execution/TestExecutionManagerTests.cs | 36 +-- .../Extensions/TestCaseExtensionsTests.cs | 15 +- .../Extensions/TestResultExtensionsTests.cs | 5 +- .../ObjectModel/UnitTestElementTests.cs | 23 +- 16 files changed, 485 insertions(+), 453 deletions(-) create mode 100644 src/Adapter/MSTest.TestAdapter/AdapterTestProperties.cs create mode 100644 src/Adapter/MSTest.TestAdapter/Execution/TcmTestPropertiesProvider.cs rename src/Adapter/{MSTestAdapter.PlatformServices => MSTest.TestAdapter}/Extensions/TestCaseExtensions.cs (87%) create mode 100644 src/Adapter/MSTest.TestAdapter/Extensions/UnitTestElementExtensions.cs delete mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs diff --git a/src/Adapter/MSTest.TestAdapter/AdapterTestProperties.cs b/src/Adapter/MSTest.TestAdapter/AdapterTestProperties.cs new file mode 100644 index 0000000000..eaa2668c8d --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/AdapterTestProperties.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +/// +/// The VSTest object-model registrations used by the adapter to carry MSTest +/// discovery/execution metadata (and host test-case-management values) on VSTest test cases and results. +/// These live in the adapter layer because they depend on the VSTest object model; the platform services +/// engine describes the same information with its own neutral types. +/// +internal static class AdapterTestProperties +{ + #region Test Property registration + internal static readonly TestProperty WorkItemIdsProperty = TestProperty.Register("WorkItemIds", WorkItemIdsLabel, typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestClassNameProperty = TestProperty.Register("MSTestDiscoverer.TestClassName", TestClassNameLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + +#pragma warning disable CS0618 // Type or member is obsolete + internal static readonly TestProperty TestCategoryProperty = TestProperty.Register("MSTestDiscoverer.TestCategory", TestCategoryLabel, typeof(string[]), TestPropertyAttributes.Hidden | TestPropertyAttributes.Trait, typeof(TestCase)); +#pragma warning restore CS0618 // Type or member is obsolete + + internal static readonly TestProperty PriorityProperty = TestProperty.Register("MSTestDiscoverer.Priority", PriorityLabel, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + +#if !WINDOWS_UWP && !WIN_UI + internal static readonly TestProperty DeploymentItemsProperty = TestProperty.Register("MSTestDiscoverer.DeploymentItems", DeploymentItemsLabel, typeof(KeyValuePair[]), TestPropertyAttributes.Hidden, typeof(TestCase)); +#endif + + internal static readonly TestProperty DoNotParallelizeProperty = TestProperty.Register("MSTestDiscoverer.DoNotParallelize", DoNotParallelizeLabel, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty ExecutionIdProperty = TestProperty.Register("ExecutionId", ExecutionIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); + + internal static readonly TestProperty ParentExecIdProperty = TestProperty.Register("ParentExecId", ParentExecIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); + + internal static readonly TestProperty TestRunIdProperty = TestProperty.Register(TestRunId, TestRunId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestPlanIdProperty = TestProperty.Register(TestPlanId, TestPlanId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestCaseIdProperty = TestProperty.Register(TestCaseId, TestCaseId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestPointIdProperty = TestProperty.Register(TestPointId, TestPointId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestConfigurationIdProperty = TestProperty.Register(TestConfigurationId, TestConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestConfigurationNameProperty = TestProperty.Register(TestConfigurationName, TestConfigurationName, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty IsInLabEnvironmentProperty = TestProperty.Register(IsInLabEnvironment, IsInLabEnvironment, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildConfigurationIdProperty = TestProperty.Register(BuildConfigurationId, BuildConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildDirectoryProperty = TestProperty.Register(BuildDirectory, BuildDirectory, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildFlavorProperty = TestProperty.Register(BuildFlavor, BuildFlavor, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildNumberProperty = TestProperty.Register(BuildNumber, BuildNumber, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildPlatformProperty = TestProperty.Register(BuildPlatform, BuildPlatform, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty BuildUriProperty = TestProperty.Register(BuildUri, BuildUri, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TfsServerCollectionUrlProperty = TestProperty.Register(TfsServerCollectionUrl, TfsServerCollectionUrl, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TfsTeamProjectProperty = TestProperty.Register(TfsTeamProject, TfsTeamProject, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty ParameterTypesProperty = TestProperty.Register("MSTest.ParameterTypes", "ParameterTypes", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestDynamicDataTypeProperty = TestProperty.Register("MSTest.DynamicDataType", "DynamicDataType", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestCaseIndexProperty = TestProperty.Register("MSTest.TestCaseIndex", "TestCaseIndex", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestDynamicDataProperty = TestProperty.Register("MSTest.DynamicData", "DynamicData", typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty TestDataSourceIgnoreMessageProperty = TestProperty.Register("MSTest.TestDataSourceIgnoreMessageProperty", "TestDataSourceIgnoreMessageProperty", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal static readonly TestProperty UnfoldingStrategy = TestProperty.Register("MSTestDiscoverer.UnfoldingStrategy", "UnfoldingStrategy", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); + + #endregion + + #region Private Constants + + /// + /// These are the Test properties used by the adapter, which essentially correspond + /// to attributes on tests, and may be available in command line/TeamBuild to filter tests. + /// These Property names should not be localized. + /// + private const string TestClassNameLabel = "ClassName"; + private const string TestCategoryLabel = "TestCategory"; + private const string PriorityLabel = "Priority"; +#if !WINDOWS_UWP && !WIN_UI + private const string DeploymentItemsLabel = "DeploymentItems"; +#endif + private const string DoNotParallelizeLabel = "DoNotParallelize"; + private const string ExecutionIdLabel = "ExecutionId"; + private const string ParentExecIdLabel = "ParentExecId"; + private const string WorkItemIdsLabel = "WorkItemIds"; + + private const string TestRunId = "__Tfs_TestRunId__"; + private const string TestPlanId = "__Tfs_TestPlanId__"; + private const string TestCaseId = "__Tfs_TestCaseId__"; + private const string TestPointId = "__Tfs_TestPointId__"; + private const string TestConfigurationId = "__Tfs_TestConfigurationId__"; + private const string TestConfigurationName = "__Tfs_TestConfigurationName__"; + private const string IsInLabEnvironment = "__Tfs_IsInLabEnvironment__"; + private const string BuildConfigurationId = "__Tfs_BuildConfigurationId__"; + private const string BuildDirectory = "__Tfs_BuildDirectory__"; + private const string BuildFlavor = "__Tfs_BuildFlavor__"; + private const string BuildNumber = "__Tfs_BuildNumber__"; + private const string BuildPlatform = "__Tfs_BuildPlatform__"; + private const string BuildUri = "__Tfs_BuildUri__"; + private const string TfsServerCollectionUrl = "__Tfs_TfsServerCollectionUrl__"; + private const string TfsTeamProject = "__Tfs_TeamProject__"; + + #endregion +} diff --git a/src/Adapter/MSTest.TestAdapter/Execution/TcmTestPropertiesProvider.cs b/src/Adapter/MSTest.TestAdapter/Execution/TcmTestPropertiesProvider.cs new file mode 100644 index 0000000000..93e2892098 --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/Execution/TcmTestPropertiesProvider.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using TestPlatformObjectModel = Microsoft.VisualStudio.TestPlatform.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; + +/// +/// Reads and parses the test-case-management (TCM) properties supplied by a host on a test case, in order to +/// surface them to the running test through TestContext. +/// +/// +/// This is a translation point between the VSTest test-case object model and the neutral execution context bag +/// consumed by the platform services engine. It runs at the adapter boundary (when a host hands the adapter +/// concrete test cases to run) and produces a string-keyed dictionary so the engine never depends on the VSTest +/// TestProperty object model. It is expected to move entirely into the adapter layer once the platform +/// services no longer reference the VSTest object model. +/// +internal static class TcmTestPropertiesProvider +{ + /// + /// Gets the host execution context properties from a test case, keyed by the host property identifier. + /// + /// Test case. + /// The properties, or when the test case is null or carries no test case id. + public static IReadOnlyDictionary? GetTcmProperties(TestPlatformObjectModel.TestCase? testCase) + { + // Return empty properties when testCase is null or when test case id is zero. + if (testCase == null || + testCase.GetPropertyValue(AdapterTestProperties.TestCaseIdProperty, default) == 0) + { + return null; + } + + var tcmProperties = new Dictionary(capacity: 15) + { + // Step 1: Add common properties. + [AdapterTestProperties.TestRunIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestRunIdProperty, default), + [AdapterTestProperties.TestPlanIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestPlanIdProperty, default), + [AdapterTestProperties.BuildConfigurationIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildConfigurationIdProperty, default), + [AdapterTestProperties.BuildDirectoryProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildDirectoryProperty, default), + [AdapterTestProperties.BuildFlavorProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildFlavorProperty, default), + [AdapterTestProperties.BuildNumberProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildNumberProperty, default), + [AdapterTestProperties.BuildPlatformProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildPlatformProperty, default), + [AdapterTestProperties.BuildUriProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.BuildUriProperty, default), + [AdapterTestProperties.TfsServerCollectionUrlProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TfsServerCollectionUrlProperty, default), + [AdapterTestProperties.TfsTeamProjectProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TfsTeamProjectProperty, default), + [AdapterTestProperties.IsInLabEnvironmentProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.IsInLabEnvironmentProperty, default), + + // Step 2: Add test case specific properties. + [AdapterTestProperties.TestCaseIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestCaseIdProperty, default), + [AdapterTestProperties.TestConfigurationIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestConfigurationIdProperty, default), + [AdapterTestProperties.TestConfigurationNameProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestConfigurationNameProperty, default), + [AdapterTestProperties.TestPointIdProperty.Id] = testCase.GetPropertyValue(AdapterTestProperties.TestPointIdProperty, default), + }; + + return tcmProperties; + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs similarity index 87% rename from src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs rename to src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs index 9d2636d2d2..b3c757cfad 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Extensions/TestCaseExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Extensions/TestCaseExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -89,31 +88,31 @@ internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase } string? managedTypeName = testCase.GetManagedType(); - string? legacyTestClassName = testCase.GetPropertyValue(EngineConstants.TestClassNameProperty) as string; + string? legacyTestClassName = testCase.GetPropertyValue(AdapterTestProperties.TestClassNameProperty) as string; string? testClassName = testCase.GetClassNameWhenFullyQualifiedNameStartsWith(managedTypeName) ?? testCase.GetClassNameWhenFullyQualifiedNameStartsWith(legacyTestClassName) ?? managedTypeName ?? legacyTestClassName; string name = testCase.GetTestName(testClassName); - var testMethod = new TestMethod(testCase.GetManagedMethod(), testCase.GetHierarchy(), name, testClassName!, source, testCase.DisplayName, testCase.GetPropertyValue(EngineConstants.ParameterTypesProperty, null)); + var testMethod = new TestMethod(testCase.GetManagedMethod(), testCase.GetHierarchy(), name, testClassName!, source, testCase.DisplayName, testCase.GetPropertyValue(AdapterTestProperties.ParameterTypesProperty, null)); - var dataType = (DynamicDataType)testCase.GetPropertyValue(EngineConstants.TestDynamicDataTypeProperty, (int)DynamicDataType.None); + var dataType = (DynamicDataType)testCase.GetPropertyValue(AdapterTestProperties.TestDynamicDataTypeProperty, (int)DynamicDataType.None); if (dataType != DynamicDataType.None) { - string[]? data = testCase.GetPropertyValue(EngineConstants.TestDynamicDataProperty, null); + string[]? data = testCase.GetPropertyValue(AdapterTestProperties.TestDynamicDataProperty, null); testMethod.DataType = dataType; testMethod.SerializedData = data; - testMethod.TestCaseIndex = testCase.GetPropertyValue(EngineConstants.TestCaseIndexProperty, 0); - testMethod.TestDataSourceIgnoreMessage = testCase.GetPropertyValue(EngineConstants.TestDataSourceIgnoreMessageProperty) as string; + testMethod.TestCaseIndex = testCase.GetPropertyValue(AdapterTestProperties.TestCaseIndexProperty, 0); + testMethod.TestDataSourceIgnoreMessage = testCase.GetPropertyValue(AdapterTestProperties.TestDataSourceIgnoreMessageProperty) as string; } var testElement = new UnitTestElement(testMethod) { - TestCategory = testCase.GetPropertyValue(EngineConstants.TestCategoryProperty) as string[], - Priority = testCase.GetPropertyValue(EngineConstants.PriorityProperty) as int?, - UnfoldingStrategy = (TestDataSourceUnfoldingStrategy)testCase.GetPropertyValue(EngineConstants.UnfoldingStrategy, (int)TestDataSourceUnfoldingStrategy.Auto), + TestCategory = testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty) as string[], + Priority = testCase.GetPropertyValue(AdapterTestProperties.PriorityProperty) as int?, + UnfoldingStrategy = (TestDataSourceUnfoldingStrategy)testCase.GetPropertyValue(AdapterTestProperties.UnfoldingStrategy, (int)TestDataSourceUnfoldingStrategy.Auto), }; if (testCase.Traits.Any()) @@ -121,21 +120,21 @@ internal static UnitTestElement ToUnitTestElementWithUpdatedSource(this TestCase testElement.Traits = [.. testCase.Traits.Select(t => new TestTrait(t.Name, t.Value))]; } - string[]? workItemIds = testCase.GetPropertyValue(EngineConstants.WorkItemIdsProperty, null); + string[]? workItemIds = testCase.GetPropertyValue(AdapterTestProperties.WorkItemIdsProperty, null); if (workItemIds is { Length: > 0 }) { testElement.WorkItemIds = workItemIds; } #if !WINDOWS_UWP && !WIN_UI - KeyValuePair[]? deploymentItems = testCase.GetPropertyValue[]>(EngineConstants.DeploymentItemsProperty, null); + KeyValuePair[]? deploymentItems = testCase.GetPropertyValue[]>(AdapterTestProperties.DeploymentItemsProperty, null); if (deploymentItems is { Length: > 0 }) { testElement.DeploymentItems = deploymentItems; } #endif - testElement.DoNotParallelize = testCase.GetPropertyValue(EngineConstants.DoNotParallelizeProperty, false); + testElement.DoNotParallelize = testCase.GetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, false); return testElement; } diff --git a/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs index 0a4824f828..834b522ee8 100644 --- a/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Extensions/TestResultExtensions.cs @@ -49,8 +49,8 @@ internal static VSTestTestResult ToTestResult(this TestResult frameworkTestResul ComputerName = computerName, }; - testResult.SetPropertyValue(EngineConstants.ExecutionIdProperty, frameworkTestResult.ExecutionId); - testResult.SetPropertyValue(EngineConstants.ParentExecIdProperty, frameworkTestResult.ParentExecId); + testResult.SetPropertyValue(AdapterTestProperties.ExecutionIdProperty, frameworkTestResult.ExecutionId); + testResult.SetPropertyValue(AdapterTestProperties.ParentExecIdProperty, frameworkTestResult.ParentExecId); if (!StringEx.IsNullOrEmpty(frameworkTestResult.LogOutput)) { diff --git a/src/Adapter/MSTest.TestAdapter/Extensions/UnitTestElementExtensions.cs b/src/Adapter/MSTest.TestAdapter/Extensions/UnitTestElementExtensions.cs new file mode 100644 index 0000000000..c6a82a8870 --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/Extensions/UnitTestElementExtensions.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; + +/// +/// Materializes a VSTest from a platform-agnostic . This is the +/// adapter-boundary translation from the neutral execution model to the VSTest object model; the platform services +/// engine never performs it and only threads the resulting test case (as an opaque handle) to the recorder and the +/// deployment boundary. +/// +internal static class UnitTestElementExtensions +{ + private static readonly byte[] OpenParen = [40, 0]; // Encoding.Unicode.GetBytes("("); + private static readonly byte[] CloseParen = [41, 0]; // Encoding.Unicode.GetBytes(")"); + private static readonly byte[] OpenBracket = [91, 0]; // Encoding.Unicode.GetBytes("["); + private static readonly byte[] CloseBracket = [93, 0]; // Encoding.Unicode.GetBytes("]"); + + /// + /// Returns the host test case for this test, reusing the host-provided one when present and otherwise + /// materializing a single VSTest test case on demand and caching it (so deployment, test-start and every + /// reported result share one instance, matching the historical "one test case per discovered test"). + /// + internal static TestCase GetOrCreateHostTestCase(this UnitTestElement element) + { + if (element.HostRecordingHandle is not TestCase testCase) + { + testCase = element.ToTestCase(); + element.HostRecordingHandle = testCase; + } + + return testCase; + } + + /// + /// Convert the UnitTestElement instance to an Object Model testCase instance. + /// + /// The unit test element to convert. + /// An instance of . + internal static TestCase ToTestCase(this UnitTestElement element) + { + TestMethod testMethod = element.TestMethod; + + // This causes compatibility problems with older runners. + // string testFullName = this.TestMethod.HasManagedMethodAndTypeProperties + // ? $"{TestMethod.ManagedTypeName}.{TestMethod.ManagedMethodName}" + // : $"{TestMethod.FullClassName}.{TestMethod.Name}"; + string testFullName = $"{testMethod.FullClassName}.{testMethod.Name}"; + + TestCase testCase = new(testFullName, EngineConstants.ExecutorUri, testMethod.AssemblyName) + { + DisplayName = testMethod.DisplayName, + LocalExtensionData = element, + }; + + if (testMethod.HasManagedMethodAndTypeProperties) + { + testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, testMethod.ManagedTypeName); + testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, testMethod.ManagedMethodName); + } + + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, testMethod.FullClassName); + + if (testMethod.ParameterTypes is not null) + { + testCase.SetPropertyValue(AdapterTestProperties.ParameterTypesProperty, testMethod.ParameterTypes); + } + + IReadOnlyCollection? hierarchy = testMethod.Hierarchy; + if (hierarchy is { Count: > 0 }) + { + testCase.SetHierarchy([.. hierarchy]); + } + + // Set only if some test category is present + if (element.TestCategory is { Length: > 0 }) + { + testCase.SetPropertyValue(AdapterTestProperties.TestCategoryProperty, element.TestCategory); + } + + // Set priority if present + if (element.Priority != null) + { + testCase.SetPropertyValue(AdapterTestProperties.PriorityProperty, element.Priority.Value); + } + + if (element.Traits is { Length: > 0 }) + { + foreach (TestTrait trait in element.Traits) + { + testCase.Traits.Add(new Trait(trait.Name, trait.Value)); + } + } + + if (element.WorkItemIds != null) + { + testCase.SetPropertyValue(AdapterTestProperties.WorkItemIdsProperty, element.WorkItemIds); + } + +#if !WINDOWS_UWP && !WIN_UI + // The list of items to deploy before running this test. + if (element.DeploymentItems is { Length: > 0 }) + { + testCase.SetPropertyValue(AdapterTestProperties.DeploymentItemsProperty, element.DeploymentItems); + } +#endif + + // Set the Do not parallelize state if present + if (element.DoNotParallelize) + { + testCase.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, element.DoNotParallelize); + } + + if (element.UnfoldingStrategy != TestDataSourceUnfoldingStrategy.Auto) + { + testCase.SetPropertyValue(AdapterTestProperties.UnfoldingStrategy, (int)element.UnfoldingStrategy); + } + + // Store resolved data if any + if (testMethod.DataType != DynamicDataType.None) + { + testCase.SetPropertyValue(AdapterTestProperties.TestDynamicDataTypeProperty, (int)testMethod.DataType); + testCase.SetPropertyValue(AdapterTestProperties.TestDynamicDataProperty, testMethod.SerializedData); + testCase.SetPropertyValue(AdapterTestProperties.TestCaseIndexProperty, testMethod.TestCaseIndex); + // VSTest serialization doesn't handle null so instead don't set the property so that it's deserialized as null + if (testMethod.TestDataSourceIgnoreMessage is not null) + { + testCase.SetPropertyValue(AdapterTestProperties.TestDataSourceIgnoreMessageProperty, testMethod.TestDataSourceIgnoreMessage); + } + } + + testCase.LineNumber = element.DeclaringLineNumber ?? -1; + testCase.CodeFilePath = element.DeclaringFilePath; + + testCase.Id = GenerateSerializedDataStrategyTestId(element, testFullName); + + return testCase; + } + + private static Guid GenerateSerializedDataStrategyTestId(UnitTestElement element, string testFullName) + { + TestMethod testMethod = element.TestMethod; + + // Below comment is copied over from Test Platform. + // If source is a file name then just use the filename for the identifier since the file might have moved between + // discovery and execution (in appx mode for example). This is not elegant because the Source contents should be + // a black box to the framework. + // For example in the database adapter case this is not a file path. + // As discussed with team, we found no scenario for netcore, & fullclr where the Source is not present where ID + // is generated, which means we would always use FileName to generate ID. In cases where somehow Source Path + // contained garbage character the API Path.GetFileName() we are simply returning original input. + // For UWP where source during discovery, & during execution can be on different machine, in such case we should + // always use Path.GetFileName(). + string fileNameOrFilePath = testMethod.AssemblyName; + try + { + fileNameOrFilePath = Path.GetFileName(fileNameOrFilePath); + } + catch (ArgumentException) + { + // In case path contains invalid characters. + } + + byte hashVersion = 1; // Increment when changing the hashing algorithm. + var hash = new TestFx.Hashing.XxHash128(); + hash.Append(Encoding.Unicode.GetBytes(fileNameOrFilePath)); + hash.Append(Encoding.Unicode.GetBytes(testFullName)); + if (testMethod.ParameterTypes is not null) + { + hash.Append(OpenParen); + hash.Append(Encoding.Unicode.GetBytes(testMethod.ParameterTypes)); + hash.Append(CloseParen); + } + + if (testMethod.DataType != DynamicDataType.None) + { + hash.Append(OpenBracket); + hash.Append(Encoding.Unicode.GetBytes(testMethod.TestCaseIndex.ToString(CultureInfo.InvariantCulture))); + hash.Append(CloseBracket); + } + + byte[] hashBytes = hash.GetCurrentHash(); + + return VersionedGuidFromHash(hashBytes, hashVersion); + } + + internal /* for testing */ static Guid VersionedGuidFromHash(byte[] hashBytes, byte hashVersion) + { + int firstByte = 0; + int versionByte = 6; + + // We set first 4 bits to 0001, which is our version. Increase this when we change the hashing algorithm. + // + // Note: The logic below is operating on int32, because bitwise operators are not defined for byte in C#. But casting + // does not affect endianness, so we can safely assume the byte data are at the end of the int. + // We also don't care to specify the whole expansion of the int in the bit masks, which effectively ignores all the data in the int + // before the last byte data, but that is okay, they will always be zero. + hashBytes[firstByte] = (byte)((hashBytes[firstByte] & 0b0000_1111) | (hashVersion << 4)); + // We set first 4bits 7th byte to 8 to indicate that this is a version 8 UUID. https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-8 + hashBytes[versionByte] = (byte)((hashBytes[versionByte] & 0b0000_1111) | 0b1000_0000); + + // We set the first 2 bits of nineth byte to 0b10. In the guid this is stored as byte, so we don't care about endiannes. + int variantByte = 8; + hashBytes[variantByte] = (byte)((hashBytes[variantByte] & 0b0011_1111) | 0b1000_0000); + + Guid guid; + + // On .NET Framework we cannot specify the endianness of the Guid because the new Guid(hashBytes, bigEndian: true); api is not available. + // Instead we construct the int and short values manually, doing what the constructor would do, to put the bytes in big-endian order. + guid = new Guid( + (hashBytes[0] << 24) | (hashBytes[1] << 16) | (hashBytes[2] << 8) | hashBytes[3], + (short)((hashBytes[4] << 8) | hashBytes[5]), + (short)((hashBytes[6] << 8) | hashBytes[7]), + hashBytes[8], hashBytes[9], hashBytes[10], hashBytes[11], hashBytes[12], hashBytes[13], hashBytes[14], hashBytes[15]); + +#if DEBUG && NET9_0_OR_GREATER + // Version property is only available on .NET 9 and later. + Debug.Assert(guid.Version == 8, $"Expected Guid version to be 8, but it was {guid.Version}"); + // The field represents the 4 bit value, but according to the specification only the first 2 bits are used for UUID v8. + // So we shift 2 bits to the right to get the actual variant value. + int variant = guid.Variant >> 2; + Debug.Assert(variant == 2, $"Expected Guid variant to be 2, but it was {variant}"); +#endif + return guid; + } +} diff --git a/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs index d0eba71eb2..317159ed4a 100644 --- a/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Services/TestResultRecorderExtensions.cs @@ -20,7 +20,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; /// the framework ) and the VSTest result object model (TestResult, /// TestOutcome, attachments, ...). It lives in the adapter layer; the platform services engine reports /// through the neutral only. The element's host test case is resolved (and -/// materialized/cached on demand for internally discovered tests) via +/// materialized/cached on demand for internally discovered tests) via UnitTestElementExtensions.GetOrCreateHostTestCase /// so recorded results preserve host-injected data (test-case-management / data-collector properties) with full fidelity. /// internal static class TestResultRecorderExtensions diff --git a/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs b/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs index 0d22dab757..79ac2f6772 100644 --- a/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs +++ b/src/Adapter/MSTest.TestAdapter/Services/UnitTestElementSinkExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -14,7 +15,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; /// /// This is the single translation point between the neutral model and the /// VSTest discovery object model (TestCase, ITestCaseDiscoverySink). It materializes a -/// VSTest TestCase for each discovered element via . It is +/// VSTest TestCase for each discovered element via UnitTestElementExtensions.ToTestCase. It is /// expected to move entirely into the adapter layer once discovery no longer flows VSTest discovery sinks /// through the platform services (see the tracking issue linked in the pull request that removes the VSTest /// object model from platform services). diff --git a/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs b/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs index a2d0f20162..4a6d42ea3c 100644 --- a/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs +++ b/src/Adapter/MSTest.TestAdapter/TestMethodFilter.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; @@ -19,12 +19,12 @@ internal sealed class TestMethodFilter internal TestMethodFilter() => _supportedProperties = new Dictionary(StringComparer.OrdinalIgnoreCase) { - [EngineConstants.TestCategoryProperty.Label] = EngineConstants.TestCategoryProperty, - [EngineConstants.PriorityProperty.Label] = EngineConstants.PriorityProperty, + [AdapterTestProperties.TestCategoryProperty.Label] = AdapterTestProperties.TestCategoryProperty, + [AdapterTestProperties.PriorityProperty.Label] = AdapterTestProperties.PriorityProperty, [TestCaseProperties.FullyQualifiedName.Label] = TestCaseProperties.FullyQualifiedName, [TestCaseProperties.DisplayName.Label] = TestCaseProperties.DisplayName, [TestCaseProperties.Id.Label] = TestCaseProperties.Id, - [EngineConstants.TestClassNameProperty.Label] = EngineConstants.TestClassNameProperty, + [AdapterTestProperties.TestClassNameProperty.Label] = AdapterTestProperties.TestClassNameProperty, }; /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs b/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs index 1fefb90f68..3b3a2f90ed 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; - namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; internal static class EngineConstants @@ -55,107 +53,6 @@ internal static class EngineConstants /// internal const string ClassCleanupFixtureTrait = "ClassCleanup"; - #region Test Property registration - internal static readonly TestProperty WorkItemIdsProperty = TestProperty.Register("WorkItemIds", WorkItemIdsLabel, typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestClassNameProperty = TestProperty.Register("MSTestDiscoverer.TestClassName", TestClassNameLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - -#pragma warning disable CS0618 // Type or member is obsolete - internal static readonly TestProperty TestCategoryProperty = TestProperty.Register("MSTestDiscoverer.TestCategory", TestCategoryLabel, typeof(string[]), TestPropertyAttributes.Hidden | TestPropertyAttributes.Trait, typeof(TestCase)); -#pragma warning restore CS0618 // Type or member is obsolete - - internal static readonly TestProperty PriorityProperty = TestProperty.Register("MSTestDiscoverer.Priority", PriorityLabel, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - -#if !WINDOWS_UWP && !WIN_UI - internal static readonly TestProperty DeploymentItemsProperty = TestProperty.Register("MSTestDiscoverer.DeploymentItems", DeploymentItemsLabel, typeof(KeyValuePair[]), TestPropertyAttributes.Hidden, typeof(TestCase)); -#endif - - internal static readonly TestProperty DoNotParallelizeProperty = TestProperty.Register("MSTestDiscoverer.DoNotParallelize", DoNotParallelizeLabel, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty ExecutionIdProperty = TestProperty.Register("ExecutionId", ExecutionIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); - - internal static readonly TestProperty ParentExecIdProperty = TestProperty.Register("ParentExecId", ParentExecIdLabel, typeof(Guid), TestPropertyAttributes.Hidden, typeof(TestResult)); - - internal static readonly TestProperty TestRunIdProperty = TestProperty.Register(TestRunId, TestRunId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestPlanIdProperty = TestProperty.Register(TestPlanId, TestPlanId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestCaseIdProperty = TestProperty.Register(TestCaseId, TestCaseId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestPointIdProperty = TestProperty.Register(TestPointId, TestPointId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestConfigurationIdProperty = TestProperty.Register(TestConfigurationId, TestConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestConfigurationNameProperty = TestProperty.Register(TestConfigurationName, TestConfigurationName, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty IsInLabEnvironmentProperty = TestProperty.Register(IsInLabEnvironment, IsInLabEnvironment, typeof(bool), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildConfigurationIdProperty = TestProperty.Register(BuildConfigurationId, BuildConfigurationId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildDirectoryProperty = TestProperty.Register(BuildDirectory, BuildDirectory, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildFlavorProperty = TestProperty.Register(BuildFlavor, BuildFlavor, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildNumberProperty = TestProperty.Register(BuildNumber, BuildNumber, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildPlatformProperty = TestProperty.Register(BuildPlatform, BuildPlatform, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty BuildUriProperty = TestProperty.Register(BuildUri, BuildUri, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TfsServerCollectionUrlProperty = TestProperty.Register(TfsServerCollectionUrl, TfsServerCollectionUrl, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TfsTeamProjectProperty = TestProperty.Register(TfsTeamProject, TfsTeamProject, typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty ParameterTypesProperty = TestProperty.Register("MSTest.ParameterTypes", "ParameterTypes", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestDynamicDataTypeProperty = TestProperty.Register("MSTest.DynamicDataType", "DynamicDataType", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestCaseIndexProperty = TestProperty.Register("MSTest.TestCaseIndex", "TestCaseIndex", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestDynamicDataProperty = TestProperty.Register("MSTest.DynamicData", "DynamicData", typeof(string[]), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty TestDataSourceIgnoreMessageProperty = TestProperty.Register("MSTest.TestDataSourceIgnoreMessageProperty", "TestDataSourceIgnoreMessageProperty", typeof(string), TestPropertyAttributes.Hidden, typeof(TestCase)); - - internal static readonly TestProperty UnfoldingStrategy = TestProperty.Register("MSTestDiscoverer.UnfoldingStrategy", "UnfoldingStrategy", typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase)); - - #endregion - - #region Private Constants - - /// - /// These are the Test properties used by the adapter, which essentially correspond - /// to attributes on tests, and may be available in command line/TeamBuild to filter tests. - /// These Property names should not be localized. - /// - private const string TestClassNameLabel = "ClassName"; - private const string TestCategoryLabel = "TestCategory"; - private const string PriorityLabel = "Priority"; -#if !WINDOWS_UWP && !WIN_UI - private const string DeploymentItemsLabel = "DeploymentItems"; -#endif - private const string DoNotParallelizeLabel = "DoNotParallelize"; - private const string ExecutionIdLabel = "ExecutionId"; - private const string ParentExecIdLabel = "ParentExecId"; - private const string WorkItemIdsLabel = "WorkItemIds"; - - private const string TestRunId = "__Tfs_TestRunId__"; - private const string TestPlanId = "__Tfs_TestPlanId__"; - private const string TestCaseId = "__Tfs_TestCaseId__"; - private const string TestPointId = "__Tfs_TestPointId__"; - private const string TestConfigurationId = "__Tfs_TestConfigurationId__"; - private const string TestConfigurationName = "__Tfs_TestConfigurationName__"; - private const string IsInLabEnvironment = "__Tfs_IsInLabEnvironment__"; - private const string BuildConfigurationId = "__Tfs_BuildConfigurationId__"; - private const string BuildDirectory = "__Tfs_BuildDirectory__"; - private const string BuildFlavor = "__Tfs_BuildFlavor__"; - private const string BuildNumber = "__Tfs_BuildNumber__"; - private const string BuildPlatform = "__Tfs_BuildPlatform__"; - private const string BuildUri = "__Tfs_BuildUri__"; - private const string TfsServerCollectionUrl = "__Tfs_TfsServerCollectionUrl__"; - private const string TfsTeamProject = "__Tfs_TeamProject__"; - - #endregion - /// /// Uri of the MSTest executor. /// diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs b/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs deleted file mode 100644 index 30c8af1f17..0000000000 --- a/src/Adapter/MSTestAdapter.PlatformServices/Execution/TcmTestPropertiesProvider.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; - -using TestPlatformObjectModel = Microsoft.VisualStudio.TestPlatform.ObjectModel; - -namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; - -/// -/// Reads and parses the test-case-management (TCM) properties supplied by a host on a test case, in order to -/// surface them to the running test through TestContext. -/// -/// -/// This is a translation point between the VSTest test-case object model and the neutral execution context bag -/// consumed by the platform services engine. It runs at the adapter boundary (when a host hands the adapter -/// concrete test cases to run) and produces a string-keyed dictionary so the engine never depends on the VSTest -/// TestProperty object model. It is expected to move entirely into the adapter layer once the platform -/// services no longer reference the VSTest object model. -/// -internal static class TcmTestPropertiesProvider -{ - /// - /// Gets the host execution context properties from a test case, keyed by the host property identifier. - /// - /// Test case. - /// The properties, or when the test case is null or carries no test case id. - public static IReadOnlyDictionary? GetTcmProperties(TestPlatformObjectModel.TestCase? testCase) - { - // Return empty properties when testCase is null or when test case id is zero. - if (testCase == null || - testCase.GetPropertyValue(EngineConstants.TestCaseIdProperty, default) == 0) - { - return null; - } - - var tcmProperties = new Dictionary(capacity: 15) - { - // Step 1: Add common properties. - [EngineConstants.TestRunIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestRunIdProperty, default), - [EngineConstants.TestPlanIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestPlanIdProperty, default), - [EngineConstants.BuildConfigurationIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildConfigurationIdProperty, default), - [EngineConstants.BuildDirectoryProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildDirectoryProperty, default), - [EngineConstants.BuildFlavorProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildFlavorProperty, default), - [EngineConstants.BuildNumberProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildNumberProperty, default), - [EngineConstants.BuildPlatformProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildPlatformProperty, default), - [EngineConstants.BuildUriProperty.Id] = testCase.GetPropertyValue(EngineConstants.BuildUriProperty, default), - [EngineConstants.TfsServerCollectionUrlProperty.Id] = testCase.GetPropertyValue(EngineConstants.TfsServerCollectionUrlProperty, default), - [EngineConstants.TfsTeamProjectProperty.Id] = testCase.GetPropertyValue(EngineConstants.TfsTeamProjectProperty, default), - [EngineConstants.IsInLabEnvironmentProperty.Id] = testCase.GetPropertyValue(EngineConstants.IsInLabEnvironmentProperty, default), - - // Step 2: Add test case specific properties. - [EngineConstants.TestCaseIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestCaseIdProperty, default), - [EngineConstants.TestConfigurationIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestConfigurationIdProperty, default), - [EngineConstants.TestConfigurationNameProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestConfigurationNameProperty, default), - [EngineConstants.TestPointIdProperty.Id] = testCase.GetPropertyValue(EngineConstants.TestPointIdProperty, default), - }; - - return tcmProperties; - } -} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs index 56c2fb4710..8549fcc16e 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/UnitTestElement.cs @@ -1,9 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; -using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; @@ -17,11 +14,6 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; [DebuggerDisplay("{TestMethod.DisplayName} ({TestMethod.ManagedTypeName})")] internal sealed class UnitTestElement { - private static readonly byte[] OpenParen = [40, 0]; // Encoding.Unicode.GetBytes("("); - private static readonly byte[] CloseParen = [41, 0]; // Encoding.Unicode.GetBytes(")"); - private static readonly byte[] OpenBracket = [91, 0]; // Encoding.Unicode.GetBytes("["); - private static readonly byte[] CloseBracket = [93, 0]; // Encoding.Unicode.GetBytes("]"); - /// /// Initializes a new instance of the class. /// @@ -100,7 +92,7 @@ public UnitTestElement(TestMethod testMethod) /// data-collector properties — that the neutral model does not otherwise carry) and to describe it to the /// (still VSTest-based) deployment service. For tests handed to the adapter by a host it is that original /// test case; for tests discovered internally by the platform services it is until - /// materialized on demand by . The execution engine treats it as an + /// materialized on demand by the adapter's UnitTestElementExtensions.GetOrCreateHostTestCase. The execution engine treats it as an /// opaque handle — it reads nothing VSTest-specific off it and only threads it into the result recorder and /// the deployment boundary. /// @@ -110,22 +102,6 @@ public UnitTestElement(TestMethod testMethod) #endif internal object? HostRecordingHandle { get; set; } - /// - /// Returns the host test case for this test, reusing the host-provided one when present and otherwise - /// materializing a single VSTest test case on demand and caching it (so deployment, test-start and every - /// reported result share one instance, matching the historical "one test case per discovered test"). - /// - internal TestCase GetOrCreateHostTestCase() - { - if (HostRecordingHandle is not TestCase testCase) - { - testCase = ToTestCase(); - HostRecordingHandle = testCase; - } - - return testCase; - } - internal UnitTestElement Clone() { var clone = (UnitTestElement)MemberwiseClone(); @@ -165,194 +141,4 @@ internal UnitTestElement CloneWithSource(string source) /// An element whose is . internal UnitTestElement WithUpdatedSource(string source) => TestMethod.AssemblyName == source ? this : CloneWithSource(source); - - /// - /// Convert the UnitTestElement instance to an Object Model testCase instance. - /// - /// An instance of . - internal TestCase ToTestCase() - { - // This causes compatibility problems with older runners. - // string testFullName = this.TestMethod.HasManagedMethodAndTypeProperties - // ? $"{TestMethod.ManagedTypeName}.{TestMethod.ManagedMethodName}" - // : $"{TestMethod.FullClassName}.{TestMethod.Name}"; - string testFullName = $"{TestMethod.FullClassName}.{TestMethod.Name}"; - - TestCase testCase = new(testFullName, EngineConstants.ExecutorUri, TestMethod.AssemblyName) - { - DisplayName = TestMethod.DisplayName, - LocalExtensionData = this, - }; - - if (TestMethod.HasManagedMethodAndTypeProperties) - { - testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, TestMethod.ManagedTypeName); - testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, TestMethod.ManagedMethodName); - } - - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, TestMethod.FullClassName); - - if (TestMethod.ParameterTypes is not null) - { - testCase.SetPropertyValue(EngineConstants.ParameterTypesProperty, TestMethod.ParameterTypes); - } - - IReadOnlyCollection? hierarchy = TestMethod.Hierarchy; - if (hierarchy is { Count: > 0 }) - { - testCase.SetHierarchy([.. hierarchy]); - } - - // Set only if some test category is present - if (TestCategory is { Length: > 0 }) - { - testCase.SetPropertyValue(EngineConstants.TestCategoryProperty, TestCategory); - } - - // Set priority if present - if (Priority != null) - { - testCase.SetPropertyValue(EngineConstants.PriorityProperty, Priority.Value); - } - - if (Traits is { Length: > 0 }) - { - foreach (TestTrait trait in Traits) - { - testCase.Traits.Add(new Trait(trait.Name, trait.Value)); - } - } - - if (WorkItemIds != null) - { - testCase.SetPropertyValue(EngineConstants.WorkItemIdsProperty, WorkItemIds); - } - -#if !WINDOWS_UWP && !WIN_UI - // The list of items to deploy before running this test. - if (DeploymentItems is { Length: > 0 }) - { - testCase.SetPropertyValue(EngineConstants.DeploymentItemsProperty, DeploymentItems); - } -#endif - - // Set the Do not parallelize state if present - if (DoNotParallelize) - { - testCase.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, DoNotParallelize); - } - - if (UnfoldingStrategy != TestDataSourceUnfoldingStrategy.Auto) - { - testCase.SetPropertyValue(EngineConstants.UnfoldingStrategy, (int)UnfoldingStrategy); - } - - // Store resolved data if any - if (TestMethod.DataType != DynamicDataType.None) - { - testCase.SetPropertyValue(EngineConstants.TestDynamicDataTypeProperty, (int)TestMethod.DataType); - testCase.SetPropertyValue(EngineConstants.TestDynamicDataProperty, TestMethod.SerializedData); - testCase.SetPropertyValue(EngineConstants.TestCaseIndexProperty, TestMethod.TestCaseIndex); - // VSTest serialization doesn't handle null so instead don't set the property so that it's deserialized as null - if (TestMethod.TestDataSourceIgnoreMessage is not null) - { - testCase.SetPropertyValue(EngineConstants.TestDataSourceIgnoreMessageProperty, TestMethod.TestDataSourceIgnoreMessage); - } - } - - testCase.LineNumber = DeclaringLineNumber ?? -1; - testCase.CodeFilePath = DeclaringFilePath; - - SetTestCaseId(testCase, testFullName); - - return testCase; - } - - private void SetTestCaseId(TestCase testCase, string testFullName) - => testCase.Id = GenerateSerializedDataStrategyTestId(testFullName); - - private Guid GenerateSerializedDataStrategyTestId(string testFullName) - { - // Below comment is copied over from Test Platform. - // If source is a file name then just use the filename for the identifier since the file might have moved between - // discovery and execution (in appx mode for example). This is not elegant because the Source contents should be - // a black box to the framework. - // For example in the database adapter case this is not a file path. - // As discussed with team, we found no scenario for netcore, & fullclr where the Source is not present where ID - // is generated, which means we would always use FileName to generate ID. In cases where somehow Source Path - // contained garbage character the API Path.GetFileName() we are simply returning original input. - // For UWP where source during discovery, & during execution can be on different machine, in such case we should - // always use Path.GetFileName(). - string fileNameOrFilePath = TestMethod.AssemblyName; - try - { - fileNameOrFilePath = Path.GetFileName(fileNameOrFilePath); - } - catch (ArgumentException) - { - // In case path contains invalid characters. - } - - byte hashVersion = 1; // Increment when changing the hashing algorithm. - var hash = new TestFx.Hashing.XxHash128(); - hash.Append(Encoding.Unicode.GetBytes(fileNameOrFilePath)); - hash.Append(Encoding.Unicode.GetBytes(testFullName)); - if (TestMethod.ParameterTypes is not null) - { - hash.Append(OpenParen); - hash.Append(Encoding.Unicode.GetBytes(TestMethod.ParameterTypes)); - hash.Append(CloseParen); - } - - if (TestMethod.DataType != DynamicDataType.None) - { - hash.Append(OpenBracket); - hash.Append(Encoding.Unicode.GetBytes(TestMethod.TestCaseIndex.ToString(CultureInfo.InvariantCulture))); - hash.Append(CloseBracket); - } - - byte[] hashBytes = hash.GetCurrentHash(); - - return VersionedGuidFromHash(hashBytes, hashVersion); - } - - internal /* for testing */ static Guid VersionedGuidFromHash(byte[] hashBytes, byte hashVersion) - { - int firstByte = 0; - int versionByte = 6; - - // We set first 4 bits to 0001, which is our version. Increase this when we change the hashing algorithm. - // - // Note: The logic below is operating on int32, because bitwise operators are not defined for byte in C#. But casting - // does not affect endianness, so we can safely assume the byte data are at the end of the int. - // We also don't care to specify the whole expansion of the int in the bit masks, which effectively ignores all the data in the int - // before the last byte data, but that is okay, they will always be zero. - hashBytes[firstByte] = (byte)((hashBytes[firstByte] & 0b0000_1111) | (hashVersion << 4)); - // We set first 4bits 7th byte to 8 to indicate that this is a version 8 UUID. https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-8 - hashBytes[versionByte] = (byte)((hashBytes[versionByte] & 0b0000_1111) | 0b1000_0000); - - // We set the first 2 bits of nineth byte to 0b10. In the guid this is stored as byte, so we don't care about endiannes. - int variantByte = 8; - hashBytes[variantByte] = (byte)((hashBytes[variantByte] & 0b0011_1111) | 0b1000_0000); - - Guid guid; - - // On .NET Framework we cannot specify the endianness of the Guid because the new Guid(hashBytes, bigEndian: true); api is not available. - // Instead we construct the int and short values manually, doing what the constructor would do, to put the bytes in big-endian order. - guid = new Guid( - (hashBytes[0] << 24) | (hashBytes[1] << 16) | (hashBytes[2] << 8) | hashBytes[3], - (short)((hashBytes[4] << 8) | hashBytes[5]), - (short)((hashBytes[6] << 8) | hashBytes[7]), - hashBytes[8], hashBytes[9], hashBytes[10], hashBytes[11], hashBytes[12], hashBytes[13], hashBytes[14], hashBytes[15]); - -#if DEBUG && NET9_0_OR_GREATER - // Version property is only available on .NET 9 and later. - Debug.Assert(guid.Version == 8, $"Expected Guid version to be 8, but it was {guid.Version}"); - // The field represents the 4 bit value, but according to the specification only the first 2 bits are used for UUID v8. - // So we shift 2 bits to the right to get the actual variant value. - int variant = guid.Variant >> 2; - Debug.Assert(variant == 2, $"Expected Guid variant to be 2, but it was {variant}"); -#endif - return guid; - } } diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs index 69fe685e75..f0b4f62f51 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TcmTestPropertiesProviderTests.cs @@ -3,6 +3,7 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.ObjectModel; @@ -15,21 +16,21 @@ public class TcmTestPropertiesProviderTests : TestContainer { private readonly TestProperty[] _tcmKnownProperties = [ - EngineConstants.TestRunIdProperty, - EngineConstants.TestPlanIdProperty, - EngineConstants.BuildConfigurationIdProperty, - EngineConstants.BuildDirectoryProperty, - EngineConstants.BuildFlavorProperty, - EngineConstants.BuildNumberProperty, - EngineConstants.BuildPlatformProperty, - EngineConstants.BuildUriProperty, - EngineConstants.TfsServerCollectionUrlProperty, - EngineConstants.TfsTeamProjectProperty, - EngineConstants.IsInLabEnvironmentProperty, - EngineConstants.TestCaseIdProperty, - EngineConstants.TestConfigurationIdProperty, - EngineConstants.TestConfigurationNameProperty, - EngineConstants.TestPointIdProperty, + AdapterTestProperties.TestRunIdProperty, + AdapterTestProperties.TestPlanIdProperty, + AdapterTestProperties.BuildConfigurationIdProperty, + AdapterTestProperties.BuildDirectoryProperty, + AdapterTestProperties.BuildFlavorProperty, + AdapterTestProperties.BuildNumberProperty, + AdapterTestProperties.BuildPlatformProperty, + AdapterTestProperties.BuildUriProperty, + AdapterTestProperties.TfsServerCollectionUrlProperty, + AdapterTestProperties.TfsTeamProjectProperty, + AdapterTestProperties.IsInLabEnvironmentProperty, + AdapterTestProperties.TestCaseIdProperty, + AdapterTestProperties.TestConfigurationIdProperty, + AdapterTestProperties.TestConfigurationNameProperty, + AdapterTestProperties.TestPointIdProperty, ]; public void GetTcmPropertiesShouldReturnEmptyDictionaryIfTestCaseIsNull() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs index 4e15c282a4..464ea6f415 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Execution/TestExecutionManagerTests.cs @@ -35,21 +35,21 @@ public class TestExecutionManagerTests : TestContainer private readonly TestProperty[] _tcmKnownProperties = [ - EngineConstants.TestRunIdProperty, - EngineConstants.TestPlanIdProperty, - EngineConstants.BuildConfigurationIdProperty, - EngineConstants.BuildDirectoryProperty, - EngineConstants.BuildFlavorProperty, - EngineConstants.BuildNumberProperty, - EngineConstants.BuildPlatformProperty, - EngineConstants.BuildUriProperty, - EngineConstants.TfsServerCollectionUrlProperty, - EngineConstants.TfsTeamProjectProperty, - EngineConstants.IsInLabEnvironmentProperty, - EngineConstants.TestCaseIdProperty, - EngineConstants.TestConfigurationIdProperty, - EngineConstants.TestConfigurationNameProperty, - EngineConstants.TestPointIdProperty, + AdapterTestProperties.TestRunIdProperty, + AdapterTestProperties.TestPlanIdProperty, + AdapterTestProperties.BuildConfigurationIdProperty, + AdapterTestProperties.BuildDirectoryProperty, + AdapterTestProperties.BuildFlavorProperty, + AdapterTestProperties.BuildNumberProperty, + AdapterTestProperties.BuildPlatformProperty, + AdapterTestProperties.BuildUriProperty, + AdapterTestProperties.TfsServerCollectionUrlProperty, + AdapterTestProperties.TfsTeamProjectProperty, + AdapterTestProperties.IsInLabEnvironmentProperty, + AdapterTestProperties.TestCaseIdProperty, + AdapterTestProperties.TestConfigurationIdProperty, + AdapterTestProperties.TestConfigurationNameProperty, + AdapterTestProperties.TestPointIdProperty, ]; private TestableRunContextTestExecutionTests _runContext; @@ -690,8 +690,8 @@ public async Task RunTestsForTestShouldRunNonParallelizableTestsSeparately() TestCase testCase3 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod3"); TestCase testCase4 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod4"); - testCase3.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, true); - testCase4.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, true); + testCase3.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, true); + testCase4.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, true); TestCase[] tests = [testCase1, testCase2, testCase3, testCase4]; _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( @@ -800,7 +800,7 @@ private async Task RunTestsForTestShouldRunTestsInTheParentDomainsApartmentState TestCase testCase3 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod3"); TestCase testCase4 = GetTestCase(typeof(DummyTestClassWithDoNotParallelizeMethods), "TestMethod4"); - testCase4.SetPropertyValue(EngineConstants.DoNotParallelizeProperty, true); + testCase4.SetPropertyValue(AdapterTestProperties.DoNotParallelizeProperty, true); TestCase[] tests = [testCase1, testCase2, testCase3, testCase4]; _runContext.MockRunSettings.Setup(rs => rs.SettingsXml).Returns( diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs index 9aa147eb9e..b515c22a03 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestCaseExtensionsTests.cs @@ -3,6 +3,7 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -22,9 +23,9 @@ public void ToUnitTestElementShouldReturnUnitTestElementWithFieldsSet() }; string[] testCategories = ["DummyCategory"]; - testCase.SetPropertyValue(EngineConstants.PriorityProperty, 2); - testCase.SetPropertyValue(EngineConstants.TestCategoryProperty, testCategories); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName"); + testCase.SetPropertyValue(AdapterTestProperties.PriorityProperty, 2); + testCase.SetPropertyValue(AdapterTestProperties.TestCategoryProperty, testCategories); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "DummyClassName"); UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); @@ -37,7 +38,7 @@ public void ToUnitTestElementShouldReturnUnitTestElementWithFieldsSet() public void ToUnitTestElementForTestCaseWithNoPropertiesShouldReturnUnitTestElementWithDefaultFields() { TestCase testCase = new("DummyClass.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName"); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "DummyClassName"); UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); @@ -49,7 +50,7 @@ public void ToUnitTestElementForTestCaseWithNoPropertiesShouldReturnUnitTestElem public void ToUnitTestElementShouldAddDeclaringClassNameToTestElementWhenAvailable() { TestCase testCase = new("DummyClass.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "DummyClassName"); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "DummyClassName"); UnitTestElement resultUnitTestElement = testCase.ToUnitTestElementWithUpdatedSource(testCase.Source); @@ -59,7 +60,7 @@ public void ToUnitTestElementShouldAddDeclaringClassNameToTestElementWhenAvailab public void ToUnitTestElementShouldPreferManagedTypeOverTestClassNameWhenAvailable() { TestCase testCase = new("SemanticClassName.DummyMethod", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, "SyntacticClassName"); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, "SyntacticClassName"); testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, "SemanticClassName"); testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, "DummyMethod"); @@ -74,7 +75,7 @@ public void ToUnitTestElementShouldParseLegacyClosedGenericFullyQualifiedNameWhe Type closedType = typeof(DummyGenericTestClass); string methodName = nameof(DummyGenericTestClass<>.GenericTestMethod); TestCase testCase = new($"{closedType.FullName}.{methodName}", new("DummyUri", UriKind.Relative), Assembly.GetCallingAssembly().FullName!); - testCase.SetPropertyValue(EngineConstants.TestClassNameProperty, closedType.FullName); + testCase.SetPropertyValue(AdapterTestProperties.TestClassNameProperty, closedType.FullName); testCase.SetPropertyValue(TestCaseExtensions.ManagedTypeProperty, typeof(DummyGenericTestClass<>).FullName); testCase.SetPropertyValue(TestCaseExtensions.ManagedMethodProperty, methodName); diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs index 8eb46d4545..cd07a3efac 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Extensions/TestResultExtensionsTests.cs @@ -3,6 +3,7 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -153,8 +154,8 @@ public void ToUnitTestResultsForTestResultShouldSetParentInfo() var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new()); - ((Guid)convertedResult.GetPropertyValue(EngineConstants.ExecutionIdProperty)!).Should().Be(executionId); - ((Guid)convertedResult.GetPropertyValue(EngineConstants.ParentExecIdProperty)!).Should().Be(parentExecId); + ((Guid)convertedResult.GetPropertyValue(AdapterTestProperties.ExecutionIdProperty)!).Should().Be(executionId); + ((Guid)convertedResult.GetPropertyValue(AdapterTestProperties.ParentExecIdProperty)!).Should().Be(parentExecId); } public void ToUnitTestResultsShouldHaveResultsFileProvidedToTestResult() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs index 45ce00f498..8a13019086 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModel/UnitTestElementTests.cs @@ -3,6 +3,7 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -113,7 +114,7 @@ public void ToTestCaseShouldSetTestClassNameProperty() { var testCase = _unitTestElement.ToTestCase(); - (testCase.GetPropertyValue(EngineConstants.TestClassNameProperty) as string).Should().Be("C"); + (testCase.GetPropertyValue(AdapterTestProperties.TestClassNameProperty) as string).Should().Be("C"); } public void ToTestCaseShouldUseFullClassNameAsManagedTypeName() @@ -129,17 +130,17 @@ public void ToTestCaseShouldSetTestCategoryIfPresent() _unitTestElement.TestCategory = null; var testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.TestCategoryProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty).Should().BeNull(); _unitTestElement.TestCategory = []; testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.TestCategoryProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty).Should().BeNull(); _unitTestElement.TestCategory = ["TC"]; testCase = _unitTestElement.ToTestCase(); - new string[] { "TC" }.SequenceEqual((string[])testCase.GetPropertyValue(EngineConstants.TestCategoryProperty)!).Should().BeTrue(); + new string[] { "TC" }.SequenceEqual((string[])testCase.GetPropertyValue(AdapterTestProperties.TestCategoryProperty)!).Should().BeTrue(); } public void ToTestCaseShouldSetPriorityIfPresent() @@ -147,12 +148,12 @@ public void ToTestCaseShouldSetPriorityIfPresent() _unitTestElement.Priority = null; var testCase = _unitTestElement.ToTestCase(); - ((int)testCase.GetPropertyValue(EngineConstants.PriorityProperty)!).Should().Be(0); + ((int)testCase.GetPropertyValue(AdapterTestProperties.PriorityProperty)!).Should().Be(0); _unitTestElement.Priority = 1; testCase = _unitTestElement.ToTestCase(); - ((int)testCase.GetPropertyValue(EngineConstants.PriorityProperty)!).Should().Be(1); + ((int)testCase.GetPropertyValue(AdapterTestProperties.PriorityProperty)!).Should().Be(1); } public void ToTestCaseShouldSetTraitsIfPresent() @@ -179,7 +180,7 @@ public void ToTestCaseShouldSetPropertiesIfPresent() var testCase = _unitTestElement.ToTestCase(); - ((string[])testCase.GetPropertyValue(EngineConstants.WorkItemIdsProperty)!).Should().Equal(["2312", "22332"]); + ((string[])testCase.GetPropertyValue(AdapterTestProperties.WorkItemIdsProperty)!).Should().Equal(["2312", "22332"]); } #if !WINDOWS_UWP && !WIN_UI @@ -188,17 +189,17 @@ public void ToTestCaseShouldSetDeploymentItemPropertyIfPresent() _unitTestElement.DeploymentItems = null; var testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.DeploymentItemsProperty).Should().BeNull(); _unitTestElement.DeploymentItems = []; testCase = _unitTestElement.ToTestCase(); - testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty).Should().BeNull(); + testCase.GetPropertyValue(AdapterTestProperties.DeploymentItemsProperty).Should().BeNull(); _unitTestElement.DeploymentItems = [new("s", "d")]; testCase = _unitTestElement.ToTestCase(); - _unitTestElement.DeploymentItems.SequenceEqual(testCase.GetPropertyValue(EngineConstants.DeploymentItemsProperty) as KeyValuePair[]).Should().BeTrue(); + _unitTestElement.DeploymentItems.SequenceEqual(testCase.GetPropertyValue(AdapterTestProperties.DeploymentItemsProperty) as KeyValuePair[]).Should().BeTrue(); } #endif @@ -226,7 +227,7 @@ public void ToTestCase_WhenStrategyIsData_DoesNotUseDefaultTestCaseId() static Guid GuidFromString(string data) { byte[] hash = TestFx.Hashing.XxHash128.Hash(Encoding.Unicode.GetBytes(data)); - return UnitTestElement.VersionedGuidFromHash(hash, hashVersion: 1); + return UnitTestElementExtensions.VersionedGuidFromHash(hash, hashVersion: 1); } } From fbcafef8a565a64d40760915ed4385aee22af2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 14:03:27 +0200 Subject: [PATCH 18/24] Neutralize runsettings parsing helpers (Phase 6e-4a) Decouple the runsettings-XML parsing files from the VSTest object model: - Replace the VSTest SettingsException thrown during runsettings/test-run-parameter parsing (RunSettingsUtilities, TestRunParameters, MSTestAdapterSettings) with a new neutral InvalidRunSettingsException. This exception is deliberately DISTINCT from the existing AdapterSettingsException to preserve behavior byte-for-byte: the only typed settings-error handler, MSTestDiscovererHelpers.InitializeDiscovery, catches AdapterSettingsException (invalid MSTest settings values -> report + no tests), while a structural runsettings error historically threw VSTest SettingsException and escaped that handler to the host. The malformed throw site is reachable through PopulateSettings via SettingsProvider.Load, so reusing AdapterSettingsException there would have changed the escape semantics; the distinct InvalidRunSettingsException (unrelated to AdapterSettingsException) preserves them. The other sites are caught only by a broad catch(Exception) in CacheSessionParameters, so behavior there is identical either way. Only the direct typed-throw unit assertions change. - Inline the VSTest ObjectModel.Constants runsettings node names (RunConfiguration, TestRunParameters) as neutral constants. - Repoint XmlRunSettingsUtilities.ReaderSettings at the equivalent neutral RunSettingsUtilities.ReaderSettings that already existed. - Add a neutral XmlReaderUtilities (ReadToRootNode + ReadToNextElement/SkipToNextElement) replacing the VSTest ObjectModel.Utilities helpers, reusing the exact navigation semantics the adapter already vendored privately in RunConfigurationSettings. Drops the PlatformServices VSTest-ObjectModel coupling from 10 to 6 files (the remaining are the netfx residuals + AssemblyResolver string literals). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Helpers/RunSettingsUtilities.cs | 16 ++++-- .../Helpers/TestRunParameters.cs | 6 +- .../Helpers/XmlReaderUtilities.cs | 57 +++++++++++++++++++ .../MSTestSettings.RunSettingsXml.cs | 6 +- .../InvalidRunSettingsException.cs | 20 +++++++ .../Services/MSTestAdapterSettings.cs | 8 +-- .../Helpers/RunSettingsUtilitiesTests.cs | 6 +- .../Services/MSTestAdapterSettingsTests.cs | 20 +++---- 8 files changed, 110 insertions(+), 29 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/InvalidRunSettingsException.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs index a8d30dc0de..9d52227cd5 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/RunSettingsUtilities.cs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; @@ -18,6 +17,11 @@ internal static class RunSettingsUtilities IgnoreWhitespace = true, }; + // The runsettings node names, historically taken from the VSTest object model's Constants type. + internal const string RunConfigurationSettingsName = "RunConfiguration"; + + internal const string TestRunParametersName = "TestRunParameters"; + /// /// Gets the set of user defined test run parameters from settings xml as key value pairs. /// @@ -25,23 +29,23 @@ internal static class RunSettingsUtilities /// The test run parameters. /// If there is no test run parameters section defined in the settingsxml a blank dictionary is returned. internal static Dictionary? GetTestRunParameters(string? settingsXml) - => GetNodeValue(settingsXml, Constants.TestRunParametersName, TestRunParameters.FromXml); + => GetNodeValue(settingsXml, TestRunParametersName, TestRunParameters.FromXml); /// /// Throws if the node has an attribute. /// /// The reader. - /// Thrown if the node has an attribute. + /// Thrown if the node has an attribute. internal static void ThrowOnHasAttributes(XmlReader reader) { if (reader.HasAttributes) { reader.MoveToNextAttribute(); - throw new SettingsException( + throw new InvalidRunSettingsException( string.Format( CultureInfo.CurrentCulture, Resource.InvalidSettingsXmlAttribute, - TestPlatform.ObjectModel.Constants.RunConfigurationSettingsName, + RunConfigurationSettingsName, reader.Name)); } } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs index 9a324be172..c891b114d4 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/TestRunParameters.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; @@ -47,11 +47,11 @@ internal static Dictionary FromXml(XmlReader reader) break; default: - throw new SettingsException( + throw new InvalidRunSettingsException( string.Format( CultureInfo.CurrentCulture, Resource.InvalidSettingsXmlElement, - Constants.TestRunParametersName, + RunSettingsUtilities.TestRunParametersName, reader.Name)); } diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs new file mode 100644 index 0000000000..7daf921451 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Helpers/XmlReaderUtilities.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; + +/// +/// Minimal, platform-agnostic navigation helpers for reading runsettings XML. +/// These replace the equivalent helpers from the VSTest object model so the platform services layer does not +/// depend on it; the navigation semantics are unchanged. +/// +internal static class XmlReaderUtilities +{ + private const string RunSettingsRootNodeName = "RunSettings"; + + /// + /// Advances the reader to the root element and verifies it is the <RunSettings> node. + /// + /// The reader positioned before the root element. + /// Thrown when the root element is not <RunSettings>. + internal static void ReadToRootNode(XmlReader reader) + { + reader.ReadToNextElement(); + + // Verify that it is a "RunSettings" node. + if (reader.Name != RunSettingsRootNodeName) + { + throw new InvalidRunSettingsException($"Could not find '{RunSettingsRootNodeName}' node in the runsettings XML. Found '<{reader.Name}>' instead."); + } + } + + /// + /// Reads until the next element node (or end of document). + /// + /// The reader. + internal static void ReadToNextElement(this XmlReader reader) + { + while (!reader.EOF && reader.Read() && reader.NodeType != XmlNodeType.Element) + { + } + } + + /// + /// Skips the current subtree and positions the reader on the next element node. + /// + /// The reader. + internal static void SkipToNextElement(this XmlReader reader) + { + reader.Skip(); + + if (reader.NodeType != XmlNodeType.Element) + { + reader.ReadToNextElement(); + } + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs index a306cff8be..84d021f440 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.RunSettingsXml.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using DebuggerLaunchMode = Microsoft.VisualStudio.TestTools.UnitTesting.DebuggerLaunchMode; using MessageLevel = Microsoft.VisualStudio.TestTools.UnitTesting.MessageLevel; @@ -45,7 +45,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) } using var stringReader = new StringReader(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); XmlReaderUtilities.ReadToRootNode(reader); reader.ReadToNextElement(); @@ -69,7 +69,7 @@ private static bool RunSettingsFileHasMSTestSettings(string? runSettingsXml) } using var stringReader = new StringReader(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); XmlReaderUtilities.ReadToRootNode(reader); reader.ReadToNextElement(); diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/InvalidRunSettingsException.cs b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/InvalidRunSettingsException.cs new file mode 100644 index 0000000000..3e9a421915 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/ObjectModel/InvalidRunSettingsException.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + +/// +/// Thrown when the runsettings XML is structurally invalid (for example a bad attribute, an unexpected element, +/// or a wrong root node). This is the platform-agnostic replacement for the settings exception the platform +/// services layer historically surfaced from the VSTest object model. It is intentionally distinct from +/// : is caught by the discovery +/// initialization path (reported and treated as "no tests"), whereas a structural runsettings error propagates +/// to the host, preserving the original behavior. +/// +internal sealed class InvalidRunSettingsException : Exception +{ + internal InvalidRunSettingsException(string? message) + : base(message) + { + } +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs index d844431a45..c10ea5169b 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/MSTestAdapterSettings.cs @@ -4,9 +4,9 @@ #if !WINDOWS_UWP using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -206,7 +206,7 @@ public static bool IsAppDomainCreationDisabled(string? settingsXml) if (!StringEx.IsNullOrEmpty(settingsXml)) { StringReader stringReader = new(settingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); var xmlDoc = new XmlDocument() { XmlResolver = null }; xmlDoc.Load(reader); @@ -384,7 +384,7 @@ private void ReadAssemblyResolutionPath(XmlReader reader) else { string message = string.Format(CultureInfo.CurrentCulture, Resource.InvalidSettingsXmlElement, reader.Name, "AssemblyResolution"); - throw new SettingsException(message); + throw new InvalidRunSettingsException(message); } // Move to the next element under tag AssemblyResolution diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs index e2301915a4..b09f10bfd0 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Helpers/RunSettingsUtilitiesTests.cs @@ -4,7 +4,7 @@ using AwesomeAssertions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using TestFramework.ForTestingMSTest; @@ -131,7 +131,7 @@ public void GetTestRunParametersThrowsWhenTRPNodeHasAttributes() """; - new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); + new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); } public void GetTestRunParametersThrowsWhenTRPNodeHasNonParameterTypeChildNodes() @@ -152,7 +152,7 @@ public void GetTestRunParametersThrowsWhenTRPNodeHasNonParameterTypeChildNodes() """; - new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); + new Action(() => RunSettingsUtilities.GetTestRunParameters(settingsXml)).Should().Throw(); } public void GetTestRunParametersIgnoresMalformedKeyValues() diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs index eaa1380860..89b89ef3aa 100644 --- a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/Services/MSTestAdapterSettingsTests.cs @@ -3,10 +3,10 @@ using AwesomeAssertions; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; +using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; using Moq; @@ -202,7 +202,7 @@ public void ToSettingsShouldNotThrowExceptionWhenRunSettingsXmlUnderTagMSTestV2I """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); MSTestAdapterSettings.ToSettings(reader); @@ -222,12 +222,12 @@ public void ToSettingsShouldThrowExceptionWhenRunSettingsXmlIsWrong() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); void ShouldThrowException() => MSTestAdapterSettings.ToSettings(reader); - new Action(ShouldThrowException).Should().Throw(); + new Action(ShouldThrowException).Should().Throw(); } #endregion @@ -242,7 +242,7 @@ public void DeploymentEnabledIsByDefaultTrueWhenNotSpecified() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeploymentEnabled.Should().BeTrue(); @@ -257,7 +257,7 @@ public void DeploymentEnabledShouldBeConsumedFromRunSettingsWhenSpecified() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeploymentEnabled.Should().BeFalse(); @@ -275,7 +275,7 @@ public void DeployTestSourceDependenciesIsEnabledByDefault() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeployTestSourceDependencies.Should().BeTrue(); @@ -290,7 +290,7 @@ public void DeployTestSourceDependenciesWhenFalse() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeployTestSourceDependencies.Should().BeFalse(); @@ -305,7 +305,7 @@ public void DeployTestSourceDependenciesWhenTrue() """; StringReader stringReader = new(runSettingsXml); - var reader = XmlReader.Create(stringReader, XmlRunSettingsUtilities.ReaderSettings); + var reader = XmlReader.Create(stringReader, RunSettingsUtilities.ReaderSettings); reader.Read(); var adapterSettings = MSTestAdapterSettings.ToSettings(reader); adapterSettings.DeployTestSourceDependencies.Should().BeTrue(); From 24e471c358a81ee3c053f4a77b7c031896c20557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 14:32:14 +0200 Subject: [PATCH 19/24] Resolve the object-model assembly by name in AppDomain wiring (Phase 6e-4b) Remove the compile-time VSTest object-model dependency from the netfx AppDomain wiring: AppDomainUtilities used typeof(TestCase).Assembly to (1) add the object-model assembly's directory to the child app-domain's resolution paths and (2) anchor the 11.0 -> current binding redirect. Both run parent-side during test source host setup, after the adapter has already loaded the object model, so the assembly is resolved by simple name from the current domain instead - returning the same (post-redirect) assembly identity the type reference did, without a compile-time reference. The only remaining mention of the object model in this file is the assembly's simple name as a string literal (used for the lookup and, formerly, by the resolver's skip-list), which is not an assembly reference and does not block dropping the package. Proven on the netfx AppDomain scenario the type reference protects: PlatformServices.Desktop.IntegrationTests (assembly-resolution-from-runsettings + deployment app-domain paths) stays green (15/15). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Utilities/AppDomainUtilities.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs index a5751f5031..f3d0b2afbc 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/AppDomainUtilities.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if NETFRAMEWORK @@ -6,7 +6,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; @@ -18,9 +17,22 @@ internal static class AppDomainUtilities { private const string ObjectModelVersionBuiltAgainst = "11.0.0.0"; + private const string ObjectModelAssemblyName = "Microsoft.VisualStudio.TestPlatform.ObjectModel"; + private static readonly Version DefaultVersion = new(); private static readonly Version Version45 = new("4.5"); + /// + /// Resolves the loaded VSTest object-model assembly by simple name, so this AppDomain-wiring code does not + /// need a compile-time reference to it. By the time these methods run (test source host setup during + /// discovery/execution) the adapter has already loaded the object model into the current (parent) domain, + /// so its identity — including any binding redirect in effect — matches what a direct type reference resolved to. + /// + /// The object-model assembly. + private static Assembly GetObjectModelAssembly() + => AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => string.Equals(a.GetName().Name, ObjectModelAssemblyName, StringComparison.Ordinal)) + ?? Assembly.Load(ObjectModelAssemblyName); + /// /// Gets or sets the Xml Utilities instance. /// @@ -90,7 +102,7 @@ internal static string GetTargetFrameworkVersionString(string testSourcePath) var resolutionPaths = new List { - Path.GetDirectoryName(typeof(TestCase).Assembly.Location), + Path.GetDirectoryName(GetObjectModelAssembly().Location), Path.GetDirectoryName(testSourcePath), }; @@ -157,10 +169,10 @@ internal static void SetConfigurationFile(AppDomainSetup appDomainSetup, string? try { // Add redirection of the built 11.0 Object Model assembly to the current version if that is not 11.0 - string currentVersionOfObjectModel = typeof(TestCase).Assembly.GetName().Version.ToString(); + string currentVersionOfObjectModel = GetObjectModelAssembly().GetName().Version.ToString(); if (!string.Equals(currentVersionOfObjectModel, ObjectModelVersionBuiltAgainst, StringComparison.Ordinal)) { - AssemblyName assemblyName = typeof(TestCase).Assembly.GetName(); + AssemblyName assemblyName = GetObjectModelAssembly().GetName(); byte[] configurationBytes = XmlUtilities.AddAssemblyRedirection( testSourceConfigFile, From 826158088e475ad6d143cd44f159de7d143edfcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 14:54:06 +0200 Subject: [PATCH 20/24] Resolve source-host anchor assemblies by name (Phase 6e-4c1) Remove the compile-time VSTest object-model dependency from the netfx source-host setup. TestSourceHost used two `typeof(...).Assembly` anchors into the VSTest object model: - `typeof(EqtTrace).Assembly` (force-loading Microsoft.TestPlatform.CoreUtilities into the child app domain to avoid a recursive assembly-resolution cycle), and - `typeof(AssemblyHelper).Assembly` (locating the test-platform directory for the resolution paths). Both run parent-side (or reflect parent-side loaded assemblies) before the child app domain resolves anything, so they are resolved by simple name from the current domain via a small `GetLoadedAssembly(simpleName)` helper - returning the same loaded assembly identity the type references did. EqtTrace's defining assembly is CoreUtilities (it is type-forwarded from the object model), so that anchor targets CoreUtilities by name; AssemblyHelper lives in the object model, so that anchor targets the object model. The only remaining object-model mention in the file is the assembly simple name as a string literal (not an assembly reference). Proven on the netfx child-app-domain path: PlatformServices.Desktop.IntegrationTests stays green (15/15). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Services/TestSourceHost.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs index d2e5960d17..933d8d3185 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHost.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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 @@ -8,12 +8,6 @@ #if NETFRAMEWORK using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; #endif -#if NETFRAMEWORK -using Microsoft.VisualStudio.TestPlatform.ObjectModel; -#endif -#if NETFRAMEWORK || (NET && !WINDOWS_UWP) -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -#endif #if !WINDOWS_UWP using Microsoft.VisualStudio.TestTools.UnitTesting; #endif @@ -29,6 +23,13 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; #pragma warning disable CA1852 // Seal internal types - needs to be non-sealed because it's mocked in tests. internal class TestSourceHost : ITestSourceHost { +#if NETFRAMEWORK || (NET && !WINDOWS_UWP) + private const string ObjectModelAssemblyName = "Microsoft.VisualStudio.TestPlatform.ObjectModel"; +#endif +#if NETFRAMEWORK + private const string CoreUtilitiesAssemblyName = "Microsoft.TestPlatform.CoreUtilities"; +#endif + #if !WINDOWS_UWP #pragma warning disable IDE0052 // Remove unread private members private readonly string _sourceFileName; @@ -178,7 +179,7 @@ public void SetupHost() // For unknown reasons, with MSTest 3.4+ we start to see infinite cycles of assembly resolution of this dll in the new app // domain. In older versions, this was not the case, and the callback was allowing to fully lookup and load the dll before // triggering the next resolution. - AppDomain.Load(typeof(EqtTrace).Assembly.GetName()); + AppDomain.Load(GetLoadedAssembly(CoreUtilitiesAssemblyName).GetName()); // Add an assembly resolver in the child app-domain... Type assemblyResolverType = typeof(AssemblyResolver); @@ -351,6 +352,17 @@ internal string GetTargetFrameworkVersionString(string sourceFileName) #endif #if NETFRAMEWORK || (NET && !WINDOWS_UWP) + /// + /// Resolves a loaded test-platform assembly by simple name, so this file needs no compile-time reference to + /// the VSTest object model. These assemblies are loaded by the adapter/host before the source host runs, so + /// the resolved identity matches what a direct type reference would have produced. + /// + /// The simple assembly name. + /// The loaded assembly. + private static Assembly GetLoadedAssembly(string simpleName) + => AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => string.Equals(a.GetName().Name, simpleName, StringComparison.Ordinal)) + ?? Assembly.Load(simpleName); + /// /// Gets the probing paths to load the test assembly dependencies. /// @@ -402,7 +414,7 @@ internal virtual List GetResolutionPaths(string sourceFileName, bool isP resolutionPaths.Add(adapterDirectory); } - string? testPlatformDirectory = Path.GetDirectoryName(AssemblyFileLocator.TryGetLocation(typeof(AssemblyHelper).Assembly)); + string? testPlatformDirectory = Path.GetDirectoryName(AssemblyFileLocator.TryGetLocation(GetLoadedAssembly(ObjectModelAssemblyName))); if (!string.IsNullOrEmpty(testPlatformDirectory) && !resolutionPaths.Contains(testPlatformDirectory)) { // Adding TestPlatform folder to resolution paths From e6dc77cb079a77ca62c00f9e96b063c6d7854b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 15:18:50 +0200 Subject: [PATCH 21/24] Reimplement the source assembly-reference check without ObjectModel (Phase 6e-4c2) TestSourceHandler.IsAssemblyReferenced (netfx) used AssemblyHelper.DoesReferencesAssembly from Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities to decide whether a source assembly references the test framework before running discovery. Replace that call with a local neutral DoesSourceReferenceAssembly helper that reproduces the exact observable behavior of the VSTest implementation: - ReflectionOnlyLoadFrom(source), then GetReferencedAssemblies(). - Match a referenced assembly by simple name (OrdinalIgnoreCase) plus public key token bytes; version is ignored -- identical to AssemblyLoadWorker.CheckAssemblyReference. - Null/empty source or null reference assembly returns null (undeterminable). - Any exception returns null so discovery proceeds conservatively. Fidelity note: the VSTest DoesReferencesAssembly created a child AppDomain and an AssemblyLoadWorker instance, but then called the *static* AssemblyLoadWorker.CheckAssemblyReference -- the worker instance is never used and the ReflectionOnlyLoadFrom actually runs in the current domain. The child domain is therefore dead code for the result, so omitting it is behavior-preserving for every input where AppDomain creation would have succeeded. Removes the last real ObjectModel dependency from TestSourceHandler. Verified: PlatformServices.UnitTests 935 (net462) / 897 (net8.0); DesktopTestSourceTests IsAssemblyReferenced branches 7/7 (referenced, not-referenced, null-name, null-source); PlatformServices.Desktop.IntegrationTests 15/15. All real TFMs build 0-warning. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Services/TestSourceHandler.cs | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs index 4255641119..a2ce548535 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs @@ -5,9 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.AppContainer; #endif using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; -#if NETFRAMEWORK -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -#endif namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; @@ -79,7 +76,7 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) { #if NETFRAMEWORK // This loads the dll in a different app domain. We can optimize this to load in the current domain since this code could be run in a new app domain anyway. - bool? utfReference = AssemblyHelper.DoesReferencesAssembly(source, assemblyName); + bool? utfReference = DoesSourceReferenceAssembly(source, assemblyName); // If no reference to UTF don't run discovery. Take conservative approach. If not able to find proceed with discovery. return !utfReference.HasValue || utfReference.Value; @@ -94,6 +91,73 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) #endif } +#if NETFRAMEWORK + /// + /// Checks whether the source assembly directly references the given assembly. + /// Only the assembly simple name and public key token are matched; version is ignored. + /// Returns if the reference could not be determined. + /// + /// The path to the source assembly to inspect. + /// The assembly to look for in the source's references. + /// if referenced, if not, if undeterminable. + private static bool? DoesSourceReferenceAssembly(string source, AssemblyName referenceAssembly) + { + if (string.IsNullOrEmpty(source) || referenceAssembly is null) + { + return null; + } + + try + { + string? referenceAssemblyName = referenceAssembly.Name; + byte[] referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken(); + + // ReflectionOnlyLoadFrom loads from the specified path only (no probing) and does not + // execute any code from the loaded assembly. + var assembly = Assembly.ReflectionOnlyLoadFrom(source); + + foreach (AssemblyName referencedAssembly in assembly.GetReferencedAssemblies()) + { + // Match without version: only the simple name and public key token. + if (!string.Equals(referencedAssembly.Name, referenceAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (ArePublicKeyTokensEqual(referencedAssembly.GetPublicKeyToken(), referenceAssemblyPublicKeyToken)) + { + return true; + } + } + + return false; + } + catch + { + // Return null if we are not able to check. + return null; + } + } + + private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right) + { + if (left.Length != right.Length) + { + return false; + } + + for (int i = 0; i < left.Length; ++i) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } +#endif + /// /// Gets the set of sources (dll's/exe's) that contain tests. If a source is a package (appx), return the file (dll/exe) that contains tests from it. /// From 9197a3f44cef306a8f1090d4b640c05f18abf149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 15:28:44 +0200 Subject: [PATCH 22/24] Vendor a neutral SuspendCodeCoverage in PlatformServices (Phase 6e-4c3) TestDeployment (netfx) wrapped the deployment file copy in `using (new SuspendCodeCoverage())` to pause dynamic code-coverage instrumentation of modules loaded while files are copied. That type came from Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities. Vendor an internal neutral copy at Utilities/SuspendCodeCoverage.cs (namespace ...PlatformServices.Utilities) that reproduces the VSTest behavior byte-for-byte: - On construction: read the current value of the process environment variable "__VANGUARD_SUSPEND_INSTRUMENT__" and set it to "TRUE". - On dispose: restore the previously captured value (idempotent). The environment-variable name and value are the collector IPC contract the dynamic code-coverage (Vanguard) engine reads, so they are preserved exactly. The child-object is internal/sealed with a straightforward idempotent Dispose (the original's Dispose(bool)/GC.SuppressFinalize plumbing has no finalizer to suppress and is behavior-equivalent to the direct restore). TestDeployment now resolves SuspendCodeCoverage via the already-imported PlatformServices.Utilities namespace; the VSTest ObjectModel.Utilities using is removed. With this change PlatformServices has zero `using`/type references to the Microsoft.TestPlatform.ObjectModel package (only string-literal assembly names used for by-name runtime lookup remain), clearing the way to drop the package reference in the capstone. Verified: PlatformServices builds 0-warning (net462 and all real TFMs; netfx-guarded change); PlatformServices.UnitTests 935 (net462) / 897 (net8.0); PlatformServices.Desktop.IntegrationTests 15/15 (exercises the deployment path that runs inside the SuspendCodeCoverage scope). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Services/TestDeployment.cs | 6 --- .../Utilities/SuspendCodeCoverage.cs | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/Utilities/SuspendCodeCoverage.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs index 5c17c97d43..1591ad3481 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestDeployment.cs @@ -8,12 +8,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; -#if NETFRAMEWORK -// SuspendCodeCoverage (used below to pause dynamic code coverage while deployment copies files) is a VSTest -// object-model type. Its neutralization is deferred to a later platform-services decoupling step; the rest of -// the deployment input is already platform-agnostic. -using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; -#endif using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Utilities/SuspendCodeCoverage.cs b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/SuspendCodeCoverage.cs new file mode 100644 index 0000000000..4a8dc95a16 --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/Utilities/SuspendCodeCoverage.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETFRAMEWORK +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; + +/// +/// Suspends dynamic code-coverage instrumentation of the modules that are loaded while this object is alive +/// (between construction and disposal) by setting the well-known collector environment variable. The previous +/// value of the environment variable is restored on dispose. +/// +internal sealed class SuspendCodeCoverage : IDisposable +{ + private const string SuspendCodeCoverageEnvVarName = "__VANGUARD_SUSPEND_INSTRUMENT__"; + private const string SuspendCodeCoverageEnvVarTrueValue = "TRUE"; + + private readonly string? _previousEnvironmentValue; + + private bool _isDisposed; + + public SuspendCodeCoverage() + { + _previousEnvironmentValue = Environment.GetEnvironmentVariable(SuspendCodeCoverageEnvVarName, EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(SuspendCodeCoverageEnvVarName, SuspendCodeCoverageEnvVarTrueValue, EnvironmentVariableTarget.Process); + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + Environment.SetEnvironmentVariable(SuspendCodeCoverageEnvVarName, _previousEnvironmentValue, EnvironmentVariableTarget.Process); + _isDisposed = true; + } +} +#endif From 16552c686ff0eec72e35db5a538a10aa445ebbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 15:52:23 +0200 Subject: [PATCH 23/24] Drop the ObjectModel package reference from PlatformServices (Phase 7) This is the capstone of the initiative: MSTestAdapter.PlatformServices no longer references the VSTest object model (Microsoft.TestPlatform.ObjectModel) at all. Production changes: - Remove the Microsoft.TestPlatform.ObjectModel PackageReference from MSTestAdapter.PlatformServices.csproj. - The only remaining consumer of a transitively-provided VSTest package was RunConfigurationSettings, which used PlatformAbstractions' PlatformApartmentState enum {MTA, STA} to parse ExecutionThreadApartmentState. Replace it with a local internal enum of the same shape (same member names/order), preserving the exact Enum.TryParse-then-map-to-System.Threading.ApartmentState behavior byte-for-byte (a 2-member by-name parse is identical; parsing directly to ApartmentState would change the handling of the "Unknown" value, so a faithful local enum is required). - Add a direct framework reference to System.Configuration on .NET Framework. ConfigurationManager/ConfigurationElementCollection (used by TestDataSource) were previously pulled in transitively via the object-model package; System.Configuration is a framework assembly, so it is now referenced directly. Result: the compiled MSTestAdapter.PlatformServices assembly has ZERO references to any Microsoft.*.TestPlatform.* assembly on every real target framework (net462/net8.0/net9.0 + windows variants), verified via assembly metadata. All VSTest coupling now lives in the MSTest.TestAdapter layer above it. Guard test: - ObjectModelDecouplingTests asserts the compiled PlatformServices assembly references no assembly whose name contains "TestPlatform" (catches ObjectModel, PlatformAbstractions, CoreUtilities, ...; MSTest's own framework is "MSTest.TestFramework", which does not match). This locks the platform-agnostic contract permanently. Test-project fix: - PlatformServices.Desktop.IntegrationTests uses ObjectModel's XmlRunSettingsUtilities directly (previously transitive through the PlatformServices project reference), so it gets its own direct Microsoft.TestPlatform.ObjectModel PackageReference. Test projects are allowed to reference the object model; only the production assembly must be neutral. Verified: PlatformServices builds 0-warning on all real TFMs (UWP builds via full msbuild in CI); PlatformServices.UnitTests 936 (net462) / 898 (net8.0) incl. the new guard test and the STA/MTA parsing tests on both the runsettings-XML and config paths; PlatformServices.Desktop.IntegrationTests 15/15; MSTestAdapter.UnitTests 21/21; MSTest.TestAdapter and MSTest.IntegrationTests build clean. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ApartmentStateSetting.cs | 18 ++++++++++++ .../MSTestAdapter.PlatformServices.csproj | 8 ++++- .../RunConfigurationSettings.cs | 21 +++++++------- ...rmServices.Desktop.IntegrationTests.csproj | 3 ++ .../ObjectModelDecouplingTests.cs | 29 +++++++++++++++++++ 5 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.cs create mode 100644 test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.cs diff --git a/src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.cs b/src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.cs new file mode 100644 index 0000000000..4bb4f09f3a --- /dev/null +++ b/src/Adapter/MSTestAdapter.PlatformServices/ApartmentStateSetting.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; + +/// +/// Apartment state parsed from the ExecutionThreadApartmentState run settings and the +/// mstest:execution:executionApartmentState configuration value. This is the adapter's own neutral +/// replacement for the former VSTest PlatformAbstractions.PlatformApartmentState enum. +/// +internal enum ApartmentStateSetting +{ + // The member order is load-bearing: Enum.TryParse maps numeric-string run settings values ("0"/"1") + // by number, so MTA must stay 0 and STA must stay 1 to preserve parse compatibility with the former + // VSTest PlatformAbstractions.PlatformApartmentState enum. Do not reorder. + MTA, + STA, +} diff --git a/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj b/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj index 6a9e8820b1..97813d16d1 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj +++ b/src/Adapter/MSTestAdapter.PlatformServices/MSTestAdapter.PlatformServices.csproj @@ -36,7 +36,6 @@ - + + + + diff --git a/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs b/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs index 5723c35a44..8423ab05b6 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/RunConfigurationSettings.cs @@ -5,7 +5,6 @@ using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; #endif -using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; @@ -96,13 +95,13 @@ private static RunConfigurationSettings ToSettings(XmlReader reader) { case "EXECUTIONTHREADAPARTMENTSTATE": { - if (Enum.TryParse(reader.ReadInnerXml(), out PlatformApartmentState platformApartmentState)) + if (Enum.TryParse(reader.ReadInnerXml(), out ApartmentStateSetting apartmentStateSetting)) { - settings.ExecutionApartmentState = platformApartmentState switch + settings.ExecutionApartmentState = apartmentStateSetting switch { - PlatformApartmentState.STA => ApartmentState.STA, - PlatformApartmentState.MTA => ApartmentState.MTA, - _ => throw new NotSupportedException($"Platform apartment state '{platformApartmentState}' is not supported."), + ApartmentStateSetting.STA => ApartmentState.STA, + ApartmentStateSetting.MTA => ApartmentState.MTA, + _ => throw new NotSupportedException($"Platform apartment state '{apartmentStateSetting}' is not supported."), }; } @@ -131,13 +130,13 @@ internal static RunConfigurationSettings SetRunConfigurationSettingsFromConfig(I // } // } string? apartmentStateValue = configuration["mstest:execution:executionApartmentState"]; - if (Enum.TryParse(apartmentStateValue, out PlatformApartmentState platformApartmentState)) + if (Enum.TryParse(apartmentStateValue, out ApartmentStateSetting apartmentStateSetting)) { - settings.ExecutionApartmentState = platformApartmentState switch + settings.ExecutionApartmentState = apartmentStateSetting switch { - PlatformApartmentState.STA => ApartmentState.STA, - PlatformApartmentState.MTA => ApartmentState.MTA, - _ => throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, platformApartmentState, "execution:executionApartmentState")), + ApartmentStateSetting.STA => ApartmentState.STA, + ApartmentStateSetting.MTA => ApartmentState.MTA, + _ => throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resource.InvalidValue, apartmentStateSetting, "execution:executionApartmentState")), }; } diff --git a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj index de991b9157..0f8dac1571 100644 --- a/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj +++ b/test/IntegrationTests/PlatformServices.Desktop.IntegrationTests/PlatformServices.Desktop.IntegrationTests.csproj @@ -15,6 +15,9 @@ + + diff --git a/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.cs b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.cs new file mode 100644 index 0000000000..16efa478fc --- /dev/null +++ b/test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/ObjectModelDecouplingTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + +using TestFramework.ForTestingMSTest; + +namespace MSTestAdapter.PlatformServices.UnitTests; + +public class ObjectModelDecouplingTests : TestContainer +{ + // Guards the platform-agnostic contract: the compiled MSTestAdapter.PlatformServices assembly must not + // reference the VSTest object model (Microsoft.VisualStudio.TestPlatform.ObjectModel) or any other + // Microsoft.*.TestPlatform.* assembly. All VSTest coupling lives in the MSTest.TestAdapter layer above it. + public void PlatformServicesAssemblyShouldNotReferenceAnyTestPlatformAssembly() + { + Assembly platformServices = typeof(TestSourceHandler).Assembly; + + IEnumerable testPlatformReferences = platformServices + .GetReferencedAssemblies() + .Select(reference => reference.Name!) + .Where(name => name.IndexOf("TestPlatform", StringComparison.OrdinalIgnoreCase) >= 0); + + testPlatformReferences.Should().BeEmpty( + "PlatformServices must stay platform-agnostic and must not reference the VSTest object model or any Microsoft.*.TestPlatform.* assembly"); + } +} From ce6569bd527ef80acebb7ef6993095fddf184c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sun, 5 Jul 2026 21:44:51 +0200 Subject: [PATCH 24/24] Handle null/empty public key tokens in ArePublicKeyTokensEqual Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Services/TestSourceHandler.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs index a2ce548535..ae97ba6ab2 100644 --- a/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs +++ b/src/Adapter/MSTestAdapter.PlatformServices/Services/TestSourceHandler.cs @@ -110,7 +110,7 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) try { string? referenceAssemblyName = referenceAssembly.Name; - byte[] referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken(); + byte[]? referenceAssemblyPublicKeyToken = referenceAssembly.GetPublicKeyToken(); // ReflectionOnlyLoadFrom loads from the specified path only (no probing) and does not // execute any code from the loaded assembly. @@ -139,8 +139,20 @@ public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) } } - private static bool ArePublicKeyTokensEqual(byte[] left, byte[] right) + private static bool ArePublicKeyTokensEqual(byte[]? left, byte[]? right) { + // GetPublicKeyToken() returns null (or an empty array) for unsigned assemblies. + // Treat two unsigned tokens as equal, and a signed vs. unsigned pair as unequal. + if (left is null || left.Length == 0) + { + return right is null || right.Length == 0; + } + + if (right is null || right.Length == 0) + { + return false; + } + if (left.Length != right.Length) { return false;