From 7b871bd5f5642999762d3f65e2ab10adff0a5043 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Tue, 6 Oct 2026 18:13:57 -0400 Subject: [PATCH] [release/10.0] Enable class-level parallelism in Process tests Backport #135160's test-only ambient-state isolation and class-level parallelism on Windows/Linux. Preserve assembly-wide serialization on desktop macOS and the Browser assembly skip. Resolve context conflicts without importing newer tests or APIs. Omit the Windows first-chance-exception test change because that test does not exist on release/10.0. (cherry picked from commit 780c55a27764cb5729babfcfef8de849cdf3fcaf) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 77e3b81a-728c-456f-97c2-210c695c4446 --- .../tests/AssemblyInfo.cs | 6 +- .../tests/Interop.cs | 10 ++- .../tests/ProcessStandardConsoleTests.cs | 63 +++++++++--------- .../tests/ProcessStartInfoTests.cs | 65 +++++++++---------- .../tests/ProcessTests.Unix.cs | 26 +++----- .../System.Diagnostics.Process.Tests.csproj | 1 + 6 files changed, 88 insertions(+), 83 deletions(-) diff --git a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs index 80471364814581..f15a1cb605513a 100644 --- a/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs +++ b/src/libraries/System.Diagnostics.Process/tests/AssemblyInfo.cs @@ -3,8 +3,10 @@ using Xunit; -// Process tests can conflict with each other, as they modify ambient state -// like the console code page and environment variables +#if TARGET_OSX +// Retain serialization until process-management hangs under concurrent test classes on macOS are resolved. +// https://github.com/dotnet/runtime/issues/135294 [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] +#endif [assembly: SkipOnPlatform(TestPlatforms.Browser, "System.Diagnostics.Process is not supported on Browser.")] diff --git a/src/libraries/System.Diagnostics.Process/tests/Interop.cs b/src/libraries/System.Diagnostics.Process/tests/Interop.cs index 6bd2ddebbc8f6a..615f7a134ea2f3 100644 --- a/src/libraries/System.Diagnostics.Process/tests/Interop.cs +++ b/src/libraries/System.Diagnostics.Process/tests/Interop.cs @@ -95,12 +95,18 @@ public struct SID_AND_ATTRIBUTES [DllImport("kernel32.dll")] internal static extern int GetConsoleOutputCP(); - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] internal static extern int SetConsoleCP(int codePage); - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] internal static extern int SetConsoleOutputCP(int codePage); + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern int FreeConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern int AllocConsole(); + [DllImport("advapi32.dll")] internal static extern bool OpenProcessToken(SafeProcessHandle ProcessHandle, uint DesiredAccess, out SafeProcessHandle TokenHandle); diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs index b55384a1606527..7309740967d737 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStandardConsoleTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using System.Text; using Microsoft.DotNet.RemoteExecutor; using Microsoft.Win32; @@ -15,22 +16,6 @@ public void TestChangesInConsoleEncoding() { const int ConsoleEncoding = 437; - void RunWithExpectedCodePage(int expectedCodePage) - { - Process p = CreateProcessLong(); - p.StartInfo.RedirectStandardInput = true; - p.StartInfo.RedirectStandardOutput = true; - p.StartInfo.RedirectStandardError = true; - p.Start(); - - Assert.Equal(expectedCodePage, p.StandardInput.Encoding.CodePage); - Assert.Equal(expectedCodePage, p.StandardOutput.CurrentEncoding.CodePage); - Assert.Equal(expectedCodePage, p.StandardError.CurrentEncoding.CodePage); - - p.Kill(); - Assert.True(p.WaitForExit(WaitInMS)); - }; - // Don't test this on Windows containers, as there is a known issue. // See https://github.com/dotnet/runtime/issues/42000 for more details. if (!OperatingSystem.IsWindows() || PlatformDetection.IsInContainer) @@ -39,21 +24,41 @@ void RunWithExpectedCodePage(int expectedCodePage) return; } - int inputEncoding = Interop.GetConsoleCP(); - int outputEncoding = Interop.GetConsoleOutputCP(); - - try + RemoteExecutor.Invoke(static () => { - Interop.SetConsoleCP(ConsoleEncoding); - Interop.SetConsoleOutputCP(ConsoleEncoding); + // Remote processes inherit the runner's console; allocate a private one before changing its code pages. + if (Interop.FreeConsole() == 0) + { + throw new Win32Exception(); + } + if (Interop.AllocConsole() == 0) + { + throw new Win32Exception(); + } + if (Interop.SetConsoleCP(ConsoleEncoding) == 0 || Interop.SetConsoleOutputCP(ConsoleEncoding) == 0) + { + throw new Win32Exception(); + } - RunWithExpectedCodePage(ConsoleEncoding); - } - finally - { - Interop.SetConsoleCP(inputEncoding); - Interop.SetConsoleOutputCP(outputEncoding); - } + using var tests = new ProcessStandardConsoleTests(); + tests.RunWithExpectedCodePage(ConsoleEncoding); + }).Dispose(); + } + + private void RunWithExpectedCodePage(int expectedCodePage) + { + Process p = CreateProcessLong(); + p.StartInfo.RedirectStandardInput = true; + p.StartInfo.RedirectStandardOutput = true; + p.StartInfo.RedirectStandardError = true; + p.Start(); + + Assert.Equal(expectedCodePage, p.StandardInput.Encoding.CodePage); + Assert.Equal(expectedCodePage, p.StandardOutput.CurrentEncoding.CodePage); + Assert.Equal(expectedCodePage, p.StandardError.CurrentEncoding.CodePage); + + p.Kill(); + Assert.True(p.WaitForExit(WaitInMS)); } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs index 5a7cf2c5c01be6..033d711a3f309e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessStartInfoTests.cs @@ -213,35 +213,41 @@ public void TestEnvironmentProperty() [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void TestSetEnvironmentOnChildProcess() { - const string name = "b5a715d3-d74f-465d-abb7-2abe844750c9"; - Environment.SetEnvironmentVariable(name, "parent-process-value"); - - Process p = CreateProcess(() => + RemoteExecutor.Invoke(static () => { - if (Environment.GetEnvironmentVariable(name) != "child-process-value") - return 1; + using var tests = new ProcessStartInfoTests(); + const string name = "b5a715d3-d74f-465d-abb7-2abe844750c9"; + Environment.SetEnvironmentVariable(name, "parent-process-value"); - return RemoteExecutor.SuccessExitCode; - }); - p.StartInfo.Environment.Add(name, "child-process-value"); - p.Start(); + Process p = tests.CreateProcess(() => + { + if (Environment.GetEnvironmentVariable(name) != "child-process-value") + return 1; - Assert.True(p.WaitForExit(WaitInMS)); - Assert.Equal(RemoteExecutor.SuccessExitCode, p.ExitCode); + return RemoteExecutor.SuccessExitCode; + }); + p.StartInfo.Environment.Add(name, "child-process-value"); + p.Start(); + + Assert.True(p.WaitForExit(WaitInMS)); + Assert.Equal(RemoteExecutor.SuccessExitCode, p.ExitCode); + }).Dispose(); } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void TestEnvironmentOfChildProcess() { - const string ExtraEnvVar = "TestEnvironmentOfChildProcess_SpecialStuff"; - Environment.SetEnvironmentVariable(ExtraEnvVar, "\x1234" + Environment.NewLine + "\x5678"); // ensure some Unicode characters and newlines are in the output - const string EmptyEnvVar = "TestEnvironmentOfChildProcess_Empty"; - Environment.SetEnvironmentVariable(EmptyEnvVar, ""); - try + RemoteExecutor.Invoke(static () => { + using var tests = new ProcessStartInfoTests(); + const string ExtraEnvVar = "TestEnvironmentOfChildProcess_SpecialStuff"; + Environment.SetEnvironmentVariable(ExtraEnvVar, "\x1234" + Environment.NewLine + "\x5678"); // ensure some Unicode characters and newlines are in the output + const string EmptyEnvVar = "TestEnvironmentOfChildProcess_Empty"; + Environment.SetEnvironmentVariable(EmptyEnvVar, ""); + // Schedule a process to see what env vars it gets. Have it write out those variables // to its output stream so we can read them. - Process p = CreateProcess(() => + Process p = tests.CreateProcess(() => { Console.Write(string.Join(ItemSeparator, Environment.GetEnvironmentVariables().Cast().Select(e => Convert.ToBase64String(Encoding.UTF8.GetBytes(e.Key + "=" + e.Value))))); return RemoteExecutor.SuccessExitCode; @@ -272,22 +278,19 @@ public void TestEnvironmentOfChildProcess() string.Join(", ", envEnv.Except(actualEnv)), Environment.NewLine, string.Join(", ", actualEnv.Except(envEnv)))); - } - finally - { - Environment.SetEnvironmentVariable(ExtraEnvVar, null); - Environment.SetEnvironmentVariable(EmptyEnvVar, null); - } + }).Dispose(); } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void EnvironmentNullValue() { - const string NullEnvVar = "TestEnvironmentOfChildProcess_Null"; - Environment.SetEnvironmentVariable(NullEnvVar, ""); - try + RemoteExecutor.Invoke(static () => { - Process p = CreateProcess(() => + using var tests = new ProcessStartInfoTests(); + const string NullEnvVar = "TestEnvironmentOfChildProcess_Null"; + Environment.SetEnvironmentVariable(NullEnvVar, ""); + + Process p = tests.CreateProcess(() => { // Verify that setting the value to null in StartInfo is going to remove the process environment. Assert.Null(Environment.GetEnvironmentVariable(NullEnvVar)); @@ -297,11 +300,7 @@ public void EnvironmentNullValue() Assert.Null(p.StartInfo.Environment[NullEnvVar]); p.Start(); Assert.True(p.WaitForExit(WaitInMS)); - } - finally - { - Environment.SetEnvironmentVariable(NullEnvVar, null); - } + }).Dispose(); } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs index a02cc15c3a6626..271758ea70ef1e 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs @@ -78,35 +78,27 @@ public void ProcessStart_UseShellExecute_OnLinux_ThrowsIfNoProgramInstalled() } } - [Fact] + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [OuterLoop("Opens program")] [SkipOnPlatform(TestPlatforms.MacCatalyst, "In App Sandbox mode, the process doesn't have read access to the binary.")] [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.Android | TestPlatforms.Browser, "Not supported on iOS/tvOS/Android/Browser.")] public void ProcessStart_DirectoryNameInCurDirectorySameAsFileNameInExecDirectory_Success() { - string fileToOpen = "dotnet"; - string curDir = Environment.CurrentDirectory; - string dotnetFolder = Path.Combine(Path.GetTempPath(),"dotnet"); - bool shouldDelete = !Directory.Exists(dotnetFolder); - try + Directory.CreateDirectory(Path.Combine(TestDirectory, "dotnet")); + + RemoteExecutor.Invoke(StartDotnet, new RemoteInvokeOptions { - Directory.SetCurrentDirectory(Path.GetTempPath()); - Directory.CreateDirectory(dotnetFolder); + StartInfo = new ProcessStartInfo { WorkingDirectory = TestDirectory } + }).Dispose(); + static void StartDotnet() + { + string fileToOpen = "dotnet"; using (var px = Process.Start(fileToOpen)) { Assert.NotNull(px); } } - finally - { - if (shouldDelete) - { - Directory.Delete(dotnetFolder); - } - - Directory.SetCurrentDirectory(curDir); - } } [Fact] diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index 718bddd017ea89..fc695e1b066a28 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -10,6 +10,7 @@ $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) $(DefineConstants);TargetsWindows + $(DefineConstants);TARGET_OSX