-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Multithreading migration: Group 6 — 3 attribute-only + 2 Pattern B tasks #53117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
422c397
e8f9b03
974db65
fac971c
0784270
9e0fc69
ffd3e45
c19ea45
78a2816
a9e0390
166837f
49ccdc3
2dc9975
6a5059d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| // Provides a default TaskEnvironment for single-threaded MSBuild execution. | ||
| // When MSBuild supports IMultiThreadableTask, it sets TaskEnvironment directly. | ||
| // This fallback ensures tasks work with older MSBuild versions that do not set it. | ||
|
|
||
| #if NETFRAMEWORK | ||
|
|
||
| using System; | ||
|
|
||
| namespace Microsoft.Build.Framework | ||
| { | ||
| internal static class TaskEnvironmentDefaults | ||
| { | ||
| /// <summary> | ||
| /// Creates a default TaskEnvironment backed by the current process environment. | ||
| /// Uses Environment.CurrentDirectory as the project directory, which in single-threaded | ||
| /// MSBuild is set to the project directory before task execution. | ||
| /// </summary> | ||
| internal static TaskEnvironment Create() => | ||
| new TaskEnvironment(new ProcessTaskEnvironmentDriver(Environment.CurrentDirectory)); | ||
| } | ||
| } | ||
|
|
||
| #endif |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| // 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 | ||
| { | ||
| [Collection("CWD-Dependent")] | ||
|
|
||
| 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 | ||
| { | ||
| 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"); | ||
| task1.ProcessedContentItems.Length.Should().Be(task2.ProcessedContentItems.Length, | ||
| "ProcessedContentItems count should be identical regardless of CWD"); | ||
| task1.CopyLocalItems.Length.Should().Be(task2.CopyLocalItems.Length, | ||
| "CopyLocalItems count should be identical regardless of CWD"); | ||
| task1.FileWrites.Length.Should().Be(task2.FileWrites.Length, | ||
| "FileWrites count should be identical regardless of CWD"); | ||
| } | ||
| 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,123 @@ | ||
| // 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 | ||
| { | ||
| [Collection("CWD-Dependent")] | ||
|
|
||
| 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 = Path.Combine("obj", "project.assets.json"), | ||
| 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
|
||
| 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"); | ||
| task1.ResolvedAssets.Length.Should().Be(task2.ResolvedAssets.Length, | ||
| "ResolvedAssets count should be identical regardless of CWD"); | ||
| } | ||
| finally | ||
| { | ||
| Directory.SetCurrentDirectory(savedCwd); | ||
| Directory.Delete(projectDir, true); | ||
| if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.