// 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.Builder; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Configurations; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.CommandLine; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.Logging; using Microsoft.Testing.Platform.OutputDevice; using Microsoft.Testing.Platform.Resources; using Microsoft.Testing.Platform.Services; using Microsoft.Testing.Platform.Telemetry; using Microsoft.Testing.Platform.TestHostControllers; using Microsoft.Testing.Platform.Tools; namespace Microsoft.Testing.Platform.Hosts; internal sealed partial class TestHostBuilder { private async Task SetupCommonServicesAsync( ApplicationLoggingState loggingState, TestApplicationOptions testApplicationOptions, IUnhandledExceptionsHandler unhandledExceptionsHandler, DateTimeOffset createBuilderStart) { // ============= SETUP COMMON SERVICE USED IN ALL MODES ===============// SystemClock systemClock = new(); DateTimeOffset buildBuilderStart = systemClock.UtcNow; Dictionary builderMetrics = new() { [TelemetryProperties.HostProperties.CreateBuilderStart] = createBuilderStart, [TelemetryProperties.HostProperties.BuildBuilderStart] = buildBuilderStart, }; SystemEnvironment systemEnvironment = new(); ServiceProvider serviceProvider = new(); TestHostControllerInfo testHostControllerInfo = new(loggingState.CommandLineParseResult); BuildContext context = new( loggingState, testApplicationOptions, unhandledExceptionsHandler, createBuilderStart, systemClock, buildBuilderStart, builderMetrics, systemEnvironment, serviceProvider, testHostControllerInfo); if (loggingState.FileLoggerProvider is not null) { serviceProvider.TryAddService(loggingState.FileLoggerProvider); } serviceProvider.TryAddService(loggingState); serviceProvider.TryAddService(systemEnvironment); SystemConsole systemConsole = new(); serviceProvider.TryAddService(systemConsole); SystemTask systemTask = new(); serviceProvider.TryAddService(systemTask); serviceProvider.TryAddService(_runtimeFeature); serviceProvider.TryAddService(_processHandler); serviceProvider.TryAddService(_fileSystem); serviceProvider.TryAddService(_testApplicationModuleInfo); serviceProvider.TryAddService(testHostControllerInfo); serviceProvider.TryAddService(systemClock); serviceProvider.TryAddService(new ArtifactNamingService(_testApplicationModuleInfo, systemEnvironment, systemClock)); SystemMonitor systemMonitor = new(); serviceProvider.TryAddService(systemMonitor); SystemMonitorAsyncFactory systemMonitorAsyncFactory = new(); serviceProvider.TryAddService(systemMonitorAsyncFactory); context.SystemMonitorAsyncFactory = systemMonitorAsyncFactory; serviceProvider.AddService(new PlatformInformation()); ILogger? logger = null; if (loggingState.FileLoggerProvider is not null) { logger = loggingState.FileLoggerProvider.CreateLogger(GetType().ToString()); serviceProvider.TryAddService(new DiagnosticLoggingInformation(loggingState.FileLoggerProvider)); } if (logger is not null) { await logger.LogInformationAsync($"Setting RegisterEnvironmentVariablesConfigurationSource: '{testApplicationOptions.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource}'").ConfigureAwait(false); } if (testApplicationOptions.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource) { Configuration.AddConfigurationSource(() => new EnvironmentVariablesConfigurationSource(systemEnvironment)); } // Issue #6349: expose the parsed CLI options through IConfiguration under the // "commandLineOptions" section so that the same lookup helpers consult both the CLI and // testconfig.json. Highest priority (Order=0) so the CLI always wins. Configuration.AddConfigurationSource(() => new CommandLineConfigurationSource()); Configuration.AddConfigurationSource(() => new JsonConfigurationSource(_testApplicationModuleInfo, _fileSystem, loggingState.FileLoggerProvider)); context.Configuration = (AggregatedConfiguration)await ((ConfigurationManager)Configuration).BuildAsync(loggingState.FileLoggerProvider, loggingState.CommandLineParseResult).ConfigureAwait(false); serviceProvider.TryAddService(context.Configuration); #pragma warning disable CA1416 // Browser does not support the test host controller process model. The provider itself is marked [UnsupportedOSPlatform("browser")]. if (!OperatingSystem.IsBrowser()) { // Insert the built-in provider at the front so later user-supplied providers - // including the VSTest bridge runsettings provider - can still override these // unlocked values when both sources are present. if (TestHostControllers is TestHostControllersManager testHostControllersManager) { testHostControllersManager.AddEnvironmentVariableProviderFirst(sp => new TestConfigurationEnvironmentVariableProvider(sp.GetConfiguration())); } else { throw ApplicationStateGuard.Unreachable(); } } #pragma warning restore CA1416 var telemetryManager = (TelemetryManager)Telemetry; if (telemetryManager.BuildOTelService(serviceProvider) is { } platformOTelService) { serviceProvider.AddService(platformOTelService); } if (telemetryManager.BuildOTelProvider(serviceProvider) is { } otelProvider) { serviceProvider.AddService(otelProvider); } // Nest the whole run under the trace context of whoever started this process (a CI pipeline step, // `dotnet test`, an IDE, or the test host controller) so the run is not an orphan trace. if (serviceProvider.GetServiceInternal() is { } registeredPlatformOTelService) { // Set before creating any span so every span picks it up, including the ones created with an // explicit parent id (which do not inherit tracestate). registeredPlatformOTelService.RootTraceState = EnvironmentTraceContext.TryGetTraceState(systemEnvironment); context.BuilderActivity = registeredPlatformOTelService.StartActivity( TestingPlatformSemanticConventions.Activities.TestHostBuilder, parentId: EnvironmentTraceContext.TryGetParentId(systemEnvironment), startTime: buildBuilderStart); } _ = bool.TryParse(context.Configuration[PlatformConfigurationConstants.PlatformExitProcessOnUnhandledException], out bool isFileConfiguredToFailFast); string? environmentSetting = _environment.GetEnvironmentVariable(EnvironmentVariableConstants.TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION); bool? isEnvConfiguredToFailFast = null; if (environmentSetting is "1" or "0") { isEnvConfiguredToFailFast = environmentSetting == "1"; } bool exitProcessOnUnhandledException = isEnvConfiguredToFailFast is not null ? environmentSetting == "1" : isFileConfiguredToFailFast; if (logger is not null) { await logger.LogInformationAsync($"Setting PlatformExitProcessOnUnhandledException: '{exitProcessOnUnhandledException}', config file: {isFileConfiguredToFailFast} environment variable: {isEnvConfiguredToFailFast}").ConfigureAwait(false); } if (exitProcessOnUnhandledException) { unhandledExceptionsHandler.Subscribe(); } serviceProvider.AddService(new UnhandledExceptionsPolicy(fastFailOnFailure: exitProcessOnUnhandledException)); context.TestApplicationCancellationTokenSource = new CTRLPlusCCancellationTokenSource( systemConsole, loggingState.FileLoggerProvider?.CreateLogger(nameof(CTRLPlusCCancellationTokenSource))); serviceProvider.AddService(context.TestApplicationCancellationTokenSource, throwIfSameInstanceExit: true); testApplicationOptions.HostLifetimeBridge?.Connect(context.TestApplicationCancellationTokenSource.Cancel); CommandLineOptionsProxy commandLineOptionsProxy = new(); serviceProvider.TryAddService(commandLineOptionsProxy); LoggerFactoryProxy loggerFactoryProxy = new(); serviceProvider.TryAddService(loggerFactoryProxy); context.CommandLineHandler = await ((CommandLineManager)CommandLine).BuildAsync(loggingState.CommandLineParseResult, serviceProvider, context.Configuration).ConfigureAwait(false); commandLineOptionsProxy.SetCommandLineOptions(context.CommandLineHandler); context.PoliciesService = new StopPoliciesService(context.TestApplicationCancellationTokenSource); serviceProvider.AddService(context.PoliciesService); bool hasServerFlag = context.CommandLineHandler.TryGetOptionArgumentList(PlatformCommandLineProvider.ServerOptionKey, out string[]? protocolName); context.IsJsonRpcProtocol = hasServerFlag && (protocolName is null || protocolName.Length == 0 || protocolName[0].Equals(PlatformCommandLineProvider.JsonRpcProtocolName, StringComparison.OrdinalIgnoreCase)); // Reuse the shared helper so the pipe-protocol detection stays in one place. bool isPipeProtocol = context.CommandLineHandler.HasDotnetTestServerOption(); // Register the single coverage accumulator before the output device is built so the terminal // output device can read from it (rather than buffering its own copy of the coverage messages). serviceProvider.AddService(new TestCoverageCapabilities()); serviceProvider.AddService(new TestCoverageResult(loggerFactoryProxy)); context.ProxyOutputDevice = await _outputDisplay.BuildAsync(serviceProvider, context.IsJsonRpcProtocol, isPipeProtocol).ConfigureAwait(false); if (loggingState.FileLoggerProvider is not null) { Logging.AddProvider((_, _) => loggingState.FileLoggerProvider); } ICommandLineOptions commandLineOptions = serviceProvider.GetCommandLineOptions(); context.LoggerFactory = await ((LoggingManager)Logging).BuildAsync(serviceProvider, loggingState.LogLevel, systemMonitor).ConfigureAwait(false); loggerFactoryProxy.SetLoggerFactory(context.LoggerFactory); if (await context.ProxyOutputDevice.OriginalOutputDevice.IsEnabledAsync().ConfigureAwait(false)) { await context.ProxyOutputDevice.OriginalOutputDevice.TryInitializeAsync().ConfigureAwait(false); } serviceProvider.TryAddService(context.ProxyOutputDevice); serviceProvider.TryAddService(context.ProxyOutputDevice.OriginalOutputDevice); // Reports extensions/consumers that have not yet completed once the test-application // cancellation token is signalled. Registered after the output device so it can use it. ShutdownProgressReporter shutdownProgressReporter = new( context.TestApplicationCancellationTokenSource, context.ProxyOutputDevice, context.LoggerFactory, systemClock); serviceProvider.AddService(shutdownProgressReporter); context.TestFrameworkCapabilities = TestFramework!.TestFrameworkCapabilitiesFactory(serviceProvider); if (context.TestFrameworkCapabilities is IAsyncInitializableExtension testFrameworkCapabilitiesAsyncInitializable) { await testFrameworkCapabilitiesAsyncInitializable.InitializeAsync().ConfigureAwait(false); } serviceProvider.AddService(context.TestFrameworkCapabilities); IReadOnlyList jsonCommandLineOptions; IReadOnlyList jsonCommandLineOptionDefaults; try { // Normalize JSON-sourced scalar option entries to the indexed shape for arg-bearing // options before any consumer reads them. This makes "foo": "value" in testconfig.json // behave identically to "foo": ["value"], removing the foot-gun where a scalar like // "timeout": "true" was misinterpreted as a presence marker. See #6349/#8830. // // Defensive: option providers may register duplicate names; CommandLineOptionsValidator // catches that later (ValidateOptionsAreNotDuplicated) and reports a clear error. Skip // duplicates here so the normalization step itself doesn't crash for that malformed setup. var optionByName = new Dictionary(StringComparer.OrdinalIgnoreCase); IEnumerable extensionOptionsProviders = loggingState.CommandLineParseResult.ToolName is string toolName ? context.CommandLineHandler.ExtensionsCommandLineOptionsProviders .OfType() .Where(provider => provider.ToolName == toolName) : context.CommandLineHandler.ExtensionsCommandLineOptionsProviders .Where(provider => provider is not IToolCommandLineOptionsProvider); foreach (ICommandLineOptionsProvider optionsProvider in context.CommandLineHandler.SystemCommandLineOptionsProviders .Concat(extensionOptionsProviders)) { foreach (CommandLineOption option in optionsProvider.GetCommandLineOptions()) { if (!optionByName.ContainsKey(option.Name)) { optionByName.Add(option.Name, option); } } } context.Configuration.NormalizeJsonCommandLineOptionScalars(optionByName); jsonCommandLineOptions = context.Configuration.EnumerateJsonCommandLineOptions(); jsonCommandLineOptionDefaults = context.Configuration.EnumerateJsonCommandLineOptionDefaults(); } catch (FormatException ex) when (!loggingState.CommandLineParseResult.HasTool) { await DisplayBannerIfEnabledAsync(context.CommandLineHandler, context.ProxyOutputDevice, context.TestFrameworkCapabilities, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false); StringBuilder sb = new(); sb.AppendLine(PlatformResources.InvalidCommandLineArguments); sb.AppendLine(CultureInfo.InvariantCulture, $"\t- {ex.Message}"); await context.ProxyOutputDevice.DisplayAsync(context.CommandLineHandler, new ErrorMessageOutputDeviceData(sb.ToString().TrimEnd()), context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false); await context.CommandLineHandler.PrintHelpAsync(context.ProxyOutputDevice, null, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false); CompleteBuilderActivity(context.BuilderActivity, nameof(InformativeCommandLineHost)); context.EarlyHost = new InformativeCommandLineHost((int)ExitCode.InvalidCommandLine, serviceProvider); return context; } catch (FormatException) { // A tool such as --info or --version is being invoked. Degrade gracefully by treating // the malformed testconfig.json as empty so the tool can still complete its job. jsonCommandLineOptions = []; jsonCommandLineOptionDefaults = []; } ValidationResult commandLineValidationResult = await CommandLineOptionsValidator.ValidateAsync( loggingState.CommandLineParseResult, context.CommandLineHandler.SystemCommandLineOptionsProviders, context.CommandLineHandler.ExtensionsCommandLineOptionsProviders, context.CommandLineHandler, jsonCommandLineOptions, jsonCommandLineOptionDefaults).ConfigureAwait(false); if (!commandLineValidationResult.IsValid) { await DisplayBannerIfEnabledAsync(context.CommandLineHandler, context.ProxyOutputDevice, context.TestFrameworkCapabilities, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false); await context.ProxyOutputDevice.DisplayAsync(context.CommandLineHandler, new ErrorMessageOutputDeviceData(commandLineValidationResult.ErrorMessage), context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false); await context.CommandLineHandler.PrintHelpAsync(context.ProxyOutputDevice, null, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false); CompleteBuilderActivity(context.BuilderActivity, nameof(InformativeCommandLineHost)); context.EarlyHost = new InformativeCommandLineHost((int)ExitCode.InvalidCommandLine, serviceProvider); return context; } serviceProvider.TryAddService(context.CommandLineHandler); context.IsHelpCommand = context.CommandLineHandler.IsHelpInvoked(); context.IsInfoCommand = context.CommandLineHandler.IsInfoInvoked(); if (!context.IsHelpCommand && !context.IsInfoCommand) { await context.Configuration.CheckTestResultsDirectoryOverrideAndCreateItAsync(loggingState.FileLoggerProvider).ConfigureAwait(false); } await DisplayBannerIfEnabledAsync(context.CommandLineHandler, context.ProxyOutputDevice, context.TestFrameworkCapabilities, context.TestApplicationCancellationTokenSource.CancellationToken).ConfigureAwait(false); ITelemetryCollector telemetryService = await ((TelemetryManager)Telemetry).BuildTelemetryAsync(serviceProvider, context.LoggerFactory, testApplicationOptions).ConfigureAwait(false); serviceProvider.TryAddService(telemetryService); AddApplicationTelemetryMetadata(serviceProvider, builderMetrics); if (commandLineOptions.IsOptionSet(PlatformCommandLineProvider.ExitOnProcessExitOptionKey)) { if (OperatingSystem.IsBrowser()) { throw new PlatformNotSupportedException(PlatformResources.PlatformCommandLineExitOnProcessExitNotSupportedInBrowser); } serviceProvider.AddService(new NonCooperativeParentProcessListener(commandLineOptions, _environment)); } serviceProvider.AddService(new TestApplicationResult( context.ProxyOutputDevice, serviceProvider.GetCommandLineOptions(), serviceProvider.GetEnvironment(), context.PoliciesService, serviceProvider.GetPlatformOTelService(), serviceProvider.GetRequiredService())); ChatClientManager.BuildChatClients(serviceProvider); return context; } }