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