File: BackEnd\TaskEnvironment_Tests.cs
Web Access
Project: ..\..\..\src\Build.UnitTests\Microsoft.Build.Engine.UnitTests.csproj (Microsoft.Build.Engine.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Shouldly;
using Xunit;
 
namespace Microsoft.Build.UnitTests
{
    public class TaskEnvironment_Tests
    {
        private const string StubEnvironmentName = "Stub";
        private const string MultithreadedEnvironmentName = "Multithreaded";
 
        public static TheoryData<string> EnvironmentTypes =>
            new TheoryData<string>
            {
                StubEnvironmentName,
                MultithreadedEnvironmentName
            };
 
        // CA2000 is suppressed because the caller is responsible for disposal via DisposeTaskEnvironment
#pragma warning disable CA2000
        private static TaskEnvironment CreateTaskEnvironment(string environmentType)
        {
            return environmentType switch
            {
                StubEnvironmentName => TaskEnvironmentHelper.CreateForTest(),
                MultithreadedEnvironmentName => new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(GetResolvedTempPath())),
                _ => throw new ArgumentException($"Unknown environment type: {environmentType}")
            };
        }
#pragma warning restore CA2000
 
        private static void DisposeTaskEnvironment(TaskEnvironment taskEnvironment)
        {
            taskEnvironment?.Dispose();
        }
 
        /// <summary>
        /// Gets the fully resolved temp directory path. On macOS, Path.GetTempPath() returns paths starting with "/var/..."
        /// which is a symbolic link that resolves to "/private/var/...".
        /// The StubTaskEnvironmentDriver uses Directory.SetCurrentDirectory which resolves symlinks, while
        /// MultiThreadedTaskEnvironmentDriver stores the path as-is.
        /// This method ensures we get the canonical path to avoid test failures when comparing paths between the two drivers.
        /// </summary>
        /// <returns>The fully resolved temp directory path</returns>
        private static string GetResolvedTempPath()
        {
            string tempPath = Path.GetFullPath(Path.GetTempPath());
 
            // On macOS, /tmp is typically a symbolic link to /private/tmp
            // And /var is typically a symbolic link to /private/var
            // We need to manually resolve this because StubTaskEnvironmentDriver (via Directory.SetCurrentDirectory)
            // will resolve the symlink, but MultiThreadedTaskEnvironmentDriver will not.
            // By resolving it here, we ensure both drivers operate on the same canonical path string.
#if NET5_0_OR_GREATER
            bool IsMacOS = OperatingSystem.IsMacOS();
#else
            bool IsMacOS = Microsoft.Build.Framework.OperatingSystem.IsMacOS();
#endif
            if (IsMacOS)
            {
                // On macOS, /tmp -> /private/tmp and /var -> /private/var
                if (tempPath.StartsWith("/tmp/", StringComparison.OrdinalIgnoreCase) || string.Equals(tempPath, "/tmp", StringComparison.OrdinalIgnoreCase))
                {
                    tempPath = "/private" + tempPath;
                }
                else if (tempPath.StartsWith("/var/", StringComparison.OrdinalIgnoreCase) || string.Equals(tempPath, "/var", StringComparison.OrdinalIgnoreCase))
                {
                    tempPath = "/private" + tempPath;
                }
            }
            
            return tempPath;
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_SetAndGetEnvironmentVariable_ShouldWork(string environmentType)
        {
            var taskEnvironment = CreateTaskEnvironment(environmentType);
            string testVarName = $"MSBUILD_TEST_VAR_{environmentType}_{Guid.NewGuid():N}";
            string testVarValue = $"test_value_{environmentType}";
 
            try
            {
                taskEnvironment.SetEnvironmentVariable(testVarName, testVarValue);
                var retrievedValue = taskEnvironment.GetEnvironmentVariable(testVarName);
 
                retrievedValue.ShouldBe(testVarValue);
 
                var allVariables = taskEnvironment.GetEnvironmentVariables();
                allVariables.TryGetValue(testVarName, out string? actualValue).ShouldBeTrue();
                actualValue.ShouldBe(testVarValue);
            }
            finally
            {
                DisposeTaskEnvironment(taskEnvironment);
                Environment.SetEnvironmentVariable(testVarName, null);
            }
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_SetEnvironmentVariableToNull_ShouldRemoveVariable(string environmentType)
        {
            var taskEnvironment = CreateTaskEnvironment(environmentType);
            string testVarName = $"MSBUILD_REMOVE_TEST_{environmentType}_{Guid.NewGuid():N}";
            string testVarValue = "value_to_remove";
 
            try
            {
                taskEnvironment.SetEnvironmentVariable(testVarName, testVarValue);
                taskEnvironment.GetEnvironmentVariable(testVarName).ShouldBe(testVarValue);
 
                taskEnvironment.SetEnvironmentVariable(testVarName, null);
                taskEnvironment.GetEnvironmentVariable(testVarName).ShouldBeNull();
 
                var allVariables = taskEnvironment.GetEnvironmentVariables();
                allVariables.TryGetValue(testVarName, out string? actualValue).ShouldBeFalse();
            }
            finally
            {
                DisposeTaskEnvironment(taskEnvironment);
                Environment.SetEnvironmentVariable(testVarName, null);
            }
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_SetEnvironment_ShouldReplaceAllVariables(string environmentType)
        {
            var taskEnvironment = CreateTaskEnvironment(environmentType);
            string prefix = $"MSBUILD_SET_ENV_TEST_{environmentType}_{Guid.NewGuid():N}";
            string var1Name = $"{prefix}_VAR1";
            string var2Name = $"{prefix}_VAR2";
            string var3Name = $"{prefix}_VAR3";
 
            var originalEnvironment = taskEnvironment.GetEnvironmentVariables().ToDictionary(k => k.Key, v => v.Value);
 
            try
            {
                taskEnvironment.SetEnvironmentVariable(var1Name, "initial_value1");
                taskEnvironment.SetEnvironmentVariable(var2Name, "initial_value2");
 
                var newEnvironment = new Dictionary<string, string>
                {
                    [var2Name] = "updated_value2", // Update existing
                    [var3Name] = "new_value3"      // Add new
                    // var1Name is intentionally omitted to test removal
                };
 
                taskEnvironment.SetEnvironment(newEnvironment);
 
                taskEnvironment.GetEnvironmentVariable(var1Name).ShouldBeNull(); // Should be removed
                taskEnvironment.GetEnvironmentVariable(var2Name).ShouldBe("updated_value2"); // Should be updated
                taskEnvironment.GetEnvironmentVariable(var3Name).ShouldBe("new_value3"); // Should be added
            }
            finally
            {
                taskEnvironment.SetEnvironment(originalEnvironment);
                DisposeTaskEnvironment(taskEnvironment);
                Environment.SetEnvironmentVariable(var1Name, null);
                Environment.SetEnvironmentVariable(var2Name, null);
                Environment.SetEnvironmentVariable(var3Name, null);
            }
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_SetAndGetProjectDirectory_ShouldWork(string environmentType)
        {
            var taskEnvironment = CreateTaskEnvironment(environmentType);
            string originalDirectory = Directory.GetCurrentDirectory();
            string testDirectory = GetResolvedTempPath().TrimEnd(Path.DirectorySeparatorChar);
            string alternateDirectory = Path.GetDirectoryName(testDirectory)!;
 
            try
            {
                // Set project directory
                taskEnvironment.ProjectDirectory = new AbsolutePath(testDirectory, ignoreRootedCheck: true);
                var retrievedDirectory = taskEnvironment.ProjectDirectory;
 
                retrievedDirectory.Value.ShouldBe(testDirectory);
 
                // Change to alternate directory
                taskEnvironment.ProjectDirectory = new AbsolutePath(alternateDirectory, ignoreRootedCheck: true);
                var newRetrievedDirectory = taskEnvironment.ProjectDirectory;
 
                newRetrievedDirectory.Value.ShouldBe(alternateDirectory);
 
                // Verify behavior differs based on environment type
                if (environmentType == StubEnvironmentName)
                {
                    // Stub should change system current directory
                    Directory.GetCurrentDirectory().ShouldBe(alternateDirectory);
                }
                else
                {
                    // Multithreaded should not change system current directory
                    Directory.GetCurrentDirectory().ShouldBe(originalDirectory);
                }
            }
            finally
            {
                DisposeTaskEnvironment(taskEnvironment);
                Directory.SetCurrentDirectory(originalDirectory);
            }
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_GetAbsolutePath_ShouldResolveCorrectly(string environmentType)
        {
            var taskEnvironment = CreateTaskEnvironment(environmentType);
            string baseDirectory = GetResolvedTempPath();
            string relativePath = Path.Combine("subdir", "file.txt");
            string originalDirectory = Directory.GetCurrentDirectory();
 
            try
            {
                taskEnvironment.ProjectDirectory = new AbsolutePath(baseDirectory, ignoreRootedCheck: true);
 
                var absolutePath = taskEnvironment.GetAbsolutePath(relativePath);
 
                Path.IsPathRooted(absolutePath.Value).ShouldBeTrue();
                string expectedPath = Path.Combine(baseDirectory, relativePath);
                absolutePath.Value.ShouldBe(expectedPath);
            }
            finally
            {
                DisposeTaskEnvironment(taskEnvironment);
                Directory.SetCurrentDirectory(originalDirectory);
            }
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_GetAbsolutePath_WithAlreadyAbsolutePath_ShouldReturnUnchanged(string environmentType)
        {
            var taskEnvironment = CreateTaskEnvironment(environmentType);
            string absoluteInputPath = Path.Combine(GetResolvedTempPath(), "already", "absolute", "path.txt");
 
            try
            {
                var resultPath = taskEnvironment.GetAbsolutePath(absoluteInputPath);
                resultPath.Value.ShouldBe(absoluteInputPath);
            }
            finally
            {
                DisposeTaskEnvironment(taskEnvironment);
            }
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_GetProcessStartInfo_ShouldConfigureCorrectly(string environmentType)
        {
            var taskEnvironment = CreateTaskEnvironment(environmentType);
            string testDirectory = GetResolvedTempPath();
            string testVarName = $"MSBUILD_PROCESS_TEST_{environmentType}_{Guid.NewGuid():N}";
            string testVarValue = "process_test_value";
            string originalDirectory = Directory.GetCurrentDirectory();
 
            try
            {
                taskEnvironment.ProjectDirectory = new AbsolutePath(testDirectory, ignoreRootedCheck: true);
                taskEnvironment.SetEnvironmentVariable(testVarName, testVarValue);
 
                var processStartInfo = taskEnvironment.GetProcessStartInfo();
 
                processStartInfo.ShouldNotBeNull();
 
                if (environmentType == StubEnvironmentName)
                {
                    // Stub should reflect system environment, but working directory should be empty
                    processStartInfo.WorkingDirectory.ShouldBe(string.Empty);
                }
                else
                {
                    // Multithreaded should reflect isolated environment
                    processStartInfo.WorkingDirectory.ShouldBe(testDirectory);
                }
 
                processStartInfo.Environment.TryGetValue(testVarName, out string? actualValue).ShouldBeTrue();
                actualValue.ShouldBe(testVarValue);
            }
            finally
            {
                DisposeTaskEnvironment(taskEnvironment);
                Environment.SetEnvironmentVariable(testVarName, null);
                Directory.SetCurrentDirectory(originalDirectory);
            }
        }
 
        [Fact]
        public void TaskEnvironment_StubEnvironment_ShouldAffectSystemEnvironment()
        {
            string testVarName = $"MSBUILD_STUB_ISOLATION_TEST_{Guid.NewGuid():N}";
            string testVarValue = "stub_test_value";
 
            var stubEnvironment = TaskEnvironmentHelper.CreateForTest();
 
            try
            {
                // Set variable in stub environment
                stubEnvironment.SetEnvironmentVariable(testVarName, testVarValue);
 
                // Stub should affect system environment
                Environment.GetEnvironmentVariable(testVarName).ShouldBe(testVarValue);
                stubEnvironment.GetEnvironmentVariable(testVarName).ShouldBe(testVarValue);
 
                // Remove from stub environment
                stubEnvironment.SetEnvironmentVariable(testVarName, null);
 
                // System environment should also be affected
                Environment.GetEnvironmentVariable(testVarName).ShouldBeNull();
                stubEnvironment.GetEnvironmentVariable(testVarName).ShouldBeNull();
            }
            finally
            {
                Environment.SetEnvironmentVariable(testVarName, null);
            }
        }
 
        [Fact]
        public void TaskEnvironment_MultithreadedEnvironment_ShouldBeIsolatedFromSystem()
        {
            string testVarName = $"MSBUILD_MULTITHREADED_ISOLATION_TEST_{Guid.NewGuid():N}";
            string testVarValue = "multithreaded_test_value";
 
            using var driver = new MultiThreadedTaskEnvironmentDriver(
                GetResolvedTempPath(),
                new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase));
            var multithreadedEnvironment = new TaskEnvironment(driver);
 
            try
            {
                // Verify system and environement doesn't have the test variable initially
                Environment.GetEnvironmentVariable(testVarName).ShouldBeNull();
                multithreadedEnvironment.GetEnvironmentVariable(testVarName).ShouldBeNull();
 
                // Set variable in multithreaded environment
                multithreadedEnvironment.SetEnvironmentVariable(testVarName, testVarValue);
 
                // Multithreaded should have the value but system should not
                multithreadedEnvironment.GetEnvironmentVariable(testVarName).ShouldBe(testVarValue);
                Environment.GetEnvironmentVariable(testVarName).ShouldBeNull();
            }
            finally
            {
                Environment.SetEnvironmentVariable(testVarName, null);
            }
        }
 
        [Theory]
        [MemberData(nameof(EnvironmentTypes))]
        public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow(string environmentType)
        {
            // Construct a path containing an invalid path character
            char invalidChar = Path.GetInvalidPathChars().FirstOrDefault();
            string invalidPath = "invalid" + invalidChar + "path";
 
            var taskEnvironment = CreateTaskEnvironment(environmentType);
 
            try
            {
                // Should not throw on invalid path characters
                var absolutePath = taskEnvironment.GetAbsolutePath(invalidPath);
 
                // The result should contain the invalid path combined with the base directory
                absolutePath.Value.ShouldNotBeNullOrEmpty();
                absolutePath.Value.ShouldContain(invalidPath);
            }
            finally
            {
                DisposeTaskEnvironment(taskEnvironment);
            }
        }
    }
}