Skip to content

Wrong environment variables when vstest runs in-process on Windows with InheritEnvironmentVariables = true #15740

Description

@jeremy-morren

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) 

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions