// 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.Internal.Framework; using Microsoft.Testing.Platform.AI; using Microsoft.Testing.Platform.Capabilities.TestFramework; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Configurations; using Microsoft.Testing.Platform.DynamicExtensions; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.ArtifactPostProcessing; using Microsoft.Testing.Platform.Extensions.TestFramework; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.Hosts; using Microsoft.Testing.Platform.Logging; using Microsoft.Testing.Platform.Resources; using Microsoft.Testing.Platform.Services; using Microsoft.Testing.Platform.Telemetry; using Microsoft.Testing.Platform.TestHost; using Microsoft.Testing.Platform.TestHostControllers; using Microsoft.Testing.Platform.TestHostOrchestrator; using Microsoft.Testing.Platform.Tools; namespace Microsoft.Testing.Platform.Builder; /// /// A builder for test applications and services. /// internal sealed class TestApplicationBuilder : IArtifactPostProcessingApplicationBuilder, IAsyncCleanableExtension, IDynamicExtensionRegistrationGuard { private readonly DateTimeOffset _createBuilderStart; private readonly ApplicationLoggingState _loggingState; private readonly TestApplicationOptions _testApplicationOptions; private readonly IUnhandledExceptionsHandler _unhandledExceptionsHandler; private readonly TestHostBuilder _testHostBuilder; private IHost? _host; private Func? _testFrameworkFactory; private Func? _testFrameworkCapabilitiesFactory; private DynamicExtensionScope? _currentDynamicExtension; internal TestApplicationBuilder( ApplicationLoggingState loggingState, DateTimeOffset createBuilderStart, TestApplicationOptions testApplicationOptions, IUnhandledExceptionsHandler unhandledExceptionsHandler, string[] args) : this(loggingState, createBuilderStart, testApplicationOptions, unhandledExceptionsHandler, args, args) { } internal TestApplicationBuilder( ApplicationLoggingState loggingState, DateTimeOffset createBuilderStart, TestApplicationOptions testApplicationOptions, IUnhandledExceptionsHandler unhandledExceptionsHandler, string[] args, string[] expandedArgs) { _testHostBuilder = new TestHostBuilder(new SystemFileSystem(), new SystemRuntimeFeature(), new SystemEnvironment(), new SystemProcessHandler(), new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler(), args, expandedArgs)); _createBuilderStart = createBuilderStart; _loggingState = loggingState; _testApplicationOptions = testApplicationOptions; _unhandledExceptionsHandler = unhandledExceptionsHandler; } public IChatClientManager ChatClientManager => _testHostBuilder.ChatClientManager; public ITestHostManager TestHost => _testHostBuilder.TestHost; public ITestHostControllersManager TestHostControllers => _testHostBuilder.TestHostControllers; /// /// /// This API is experimental. It may change, break, or be removed at any time without notice. /// [Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] ITestHostOrchestratorManager ITestApplicationBuilder.TestHostOrchestrator => _testHostBuilder.TestHostOrchestrator; // Binary backward compatibility: old extensions access this property on the concrete class. internal Extensions.TestHostOrchestrator.ITestHostOrchestratorManager TestHostOrchestrator => (Extensions.TestHostOrchestrator.ITestHostOrchestratorManager)_testHostBuilder.TestHostOrchestrator; public ICommandLineManager CommandLine => _testHostBuilder.CommandLine; /// /// /// This API is experimental. It may change, break, or be removed at any time without notice. /// [Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] public IConfigurationManager Configuration => _testHostBuilder.Configuration; /// /// /// This API is experimental. It may change, break, or be removed at any time without notice. /// [Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] public ILoggingManager Logging => _testHostBuilder.Logging; internal ITelemetryManager Telemetry => _testHostBuilder.Telemetry; /// /// /// This API is experimental. It may change, break, or be removed at any time without notice. /// [Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] public IArtifactPostProcessingManager ArtifactPostProcessing => _testHostBuilder.ArtifactPostProcessing; /// /// /// This API is experimental. It may change, break, or be removed at any time without notice. /// [Experimental("TPEXP", UrlFormat = "https://aka.ms/testingplatform/diagnostics#{0}")] public IToolsManager Tools => _testHostBuilder.Tools; public ITestApplicationBuilder RegisterTestFramework( Func capabilitiesFactory, Func frameworkFactory) { if (_currentDynamicExtension is { } dynamicExtension) { throw new InvalidOperationException(string.Format( CultureInfo.InvariantCulture, PlatformResources.DynamicExtensionCannotRegisterTestFrameworkErrorMessage, dynamicExtension.DisplayName, dynamicExtension.ManifestPath)); } if (frameworkFactory is null) { throw new ArgumentNullException(nameof(frameworkFactory)); } if (capabilitiesFactory is null) { throw new ArgumentNullException(nameof(capabilitiesFactory)); } if (_testFrameworkFactory is not null) { throw new InvalidOperationException(PlatformResources.TestApplicationBuilderFrameworkAdapterFactoryAlreadyRegisteredErrorMessage); } _testFrameworkFactory = frameworkFactory; if (_testFrameworkCapabilitiesFactory is not null) { throw new InvalidOperationException(PlatformResources.TestApplicationBuilderTestFrameworkCapabilitiesAlreadyRegistered); } _testFrameworkCapabilitiesFactory = capabilitiesFactory; _testHostBuilder.TestFramework = new TestFrameworkManager(_testFrameworkFactory, _testFrameworkCapabilitiesFactory); return this; } public async Task BuildAsync() { if (_testFrameworkFactory is null) { throw new InvalidOperationException(PlatformResources.TestApplicationBuilderTestFrameworkNotRegistered); } if (_host is not null) { throw new InvalidOperationException(PlatformResources.TestApplicationBuilderApplicationAlreadyRegistered); } _host = await _testHostBuilder.BuildAsync(_loggingState, _testApplicationOptions, _unhandledExceptionsHandler, _createBuilderStart).ConfigureAwait(false); return new TestApplication(_host); } async Task IAsyncCleanableExtension.CleanupAsync() { if (_host is null && _loggingState.FileLoggerProvider is { } fileLoggerProvider) { await DisposeHelper.DisposeAsync(fileLoggerProvider).ConfigureAwait(false); } } // The scope covers the synchronous execution of the hook, which is the whole window in which a hook is // contracted to touch the builder. ITestApplicationBuilder is not thread-safe, so a hook that captures the // builder and uses it from a background task after returning is already outside the contract for every // member, not just RegisterTestFramework. That is also why the guard is not flowed through the execution // context: it would single out one member, would not survive ExecutionContext.SuppressFlow anyway, and // would surface as an unhandled exception on a thread pool thread rather than an actionable startup error. IDisposable IDynamicExtensionRegistrationGuard.EnterDynamicExtensionScope(string displayName, string manifestPath) { DynamicExtensionScope scope = new(this, displayName, manifestPath); _currentDynamicExtension = scope; return scope; } private sealed class DynamicExtensionScope : IDisposable { private readonly TestApplicationBuilder _owner; public DynamicExtensionScope(TestApplicationBuilder owner, string displayName, string manifestPath) { _owner = owner; DisplayName = displayName; ManifestPath = manifestPath; } public string DisplayName { get; } public string ManifestPath { get; } public void Dispose() => _owner._currentDynamicExtension = null; } }