diff --git a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs index cb9e4b35fdd2..b41598f528f3 100644 --- a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs +++ b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs @@ -103,6 +103,7 @@ public ProcessStartInfo GetProcessStartInfo() var startInfo = new ProcessStartInfo { WorkingDirectory = _projectDirectory.Value, + UseShellExecute = false, }; // Populate environment from the scoped environment dictionary diff --git a/src/Tasks/Common/TaskEnvironmentDefaults.cs b/src/Tasks/Common/TaskEnvironmentDefaults.cs new file mode 100644 index 000000000000..7ef5666a4175 --- /dev/null +++ b/src/Tasks/Common/TaskEnvironmentDefaults.cs @@ -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 + { + /// + /// 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. + /// + internal static TaskEnvironment Create() => + new TaskEnvironment(new ProcessTaskEnvironmentDriver(Environment.CurrentDirectory)); + } +} + +#endif diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAFilterResolvedFilesMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAFilterResolvedFilesMultiThreading.cs new file mode 100644 index 000000000000..3d49577935d4 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAFilterResolvedFilesMultiThreading.cs @@ -0,0 +1,176 @@ +// 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 System.Collections.Concurrent; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection("CWD-Dependent")] + + public class GivenAFilterResolvedFilesMultiThreading + { + 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 FilterResolvedFiles_HasMultiThreadableAttribute() + { + typeof(FilterResolvedFiles).GetCustomAttribute() + .Should().NotBeNull("task must be decorated with [MSBuildMultiThreadableTask]"); + } + + [Fact] + public void AssetsFilePath_IsResolvedRelativeToProjectDirectory() + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"filter-mt-{Guid.NewGuid():N}")); + Directory.CreateDirectory(projectDir); + try + { + var assetsDir = Path.Combine(projectDir, "obj"); + Directory.CreateDirectory(assetsDir); + File.WriteAllText(Path.Combine(assetsDir, "project.assets.json"), AssetsJson); + + var task = new FilterResolvedFiles + { + BuildEngine = new MockBuildEngine(), + AssetsFilePath = Path.Combine("obj", "project.assets.json"), + ResolvedFiles = Array.Empty(), + PackagesToPrune = Array.Empty(), + TargetFramework = ".NETCoreApp,Version=v8.0", + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + }; + + var result = task.Execute(); + result.Should().BeTrue("task should succeed when assets file is found via TaskEnvironment"); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + private static (bool result, MockBuildEngine engine) RunTask( + string assetsRelPath, string projectDir) + { + var engine = new MockBuildEngine(); + var task = new FilterResolvedFiles + { + BuildEngine = engine, + AssetsFilePath = assetsRelPath, + ResolvedFiles = Array.Empty(), + PackagesToPrune = Array.Empty(), + TargetFramework = ".NETCoreApp,Version=v8.0", + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + }; + + var result = task.Execute(); + return (result, engine); + } + + [Fact] + public void ItProducesSameResultsInMultiProcessAndMultiThreadedEnvironments() + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"filter-parity-{Guid.NewGuid():N}")); + var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"filter-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"); + + // --- Multiprocess mode: CWD == projectDir --- + Directory.SetCurrentDirectory(projectDir); + var (mpResult, mpEngine) = RunTask(assetsRelPath, projectDir); + + // --- Multithreaded mode: CWD == otherDir --- + Directory.SetCurrentDirectory(otherDir); + var (mtResult, mtEngine) = RunTask(assetsRelPath, projectDir); + + mpResult.Should().Be(mtResult, + "task should return the same success/failure in both environments"); + mpEngine.Errors.Count.Should().Be(mtEngine.Errors.Count, + "error count should be the same in both environments"); + mpEngine.Warnings.Count.Should().Be(mtEngine.Warnings.Count, + "warning count should be the same in both environments"); + } + 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 FilterResolvedFiles_ConcurrentExecution(int parallelism) + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"filter-concurrent-{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 errors = new ConcurrentBag(); + 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 FilterResolvedFiles + { + BuildEngine = new MockBuildEngine(), + AssetsFilePath = Path.Combine("obj", "project.assets.json"), + ResolvedFiles = Array.Empty(), + PackagesToPrune = Array.Empty(), + TargetFramework = ".NETCoreApp,Version=v8.0", + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + }; + startGate.Wait(); + var result = task.Execute(); + if (!result) errors.Add($"Thread {idx}: Execute returned false"); + } + catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + finally + { + Directory.Delete(projectDir, true); + } + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenASelectRuntimeIdentifierSpecificItems.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenASelectRuntimeIdentifierSpecificItems.cs index c6dea601a9ff..0bdc931d5706 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenASelectRuntimeIdentifierSpecificItems.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenASelectRuntimeIdentifierSpecificItems.cs @@ -28,7 +28,8 @@ public void ItSelectsCompatibleItems() TargetRuntimeIdentifier = "ubuntu.18.04-x64", Items = items, RuntimeIdentifierGraphPath = testRuntimeGraphPath, - BuildEngine = new MockBuildEngine() + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; // Act @@ -59,7 +60,8 @@ public void ItSelectsItemsWithExactMatch() TargetRuntimeIdentifier = "win-x64", Items = items, RuntimeIdentifierGraphPath = testRuntimeGraphPath, - BuildEngine = new MockBuildEngine() + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; // Act @@ -88,7 +90,8 @@ public void ItSkipsItemsWithoutRuntimeIdentifierMetadata() TargetRuntimeIdentifier = "linux-x64", Items = items, RuntimeIdentifierGraphPath = testRuntimeGraphPath, - BuildEngine = new MockBuildEngine() + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; // Act @@ -114,7 +117,8 @@ public void ItUsesCustomRuntimeIdentifierMetadata() Items = new[] { item }, RuntimeIdentifierItemMetadata = "CustomRID", RuntimeIdentifierGraphPath = testRuntimeGraphPath, - BuildEngine = new MockBuildEngine() + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; // Act @@ -137,7 +141,8 @@ public void ItReturnsEmptyArrayWhenNoItemsProvided() TargetRuntimeIdentifier = "linux-x64", Items = new ITaskItem[0], RuntimeIdentifierGraphPath = testRuntimeGraphPath, - BuildEngine = new MockBuildEngine() + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; // Act diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup7.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup7.cs new file mode 100644 index 000000000000..439bbcca5979 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAttributeOnlyTasksGroup7.cs @@ -0,0 +1,512 @@ +// 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 System.Collections.Concurrent; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + /// + /// Behavioral tests for attribute-only tasks in merge-group-7. + /// These tasks received only the [MSBuildMultiThreadableTask] attribute — no source + /// code changes — so we verify they still produce correct results. + /// + [Collection("CWD-Dependent")] + + public class GivenAttributeOnlyTasksGroup7 + { + #region Attribute Presence + + [Fact] + public void SelectRuntimeIdentifierSpecificItems_HasMultiThreadableAttribute() + { + typeof(SelectRuntimeIdentifierSpecificItems).GetCustomAttribute() + .Should().NotBeNull("task must be decorated with [MSBuildMultiThreadableTask]"); + } + + [Fact] + public void SetGeneratedAppConfigMetadata_HasMultiThreadableAttribute() + { + typeof(SetGeneratedAppConfigMetadata).GetCustomAttribute() + .Should().NotBeNull("task must be decorated with [MSBuildMultiThreadableTask]"); + } + + [Fact] + public void ValidateExecutableReferences_HasMultiThreadableAttribute() + { + typeof(ValidateExecutableReferences).GetCustomAttribute() + .Should().NotBeNull("task must be decorated with [MSBuildMultiThreadableTask]"); + } + + [Fact] + public void RemoveDuplicatePackageReferences_HasMultiThreadableAttribute() + { + typeof(RemoveDuplicatePackageReferences).GetCustomAttribute() + .Should().NotBeNull("task must be decorated with [MSBuildMultiThreadableTask]"); + } + + #endregion + + #region SelectRuntimeIdentifierSpecificItems (parity test) + + [Fact] + public void SelectRuntimeIdentifierSpecificItems_ProducesSameResultsRegardlessOfCwd() + { + // This parity test verifies the task behaves identically + // when CWD is projectDir vs a different directory. + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"select-rid-parity-{Guid.NewGuid():N}")); + var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"select-rid-decoy-{Guid.NewGuid():N}")); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(otherDir); + var savedCwd = Directory.GetCurrentDirectory(); + try + { + var runtimeGraphJson = @"{ + ""runtimes"": { + ""linux"": {}, + ""linux-x64"": { ""#import"": [""linux""] }, + ""win"": {}, + ""win-x64"": { ""#import"": [""win""] } + } + }"; + var graphPath = Path.Combine(projectDir, "runtime.json"); + File.WriteAllText(graphPath, runtimeGraphJson); + + var items = new[] + { + CreateItemWithRid("Item1", "linux-x64"), + CreateItemWithRid("Item2", "win-x64"), + CreateItemWithRid("Item3", "linux") + }; + + // --- Run with CWD = projectDir --- + Directory.SetCurrentDirectory(projectDir); + var (cwdResult, cwdSelected) = RunSelectRidTask("linux-x64", items, graphPath, projectDir); + + // --- Run with CWD = otherDir --- + Directory.SetCurrentDirectory(otherDir); + var (otherResult, otherSelected) = RunSelectRidTask("linux-x64", items, graphPath, projectDir); + + cwdResult.Should().Be(otherResult, "task should return same result regardless of CWD"); + cwdSelected.Length.Should().Be(otherSelected.Length, "same number of items should be selected"); + + // Both should select linux-x64 and linux, but NOT win-x64 + cwdSelected.Should().HaveCount(2); + cwdSelected.Should().Contain(i => i.ItemSpec == "Item1"); // linux-x64 + cwdSelected.Should().Contain(i => i.ItemSpec == "Item3"); // linux + } + finally + { + Directory.SetCurrentDirectory(savedCwd); + Directory.Delete(projectDir, true); + if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); + } + } + + private static (bool result, ITaskItem[] selected) RunSelectRidTask( + string targetRid, ITaskItem[] items, string graphPath, string projectDir) + { + var task = new SelectRuntimeIdentifierSpecificItems + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + TargetRuntimeIdentifier = targetRid, + Items = items, + RuntimeIdentifierGraphPath = graphPath + }; + var result = task.Execute(); + return (result, task.SelectedItems ?? Array.Empty()); + } + + private static TaskItem CreateItemWithRid(string itemSpec, string rid) + { + var item = new TaskItem(itemSpec); + item.SetMetadata("RuntimeIdentifier", rid); + return item; + } + + #endregion + + #region SetGeneratedAppConfigMetadata + + [Fact] + public void SetGeneratedAppConfigMetadata_WithNoSourceAppConfig_SetsTargetPath() + { + var task = new SetGeneratedAppConfigMetadata + { + BuildEngine = new MockBuildEngine(), + GeneratedAppConfigFile = "obj/myapp.exe.config", + TargetName = "myapp.exe.config" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.OutputAppConfigFileWithMetadata.Should().NotBeNull(); + task.OutputAppConfigFileWithMetadata.ItemSpec.Should().Be("obj/myapp.exe.config"); + task.OutputAppConfigFileWithMetadata.GetMetadata("TargetPath").Should().Be("myapp.exe.config"); + } + + [Fact] + public void SetGeneratedAppConfigMetadata_WithSourceAppConfig_CopiesMetadata() + { + var sourceConfig = new MockTaskItem("app.config", new Dictionary + { + { "Link", "linked/app.config" }, + { "TargetPath", "myapp.exe.config" } + }); + + var task = new SetGeneratedAppConfigMetadata + { + BuildEngine = new MockBuildEngine(), + AppConfigFile = sourceConfig, + GeneratedAppConfigFile = "obj/myapp.exe.config", + TargetName = "myapp.exe.config" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.OutputAppConfigFileWithMetadata.Should().NotBeNull(); + task.OutputAppConfigFileWithMetadata.ItemSpec.Should().Be("obj/myapp.exe.config"); + // Source metadata should be copied to the output + task.OutputAppConfigFileWithMetadata.GetMetadata("TargetPath").Should().Be("myapp.exe.config"); + } + + [Fact] + public void SetGeneratedAppConfigMetadata_OutputItemSpecIsGeneratedPath() + { + var task = new SetGeneratedAppConfigMetadata + { + BuildEngine = new MockBuildEngine(), + GeneratedAppConfigFile = "generated/config.xml", + TargetName = "output.exe.config" + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.OutputAppConfigFileWithMetadata.ItemSpec.Should().Be("generated/config.xml", + "the output item should use the generated file path as its ItemSpec"); + } + + #endregion + + #region ValidateExecutableReferences + + [Fact] + public void ValidateExecutableReferences_NonExecutableProject_SkipsValidation() + { + var task = new ValidateExecutableReferences + { + BuildEngine = new MockBuildEngine(), + IsExecutable = false, + SelfContained = true, + ReferencedProjects = new ITaskItem[] + { + new TaskItem("SomeProject.csproj") + } + }; + + var result = task.Execute(); + + result.Should().BeTrue("non-executable projects should skip validation entirely"); + } + + [Fact] + public void ValidateExecutableReferences_NoReferencedProjects_Succeeds() + { + var task = new ValidateExecutableReferences + { + BuildEngine = new MockBuildEngine(), + IsExecutable = true, + SelfContained = false, + ReferencedProjects = Array.Empty() + }; + + var result = task.Execute(); + + result.Should().BeTrue("no references means nothing to validate"); + } + + [Fact] + public void ValidateExecutableReferences_ProjectWithoutNearestTfm_SkipsProject() + { + // Projects without NearestTargetFramework metadata (e.g., C++ projects) + // should be silently skipped + var project = new MockTaskItem("NativeProject.vcxproj", new Dictionary()); + + var task = new ValidateExecutableReferences + { + BuildEngine = new MockBuildEngine(), + IsExecutable = true, + SelfContained = false, + ReferencedProjects = new ITaskItem[] { project } + }; + + var result = task.Execute(); + + result.Should().BeTrue("projects without NearestTargetFramework should be skipped"); + } + + #endregion + + #region RemoveDuplicatePackageReferences + + [Fact] + public void RemoveDuplicatePackageReferences_RemovesDuplicates() + { + var packages = new ITaskItem[] + { + new MockTaskItem("MyPackage", new Dictionary { { "Version", "1.0.0" } }), + new MockTaskItem("MyPackage", new Dictionary { { "Version", "1.0.0" } }), + new MockTaskItem("OtherPackage", new Dictionary { { "Version", "2.0.0" } }) + }; + + var task = new RemoveDuplicatePackageReferences + { + BuildEngine = new MockBuildEngine(), + InputPackageReferences = packages + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.UniquePackageReferences.Should().HaveCount(2); + task.UniquePackageReferences.Should().Contain(i => i.ItemSpec == "MyPackage"); + task.UniquePackageReferences.Should().Contain(i => i.ItemSpec == "OtherPackage"); + } + + [Fact] + public void RemoveDuplicatePackageReferences_PreservesVersionMetadata() + { + var packages = new ITaskItem[] + { + new MockTaskItem("MyPackage", new Dictionary { { "Version", "3.5.1" } }) + }; + + var task = new RemoveDuplicatePackageReferences + { + BuildEngine = new MockBuildEngine(), + InputPackageReferences = packages + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.UniquePackageReferences.Should().HaveCount(1); + task.UniquePackageReferences[0].GetMetadata("Version").Should().Be("3.5.1"); + } + + [Fact] + public void RemoveDuplicatePackageReferences_DifferentVersionsAreNotDuplicates() + { + var packages = new ITaskItem[] + { + new MockTaskItem("MyPackage", new Dictionary { { "Version", "1.0.0" } }), + new MockTaskItem("MyPackage", new Dictionary { { "Version", "2.0.0" } }) + }; + + var task = new RemoveDuplicatePackageReferences + { + BuildEngine = new MockBuildEngine(), + InputPackageReferences = packages + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.UniquePackageReferences.Should().HaveCount(2, + "same package with different versions are distinct identities"); + } + + [Fact] + public void RemoveDuplicatePackageReferences_SinglePackage_PassesThrough() + { + var packages = new ITaskItem[] + { + new MockTaskItem("SinglePackage", new Dictionary { { "Version", "1.0.0" } }) + }; + + var task = new RemoveDuplicatePackageReferences + { + BuildEngine = new MockBuildEngine(), + InputPackageReferences = packages + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.UniquePackageReferences.Should().HaveCount(1); + task.UniquePackageReferences[0].ItemSpec.Should().Be("SinglePackage"); + } + + #endregion + + // FilterResolvedFiles parity test removed — it belongs in GivenAFilterResolvedFilesMultiThreading.cs + // (FilterResolvedFiles is Pattern B, not attribute-only). The duplicate here also had a bug: + // it created FilterResolvedFiles without setting TaskEnvironment, causing NullReferenceException. + + #region Concurrent Execution + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task SelectRuntimeIdentifierSpecificItems_ConcurrentExecution(int parallelism) + { + var projectDir = Path.Combine(Path.GetTempPath(), $"select-rid-concurrent-{Guid.NewGuid():N}"); + Directory.CreateDirectory(projectDir); + var runtimeGraphPath = Path.Combine(projectDir, "runtime.json"); + File.WriteAllText(runtimeGraphPath, @"{ ""runtimes"": { ""linux-x64"": { ""#import"": [""linux"", ""unix""] } } }"); + try + { + var errors = new ConcurrentBag(); + 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 SelectRuntimeIdentifierSpecificItems + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + TargetRuntimeIdentifier = "linux-x64", + Items = new ITaskItem[] + { + CreateItemWithRid($"Item{idx}", "linux-x64") + }, + RuntimeIdentifierGraphPath = "runtime.json" + }; + startGate.Wait(); + var result = task.Execute(); + if (!result) errors.Add($"Thread {idx}: Execute returned false"); + } + catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + finally + { + Directory.Delete(projectDir, true); + } + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task SetGeneratedAppConfigMetadata_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + 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 SetGeneratedAppConfigMetadata + { + BuildEngine = new MockBuildEngine(), + GeneratedAppConfigFile = $"obj/app{idx}.exe.config", + TargetName = $"app{idx}.exe.config" + }; + 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(); + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ValidateExecutableReferences_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + 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 ValidateExecutableReferences + { + BuildEngine = new MockBuildEngine(), + IsExecutable = false, + SelfContained = false, + ReferencedProjects = Array.Empty() + }; + 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(); + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task RemoveDuplicatePackageReferences_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + 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 RemoveDuplicatePackageReferences + { + BuildEngine = new MockBuildEngine(), + InputPackageReferences = new ITaskItem[] + { + new MockTaskItem($"Package{idx}", new Dictionary { { "Version", "1.0.0" } }) + } + }; + 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(); + } + + #endregion + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs index dc383cfdeaa0..1076e8169314 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Globalization; +using System.Reflection; using System.Text.RegularExpressions; using FluentAssertions; using Xunit; @@ -95,7 +96,8 @@ public void ThereAreNoGapsDuplicatesOrIncorrectlyFormattedCodes() [Fact] public void ResxIsCommentedWithCorrectStrBegin() { - var doc = XDocument.Load("Strings.resx"); + var resxPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Strings.resx"); + var doc = XDocument.Load(resxPath); var ns = doc.Root.Name.Namespace; foreach (var data in doc.Root.Elements(ns + "data")) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/FilterResolvedFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks/FilterResolvedFiles.cs index a58f1e59c578..5c7fdbde6475 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/FilterResolvedFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/FilterResolvedFiles.cs @@ -13,11 +13,23 @@ namespace Microsoft.NET.Build.Tasks /// /// Filters out the assemblies from the list based on a given package closure. /// - public class FilterResolvedFiles : TaskBase + [MSBuildMultiThreadableTask] + public class FilterResolvedFiles : TaskBase, IMultiThreadableTask { private readonly List _assembliesToPublish = new(); private readonly List _packagesResolved = new(); +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } +#endif + public string AssetsFilePath { get; set; } [Required] @@ -52,7 +64,7 @@ public ITaskItem[] PublishedPackages protected override void ExecuteCore() { var lockFileCache = new LockFileCache(this); - LockFile lockFile = lockFileCache.GetLockFile(AssetsFilePath); + LockFile lockFile = lockFileCache.GetLockFile(TaskEnvironment.GetAbsolutePath(AssetsFilePath)); ProjectContext projectContext = lockFile.CreateProjectContext( TargetFramework, diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/SelectRuntimeIdentifierSpecificItems.cs b/src/Tasks/Microsoft.NET.Build.Tasks/SelectRuntimeIdentifierSpecificItems.cs index f3995157dfb9..f5568d959ab7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/SelectRuntimeIdentifierSpecificItems.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/SelectRuntimeIdentifierSpecificItems.cs @@ -11,8 +11,20 @@ namespace Microsoft.NET.Build.Tasks; /// This task filters an Item list by those items that contain a specific Metadata that is /// compatible with a specified Runtime Identifier, according to a given RuntimeIdentifierGraph file. /// -public class SelectRuntimeIdentifierSpecificItems : TaskBase +[MSBuildMultiThreadableTask] +public class SelectRuntimeIdentifierSpecificItems : TaskBase, IMultiThreadableTask { +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } = null!; +#endif + /// /// The target runtime identifier to check compatibility against. /// @@ -52,7 +64,7 @@ protected override void ExecuteCore() string ridMetadata = RuntimeIdentifierItemMetadata ?? "RuntimeIdentifier"; - RuntimeGraph runtimeGraph = new RuntimeGraphCache(this).GetRuntimeGraph(RuntimeIdentifierGraphPath); + RuntimeGraph runtimeGraph = new RuntimeGraphCache(this).GetRuntimeGraph(TaskEnvironment.GetAbsolutePath(RuntimeIdentifierGraphPath)); var selectedItems = new List(); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/SetGeneratedAppConfigMetadata.cs b/src/Tasks/Microsoft.NET.Build.Tasks/SetGeneratedAppConfigMetadata.cs index 2760c0909ba0..084548a03f0b 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/SetGeneratedAppConfigMetadata.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/SetGeneratedAppConfigMetadata.cs @@ -8,6 +8,7 @@ namespace Microsoft.NET.Build.Tasks { + [MSBuildMultiThreadableTask] public sealed class SetGeneratedAppConfigMetadata : TaskBase { /// diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs index 09fa432633fa..8ad75727e58c 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs @@ -9,6 +9,7 @@ namespace Microsoft.NET.Build.Tasks { + [MSBuildMultiThreadableTask] public class ValidateExecutableReferences : TaskBase { public bool SelfContained { get; set; }