-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Multithreading migration: Group 11 — 5 Framework & Environment tasks (Pattern B) #53122
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
4b60bed
be29117
1aa3a28
0af0c18
691534b
ba123d2
7261071
edf0de7
7c2e2d4
323495b
fc0013e
ad94f20
fad0a41
7b8e1ba
aa47a0e
3034b17
b4a5e1a
8180bff
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,145 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections.Concurrent; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using FluentAssertions; | ||
| using Microsoft.Build.Framework; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.NET.Build.Tasks.UnitTests | ||
| { | ||
| [Collection("CWD-Dependent")] | ||
|
|
||
| public class GivenAProcessFrameworkReferencesMultiThreading | ||
| { | ||
| [Fact] | ||
| public void EmptyFrameworkReferences_DoesNotCrash() | ||
| { | ||
| var task = new ProcessFrameworkReferences | ||
| { | ||
| BuildEngine = new MockBuildEngine(), | ||
| TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), | ||
| TargetFrameworkVersion = "8.0", | ||
| TargetingPackRoot = "", | ||
| RuntimeGraphPath = "", | ||
| FrameworkReferences = Array.Empty<ITaskItem>(), | ||
| KnownFrameworkReferences = Array.Empty<ITaskItem>(), | ||
| KnownRuntimePacks = Array.Empty<ITaskItem>(), | ||
| KnownCrossgen2Packs = Array.Empty<ITaskItem>(), | ||
| KnownILCompilerPacks = Array.Empty<ITaskItem>(), | ||
| KnownILLinkPacks = Array.Empty<ITaskItem>(), | ||
| KnownWebAssemblySdkPacks = Array.Empty<ITaskItem>(), | ||
| }; | ||
|
|
||
| // Should not throw; may return true (nothing to process) or false (missing required props) | ||
| var act = () => task.Execute(); | ||
| act.Should().NotThrow("empty framework references should be handled gracefully"); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ProjectDirectory_UsedInsteadOfEnvironmentCurrentDirectory() | ||
| { | ||
| // Verify that the task uses TaskEnvironment.ProjectDirectory for global.json search | ||
| // instead of Environment.CurrentDirectory | ||
| var projectDir = Path.Combine(Path.GetTempPath(), "pfr-mt-" + Guid.NewGuid().ToString("N")); | ||
| var otherDir = Path.Combine(Path.GetTempPath(), "pfr-decoy-" + Guid.NewGuid().ToString("N")); | ||
| Directory.CreateDirectory(projectDir); | ||
| Directory.CreateDirectory(otherDir); | ||
| var savedCwd = Directory.GetCurrentDirectory(); | ||
|
|
||
| try | ||
| { | ||
| // Multiprocess mode: CWD == projectDir | ||
| Directory.SetCurrentDirectory(projectDir); | ||
| var (result1, engine1) = RunTask(projectDir); | ||
|
|
||
| // Multithreaded mode: CWD == otherDir | ||
| Directory.SetCurrentDirectory(otherDir); | ||
| var (result2, engine2) = RunTask(projectDir); | ||
|
|
||
| // Both should produce the same result | ||
| result1.Should().Be(result2, | ||
| "task should return the same result regardless of CWD"); | ||
| engine1.Errors.Count.Should().Be(engine2.Errors.Count, | ||
| "error count should match between multiprocess and multithreaded modes"); | ||
| engine1.Warnings.Count.Should().Be(engine2.Warnings.Count, | ||
| "warning count should match between multiprocess and multithreaded modes"); | ||
| } | ||
| finally | ||
| { | ||
| Directory.SetCurrentDirectory(savedCwd); | ||
| Directory.Delete(projectDir, true); | ||
| if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); | ||
| } | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData(4)] | ||
| [InlineData(16)] | ||
| public async System.Threading.Tasks.Task ProcessFrameworkReferences_ConcurrentExecution(int parallelism) | ||
| { | ||
| var errors = new ConcurrentBag<string>(); | ||
| using var startGate = new ManualResetEventSlim(false); | ||
| var tasks = new System.Threading.Tasks.Task[parallelism]; | ||
| for (int i = 0; i < parallelism; i++) | ||
| { | ||
| int idx = i; | ||
| tasks[idx] = System.Threading.Tasks.Task.Run(() => | ||
| { | ||
| try | ||
| { | ||
| var task = new ProcessFrameworkReferences | ||
| { | ||
| BuildEngine = new MockBuildEngine(), | ||
| TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), | ||
| TargetFrameworkVersion = "8.0", | ||
| TargetingPackRoot = "", | ||
| RuntimeGraphPath = "", | ||
| FrameworkReferences = Array.Empty<ITaskItem>(), | ||
| KnownFrameworkReferences = Array.Empty<ITaskItem>(), | ||
| KnownRuntimePacks = Array.Empty<ITaskItem>(), | ||
| KnownCrossgen2Packs = Array.Empty<ITaskItem>(), | ||
| KnownILCompilerPacks = Array.Empty<ITaskItem>(), | ||
| KnownILLinkPacks = Array.Empty<ITaskItem>(), | ||
| KnownWebAssemblySdkPacks = Array.Empty<ITaskItem>(), | ||
| }; | ||
| startGate.Wait(); | ||
| task.Execute(); | ||
| } | ||
|
Comment on lines
+83
to
+110
|
||
| catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } | ||
| }); | ||
| } | ||
| startGate.Set(); | ||
| await System.Threading.Tasks.Task.WhenAll(tasks); | ||
|
|
||
| errors.Should().BeEmpty(); | ||
| } | ||
|
|
||
| private static (bool result, MockBuildEngine engine) RunTask(string projectDir) | ||
| { | ||
| var engine = new MockBuildEngine(); | ||
| var task = new ProcessFrameworkReferences | ||
| { | ||
| BuildEngine = engine, | ||
| TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), | ||
| TargetFrameworkVersion = "8.0", | ||
| TargetingPackRoot = "", | ||
| RuntimeGraphPath = "", | ||
| FrameworkReferences = Array.Empty<ITaskItem>(), | ||
| KnownFrameworkReferences = Array.Empty<ITaskItem>(), | ||
| KnownRuntimePacks = Array.Empty<ITaskItem>(), | ||
| KnownCrossgen2Packs = Array.Empty<ITaskItem>(), | ||
| KnownILCompilerPacks = Array.Empty<ITaskItem>(), | ||
| KnownILLinkPacks = Array.Empty<ITaskItem>(), | ||
| KnownWebAssemblySdkPacks = Array.Empty<ITaskItem>(), | ||
| }; | ||
|
|
||
| bool result; | ||
| try { result = task.Execute(); } | ||
| catch { result = false; } | ||
| return (result, engine); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Collections.Concurrent; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using FluentAssertions; | ||
| using Microsoft.Build.Framework; | ||
| using Xunit; | ||
|
|
||
| namespace Microsoft.NET.Build.Tasks.UnitTests | ||
| { | ||
| public class GivenAResolveFrameworkReferencesMultiThreading | ||
| { | ||
| [Fact] | ||
| public void EmptyInputs_DoesNotCrash() | ||
| { | ||
| var task = new ResolveFrameworkReferences | ||
| { | ||
| BuildEngine = new MockBuildEngine(), | ||
| FrameworkReferences = Array.Empty<ITaskItem>(), | ||
| ResolvedTargetingPacks = Array.Empty<ITaskItem>(), | ||
| ResolvedRuntimePacks = Array.Empty<ITaskItem>(), | ||
| }; | ||
|
|
||
| var result = task.Execute(); | ||
|
|
||
| result.Should().BeTrue("empty inputs should succeed with no output"); | ||
| (task.ResolvedFrameworkReferences ?? Array.Empty<ITaskItem>()).Should().BeEmpty(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ResolvesFrameworkReferences_WithMatchingPacks() | ||
| { | ||
| var fwRef = new MockTaskItem("Microsoft.NETCore.App", new Dictionary<string, string>()); | ||
|
|
||
| var targetingPack = new MockTaskItem("Microsoft.NETCore.App", new Dictionary<string, string>()); | ||
| targetingPack.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); | ||
| targetingPack.SetMetadata("NuGetPackageVersion", "8.0.0"); | ||
| targetingPack.SetMetadata("Path", @"C:\packs\targeting"); | ||
|
|
||
| var runtimePack = new MockTaskItem("Microsoft.NETCore.App.Runtime.win-x64", new Dictionary<string, string>()); | ||
| runtimePack.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); | ||
| runtimePack.SetMetadata("NuGetPackageVersion", "8.0.0"); | ||
| runtimePack.SetMetadata("PackageDirectory", @"C:\packs\runtime"); | ||
|
|
||
| var task = new ResolveFrameworkReferences | ||
| { | ||
| BuildEngine = new MockBuildEngine(), | ||
| FrameworkReferences = new ITaskItem[] { fwRef }, | ||
| ResolvedTargetingPacks = new ITaskItem[] { targetingPack }, | ||
| ResolvedRuntimePacks = new ITaskItem[] { runtimePack }, | ||
| }; | ||
|
|
||
| var result = task.Execute(); | ||
|
|
||
| result.Should().BeTrue(); | ||
| task.ResolvedFrameworkReferences.Should().NotBeEmpty(); | ||
| } | ||
| [Theory] | ||
| [InlineData(4)] | ||
| [InlineData(16)] | ||
| public async System.Threading.Tasks.Task ResolveFrameworkReferences_ConcurrentExecution(int parallelism) | ||
| { | ||
| var errors = new ConcurrentBag<string>(); | ||
| using var startGate = new ManualResetEventSlim(false); | ||
| var tasks = new System.Threading.Tasks.Task[parallelism]; | ||
| for (int i = 0; i < parallelism; i++) | ||
| { | ||
| int idx = i; | ||
| tasks[idx] = System.Threading.Tasks.Task.Run(() => | ||
| { | ||
| try | ||
| { | ||
| var task = new ResolveFrameworkReferences | ||
| { | ||
| BuildEngine = new MockBuildEngine(), | ||
| FrameworkReferences = Array.Empty<ITaskItem>(), | ||
| ResolvedTargetingPacks = Array.Empty<ITaskItem>(), | ||
| ResolvedRuntimePacks = Array.Empty<ITaskItem>(), | ||
| }; | ||
| startGate.Wait(); | ||
| task.Execute(); | ||
| } | ||
| catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } | ||
| }); | ||
| } | ||
| startGate.Set(); | ||
| await System.Threading.Tasks.Task.WhenAll(tasks); | ||
|
|
||
| errors.Should().BeEmpty(); | ||
| } | ||
| } | ||
| } |
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.
ProjectDirectory_UsedInsteadOfEnvironmentCurrentDirectorychanges the process CWD, butRunTask()uses emptyFrameworkReferences/known packs. In that configuration,ProcessFrameworkReferencesmay never invoke the code path that readsTaskEnvironment.ProjectDirectoryfor global.json resolution, so the test may not validate the intended behavior. Consider supplying the minimal inputs needed to force workload resolver/global.json lookup, and isolate theDirectory.SetCurrentDirectoryusage from xUnit parallel execution.