Skip to content

Commit 4ac9f0d

Browse files
authored
[rel/17.14] Fix setting DOTNET_ROOT (#15252)
* Handle dotnet_root in testhost version aware way (#15184) * Handle dotnet_root in testhost version aware way * Fix unit tests * Fixes * Fixes * Fixes * Apply suggestion from @nohwnd * Apply suggestion from @nohwnd * Revert changes to playground * Only apply architecture when it is the same (#15250)
1 parent 490850a commit 4ac9f0d

File tree

6 files changed

+124
-19
lines changed

6 files changed

+124
-19
lines changed

docs/contribute.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ default, `Debug` configuration is run.
147147
If you want to run a particular test. Eg: Test Name that contains Blame in Acceptance test
148148

149149
```shell
150-
> test.cmd -p accept -f net451 -filter blame
150+
> .\test.cmd -bl -c release /p:TestRunnerAdditionalArguments="'--filter Blame'" -Integration
151151
```
152152

153153
## Deployment

playground/TestPlatform.Playground/TestPlatform.Playground.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@
5656

5757
<ItemGroup>
5858
<!-- .NET Framework console -->
59-
<FileToCopy Include="$(SourcePath)bin\vstest.console\$(Configuration)\$(NetFrameworkMinimum)\win7-x64\**\*.*" SubFolder="netfx" />
60-
<FileToCopy Include="$(SourcePath)bin\Microsoft.TestPlatform.TestHostProvider\$(Configuration)\$(NetFrameworkMinimum)\**\*.*" SubFolder="netfx\Extensions\" />
59+
<FileToCopy Include="$(SourcePath)bin\vstest.console\$(Configuration)\$(NetFrameworkRunnerTargetFramework)\win7-x64\**\*.*" SubFolder="netfx" />
60+
<FileToCopy Include="$(SourcePath)bin\Microsoft.TestPlatform.TestHostProvider\$(Configuration)\$(NetFrameworkRunnerTargetFramework)\**\*.*" SubFolder="netfx\Extensions\" />
6161

6262
<!-- copy net462, net47, net471, net472, net48 and net481 testhosts -->
6363
<FileToCopy Include="$(SourcePath)bin\testhost.x86\$(Configuration)\$(NetFrameworkMinimum)\win-x86\**\*.*" SubFolder="netfx\TestHostNetFramework\" />

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

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -513,23 +513,119 @@ public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
513513
// i.e. I've got only private install and no global installation, in this case apphost needs to use env var to locate runtime.
514514
if (testHostExeFound)
515515
{
516-
string prefix = "VSTEST_WINAPPHOST_";
517-
string dotnetRootEnvName = $"{prefix}DOTNET_ROOT(x86)";
518-
var dotnetRoot = _environmentVariableHelper.GetEnvironmentVariable(dotnetRootEnvName);
519-
if (dotnetRoot is null)
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))
520520
{
521-
dotnetRootEnvName = $"{prefix}DOTNET_ROOT";
522-
dotnetRoot = _environmentVariableHelper.GetEnvironmentVariable(dotnetRootEnvName);
523-
}
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");
524523

525-
if (dotnetRoot != null)
526-
{
527-
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: Found '{dotnetRootEnvName}' in env variables, value '{dotnetRoot}', forwarding to '{dotnetRootEnvName.Replace(prefix, string.Empty)}'");
528-
startInfo.EnvironmentVariables.Add(dotnetRootEnvName.Replace(prefix, string.Empty), dotnetRoot);
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+
532+
// The parent process is passing to us the path in which the dotnet.exe is and is passing the architecture of the dotnet.exe,
533+
// so if the child process (testhost) is the same architecture it can pick up that dotnet.exe location and run. This is to allow
534+
// local installations of dotnet/sdk to work with testhost.
535+
//
536+
// There are 2 complications in this process:
537+
// 1) There are differences between how .NET Apphosts are handling DOTNET_ROOT, versions pre-net6 are only looking at
538+
// DOTNET_ROOT(x86) and then DOTNET_ROOT. This makes is really easy to set DOTNET_ROOT to point at x64 dotnet installation
539+
// and have that picked up by x86 testhost and fail.
540+
// Unfortunately vstest.console has to support both new (17.14+) testhosts that are built against net8, and old (pre 17.14)
541+
// testhosts that are built using netcoreapp3.1 apphost, and so their approach to resolving DOTNET_ROOT differ.
542+
//
543+
// /!\ The apphost version does not align with the targeted framework (tfm), an older testhost is built against netcoreapp3.1
544+
// but can be used to run net8 tests. The only way to tell is the version of the testhost.
545+
//
546+
// netcoreapp3.1 hosts only support DOTNET_ROOT and DOTNET_ROOT(x86) env variables.
547+
// net8 hosts, support also DOTNET_ROOT_<ARCH> variables, which is what we should prefer to set the location of dotnet
548+
// in a more architecture specific way.
549+
//
550+
// 2) The surrounding environment might already have the environment variables set, most likely by setting DOTNET_ROOT, which is
551+
// a universal way of setting where the dotnet is, that works across all different architectures of the .NET apphost.
552+
// By setting our (hopefully more specific variable) we might overwrite what user specified, and in case of DOTNET_ROOT it is probably
553+
// preferable when we can set the DOTNET_ROOT_<ARCH> variable.
554+
var testhostDllPath = Path.ChangeExtension(startInfo.FileName, ".dll");
555+
// This file check is for unit tests, we expect the file to always be there. Otherwise testhost.exe would not be able to run.
556+
var testhostVersionInfo = _fileHelper.Exists(testhostDllPath) ? FileVersionInfo.GetVersionInfo(testhostDllPath) : null;
557+
if (testhostVersionInfo != null && testhostVersionInfo.ProductMajorPart >= 17 && testhostVersionInfo.ProductMinorPart >= 14)
558+
{
559+
// This is a new testhost that builds at least against net8 we should set the architecture specific DOTNET_ROOT_<ARCH>.
560+
//
561+
// We ship just testhost.exe and testhost.x86.exe if the architecture is different we won't find the testhost*.exe and
562+
// 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+
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+
}
578+
}
579+
}
580+
else
581+
{
582+
// This is an old testhost that built against netcoreapp3.1, it does not understand architecture specific DOTNET_ROOT_<ARCH>, we have to set it more carefully
583+
// to avoid setting DOTNET_ROOT that points to x64 but is picked up by x86 host.
584+
//
585+
// 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);
587+
if (architectureFromEnv == _architecture)
588+
{
589+
if (_architecture == Architecture.X86)
590+
{
591+
const string dotnetRootX86 = "DOTNET_ROOT(x86)";
592+
if (StringUtils.IsNullOrWhiteSpace(_environmentVariableHelper.GetEnvironmentVariable(dotnetRootX86)))
593+
{
594+
startInfo.EnvironmentVariables.Add(dotnetRootX86, dotnetRootPath);
595+
}
596+
}
597+
else
598+
{
599+
const string dotnetRoot = "DOTNET_ROOT";
600+
if (StringUtils.IsNullOrWhiteSpace(_environmentVariableHelper.GetEnvironmentVariable(dotnetRoot)))
601+
{
602+
startInfo.EnvironmentVariables.Add(dotnetRoot, dotnetRootPath);
603+
}
604+
}
605+
}
606+
}
529607
}
530608
else
531609
{
532-
EqtTrace.Verbose($"DotnetTestHostmanager.LaunchTestHostAsync: Prefix '{prefix}*' not found in env variables");
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+
}
533629
}
534630
}
535631

src/Microsoft.TestPlatform.VsTestConsole.TranslationLayer/VsTestConsoleRequestSender.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,7 @@ private void SendMessageAndListenAndReportTestResults(
11971197
EqtTrace.Error("Aborting Test Run Operation: {0}", exception);
11981198
eventHandler.HandleLogMessage(
11991199
TestMessageLevel.Error,
1200-
TranslationLayerResources.AbortedTestsRun);
1200+
TranslationLayerResources.AbortedTestsRun + " " + exception.ToString());
12011201
var completeArgs = new TestRunCompleteEventArgs(
12021202
null, false, true, exception, null, null, TimeSpan.Zero);
12031203
eventHandler.HandleTestRunComplete(completeArgs, null, null, null);
@@ -1282,7 +1282,7 @@ private async Task SendMessageAndListenAndReportTestResultsAsync(
12821282
EqtTrace.Error("Aborting Test Run Operation: {0}", exception);
12831283
eventHandler.HandleLogMessage(
12841284
TestMessageLevel.Error,
1285-
TranslationLayerResources.AbortedTestsRun);
1285+
TranslationLayerResources.AbortedTestsRun + " " + exception.ToString());
12861286
var completeArgs = new TestRunCompleteEventArgs(
12871287
null, false, true, exception, null, null, TimeSpan.Zero);
12881288
eventHandler.HandleTestRunComplete(completeArgs, null, null, null);

test/Microsoft.TestPlatform.Acceptance.IntegrationTests/Build.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,17 @@ private static void CopyAndPatchDotnet()
6262
// e.g. artifacts\tmp\.dotnet\sdk\
6363
var sdkDirectory = Path.Combine(patchedDotnetDir, "sdk");
6464
// e.g. artifacts\tmp\.dotnet\sdk\8.0.100-preview.6.23330.14
65-
var dotnetSdkDirectory = Directory.GetDirectories(sdkDirectory).Single();
65+
var dotnetSdkDirectories = Directory.GetDirectories(sdkDirectory);
66+
if (dotnetSdkDirectories.Length == 0)
67+
{
68+
throw new InvalidOperationException($"No .NET SDK directories found in '{sdkDirectory}'.");
69+
}
70+
if (dotnetSdkDirectories.Length > 1)
71+
{
72+
throw new InvalidOperationException($"More than 1 .NET SDK directories found in '{sdkDirectory}': {string.Join(", ", dotnetSdkDirectories)}.");
73+
}
74+
75+
var dotnetSdkDirectory = dotnetSdkDirectories.Single();
6676
DirectoryUtils.CopyDirectory(Path.Combine(packagePath, "lib", "netstandard2.0"), dotnetSdkDirectory);
6777
DirectoryUtils.CopyDirectory(Path.Combine(packagePath, "runtimes", "any", "native"), dotnetSdkDirectory);
6878
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ public DotnetTestHostManagerTests()
8686
_mockProcessHelper.Setup(ph => ph.GetCurrentProcessFileName()).Returns(DefaultDotnetPath);
8787
_mockProcessHelper.Setup(ph => ph.GetTestEngineDirectory()).Returns(DefaultDotnetPath);
8888
_mockProcessHelper.Setup(ph => ph.GetCurrentProcessArchitecture()).Returns(PlatformArchitecture.X64);
89-
_mockEnvironmentVariable.Setup(ev => ev.GetEnvironmentVariable(It.IsAny<string>())).Returns(Path.GetDirectoryName(DefaultDotnetPath)!);
9089
_mockFileHelper.Setup(ph => ph.Exists(_defaultTestHostPath)).Returns(true);
9190
_mockFileHelper.Setup(ph => ph.Exists(DefaultDotnetPath)).Returns(true);
9291

0 commit comments

Comments
 (0)