// 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.Extensions.GitHubActionsReport; using Microsoft.Testing.Extensions.GitHubActionsReport.Resources; using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.ArtifactPostProcessing; using Microsoft.Testing.Platform.Logging; using Microsoft.Testing.Platform.ServerMode; using Microsoft.Testing.Platform.Services; namespace Microsoft.Testing.Extensions; /// /// Provides extension methods for adding GitHub Actions reporting support to the test application builder. /// public static class GitHubActionsExtensions { // Must match Microsoft.Testing.Extensions.Retry's hidden child-host option. Referencing the retry assembly // directly would create a package dependency solely for this internal orchestration handshake. private const string RetryPipeOptionName = "internal-retry-pipename"; /// /// Adds support to the test application builder. /// /// The test application builder. public static void AddGitHubActionsProvider(this ITestApplicationBuilder builder) { Lazy? historyService = null; object historyServiceLock = new(); Func getHistoryService = serviceProvider => { lock (historyServiceLock) { historyService ??= new Lazy( () => CreateHistoryService(serviceProvider), LazyThreadSafetyMode.ExecutionAndPublication); return historyService.Value; } }; var compositeSummaryReporter = new CompositeExtensionFactory(serviceProvider => new GitHubActionsSummaryReporter( serviceProvider.GetCommandLineOptions(), serviceProvider.GetConfiguration(), serviceProvider.GetEnvironment(), serviceProvider.GetFileSystem(), serviceProvider.GetMessageBus(), serviceProvider.GetOutputDevice(), serviceProvider.GetTestApplicationModuleInfo(), serviceProvider.GetTestApplicationProcessExitCode(), serviceProvider.GetRequiredService(), serviceProvider.GetLoggerFactory(), () => ShouldDeferToArtifactPostProcessing(serviceProvider), getHistoryService(serviceProvider))); var compositeSlowTestReporter = new CompositeExtensionFactory(serviceProvider => new GitHubActionsSlowTestReporter( serviceProvider.GetCommandLineOptions(), serviceProvider.GetEnvironment(), serviceProvider.GetOutputDevice(), serviceProvider.GetTask(), serviceProvider.GetClock(), serviceProvider.GetLoggerFactory())); var compositeReporter = new CompositeExtensionFactory(serviceProvider => new GitHubActionsReporter( serviceProvider.GetCommandLineOptions(), serviceProvider.GetEnvironment(), serviceProvider.GetOutputDevice(), serviceProvider.GetTestApplicationModuleInfo(), serviceProvider.GetLoggerFactory())); var compositeAnnotationReporter = new CompositeExtensionFactory(serviceProvider => new GitHubActionsAnnotationReporter( serviceProvider.GetCommandLineOptions(), serviceProvider.GetEnvironment(), serviceProvider.GetFileSystem(), serviceProvider.GetOutputDevice(), serviceProvider.GetTestApplicationProcessExitCode(), serviceProvider.GetLoggerFactory(), getHistoryService(serviceProvider))); builder.TestHost.AddTestSessionLifetimeHandler(serviceProvider => getHistoryService(serviceProvider)); builder.TestHost.AddDataConsumer(compositeAnnotationReporter); builder.TestHost.AddTestSessionLifetimeHandler(compositeAnnotationReporter); builder.TestHost.AddDataConsumer(compositeSummaryReporter); builder.TestHost.AddTestSessionLifetimeHandler(compositeSummaryReporter); builder.TestHost.AddDataConsumer(compositeSlowTestReporter); builder.TestHost.AddTestSessionLifetimeHandler(compositeSlowTestReporter); // Register the group reporter last, as both a data consumer (no-op) and a session-lifetime handler, so its // closing '::endgroup::' is ordered into the consumer phase after every other reporter's final output. builder.TestHost.AddDataConsumer(compositeReporter); builder.TestHost.AddTestSessionLifetimeHandler(compositeReporter); builder.CommandLine.AddProvider(() => new GitHubActionsCommandLineProvider()); CiCoverageSummaryControllerHandlerRegistration.Register( builder.TestHostControllers, GitHubActionsSummaryArtifactPostProcessor.Provider, GitHubActionsSummaryArtifactPostProcessor.FragmentArtifactKind, serviceProvider => serviceProvider.GetLoggerFactory().CreateLogger(), serviceProvider => serviceProvider.GetServicesInternal().OfType().Single(), ShouldDeferToArtifactPostProcessing, (path, exception) => string.Format(CultureInfo.InvariantCulture, GitHubActionsResources.StepSummaryWriteFailedWarning, path, exception.Message)); if (builder is IArtifactPostProcessingApplicationBuilder artifactPostProcessingBuilder) { artifactPostProcessingBuilder.ArtifactPostProcessing.AddArtifactPostProcessor(serviceProvider => new GitHubActionsSummaryArtifactPostProcessor( serviceProvider.GetCommandLineOptions(), serviceProvider.GetEnvironment(), serviceProvider.GetFileSystem(), serviceProvider.GetLoggerFactory(), () => serviceProvider.GetService() is DotnetTestConnection { IsRequiredArtifactPostProcessingSupported: true, }, CreateHistoryService(serviceProvider))); } } private static GitHubActionsHistoryService CreateHistoryService(IServiceProvider serviceProvider) => new( serviceProvider.GetCommandLineOptions(), serviceProvider.GetEnvironment(), serviceProvider.GetClock(), serviceProvider.GetLoggerFactory(), new GitHubActionsHistoryScope( serviceProvider.GetTestApplicationModuleInfo().TryGetAssemblyName() ?? "unknown assembly name", TargetFrameworkMonikerHelper.GetTargetFrameworkMonikerIncludingPlatform(), RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant(), serviceProvider.GetEnvironment().GetEnvironmentVariable("RUNNER_OS") ?? string.Empty)); private static bool ShouldDeferToArtifactPostProcessing(IServiceProvider serviceProvider) { bool dotnetTestRequiresPostProcessing = serviceProvider.GetService() is DotnetTestConnection connection && connection.IsRequiredArtifactPostProcessingSupported; return dotnetTestRequiresPostProcessing || serviceProvider.GetCommandLineOptions().IsOptionSet(RetryPipeOptionName) || CoverageThresholdPolicy.IsDeferredToController(serviceProvider.GetCommandLineOptions(), serviceProvider.GetEnvironment()); } }