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/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
new file mode 100644
index 00000000..44276314
--- /dev/null
+++ b/Tests/Confuser.MSBuild.Tasks.Tests/DotNetCliHelper.cs
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2025 Cesium contributors
+//
+// SPDX-License-Identifier: MIT
+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..2cc27f42
--- /dev/null
+++ b/Tests/Confuser.MSBuild.Tasks.Tests/ExecUtil.cs
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2025 Cesium contributors
+//
+// SPDX-License-Identifier: MIT
+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..65267d79
--- /dev/null
+++ b/Tests/Confuser.MSBuild.Tasks.Tests/ProjectTestBase.cs
@@ -0,0 +1,202 @@
+// 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.Json;
+using System.Threading.Tasks;
+using TruePath;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Confuser.MSBuild.Tasks.Tests {
+ // 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;
+
+ private string NuGetConfigPath => Path.Combine(_temporaryPath, "NuGet.config");
+
+ 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);
+
+#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);
+ }
+
+ 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, $"""
+
+
+
+
+
+ """);
+ }
+
+ 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..5ae3789c
--- /dev/null
+++ b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadata.cs
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: 2025 Cesium contributors
+//
+// SPDX-License-Identifier: MIT
+using System;
+using System.Reflection;
+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..29a39a47
--- /dev/null
+++ b/Tests/Confuser.MSBuild.Tasks.Tests/SolutionMetadataAttribute.cs
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2025 Cesium contributors
+//
+// SPDX-License-Identifier: MIT
+using System;
+
+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!");
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 {