Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using FluentAssertions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Xunit;

namespace Microsoft.NET.Build.Tasks.UnitTests
{
public class GivenAProduceContentAssetsMultiThreading
{
[Fact]
public void ContentPreprocessorOutputDirectory_IsResolvedRelativeToProjectDirectory()
{
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"produce-mt-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
try
{
// Create a relative output directory under projectDir
var ppOutputDir = Path.Combine(projectDir, "obj", "pp");
Directory.CreateDirectory(ppOutputDir);

var contentFile = new MockTaskItem("path/to/content.cs", new Dictionary<string, string>
{
{ "NuGetPackageId", "MyPackage" },
{ "NuGetPackageVersion", "1.0.0" },
{ "BuildAction", "Compile" },
{ "CodeLanguage", "any" },
{ "CopyToOutput", "false" },
{ "PPOutputPath", "" },
{ "OutputPath", "" }
});

var task = new ProduceContentAssets
{
BuildEngine = new MockBuildEngine(),
ContentFileDependencies = new ITaskItem[] { contentFile },
ContentPreprocessorOutputDirectory = Path.Combine("obj", "pp"),
ProjectLanguage = "C#",
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
};

var result = task.Execute();

result.Should().BeTrue("task should succeed with relative ContentPreprocessorOutputDirectory resolved via TaskEnvironment");
}
finally
{
Directory.Delete(projectDir, true);
}
}

[Fact]
public void ItProducesSameResultsRegardlessOfCwd()
{
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"produce-parity-{Guid.NewGuid():N}"));
var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"produce-decoy-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
Directory.CreateDirectory(otherDir);
var savedCwd = Directory.GetCurrentDirectory();
try
{
var contentFile1 = new MockTaskItem("path/to/content.cs", new Dictionary<string, string>
{
{ "NuGetPackageId", "MyPackage" },
{ "NuGetPackageVersion", "1.0.0" },
{ "BuildAction", "Compile" },
{ "CodeLanguage", "any" },
{ "CopyToOutput", "false" },
{ "PPOutputPath", "" },
{ "OutputPath", "" }
});
var contentFile2 = new MockTaskItem("path/to/content.cs", new Dictionary<string, string>
{
{ "NuGetPackageId", "MyPackage" },
{ "NuGetPackageVersion", "1.0.0" },
{ "BuildAction", "Compile" },
{ "CodeLanguage", "any" },
{ "CopyToOutput", "false" },
{ "PPOutputPath", "" },
{ "OutputPath", "" }
});

var taskEnv = TaskEnvironmentHelper.CreateForTest(projectDir);

// --- Multiprocess mode: CWD == projectDir ---
Directory.SetCurrentDirectory(projectDir);
var engine1 = new MockBuildEngine();
var task1 = new ProduceContentAssets
{
Comment on lines +89 to +93
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

This test mutates the process-wide current directory via Directory.SetCurrentDirectory(...). Because xUnit runs tests in parallel by default, this can race with other tests in the same assembly and cause flakiness. Consider isolating this test in a non-parallelized collection or restructuring to avoid changing CWD.

Copilot uses AI. Check for mistakes.
BuildEngine = engine1,
ContentFileDependencies = new ITaskItem[] { contentFile1 },
ProjectLanguage = "C#",
TaskEnvironment = taskEnv,
};
var result1 = task1.Execute();

// --- Multithreaded mode: CWD == otherDir ---
Directory.SetCurrentDirectory(otherDir);
var engine2 = new MockBuildEngine();
var task2 = new ProduceContentAssets
{
BuildEngine = engine2,
ContentFileDependencies = new ITaskItem[] { contentFile2 },
ProjectLanguage = "C#",
TaskEnvironment = taskEnv,
};
var result2 = task2.Execute();

result1.Should().Be(result2,
"task should return the same result regardless of CWD");
engine1.Errors.Count.Should().Be(engine2.Errors.Count,
"error count should be the same in both environments");
}
finally
{
Directory.SetCurrentDirectory(savedCwd);
Directory.Delete(projectDir, true);
if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using FluentAssertions;
using Microsoft.Build.Framework;
using Xunit;

namespace Microsoft.NET.Build.Tasks.UnitTests
{
public class GivenAResolveCopyLocalAssetsMultiThreading
{
private const string AssetsJson = """
{
"version": 3,
"targets": { ".NETCoreApp,Version=v8.0": {} },
"libraries": {},
"packageFolders": {},
"projectFileDependencyGroups": { ".NETCoreApp,Version=v8.0": [] },
"project": {
"version": "1.0.0",
"frameworks": { "net8.0": {} }
}
}
""";

[Fact]
public void AssetsFilePath_IsResolvedRelativeToProjectDirectory()
{
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"copyloc-mt-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
try
{
var objDir = Path.Combine(projectDir, "obj");
Directory.CreateDirectory(objDir);
File.WriteAllText(Path.Combine(objDir, "project.assets.json"), AssetsJson);

var task = new ResolveCopyLocalAssets
{
BuildEngine = new MockBuildEngine(),
AssetsFilePath = "obj\\project.assets.json",
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

This test uses a Windows-only path separator ("obj\\project.assets.json"). On non-Windows platforms, backslash is a valid filename character rather than a directory separator, so the task won’t find the file and the test will fail. Use Path.Combine("obj", "project.assets.json") (or Path.DirectorySeparatorChar) to build the relative path.

Suggested change
AssetsFilePath = "obj\\project.assets.json",
AssetsFilePath = Path.Combine("obj", "project.assets.json"),

Copilot uses AI. Check for mistakes.
TargetFramework = ".NETCoreApp,Version=v8.0",
RuntimeIdentifier = "",
IsSelfContained = false,
ResolveRuntimeTargets = false,
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
};

var result = task.Execute();

result.Should().BeTrue("task should succeed when assets file is found via TaskEnvironment");
task.ResolvedAssets.Should().BeEmpty("no packages in the lock file means no assets");
}
finally
{
Directory.Delete(projectDir, true);
}
}

[Fact]
public void ItProducesSameResultsInMultiProcessAndMultiThreadedEnvironments()
{
var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"copyloc-parity-{Guid.NewGuid():N}"));
var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"copyloc-decoy-{Guid.NewGuid():N}"));
Directory.CreateDirectory(projectDir);
Directory.CreateDirectory(otherDir);
var savedCwd = Directory.GetCurrentDirectory();
try
{
var objDir = Path.Combine(projectDir, "obj");
Directory.CreateDirectory(objDir);
File.WriteAllText(Path.Combine(objDir, "project.assets.json"), AssetsJson);

var assetsRelPath = Path.Combine("obj", "project.assets.json");
var taskEnv = TaskEnvironmentHelper.CreateForTest(projectDir);

// --- Multiprocess mode: CWD == projectDir ---
Directory.SetCurrentDirectory(projectDir);
var engine1 = new MockBuildEngine();
var task1 = new ResolveCopyLocalAssets
{
Comment on lines +78 to +82
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

This test changes the process-wide current directory (Directory.SetCurrentDirectory). With xUnit’s default parallel execution, this can interfere with unrelated tests and introduce flakiness. Consider putting this test into a non-parallelized collection or avoiding global CWD mutation.

Copilot uses AI. Check for mistakes.
BuildEngine = engine1,
AssetsFilePath = assetsRelPath,
TargetFramework = ".NETCoreApp,Version=v8.0",
RuntimeIdentifier = "",
IsSelfContained = false,
ResolveRuntimeTargets = false,
TaskEnvironment = taskEnv,
};
var result1 = task1.Execute();

// --- Multithreaded mode: CWD == otherDir ---
Directory.SetCurrentDirectory(otherDir);
var engine2 = new MockBuildEngine();
var task2 = new ResolveCopyLocalAssets
{
BuildEngine = engine2,
AssetsFilePath = assetsRelPath,
TargetFramework = ".NETCoreApp,Version=v8.0",
RuntimeIdentifier = "",
IsSelfContained = false,
ResolveRuntimeTargets = false,
TaskEnvironment = taskEnv,
};
var result2 = task2.Execute();

result1.Should().Be(result2,
"task should return the same result regardless of CWD");
engine1.Errors.Count.Should().Be(engine2.Errors.Count,
"error count should be identical in both environments");
}
finally
{
Directory.SetCurrentDirectory(savedCwd);
Directory.Delete(projectDir, true);
if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true);
}
}
}
}
Loading
Loading