Skip to content
Closed
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
65 changes: 56 additions & 9 deletions src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,11 @@ 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
nameof(MultiProcessTaskEnvironmentDriver),
nameof(MultiThreadedTaskEnvironmentDriver)
};

// CA2000 is suppressed because the caller is responsible for disposal via DisposeTaskEnvironment
Expand All @@ -30,8 +27,8 @@ private static TaskEnvironment CreateTaskEnvironment(string environmentType)
{
return environmentType switch
{
StubEnvironmentName => TaskEnvironmentHelper.CreateForTest(),
MultithreadedEnvironmentName => new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(GetResolvedTempPath())),
nameof(MultiProcessTaskEnvironmentDriver) => TaskEnvironmentHelper.CreateForTest(),
nameof(MultiThreadedTaskEnvironmentDriver) => new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(GetResolvedTempPath())),
_ => throw new ArgumentException($"Unknown environment type: {environmentType}")
};
}
Expand Down Expand Up @@ -196,7 +193,7 @@ public void TaskEnvironment_SetAndGetProjectDirectory_ShouldWork(string environm
newRetrievedDirectory.Value.ShouldBe(alternateDirectory);

// Verify behavior differs based on environment type
if (environmentType == StubEnvironmentName)
if (environmentType == nameof(MultiProcessTaskEnvironmentDriver))
{
// Stub should change system current directory
Directory.GetCurrentDirectory().ShouldBe(alternateDirectory);
Expand Down Expand Up @@ -257,6 +254,56 @@ public void TaskEnvironment_GetAbsolutePath_WithAlreadyAbsolutePath_ShouldReturn
DisposeTaskEnvironment(taskEnvironment);
}
}

[Theory]
[MemberData(nameof(EnvironmentTypes))]
public void TaskEnvironment_TryGetAbsolutePath_ShouldReturnTrueForValidPaths(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 expected = taskEnvironment.GetAbsolutePath(relativePath);

bool result = taskEnvironment.TryGetAbsolutePath(relativePath, out AbsolutePath actual);

result.ShouldBeTrue();
actual.Value.ShouldBe(expected.Value);
}
finally
{
DisposeTaskEnvironment(taskEnvironment);
Directory.SetCurrentDirectory(originalDirectory);
}
}

[Theory]
[MemberData(nameof(EnvironmentTypes))]
public void TaskEnvironment_TryGetAbsolutePath_ShouldReturnFalseForInvalidPath(string environmentType)
{
var taskEnvironment = CreateTaskEnvironment(environmentType);

try
{
// Construct a path containing an invalid path character
char invalidChar = Path.GetInvalidPathChars().FirstOrDefault();
string invalidPath = "invalid" + invalidChar + "path";

bool result = taskEnvironment.TryGetAbsolutePath(invalidPath, out AbsolutePath _);

// TryGetAbsolutePath should return false rather than throw
result.ShouldBeFalse();
}
finally
{
DisposeTaskEnvironment(taskEnvironment);
}
}

[Theory]
[MemberData(nameof(EnvironmentTypes))]
Expand All @@ -277,7 +324,7 @@ public void TaskEnvironment_GetProcessStartInfo_ShouldConfigureCorrectly(string

processStartInfo.ShouldNotBeNull();

if (environmentType == StubEnvironmentName)
if (environmentType == nameof(MultiProcessTaskEnvironmentDriver))
{
// Stub should reflect system environment, but working directory should be empty
processStartInfo.WorkingDirectory.ShouldBe(string.Empty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;

Expand Down Expand Up @@ -41,7 +40,13 @@ public AbsolutePath ProjectDirectory
/// <inheritdoc/>
public AbsolutePath GetAbsolutePath(string path)
{
return new AbsolutePath(Path.GetFullPath(path), ignoreRootedCheck: true);
#pragma warning disable IDE0002
// Calling to the System.IO.Path.Combine via fully-qualified name,
// as Microsoft.IO.Path.Combine does not throw on illegal characters in the path.
// We would like to keep the behavior consistent between dotnet build and msbuild.exe.
return new AbsolutePath(System.IO.Path.Combine(NativeMethodsShared.GetCurrentDirectory(), path), ignoreRootedCheck: true);
#pragma warning restore IDE0002

}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public AbsolutePath ProjectDirectory
{
_currentDirectory = value;
// Keep the thread-static in sync for use by Expander and Modifiers during property/item expansion.
// This allows Path.GetFullPath and %(FullPath) to resolve relative paths correctly in multithreaded mode.
// This allows Path.GetFullPath and %(FullPath) functions used in project files to resolve relative paths correctly in multithreaded mode.
FileUtilities.CurrentThreadWorkingDirectory = value.Value;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/Framework/ITaskEnvironmentDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ internal interface ITaskEnvironmentDriver : IDisposable
/// </summary>
/// <param name="path">The path to convert to absolute.</param>
/// <returns>An absolute path representation.</returns>
/// <remarks>
/// May throw exceptions in case of invalid input (the exceptions are thrown by <see cref="System.IO.Path.Combine(string,string)"/> that is used internally in both implementations).
/// </remarks>
AbsolutePath GetAbsolutePath(string path);

/// <summary>
Expand Down
14 changes: 13 additions & 1 deletion src/Framework/PathHelpers/AbsolutePath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,19 @@ private static void ValidatePath(string path)
/// </summary>
/// <param name="path">The path to combine with the base path.</param>
/// <param name="basePath">The base path to combine with.</param>
public AbsolutePath(string path, AbsolutePath basePath) => Value = Path.Combine(basePath.Value, path);
/// <exception cref="System.ArgumentException">
/// Thrown when either <paramref name="path"/> or <paramref name="basePath"/> contains one or more invalid path characters as
/// returned by <see cref="System.IO.Path.GetInvalidPathChars"/>.
/// </exception>
public AbsolutePath(string path, AbsolutePath basePath)
{
#pragma warning disable IDE0002
// Calling to the System.IO.Path.Combine via fully-qualified name,
// as Microsoft.IO.Path.Combine does not throw on illegal characters in the path.
// We would like to keep the behavior consistent between dotnet build and msbuild.exe.
Value = System.IO.Path.Combine(basePath.Value, path);
#pragma warning restore IDE0002
}

/// <summary>
/// Implicitly converts an AbsolutePath to a string.
Expand Down
29 changes: 27 additions & 2 deletions src/Framework/TaskEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,37 @@ public AbsolutePath ProjectDirectory

/// <summary>
/// Converts a relative or absolute path string to an absolute path.
/// This function resolves paths relative to ProjectDirectory.
/// This function resolves paths relative to <see cref="ProjectDirectory"/>.
/// </summary>
/// <param name="path">The path to convert.</param>
/// <returns>An absolute path representation.</returns>
/// <exception cref="System.ArgumentException">
/// Thrown when <paramref name="path"/> contains one or more invalid path characters as
/// returned by <see cref="System.IO.Path.GetInvalidPathChars"/>.
/// </exception>
public AbsolutePath GetAbsolutePath(string path) => _driver.GetAbsolutePath(path);

/// <summary>
/// Converts a relative or absolute path string to an absolute path.
/// This function resolves paths relative to <see cref="ProjectDirectory"/>.
/// </summary>
/// <param name="path">The path to convert to absolute.</param>
/// <param name="absolutePath">The resulting absolute path if the conversion succeeded; default otherwise.</param>
/// <returns>True if the conversion succeeded; false otherwise.</returns>
public bool TryGetAbsolutePath(string path, out AbsolutePath absolutePath)
{
try
{
absolutePath = GetAbsolutePath(path);
return true;
}
catch
{
absolutePath = default;
return false;
}
Comment on lines +60 to +64
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch-all exception handler with an empty catch block is not following best practices for Try* pattern methods. The method should only catch specific expected exceptions (like ArgumentException from Path.GetInvalidPathChars violations) rather than catching all exceptions.

Consider catching only the specific exceptions that GetAbsolutePath is documented to throw (ArgumentException based on the XML documentation), and let unexpected exceptions propagate. This makes debugging easier and prevents masking programming errors.

Copilot uses AI. Check for mistakes.
}
Comment on lines +53 to +65
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new public API method TryGetAbsolutePath lacks test coverage. While there are existing tests for GetAbsolutePath in TaskEnvironment_Tests.cs, there are no tests for the new Try* variant.

Tests should be added to verify:

  1. Successful path conversion returns true with correct absolutePath
  2. Invalid path input returns false with default absolutePath
  3. Both Stub and Multithreaded environment types work correctly

Copilot uses AI. Check for mistakes.

/// <summary>
/// Gets the value of an environment variable.
/// </summary>
Expand Down Expand Up @@ -77,4 +102,4 @@ public AbsolutePath ProjectDirectory
/// </summary>
internal void Dispose() => _driver.Dispose();
}
}
}
Loading