// 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.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; 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; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; /// /// Platform-agnostic discovery/execution orchestrator for the MSTest adapter. It owns the parts that used to /// live inside the VSTest / orchestrators but do not /// depend on the VSTest object model: telemetry lifetime, the STA/apartment run wrapper, and the calls into the /// platform-services engine (, , /// ). /// /// /// Every input is already neutral (, , /// , , run settings XML, test-run directory), /// so both hosts can reach the engine without one going through the other: the VSTest orchestrators unwrap their /// IDiscoveryContext/IRunContext/IFrameworkHandle into these neutral inputs, and the native /// Microsoft.Testing.Platform framework (MSTestTestFramework) builds them directly — it no longer calls the /// VSTest / classes. /// internal sealed class MSTestEngine { private readonly CancellationToken _cancellationToken; private readonly TestExecutionManager _testExecutionManager; #if !WINDOWS_UWP && !WIN_UI private readonly Func, Task>? _telemetrySender; #endif /// /// Token for canceling the test run, set for the duration of a run so can reach it. /// private TestRunCancellationToken? _testRunCancellationToken; internal MSTestEngine( CancellationToken cancellationToken, Func, Task>? telemetrySender = null, TestExecutionManager? testExecutionManager = null) { _cancellationToken = cancellationToken; _testExecutionManager = testExecutionManager ?? new TestExecutionManager(); #if !WINDOWS_UWP && !WIN_UI _telemetrySender = telemetrySender; #else _ = telemetrySender; #endif } internal ITestClassInstanceFactory? TestClassInstanceFactory { get; set; } /// /// Cancels the in-flight run, if any. Mirrors the VSTest ITestExecutor.Cancel contract for the /// orchestrator that forwards to this engine. /// internal void Cancel() => _testRunCancellationToken?.Cancel(); /// /// Discovers tests from , reporting each discovered element to /// . /// [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Instance method by design: part of the MSTestEngine instance surface, and it consumes instance telemetry state on platforms where telemetry is available.")] internal async Task DiscoverAsync( IEnumerable sources, string? settingsXml, IAdapterMessageLogger logger, IUnitTestElementSink elementSink, ITestElementFilterProvider? filterProvider, IConfiguration? configuration, ITestSourceHandler testSourceHandler, bool isMTP) { using IDisposable activationScope = TestClassInstanceFactoryProvider.Push(TestClassInstanceFactory); EnsureTelemetryInitialized(); try { if (MSTestDiscovererHelpers.InitializeDiscovery(sources, settingsXml, logger, configuration, testSourceHandler)) { await new UnitTestDiscoverer(testSourceHandler) .DiscoverTestsAsync(sources, logger, elementSink, settingsXml, filterProvider, isMTP) .ConfigureAwait(false); } } finally { #if !WINDOWS_UWP && !WIN_UI // Send the discovery telemetry event ('mstest/discovery'). This always runs at the end of discovery — // for discover-only sessions it is the only event; for sessions where a run follows, execution will // send a separate 'mstest/sessionexit' event carrying assertion usage. await MSTestTelemetryDataCollector.SendDiscoveryTelemetryAndResetAsync(_telemetrySender).ConfigureAwait(false); #endif } } /// /// Runs tests from . The is invoked after settings /// are resolved so the recorder observes the effective . /// internal async Task RunFromSourcesAsync( IEnumerable sources, string? settingsXml, string? testRunDirectory, IAdapterMessageLogger logger, Func recorderFactory, ITestElementFilterProvider? filterProvider, IConfiguration? configuration, ITestSourceHandler testSourceHandler, bool isMTP) { using IDisposable activationScope = TestClassInstanceFactoryProvider.Push(TestClassInstanceFactory); Ensure.NotEmpty(sources); EnsureTelemetryInitialized(); try { if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, settingsXml, logger, configuration, testSourceHandler)) { return; } sources = testSourceHandler.GetTestSources(sources); // Build the neutral result recorder now that settings have been resolved by InitializeDiscovery; the // execution engine reports results through this recorder and never constructs VSTest results. For the // VSTest path this wraps the framework handle; for the native MTP path it publishes test nodes directly. ITestResultRecorder testResultRecorder = recorderFactory(MSTestSettings.CurrentSettings); var deploymentContext = new DeploymentContext(testRunDirectory, settingsXml); await RunUnderApartmentAsync(logger, async testRunToken => await _testExecutionManager.RunTestsAsync(sources, deploymentContext, logger, testResultRecorder, filterProvider, testSourceHandler, isMTP, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { await SendExecutionTelemetryAsync().ConfigureAwait(false); } } /// /// Runs the already-materialized test elements produced by (the VSTest /// "run selected test cases" path). The factory is invoked only after discovery/settings initialization /// succeeds, so a settings error bails out before any element is materialized (matching the historical order). /// is used only to initialize discovery/settings. /// internal async Task RunFromTestElementsAsync( Func> testElementsFactory, IEnumerable sourcesForInitialization, string? settingsXml, string? testRunDirectory, IAdapterMessageLogger logger, Func recorderFactory, ITestElementFilterProvider? filterProvider, IConfiguration? configuration, ITestSourceHandler testSourceHandler) { using IDisposable activationScope = TestClassInstanceFactoryProvider.Push(TestClassInstanceFactory); EnsureTelemetryInitialized(); try { if (!MSTestDiscovererHelpers.InitializeDiscovery(sourcesForInitialization, settingsXml, logger, configuration, testSourceHandler)) { return; } IEnumerable testElements = testElementsFactory(); ITestResultRecorder testResultRecorder = recorderFactory(MSTestSettings.CurrentSettings); var deploymentContext = new DeploymentContext(testRunDirectory, settingsXml); await RunUnderApartmentAsync(logger, async testRunToken => await _testExecutionManager.RunTestsAsync(testElements, deploymentContext, logger, testResultRecorder, filterProvider, testRunToken).ConfigureAwait(false)).ConfigureAwait(false); } finally { await SendExecutionTelemetryAsync().ConfigureAwait(false); } } private static void EnsureTelemetryInitialized() { #if !WINDOWS_UWP && !WIN_UI #if NETCOREAPP // Telemetry anonymizes custom test-attribute type names with SHA256, which is not supported // on wasi-wasm (https://github.com/dotnet/runtime/issues/99126). Skip telemetry collection // there so discovery does not throw a PlatformNotSupportedException; the platform side // already skips its own SHA256-based telemetry on wasi for the same reason. if (OperatingSystem.IsWasi()) { return; } #endif // Initialize telemetry collection if not already set (e.g. first call in the session). if (!MSTestTelemetryDataCollector.IsTelemetryOptedOut()) { _ = MSTestTelemetryDataCollector.EnsureInitialized(); } #endif } #if !WINDOWS_UWP && !WIN_UI private Task SendExecutionTelemetryAsync() => MSTestTelemetryDataCollector.SendExecutionTelemetryAndResetAsync(_telemetrySender); #else private static Task SendExecutionTelemetryAsync() => Task.CompletedTask; #endif private async Task RunUnderApartmentAsync(IAdapterMessageLogger logger, Func runTestsAction) { ApartmentState? requestedApartmentState = MSTestSettings.RunConfigurationSettings.ExecutionApartmentState; await StaThreadHelper.RunOnApartmentThreadIfNeededAsync( requestedApartmentState, "MSTest Entry Point", async () => { await DoRunTestsAsync().ConfigureAwait(false); return 0; }, () => 0, entryPointThread => Task.Run(entryPointThread.Join, _cancellationToken), (_, ex) => { logger.SendMessage(MessageLevel.Error, ex.ToString()); return 0; }, () => logger.SendMessage( MessageLevel.Warning, Resource.STAIsOnlySupportedOnWindowsWarning)).ConfigureAwait(false); // Local functions async Task DoRunTestsAsync() { using (_cancellationToken.Register(Cancel)) { try { _testRunCancellationToken = new TestRunCancellationToken(_cancellationToken); await runTestsAction(_testRunCancellationToken).ConfigureAwait(false); } finally { _testRunCancellationToken = null; } } } } }