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