From 3eec45480d0e84ba59be3bc555785be6fafb4d1d Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:45:52 +0200 Subject: [PATCH 1/2] Pre-build VSTest project and use --no-build for TelemetryTests The VSTest_RunTests_Succeeds and VSTest_DiscoverTests_Succeeds tests were still flaking on SDK 11 preview with: error MSB4018: The `GenerateRuntimeConfigurationFiles` task failed unexpectedly. System.IO.IOException: The process cannot access the file '...bin/Release//TelemetryVSTestProject.runtimeconfig.json' because it is being used by another process. Even though [DoNotParallelize] (PR #8292) serializes the test methods, `dotnet test` re-runs the build (and `GenerateRuntimeConfigurationFiles`) for each invocation, and the shared bin// outputs were racing with leftover handles (MSBuild server / lingering testhost) across serialized invocations. Build the VSTest project lazily once per TFM via a SemaphoreSlim+HashSet on the fixture, then run `dotnet test ... --no-build --no-restore` so the file-writing build targets do not run at test time at all. Keep [DoNotParallelize] as defense in depth. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TelemetryTests.cs | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs index 9d8fc98e34..0a887d963f 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs @@ -129,8 +129,15 @@ public async Task MTP_WhenTelemetryDisabled_DoesNotSendMSTestEvent(string tfm) [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] public async Task VSTest_RunTests_Succeeds(string tfm) { + // Pre-build the VSTest project for this TFM exactly once, then run `dotnet test` + // with `--no-build --no-restore` so the build/restore targets that write to + // `bin//TelemetryVSTestProject.runtimeconfig.json` do not run per-test. + // This removes the GenerateRuntimeConfigurationFiles race that was still flaking + // intermittently even with [DoNotParallelize] on the class. + await AssetFixture.EnsureVSTestProjectBuiltAsync(tfm, TestContext.CancellationToken); + DotnetMuxerResult testResult = await DotnetCli.RunAsync( - $"test -c Release {AssetFixture.VSTestProjectPath} --framework {tfm}", + $"test -c Release {AssetFixture.VSTestProjectPath} --framework {tfm} --no-build --no-restore", workingDirectory: AssetFixture.VSTestProjectPath, failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); @@ -147,8 +154,10 @@ public async Task VSTest_RunTests_Succeeds(string tfm) [DynamicData(nameof(TargetFrameworks.AllForDynamicData), typeof(TargetFrameworks))] public async Task VSTest_DiscoverTests_Succeeds(string tfm) { + await AssetFixture.EnsureVSTestProjectBuiltAsync(tfm, TestContext.CancellationToken); + DotnetMuxerResult testResult = await DotnetCli.RunAsync( - $"test -c Release {AssetFixture.VSTestProjectPath} --framework {tfm} --list-tests", + $"test -c Release {AssetFixture.VSTestProjectPath} --framework {tfm} --list-tests --no-build --no-restore", workingDirectory: AssetFixture.VSTestProjectPath, failIfReturnValueIsNotZero: false, cancellationToken: TestContext.CancellationToken); @@ -192,6 +201,9 @@ public sealed class TestAssetFixture() : TestAssetFixtureBase() { private const string AssetId = nameof(TelemetryTests); + private readonly SemaphoreSlim _vstestBuildLock = new(1, 1); + private readonly HashSet _builtVSTestFrameworks = new(StringComparer.OrdinalIgnoreCase); + public string MTPProjectPath => GetAssetPath(AssetId); public string VSTestProjectPath => Path.Combine(GetAssetPath(AssetId), "vstest"); @@ -203,6 +215,49 @@ public override (string ID, string Name, string Code) GetAssetsToGenerate() .PatchCodeWithReplace("$MSTestVersion$", MSTestVersion) .PatchCodeWithReplace("$MicrosoftNETTestSdkVersion$", MicrosoftNETTestSdkVersion)); + // Pre-builds the VSTest project for the given TFM exactly once per TFM. Callers can + // then invoke `dotnet test ... --no-build --no-restore` to avoid having every test + // method redo the build, which is what produces the + // `GenerateRuntimeConfigurationFiles` file-in-use races on shared build outputs. + public async Task EnsureVSTestProjectBuiltAsync(string targetFramework, CancellationToken cancellationToken) + { + await _vstestBuildLock.WaitAsync(cancellationToken); + try + { + if (_builtVSTestFrameworks.Contains(targetFramework)) + { + return; + } + + DotnetMuxerResult buildResult = await DotnetCli.RunAsync( + $"build -c Release {VSTestProjectPath} --framework {targetFramework}", + workingDirectory: VSTestProjectPath, + cancellationToken: cancellationToken); + + if (buildResult.ExitCode != 0) + { + throw new InvalidOperationException( + $"Failed to pre-build VSTest project for {targetFramework}:\n{buildResult.StandardOutput}\n{buildResult.StandardError}"); + } + + _builtVSTestFrameworks.Add(targetFramework); + } + finally + { + _vstestBuildLock.Release(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _vstestBuildLock.Dispose(); + } + + base.Dispose(disposing); + } + private const string SourceCode = """ #file TelemetryMTPProject.csproj From b8181b348f3d0ca50a52b8d483c084fc2c708fa2 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 11:13:09 +0200 Subject: [PATCH 2/2] Address review feedback: remove dead exit code check and fix IDE0028 - Remove unreachable ExitCode!=0 check (DotnetCli.RunAsync already throws on non-zero exit by default) - Use collection expression [with(comparer)] syntax for HashSet to fix IDE0028 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TelemetryTests.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs index 0a887d963f..dd4283bf69 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/TelemetryTests.cs @@ -202,7 +202,7 @@ public sealed class TestAssetFixture() : TestAssetFixtureBase() private const string AssetId = nameof(TelemetryTests); private readonly SemaphoreSlim _vstestBuildLock = new(1, 1); - private readonly HashSet _builtVSTestFrameworks = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _builtVSTestFrameworks = [with(StringComparer.OrdinalIgnoreCase)]; public string MTPProjectPath => GetAssetPath(AssetId); @@ -229,17 +229,11 @@ public async Task EnsureVSTestProjectBuiltAsync(string targetFramework, Cancella return; } - DotnetMuxerResult buildResult = await DotnetCli.RunAsync( + await DotnetCli.RunAsync( $"build -c Release {VSTestProjectPath} --framework {targetFramework}", workingDirectory: VSTestProjectPath, cancellationToken: cancellationToken); - if (buildResult.ExitCode != 0) - { - throw new InvalidOperationException( - $"Failed to pre-build VSTest project for {targetFramework}:\n{buildResult.StandardOutput}\n{buildResult.StandardError}"); - } - _builtVSTestFrameworks.Add(targetFramework); } finally