// 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 using System.Security; #endif using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// /// The runner that runs a single unit test. Also manages the assembly and class cleanup methods at the end of the run. /// [StackTraceHidden] internal sealed partial class UnitTestRunner #if NETFRAMEWORK : MarshalByRefObject #endif { // Reusable TestResult returned for the assembly-init fast path (init already passed). // Safe to share across concurrent test runs because it is private to this type and treated // as immutable: the fast path only reads from it (Outcome plus the null Log/DebugTrace fields) // and the instance never escapes RunSingleTestAsync. Do not mutate it. private static readonly TestResult AssemblyInitPassedResult = new() { Outcome = UnitTestOutcome.Passed }; private readonly TypeCache _typeCache; private readonly ClassCleanupManager _classCleanupManager; // Only needed to attach class cleanup failures to the right test. // So we only add to this dictionary if the class has a class cleanup. private readonly ConcurrentDictionary _lastRunnableTestByClass = new(); // Used to attach assembly cleanup failures to the right test. private UnitTestElement? _lastRunnableTestInWholeAssembly; // Tracks whether at least one test in this runner's lifetime triggered AssemblyInitialize. // Needed so that end-of-assembly cleanup still runs when the very last test of the assembly // was filtered out by a [TestFilterProvider] (in which case testMethodInfo is null for the // cleanup decision in RunSingleTestAsync). private bool _assemblyInitializeWasExecuted; /// /// Initializes a new instance of the class. /// /// Specifies adapter settings that need to be instantiated in the domain running these tests. /// The tests to run. public UnitTestRunner(MSTestSettings settings, UnitTestElement[] testsToRun) : this(settings, testsToRun, ReflectHelper.Instance) { } /// /// Initializes a new instance of the class. /// /// Specifies adapter settings. /// The tests to run. /// The reflect Helper. internal UnitTestRunner(MSTestSettings settings, UnitTestElement[] testsToRun, ReflectHelper reflectHelper) { // Populate the settings into the domain(Desktop workflow) performing discovery. // This would just be resetting the settings to itself in non desktop workflows. MSTestSettings.PopulateSettings(settings); // Bridge the adapter setting to the TestFramework for assertion failure behavior. AssertionFailureSettings.LaunchDebuggerOnAssertionFailure = MSTestSettings.CurrentSettings.LaunchDebuggerOnAssertionFailure; #if WINDOWS_UWP || WIN_UI if (MSTestSettings.CurrentSettings.CaptureAssertionFailureDiagnostics) { PlatformServiceProvider.Instance.AdapterTraceLogger.Warning( "CaptureAssertionFailureDiagnostics is not supported on UWP or WinUI and will be ignored."); } AssertionFailureSettings.CaptureDiagnosticsOnFailure = null; #else AssertionFailureSettings.CaptureDiagnosticsOnFailure = MSTestSettings.CurrentSettings.CaptureAssertionFailureDiagnostics ? TestContextImplementation.CaptureAssertionFailureDiagnostics : null; #endif Logger.OnLogMessage += message => (TestContext.Current as TestContextImplementation)?.StandardOutputBuilder.Append(message); ConfigureOutputRouting(MSTestSettings.CurrentSettings.OutputCaptureMode); PlatformServiceProvider.Instance.TestRunCancellationToken ??= new TestRunCancellationToken(); _typeCache = new TypeCache(reflectHelper); _classCleanupManager = new ClassCleanupManager(testsToRun); // Expose the planned (post-filter) test list so that user code (typically [AssemblyInitialize] // or fixtures) can query TestRun.Current.PlannedTests to decide whether expensive setup is // needed. Set here so the snapshot lives in the same AppDomain/process that will execute the // assembly initialize and the tests themselves. TestRun.SetCurrent(TestRunInfo.CreateFrom(testsToRun)); } // Console.SetOut/SetError replace the process-wide console, and Trace.Listeners is process-wide too. // We install our routing exactly once per process: re-wrapping on every runner would stack routers on // top of each other (and Console.Out returns a synchronized wrapper, not our router, so a type check // cannot reliably detect an already-installed router). The installed routers read the capture mode on // every write (via MSTestSettings.CurrentSettings), so a reused host that changes OutputCaptureMode // between runs is still honored without re-installing. #if NET9_0_OR_GREATER private static readonly Lock OutputRoutingLock = new(); #else private static readonly object OutputRoutingLock = new(); #endif private static bool s_outputRoutingInstalled; private static void ConfigureOutputRouting(TestOutputCaptureMode mode) { lock (OutputRoutingLock) { // Nothing to install while the very first run does not capture. Once any run needs capture we install // the routers, and from then on their per-write mode check handles None/Result/Live for every run. if (s_outputRoutingInstalled || mode == TestOutputCaptureMode.None) { return; } Func modeProvider = static () => MSTestSettings.CurrentSettings.OutputCaptureMode; TextWriter originalOut = Console.Out; TextWriter originalError = Console.Error; TextWriter liveOutputWriter = CreateLiveOutputWriter(Console.OpenStandardOutput(), Console.OutputEncoding); TestContextImplementation.ConfigureLiveOutputWriter(liveOutputWriter); Console.SetOut(new ConsoleOutRouter(originalOut, modeProvider)); Console.SetError(new ConsoleErrorRouter(originalError, modeProvider)); Trace.Listeners.Add(new TextWriterTraceListener(new TraceTextWriter(liveOutputWriter, modeProvider))); s_outputRoutingInstalled = true; } } internal static TextWriter CreateLiveOutputWriter(Stream standardOutput, Encoding encoding) { Encoding preamblelessEncoding = encoding.GetPreamble().Length == 0 ? encoding : new PreamblelessEncoding(encoding); var streamWriter = new StreamWriter(standardOutput, preamblelessEncoding, bufferSize: 1024, leaveOpen: true) { AutoFlush = true, }; return TextWriter.Synchronized(streamWriter); } private sealed class PreamblelessEncoding(Encoding encoding) : Encoding { public override int CodePage => encoding.CodePage; public override string EncodingName => encoding.EncodingName; public override bool IsSingleByte => encoding.IsSingleByte; public override string WebName => encoding.WebName; public override Decoder GetDecoder() => encoding.GetDecoder(); public override Encoder GetEncoder() => encoding.GetEncoder(); public override int GetByteCount(char[] chars, int index, int count) => encoding.GetByteCount(chars, index, count); public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) => encoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex); public override int GetCharCount(byte[] bytes, int index, int count) => encoding.GetCharCount(bytes, index, count); public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) => encoding.GetChars(bytes, byteIndex, byteCount, chars, charIndex); public override int GetMaxByteCount(int charCount) => encoding.GetMaxByteCount(charCount); public override int GetMaxCharCount(int byteCount) => encoding.GetMaxCharCount(byteCount); public override byte[] GetPreamble() => []; } #pragma warning disable CA1822 // Mark members as static public void Cancel() => PlatformServiceProvider.Instance.TestRunCancellationToken?.Cancel(); #pragma warning restore CA1822 // Mark members as static #if NETFRAMEWORK /// /// Returns object to be used for controlling lifetime, null means infinite lifetime. /// /// /// The . /// [SecurityCritical] public override object? InitializeLifetimeService() => null; #endif }