Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e5763fd
Migrate manifest-handling tasks to TaskEnvironment API
JanProvaznik Feb 2, 2026
96ef4a4
Add path canonicalization and TaskEnvironment tests
JanProvaznik Feb 4, 2026
5d24c3b
Merge branch 'main' into resource-manifests-migration
JanProvaznik Feb 10, 2026
b2876ad
Fix compatibility issues: preserve original paths in error messages a…
JanProvaznik Feb 10, 2026
f6d9071
Address PR review: AbsolutePath types, GetCanonicalForm, remove unuse…
JanProvaznik Feb 10, 2026
6d3a74b
Deduplicate skill file: merge Sin 5+7, unify checklists, trim redunda…
JanProvaznik Feb 10, 2026
86f7785
Remove skill file changes (extracted to separate PR)
JanProvaznik Feb 10, 2026
ccbec40
Merge remote-tracking branch 'origin/main' into resource-manifests-mi…
JanProvaznik Feb 17, 2026
93cbd5e
Fix Constants ambiguity after merge with main
JanProvaznik Feb 17, 2026
d92d315
Remove [MSBuildMultiThreadableTask] from abstract classes (Inherited=…
JanProvaznik Feb 19, 2026
1cd9eb3
Fix CWD dependency in Util.SortItems: use TaskEnvironment for path ca…
JanProvaznik Feb 19, 2026
651ea49
Change CreateFileStream delegate to take AbsolutePath instead of string
JanProvaznik Feb 19, 2026
80bd870
Merge branch 'main' into resource-manifests-migration
JanProvaznik Feb 23, 2026
c8b44ad
Merge branch 'main' into resource-manifests-migration
JanProvaznik Mar 6, 2026
7b75e5a
Merge branch 'main' into resource-manifests-migration
JanProvaznik Apr 28, 2026
a8525b7
fallback behavior
JanProvaznik Apr 28, 2026
c98970d
remove obsolete test setup
JanProvaznik Apr 28, 2026
a8b1981
note the behavior of Manifest.ResolveFiles for consumers
JanProvaznik Apr 28, 2026
a697593
absolutize before resolving
JanProvaznik Apr 28, 2026
d383811
fix test absolutepath handling
JanProvaznik Apr 28, 2026
9795b84
fix test pr comments
JanProvaznik Apr 28, 2026
a2e90df
Merge branch 'main' into resource-manifests-migration
JanProvaznik Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public void ManifestPopulationCheck(string? manifestName, bool expectedResult)
{
AddToWin32Manifest task = new AddToWin32Manifest()
{
BuildEngine = new MockEngine(_testOutput)
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest()
};

using (TestEnvironment env = TestEnvironment.Create())
Expand Down
12 changes: 12 additions & 0 deletions src/Tasks.UnitTests/CreateCSharpManifestResourceName_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ public void Regress188319()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("SR1.resx");
i.SetMetadata("BuildAction", "EmbeddedResource");
i.SetMetadata("DependentUpon", "SR1.strings"); // Normally, this would be a C# file.
Expand Down Expand Up @@ -393,6 +394,7 @@ public void DependentUponConvention_FindsMatch()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName
{
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = true,
ResourceFiles = new ITaskItem[] { i }
};
Expand Down Expand Up @@ -433,6 +435,7 @@ public void DependentUponConvention_DoesNotApplyToNonResx(bool explicitlySpecify
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName
{
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = true,
ResourceFiles = new ITaskItem[] { i }
};
Expand Down Expand Up @@ -466,6 +469,7 @@ public void DependentUponConvention_FindsMatchInSubfolder()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName
{
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = true,
ResourceFiles = new ITaskItem[] { i }
};
Expand Down Expand Up @@ -500,6 +504,7 @@ public void DependentUpon_UseConventionFileDoesNotExist()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName
{
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = true,
ResourceFiles = new ITaskItem[] { i }
};
Expand Down Expand Up @@ -531,6 +536,7 @@ public void DependentUpon_SpecifyNewFile()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName
{
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = true,
ResourceFiles = new ITaskItem[] { i }
};
Expand Down Expand Up @@ -565,6 +571,7 @@ public void DependentUponConvention_ConventionDisabledDoesNotReadConventionFile(
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName
{
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = false,
ResourceFiles = new ITaskItem[] { i }
};
Expand Down Expand Up @@ -601,6 +608,7 @@ public void CulturedResourceFileFindByConvention()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName
{
BuildEngine = new MockEngine(),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = true,
ResourceFiles = new ITaskItem[] { i },
};
Expand Down Expand Up @@ -794,6 +802,7 @@ public void ResourceFilesWithManifestResourceNamesContainsAdditionalMetadata()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("strings.resx");

t.ResourceFiles = new ITaskItem[] { i };
Expand All @@ -819,6 +828,7 @@ public void AddLogicalNameForNonResx()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("pic.bmp");
i.SetMetadata("Type", "Non-Resx");

Expand All @@ -844,6 +854,7 @@ public void PreserveLogicalNameForNonResx()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("pic.bmp");
i.SetMetadata("LogicalName", "foo");
i.SetMetadata("Type", "Non-Resx");
Expand All @@ -870,6 +881,7 @@ public void NoLogicalNameAddedForResx()
CreateCSharpManifestResourceName t = new CreateCSharpManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("strings.resx");
i.SetMetadata("Type", "Resx");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ public void Regress188319()
CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();

ITaskItem i = new TaskItem("SR1.resx");

Expand Down Expand Up @@ -391,6 +392,7 @@ End Class
CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName
{
BuildEngine = new MockEngine(_testOutput),
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
UseDependentUponConvention = true,
ResourceFiles = new ITaskItem[] { i },
};
Expand Down Expand Up @@ -514,6 +516,7 @@ public void ResourceFilesWithManifestResourceNamesContainsAdditionalMetadata()
CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("strings.resx");

t.ResourceFiles = new ITaskItem[] { i };
Expand All @@ -539,6 +542,7 @@ public void AddLogicalNameForNonResx()
CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("pic.bmp");
i.SetMetadata("Type", "Non-Resx");

Expand All @@ -564,6 +568,7 @@ public void PreserveLogicalNameForNonResx()
CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("pic.bmp");
i.SetMetadata("LogicalName", "foo");
i.SetMetadata("Type", "Non-Resx");
Expand All @@ -590,6 +595,7 @@ public void NoLogicalNameAddedForResx()
CreateVisualBasicManifestResourceName t = new CreateVisualBasicManifestResourceName();

t.BuildEngine = new MockEngine();
t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
ITaskItem i = new TaskItem("strings.resx");
i.SetMetadata("Type", "Resx");

Expand Down
236 changes: 236 additions & 0 deletions src/Tasks.UnitTests/ManifestTaskEnvironmentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Tasks;
using Microsoft.Build.UnitTests;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;

namespace Microsoft.Build.Tasks.UnitTests
{
/// <summary>
/// Tests verifying TaskEnvironment migration compatibility for manifest tasks.
/// These tests focus on path handling changes from the migration.
/// </summary>
public class ManifestTaskEnvironmentTests
{
Comment thread
JanProvaznik marked this conversation as resolved.
// Test 1: Empty ItemSpec - verifies exception handling matches pre-migration behavior
// GetAbsolutePath throws on empty, but this flows through existing exception handling
[Fact]
public void CreateManifestResourceName_EmptyItemSpec_ShouldFail()
{
var engine = new MockEngine(true);
var task = new CreateCSharpManifestResourceName
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = engine,
ResourceFiles = new ITaskItem[] { new TaskItem("") },
RootNamespace = "Test"
};

// On .NET Framework: returns false with logged error
// On .NET Core+: throws ArgumentNullException (pre-existing behavior in Path.GetDirectoryName)
#if NETFRAMEWORK
bool result = task.Execute();
result.ShouldBeFalse();
#else
Should.Throw<ArgumentNullException>(() => task.Execute());
#endif
}

// Test 2: Path with .. segments - critical test for canonicalization
// GetAbsolutePath does NOT canonicalize, so we wrap with Path.GetFullPath where needed
[Fact]
public void CreateManifestResourceName_PathWithDotDot_ShouldResolve()
{
using var env = TestEnvironment.Create();
var folder = env.CreateFolder();
var subFolder = Path.Combine(folder.Path, "sub");
Directory.CreateDirectory(subFolder);

var resxPath = Path.Combine(subFolder, "Test.resx");
File.WriteAllText(resxPath, "<root></root>");

var csPath = Path.Combine(subFolder, "Test.cs");
File.WriteAllText(csPath, "namespace Test { class Test { } }");

// Use path with .. segments - tests canonicalization
var pathWithDotDot = Path.Combine(folder.Path, "sub", "..", "sub", "Test.resx");

var task = new CreateCSharpManifestResourceName
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
ResourceFiles = new ITaskItem[] { new TaskItem(pathWithDotDot) },
RootNamespace = "Test",
UseDependentUponConvention = true
};

bool result = task.Execute();
result.ShouldBeTrue();
}

// Test 3: Forward slashes - tests path normalization
[Fact]
public void CreateManifestResourceName_ForwardSlashes_ShouldWork()
{
using var env = TestEnvironment.Create();
var folder = env.CreateFolder();
var subFolder = Path.Combine(folder.Path, "Resources");
Directory.CreateDirectory(subFolder);

var resxPath = Path.Combine(subFolder, "Strings.resx");
File.WriteAllText(resxPath, "<root></root>");

// Replace backslashes with forward slashes
var pathWithForwardSlashes = resxPath.Replace('\\', '/');

var task = new CreateCSharpManifestResourceName
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
ResourceFiles = new ITaskItem[] { new TaskItem(pathWithForwardSlashes) },
RootNamespace = "Test"
};

bool result = task.Execute();
result.ShouldBeTrue();
}

// Test 4: Mixed slashes - tests path normalization handles both
[Fact]
public void CreateManifestResourceName_MixedSlashes_ShouldWork()
{
using var env = TestEnvironment.Create();
var folder = env.CreateFolder();
var subFolder = Path.Combine(folder.Path, "Sub", "Folder");
Directory.CreateDirectory(subFolder);

var resxPath = Path.Combine(subFolder, "Test.resx");
File.WriteAllText(resxPath, "<root></root>");

// Mix forward and back slashes
var mixedPath = folder.Path + "/Sub\\Folder/Test.resx";

var task = new CreateCSharpManifestResourceName
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
ResourceFiles = new ITaskItem[] { new TaskItem(mixedPath) },
RootNamespace = "Test"
};

bool result = task.Execute();
result.ShouldBeTrue();
}

// Test 5: AddToWin32Manifest with null ApplicationManifest - tests graceful handling
[Fact]
public void AddToWin32Manifest_NullApplicationManifest_HandledGracefully()
{
using var env = TestEnvironment.Create();
var folder = env.CreateFolder();

var task = new AddToWin32Manifest
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
ApplicationManifest = null,
OutputDirectory = folder.Path,
SupportedArchitectures = "amd64"
};

// Null is treated as "no manifest" - should generate new one
bool result = task.Execute();
result.ShouldBeTrue();
}

// Test 6: Batch processing - one error should not abort remaining items
[Fact]
public void CreateManifestResourceName_BatchProcessing_ContinuesAfterError()
{
using var env = TestEnvironment.Create();
var folder = env.CreateFolder();

// Create one valid resource
var validPath = Path.Combine(folder.Path, "Valid.resx");
File.WriteAllText(validPath, "<root></root>");

// Create another valid resource
var valid2Path = Path.Combine(folder.Path, "Valid2.resx");
File.WriteAllText(valid2Path, "<root></root>");

// Invalid: DependentUpon points to non-existent file
var invalidItem = new TaskItem(validPath);
invalidItem.SetMetadata("DependentUpon", "NonExistent.cs");

var validItem = new TaskItem(valid2Path);

var task = new CreateCSharpManifestResourceName
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
ResourceFiles = new ITaskItem[] { invalidItem, validItem },
RootNamespace = "Test"
};

// Should return false due to error, but should still process both items
bool result = task.Execute();
result.ShouldBeFalse();
// Both items should have manifest names assigned (even though task failed)
task.ManifestResourceNames.Length.ShouldBe(2);
Comment thread
JanProvaznik marked this conversation as resolved.
Outdated
}

// Test 7: Deeply nested folder - tests path handling with many segments
[Fact]
public void CreateManifestResourceName_DeepNesting_ShouldWork()
{
using var env = TestEnvironment.Create();
var folder = env.CreateFolder();
var deepFolder = Path.Combine(folder.Path, "a", "b", "c", "d", "e");
Directory.CreateDirectory(deepFolder);

var resxPath = Path.Combine(deepFolder, "Test.resx");
File.WriteAllText(resxPath, "<root></root>");

var task = new CreateCSharpManifestResourceName
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
ResourceFiles = new ITaskItem[] { new TaskItem(resxPath) },
RootNamespace = "Test"
};

bool result = task.Execute();
result.ShouldBeTrue();
}

// Test 8: Path with spaces - tests no issues with space handling
[Fact]
public void CreateManifestResourceName_PathWithSpaces_ShouldWork()
{
using var env = TestEnvironment.Create();
var folder = env.CreateFolder();
var spaceFolder = Path.Combine(folder.Path, "My Resources");
Directory.CreateDirectory(spaceFolder);

var resxPath = Path.Combine(spaceFolder, "My Strings.resx");
File.WriteAllText(resxPath, "<root></root>");

var task = new CreateCSharpManifestResourceName
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
ResourceFiles = new ITaskItem[] { new TaskItem(resxPath) },
RootNamespace = "Test"
};

bool result = task.Execute();
result.ShouldBeTrue();
}
}
}
Loading