diff --git a/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs b/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs index cb40d81de8..9667792857 100644 --- a/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs +++ b/src/Microsoft.TestPlatform.PlatformAbstractions/common/System/ProcessHelper.cs @@ -245,6 +245,20 @@ internal static void WaitForErrorStreamToDrain(ManualResetEventSlim? errorStream /// public string? GetCurrentProcessFileName() { +#if NET + // Environment.ProcessPath is more reliable than MainModule under sandboxes such as + // proot, which is commonly used to run Linux distributions on Android (e.g. Termux). + // proot starts the real executable through an injected loader, and it cannot rewrite + // the contents of /proc/self/maps that MainModule is built from. When the loader is + // mapped below the executable (as it is on ARM64) MainModule reports the loader + // instead of the running dotnet host, which breaks test host resolution. + // See https://github.com/microsoft/vstest/issues/16446. + if (Environment.ProcessPath is { } processPath) + { + return processPath; + } +#endif + return _currentProcess.MainModule?.FileName; } diff --git a/test/vstest.console.UnitTests/ProcessHelperTests.cs b/test/vstest.console.UnitTests/ProcessHelperTests.cs index d9a00cff82..c1e2ea66f3 100644 --- a/test/vstest.console.UnitTests/ProcessHelperTests.cs +++ b/test/vstest.console.UnitTests/ProcessHelperTests.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +#if NET +using System; +#endif using System.Diagnostics; using System.Threading; @@ -11,17 +14,44 @@ namespace Microsoft.VisualStudio.TestPlatform.CommandLine.UnitTests; /// -/// Tests for , the bounded wait that lets the process +/// Tests for . +/// +/// +/// is the bounded wait that lets the process /// exit callback observe the complete standard error output of a crashed test host. Without it, the exit /// callback could read the asynchronously-collected stderr before all ErrorDataReceived callbacks had run, /// dropping a crash callstack such as "Stack overflow." (the cause of the flaky /// RunTestsShouldThrowOnStackOverflowException test). -/// +/// [TestClass] public class ProcessHelperTests { private const int BudgetMs = 500; + [TestMethod] + public void GetCurrentProcessFileNameShouldReturnThePathOfTheRunningExecutable() + { + var processHelper = new ProcessHelper(); + + string? currentProcessFileName = processHelper.GetCurrentProcessFileName(); + + using var currentProcess = Process.GetCurrentProcess(); + string? mainModuleFileName = currentProcess.MainModule?.FileName; + +#if NET + // On .NET we prefer Environment.ProcessPath, because MainModule can report an injected loader + // rather than the running executable under sandboxes such as proot. See issue #16446. + // ProcessPath is nullable, so mirror the fallback the production code performs instead of + // assuming it is always set. + string? expectedFileName = Environment.ProcessPath ?? mainModuleFileName; +#else + // .NET Framework has no Environment.ProcessPath, so the MainModule behavior must be preserved. + string? expectedFileName = mainModuleFileName; +#endif + + Assert.AreEqual(expectedFileName, currentProcessFileName); + } + [TestMethod] public void WaitForErrorStreamToDrainShouldReturnOnceTheErrorStreamCloses() {