diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs index 4a9d6126ced..43b7910f1c0 100644 --- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs @@ -360,6 +360,111 @@ public void TaskEnvironment_MultithreadedEnvironment_ShouldBeIsolatedFromSystem( } } + [Fact] + public void TaskEnvironment_Fallback_ReadsProcessEnvironment() + { + string testVarName = $"MSBUILD_DEFAULT_ENV_TEST_{Guid.NewGuid():N}"; + string testVarValue = "default_env_test_value"; + + try + { + Environment.SetEnvironmentVariable(testVarName, testVarValue); + + TaskEnvironment.Fallback.GetEnvironmentVariable(testVarName).ShouldBe(testVarValue); + } + finally + { + Environment.SetEnvironmentVariable(testVarName, null); + } + } + + [Fact] + public void TaskEnvironment_CreateWithProjectDirectoryAndEnvironment_SnapshotsCurrentEnvironment() + { + string testVarName = $"MSBUILD_CREATE_MT_TEST_{Guid.NewGuid():N}"; + string testVarValue = "snapshot_test_value"; + string projectDir = GetResolvedTempPath(); + + try + { + Environment.SetEnvironmentVariable(testVarName, testVarValue); + + TaskEnvironment env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + env.ShouldNotBeNull(); + env.GetEnvironmentVariable(testVarName).ShouldBe(testVarValue); + env.ProjectDirectory.Value.ShouldBe(projectDir); + + // Changing the process env var after snapshot should not affect the isolated environment. + Environment.SetEnvironmentVariable(testVarName, "changed_after_snapshot"); + env.GetEnvironmentVariable(testVarName).ShouldBe(testVarValue); + } + finally + { + Environment.SetEnvironmentVariable(testVarName, null); + } + } + + [Fact] + public void TaskEnvironment_CreateWithProjectDirectoryAndEnvironment_WithCustomEnvironment_UsesProvidedDictionary() + { + string excludedVarName = $"MSBUILD_EXCLUDED_VAR_{Guid.NewGuid():N}"; + string projectDir = GetResolvedTempPath(); + + try + { + // Set a process-level env var that should NOT appear in the custom environment. + Environment.SetEnvironmentVariable(excludedVarName, "process_level_value"); + + var customEnv = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["MY_CUSTOM_VAR"] = "custom_value", + ["ANOTHER_VAR"] = "another_value" + }; + + TaskEnvironment env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir, customEnv); + + env.ShouldNotBeNull(); + env.GetEnvironmentVariable("MY_CUSTOM_VAR").ShouldBe("custom_value"); + env.GetEnvironmentVariable("ANOTHER_VAR").ShouldBe("another_value"); + env.GetEnvironmentVariable(excludedVarName).ShouldBeNull(); + env.ProjectDirectory.Value.ShouldBe(projectDir); + } + finally + { + Environment.SetEnvironmentVariable(excludedVarName, null); + } + } + + [Fact] + public void TaskEnvironment_CreateWithProjectDirectoryAndEnvironment_ReturnsIsolatedInstances() + { + string projectDir = GetResolvedTempPath(); + + TaskEnvironment env1 = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + TaskEnvironment env2 = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + env1.ShouldNotBeSameAs(env2); + + string testVarName = $"MSBUILD_ISOLATION_TEST_{Guid.NewGuid():N}"; + env1.SetEnvironmentVariable(testVarName, "only_in_env1"); + + env1.GetEnvironmentVariable(testVarName).ShouldBe("only_in_env1"); + env2.GetEnvironmentVariable(testVarName).ShouldNotBe("only_in_env1"); + } + + [Fact] + public void TaskEnvironment_CreateWithProjectDirectoryAndEnvironment_NullProjectDirectory_Throws() + { + Should.Throw(() => TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(null!)); + } + + [Fact] + public void TaskEnvironment_CreateWithProjectDirectoryAndEnvironment_EmptyProjectDirectory_Throws() + { + Should.Throw(() => TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(string.Empty)); + } + [Theory] [MemberData(nameof(EnvironmentTypes))] public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow(string environmentType) diff --git a/src/Framework/TaskEnvironment.cs b/src/Framework/TaskEnvironment.cs index ec18be1d817..43f3c92315e 100644 --- a/src/Framework/TaskEnvironment.cs +++ b/src/Framework/TaskEnvironment.cs @@ -1,6 +1,7 @@ // 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.Diagnostics; @@ -22,6 +23,38 @@ internal TaskEnvironment(ITaskEnvironmentDriver driver) _driver = driver; } + /// + /// Gets the fallback task environment that directly accesses the system environment variables + /// and working directory of the current process. + /// + /// + /// This is the environment provided to tasks by the MSBuild engine in multi-process execution mode, + /// where each task runs in its own process and process-level state is inherently isolated. + /// + public static TaskEnvironment Fallback { get; } = new(MultiProcessTaskEnvironmentDriver.Instance); + + /// + /// Creates a new with isolated working directory and environment variables. + /// + /// + /// This method is primarily intended for testing scenarios. In normal MSBuild operation, the correct task environment is provided by the MSBuild engine. + /// The created TaskEnvironment provides isolated environment state similar to what tasks receive in multithreaded execution mode, enabling testing of task isolation behavior. + /// + /// The initial working directory for the task. + /// A dictionary of environment variables to use, or to use the current process environment variables. + /// A new with isolated environment state. + /// is . + /// is empty. + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public static TaskEnvironment CreateWithProjectDirectoryAndEnvironment(string projectDirectory, IDictionary? environmentVariables = null) + { + ArgumentException.ThrowIfNullOrEmpty(projectDirectory); + + return environmentVariables is null + ? new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(projectDirectory)) + : new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(projectDirectory, environmentVariables)); + } + /// /// Gets or sets the project directory for the task execution. ///