// 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.Testing.Platform.Capabilities.TestFramework; using Microsoft.Testing.Platform.Extensions.ArtifactPostProcessing; using Microsoft.Testing.Platform.Extensions.TestHost; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.IPC; using Microsoft.Testing.Platform.Logging; using Microsoft.Testing.Platform.ServerMode; using Microsoft.Testing.Platform.Services; using Microsoft.Testing.Platform.Telemetry; namespace Microsoft.Testing.Platform.Hosts; /// /// This represents either a test host (console or server), or a test host controller. /// This doesn't represent an orchestrator host. /// [StackTraceHidden] internal abstract partial class CommonHost(ServiceProvider serviceProvider) : IHost { #if NET9_0_OR_GREATER private readonly Lock _activeGracefulStopCapabilitiesSync = new(); #else private readonly object _activeGracefulStopCapabilitiesSync = new(); #endif private readonly List _activeGracefulStopCapabilities = []; private CancellationToken _gracefulSessionStopCancellationToken; private bool _isGracefulSessionStopRequested; public ServiceProvider ServiceProvider => serviceProvider; protected IPushOnlyProtocol? PushOnlyProtocol => ServiceProvider.GetService(); protected abstract bool RunTestApplicationLifeCycleCallbacks { get; } /// /// Gets a value indicating whether this host is the one that observes the test results, and therefore owns the /// run-level verdict reported to telemetry. /// /// /// The test host controller shares the application's TestApplicationResult but never consumes a test node /// update: the tests run in the child test host it launches. /// private bool OwnsRunVerdict => this is not TestHostControllersTestHost; public async Task RunAsync() { CancellationToken testApplicationCancellationToken = ServiceProvider.GetTestApplicationCancellationTokenSource().CancellationToken; List alreadyDisposed = []; int exitCode = (int)ExitCode.GenericFailure; IPlatformOpenTelemetryService? platformOTelService = null; IPlatformActivity? activity = null; try { platformOTelService = ServiceProvider.GetPlatformOTelService(); if (platformOTelService is not null) { ExtensionHelper.ConfigureOTelLegacyAttributes( PlatformOpenTelemetryOptions.FromEnvironment(ServiceProvider.GetEnvironment())); } string hostType = GetHostType(); // When the builder activity has already been closed (or OTel was configured late) there is no ambient // parent, so fall back to the W3C trace context published by the process that started this run. string? environmentParentId = platformOTelService is not null && !platformOTelService.HasCurrentActivity ? EnvironmentTraceContext.TryGetParentId(ServiceProvider.GetEnvironment()) : null; if (environmentParentId is not null && platformOTelService!.RootTraceState is null) { platformOTelService.RootTraceState = EnvironmentTraceContext.TryGetTraceState(ServiceProvider.GetEnvironment()); } activity = platformOTelService?.StartActivity( hostType, tags: [new(TestingPlatformSemanticConventions.Attributes.TestHostType, hostType)], parentId: environmentParentId); if (PushOnlyProtocol is null || PushOnlyProtocol?.IsServerMode == false) { exitCode = await RunTestAppAsync(platformOTelService, testApplicationCancellationToken, alreadyDisposed).ConfigureAwait(false); if (testApplicationCancellationToken.IsCancellationRequested) { exitCode = (int)ExitCode.TestSessionAborted; } return exitCode; } try { RoslynDebug.Assert(PushOnlyProtocol is not null); IReadOnlyDictionary? additionalHandshakeProperties = SupportsArtifactPostProcessing(hostType) ? ArtifactPostProcessingHandshakeProperties.Create(ServiceProvider.GetServicesInternal()) : null; bool isValidProtocol = await PushOnlyProtocol.IsCompatibleProtocolAsync(hostType, additionalHandshakeProperties).ConfigureAwait(false); if (isValidProtocol && PushOnlyProtocol.IsServerControlChannelSupported) { // Start listening for server-initiated signals (e.g. session cancellation) before running tests // so a signal that arrives mid-run is observed. React by stopping gracefully where possible. await PushOnlyProtocol.StartServerControlChannelAsync(RequestGracefulSessionStopAsync).ConfigureAwait(false); } exitCode = isValidProtocol ? await RunTestAppAsync(platformOTelService, testApplicationCancellationToken, alreadyDisposed).ConfigureAwait(false) : (int)ExitCode.IncompatibleProtocolVersion; } finally { if (PushOnlyProtocol is not null) { await PushOnlyProtocol.OnExitAsync().ConfigureAwait(false); } } } catch (OperationCanceledException) when (testApplicationCancellationToken.IsCancellationRequested) { // We do nothing we're canceling } finally { // Normalize the cancellation verdict *before* the span and the run telemetry read it. The // post-finally adjustment below runs too late for them, so without this a cancelled run was traced // as a generic failure and test.run.exit_code did not match the code the process exits with. if (testApplicationCancellationToken.IsCancellationRequested) { exitCode = (int)ExitCode.TestSessionAborted; } // Emit the run-level telemetry while the OpenTelemetry providers and the root span are still alive. // DisposeServiceProviderAsync below tears the providers down in registration order, and they are // registered before TestApplicationResult, so anything recorded after this point would be dropped. // // Only the host that actually observed the results owns the verdict. The test host controller shares // the same TestApplicationResult instance but consumes no TestNodeUpdateMessage (the tests run in the // child process), so reporting from there would emit a second, contradictory run record with zero // counts and a ZeroTests exit code. if (OwnsRunVerdict && ServiceProvider.GetService() is TestApplicationResult testApplicationResult) { testApplicationResult.ReportRunTelemetry(activity, exitCode); } // Record the run verdict on the root span before closing it, so a trace search on // test.run.exit_code finds failing runs without having to open them. activity?.SetTag(TestingPlatformSemanticConventions.Attributes.TestRunExitCode, exitCode); activity?.SetStatus( exitCode == (int)ExitCode.Success ? PlatformActivityStatusCode.Ok : PlatformActivityStatusCode.Error); // Dispose the activity activity?.Dispose(); await DisposeServiceProviderCoreAsync( ServiceProvider, filter: null, alreadyDisposed: alreadyDisposed, isProcessShutdown: true, disposeServiceAsync: DisposeProcessShutdownServiceAsync).ConfigureAwait(false); if (ServiceProvider.GetService() is { } fileLoggerProvider && !alreadyDisposed.Contains(fileLoggerProvider)) { await DisposeProcessShutdownServiceAsync(fileLoggerProvider).ConfigureAwait(false); alreadyDisposed.Add(fileLoggerProvider); } // Dispose the LoggerFactoryProxy last so that all user-registered logger providers // (e.g., Microsoft.Extensions.Logging providers added via the Microsoft.Testing.Extensions.Logging // bridge such as Serilog, Application Insights, OpenTelemetry) get a chance to flush their buffers. // The proxy is skipped by DisposeServiceProviderAsync for ordering reasons. if (ServiceProvider.GetService() is { } loggerFactoryProxy && !alreadyDisposed.Contains(loggerFactoryProxy)) { await DisposeProcessShutdownServiceAsync(loggerFactoryProxy).ConfigureAwait(false); alreadyDisposed.Add(loggerFactoryProxy); } if (PushOnlyProtocol is not null && !alreadyDisposed.Contains(PushOnlyProtocol)) { await DisposeProcessShutdownServiceAsync(PushOnlyProtocol).ConfigureAwait(false); alreadyDisposed.Add(PushOnlyProtocol); } // This is intentional that we are not disposing the CTS. // An unobserved task exception could be raised after the dispose, and we want to use OutputDevice there // which needs CTS down the path. // await DisposeHelper.DisposeAsync(ServiceProvider.GetTestApplicationCancellationTokenSource()); } if (testApplicationCancellationToken.IsCancellationRequested) { exitCode = (int)ExitCode.TestSessionAborted; } return exitCode; } internal static bool SupportsArtifactPostProcessing(string hostType) => hostType is HandshakeMessageHostTypes.TestHost or HandshakeMessageHostTypes.ServerTestHost or HandshakeMessageHostTypes.TestHostController; protected virtual string HostType => this switch { ConsoleTestHost => HandshakeMessageHostTypes.TestHost, TestHostControllersTestHost => HandshakeMessageHostTypes.TestHostController, ServerTestHost => HandshakeMessageHostTypes.ServerTestHost, _ => throw new InvalidOperationException($"Unknown host type '{GetType().FullName}'"), }; private string GetHostType() { // TestHostOrchestratorHost does not inherit from CommonHost, so the orchestrator handshake is // performed there directly (see TestHostOrchestratorHost.RunAsync) rather than going through // this path. This method only covers the test host and test host controller roles. string hostType = HostType; return hostType; } // Reaction to a server-initiated session cancellation coming over the reverse control pipe. Prefer a graceful // stop so the framework stops scheduling new tests but still emits trx/logs/artifacts for whatever completed // (mirroring the local '--maximum-failed-tests' behavior). Fall back to hard cancellation when the running // framework has no graceful-stop capability (e.g. the test host controller), which is the only lever left. protected Task RegisterActiveGracefulStopCapabilityAsync(IGracefulStopTestExecutionCapability capability) { CancellationToken cancellationToken; bool stopCapability; lock (_activeGracefulStopCapabilitiesSync) { stopCapability = _isGracefulSessionStopRequested && !_activeGracefulStopCapabilities.Contains(capability); cancellationToken = _gracefulSessionStopCancellationToken; _activeGracefulStopCapabilities.Add(capability); } return stopCapability ? capability.StopTestExecutionAsync(cancellationToken) : Task.CompletedTask; } protected void UnregisterActiveGracefulStopCapability(IGracefulStopTestExecutionCapability capability) { lock (_activeGracefulStopCapabilitiesSync) { _activeGracefulStopCapabilities.Remove(capability); } } protected async Task RequestGracefulSessionStopAsync(CancellationToken cancellationToken) { IGracefulStopTestExecutionCapability[] capabilities; lock (_activeGracefulStopCapabilitiesSync) { _isGracefulSessionStopRequested = true; _gracefulSessionStopCancellationToken = cancellationToken; capabilities = [.. _activeGracefulStopCapabilities.Distinct()]; } if (capabilities.Length > 0) { await Task.WhenAll(capabilities.Select(capability => capability.StopTestExecutionAsync(cancellationToken))).ConfigureAwait(false); return; } IGracefulStopTestExecutionCapability? applicationCapability = ServiceProvider.GetService()?.GetCapability(); if (applicationCapability is not null) { await applicationCapability.StopTestExecutionAsync(cancellationToken).ConfigureAwait(false); } else { ServiceProvider.GetTestApplicationCancellationTokenSource().Cancel(); } } private async Task RunTestAppAsync(IPlatformOpenTelemetryService? platformOTelService, CancellationToken testApplicationCancellationToken, List alreadyDisposed) { if (RunTestApplicationLifeCycleCallbacks) { using (platformOTelService?.StartActivity("BeforeRunCallbacks")) { // Get the test application lifecycle callbacks to be able to call the before run foreach (ITestHostApplicationLifetime testApplicationLifecycleCallbacks in ServiceProvider.GetServicesInternal()) { using IPlatformActivity? activity = platformOTelService?.StartActivity(testApplicationLifecycleCallbacks.Uid, testApplicationLifecycleCallbacks.ToOTelTags()); await testApplicationLifecycleCallbacks.BeforeRunAsync(testApplicationCancellationToken).ConfigureAwait(false); } } } int exitCode; using (platformOTelService?.StartActivity("Run")) { exitCode = await InternalRunAsync(testApplicationCancellationToken, alreadyDisposed).ConfigureAwait(false); } if (RunTestApplicationLifeCycleCallbacks) { using (platformOTelService?.StartActivity("AfterRunCallbacks")) { foreach (ITestHostApplicationLifetime testApplicationLifecycleCallbacks in ServiceProvider.GetServicesInternal()) { using IPlatformActivity? activity = platformOTelService?.StartActivity(testApplicationLifecycleCallbacks.Uid, testApplicationLifecycleCallbacks.ToOTelTags()); await testApplicationLifecycleCallbacks.AfterRunAsync(exitCode, testApplicationCancellationToken).ConfigureAwait(false); await DisposeHelper.DisposeAsync(testApplicationLifecycleCallbacks).ConfigureAwait(false); alreadyDisposed.Add(testApplicationLifecycleCallbacks); } } } return exitCode; } protected abstract Task InternalRunAsync(CancellationToken cancellationToken, List alreadyDisposed); protected virtual Task DisposeProcessShutdownServiceAsync(object service) => DisposeHelper.DisposeAsync(service); }