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