// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Combinatorial.MSTest; using SL = Microsoft.Build.Logging.StructuredLogger; namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests; [TestClass] public class MSBuildTests_KnownExtensionRegistration : AcceptanceTestBase { private const string AssetName = "MSBuildTests"; [CombinatorialData] [TestMethod] public async Task Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered([AllTargetFrameworks] string tfm, BuildConfiguration compilationMode, Verb verb) { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( nameof(Microsoft_Testing_Platform_Extensions_ShouldBe_Correctly_Registered), SourceCode .PatchCodeWithReplace("$TargetFrameworks$", tfm) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) .PatchCodeWithReplace("$MicrosoftTestingExtensionsAzureFoundryVersion$", MicrosoftTestingExtensionsAzureFoundryVersion) .PatchCodeWithReplace("$MicrosoftTestingExtensionsCtrfReportVersion$", MicrosoftTestingExtensionsCtrfReportVersion) .PatchCodeWithReplace("$MicrosoftTestingExtensionsGitHubActionsReportVersion$", MicrosoftTestingExtensionsGitHubActionsReportVersion) .PatchCodeWithReplace("$MicrosoftTestingExtensionsJUnitReportVersion$", MicrosoftTestingExtensionsJUnitReportVersion) .PatchCodeWithReplace("$MicrosoftTestingExtensionsVideoRecorderVersion$", MicrosoftTestingExtensionsVideoRecorderVersion)); DotnetMuxerResult result = await DotnetCli.RunAsync($"{(verb == Verb.publish ? $"publish -f {tfm}" : "build")} -c {compilationMode} -r {RID} {testAsset.TargetAssetPath} -v:n", cancellationToken: TestContext.CancellationToken); string binlogFile = result.BinlogPath!; var testHost = TestInfrastructure.TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, tfm, rid: RID, verb: verb, buildConfiguration: compilationMode); TestHostResult testHostResult = await testHost.ExecuteAsync("--help", cancellationToken: TestContext.CancellationToken); testHostResult.AssertOutputContains("--coverage-threshold-line"); testHostResult.AssertOutputContains("--coverage-threshold-branch"); testHostResult.AssertOutputContains("--crashdump"); testHostResult.AssertOutputContains("--hangdump"); testHostResult.AssertOutputContains("--publish-azdo-run-name"); testHostResult.AssertOutputContains("--publish-azdo-test-results"); testHostResult.AssertOutputContains("--report-ctrf"); testHostResult.AssertOutputContains("--report-gh"); testHostResult.AssertOutputContains("--report-html"); testHostResult.AssertOutputContains("--report-junit"); testHostResult.AssertOutputContains("--report-trx"); testHostResult.AssertOutputContains("--retry-failed-tests"); testHostResult.AssertOutputContains("--capture-video"); SL.Build binLog = BinlogReader.Read(binlogFile); SL.Target generateSelfRegisteredExtensions = binLog.FindChildrenRecursive().Single(t => t.Name == "_GenerateSelfRegisteredExtensions"); SL.Task testingPlatformSelfRegisteredExtensions = generateSelfRegisteredExtensions.FindChildrenRecursive().Single(t => t.Name == "TestingPlatformSelfRegisteredExtensions"); SL.Message generatedSource = testingPlatformSelfRegisteredExtensions.FindChildrenRecursive().Single(m => m.Text.Contains("SelfRegisteredExtensions source:")); Assert.Contains("Microsoft.Testing.Extensions.AzureDevOpsReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.AzureFoundry.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.CrashDump.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.CtrfReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.GitHubActionsReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.HangDump.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.HotReload.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.HtmlReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.JUnitReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.Retry.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.Telemetry.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.TrxReport.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); Assert.Contains("Microsoft.Testing.Extensions.VideoRecorder.TestingPlatformBuilderHook.AddExtensions", generatedSource.Text, generatedSource.Text); } [TestMethod] public async Task TestingPlatformBuilderHook_With_Conflicting_Metadata_Fails_Build() { string tfm = TargetFrameworks.NetCurrent; using TestAsset testAsset = await TestAsset.GenerateAssetAsync( nameof(TestingPlatformBuilderHook_With_Conflicting_Metadata_Fails_Build), ConflictingHookSourceCode .PatchCodeWithReplace("$TargetFrameworks$", tfm) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); DotnetMuxerResult result = await DotnetCli.RunAsync( $"build -c {BuildConfiguration.Release} -r {RID} {testAsset.TargetAssetPath} -v:n", failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); result.AssertExitCodeIsNot(0); result.AssertOutputContains("Duplicate 'TestingPlatformBuilderHook' item with Include 'CONFLICT-HOOK-ID' has conflicting metadata."); // Ensure no self-registered extensions source file was generated when validation failed. SL.Build binLog = BinlogReader.Read(result.BinlogPath!); SL.Target? generateSelfRegisteredExtensions = binLog.FindChildrenRecursive().SingleOrDefault(t => t.Name == "_GenerateSelfRegisteredExtensions"); Assert.IsNotNull(generateSelfRegisteredExtensions); SL.Task testingPlatformSelfRegisteredExtensions = generateSelfRegisteredExtensions.FindChildrenRecursive().Single(t => t.Name == "TestingPlatformSelfRegisteredExtensions"); Assert.IsEmpty(testingPlatformSelfRegisteredExtensions.FindChildrenRecursive().Where(m => m.Text.Contains("SelfRegisteredExtensions source:"))); } private const string SourceCode = """ #file MSBuildTests.csproj DummyTestFramework MyNamespaceRoot.Level1.Level2.DummyTestFrameworkRegistration $TargetFrameworks$ enable enable preview Exe $(NoWarn);NETSDK1201 (MSBuild Tests) #file Program.cs using MSBuildTests; using System.Collections.Generic; using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.Capabilities; using Microsoft.Testing.Platform.Capabilities.TestFramework; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.TestFramework; namespace MyNamespaceRoot.Level1.Level2; public static class DummyTestFrameworkRegistration { public static void AddExtensions(ITestApplicationBuilder testApplicationBuilder, string[] args) { testApplicationBuilder.RegisterTestFramework(_ => new Capabilities(), (_, __) => new DummyTestFramework()); } } internal sealed class DummyTestFramework : ITestFramework, IDataProducer { public string Uid => nameof(DummyTestFramework); public string Version => string.Empty; public string DisplayName => string.Empty; public string Description => string.Empty; public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) }; public Task CloseTestSessionAsync(CloseTestSessionContext context) => Task.FromResult(new CloseTestSessionResult() { IsSuccess = true }); public Task CreateTestSessionAsync(CreateTestSessionContext context) => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); public async Task ExecuteRequestAsync(ExecuteRequestContext context) { await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode() { Uid = "1", DisplayName = "DummyTest", Properties = new(PassedTestNodeStateProperty.CachedInstance) })); context.Complete(); } public Task IsEnabledAsync() => Task.FromResult(true); } internal sealed class Capabilities : ITestFrameworkCapabilities { IReadOnlyCollection ICapabilities.Capabilities => Array.Empty(); } #file DummyClass.cs namespace MSBuildTests.Microsoft; // This serves as a test to ensure that we don't regress generation of 'global::' // If the self registered generation used Microsoft.Testing.Extensions.ExtensionName.TestingPlatformBuilderHook.AddExtension, // Then, without global::, Microsoft will be referring to MSBuildTests.Microsoft namespace and will fail to compile public static class DummyClass { } """; private const string ConflictingHookSourceCode = """ #file MSBuildTests.csproj FirstHook Contoso.FirstHook SecondHook Contoso.SecondHook $TargetFrameworks$ enable enable preview Exe $(NoWarn);NETSDK1201 #file Program.cs // Empty program. Build should fail before reaching compilation because of the // duplicate TestingPlatformBuilderHook items with conflicting metadata. """; public TestContext TestContext { get; set; } }