Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 0 additions & 163 deletions src/Tasks.UnitTests/Exec_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ private Exec PrepareExec(string command)
{
IBuildEngine2 mockEngine = new MockEngine(_output);
Exec exec = new Exec();
exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
exec.BuildEngine = mockEngine;
exec.Command = command;
return exec;
Expand All @@ -47,7 +46,6 @@ private ExecWrapper PrepareExecWrapper(string command)
{
IBuildEngine2 mockEngine = new MockEngine(_output);
ExecWrapper exec = new ExecWrapper();
exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
exec.BuildEngine = mockEngine;
exec.Command = command;
return exec;
Expand Down Expand Up @@ -907,7 +905,6 @@ public void ValidateParametersNoCommand()
public void SetEnvironmentVariableParameter()
{
Exec exec = new Exec();
exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
exec.BuildEngine = new MockEngine();
exec.Command = NativeMethodsShared.IsWindows ? "echo [%MYENVVAR%]" : "echo [$myenvvar]";
exec.EnvironmentVariables = new[] { "myenvvar=myvalue" };
Expand Down Expand Up @@ -1072,166 +1069,6 @@ public void ConsoleOutputDoesNotTrimLeadingWhitespace()
exec.ConsoleOutput[0].ItemSpec.ShouldBe(lineWithLeadingWhitespace);
}
}

/// <summary>
/// Runs an Exec task that lists directory contents and asserts expected/unexpected files in the output.
/// </summary>
/// <param name="taskEnvironment">The TaskEnvironment to configure on the Exec task.</param>
/// <param name="workingDirectory">The WorkingDirectory to set, or null to use the default.</param>
/// <param name="expectedFile">A filename that must appear in the output.</param>
/// <param name="notExpectedFile">A filename that must NOT appear in the output, or null to skip.</param>
private void ExecuteListCommandInDirectory(
TaskEnvironment taskEnvironment,
string workingDirectory,
string expectedFile,
string notExpectedFile = null)
{
Exec exec = new Exec();
exec.TaskEnvironment = taskEnvironment;
exec.BuildEngine = new MockEngine(_output);
exec.Command = NativeMethodsShared.IsWindows ? "dir /b" : "ls";
exec.ConsoleToMSBuild = true;

if (workingDirectory != null)
{
exec.WorkingDirectory = workingDirectory;
}

bool result = exec.Execute();

result.ShouldBeTrue();
((MockEngine)exec.BuildEngine).AssertLogContains(expectedFile);
if (notExpectedFile != null)
{
((MockEngine)exec.BuildEngine).AssertLogDoesntContain(notExpectedFile);
}
}

/// <summary>
/// Verify that Exec resolves relative WorkingDirectory via TaskEnvironment.GetAbsolutePath in multiprocess mode.
/// </summary>
[Fact]
public void ExecResolvesRelativeWorkingDirectoryWithMultiProcessDriver()
{
using (var testEnv = TestEnvironment.Create(_output))
{
var projectDir = testEnv.CreateFolder();
var subDir = Directory.CreateDirectory(Path.Combine(projectDir.Path, "subdir"));
File.WriteAllText(Path.Combine(subDir.FullName, "testfile.txt"), "test content");

var differentDir = testEnv.CreateFolder();
var decoySubDir = Directory.CreateDirectory(Path.Combine(differentDir.Path, "subdir"));
File.WriteAllText(Path.Combine(decoySubDir.FullName, "decoyfile.txt"), "decoy content");

string originalDirectory = Directory.GetCurrentDirectory();
try
{
Directory.SetCurrentDirectory(projectDir.Path);

ExecuteListCommandInDirectory(
TaskEnvironmentHelper.CreateForTest(),
workingDirectory: "subdir",
expectedFile: "testfile.txt",
notExpectedFile: "decoyfile.txt");
}
finally
{
Directory.SetCurrentDirectory(originalDirectory);
}
}
}

/// <summary>
/// Verify that Exec uses TaskEnvironment.ProjectDirectory when WorkingDirectory is not specified.
/// Uses MultiThreadedTaskEnvironmentDriver so process CWD differs from project directory.
/// </summary>
[Fact]
public void ExecUsesProjectDirectoryAsDefaultWorkingDirectory()
{
using (var testEnv = TestEnvironment.Create(_output))
{
var projectDir = testEnv.CreateFolder();
File.WriteAllText(Path.Combine(projectDir.Path, "projectfile.txt"), "project content");

var differentCwd = testEnv.CreateFolder();
File.WriteAllText(Path.Combine(differentCwd.Path, "decoyfile.txt"), "decoy content");

string originalDirectory = Directory.GetCurrentDirectory();
TaskEnvironment taskEnvironment = null;
try
{
Directory.SetCurrentDirectory(differentCwd.Path);

taskEnvironment = TaskEnvironmentHelper.CreateMultithreadedForTest(projectDir.Path);
ExecuteListCommandInDirectory(
taskEnvironment,
workingDirectory: null,
expectedFile: "projectfile.txt",
notExpectedFile: "decoyfile.txt");
}
finally
{
taskEnvironment?.Dispose();
Directory.SetCurrentDirectory(originalDirectory);
}
}
}

/// <summary>
/// Verify that Exec correctly handles absolute WorkingDirectory paths.
/// </summary>
[Fact]
public void ExecHandlesAbsoluteWorkingDirectory()
{
using (var testEnv = TestEnvironment.Create(_output))
{
var workDir = testEnv.CreateFolder();
File.WriteAllText(Path.Combine(workDir.Path, "absolutedir.txt"), "absolute content");

ExecuteListCommandInDirectory(
TaskEnvironmentHelper.CreateForTest(),
workingDirectory: workDir.Path,
expectedFile: "absolutedir.txt");
}
}

/// <summary>
/// Verify that Exec resolves relative WorkingDirectory relative to TaskEnvironment.ProjectDirectory,
/// not the process current directory. Uses MultiThreadedTaskEnvironmentDriver to simulate
/// multithreaded mode where process CWD differs from project directory.
/// </summary>
[Fact]
public void ExecResolvesRelativeWorkingDirectoryRelativeToProjectDirectory()
{
using (var testEnv = TestEnvironment.Create(_output))
{
var projectDir = testEnv.CreateFolder();
var subDir = Directory.CreateDirectory(Path.Combine(projectDir.Path, "builddir"));
File.WriteAllText(Path.Combine(subDir.FullName, "multithreaded.txt"), "multithreaded content");

var differentCwd = testEnv.CreateFolder();
File.WriteAllText(Path.Combine(differentCwd.Path, "decoyfile.txt"), "decoy content");

string originalDirectory = Directory.GetCurrentDirectory();
TaskEnvironment taskEnvironment = null;
try
{
Directory.SetCurrentDirectory(differentCwd.Path);

taskEnvironment = TaskEnvironmentHelper.CreateMultithreadedForTest(projectDir.Path);
ExecuteListCommandInDirectory(
taskEnvironment,
workingDirectory: "builddir",
expectedFile: "multithreaded.txt",
notExpectedFile: "decoyfile.txt");
}
finally
{
taskEnvironment?.Dispose();
Directory.SetCurrentDirectory(originalDirectory);
}
}
}
}

internal sealed class ExecWrapper : Exec
Expand Down
74 changes: 7 additions & 67 deletions src/Tasks/Exec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
Expand All @@ -22,8 +21,7 @@ namespace Microsoft.Build.Tasks
/// for it to complete, and then returns True if the process completed successfully, and False if an error occurred.
/// </summary>
// UNDONE: ToolTask has a "UseCommandProcessor" flag that duplicates much of the code in this class. Remove the duplication.
[MSBuildMultiThreadableTask]
public class Exec : ToolTaskExtension, IMultiThreadableTask
public class Exec : ToolTaskExtension
{
#region Constructors

Expand All @@ -48,7 +46,7 @@ public Exec()

// Are the encodings for StdErr and StdOut streams valid
private bool _encodingParametersValid = true;
private AbsolutePath _workingDirectory;
private string _workingDirectory;
private ITaskItem[] _outputs;
internal bool workingDirectoryIsUNC; // internal for unit testing
private string _batchFile;
Expand Down Expand Up @@ -84,9 +82,6 @@ public string Command

public bool IgnoreExitCode { get; set; }

/// <inheritdoc />
public TaskEnvironment TaskEnvironment { get; set; }

/// <summary>
/// Enable the pipe of the standard out to an item (StandardOutput).
/// </summary>
Expand Down Expand Up @@ -463,12 +458,10 @@ protected override bool ValidateParameters()
}

// determine what the working directory for the exec command is going to be -- if the user specified a working
// directory use that, otherwise default to the project directory (TaskEnvironment.ProjectDirectory). Using the
// project directory instead of the process current directory is important for correctness in multithreaded (/mt)
// builds, where the process working directory may not match the project being built.
// directory use that, otherwise it's the current directory
_workingDirectory = !string.IsNullOrEmpty(WorkingDirectory)
? TaskEnvironment.GetAbsolutePath(WorkingDirectory)
: TaskEnvironment.ProjectDirectory;
? WorkingDirectory
: Directory.GetCurrentDirectory();

// check if the working directory we're going to use for the exec command is a UNC path
workingDirectoryIsUNC = FileUtilitiesRegex.StartsWithUncPattern(_workingDirectory);
Expand All @@ -477,7 +470,7 @@ protected override bool ValidateParameters()
// will not be able to auto-map to the UNC path
if (workingDirectoryIsUNC && NativeMethods.AllDrivesMapped())
{
Log.LogErrorWithCodeFromResources("Exec.AllDriveLettersMappedError", _workingDirectory.OriginalValue);
Log.LogErrorWithCodeFromResources("Exec.AllDriveLettersMappedError", _workingDirectory);
return false;
}

Expand Down Expand Up @@ -540,7 +533,7 @@ protected override string GetWorkingDirectory()
// So verify it's valid here.
if (!FileSystems.Default.DirectoryExists(_workingDirectory))
{
throw new DirectoryNotFoundException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Exec.InvalidWorkingDirectory", _workingDirectory.OriginalValue));
throw new DirectoryNotFoundException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Exec.InvalidWorkingDirectory", _workingDirectory));
}

if (workingDirectoryIsUNC)
Expand All @@ -567,59 +560,6 @@ internal string GetWorkingDirectoryAccessor()
return GetWorkingDirectory();
}

/// <summary>
/// Gets the ProcessStartInfo for the spawned process, with environment variables from TaskEnvironment.
/// In multithreaded mode, TaskEnvironment contains the virtualized environment for this project,
/// which must be passed to the spawned process since it won't inherit it from the (shared) process environment.
/// </summary>
protected override ProcessStartInfo GetProcessStartInfo(
string pathToTool,
string commandLineCommands,
string responseFileSwitch)
{
// Get the base ProcessStartInfo with all ToolTask settings (command line, redirections, encodings, etc.)
// This also applies EnvironmentVariables overrides from the task property.
ProcessStartInfo startInfo = base.GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch);

// Replace the inherited process environment with the virtualized one from TaskEnvironment.
// TaskEnvironment.GetProcessStartInfo() already configures env vars and working directory correctly
// for both multithreaded (virtualized) and multi-process (inherited) modes.
ProcessStartInfo taskEnvStartInfo = TaskEnvironment.GetProcessStartInfo();
startInfo.Environment.Clear();
foreach (var kvp in taskEnvStartInfo.Environment)
{
startInfo.Environment[kvp.Key] = kvp.Value;
}

// Re-apply obsolete EnvironmentOverride and EnvironmentVariables property overrides —
// they should take precedence over TaskEnvironment. The base class already applied these,
// but we cleared the environment above, so we need to re-apply them.
#pragma warning disable 0618 // obsolete
Dictionary<string, string> envOverrides = EnvironmentOverride;
if (envOverrides != null)
{
foreach (KeyValuePair<string, string> entry in envOverrides)
{
startInfo.Environment[entry.Key] = entry.Value;
}
}
#pragma warning restore 0618

if (EnvironmentVariables != null)
{
foreach (string entry in EnvironmentVariables)
{
string[] nameValuePair = entry.Split(['='], 2);
if (nameValuePair.Length == 2 && nameValuePair[0].Length > 0)
{
startInfo.Environment[nameValuePair[0]] = nameValuePair[1];
}
}
}

return startInfo;
}

/// <summary>
/// Adds the arguments for cmd.exe
/// </summary>
Expand Down
19 changes: 0 additions & 19 deletions src/UnitTests.Shared/TaskEnvironmentHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,5 @@ public static TaskEnvironment CreateForTest()
{
return new TaskEnvironment(MultiProcessTaskEnvironmentDriver.Instance);
}

/// <summary>
/// Creates a TaskEnvironment backed by the multi-threaded driver which virtualizes
/// environment variables and current directory. This allows testing of multithreaded mode
/// behavior where each project has its own isolated environment.
/// </summary>
/// <param name="projectDirectory">The project directory to use for the task environment.</param>
/// <returns>A TaskEnvironment suitable for testing multithreaded mode scenarios.</returns>
/// <remarks>
/// The caller is responsible for disposing the TaskEnvironment via TaskEnvironment.Dispose(),
/// which will clean up the underlying driver's thread-local state.
/// </remarks>
// CA2000 is suppressed because the driver is owned by the TaskEnvironment and disposed via TaskEnvironment.Dispose()
#pragma warning disable CA2000
public static TaskEnvironment CreateMultithreadedForTest(string projectDirectory)
{
return new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(projectDirectory));
}
#pragma warning restore CA2000
}
}
Loading