From 40dd0fd3575371103c66c02bfd0384e80ac6a8f3 Mon Sep 17 00:00:00 2001 From: Jan Kratochvil Date: Sun, 19 Apr 2026 16:25:20 +0200 Subject: [PATCH 1/4] Enlighten RequiresFramework35SP1Assembly task for multithreaded mode The task computes a single boolean output from in-memory inputs only: no filesystem, environment, process, or AppDomain access, and no shared mutable static state. Per the multithreaded-task-migration skill (Step 1b), the `[MSBuildMultiThreadableTask]` attribute alone is sufficient -- `IMultiThreadableTask` is not implemented because there is no `TaskEnvironment` consumer. Adds baseline unit tests covering each predicate in `Execute`, the `CreateDesktopShortcut` framework-version gate, and the SP1/Net35 sentinel identity short-circuit, since the task previously had no direct coverage. Fixes #13572 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RequiresFramework35SP1Assembly_Tests.cs | 167 ++++++++++++++++++ src/Tasks/RequiresFramework35SP1Assembly.cs | 1 + 2 files changed, 168 insertions(+) create mode 100644 src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs diff --git a/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs new file mode 100644 index 00000000000..c2be24d9b18 --- /dev/null +++ b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs @@ -0,0 +1,167 @@ +// 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.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.UnitTests +{ + public sealed class RequiresFramework35SP1Assembly_Tests + { + private readonly ITestOutputHelper _output; + + public RequiresFramework35SP1Assembly_Tests(ITestOutputHelper output) + { + _output = output; + } + + private RequiresFramework35SP1Assembly CreateTask() + { + return new RequiresFramework35SP1Assembly + { + BuildEngine = new MockEngine(_output), + // SigningManifests defaults to false, which UncheckedSigning() treats as a "requires SP1" signal. + // Default to true so per-predicate tests are not contaminated by it. + SigningManifests = true, + }; + } + + [Fact] + public void Defaults_NoSignalsTriggered() + { + RequiresFramework35SP1Assembly task = CreateTask(); + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeFalse(); + } + + [Fact] + public void ErrorReportUrl_Triggers() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.ErrorReportUrl = "https://example.com/errors"; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + [Fact] + public void CreateDesktopShortcut_OnNet35_Triggers() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.TargetFrameworkVersion = "v3.5"; + task.CreateDesktopShortcut = true; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + [Fact] + public void UncheckedSigning_Triggers() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.SigningManifests = false; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + [Fact] + public void SuiteName_Triggers() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.SuiteName = "MyAppSuite"; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + [Theory] + [InlineData("ReferencedAssemblies")] + [InlineData("Assemblies")] + [InlineData("Files")] + [InlineData("DeploymentManifestEntryPoint")] + [InlineData("EntryPoint")] + public void IncludeHashFalse_OnAnyItemInput_Triggers(string inputName) + { + RequiresFramework35SP1Assembly task = CreateTask(); + ITaskItem item = new TaskItem("some.dll", new Dictionary { { "IncludeHash", "false" } }); + AssignItemInput(task, inputName, item); + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + [Fact] + public void CreateDesktopShortcut_OnNet20_DoesNotTrigger() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.TargetFrameworkVersion = "v2.0"; + task.CreateDesktopShortcut = true; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeFalse(); + } + + [Fact] + public void CreateDesktopShortcut_BareVersionString_Works() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.TargetFrameworkVersion = "3.5"; + task.CreateDesktopShortcut = true; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + [Fact] + public void Sp1AssemblyIdentity_TriggersWithoutIncludeHash() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.Files = [new TaskItem("System.Data.Entity")]; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + [Fact] + public void Net35ClientSentinelIdentity_Triggers() + { + RequiresFramework35SP1Assembly task = CreateTask(); + task.Assemblies = [new TaskItem("Sentinel.v3.5Client")]; + + task.Execute().ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + } + + private static void AssignItemInput(RequiresFramework35SP1Assembly task, string inputName, ITaskItem item) + { + switch (inputName) + { + case "ReferencedAssemblies": + task.ReferencedAssemblies = [item]; + break; + case "Assemblies": + task.Assemblies = [item]; + break; + case "Files": + task.Files = [item]; + break; + case "DeploymentManifestEntryPoint": + task.DeploymentManifestEntryPoint = item; + break; + case "EntryPoint": + task.EntryPoint = item; + break; + default: + throw new System.ArgumentOutOfRangeException(nameof(inputName)); + } + } + } +} diff --git a/src/Tasks/RequiresFramework35SP1Assembly.cs b/src/Tasks/RequiresFramework35SP1Assembly.cs index 391e28041b1..83d9cc827d5 100644 --- a/src/Tasks/RequiresFramework35SP1Assembly.cs +++ b/src/Tasks/RequiresFramework35SP1Assembly.cs @@ -12,6 +12,7 @@ namespace Microsoft.Build.Tasks /// /// This task determines if this project requires VS2008 SP1 assembly. /// + [MSBuildMultiThreadableTask] public sealed class RequiresFramework35SP1Assembly : TaskExtension { #region Fields From 10eecc9a9fc014b74096d6eafc37fbb85e2efd43 Mon Sep 17 00:00:00 2001 From: Jan Kratochvil Date: Sun, 19 Apr 2026 17:15:01 +0200 Subject: [PATCH 2/4] Address review feedback - Drop SigningManifests=true default from CreateTask helper. Split the defaults case into two tests so the bare-default behavior (UncheckedSigning triggers) is documented alongside the signing-enabled no-trigger baseline. Per-predicate tests now set SigningManifests=true explicitly. - Remove `#nullable disable` from the new test file per repo convention for new files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RequiresFramework35SP1Assembly_Tests.cs | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs index c2be24d9b18..1bcbe437d08 100644 --- a/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs +++ b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs @@ -8,8 +8,6 @@ using Shouldly; using Xunit; -#nullable disable - namespace Microsoft.Build.UnitTests { public sealed class RequiresFramework35SP1Assembly_Tests @@ -21,52 +19,51 @@ public RequiresFramework35SP1Assembly_Tests(ITestOutputHelper output) _output = output; } - private RequiresFramework35SP1Assembly CreateTask() - { - return new RequiresFramework35SP1Assembly + private RequiresFramework35SP1Assembly CreateTask() => + new() { BuildEngine = new MockEngine(_output), - // SigningManifests defaults to false, which UncheckedSigning() treats as a "requires SP1" signal. - // Default to true so per-predicate tests are not contaminated by it. - SigningManifests = true, }; - } [Fact] - public void Defaults_NoSignalsTriggered() + public void Defaults_UncheckedSigningTriggers() { + // The task's default SigningManifests=false is itself an "unchecked signing" trigger, + // so the all-defaults case reports RequiresMinimumFramework35SP1=true. RequiresFramework35SP1Assembly task = CreateTask(); task.Execute().ShouldBeTrue(); - task.RequiresMinimumFramework35SP1.ShouldBeFalse(); + task.RequiresMinimumFramework35SP1.ShouldBeTrue(); } [Fact] - public void ErrorReportUrl_Triggers() + public void SigningEnabled_NoOtherSignals_DoesNotTrigger() { RequiresFramework35SP1Assembly task = CreateTask(); - task.ErrorReportUrl = "https://example.com/errors"; + task.SigningManifests = true; task.Execute().ShouldBeTrue(); - task.RequiresMinimumFramework35SP1.ShouldBeTrue(); + task.RequiresMinimumFramework35SP1.ShouldBeFalse(); } [Fact] - public void CreateDesktopShortcut_OnNet35_Triggers() + public void ErrorReportUrl_Triggers() { RequiresFramework35SP1Assembly task = CreateTask(); - task.TargetFrameworkVersion = "v3.5"; - task.CreateDesktopShortcut = true; + task.SigningManifests = true; + task.ErrorReportUrl = "https://example.com/errors"; task.Execute().ShouldBeTrue(); task.RequiresMinimumFramework35SP1.ShouldBeTrue(); } [Fact] - public void UncheckedSigning_Triggers() + public void CreateDesktopShortcut_OnNet35_Triggers() { RequiresFramework35SP1Assembly task = CreateTask(); - task.SigningManifests = false; + task.SigningManifests = true; + task.TargetFrameworkVersion = "v3.5"; + task.CreateDesktopShortcut = true; task.Execute().ShouldBeTrue(); task.RequiresMinimumFramework35SP1.ShouldBeTrue(); @@ -76,6 +73,7 @@ public void UncheckedSigning_Triggers() public void SuiteName_Triggers() { RequiresFramework35SP1Assembly task = CreateTask(); + task.SigningManifests = true; task.SuiteName = "MyAppSuite"; task.Execute().ShouldBeTrue(); @@ -91,7 +89,8 @@ public void SuiteName_Triggers() public void IncludeHashFalse_OnAnyItemInput_Triggers(string inputName) { RequiresFramework35SP1Assembly task = CreateTask(); - ITaskItem item = new TaskItem("some.dll", new Dictionary { { "IncludeHash", "false" } }); + task.SigningManifests = true; + ITaskItem item = new TaskItem("some.dll", new Dictionary { { "IncludeHash", "false" } }); AssignItemInput(task, inputName, item); task.Execute().ShouldBeTrue(); @@ -102,6 +101,7 @@ public void IncludeHashFalse_OnAnyItemInput_Triggers(string inputName) public void CreateDesktopShortcut_OnNet20_DoesNotTrigger() { RequiresFramework35SP1Assembly task = CreateTask(); + task.SigningManifests = true; task.TargetFrameworkVersion = "v2.0"; task.CreateDesktopShortcut = true; @@ -113,6 +113,7 @@ public void CreateDesktopShortcut_OnNet20_DoesNotTrigger() public void CreateDesktopShortcut_BareVersionString_Works() { RequiresFramework35SP1Assembly task = CreateTask(); + task.SigningManifests = true; task.TargetFrameworkVersion = "3.5"; task.CreateDesktopShortcut = true; @@ -124,6 +125,7 @@ public void CreateDesktopShortcut_BareVersionString_Works() public void Sp1AssemblyIdentity_TriggersWithoutIncludeHash() { RequiresFramework35SP1Assembly task = CreateTask(); + task.SigningManifests = true; task.Files = [new TaskItem("System.Data.Entity")]; task.Execute().ShouldBeTrue(); @@ -134,6 +136,7 @@ public void Sp1AssemblyIdentity_TriggersWithoutIncludeHash() public void Net35ClientSentinelIdentity_Triggers() { RequiresFramework35SP1Assembly task = CreateTask(); + task.SigningManifests = true; task.Assemblies = [new TaskItem("Sentinel.v3.5Client")]; task.Execute().ShouldBeTrue(); From 74f22044bb28150dc4ae19097879e02e90146148 Mon Sep 17 00:00:00 2001 From: Jan Kratochvil Date: Tue, 21 Apr 2026 11:35:58 +0200 Subject: [PATCH 3/4] Minor refactor to reduce duplication --- src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs index 1bcbe437d08..eb054b91864 100644 --- a/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs +++ b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs @@ -137,7 +137,7 @@ public void Net35ClientSentinelIdentity_Triggers() { RequiresFramework35SP1Assembly task = CreateTask(); task.SigningManifests = true; - task.Assemblies = [new TaskItem("Sentinel.v3.5Client")]; + AssignItemInput(task, "Assemblies", new TaskItem("Sentinel.v3.5Client")); task.Execute().ShouldBeTrue(); task.RequiresMinimumFramework35SP1.ShouldBeTrue(); From 37df3187198c9bc2d4f800bba06087d9653f4b6e Mon Sep 17 00:00:00 2001 From: Jan Kratochvil Date: Tue, 21 Apr 2026 11:40:28 +0200 Subject: [PATCH 4/4] Minor refactor --- src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs index eb054b91864..dbeb7412dc2 100644 --- a/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs +++ b/src/Tasks.UnitTests/RequiresFramework35SP1Assembly_Tests.cs @@ -126,7 +126,7 @@ public void Sp1AssemblyIdentity_TriggersWithoutIncludeHash() { RequiresFramework35SP1Assembly task = CreateTask(); task.SigningManifests = true; - task.Files = [new TaskItem("System.Data.Entity")]; + AssignItemInput(task, "Files", new TaskItem("System.Data.Entity")); task.Execute().ShouldBeTrue(); task.RequiresMinimumFramework35SP1.ShouldBeTrue();