// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.Testing.Platform.Acceptance.IntegrationTests; // InvokeTestingPlatformTask does not yet preserve its terminal summary and artifacts in -mt's // out-of-process TaskHost, so tests that exercise the MSBuild Test target must opt out. [TestClass] public class MSBuildTests_Test : AcceptanceTestBase { private const string AssetName = "MSBuildTests"; public static string? FormatBuildMatrixEntry(MethodInfo method, object?[]? data) => $"{data![0]},{(Equals(TargetFrameworks.All.ToMSBuildTargetFrameworks(), data[1]) ? "multitfm" : data[1])},{data[2]},{((bool)data[3]! ? "Succeeded" : "Failed")}"; internal static IEnumerable<(string BuildCommand, string TargetFramework, BuildConfiguration BuildConfiguration, bool TestSucceeded)> GetBuildMatrix() { foreach (string tfm in TargetFrameworks.All) { foreach ((string buildCommand, _, BuildConfiguration buildConfiguration, bool testSucceeded) in GetBuildMatrixMultiTfm()) { yield return (buildCommand, tfm, buildConfiguration, testSucceeded); } } } internal static IEnumerable<(string BuildCommand, string TargetFramework, BuildConfiguration BuildConfiguration, bool TestSucceeded)> GetBuildMatrixMultiTfm() { foreach (BuildConfiguration compilationMode in Enum.GetValues()) { foreach (bool testSucceeded in new bool[] { true, false }) { yield return ("build -t:Test", TargetFrameworks.All.ToMSBuildTargetFrameworks(), compilationMode, testSucceeded); } } } [DynamicData(nameof(GetBuildMatrix), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))] [TestMethod] public async Task InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail_SingleTfm(string testCommand, string tfm, BuildConfiguration compilationMode, bool testSucceeded) => await InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail(testCommand, tfm, false, [tfm], compilationMode, testSucceeded); [DynamicData(nameof(GetBuildMatrixMultiTfm), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))] [TestMethod] public async Task InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail_MultiTfm(string testCommand, string multiTfm, BuildConfiguration compilationMode, bool testSucceeded) => await InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail(testCommand, multiTfm, true, TargetFrameworks.All, compilationMode, testSucceeded); private async Task InvokeTestingPlatform_Target_Should_Execute_Tests_Without_Showing_Error_Detail(string testCommand, string tfm, bool isMultiTfm, string[] tfmsToAssert, BuildConfiguration compilationMode, bool testSucceeded) { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", "x64") .PatchCodeWithReplace("$TargetFrameworks$", isMultiTfm ? $"{tfm}" : $"{tfm}") .PatchCodeWithReplace("$AssertValue$", testSucceeded.ToString().ToLowerInvariant()) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); string testResultFolder = Path.Combine(testAsset.TargetAssetPath, Guid.NewGuid().ToString("N")); DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"{testCommand} -p:TestingPlatformCommandLineArguments=\"--results-directory %22{testResultFolder}%22\" -p:Configuration={compilationMode} \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, failIfReturnValueIsNotZero: false, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); foreach (string tfmToAssert in tfmsToAssert) { CommonAssert(compilationResult, tfmToAssert, testSucceeded, testResultFolder); } } [DynamicData(nameof(GetBuildMatrix), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))] [TestMethod] public async Task InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_SingleTfm(string testCommand, string tfm, BuildConfiguration compilationMode, bool testSucceeded) => await InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_Detail(testCommand, tfm, false, [tfm], compilationMode, testSucceeded); [DynamicData(nameof(GetBuildMatrixMultiTfm), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))] [TestMethod] public async Task InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_MultiTfm(string testCommand, string multiTfm, BuildConfiguration compilationMode, bool testSucceeded) => await InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_Detail(testCommand, multiTfm, true, TargetFrameworks.All, compilationMode, testSucceeded); private async Task InvokeTestingPlatform_Target_Should_Build_Without_Warnings_And_Execute_Passing_Test_And_Pass_TheRun_Detail(string testCommand, string tfm, bool isMultiTfm, string[] tfmsToAssert, BuildConfiguration compilationMode, bool testSucceeded) { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", "x64") .PatchCodeWithReplace("$TargetFrameworks$", isMultiTfm ? $"{tfm}" : $"{tfm}") .PatchCodeWithReplace("$AssertValue$", testSucceeded.ToString().ToLowerInvariant()) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); string testResultFolder = Path.Combine(testAsset.TargetAssetPath, Guid.NewGuid().ToString("N")); DotnetMuxerResult compilationResult = testCommand.StartsWith("test", StringComparison.OrdinalIgnoreCase) ? await DotnetCli.RunAsync( $"{testCommand} -p:Configuration={compilationMode} \"{testAsset.TargetAssetPath}\" -- --treenode-filter --results-directory \"{testResultFolder}\"", workingDirectory: testAsset.TargetAssetPath, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken) : await DotnetCli.RunAsync( $"{testCommand} -p:TestingPlatformCommandLineArguments=\"--treenode-filter --results-directory \"{testResultFolder}\"\" -p:Configuration={compilationMode} \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); foreach (string tfmToAssert in tfmsToAssert) { CommonAssert(compilationResult, tfmToAssert, testSucceeded, testResultFolder); } } [TestMethod] [OSCondition(OperatingSystems.Windows)] public async Task RunUsingTestTargetWithNetfxMSBuild() { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", string.Empty) .PatchCodeWithReplace("$TargetFrameworks$", $"{TargetFrameworks.NetCurrent}") .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant()) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); string msbuildExe = await FindMsbuildWithVsWhereAsync(TestContext.CancellationToken); using var commandLine = new TestInfrastructure.CommandLine(); string restoreBinlogFile = Path.Combine(TempDirectory.TestSuiteDirectory, $"{nameof(RunUsingTestTargetWithNetfxMSBuild)}-restore-{{}}.binlog"); string binlogFile = Path.Combine(TempDirectory.TestSuiteDirectory, $"{nameof(RunUsingTestTargetWithNetfxMSBuild)}-build-{{}}.binlog"); await commandLine.RunAsync($"\"{msbuildExe}\" \"{testAsset.TargetAssetPath}\" /t:Restore /bl:\"{restoreBinlogFile}\"", cancellationToken: TestContext.CancellationToken); await commandLine.RunAsync( $"\"{msbuildExe}\" \"{testAsset.TargetAssetPath}\" /t:\"Build;Test\" /p:UseSharedCompilation=false /bl:\"{binlogFile}\"", environmentVariables: new Dictionary { ["DOTNET_ROOT"] = Path.Combine(RootFinder.Find(), ".dotnet"), }, cancellationToken: TestContext.CancellationToken); Assert.Contains("Tests succeeded", commandLine.StandardOutput); } [TestMethod] [OSCondition(OperatingSystems.Windows)] public async Task Invoke_TestTarget_With_Arch_Switch_x86_Should_Work() { string root = RootFinder.Find(); string x86Muxer = Path.Combine(root, ".dotnet", "x86"); var dotnetRootX86 = new Dictionary { { "DOTNET_ROOT_X86", x86Muxer }, }; using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", string.Empty) .PatchCodeWithReplace("$TargetFrameworks$", $"{TargetFrameworks.NetCurrent}") .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant()) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); await DotnetCli.RunAsync( $"build -t:Test --arch x86 -p:Configuration=Release \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, environmentVariables: dotnetRootX86, failIfReturnValueIsNotZero: false, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); string[] outputLogFiles = Directory.GetFiles(testAsset.TargetAssetPath, $"MSBuild Tests_{TargetFrameworks.NetCurrent}_x86.log", SearchOption.AllDirectories); Assert.ContainsSingle(outputLogFiles, $"Was expecting to find a single log file but found {outputLogFiles.Length}"); string logFileContent = File.ReadAllText(outputLogFiles[0]); Assert.IsTrue(Regex.IsMatch(logFileContent, ".*win-x86.*"), logFileContent); // This is the architecture part that's written by TerminalOutputDevice when there is no banner specified. Assert.Contains($"[win-x86 - {TargetFrameworks.NetCurrent}]", logFileContent); } [TestMethod] public async Task Invoke_TestTarget_With_Incompatible_Arch() { // TODO: Test with both old and new dotnet test experience. Architecture currentArchitecture = RuntimeInformation.ProcessArchitecture; string incompatibleArchitecture = currentArchitecture switch { Architecture.X86 or Architecture.X64 => "arm64", _ => "x64", }; using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", string.Empty) .PatchCodeWithReplace("$TargetFrameworks$", $"{TargetFrameworks.NetCurrent}") .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant()) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); DotnetMuxerResult result = await DotnetCli.RunAsync( $"build -t:Test --arch {incompatibleArchitecture} \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, failIfReturnValueIsNotZero: false, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); // The build should fail and the failure should mention the incompatible target architecture // so we know the build/launch reached the apphost rather than failing for an unrelated reason. result.AssertExitCodeIsNot(0); result.AssertOutputContains($"[{TargetFrameworks.NetCurrent}|{incompatibleArchitecture}]"); if (OperatingSystem.IsLinux()) { // On Linux, the apphost launch failure can surface in two different ways depending on // whether the kernel/runtime path rejects the binary up-front or falls back to a shell: // 1) ToolTask catches a Win32Exception ("Exec format error") and emits MSB6003. // 2) The apphost falls back to /bin/sh which then fails to parse the binary, and // InvokeTestingPlatformTask.HandleTaskExecutionErrors reports the captured tool // output via "error run failed: Tests failed:" (often a shell-parse error like // "Syntax error: word unexpected"). // Either shape is acceptable — both prove we attempted to launch the incompatible-arch // apphost and failed. bool hasMsb6003 = result.StandardOutput.Contains("error MSB6003:", StringComparison.Ordinal) && result.StandardOutput.Contains("Exec format error", StringComparison.Ordinal); bool hasRunFailed = result.StandardOutput.Contains("error run failed: Tests failed:", StringComparison.Ordinal); Assert.IsTrue( hasMsb6003 || hasRunFailed, $"Expected output to contain either MSB6003+\"Exec format error\" or \"error run failed: Tests failed:\". Actual output: {result.StandardOutput}"); } else { // On Windows we run the exe directly; on macOS we run the apphost. string executableName = OperatingSystem.IsWindows() ? "MSBuild Tests.exe" : "MSBuild Tests"; result.AssertOutputContains($"error MSB6003: The specified task executable \"{executableName}\" could not be run. System.ComponentModel.Win32Exception"); result.AssertOutputContains("An error occurred trying to start process"); if (OperatingSystem.IsWindows()) { // Launching an incompatible-architecture apphost can surface with different native // error text depending on the exact Windows build/runtime path: // 1) CreateProcess fails with ERROR_BAD_EXE_FORMAT / ERROR_EXE_MACHINE_TYPE_MISMATCH // and .NET wraps it with the friendly "not a valid application for this OS platform". // 2) CreateProcess fails but GetLastError() reports 0, so the raw Win32Exception message // is "The operation completed successfully." (HResult 0x80004005). // Both prove we attempted to launch the incompatible-arch apphost and failed. The MSB6003 + // "An error occurred trying to start process" assertions above already establish that. bool hasInvalidApplication = result.StandardOutput.Contains("The specified executable is not a valid application for this OS platform.", StringComparison.Ordinal); bool hasOperationCompleted = result.StandardOutput.Contains("The operation completed successfully.", StringComparison.Ordinal); Assert.IsTrue( hasInvalidApplication || hasOperationCompleted, $"Expected output to contain either \"The specified executable is not a valid application for this OS platform.\" or \"The operation completed successfully.\". Actual output: {result.StandardOutput}"); } else if (OperatingSystem.IsMacOS()) { result.AssertOutputContains("Bad CPU type in executable"); } else { // Unexpected OS. throw ApplicationStateGuard.Unreachable(); } } } [TestMethod] [OSCondition(OperatingSystems.Windows)] public async Task Invoke_TestTarget_With_DOTNET_HOST_PATH_Should_Work() { string root = RootFinder.Find(); string dotnetHostPath = Path.Combine(root, ".dotnet", "dotnet.exe"); var dotnetHostPathEnvVar = new Dictionary { { "DOTNET_HOST_PATH", dotnetHostPath }, }; using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", string.Empty) .PatchCodeWithReplace("$TargetFrameworks$", $"{TargetFrameworks.NetCurrent}") .PatchCodeWithReplace("$AssertValue$", bool.TrueString.ToLowerInvariant()) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); await DotnetCli.RunAsync( $"build -t:Test -p:Configuration=Release \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, environmentVariables: dotnetHostPathEnvVar, failIfReturnValueIsNotZero: false, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); string[] outputLogFiles = Directory.GetFiles(testAsset.TargetAssetPath, $"MSBuild Tests_{TargetFrameworks.NetCurrent}_x64.log", SearchOption.AllDirectories); Assert.ContainsSingle(outputLogFiles, $"Was expecting to find a single log file but found {outputLogFiles.Length}"); string logFileContent = File.ReadAllText(outputLogFiles[0]); // This is the architecture part that's written by TerminalOutputDevice when there is no banner specified. Assert.Contains($"[win-x64 - {TargetFrameworks.NetCurrent}]", logFileContent); } private static void CommonAssert(DotnetMuxerResult compilationResult, string tfm, bool testSucceeded, string testResultFolder) { Assert.IsTrue(Regex.IsMatch(compilationResult.StandardOutput, $".*Run tests:.* \\[{tfm}|x64\\]"), compilationResult.StandardOutput); if (testSucceeded) { Assert.IsTrue(Regex.IsMatch(compilationResult.StandardOutput, $"Tests succeeded:.* \\[{tfm}|x64\\]"), compilationResult.StandardOutput); } else { Assert.IsTrue(Regex.IsMatch(compilationResult.StandardOutput, $".*error : Tests failed:.* \\[{tfm}|x64\\]"), compilationResult.StandardOutput); } // Regression test for https://github.com/microsoft/testfx/issues/8437: the MSBuild // extension must keep reporting a non-empty Duration in its run summary even after // dropping its dependency on the internal TestRequestExecutionTimeInfo message. Assert.IsTrue( Regex.IsMatch(compilationResult.StandardOutput, @"Failed: \d+, Passed: \d+, Skipped: \d+, Total: \d+, Duration: \S+"), compilationResult.StandardOutput); string outputFileLog = Path.Combine(testResultFolder, $"MSBuild Tests_{tfm}_x64.log"); Assert.IsTrue(File.Exists(outputFileLog), $"Expected file '{outputFileLog}'"); Assert.IsFalse(string.IsNullOrEmpty(File.ReadAllText(outputFileLog)), $"Content of file '{File.ReadAllText(outputFileLog)}'"); } // We avoid to test the multi-tfm because it's already tested with the above tests and we don't want to have too heavy testing, // msbuild is pretty heavy (a lot of processes started due to the no 'nodereuse') and makes tests flaky. // We test two functionality for the same reason, we don't want to load too much the CI only for UX reasons. [DynamicData(nameof(GetBuildMatrix), DynamicDataDisplayName = nameof(FormatBuildMatrixEntry))] [TestMethod] public async Task InvokeTestingPlatform_Target_Showing_Error_And_Do_Not_Capture_The_Output_SingleTfm(string testCommand, string tfm, BuildConfiguration compilationMode, bool testSucceeded) { // We test only failed but we don't want to have too much argument provider overload. if (testSucceeded) { return; } using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", "x64") .PatchCodeWithReplace("$TargetFrameworks$", $"{tfm}") .PatchCodeWithReplace("$AssertValue$", testSucceeded.ToString().ToLowerInvariant()) .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"{testCommand} -p:TestingPlatformShowTestsFailure=True -p:TestingPlatformCaptureOutput=False -p:Configuration={compilationMode} {testAsset.TargetAssetPath}", workingDirectory: testAsset.TargetAssetPath, failIfReturnValueIsNotZero: false, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); compilationResult.AssertOutputContains("error test failed: Test2 ("); compilationResult.AssertOutputContains("FAILED: Expected 'true', but got 'false'."); compilationResult.AssertOutputContains("Microsoft.Testing.Platform"); } [TestMethod] public async Task TestingPlatformDisableCustomTestTarget_Should_Cause_UserDefined_Target_To_Run() { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", "x64") .PatchCodeWithReplace("$TargetFrameworks$", $"{TargetFrameworks.NetCurrent}") .PatchCodeWithReplace("$AssertValue$", "true") .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build {testAsset.TargetAssetPath} -p:TestingPlatformDisableCustomTestTarget=true -p:ImportUserDefinedTestTarget=true -t:\"Build;Test\"", failIfReturnValueIsNotZero: false, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); compilationResult.AssertOutputContains("Error from UserDefinedTestTarget.targets"); } [TestMethod] public async Task InvokeTestingPlatform_Target_Should_Pass_EnvironmentVariables_To_TestProcess() { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", "x64") .PatchCodeWithReplace("$TargetFrameworks$", $"{TargetFrameworks.NetCurrent}") .PatchCodeWithReplace("$AssertValue$", "true") .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); // TestingPlatformCaptureOutput=False routes the test process stdout through the MSBuild log so the values the // test process actually observed can be asserted on. DotnetMuxerResult compilationResult = await DotnetCli.RunAsync( $"build -t:Test -p:TestingPlatformCaptureOutput=False -p:AddTestingPlatformEnvironmentVariableItem=true \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, environmentVariables: new Dictionary { // Deliberately NOT declared as a TestingPlatformEnvironmentVariable item: it only reaches the test // process by inheritance from the MSBuild process. ["MTP_ENV_INHERITED"] = "inherited-value", }, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); compilationResult.AssertOutputContains("[env] MTP_ENV_SIMPLE=simple-value"); // The value is carried by item metadata, which MSBuild does not split on ';', so it must arrive intact. compilationResult.AssertOutputContains("[env] MTP_ENV_WITH_SEMICOLON=first;second"); // ToolTask layers EnvironmentVariables on top of the inherited environment rather than replacing it. This is // the load-bearing half of the feature: if it ever became replace semantics, declaring a single variable would // strip PATH / DOTNET_ROOT / ... from every test process, so pin it here alongside the declared values. compilationResult.AssertOutputContains("[env] MTP_ENV_INHERITED=inherited-value"); } [TestMethod] public async Task InvokeTestingPlatform_Target_Should_Set_DotnetRoot_For_AppHost() { string architecture = RuntimeInformation.ProcessArchitecture.ToString(); string dotnetRootVariableName = $"DOTNET_ROOT_{architecture.ToUpperInvariant()}"; string dotnetRoot = Path.Combine(RootFinder.Find(), ".dotnet"); using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SourceCode .PatchCodeWithReplace("$PlatformTarget$", $"{architecture.ToLowerInvariant()}") .PatchCodeWithReplace("$TargetFrameworks$", $"{TargetFrameworks.NetCurrent}") .PatchCodeWithReplace("$AssertValue$", "true") .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion)); Dictionary CreateEnvironmentVariables() => new() { [dotnetRootVariableName] = null, }; DotnetMuxerResult derivedResult = await DotnetCli.RunAsync( $"build -t:Test -p:TestingPlatformCaptureOutput=False \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, environmentVariables: CreateEnvironmentVariables(), useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); string dotnetRootOutputPrefix = $"[env] {dotnetRootVariableName}="; string? dotnetRootOutput = derivedResult.StandardOutputLines .Select(line => line.TrimStart()) .FirstOrDefault(line => line.StartsWith(dotnetRootOutputPrefix, StringComparison.Ordinal)); Assert.IsNotNull(dotnetRootOutput, derivedResult.ToString()); // DOTNET_HOST_PATH can preserve different drive-letter casing than RootFinder on Windows. Assert.AreEqual( dotnetRoot, dotnetRootOutput[dotnetRootOutputPrefix.Length..], ignoreCase: OperatingSystem.IsWindows(), derivedResult.ToString()); DotnetMuxerResult explicitResult = await DotnetCli.RunAsync( $"build -t:Test -p:TestingPlatformCaptureOutput=False -p:ExplicitDotnetRoot=\"{dotnetRoot}\" \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, environmentVariables: new Dictionary { // An inherited architecture-specific root has higher apphost precedence than DOTNET_ROOT. Use an // existing directory without a dotnet installation to prove the explicit item clears this override. [dotnetRootVariableName] = Path.GetTempPath(), }, useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); explicitResult.AssertOutputContains($"[env] DOTNET_ROOT={dotnetRoot}"); explicitResult.AssertOutputContains($"[env] {dotnetRootVariableName}="); DotnetMuxerResult disabledResult = await DotnetCli.RunAsync( $"build -t:Test -p:TestingPlatformCaptureOutput=False -p:TestingPlatformDisableAppHostDotnetRoot=true \"{testAsset.TargetAssetPath}\"", workingDirectory: testAsset.TargetAssetPath, environmentVariables: CreateEnvironmentVariables(), useMultithreadedMSBuild: false, cancellationToken: TestContext.CancellationToken); disabledResult.AssertOutputContains($"[env] {dotnetRootVariableName}="); } private const string SourceCode = """ #file MSBuild Tests.csproj $TargetFrameworks$ $PlatformTarget$ enable enable Exe true preview false MSBuildTests #file UserDefinedTestTarget.targets #file global.json { "test": { "runner": "VSTest" } } #file Program.cs using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.Capabilities.TestFramework; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.TestFramework; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.MSBuild; using Microsoft.Testing.Platform.Services; using System.Runtime.InteropServices; public class Program { public static async Task Main(string[] args) { string dotnetRootArchVariableName = $"DOTNET_ROOT_{RuntimeInformation.ProcessArchitecture.ToString().ToUpperInvariant()}"; foreach (string name in new[] { "MTP_ENV_SIMPLE", "MTP_ENV_WITH_SEMICOLON", "MTP_ENV_INHERITED", "DOTNET_ROOT", dotnetRootArchVariableName }) { Console.WriteLine($"[env] {name}={Environment.GetEnvironmentVariable(name)}"); } ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); MyExtension myExtension = new(); builder.RegisterTestFramework( sp => new TestFrameworkCapabilities(), (_,sp) => new DummyTestFramework(sp, myExtension)); builder.AddTreeNodeFilterService(myExtension); builder.AddMSBuild(); using ITestApplication app = await builder.BuildAsync(); return await app.RunAsync(); } } public class MyExtension : IExtension { public string Uid => "MyExtension"; public string Version => "1.0.0"; public string DisplayName => "My Extension"; public string Description => "My Extension Description"; public Task IsEnabledAsync() => Task.FromResult(true); } public class DummyTestFramework : ITestFramework, IDataProducer { private IServiceProvider _sp; private MyExtension _myExtension; public DummyTestFramework(IServiceProvider sp, MyExtension myExtension) { _sp = sp; _myExtension = myExtension; } public string Uid => _myExtension.Uid; public string Version => _myExtension.Version; public string DisplayName => _myExtension.DisplayName; public string Description => _myExtension.Description; public Type[] DataTypesProduced => [typeof(TestNodeUpdateMessage)]; public Task IsEnabledAsync() => _myExtension.IsEnabledAsync(); public Task CreateTestSessionAsync(CreateTestSessionContext context) => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); public Task CloseTestSessionAsync(CloseTestSessionContext context) => Task.FromResult(new CloseTestSessionResult() { IsSuccess = true }); public async Task ExecuteRequestAsync(ExecuteRequestContext context) { await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode { Uid = "1", DisplayName = "Test1", Properties = new(DiscoveredTestNodeStateProperty.CachedInstance) })); await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode { Uid = "1", DisplayName = "Test1", Properties = new(PassedTestNodeStateProperty.CachedInstance) })); if (!_sp.GetCommandLineOptions().TryGetOptionArgumentList("--treenode-filter", out _)) { await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode { Uid = "2", DisplayName = "Test2", Properties = new(DiscoveredTestNodeStateProperty.CachedInstance) })); await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode { Uid = "2", DisplayName = "Test2", Properties = new($AssertValue$ ? PassedTestNodeStateProperty.CachedInstance : new FailedTestNodeStateProperty("FAILED: Expected 'true', but got 'false'.")) })); } context.Complete(); } } """; public TestContext TestContext { get; set; } }