From b4dc7f378fc07b3a0e7f96b9934d605961364acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 20:32:18 +0200 Subject: [PATCH 1/5] Add MSTestParallelizeScope/Workers MSBuild SDK properties (#4116) Allow MSTest.Sdk users to opt into assembly-level parallelization without authoring a MSTestSettings.cs file by setting two MSBuild properties: - MSTestParallelizeScope: None | ClassLevel | MethodLevel - MSTestParallelizeWorkers: non-negative integer These translate to [assembly: Parallelize(Scope = ..., Workers = ...)] or [assembly: DoNotParallelize] via the standard item (consumed by WriteCodeFragment when GenerateAssemblyInfo is true). Works with both VSTest and MTP runners. --- eng/verify-nupkgs.ps1 | 2 +- src/Package/MSTest.Sdk/MSTest.Sdk.nuspec | 1 + .../MSTest.Sdk/Sdk/Parallelize.targets | 52 ++++++ src/Package/MSTest.Sdk/Sdk/Sdk.targets | 3 + .../SdkTests.cs | 172 ++++++++++++++++++ 5 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 src/Package/MSTest.Sdk/Sdk/Parallelize.targets diff --git a/eng/verify-nupkgs.ps1 b/eng/verify-nupkgs.ps1 index be34a6851f..3cdeda14f0 100644 --- a/eng/verify-nupkgs.ps1 +++ b/eng/verify-nupkgs.ps1 @@ -19,7 +19,7 @@ function Unzip { function Confirm-NugetPackages { Write-Verbose "Starting Confirm-NugetPackages." $expectedNumOfFiles = @{ - "MSTest.Sdk" = 15 + "MSTest.Sdk" = 16 "MSTest.TestFramework" = 105 "MSTest.TestAdapter" = 49 "MSTest" = 10 diff --git a/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec b/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec index 1035b41731..a67531cd6e 100644 --- a/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec +++ b/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec @@ -9,6 +9,7 @@ + diff --git a/src/Package/MSTest.Sdk/Sdk/Parallelize.targets b/src/Package/MSTest.Sdk/Sdk/Parallelize.targets new file mode 100644 index 0000000000..11b59cee26 --- /dev/null +++ b/src/Package/MSTest.Sdk/Sdk/Parallelize.targets @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Workers = $(MSTestParallelizeWorkers) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + <_Parameter2>Workers = $(MSTestParallelizeWorkers) + <_Parameter2_IsLiteral>true + + + + + + + + + diff --git a/src/Package/MSTest.Sdk/Sdk/Sdk.targets b/src/Package/MSTest.Sdk/Sdk/Sdk.targets index c87ffc3194..9911ababbf 100644 --- a/src/Package/MSTest.Sdk/Sdk/Sdk.targets +++ b/src/Package/MSTest.Sdk/Sdk/Sdk.targets @@ -13,6 +13,9 @@ + + + diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index 934d962192..2893632ae7 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -362,4 +362,176 @@ public async Task SettingIsTestApplicationToFalseReducesAddedExtensionsAndMakesP // It's not an executable Assert.DoesNotContain(p => p.Value == "Exe", binLog.FindChildrenRecursive(p => p.Name == "OutputType")); } + + private const string ParallelizeAssertSourceCode = """ +#file MSTestSdk.csproj + + + + true + $TargetFramework$ + x64 + $(NoWarn);NU1507 + $ExtraProperties$ + + + + +#file UnitTest1.cs +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MSTestSdkTest +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} +"""; + + [TestMethod] + public async Task MSTestParallelizeScope_ClassLevel_EmitsParallelizeAttribute() + { + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + ParallelizeAssertSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", "ClassLevel")); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(0, 1, 0); + testHostResult.AssertOutputMatchesRegex(@"Test Parallelization enabled .* \(Workers: \d+, Scope: ClassLevel\)"); + } + + [TestMethod] + public async Task MSTestParallelizeScope_MethodLevelWithWorkers_EmitsParallelizeAttribute() + { + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + ParallelizeAssertSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", """ + MethodLevel + 3 + """)); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(0, 1, 0); + testHostResult.AssertOutputContains("Test Parallelization enabled"); + testHostResult.AssertOutputContains("(Workers: 3, Scope: MethodLevel)"); + } + + [TestMethod] + public async Task MSTestParallelizeWorkers_Only_EmitsParallelizeAttributeWithDefaultScope() + { + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + ParallelizeAssertSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", "2")); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(0, 1, 0); + + // ParallelizeAttribute defaults Scope to ClassLevel when only Workers is provided. + testHostResult.AssertOutputContains("Test Parallelization enabled"); + testHostResult.AssertOutputContains("(Workers: 2, Scope: ClassLevel)"); + } + + [TestMethod] + public async Task MSTestParallelizeScope_None_EmitsDoNotParallelizeAttribute() + { + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + ParallelizeAssertSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", "None")); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(0, 1, 0); + + // DoNotParallelize disables parallelization entirely. + testHostResult.AssertOutputDoesNotContain("Test Parallelization enabled"); + } + + [TestMethod] + public async Task MSTestParallelizeScope_InvalidValue_FailsBuild() + { + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + SingleTestSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", "NotAValidValue")); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync( + $"build {testAsset.TargetAssetPath}", + failIfReturnValueIsNotZero: false, + cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(1); + compilationResult.AssertOutputContains("Invalid value 'NotAValidValue' for property MSTestParallelizeScope. Valid values are 'None', 'ClassLevel' and 'MethodLevel'."); + } + + [TestMethod] + public async Task MSTestParallelizeWorkers_NonInteger_FailsBuild() + { + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + SingleTestSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", "abc")); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync( + $"build {testAsset.TargetAssetPath}", + failIfReturnValueIsNotZero: false, + cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(1); + compilationResult.AssertOutputContains("Invalid value 'abc' for property MSTestParallelizeWorkers. The value must be a non-negative integer."); + } + + [TestMethod] + public async Task MSTestParallelizeScope_None_With_Workers_FailsBuild() + { + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + SingleTestSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", """ + None + 2 + """)); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync( + $"build {testAsset.TargetAssetPath}", + failIfReturnValueIsNotZero: false, + cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(1); + compilationResult.AssertOutputContains("Property MSTestParallelizeWorkers cannot be set when MSTestParallelizeScope is 'None'."); + } } From 79355ca7bbde5d5cfbacc350d170f9899ad1bc1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 20:57:47 +0200 Subject: [PATCH 2/5] Move MSTestParallelize* properties to MSTest.TestAdapter targets So that the feature also works for projects that reference MSTest / MSTest.TestAdapter directly without using MSTest.Sdk. Adds an additional acceptance test covering the non-SDK scenario. --- eng/verify-nupkgs.ps1 | 2 +- .../common/MSTest.TestAdapter.targets | 48 +++++++++++++++ .../uwp/MSTest.TestAdapter.targets | 48 +++++++++++++++ src/Package/MSTest.Sdk/MSTest.Sdk.nuspec | 2 +- .../MSTest.Sdk/Sdk/Parallelize.targets | 52 ---------------- src/Package/MSTest.Sdk/Sdk/Sdk.targets | 3 - .../SdkTests.cs | 59 +++++++++++++++++++ 7 files changed, 157 insertions(+), 57 deletions(-) delete mode 100644 src/Package/MSTest.Sdk/Sdk/Parallelize.targets diff --git a/eng/verify-nupkgs.ps1 b/eng/verify-nupkgs.ps1 index 3cdeda14f0..be34a6851f 100644 --- a/eng/verify-nupkgs.ps1 +++ b/eng/verify-nupkgs.ps1 @@ -19,7 +19,7 @@ function Unzip { function Confirm-NugetPackages { Write-Verbose "Starting Confirm-NugetPackages." $expectedNumOfFiles = @{ - "MSTest.Sdk" = 16 + "MSTest.Sdk" = 15 "MSTest.TestFramework" = 105 "MSTest.TestAdapter" = 49 "MSTest" = 10 diff --git a/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets b/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets index d68f1f6640..059b924116 100644 --- a/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets +++ b/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets @@ -74,4 +74,52 @@ + + + + + + + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Workers = $(MSTestParallelizeWorkers) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + <_Parameter2>Workers = $(MSTestParallelizeWorkers) + <_Parameter2_IsLiteral>true + + + + + + + diff --git a/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets b/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets index be91415206..fb636b5fe2 100644 --- a/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets +++ b/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets @@ -51,4 +51,52 @@ + + + + + + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Workers = $(MSTestParallelizeWorkers) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + <_Parameter2>Workers = $(MSTestParallelizeWorkers) + <_Parameter2_IsLiteral>true + + + + + + + + diff --git a/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec b/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec index a67531cd6e..dd3cb50a93 100644 --- a/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec +++ b/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec @@ -9,7 +9,7 @@ - + diff --git a/src/Package/MSTest.Sdk/Sdk/Parallelize.targets b/src/Package/MSTest.Sdk/Sdk/Parallelize.targets deleted file mode 100644 index 11b59cee26..0000000000 --- a/src/Package/MSTest.Sdk/Sdk/Parallelize.targets +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) - <_Parameter1_IsLiteral>true - - - - - - - <_Parameter1>Workers = $(MSTestParallelizeWorkers) - <_Parameter1_IsLiteral>true - - - - - - - <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) - <_Parameter1_IsLiteral>true - <_Parameter2>Workers = $(MSTestParallelizeWorkers) - <_Parameter2_IsLiteral>true - - - - - - - - - diff --git a/src/Package/MSTest.Sdk/Sdk/Sdk.targets b/src/Package/MSTest.Sdk/Sdk/Sdk.targets index 9911ababbf..c87ffc3194 100644 --- a/src/Package/MSTest.Sdk/Sdk/Sdk.targets +++ b/src/Package/MSTest.Sdk/Sdk/Sdk.targets @@ -13,9 +13,6 @@ - - - diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index 2893632ae7..8bf94efa97 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -534,4 +534,63 @@ public async Task MSTestParallelizeScope_None_With_Workers_FailsBuild() compilationResult.AssertExitCodeIs(1); compilationResult.AssertOutputContains("Property MSTestParallelizeWorkers cannot be set when MSTestParallelizeScope is 'None'."); } + + // Verifies that the MSTestParallelizeScope/MSTestParallelizeWorkers properties also work for + // projects that don't use MSTest.Sdk (i.e. they reference MSTest.TestAdapter directly via the + // MSTest meta-package), since the targets logic ships in MSTest.TestAdapter. + [TestMethod] + public async Task MSTestParallelizeScope_NonSdkProject_EmitsParallelizeAttribute() + { + const string NonSdkSource = """ +#file MSTestPlain.csproj + + + $TargetFramework$ + Exe + true + x64 + $(NoWarn);NU1507 + MethodLevel + 3 + + + + + + + +#file UnitTest1.cs +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MSTestPlainTest +{ + [TestClass] + public class UnitTest1 + { + [TestMethod] + public void TestMethod1() + { + } + } +} +"""; + + const string PlainAssetName = "MSTestPlain"; + + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + PlainAssetName, + NonSdkSource + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$MicrosoftTestingPlatformVersion$", MicrosoftTestingPlatformVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent)); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, PlainAssetName, TargetFrameworks.NetCurrent, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(0, 1, 0); + testHostResult.AssertOutputContains("Test Parallelization enabled"); + testHostResult.AssertOutputContains("(Workers: 3, Scope: MethodLevel)"); + } } From 6e7eb8fd884177a57bdd99bcee3bbf57d86f6340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 21:03:28 +0200 Subject: [PATCH 3/5] Address PR review: rename ParallelizeAssertSourceCode to ParallelizeAssetSourceCode --- .../MSTest.Acceptance.IntegrationTests/SdkTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index 8bf94efa97..e1b22d385d 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.Platform.Acceptance.IntegrationTests; @@ -363,7 +363,7 @@ public async Task SettingIsTestApplicationToFalseReducesAddedExtensionsAndMakesP Assert.DoesNotContain(p => p.Value == "Exe", binLog.FindChildrenRecursive(p => p.Name == "OutputType")); } - private const string ParallelizeAssertSourceCode = """ + private const string ParallelizeAssetSourceCode = """ #file MSTestSdk.csproj @@ -398,7 +398,7 @@ public async Task MSTestParallelizeScope_ClassLevel_EmitsParallelizeAttribute() { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssertSourceCode + ParallelizeAssetSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", "ClassLevel")); @@ -417,7 +417,7 @@ public async Task MSTestParallelizeScope_MethodLevelWithWorkers_EmitsParallelize { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssertSourceCode + ParallelizeAssetSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", """ @@ -440,7 +440,7 @@ public async Task MSTestParallelizeWorkers_Only_EmitsParallelizeAttributeWithDef { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssertSourceCode + ParallelizeAssetSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", "2")); @@ -462,7 +462,7 @@ public async Task MSTestParallelizeScope_None_EmitsDoNotParallelizeAttribute() { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssertSourceCode + ParallelizeAssetSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", "None")); From 6eacaa6af954ae89e6fce5c90bb2beca90c5d336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 21:30:40 +0200 Subject: [PATCH 4/5] Address PR review: dedupe Parallelize targets, add tests, revert unrelated edits - Extract Parallelize targets into a shared buildTransitive\Parallelize.targets imported by both common and uwp variants of MSTest.TestAdapter.targets to remove duplication. - Restore the UTF-8 BOM accidentally stripped from SdkTests.cs and revert the unrelated blank line in MSTest.Sdk.nuspec. - Reuse the existing SingleTestSourceCode constant for the happy-path tests instead of introducing a near-identical duplicate. - Add three new acceptance tests: Workers=0 boundary, GenerateAssemblyInfo=false warning path, VSTest runner build-only check. --- eng/verify-nupkgs.ps1 | 2 +- .../MSTest.TestAdapter.NonWindows.nuspec | 2 + .../MSTest.TestAdapter.csproj | 3 + .../MSTest.TestAdapter.nuspec | 4 + .../buildTransitive/Parallelize.targets | 52 +++++++++ .../common/MSTest.TestAdapter.targets | 48 +------- .../uwp/MSTest.TestAdapter.targets | 48 +------- src/Package/MSTest.Sdk/MSTest.Sdk.nuspec | 1 - .../SdkTests.cs | 107 ++++++++++++------ 9 files changed, 136 insertions(+), 131 deletions(-) create mode 100644 src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets diff --git a/eng/verify-nupkgs.ps1 b/eng/verify-nupkgs.ps1 index be34a6851f..96f5e560f3 100644 --- a/eng/verify-nupkgs.ps1 +++ b/eng/verify-nupkgs.ps1 @@ -21,7 +21,7 @@ function Confirm-NugetPackages { $expectedNumOfFiles = @{ "MSTest.Sdk" = 15 "MSTest.TestFramework" = 105 - "MSTest.TestAdapter" = 49 + "MSTest.TestAdapter" = 53 "MSTest" = 10 "MSTest.Analyzers" = 56 } diff --git a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.NonWindows.nuspec b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.NonWindows.nuspec index 56137e1908..b894e0ad5f 100644 --- a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.NonWindows.nuspec +++ b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.NonWindows.nuspec @@ -20,6 +20,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj index 5d44f04283..9efeb89de2 100644 --- a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj +++ b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.csproj @@ -84,6 +84,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.nuspec b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.nuspec index 2d46894d6f..a62f94b4f2 100644 --- a/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.nuspec +++ b/src/Adapter/MSTest.TestAdapter/MSTest.TestAdapter.nuspec @@ -32,6 +32,7 @@ + @@ -40,6 +41,7 @@ + @@ -50,6 +52,7 @@ + @@ -60,6 +63,7 @@ + diff --git a/src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets b/src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets new file mode 100644 index 0000000000..531b0d00cc --- /dev/null +++ b/src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Workers = $(MSTestParallelizeWorkers) + <_Parameter1_IsLiteral>true + + + + + + + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) + <_Parameter1_IsLiteral>true + <_Parameter2>Workers = $(MSTestParallelizeWorkers) + <_Parameter2_IsLiteral>true + + + + + + + + + diff --git a/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets b/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets index 059b924116..4b5401a6a2 100644 --- a/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets +++ b/src/Adapter/MSTest.TestAdapter/buildTransitive/common/MSTest.TestAdapter.targets @@ -75,51 +75,5 @@ - - - - - - - - - - - - <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) - <_Parameter1_IsLiteral>true - - - - - - - <_Parameter1>Workers = $(MSTestParallelizeWorkers) - <_Parameter1_IsLiteral>true - - - - - - - <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) - <_Parameter1_IsLiteral>true - <_Parameter2>Workers = $(MSTestParallelizeWorkers) - <_Parameter2_IsLiteral>true - - - - - - - + diff --git a/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets b/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets index fb636b5fe2..a985acd04c 100644 --- a/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets +++ b/src/Adapter/MSTest.TestAdapter/buildTransitive/uwp/MSTest.TestAdapter.targets @@ -51,52 +51,6 @@ - - - - - - - - - - - - <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) - <_Parameter1_IsLiteral>true - - - - - - - <_Parameter1>Workers = $(MSTestParallelizeWorkers) - <_Parameter1_IsLiteral>true - - - - - - - <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) - <_Parameter1_IsLiteral>true - <_Parameter2>Workers = $(MSTestParallelizeWorkers) - <_Parameter2_IsLiteral>true - - - - - - - + diff --git a/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec b/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec index dd3cb50a93..1035b41731 100644 --- a/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec +++ b/src/Package/MSTest.Sdk/MSTest.Sdk.nuspec @@ -9,7 +9,6 @@ - diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index e1b22d385d..c57e17fa45 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.Platform.Acceptance.IntegrationTests; @@ -363,42 +363,12 @@ public async Task SettingIsTestApplicationToFalseReducesAddedExtensionsAndMakesP Assert.DoesNotContain(p => p.Value == "Exe", binLog.FindChildrenRecursive(p => p.Name == "OutputType")); } - private const string ParallelizeAssetSourceCode = """ -#file MSTestSdk.csproj - - - - true - $TargetFramework$ - x64 - $(NoWarn);NU1507 - $ExtraProperties$ - - - - -#file UnitTest1.cs -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MSTestSdkTest -{ - [TestClass] - public class UnitTest1 - { - [TestMethod] - public void TestMethod1() - { - } - } -} -"""; - [TestMethod] public async Task MSTestParallelizeScope_ClassLevel_EmitsParallelizeAttribute() { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssetSourceCode + SingleTestSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", "ClassLevel")); @@ -417,7 +387,7 @@ public async Task MSTestParallelizeScope_MethodLevelWithWorkers_EmitsParallelize { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssetSourceCode + SingleTestSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", """ @@ -440,7 +410,7 @@ public async Task MSTestParallelizeWorkers_Only_EmitsParallelizeAttributeWithDef { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssetSourceCode + SingleTestSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", "2")); @@ -457,12 +427,37 @@ public async Task MSTestParallelizeWorkers_Only_EmitsParallelizeAttributeWithDef testHostResult.AssertOutputContains("(Workers: 2, Scope: ClassLevel)"); } + [TestMethod] + public async Task MSTestParallelizeWorkers_Zero_IsAcceptedAndEmitsParallelizeAttribute() + { + // Workers=0 is a boundary: the regex ^\d+$ accepts it and ParallelizeAttribute + // interprets it as "use the number of available processors". Make sure the build + // succeeds and the runtime picks it up. + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + SingleTestSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", "0")); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + + var testHost = TestHost.LocateFrom(testAsset.TargetAssetPath, AssetName, TargetFrameworks.NetCurrent, buildConfiguration: BuildConfiguration.Release); + TestHostResult testHostResult = await testHost.ExecuteAsync(cancellationToken: TestContext.CancellationToken); + testHostResult.AssertOutputContainsSummary(0, 1, 0); + testHostResult.AssertOutputContains("Test Parallelization enabled"); + + // 0 workers means "use Environment.ProcessorCount" so the actual reported value is the host CPU count, not 0. + testHostResult.AssertOutputMatchesRegex(@"\(Workers: \d+, Scope: ClassLevel\)"); + } + [TestMethod] public async Task MSTestParallelizeScope_None_EmitsDoNotParallelizeAttribute() { using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, - ParallelizeAssetSourceCode + SingleTestSourceCode .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) .PatchCodeWithReplace("$ExtraProperties$", "None")); @@ -478,6 +473,27 @@ public async Task MSTestParallelizeScope_None_EmitsDoNotParallelizeAttribute() testHostResult.AssertOutputDoesNotContain("Test Parallelization enabled"); } + [TestMethod] + public async Task MSTestParallelizeScope_GenerateAssemblyInfoFalse_EmitsWarning() + { + // Without GenerateAssemblyInfo, the AssemblyAttribute items are not emitted, so the + // properties would silently do nothing. Verify that the build succeeds but a warning + // is reported to make this case discoverable. + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + SingleTestSourceCode + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", """ + false + ClassLevel + """)); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", warnAsError: false, cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + compilationResult.AssertOutputContains("MSTestParallelizeScope and MSTestParallelizeWorkers require GenerateAssemblyInfo to be true to take effect."); + } + [TestMethod] public async Task MSTestParallelizeScope_InvalidValue_FailsBuild() { @@ -535,6 +551,27 @@ public async Task MSTestParallelizeScope_None_With_Workers_FailsBuild() compilationResult.AssertOutputContains("Property MSTestParallelizeWorkers cannot be set when MSTestParallelizeScope is 'None'."); } + [TestMethod] + public async Task MSTestParallelizeScope_VSTestRunner_BuildSucceeds() + { + // The targets emit assembly attributes via WriteCodeFragment, which is runner-agnostic. + // Make sure the targets don't interfere with VSTest projects (build only — observing the + // "Test Parallelization enabled" diagnostic is MTP-specific). + using TestAsset testAsset = await TestAsset.GenerateAssetAsync( + AssetName, + SingleTestSourceCodeVSTest + .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) + .PatchCodeWithReplace("$TargetFramework$", TargetFrameworks.NetCurrent) + .PatchCodeWithReplace("$ExtraProperties$", """ + true + MethodLevel + 2 + """)); + + DotnetMuxerResult compilationResult = await DotnetCli.RunAsync($"build -c {BuildConfiguration.Release} {testAsset.TargetAssetPath}", cancellationToken: TestContext.CancellationToken); + compilationResult.AssertExitCodeIs(0); + } + // Verifies that the MSTestParallelizeScope/MSTestParallelizeWorkers properties also work for // projects that don't use MSTest.Sdk (i.e. they reference MSTest.TestAdapter directly via the // MSTest meta-package), since the targets logic ships in MSTest.TestAdapter. From cc3403b152c8e119822efdda031e1064d0207c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 15 May 2026 11:33:43 +0200 Subject: [PATCH 5/5] Address PR review: drop redundant Parallelize validation checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove invalid-Scope and non-integer-Workers MSBuild validation. Relax the ItemGroup conditions so any non-empty, non-None Scope value flows through to the generated assembly attribute and is rejected by the C# compiler (CS0117 / CS0103) — producing a clear build error without duplicate validation logic. - Keep the two validations that catch silent-failure modes (Scope=None+Workers, GenerateAssemblyInfo=false). - Update the two failure-path acceptance tests to assert the C# compile error instead of the MSBuild error message. --- .../buildTransitive/Parallelize.targets | 12 ++++++------ .../MSTest.Acceptance.IntegrationTests/SdkTests.cs | 8 ++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets b/src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets index 531b0d00cc..c15fb82f51 100644 --- a/src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets +++ b/src/Adapter/MSTest.TestAdapter/buildTransitive/Parallelize.targets @@ -7,19 +7,19 @@ [assembly: Parallelize(...)] / [assembly: DoNotParallelize] attribute via the standard AssemblyAttribute MSBuild item (consumed by WriteCodeFragment when GenerateAssemblyInfo is true). --> + - - - + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) <_Parameter1_IsLiteral>true @@ -35,7 +35,7 @@ - + <_Parameter1>Scope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope.$(MSTestParallelizeScope) <_Parameter1_IsLiteral>true diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs index c57e17fa45..e85040e40e 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SdkTests.cs @@ -497,6 +497,7 @@ public async Task MSTestParallelizeScope_GenerateAssemblyInfoFalse_EmitsWarning( [TestMethod] public async Task MSTestParallelizeScope_InvalidValue_FailsBuild() { + // An invalid scope value is emitted into the generated assembly attribute and rejected by the C# compiler (CS0117). using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SingleTestSourceCode @@ -509,12 +510,14 @@ public async Task MSTestParallelizeScope_InvalidValue_FailsBuild() failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); compilationResult.AssertExitCodeIs(1); - compilationResult.AssertOutputContains("Invalid value 'NotAValidValue' for property MSTestParallelizeScope. Valid values are 'None', 'ClassLevel' and 'MethodLevel'."); + compilationResult.AssertOutputContains("CS0117"); + compilationResult.AssertOutputContains("NotAValidValue"); } [TestMethod] public async Task MSTestParallelizeWorkers_NonInteger_FailsBuild() { + // A non-integer Workers value is emitted into the generated assembly attribute and rejected by the C# compiler (CS0103). using TestAsset testAsset = await TestAsset.GenerateAssetAsync( AssetName, SingleTestSourceCode @@ -527,7 +530,8 @@ public async Task MSTestParallelizeWorkers_NonInteger_FailsBuild() failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); compilationResult.AssertExitCodeIs(1); - compilationResult.AssertOutputContains("Invalid value 'abc' for property MSTestParallelizeWorkers. The value must be a non-negative integer."); + compilationResult.AssertOutputContains("CS0103"); + compilationResult.AssertOutputContains("abc"); } [TestMethod]