// 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.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Helpers; using MelIConfiguration = Microsoft.Extensions.Configuration.IConfiguration; using MelILoggerFactory = Microsoft.Extensions.Logging.ILoggerFactory; namespace Microsoft.Testing.Extensions; /// /// Extension methods for composing an application-owned with Microsoft Testing Platform. /// /// /// 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 static class MicrosoftExtensionsHostingExtensions { /// /// Determines whether a generated entry point should run an informational MTP command without creating the application host. /// /// The MTP command-line arguments. /// for help and info options, including options expanded from response files. /// /// This API is experimental. It may change, break, or be removed at any time without notice. /// public static bool ShouldBypassApplicationHost(string[] args) { _ = args ?? throw new ArgumentNullException(nameof(args)); CommandLineParseResult parseResult = CommandLineParser.Parse(args, new SystemEnvironment()); return parseResult.Options.Any( static option => option.Name.Equals(PlatformCommandLineProvider.HelpOptionKey, StringComparison.OrdinalIgnoreCase) || option.Name.Equals(PlatformCommandLineProvider.HelpOptionQuestionMark, StringComparison.OrdinalIgnoreCase) || option.Name.Equals(PlatformCommandLineProvider.InfoOptionKey, StringComparison.OrdinalIgnoreCase)); } /// /// Starts the host, runs a Microsoft Testing Platform application, and stops the host. /// /// The application-owned host. /// The test application command-line arguments. /// Registers the test framework and optional MTP extensions. /// A token used while starting the host and running the test application. /// The Microsoft Testing Platform exit code. /// /// /// The host remains owned by the caller and is not disposed by this method. Its configuration is /// imported as a read-only snapshot, and its logger factory is borrowed without transferring ownership. /// /// /// The host is started before the MTP application is built so hosted observability providers can /// subscribe before MTP creates diagnostic activities and meters. The host is stopped in a /// block after MTP completes or fails. /// /// /// Composition is process-local. Host services are not propagated to separately launched test host /// or controller processes. /// /// public static async Task RunTestingPlatformAsync( this IHost host, string[] args, Action configure, CancellationToken cancellationToken = default) { _ = host ?? throw new ArgumentNullException(nameof(host)); _ = args ?? throw new ArgumentNullException(nameof(args)); _ = configure ?? throw new ArgumentNullException(nameof(configure)); MelIConfiguration configuration = host.Services.GetRequiredService(); MelILoggerFactory loggerFactory = host.Services.GetRequiredService(); IHostApplicationLifetime? hostApplicationLifetime = host.Services.GetService(); HostApplicationLifetimeBridge? hostApplicationLifetimeBridge = hostApplicationLifetime is null ? null : new(hostApplicationLifetime); var testApplicationOptions = new TestApplicationOptions { CancellationToken = cancellationToken, HostLifetimeBridge = hostApplicationLifetimeBridge, }; ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args, testApplicationOptions).ConfigureAwait(continueOnCapturedContext: default); try { testApplicationBuilder.AddMicrosoftExtensionsConfigurationSnapshot(configuration); testApplicationBuilder.AddMicrosoftExtensionsLogging(loggerFactory); foreach (ITestingPlatformBuilderConfigurator configurator in host.Services.GetServices()) { configurator.Configure(testApplicationBuilder); } configure(testApplicationBuilder); Exception? operationException = null; try { await host.StartAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: default); using ITestApplication testApplication = await testApplicationBuilder.BuildAsync().ConfigureAwait(continueOnCapturedContext: default); return await testApplication.RunAsync().ConfigureAwait(continueOnCapturedContext: default); } catch (Exception exception) { operationException = exception; throw; } finally { hostApplicationLifetimeBridge?.Disconnect(); try { await host.StopAsync(CancellationToken.None).ConfigureAwait(continueOnCapturedContext: default); } catch (Exception stopException) when (operationException is not null) { AddSecondaryExceptionOrThrow(operationException, nameof(IHost.StopAsync), stopException); } } } catch (Exception exception) { try { if (testApplicationBuilder is IAsyncCleanableExtension cleanableBuilder) { await cleanableBuilder.CleanupAsync().ConfigureAwait(continueOnCapturedContext: default); } } catch (Exception disposeException) { AddSecondaryExceptionOrThrow(exception, nameof(IAsyncCleanableExtension.CleanupAsync), disposeException); } throw; } } private static void AddSecondaryExceptionOrThrow(Exception primaryException, string operation, Exception secondaryException) { try { if (primaryException.Data is { IsReadOnly: false } data) { data[operation] = secondaryException; return; } } catch (Exception) { // An exception can expose an IDictionary implementation that rejects reads or writes. } throw new AggregateException( $"{operation} failed while handling another exception.", primaryException, secondaryException); } }