Skip to content
35 changes: 35 additions & 0 deletions src/Build.UnitTests/BackEnd/ReadEnvironmentVariableTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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.Diagnostics;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.Build.UnitTests
{
/// <summary>
/// Reads an environment variable from inside the (possibly out-of-proc) task execution process and returns
/// its value together with the executing process id. Used to verify that an out-of-proc task host applies the
/// build process environment correctly, including across consecutive tasks where the environment apply may be
/// skipped as an optimization.
/// </summary>
public class ReadEnvironmentVariableTask : Task
{
[Required]
public string VariableName { get; set; } = string.Empty;

[Output]
public string Value { get; set; } = string.Empty;

[Output]
public int Pid { get; set; }

public override bool Execute()
{
Value = Environment.GetEnvironmentVariable(VariableName) ?? string.Empty;
Pid = Process.GetCurrentProcess().Id;
return true;
}
}
}
44 changes: 44 additions & 0 deletions src/Build.UnitTests/BackEnd/ReadGlobalPropertyTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.Build.UnitTests
{
/// <summary>
/// Reads a global property (via <see cref="IBuildEngine6.GetGlobalProperties"/>) from inside the (possibly
/// out-of-proc) task execution process and returns its value together with the executing process id. Used to
/// verify that an out-of-proc task host reconstructs the global properties correctly across consecutive tasks,
/// including ones whose global-properties configuration was sent as the deduplicated "identical" marker.
/// </summary>
public class ReadGlobalPropertyTask : Task
{
[Required]
public string PropertyName { get; set; } = string.Empty;

[Output]
public string Value { get; set; } = string.Empty;

[Output]
public int Pid { get; set; }

public override bool Execute()
{
Pid = Process.GetCurrentProcess().Id;

if (BuildEngine is IBuildEngine6 buildEngine6)
{
IReadOnlyDictionary<string, string> globalProperties = buildEngine6.GetGlobalProperties();
if (globalProperties.TryGetValue(PropertyName, out string? value) && value is not null)
{
Value = value;
}
}

return true;
}
}
}
35 changes: 35 additions & 0 deletions src/Build.UnitTests/BackEnd/SetEnvironmentVariableTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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.Diagnostics;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.Build.UnitTests
{
/// <summary>
/// Sets an environment variable from inside the (possibly out-of-proc) task execution process and returns the
/// executing process id. Used together with <see cref="ReadEnvironmentVariableTask"/> to verify that an
/// environment change made by one task-host task is observed by a subsequent task-host task on the same
/// connection, i.e. that the build process environment is re-sent (not falsely deduplicated) after it changes.
/// </summary>
public class SetEnvironmentVariableTask : Task
{
[Required]
public string VariableName { get; set; } = string.Empty;

[Required]
public string Value { get; set; } = string.Empty;

[Output]
public int Pid { get; set; }

public override bool Execute()
{
Environment.SetEnvironmentVariable(VariableName, Value);
Pid = Process.GetCurrentProcess().Id;
return true;
}
}
}
35 changes: 35 additions & 0 deletions src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,41 @@ public void TaskEnvironment_SetEnvironmentVariableToNull_ShouldRemoveVariable(st
}
}

/// <summary>
/// Regression test for the task-host environment-delta self-clear bug. In multithreaded mode
/// <see cref="TaskEnvironment.GetEnvironmentVariables"/> returns the driver's backing dictionary by
/// reference, so the "environment unchanged" task-host completion path can pass that very dictionary
/// back to SetEnvironment. A naive Clear()-then-copy implementation would self-empty the environment
/// because the source and destination are the same object; this verifies the environment is preserved.
/// </summary>
[Fact]
public void TaskEnvironment_SetEnvironment_WithAliasedDictionary_PreservesEnvironment()
{
var taskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(GetResolvedTempPath());
try
{
string testVarName = $"MSBUILD_ALIAS_TEST_{Guid.NewGuid():N}";
taskEnvironment.SetEnvironmentVariable(testVarName, "alias_value");

int countBefore = taskEnvironment.GetEnvironmentVariables().Count;
countBefore.ShouldBeGreaterThan(1);

// Pass the driver's own (aliased) dictionary back to SetEnvironment, exactly as the
// task-host "environment unchanged" path does for the multithreaded driver.
var aliased = (IDictionary<string, string>)taskEnvironment.GetEnvironmentVariables();
taskEnvironment.SetEnvironment(aliased);

var after = taskEnvironment.GetEnvironmentVariables();
after.Count.ShouldBe(countBefore);
after.TryGetValue(testVarName, out string? value).ShouldBeTrue();
value.ShouldBe("alias_value");
}
finally
{
DisposeTaskEnvironment(taskEnvironment);
}
}

[Theory]
[MemberData(nameof(EnvironmentTypes))]
public void TaskEnvironment_SetEnvironment_ShouldReplaceAllVariables(string environmentType)
Expand Down
Loading
Loading