Multithreading migration: Group 6 — 3 attribute-only + 2 Pattern B tasks#53117
Multithreading migration: Group 6 — 3 attribute-only + 2 Pattern B tasks#53117SimaTian wants to merge 14 commits intodotnet:mainfrom
Conversation
Tasks: GetDefaultPlatformTargetForNetFramework, GetEmbeddedApphostPaths, GetNuGetShortFolderName, ProduceContentAssets, ResolveCopyLocalAssets. These tasks already have [MSBuildMultiThreadableTask] attribute. This commit adds TDD tests verifying attribute presence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…(Group 6) Both tasks perform file I/O and were incorrectly treated as attribute-only. ProduceContentAssets: Add [MSBuildMultiThreadableTask], IMultiThreadableTask, TaskEnvironment. Absolutize ContentPreprocessorOutputDirectory via TaskEnvironment.GetAbsolutePath(). ResolveCopyLocalAssets: Add [MSBuildMultiThreadableTask], IMultiThreadableTask, TaskEnvironment. Absolutize AssetsFilePath via TaskEnvironment.GetAbsolutePath(). Add attribute-presence tests for all 5 Group 6 tasks. Add parity test files for the two migrated tasks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…up 6) - Add TaskEnvironment to ResolveCopyLocalAssets_WithMinimalAssetsFile_Succeeds test - Absolutize contentFile.ItemSpec before passing to AssetPreprocessor.Process() Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Migrates a set of MSBuild tasks in Microsoft.NET.Build.Tasks to be safe under multithreaded MSBuild execution by marking them multi-threadable and (for Pattern B) resolving file-system paths via TaskEnvironment instead of process-wide CWD assumptions.
Changes:
- Mark
ProduceContentAssetsandResolveCopyLocalAssetsas[MSBuildMultiThreadableTask]and implementIMultiThreadableTask, usingTaskEnvironment.GetAbsolutePath(...)before file I/O. - Add a new consolidated unit test file covering attribute presence, behavior, and parallel execution smoke tests for the 5 tasks.
- Add CWD-independence parity tests for the two Pattern B tasks.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Tasks/Microsoft.NET.Build.Tasks/ProduceContentAssets.cs | Implements Pattern B migration and uses TaskEnvironment to absolutize output directory and content file paths. |
| src/Tasks/Microsoft.NET.Build.Tasks/ResolveCopyLocalAssets.cs | Implements Pattern B migration and uses TaskEnvironment to absolutize AssetsFilePath before lock file load. |
| src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup6.cs | Adds attribute presence, behavioral coverage, and parallel execution smoke tests for the group. |
| src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentAssetsMultiThreading.cs | Adds CWD-independence test coverage for ProduceContentAssets. |
| src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveCopyLocalAssetsMultiThreading.cs | Adds CWD-independence test coverage for ResolveCopyLocalAssets. |
Comments suppressed due to low confidence (1)
src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup6.cs:521
- The
Barrier.SignalAndWait()is inside atry. If any iteration throws before reaching the barrier, the remaining iterations can block indefinitely and hang the test run. Consider ensuring the barrier is always signaled (e.g., viafinally/timeout) or using a coordination primitive that won’t deadlock the runner.
var barrier = new Barrier(parallelism);
Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
{
try
| { | ||
| var lockFileCache = new LockFileCache(this); | ||
| LockFile lockFile = lockFileCache.GetLockFile(AssetsFilePath); | ||
| LockFile lockFile = lockFileCache.GetLockFile(TaskEnvironment.GetAbsolutePath(AssetsFilePath)); | ||
| HashSet<PackageIdentity> packagestoBeFiltered = null; |
There was a problem hiding this comment.
TaskEnvironment can be null when this task is executed under MSBuild versions that don't populate IMultiThreadableTask.TaskEnvironment (or if invoked directly in-process). Calling TaskEnvironment.GetAbsolutePath(AssetsFilePath) will then throw NullReferenceException, and it can also change the error behavior for empty/null AssetsFilePath (throwing ArgumentException before LockFileCache produces the intended BuildErrorException). Consider falling back to the original AssetsFilePath when TaskEnvironment is null and only absolutizing when it’s non-null/non-empty so existing validation/messages remain consistent.
| } | ||
|
|
||
| AssetPreprocessor.ConfigurePreprocessor(ContentPreprocessorOutputDirectory, preprocessorValues); | ||
| AssetPreprocessor.ConfigurePreprocessor(TaskEnvironment.GetAbsolutePath(ContentPreprocessorOutputDirectory), preprocessorValues); |
There was a problem hiding this comment.
TaskEnvironment may be null at runtime (e.g., older MSBuild that doesn't set it). Unconditionally calling TaskEnvironment.GetAbsolutePath(ContentPreprocessorOutputDirectory) can cause a NullReferenceException and break existing behavior where relative paths were resolved against the process CWD. Consider guarding on TaskEnvironment != null (and leaving the original string when it’s null) to preserve backwards compatibility.
| AssetPreprocessor.ConfigurePreprocessor(TaskEnvironment.GetAbsolutePath(ContentPreprocessorOutputDirectory), preprocessorValues); | |
| AssetPreprocessor.ConfigurePreprocessor( | |
| TaskEnvironment != null | |
| ? TaskEnvironment.GetAbsolutePath(ContentPreprocessorOutputDirectory) | |
| : ContentPreprocessorOutputDirectory, | |
| preprocessorValues); |
| private void ProduceContentAsset(ITaskItem contentFile) | ||
| { | ||
| string resolvedPath = contentFile.ItemSpec; | ||
| string resolvedPath = TaskEnvironment.GetAbsolutePath(contentFile.ItemSpec); |
There was a problem hiding this comment.
TaskEnvironment.GetAbsolutePath(contentFile.ItemSpec) will throw if TaskEnvironment is null (older MSBuild) and also changes behavior compared to the previous code path that used ItemSpec directly (resolved relative to process CWD). To keep compatibility, consider using contentFile.ItemSpec when TaskEnvironment is null and only absolutizing when it’s available.
| string resolvedPath = TaskEnvironment.GetAbsolutePath(contentFile.ItemSpec); | |
| string resolvedPath = TaskEnvironment != null | |
| ? TaskEnvironment.GetAbsolutePath(contentFile.ItemSpec) | |
| : contentFile.ItemSpec; |
| var task = new ResolveCopyLocalAssets | ||
| { | ||
| BuildEngine = new MockBuildEngine(), | ||
| AssetsFilePath = "obj\\project.assets.json", |
There was a problem hiding this comment.
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.
| AssetsFilePath = "obj\\project.assets.json", | |
| AssetsFilePath = Path.Combine("obj", "project.assets.json"), |
| // --- Multiprocess mode: CWD == projectDir --- | ||
| Directory.SetCurrentDirectory(projectDir); | ||
| var engine1 = new MockBuildEngine(); | ||
| var task1 = new ProduceContentAssets | ||
| { |
There was a problem hiding this comment.
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.
| // --- Multiprocess mode: CWD == projectDir --- | ||
| Directory.SetCurrentDirectory(projectDir); | ||
| var engine1 = new MockBuildEngine(); | ||
| var task1 = new ResolveCopyLocalAssets | ||
| { |
There was a problem hiding this comment.
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.
- Replace Barrier+Parallel.For with ManualResetEventSlim+Task.Run - Use async test methods with Task.WhenAll - Replace Windows path separator with Path.Combine Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ProduceContentAssets now uses TaskEnvironment.GetAbsolutePath() in ProduceContentAsset(). Tests must provide TaskEnvironment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ProduceContentAssets now uses TaskEnvironment.GetAbsolutePath() in ProduceContentAsset(). All 4 behavioral tests must provide TaskEnvironment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
51e3e98 to
ffd3e45
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add TaskEnvironmentDefaults.cs for NETFRAMEWORK lazy-init fallback - Apply lazy-init pattern to ProduceContentAssets, ResolveCopyLocalAssets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The combining constructor AbsolutePath(string, AbsolutePath) used Path.Combine without Path.GetFullPath, leaving '..' segments unresolved. This caused output paths like 'dir\..\ClassLib\...' instead of 'ClassLib\...', breaking string-based path comparisons in downstream MSBuild targets and tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On .NET Framework, ProcessStartInfo defaults to UseShellExecute=true, which prevents EnvironmentVariables from being applied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Normalization is caller's responsibility, matching real MSBuild polyfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Removed 5 attribute-presence tests from GivenAttributeOnlyTasksGroup6.cs (redundant per skill guidelines) - Fixed 8 NullReferenceExceptions in GivenAProduceContentsAssetsTask.cs: existing tests weren't setting TaskEnvironment after ProduceContentAssets migration - Added TaskEnvironment + BuildEngine to all 8 test methods - Made ContentOutputDirectory/PackageRootDirectory absolute to match GetAbsolutePath() output Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tems in parity tests - Concurrent tests now validate per-thread output values (not just exception-free) - Parity tests compare output item counts (ProcessedContentItems, CopyLocalItems, FileWrites, ResolvedAssets) - ProduceContentAssets_EmptyDependencies_Succeeds now sets TaskEnvironment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Temporarily closing due to review-queue bottleneck — one big grouped PR has turned out harder to review than 5 individual task PRs (see #52936 thread). Strategy update: tasks in this PR have been recorded in our internal postponed-tasks backlog and will be re-opened as individual per-task PRs once the current merge-group-2 and merge-group-5 splits are cleared. Note that some tasks in this group (ValidateExecutableReferences, ProcessFrameworkReferences, ResolveTargetingPackAssets) have already been delivered via individual PRs based on OrchardCore profiling priority — see #53942, #53943, #53944, #53945, #53946. |
Summary
Migrate 5 tasks to support multithreaded MSBuild execution. Three tasks are attribute-only (Pattern A), two require full interface-based migration (Pattern B) with
TaskEnvironmentpath absolutization.Tasks Migrated
IMultiThreadableTask,TaskEnvironment; absolutizedContentPreprocessorOutputDirectoryandcontentFile.ItemSpecbefore file I/OIMultiThreadableTask,TaskEnvironment; absolutizedAssetsFilePathbeforeLockFileCache.GetLockFile()Pattern B Details
ProduceContentAssets: Delegates to
NugetContentAssetPreprocessorwhich usesFile.Exists,File.OpenRead,File.WriteAllText,Directory.CreateDirectory. BothContentPreprocessorOutputDirectoryandcontentFile.ItemSpecare absolutized viaTaskEnvironment.GetAbsolutePath()before passing to the preprocessor.ResolveCopyLocalAssets: Calls
LockFileCache.GetLockFile(AssetsFilePath)which enforcesPath.IsPathRooted()and throws if not.AssetsFilePathis absolutized before the call.Tests Added
Files Changed
src/Tasks/Microsoft.NET.Build.Tasks/ProduceContentAssets.cssrc/Tasks/Microsoft.NET.Build.Tasks/ResolveCopyLocalAssets.cssrc/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup6.cs(new)src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProduceContentAssetsMultiThreading.cs(new)src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveCopyLocalAssetsMultiThreading.cs(new)