Skip to content
Draft
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: 163 additions & 0 deletions src/Tasks.UnitTests/Exec_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ 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 @@ -45,6 +46,7 @@ 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 @@ -904,6 +906,7 @@ 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 @@ -1068,6 +1071,166 @@ 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
15 changes: 9 additions & 6 deletions src/Tasks/Exec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +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
{
#region Constructors
Expand All @@ -46,7 +47,7 @@ public Exec()

// Are the encodings for StdErr and StdOut streams valid
private bool _encodingParametersValid = true;
private string _workingDirectory;
private AbsolutePath _workingDirectory;
private ITaskItem[] _outputs;
internal bool workingDirectoryIsUNC; // internal for unit testing
private string _batchFile;
Expand Down Expand Up @@ -458,10 +459,12 @@ 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 it's the current directory
// 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.
_workingDirectory = !string.IsNullOrEmpty(WorkingDirectory)
? WorkingDirectory
: Directory.GetCurrentDirectory();
? TaskEnvironment.GetAbsolutePath(WorkingDirectory)
: TaskEnvironment.ProjectDirectory;

// check if the working directory we're going to use for the exec command is a UNC path
workingDirectoryIsUNC = FileUtilitiesRegex.StartsWithUncPattern(_workingDirectory);
Expand All @@ -470,7 +473,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);
Log.LogErrorWithCodeFromResources("Exec.AllDriveLettersMappedError", _workingDirectory.OriginalValue);
return false;
}

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

if (workingDirectoryIsUNC)
Expand Down
19 changes: 19 additions & 0 deletions src/UnitTests.Shared/TaskEnvironmentHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,24 @@ public static TaskEnvironment CreateForTest()
{
return TaskEnvironment.Fallback;
}

/// <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