From 9fa142f4b0000b83635ea049786e2a98b1ff3eca Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sun, 29 Mar 2026 18:11:07 +0200 Subject: [PATCH 1/4] Add tests for building using MSBuild files --- .github/workflows/dotnet-desktop.yml | 6 +- .../Confuser.MSBuild.Tasks.Tests.csproj | 29 ++- .../DotNetCliHelper.cs | 59 +++++ .../Confuser.MSBuild.Tasks.Tests/ExecUtil.cs | 50 ++++ .../PathExtensions._cs | 162 ++++++++++++ .../ProjectTestBase.cs | 230 ++++++++++++++++++ .../SolutionMetadata.cs | 20 ++ .../SolutionMetadataAttribute.cs | 19 ++ .../TestProjects/HelloWorld/HelloWorld.csproj | 14 ++ .../TestProjects/HelloWorld/Program.cs | 1 + 10 files changed, 581 insertions(+), 9 deletions(-) create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/PathExtensions._cs create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/HelloWorld.csproj create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/Program.cs diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index d007c7eb..9fc13da4 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -86,7 +86,7 @@ jobs: run: | vstest.console "artifacts/bin/*/release/*Test.exe" /Framework:.NETFramework,Version=v4.7.2 - - name: Run tests via vstest.console .NET FW Part 2 + - name: Run tests via vstest.console .NET FW - Part 2 run: | vstest.console "artifacts/bin/*/release_net472/*Test.exe" /Framework:.NETFramework,Version=v4.7.2 @@ -94,6 +94,10 @@ jobs: run: | vstest.console "artifacts/bin/*/release_net8.0/*Test.dll" /Framework:.NETCoreApp,Version=v8.0 + - name: Run tests via vstest.console .NET - Part 2 + run: | + vstest.console "artifacts/bin/*/release/*Tests.dll" /Framework:.NETCoreApp,Version=v8.0 + # Upload the package: https://github.com/marketplace/actions/upload-a-build-artifact - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/Confuser.MSBuild.Tasks.Tests.csproj b/Tests/Confuser.MSBuild.Tasks.Tests/Confuser.MSBuild.Tasks.Tests.csproj index 7cc018c0..fbd8b959 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/Confuser.MSBuild.Tasks.Tests.csproj +++ b/Tests/Confuser.MSBuild.Tasks.Tests/Confuser.MSBuild.Tasks.Tests.csproj @@ -1,10 +1,12 @@  - net472 + net8.0 false Exe + 14.0 ..\..\Confuser.CLI\Confuser.CLI.csproj + true @@ -20,8 +22,10 @@ + + @@ -32,6 +36,11 @@ + + + + + @@ -55,15 +64,19 @@ <_RuntimeOutputsNet>@(RuntimeOutputsNet) - <_RuntimeOutputsNetFrameworkFiles Include="$([System.IO.Path]::GetDirectoryName($(_RuntimeOutputsNetFramework)))\*.*"/> + <_RuntimeOutputsNetFrameworkFiles Include="$([System.IO.Path]::GetDirectoryName($(_RuntimeOutputsNetFramework)))\*.*" /> - - + + + + + <_Parameter1>$(ArtifactsPath)\.. + <_Parameter2>$(ArtifactsPath) + <_Parameter3>$(VersionPrefix) + + + diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs b/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs new file mode 100644 index 00000000..76d1e664 --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using TruePath; +using Xunit; + +namespace Confuser.MSBuild.Tasks.Tests { + internal class DotNetCliHelper { + + public static async Task> EvaluateMSBuildProperties( + ITestOutputHelper output, + string projectPath, + IReadOnlyDictionary? env = null, + params string[] propertyNames) { + if (!propertyNames.Any()) + return new Dictionary(); + + var result = await ExecUtil.Run( + output, + ExecUtil.DotNetHost, + AbsolutePath.CurrentWorkingDirectory, + ["msbuild", $"\"{projectPath}\"", $"-getProperty:{string.Join(",", propertyNames)}"], + null, + additionalEnvironment: env); + var resultString = result.StandardOutput; + if (propertyNames.Length == 1) + return new Dictionary { { propertyNames[0], resultString } }; + + var resultJson = JsonDocument.Parse(resultString); + var propertiesJson = resultJson.RootElement.GetProperty("Properties").EnumerateObject().ToArray(); + + return propertiesJson + .ToDictionary(property => property.Name, property => property.Value.GetString() ?? string.Empty); + } + + public static async Task> EvaluateMSBuildItem( + ITestOutputHelper output, + string projectPath, + string itemName, + IReadOnlyDictionary? env = null) { + var result = await ExecUtil.Run( + output, + ExecUtil.DotNetHost, + AbsolutePath.CurrentWorkingDirectory, + ["msbuild", $"\"{projectPath}\"", $"-getItem:{itemName}"], + null, + additionalEnvironment: env); + var resultString = result.StandardOutput; + var resultJson = JsonDocument.Parse(resultString); + var itemsJson = resultJson.RootElement.GetProperty("Items").EnumerateObject().ToArray(); + var itemsDict = itemsJson.ToDictionary(item => item.Name, item => item.Value.EnumerateArray()); + + return itemsDict[itemName].Select(meta => (meta.GetProperty("Identity").GetString()!, meta.GetProperty("FullPath").GetString())); + } + } +} diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs b/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs new file mode 100644 index 00000000..d5a1851d --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Medallion.Shell; +using TruePath; +using Xunit; + +namespace Confuser.MSBuild.Tasks.Tests { + internal class ExecUtil { + public static readonly LocalPath DotNetHost = new("dotnet"); + + public static async Task Run( + ITestOutputHelper? output, + LocalPath executable, + AbsolutePath workingDirectory, + string[] args, + string? inputContent = null, + IReadOnlyDictionary? additionalEnvironment = null) { + output?.WriteLine($"$ {executable} {string.Join(" ", args)}"); + var command = Command.Run(executable.Value, args, o => + { + o.WorkingDirectory(workingDirectory.Value); + if (inputContent is { }) { + o.StartInfo(_ => _.RedirectStandardInput = true); + } + + if (additionalEnvironment != null) { + foreach (var (key, value) in additionalEnvironment) { + o.EnvironmentVariable(key, value); + } + } + }); + if (inputContent is { }) { + command.StandardInput.Write(inputContent); + command.StandardInput.Close(); + } + + var result = await command.Task; + foreach (var s in result.StandardOutput.Split("\n")) + output?.WriteLine(s.TrimEnd()); + if (result.StandardError.Trim() != "") { + foreach (var s in result.StandardError.Split("\n")) + output?.WriteLine($"[ERR] {s.TrimEnd()}"); + } + + output?.WriteLine($"Command exit code: {result.ExitCode}"); + return result; + } + } +} diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/PathExtensions._cs b/Tests/Confuser.MSBuild.Tasks.Tests/PathExtensions._cs new file mode 100644 index 00000000..60082be0 --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/PathExtensions._cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace Confuser.MSBuild.Tasks.Tests { + internal static class PathExtensions { + internal static bool EndsInDirectorySeparator(ReadOnlySpan path) => + path.Length > 0 && IsDirectorySeparator(path[^1]); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(char c) { + return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar; + } + /// Returns a comparison that can be used to compare file and directory names for equality. + public static string GetRelativePath(string relativeTo, string path) { + ArgumentNullException.ThrowIfNull(relativeTo); + ArgumentNullException.ThrowIfNull(path); + + if (PathInternal.IsEffectivelyEmpty(relativeTo.AsSpan())) + throw new ArgumentException("Arg_PathEmpty", nameof(relativeTo)); + if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) + throw new ArgumentException("Arg_PathEmpty", nameof(path)); + + relativeTo = Path.GetFullPath(relativeTo); + path = Path.GetFullPath(path); + + // Need to check if the roots are different- if they are we need to return the "to" path. + if (!AreRootsEqual(relativeTo, path, StringComparison.OrdinalIgnoreCase)) + return path; + + int commonLength = GetCommonPathLength(relativeTo, path, ignoreCase: true); + + // If there is nothing in common they can't share the same root, return the "to" path as is. + if (commonLength == 0) + return path; + + // Trailing separators aren't significant for comparison + int relativeToLength = relativeTo.Length; + if (EndsInDirectorySeparator(relativeTo.AsSpan())) + relativeToLength--; + + bool pathEndsInSeparator = EndsInDirectorySeparator(path.AsSpan()); + int pathLength = path.Length; + if (pathEndsInSeparator) + pathLength--; + + // If we have effectively the same path, return "." + if (relativeToLength == pathLength && commonLength >= relativeToLength) return "."; + + // We have the same root, we need to calculate the difference now using the + // common Length and Segment count past the length. + // + // Some examples: + // + // C:\Foo C:\Bar L3, S1 -> ..\Bar + // C:\Foo C:\Foo\Bar L6, S0 -> Bar + // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar + // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar + + var sb = new StringBuilder(260); + sb.EnsureCapacity(Math.Max(relativeTo.Length, path.Length)); + + // Add parent segments for segments past the common on the "from" path + if (commonLength < relativeToLength) { + sb.Append(".."); + + for (int i = commonLength + 1; i < relativeToLength; i++) { + if (IsDirectorySeparator(relativeTo[i])) { + sb.Append(Path.DirectorySeparatorChar); + sb.Append(".."); + } + } + } + else if (IsDirectorySeparator(path[commonLength])) { + // No parent segments and we need to eat the initial separator + // (C:\Foo C:\Foo\Bar case) + commonLength++; + } + + // Now add the rest of the "to" path, adding back the trailing separator + int differenceLength = pathLength - commonLength; + if (pathEndsInSeparator) + differenceLength++; + + if (differenceLength > 0) { + if (sb.Length > 0) { + sb.Append(Path.DirectorySeparatorChar); + } + + sb.Append(path.AsSpan(commonLength, differenceLength).ToString()); + } + + return sb.ToString(); + } + + /// + /// Get the common path length from the start of the string. + /// + internal static int GetCommonPathLength(string first, string second, bool ignoreCase) { + int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase); + + // If nothing matches + if (commonChars == 0) + return commonChars; + + // Or we're a full string and equal length or match to a separator + if (commonChars == first.Length + && (commonChars == second.Length || IsDirectorySeparator(second[commonChars]))) + return commonChars; + + if (commonChars == second.Length && IsDirectorySeparator(first[commonChars])) + return commonChars; + + // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar. + while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1])) + commonChars--; + + return commonChars; + } + + /// + /// Gets the count of common characters from the left optionally ignoring case + /// + internal static int EqualStartingCharacterCount(string? first, string? second, bool ignoreCase) { + if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) { + return 0; + } + + int commonLength = first.AsSpan().CommonPrefixLength(second); + if (ignoreCase) { + for (; (uint)commonLength < (uint)first.Length; commonLength++) { + if (commonLength >= second.Length || + char.ToUpperInvariant(first[commonLength]) != char.ToUpperInvariant(second[commonLength])) { + break; + } + } + } + return commonLength; + } + + /// + /// Returns true if the two paths have the same root + /// + internal static bool AreRootsEqual(string? first, string? second, StringComparison comparisonType) { + int firstRootLength = GetRootLength(first.AsSpan()); + int secondRootLength = GetRootLength(second.AsSpan()); + + return firstRootLength == secondRootLength + && string.Compare( + strA: first, + indexA: 0, + strB: second, + indexB: 0, + length: firstRootLength, + comparisonType: comparisonType) == 0; + } + } +} diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs new file mode 100644 index 00000000..87cd44a1 --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using TruePath; +using Xunit; +using Xunit.Sdk; + +namespace Confuser.MSBuild.Tasks.Tests { + public class ProjectTestBase : IDisposable { + private readonly ITestOutputHelper _testOutputHelper; + private readonly string _temporaryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + private readonly Dictionary _dotNetEnvVars; + + private string NuGetConfigPath => Path.Combine(_temporaryPath, "NuGet.config"); + private string GlobalJsonPath => Path.Combine(_temporaryPath, "global.json"); + + public ProjectTestBase(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; + _dotNetEnvVars = new() { ["NUGET_PACKAGES"] = Path.Combine(_temporaryPath, "package-cache") }; + + File.Delete(_temporaryPath); + + _testOutputHelper.WriteLine($"Test projects folder: {_temporaryPath}"); + + var assemblyPath = Assembly.GetExecutingAssembly().Location; + var testDataPath = Path.Combine(Path.GetDirectoryName(assemblyPath)!, "TestProjects"); + _testOutputHelper.WriteLine($"Copying TestProjects to {_temporaryPath}..."); + CopyDirectoryRecursive(testDataPath, _temporaryPath); + + var nupkgPath = (SolutionMetadata.SourceRoot / "artifacts/package/debug").Canonicalize(); + _testOutputHelper.WriteLine($"Local NuGet feed: {nupkgPath}."); + EmitNuGetConfig(NuGetConfigPath, nupkgPath); + //EmitGlobalJson(GlobalJsonPath, $"{SolutionMetadata.VersionPrefix}"); + } + + [Theory] + [InlineData("HelloWorld")] + public async Task Confuse_Exe_ShouldSucceed(string projectName) { + HashSet expectedObjArtifacts = + [ + $"{projectName}.dll" + ]; + + var hostExeFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{projectName}.exe" : projectName; + HashSet expectedBinArtifacts = + [ + $"{projectName}.dll", + hostExeFile, + $"{projectName}.runtimeconfig.json", + $"{projectName}.deps.json", + ]; + + var result = await ExecuteTargets(projectName, "Restore", "Build"); + + Assert.True(result.ExitCode == 0); + AssertIncludes(expectedObjArtifacts, result.IntermediateArtifacts.Select(a => a.FileName).ToList()); + AssertIncludes(expectedBinArtifacts, result.OutputArtifacts.Select(a => a.FileName).ToList()); + } + public static void AssertIncludes(IReadOnlyCollection expected, IReadOnlyCollection all) { + var foundItems = all.Where(expected.Contains).ToList(); + var remainingItems = expected.Except(foundItems).ToList(); + if (remainingItems.Count != 0) + throw new XunitException($"Expected elements are missing: [{string.Join(", ", remainingItems)}]"); + } + + + + private static void EmitNuGetConfig(string configFilePath, AbsolutePath packageSourcePath) { + File.WriteAllText(configFilePath, $""" + + + + + + """); + } + + //private static void EmitGlobalJson(string globalJsonPath, string packageVersion) { + // var actualGlobalJson = SolutionMetadata.SourceRoot / "global.json"; + // var globalConfig = JsonNode.Parse(File.ReadAllText(actualGlobalJson.Value))!; + // globalConfig["msbuild-sdks"] = new JsonObject([new KeyValuePair("Cesium.Sdk", packageVersion)]); + // var content = globalConfig.ToJsonString(new JsonSerializerOptions { + // WriteIndented = true + // }); + // File.WriteAllText(globalJsonPath, content); + //} + + protected async Task ExecuteTargets(string projectName, params string[] targets) { + var projectFile = $"{projectName}/{projectName}.csproj"; + var joinedTargets = string.Join(";", targets); + var testProjectFile = Path.GetFullPath(Path.Combine(_temporaryPath, projectFile)); + var testProjectFolder = Path.GetDirectoryName(testProjectFile) ?? throw new ArgumentNullException(nameof(testProjectFile)); + var binLogFile = Path.Combine(testProjectFolder, $"build_result_{projectName}_{DateTime.UtcNow:yyyy-dd-M_HH-mm-s}.binlog"); + + const string objFolderPropertyName = "IntermediateOutputPath"; + const string binFolderPropertyName = "OutDir"; + + var startInfo = new ProcessStartInfo { + WorkingDirectory = testProjectFolder, + FileName = "dotnet", + ArgumentList = { "msbuild", testProjectFile, $"/t:{joinedTargets}", "/restore", $"/bl:{binLogFile}" }, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + UseShellExecute = false, + }; + foreach (var pair in _dotNetEnvVars) { + var name = pair.Key; + var var = pair.Value; + startInfo.Environment[name] = var; + } + + using var process = new Process(); + process.StartInfo = startInfo; + var stdOutOutput = ""; + var stdErrOutput = ""; + process.OutputDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) { + stdOutOutput += e.Data + Environment.NewLine; + _testOutputHelper.WriteLine($"[stdout]: {e.Data}"); + } + }; + + process.ErrorDataReceived += (_, e) => + { + if (!string.IsNullOrEmpty(e.Data)) { + stdErrOutput += e.Data + Environment.NewLine; + _testOutputHelper.WriteLine($"[stderr]: {e.Data}"); + } + }; + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + process.WaitForExit(); + + var success = process.ExitCode == 0; + + _testOutputHelper.WriteLine(success + ? "Build succeeded" + : $"Build failed with exit code {process.ExitCode}"); + + var properties = await DotNetCliHelper.EvaluateMSBuildProperties( + _testOutputHelper, + testProjectFile, + env: _dotNetEnvVars, + objFolderPropertyName, + binFolderPropertyName); + _testOutputHelper.WriteLine($"Properties request result: {JsonSerializer.Serialize(properties, new JsonSerializerOptions { WriteIndented = false })}"); + + var binFolder = NormalizePath(Path.GetFullPath(properties[binFolderPropertyName], testProjectFolder)); + var objFolder = NormalizePath(Path.GetFullPath(properties[objFolderPropertyName], testProjectFolder)); + + var binArtifacts = CollectArtifacts(binFolder); + var objArtifacts = CollectArtifacts(objFolder); + + var result = new BuildResult(process.ExitCode, stdOutOutput, stdErrOutput, binArtifacts, objArtifacts); + _testOutputHelper.WriteLine($"Build result: {JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true })}"); + return result; + + IReadOnlyCollection CollectArtifacts(string folder) { + _testOutputHelper.WriteLine($"Collecting artifacts from '{folder}' folder"); + return Directory.Exists(folder) + ? Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) + .Select(path => new BuildArtifact(Path.GetRelativePath(folder, path), path)) + .ToList() + : Array.Empty(); + } + } + + private static void CopyDirectoryRecursive(string source, string target) { + Directory.CreateDirectory(target); + + foreach (var subDirPath in Directory.GetDirectories(source)) { + var dirName = Path.GetFileName(subDirPath); + CopyDirectoryRecursive(subDirPath, Path.Combine(target, dirName)); + } + + foreach (var filePath in Directory.GetFiles(source)) { + var fileName = Path.GetFileName(filePath); + File.Copy(filePath, Path.Combine(target, fileName)); + } + } + + private static string NormalizePath(string path) { + var normalizedPath = new Uri(path).LocalPath; + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? normalizedPath + : normalizedPath.Replace('\\', '/'); + } + + private void ClearOutput() { + Directory.Delete(_temporaryPath, true); + } + + public void Dispose() { + ClearOutput(); + } + + protected class BuildResult( + int exitCode, + string stdOutOutput, + string stdErrOutput, + IReadOnlyCollection outputArtifacts, + IReadOnlyCollection intermediateArtifacts) { + public int ExitCode { get; private set; } = exitCode; + public string StdOutOutput { get; private set; } = stdOutOutput; + public string StdErrOutput { get; private set; } = stdErrOutput; + public IReadOnlyCollection OutputArtifacts { get; private set; } = outputArtifacts; + public IReadOnlyCollection IntermediateArtifacts { get; private set; } = intermediateArtifacts; + } + + protected class BuildArtifact( + string fileName, + string fullPath) { + public string FileName { get; private set; } = fileName; + public string FullPath { get; private set; } = fullPath; + } + } +} diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs new file mode 100644 index 00000000..bc491216 --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using TruePath; + +namespace Confuser.MSBuild.Tasks.Tests { + internal class SolutionMetadata { + public static AbsolutePath SourceRoot => new(ResolvedAttribute.SourceRoot); + public static AbsolutePath ArtifactsRoot => new(ResolvedAttribute.ArtifactsRoot); + public static string VersionPrefix => ResolvedAttribute.VersionPrefix; + + private static SolutionMetadataAttribute ResolvedAttribute => + typeof(SolutionMetadata).Assembly.GetCustomAttribute() + ?? throw new Exception($"Missing {nameof(SolutionMetadataAttribute)} metadata attribute."); + + } +} diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs new file mode 100644 index 00000000..85f94607 --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Confuser.MSBuild.Tasks.Tests { + internal class SolutionMetadataAttribute : Attribute { + public string SourceRoot { get; } + public string ArtifactsRoot { get; } + public string VersionPrefix { get; } + + public SolutionMetadataAttribute(string sourceRoot, string artifactsRoot, string versionPrefix) { + SourceRoot = sourceRoot; + ArtifactsRoot = artifactsRoot; + VersionPrefix = versionPrefix; + } + } +} diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/HelloWorld.csproj b/Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/HelloWorld.csproj new file mode 100644 index 00000000..d77595d9 --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/HelloWorld.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/Program.cs b/Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/Program.cs new file mode 100644 index 00000000..f7f02a1c --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/TestProjects/HelloWorld/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("Hello, World!"); From a8e94e8a272862a33c03da41015733d52bb3ff7e Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sun, 29 Mar 2026 18:14:30 +0200 Subject: [PATCH 2/4] Assign license for derived files --- .../ConfusionTest.cs | 34 +++++++++++++++++++ .../DotNetCliHelper.cs | 5 ++- .../Confuser.MSBuild.Tasks.Tests/ExecUtil.cs | 4 ++- .../ProjectTestBase.cs | 33 ++++-------------- .../SolutionMetadata.cs | 9 +++-- .../SolutionMetadataAttribute.cs | 9 +++-- .../VerifyTestBase.cs | 9 +++-- 7 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 Tests/Confuser.MSBuild.Tasks.Tests/ConfusionTest.cs diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/ConfusionTest.cs b/Tests/Confuser.MSBuild.Tasks.Tests/ConfusionTest.cs new file mode 100644 index 00000000..c1b1d226 --- /dev/null +++ b/Tests/Confuser.MSBuild.Tasks.Tests/ConfusionTest.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Xunit; + +namespace Confuser.MSBuild.Tasks.Tests { + public class ConfusionTest(ITestOutputHelper testOutputHelper) : ProjectTestBase(testOutputHelper) { + + [Theory] + [InlineData("HelloWorld")] + public async Task Confuse_Exe_ShouldSucceed(string projectName) { + HashSet expectedObjArtifacts = + [ + $"{projectName}.dll" + ]; + + var hostExeFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{projectName}.exe" : projectName; + HashSet expectedBinArtifacts = + [ + $"{projectName}.dll", + hostExeFile, + $"{projectName}.runtimeconfig.json", + $"{projectName}.deps.json", + ]; + + var result = await ExecuteTargets(projectName, "Restore", "Build"); + + Assert.True(result.ExitCode == 0); + AssertIncludes(expectedObjArtifacts, result.IntermediateArtifacts.Select(a => a.FileName).ToList()); + AssertIncludes(expectedBinArtifacts, result.OutputArtifacts.Select(a => a.FileName).ToList()); + } + } +} diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs b/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs index 76d1e664..44276314 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs @@ -1,4 +1,7 @@ -using System; +// SPDX-FileCopyrightText: 2025 Cesium contributors +// +// SPDX-License-Identifier: MIT +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs b/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs index d5a1851d..2cc27f42 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs @@ -1,4 +1,6 @@ -using System; +// SPDX-FileCopyrightText: 2025 Cesium contributors +// +// SPDX-License-Identifier: MIT using System.Collections.Generic; using System.Threading.Tasks; using Medallion.Shell; diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs index 87cd44a1..2f4f4a94 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs @@ -1,11 +1,13 @@ -using System; +// SPDX-FileCopyrightText: 2025 Cesium contributors +// +// SPDX-License-Identifier: MIT +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using TruePath; @@ -13,7 +15,8 @@ using Xunit.Sdk; namespace Confuser.MSBuild.Tasks.Tests { - public class ProjectTestBase : IDisposable { + // Adapted from SdkTestBase.cs + public abstract class ProjectTestBase : IDisposable { private readonly ITestOutputHelper _testOutputHelper; private readonly string _temporaryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); private readonly Dictionary _dotNetEnvVars; @@ -39,30 +42,6 @@ public ProjectTestBase(ITestOutputHelper testOutputHelper) { EmitNuGetConfig(NuGetConfigPath, nupkgPath); //EmitGlobalJson(GlobalJsonPath, $"{SolutionMetadata.VersionPrefix}"); } - - [Theory] - [InlineData("HelloWorld")] - public async Task Confuse_Exe_ShouldSucceed(string projectName) { - HashSet expectedObjArtifacts = - [ - $"{projectName}.dll" - ]; - - var hostExeFile = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{projectName}.exe" : projectName; - HashSet expectedBinArtifacts = - [ - $"{projectName}.dll", - hostExeFile, - $"{projectName}.runtimeconfig.json", - $"{projectName}.deps.json", - ]; - - var result = await ExecuteTargets(projectName, "Restore", "Build"); - - Assert.True(result.ExitCode == 0); - AssertIncludes(expectedObjArtifacts, result.IntermediateArtifacts.Select(a => a.FileName).ToList()); - AssertIncludes(expectedBinArtifacts, result.OutputArtifacts.Select(a => a.FileName).ToList()); - } public static void AssertIncludes(IReadOnlyCollection expected, IReadOnlyCollection all) { var foundItems = all.Where(expected.Contains).ToList(); var remainingItems = expected.Except(foundItems).ToList(); diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs index bc491216..5ae3789c 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; +// SPDX-FileCopyrightText: 2025 Cesium contributors +// +// SPDX-License-Identifier: MIT +using System; using System.Reflection; -using System.Text; -using System.Threading.Tasks; using TruePath; namespace Confuser.MSBuild.Tasks.Tests { diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs index 85f94607..29a39a47 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// SPDX-FileCopyrightText: 2025 Cesium contributors +// +// SPDX-License-Identifier: MIT +using System; namespace Confuser.MSBuild.Tasks.Tests { internal class SolutionMetadataAttribute : Attribute { diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/VerifyTestBase.cs b/Tests/Confuser.MSBuild.Tasks.Tests/VerifyTestBase.cs index c8fa5b39..b72881aa 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/VerifyTestBase.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/VerifyTestBase.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// SPDX-FileCopyrightText: 2025 Cesium contributors +// +// SPDX-License-Identifier: MIT +using System; using VerifyTests; namespace Confuser.MSBuild.Tasks.Tests { From b782a40867fbdde184e5c07647ac330132074b77 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sun, 29 Mar 2026 18:27:19 +0200 Subject: [PATCH 3/4] 1 --- Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs index 2f4f4a94..af39575d 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs @@ -37,7 +37,11 @@ public ProjectTestBase(ITestOutputHelper testOutputHelper) { _testOutputHelper.WriteLine($"Copying TestProjects to {_temporaryPath}..."); CopyDirectoryRecursive(testDataPath, _temporaryPath); +#if DEBUG var nupkgPath = (SolutionMetadata.SourceRoot / "artifacts/package/debug").Canonicalize(); +#else + var nupkgPath = (SolutionMetadata.SourceRoot / "artifacts/package/release").Canonicalize(); +#endif _testOutputHelper.WriteLine($"Local NuGet feed: {nupkgPath}."); EmitNuGetConfig(NuGetConfigPath, nupkgPath); //EmitGlobalJson(GlobalJsonPath, $"{SolutionMetadata.VersionPrefix}"); From e374a28a925ad3a8cd9ba6c079e15d51dbf19281 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sun, 29 Mar 2026 21:12:29 +0200 Subject: [PATCH 4/4] Remove not needed code --- .../Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs index af39575d..65267d79 100644 --- a/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs +++ b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs @@ -22,7 +22,6 @@ public abstract class ProjectTestBase : IDisposable { private readonly Dictionary _dotNetEnvVars; private string NuGetConfigPath => Path.Combine(_temporaryPath, "NuGet.config"); - private string GlobalJsonPath => Path.Combine(_temporaryPath, "global.json"); public ProjectTestBase(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; @@ -44,8 +43,8 @@ public ProjectTestBase(ITestOutputHelper testOutputHelper) { #endif _testOutputHelper.WriteLine($"Local NuGet feed: {nupkgPath}."); EmitNuGetConfig(NuGetConfigPath, nupkgPath); - //EmitGlobalJson(GlobalJsonPath, $"{SolutionMetadata.VersionPrefix}"); } + public static void AssertIncludes(IReadOnlyCollection expected, IReadOnlyCollection all) { var foundItems = all.Where(expected.Contains).ToList(); var remainingItems = expected.Except(foundItems).ToList(); @@ -65,16 +64,6 @@ private static void EmitNuGetConfig(string configFilePath, AbsolutePath packageS """); } - //private static void EmitGlobalJson(string globalJsonPath, string packageVersion) { - // var actualGlobalJson = SolutionMetadata.SourceRoot / "global.json"; - // var globalConfig = JsonNode.Parse(File.ReadAllText(actualGlobalJson.Value))!; - // globalConfig["msbuild-sdks"] = new JsonObject([new KeyValuePair("Cesium.Sdk", packageVersion)]); - // var content = globalConfig.ToJsonString(new JsonSerializerOptions { - // WriteIndented = true - // }); - // File.WriteAllText(globalJsonPath, content); - //} - protected async Task ExecuteTargets(string projectName, params string[] targets) { var projectFile = $"{projectName}/{projectName}.csproj"; var joinedTargets = string.Join(";", targets);