From 33e264a9d5251bd8c892bc9adc6b96bcc1ebd206 Mon Sep 17 00:00:00 2001 From: Louis Seubert Date: Wed, 21 Oct 2026 22:40:27 +0200 Subject: [PATCH] test(sdk): add package integration coverage --- .forgejo/workflows/ci.yml | 3 + .gitignore | 1 + Directory.Packages.props | 1 + src/sdk.tests/DirectoryExtensions.cs | 23 +++ src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj | 15 ++ src/sdk.tests/SdkPackageIntegrationTests.cs | 171 ++++++++++++++++++ .../fixtures/base/BaseConsumer.csproj | 9 + src/sdk.tests/fixtures/base/Program.cs | 4 + src/sdk.tests/fixtures/razor/Program.cs | 4 + .../fixtures/test/Directory.Build.props | 5 + src/sdk.tests/fixtures/test/Program.cs | 7 + .../fixtures/test/TestConsumer.csproj | 14 ++ src/sdk.tests/fixtures/web/Program.cs | 3 + src/sdk.tests/fixtures/web/WebConsumer.csproj | 10 + 14 files changed, 270 insertions(+) create mode 100644 src/sdk.tests/DirectoryExtensions.cs create mode 100644 src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj create mode 100644 src/sdk.tests/SdkPackageIntegrationTests.cs create mode 100644 src/sdk.tests/fixtures/base/BaseConsumer.csproj create mode 100644 src/sdk.tests/fixtures/base/Program.cs create mode 100644 src/sdk.tests/fixtures/razor/Program.cs create mode 100644 src/sdk.tests/fixtures/test/Directory.Build.props create mode 100644 src/sdk.tests/fixtures/test/Program.cs create mode 100644 src/sdk.tests/fixtures/test/TestConsumer.csproj create mode 100644 src/sdk.tests/fixtures/web/Program.cs create mode 100644 src/sdk.tests/fixtures/web/WebConsumer.csproj diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 583716d..7e07a1f 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -46,6 +46,9 @@ jobs: *.nupkg *.snupkg + - name: test SDK packages + run: dotnet test src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj --no-build --no-restore + - name: publish to nuget if: startsWith(forgejo.ref, 'refs/tags/') shell: bash diff --git a/.gitignore b/.gitignore index a9a1271..0ab95b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ artifacts/ +TestResults/ node_modules/ *.DotSettings.user diff --git a/Directory.Packages.props b/Directory.Packages.props index b8e7e93..649f17a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,5 +3,6 @@ true + diff --git a/src/sdk.tests/DirectoryExtensions.cs b/src/sdk.tests/DirectoryExtensions.cs new file mode 100644 index 0000000..1b73e4d --- /dev/null +++ b/src/sdk.tests/DirectoryExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) The Geekeey Authors +// SPDX-License-Identifier: EUPL-1.2 + +namespace Geekeey.Arcade.NET.Tests; + +public static class DirectoryExtensions +{ + extension(Directory) + { + public static void Copy(string sourceDirectory, string destinationDirectory) + { + foreach (var directory in Directory.EnumerateDirectories(sourceDirectory, "*", SearchOption.AllDirectories)) + { + Directory.CreateDirectory(Path.Combine(destinationDirectory, Path.GetRelativePath(sourceDirectory, directory))); + } + + foreach (var file in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories)) + { + File.Copy(file, Path.Combine(destinationDirectory, Path.GetRelativePath(sourceDirectory, file))); + } + } + } +} diff --git a/src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj b/src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj new file mode 100644 index 0000000..ae672d9 --- /dev/null +++ b/src/sdk.tests/Geekeey.Arcade.NET.Tests.csproj @@ -0,0 +1,15 @@ + + + net10.0 + false + true + Exe + + + + + + + + + diff --git a/src/sdk.tests/SdkPackageIntegrationTests.cs b/src/sdk.tests/SdkPackageIntegrationTests.cs new file mode 100644 index 0000000..6fb64ac --- /dev/null +++ b/src/sdk.tests/SdkPackageIntegrationTests.cs @@ -0,0 +1,171 @@ +// Copyright (c) The Geekeey Authors +// SPDX-License-Identifier: EUPL-1.2 + +using System.Diagnostics; +using System.Text.RegularExpressions; + +namespace Geekeey.Arcade.NET.Tests; + +public sealed class SdkPackageIntegrationTests +{ + + private static string RepositoryPath + { + get + { + for (var directory = new DirectoryInfo(AppContext.BaseDirectory); directory is not null; directory = directory.Parent) + { + if (File.Exists(Path.Combine(directory.FullName, "global.json"))) + { + return directory.FullName; + } + } + + throw new InvalidOperationException("Could not locate the repository root containing global.json."); + } + } + + private static string NuGetFeedPath { get; } = Path.Combine(RepositoryPath, "artifacts", "package", "release"); + + [Test] + [Arguments("Geekeey.Arcade.NET.Sdk", "base")] + [Arguments("Geekeey.Arcade.NET.Sdk.Razor", "razor")] + [Arguments("Geekeey.Arcade.NET.Sdk.Web", "web")] + public async Task SdkPackageCanBeConsumedFromAFeed(string packageId, string name) + { + // Arrange + var version = FindPackageVersion(packageId); + using var sut = await SdkTestProject.CreateAsync(name, version); + + // Act + await sut.RunDotnetAsync($"restore --configfile \"{sut.NuGetConfigPath}\" --packages \"{sut.PackagesPath}\" --nologo"); + await sut.RunDotnetAsync("build --no-restore --nologo"); + await sut.RunDotnetAsync($"pack --no-restore --configuration Debug --output \"{sut.PackedPath}\" --nologo"); + + // Assert + await Assert.That(File.Exists(Path.Combine(sut.ProjectPath, "bin", "Debug", "net10.0", $"{sut.ProjectName}.dll"))).IsTrue(); + await Assert.That(Directory.Exists(sut.PackedPath)).IsTrue(); + await Assert.That(Directory.EnumerateFiles(sut.PackedPath, $"{sut.ProjectName}.*.nupkg").Any()).IsTrue(); + } + + [Test] + public async Task TestSdkPackageStoresTestOutputInArtifacts() + { + // Arrange + var version = FindPackageVersion("Geekeey.Arcade.NET.Sdk.Test"); + using var sut = await SdkTestProject.CreateAsync("test", version); + await sut.RunDotnetAsync($"restore --configfile \"{sut.NuGetConfigPath}\" --packages \"{sut.PackagesPath}\" --nologo"); + await sut.RunDotnetAsync("build --no-restore --nologo"); + + var outputDirectory = Path.Combine(sut.ProjectPath, "artifacts", "bin", sut.ProjectName, "debug"); + await Assert.That(File.Exists(Path.Combine(outputDirectory, $"{sut.ProjectName}.dll"))).IsTrue(); + var testConfig = await File.ReadAllTextAsync(Path.Combine(outputDirectory, $"{sut.ProjectName}.testconfig.json")); + await Assert.That(testConfig.Contains("results-directory", StringComparison.Ordinal)).IsTrue(); + + // Act + var stdout = await sut.RunDotnetAsync("run --no-build --no-restore"); + + // Assert + await Assert.That(stdout.Contains("succeeded: 1", StringComparison.Ordinal)).IsTrue(); + await Assert.That(Directory.EnumerateFiles(Path.Combine(sut.ProjectPath, "artifacts", "testresults"), $"{sut.ProjectName}-*").Any()).IsTrue(); + } + + private static string FindPackageVersion(string packageId) + { + var pattern = $"""^{Regex.Escape(packageId)}\.(?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\.nupkg$"""; + foreach (var package in Directory.EnumerateFiles(NuGetFeedPath, "*.nupkg")) + { + var match = Regex.Match(Path.GetFileName(package), pattern, RegexOptions.CultureInvariant); + if (match.Success) + { + return match.Groups["version"].Value; + } + } + + throw new InvalidOperationException($"No current package found for {packageId}. Run dotnet pack first."); + } + + private sealed class SdkTestProject : IDisposable + { + private SdkTestProject(string root) + { + RootPath = root; + ProjectPath = Path.Combine(RootPath, "Project"); + PackagesPath = Path.Combine(RootPath, "packages"); + PackedPath = Path.Combine(RootPath, "packed"); + NuGetConfigPath = Path.Combine(RootPath, "NuGet.config"); + } + + public static async Task CreateAsync(string name, string version) + { + var sut = new SdkTestProject(Path.Combine(Path.GetTempPath(), $"arcade-sdk-test-{Guid.NewGuid():N}")); + try + { + Directory.CreateDirectory(sut.ProjectPath); + Directory.Copy(Path.Combine(AppContext.BaseDirectory, "fixtures", name), sut.ProjectPath); + sut.ProjectFilePath = Directory.EnumerateFiles(sut.ProjectPath, "*.csproj").Single(); + sut.ProjectName = Path.GetFileNameWithoutExtension(sut.ProjectFilePath); + var project = await File.ReadAllTextAsync(sut.ProjectFilePath); + await File.WriteAllTextAsync(sut.ProjectFilePath, project.Replace("{0}", version, StringComparison.Ordinal)); + var contents = $""" + + + + + + + + """; + await File.WriteAllTextAsync(sut.NuGetConfigPath, contents); + return sut; + } + catch + { + sut.Dispose(); + throw; + } + } + + public string RootPath { get; } + + public string ProjectPath { get; } + + public string ProjectFilePath { get; private set; } = ""; + + public string ProjectName { get; private set; } = ""; + + public string PackagesPath { get; } + + public string PackedPath { get; } + + public string NuGetConfigPath { get; } + + public async Task RunDotnetAsync(string arguments) + { + var startInfo = new ProcessStartInfo("dotnet", arguments) + { + WorkingDirectory = ProjectPath, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + Environment = { ["NUGET_PACKAGES"] = PackagesPath, ["DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER"] = "1" } + }; + var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Could not start dotnet."); + var output = await process.StandardOutput.ReadToEndAsync(); + var error = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + if (process.ExitCode is not 0) + { + throw new InvalidOperationException($"dotnet {arguments} failed with exit code {process.ExitCode}.\n{output}\n{error}"); + } + + return output; + } + + public void Dispose() + { + Directory.Delete(RootPath, recursive: true); + } + } +} diff --git a/src/sdk.tests/fixtures/base/BaseConsumer.csproj b/src/sdk.tests/fixtures/base/BaseConsumer.csproj new file mode 100644 index 0000000..65f8f3f --- /dev/null +++ b/src/sdk.tests/fixtures/base/BaseConsumer.csproj @@ -0,0 +1,9 @@ + + + net10.0 + true + false + BaseConsumer + IDE0055 + + diff --git a/src/sdk.tests/fixtures/base/Program.cs b/src/sdk.tests/fixtures/base/Program.cs new file mode 100644 index 0000000..ddbad5a --- /dev/null +++ b/src/sdk.tests/fixtures/base/Program.cs @@ -0,0 +1,4 @@ +public static class EntryPoint +{ + public static string Value => "ok"; +} diff --git a/src/sdk.tests/fixtures/razor/Program.cs b/src/sdk.tests/fixtures/razor/Program.cs new file mode 100644 index 0000000..a150b55 --- /dev/null +++ b/src/sdk.tests/fixtures/razor/Program.cs @@ -0,0 +1,4 @@ +public sealed class ComponentModel +{ + public string Value => "ok"; +} diff --git a/src/sdk.tests/fixtures/test/Directory.Build.props b/src/sdk.tests/fixtures/test/Directory.Build.props new file mode 100644 index 0000000..1b15241 --- /dev/null +++ b/src/sdk.tests/fixtures/test/Directory.Build.props @@ -0,0 +1,5 @@ + + + $(MSBuildProjectDirectory)/artifacts/ + + diff --git a/src/sdk.tests/fixtures/test/Program.cs b/src/sdk.tests/fixtures/test/Program.cs new file mode 100644 index 0000000..be2f92a --- /dev/null +++ b/src/sdk.tests/fixtures/test/Program.cs @@ -0,0 +1,7 @@ +using TUnit.Core; + +public class ConsumerTests +{ + [Test] + public void TestPasses() { } +} diff --git a/src/sdk.tests/fixtures/test/TestConsumer.csproj b/src/sdk.tests/fixtures/test/TestConsumer.csproj new file mode 100644 index 0000000..6f86a2d --- /dev/null +++ b/src/sdk.tests/fixtures/test/TestConsumer.csproj @@ -0,0 +1,14 @@ + + + net10.0 + true + false + TestConsumer + true + Exe + IDE0055 + + + + + diff --git a/src/sdk.tests/fixtures/web/Program.cs b/src/sdk.tests/fixtures/web/Program.cs new file mode 100644 index 0000000..971a50d --- /dev/null +++ b/src/sdk.tests/fixtures/web/Program.cs @@ -0,0 +1,3 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); +app.MapGet("/", () => "ok"); diff --git a/src/sdk.tests/fixtures/web/WebConsumer.csproj b/src/sdk.tests/fixtures/web/WebConsumer.csproj new file mode 100644 index 0000000..3d9e2d9 --- /dev/null +++ b/src/sdk.tests/fixtures/web/WebConsumer.csproj @@ -0,0 +1,10 @@ + + + net10.0 + true + false + WebConsumer + Exe + IDE0055 + +