Bug Report
Using dotnet test --tl:off, environment variables are occasionally not passed correctly. Behaviour is based on the shell (i.e. whether a special =::=::\ env var is present, not everywhere). It does not occur in windows terminal i.e. to reproduce, open console Windows Key + R, cmd.exe, then run dotnet test --tl:off.
Affected Area
In-process vstest path on Windows with multi-target test runs, where at least one of the targets is .NET framework.
Expected Behavior
If environment inheritance is enabled, the child testhost should inherit the same native environment block as the parent process.
Actual Behavior
The in-process path snapshots the current environment through Environment.GetEnvironmentVariables(), clears the child environment, and rebuilds it from that managed snapshot.
On Windows, the managed wrapper can omit entries that are present in the native environment block. Those entries are lost in the child process.
This issue depends on differences between the native environment block and Environment.GetEnvironmentVariables() and seems to be Windows-specific (i.e. I can only reproduce it with a .NET Framework target).
Minimal Repro
EnvironmentVariableTests.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;net48;net9.0</TargetFrameworks>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="MSTest.TestAdapter" Version="4.2.2" />
<PackageReference Include="MSTest.TestFramework" Version="4.2.2" />
</ItemGroup>
</Project>
EnvironmentInheritanceTests.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace EnvironmentVariableTests;
[TestClass]
public class EnvironmentInheritanceTests
{
[TestMethod]
public void ManagedEnvironmentWrapperMatchesNativeEnvironmentBlock()
{
IDictionary managed = Environment.GetEnvironmentVariables();
List<string> rawEntries = GetRawEnvironmentEntries();
Assert.AreEqual(
rawEntries.Count,
managed.Count,
$"Managed wrapper returned {managed.Count} entries, raw Win32 block returned {rawEntries.Count}. " +
$"Raw-only entries: [{string.Join(", ", GetRawOnlyEntries(managed, rawEntries))}]");
}
private static List<string> GetRawEnvironmentEntries()
{
var entries = new List<string>();
var block = Native.GetEnvironmentStringsW();
if (block == IntPtr.Zero)
{
throw new InvalidOperationException("GetEnvironmentStringsW failed.");
}
try
{
var current = block;
while (true)
{
var value = Marshal.PtrToStringUni(current);
if (string.IsNullOrEmpty(value))
{
break;
}
entries.Add(value);
current += (value.Length + 1) * 2;
}
return entries;
}
finally
{
Native.FreeEnvironmentStringsW(block);
}
}
private static IEnumerable<string> GetRawOnlyEntries(IDictionary managed, IEnumerable<string> rawEntries)
{
var keys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DictionaryEntry entry in managed)
{
keys.Add(entry.Key.ToString()!);
}
foreach (var entry in rawEntries)
{
var separator = entry.IndexOf('=');
var key = separator >= 0 ? entry.Substring(0, separator) : entry;
if (!keys.Contains(key))
{
yield return entry;
}
}
}
private static class Native
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr GetEnvironmentStringsW();
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool FreeEnvironmentStringsW(IntPtr lpszEnvironmentBlock);
}
}
Output of dotnet test --tl:off:
Determining projects to restore...
All projects are up-to-date for restore.
EnvironmentTests ->...\EnvironmentTests\bin\Debug\net472\EnvironmentTests.dll
EnvironmentTests -> ...\EnvironmentTests\bin\Debug\net48\EnvironmentTests.dll
EnvironmentTests -> ...\EnvironmentTests\bin\Debug\net9.0\EnvironmentTests.dll
Test run for ...\EnvironmentTests\bin\Debug\net9.0\EnvironmentTests.dll (.NETCoreApp,Version=v9.0)
Test run for ...\bin\Debug\net472\EnvironmentTests.dll (.NETFramework,Version=v4.7.2)
Test run for ...\bin\Debug\net48\EnvironmentTests.dll (.NETFramework,Version=v4.8)
VSTest version 17.14.1 (x64)
VSTest version 17.14.1 (x64)
VSTest version 17.14.1 (x64)
Starting test execution, please wait...
Starting test execution, please wait...
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
A total of 1 test files matched the specified pattern.
A total of 1 test files matched the specified pattern.
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 19 ms - EnvironmentTests.dll (net9.0)
Failed ManagedEnvironmentWrapperMatchesNativeEnvironmentBlock [121 ms]
Failed Error Message:
ManagedEnvironmentWrapperMatchesNativeEnvironmentBlock [124 ms]
Error Message:
Assert.AreEqual failed. Expected:<73>. Actual:<72>. Managed wrapper returned 72 entries, raw Win32 block returned 73. Raw-only entries: [=::=::\]
Assert.AreEqual failed. Expected:<73>. Actual:<72>. Managed wrapper returned 72 entries, raw Win32 block returned 73. Raw-only entries: [=::=::\]
Stack Trace:
Stack Trace:
at EnvironmentVariableTests.EnvironmentInheritanceTests.ManagedEnvironmentWrapperMatchesNativeEnvironmentBlock() in ...\EnvironmentTests\EnvironmentVariableTests.cs:line 18
at EnvironmentVariableTests.EnvironmentInheritanceTests.ManagedEnvironmentWrapperMatchesNativeEnvironmentBlock() in ...\EnvironmentTests\EnvironmentVariableTests.cs:line 18
Failed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1, Duration: 311 msFailed! - Failed: 1, Passed: 0, Skipped: 0, Total: 1, Duration: 314 ms - EnvironmentTests.dll (net472)
- EnvironmentTests.dll (net48)
Bug Report
Using
dotnet test --tl:off, environment variables are occasionally not passed correctly. Behaviour is based on the shell (i.e. whether a special=::=::\env var is present, not everywhere). It does not occur in windows terminal i.e. to reproduce, open console Windows Key + R, cmd.exe, then rundotnet test --tl:off.Affected Area
In-process
vstestpath on Windows with multi-target test runs, where at least one of the targets is .NET framework.Expected Behavior
If environment inheritance is enabled, the child testhost should inherit the same native environment block as the parent process.
Actual Behavior
The in-process path snapshots the current environment through
Environment.GetEnvironmentVariables(), clears the child environment, and rebuilds it from that managed snapshot.On Windows, the managed wrapper can omit entries that are present in the native environment block. Those entries are lost in the child process.
This issue depends on differences between the native environment block and
Environment.GetEnvironmentVariables()and seems to be Windows-specific (i.e. I can only reproduce it with a .NET Framework target).Minimal Repro
EnvironmentVariableTests.csprojEnvironmentInheritanceTests.csOutput of
dotnet test --tl:off: