Skip to content

Commit 9cbffb0

Browse files
authored
Set dotnet_root_<arch> always (#15266)
* Set dotnet_root_<arch> always * null ref * Fixing test that was relying on fallback
1 parent 5a4d2cc commit 9cbffb0

File tree

3 files changed

+72
-70
lines changed

3 files changed

+72
-70
lines changed

src/Microsoft.TestPlatform.CoreUtilities/FeatureFlag/FeatureFlag.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ private FeatureFlag() { }
7272
// Disable not sharing .NET Framework testhosts. Which will return behavior to sharing testhosts when they are running .NET Framework dlls, and are not disabling appdomains or running in parallel.
7373
public const string VSTEST_DISABLE_SHARING_NETFRAMEWORK_TESTHOST = nameof(VSTEST_DISABLE_SHARING_NETFRAMEWORK_TESTHOST);
7474

75+
// Disable setting DOTNET_ROOT environment variable on non-Windows platforms. We used to set it only only on Windows when we found testhost.exe, now we set it always to allow xunit v3 to run tests in child process.
76+
public const string VSTEST_DISABLE_DOTNET_ROOT_ON_NONWINDOWS = nameof(VSTEST_DISABLE_DOTNET_ROOT_ON_NONWINDOWS);
77+
7578

7679
[Obsolete("Only use this in tests.")]
7780
internal static void Reset()

src/Microsoft.TestPlatform.TestHostProvider/Hosting/DotnetTestHostManager.cs

Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,29 @@ public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
231231
EqtTrace.Verbose($"DotnetTestHostmanager.GetTestHostProcessStartInfo: Platform environment '{_platformEnvironment.Architecture}' target architecture '{_architecture}' framework '{_targetFramework}' OS '{_platformEnvironment.OperatingSystem}'");
232232

233233
var startInfo = new TestProcessStartInfo();
234+
startInfo.EnvironmentVariables = environmentVariables ?? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
235+
236+
string? dotnetRootPath = _environmentVariableHelper.GetEnvironmentVariable("VSTEST_DOTNET_ROOT_PATH");
237+
string? dotnetRootArchitecture = _environmentVariableHelper.GetEnvironmentVariable("VSTEST_DOTNET_ROOT_ARCHITECTURE");
238+
239+
if (!StringUtilities.IsNullOrWhiteSpace(dotnetRootPath))
240+
{
241+
if (StringUtils.IsNullOrWhiteSpace(dotnetRootArchitecture))
242+
{
243+
throw new InvalidOperationException("'VSTEST_DOTNET_ROOT_PATH' and 'VSTEST_DOTNET_ROOT_ARCHITECTURE' must be both always set. If you are seeing this error, this is a bug in dotnet SDK that sets those variables.");
244+
}
245+
246+
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: VSTEST_DOTNET_ROOT_PATH={dotnetRootPath}");
247+
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: VSTEST_DOTNET_ROOT_ARCHITECTURE={dotnetRootArchitecture}");
248+
249+
if (!FeatureFlag.Instance.IsSet(FeatureFlag.VSTEST_DISABLE_DOTNET_ROOT_ON_NONWINDOWS))
250+
{
251+
// Set DOTNET_ROOT_<ARCH> for any run, so it gets propagated to testhost and its child processes, like dotnet run does it. This allows executables that start under testhost to find the path to dotnet
252+
// from which we called dotnet test. Before this change we only expected testhost.exe to be in this situation, but with xunit v3 running separate exe under testhost, the need for setting architecture
253+
// specific DOTNET_ROOT increases and makes this necessary for users to have good experience.
254+
SetDotnetRootForArchitecture(startInfo, dotnetRootPath!, dotnetRootArchitecture);
255+
}
256+
}
234257

235258
// .NET core host manager is not a shared host. It will expect a single test source to be provided.
236259
// TODO: Throw an exception when we get 0 or more than 1 source, that explains what happened, instead of .Single throwing a generic exception?
@@ -506,29 +529,14 @@ public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
506529
// G:\nuget-package-path\microsoft.testplatform.testhost\version\**\testhost.dll
507530
// G:\tmp\netcore-test\bin\Debug\netcoreapp1.0\netcore-test.dll
508531
startInfo.Arguments = args;
509-
startInfo.EnvironmentVariables = environmentVariables ?? new Dictionary<string, string?>();
510532

511533
// If we're running using custom apphost we need to set DOTNET_ROOT/DOTNET_ROOT(x86)
512534
// We're setting it inside SDK to support private install scenario.
513535
// i.e. I've got only private install and no global installation, in this case apphost needs to use env var to locate runtime.
514536
if (testHostExeFound)
515537
{
516-
// This change needs to happen first on vstest side, and then on dotnet/sdk, so prefer this approach and fallback to the old one.
517-
// VSTEST_DOTNET_ROOT v2
518-
string? dotnetRootPath = _environmentVariableHelper.GetEnvironmentVariable("VSTEST_DOTNET_ROOT_PATH");
519-
if (!StringUtils.IsNullOrWhiteSpace(dotnetRootPath))
538+
if (!StringUtilities.IsNullOrWhiteSpace(dotnetRootPath))
520539
{
521-
// This is v2 of the environment variables that we are passing, we are in new dotnet sdk. So also grab the architecture.
522-
string? dotnetRootArchitecture = _environmentVariableHelper.GetEnvironmentVariable("VSTEST_DOTNET_ROOT_ARCHITECTURE");
523-
524-
if (StringUtils.IsNullOrWhiteSpace(dotnetRootArchitecture))
525-
{
526-
throw new InvalidOperationException("'VSTEST_DOTNET_ROOT_PATH' and 'VSTEST_DOTNET_ROOT_ARCHITECTURE' must be both always set. If you are seeing this error, this is a bug in dotnet SDK that sets those variables.");
527-
}
528-
529-
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: VSTEST_DOTNET_ROOT_PATH={dotnetRootPath}");
530-
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: VSTEST_DOTNET_ROOT_ARCHITECTURE={dotnetRootArchitecture}");
531-
532540
// The parent process is passing to us the path in which the dotnet.exe is and is passing the architecture of the dotnet.exe,
533541
// so if the child process (testhost) is the same architecture it can pick up that dotnet.exe location and run. This is to allow
534542
// local installations of dotnet/sdk to work with testhost.
@@ -560,21 +568,11 @@ public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
560568
//
561569
// We ship just testhost.exe and testhost.x86.exe if the architecture is different we won't find the testhost*.exe and
562570
// won't reach this code, but let's write this in a generic way anyway, to avoid breaking if we add more variants of testhost*.exe.
563-
var environmentVariableName = $"DOTNET_ROOT_{_architecture.ToString().ToUpperInvariant()}";
564-
565-
var existingDotnetRoot = _environmentVariableHelper.GetEnvironmentVariable(environmentVariableName);
566-
if (!StringUtilities.IsNullOrWhiteSpace(existingDotnetRoot))
567-
{
568-
// The variable is already set in the surrounding environment, don't set it, because we want to keep what user provided.
569-
}
570-
else
571+
//
572+
// If the feature flag is set, revert to previous behavior of setting DOTNET_ROOT_<ARCH> only on Windows after we found testhost.exe.
573+
if (FeatureFlag.Instance.IsSet(FeatureFlag.VSTEST_DISABLE_DOTNET_ROOT_ON_NONWINDOWS))
571574
{
572-
var architectureFromEnv = (Architecture)Enum.Parse(typeof(Architecture), dotnetRootArchitecture, ignoreCase: true);
573-
if (architectureFromEnv == _architecture)
574-
{
575-
// Set the architecture specific variable to the environment of the process so it is picked up.
576-
startInfo.EnvironmentVariables.Add(environmentVariableName, dotnetRootPath);
577-
}
575+
SetDotnetRootForArchitecture(startInfo, dotnetRootPath!, dotnetRootArchitecture!);
578576
}
579577
}
580578
else
@@ -583,7 +581,7 @@ public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
583581
// to avoid setting DOTNET_ROOT that points to x64 but is picked up by x86 host.
584582
//
585583
// Also avoid setting it if we are already getting it from the surrounding environment.
586-
var architectureFromEnv = (Architecture)Enum.Parse(typeof(Architecture), dotnetRootArchitecture, ignoreCase: true);
584+
var architectureFromEnv = (Architecture)Enum.Parse(typeof(Architecture), dotnetRootArchitecture!, ignoreCase: true);
587585
if (architectureFromEnv == _architecture)
588586
{
589587
if (_architecture == Architecture.X86)
@@ -605,28 +603,6 @@ public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
605603
}
606604
}
607605
}
608-
else
609-
{
610-
// Fallback, can delete this once the change is in dotnet sdk. because they are always used together.
611-
string prefix = "VSTEST_WINAPPHOST_";
612-
string dotnetRootEnvName = $"{prefix}DOTNET_ROOT(x86)";
613-
var dotnetRoot = _environmentVariableHelper.GetEnvironmentVariable(dotnetRootEnvName);
614-
if (dotnetRoot is null)
615-
{
616-
dotnetRootEnvName = $"{prefix}DOTNET_ROOT";
617-
dotnetRoot = _environmentVariableHelper.GetEnvironmentVariable(dotnetRootEnvName);
618-
}
619-
620-
if (dotnetRoot != null)
621-
{
622-
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: Found '{dotnetRootEnvName}' in env variables, value '{dotnetRoot}', forwarding to '{dotnetRootEnvName.Replace(prefix, string.Empty)}'");
623-
startInfo.EnvironmentVariables.Add(dotnetRootEnvName.Replace(prefix, string.Empty), dotnetRoot);
624-
}
625-
else
626-
{
627-
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: Prefix '{prefix}*' not found in env variables");
628-
}
629-
}
630606
}
631607

632608
startInfo.WorkingDirectory = sourceDirectory;
@@ -732,6 +708,26 @@ bool IsNativeModule(string modulePath)
732708
}
733709
}
734710

711+
private void SetDotnetRootForArchitecture(TestProcessStartInfo startInfo, string dotnetRootPath, string dotnetRootArchitecture)
712+
{
713+
var environmentVariableName = $"DOTNET_ROOT_{dotnetRootArchitecture.ToUpperInvariant()}";
714+
715+
var existingDotnetRoot = _environmentVariableHelper.GetEnvironmentVariable(environmentVariableName);
716+
if (!StringUtilities.IsNullOrWhiteSpace(existingDotnetRoot))
717+
{
718+
EqtTrace.Verbose($"DotnetTestHostManager.SetDotnetRootForArchitecture: The variable {environmentVariableName} is already set in the surrounding environment, don't add it to testhost start info, because we want to keep what user provided externally.");
719+
}
720+
else
721+
{
722+
startInfo.EnvironmentVariables ??= new Dictionary<string, string?>();
723+
724+
// Set the architecture specific variable to the environment of the process so it is picked up.
725+
startInfo.EnvironmentVariables.Add(environmentVariableName, dotnetRootPath);
726+
727+
EqtTrace.Verbose($"DotnetTestHostManager.SetDotnetRootForArchitecture: Adding {environmentVariableName}={dotnetRootPath} to testhost start info.");
728+
}
729+
}
730+
735731
/// <inheritdoc/>
736732
public IEnumerable<string> GetTestPlatformExtensions(IEnumerable<string> sources, IEnumerable<string> extensions)
737733
{
@@ -837,7 +833,15 @@ private bool LaunchHost(TestProcessStartInfo testHostStartInfo, CancellationToke
837833
|| (_customTestHostLauncher.IsDebug
838834
&& _customTestHostLauncher is ITestHostLauncher2))
839835
{
840-
EqtTrace.Verbose("DotnetTestHostManager: Starting process '{0}' with command line '{1}'", testHostStartInfo.FileName, testHostStartInfo.Arguments);
836+
if (EqtTrace.IsVerboseEnabled)
837+
{
838+
var dotnetEnvVars = testHostStartInfo.EnvironmentVariables?
839+
.Where(kvp => kvp.Key.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase))
840+
.Select(kvp => $"{kvp.Key}={kvp.Value}")
841+
.ToArray() ?? Array.Empty<string>();
842+
843+
EqtTrace.Verbose($"DotnetTestHostManager: Starting process '{0}' with command line '{1}' and DOTNET environment: {string.Join(", ", dotnetEnvVars)} ", testHostStartInfo.FileName, testHostStartInfo.Arguments);
844+
}
841845

842846
cancellationToken.ThrowIfCancellationRequested();
843847

test/Microsoft.TestPlatform.TestHostProvider.UnitTests/Hosting/DotnetTestHostManagerTests.cs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -915,28 +915,23 @@ public void GetTestHostProcessStartInfoShouldSkipInvalidAdditionalProbingPaths()
915915
}
916916

917917
[TestMethod]
918-
[DataRow("DOTNET_ROOT(x86)", "x86")]
919-
[DataRow("DOTNET_ROOT", "x64")]
920-
[DataRow("DOTNET_ROOT_WRONG", "")]
921-
[TestCategory("Windows")]
922-
public void GetTestHostProcessStartInfoShouldForwardDOTNET_ROOTEnvVarsForAppHost(string envVar, string expectedValue)
918+
[DataRow("x64")]
919+
[DataRow("x86")]
920+
[DataRow("arm64")]
921+
public void GetTestHostProcessStartInfoShouldForwardDOTNET_ROOTEnvVarsForAppHost(string architecture)
923922
{
923+
var path = @"C:\dotnet";
924924
_mockFileHelper.Setup(ph => ph.Exists("testhost.exe")).Returns(true);
925925
_mockEnvironment.Setup(ev => ev.OperatingSystem).Returns(PlatformOperatingSystem.Windows);
926926
_mockEnvironmentVariable.Reset();
927-
_mockEnvironmentVariable.Setup(x => x.GetEnvironmentVariable($"VSTEST_WINAPPHOST_{envVar}")).Returns(expectedValue);
927+
_mockEnvironmentVariable.Setup(x => x.GetEnvironmentVariable($"VSTEST_DOTNET_ROOT_PATH")).Returns(path);
928+
_mockEnvironmentVariable.Setup(x => x.GetEnvironmentVariable($"VSTEST_DOTNET_ROOT_ARCHITECTURE")).Returns(architecture);
928929

929930
var startInfo = _dotnetHostManager.GetTestHostProcessStartInfo(_testSource, null, _defaultConnectionInfo);
930-
if (!string.IsNullOrEmpty(expectedValue))
931-
{
932-
Assert.AreEqual(1, startInfo.EnvironmentVariables!.Count);
933-
Assert.IsNotNull(startInfo.EnvironmentVariables[envVar]);
934-
Assert.AreEqual(startInfo.EnvironmentVariables[envVar], expectedValue);
935-
}
936-
else
937-
{
938-
Assert.AreEqual(0, startInfo.EnvironmentVariables!.Count);
939-
}
931+
932+
var envVar = $"DOTNET_ROOT_{architecture.ToUpperInvariant()}";
933+
Assert.IsNotNull(startInfo.EnvironmentVariables![envVar]);
934+
Assert.AreEqual(startInfo.EnvironmentVariables![envVar], path);
940935
}
941936

942937
[TestMethod]

0 commit comments

Comments
 (0)