Every push to main now runs the gate on the self-hosted runners and, when
green, publishes a Gitea Release carrying the client, launcher+bake, and
manifest.
Pipeline (.gitea/workflows/ci.yml):
- windows-gate runs tools/run-release-gate.ps1, the project's own bounded
gate. A bare `dotnet test AcDream.slnx` is NOT usable as a gate: it fails
~36 tests by design, because the InstalledDat/Live/Manual/OS lanes assert
their own preconditions. The gate script's trait filter is what excludes
them.
- linux-portable runs the portable closure, where the Linux-lane tests
actually execute instead of failing on Windows.
- release depends on both, so a red gate cannot publish. It is a job in the
same workflow rather than a workflow_run trigger, whose Forgejo support is
unreliable; `needs` is guaranteed.
No actions/setup-dotnet: data.forgejo.org does not mirror it at all (404),
and both runners carry the pinned SDK band already. actions/checkout IS
mirrored and is used normally.
Release payloads become release ATTACHMENTS, outside git history, so ~120 MB
per build never enters a branch. Only the ~500-byte manifest.json is
committed, to the payload-free dist branch, because Forgejo has no
/releases/latest/download/ route (verified 404) for the launcher to poll.
publish-bin.ps1 takes -BaseUrl so the manifest points at the release tag.
Two real gate failures fixed:
- LauncherProjectBoundaryTests asserted four `**` path filters belonging to
the push triggers that 8be14d39 removed when workflows went manual-only.
The assertions about what the workflow DOES are untouched.
- MainWindowViewTests failed in Test Case Cleanup with "calling thread cannot
access this object" while passing in isolation: Avalonia's headless session
is thread-affine and xUnit ran collections in parallel. Serialized via
xunit.runner.json, the same settings AcDream.Core.Tests already uses.
Local gate: 12 projects, 14,346 tests, 0 failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
213 lines
9 KiB
C#
213 lines
9 KiB
C#
using System.Diagnostics;
|
|
using System.Xml.Linq;
|
|
|
|
namespace AcDream.Launcher.Tests;
|
|
|
|
public sealed class LauncherProjectBoundaryTests
|
|
{
|
|
private static readonly string[] ExpectedAvaloniaPackages =
|
|
[
|
|
"Avalonia",
|
|
"Avalonia.Desktop",
|
|
"Avalonia.Themes.Fluent",
|
|
];
|
|
|
|
[Fact]
|
|
public void LauncherReferencesOnlyLauncherCoreAndUsesCentralAvaloniaVersion()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
string projectPath = Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.Launcher",
|
|
"AcDream.Launcher.csproj");
|
|
XDocument project = XDocument.Load(projectPath);
|
|
|
|
string projectReference = Assert.Single(
|
|
project.Descendants("ProjectReference")
|
|
.Select(element => element.Attribute("Include")?.Value ?? string.Empty));
|
|
Assert.EndsWith(
|
|
"AcDream.Launcher.Core\\AcDream.Launcher.Core.csproj",
|
|
projectReference,
|
|
StringComparison.Ordinal);
|
|
|
|
(string Name, string? Version)[] packages = project
|
|
.Descendants("PackageReference")
|
|
.Select(element => (
|
|
element.Attribute("Include")?.Value ?? string.Empty,
|
|
element.Attribute("Version")?.Value))
|
|
.OrderBy(package => package.Item1, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
Assert.Equal(ExpectedAvaloniaPackages, packages.Select(package => package.Name));
|
|
Assert.All(packages, package => Assert.Null(package.Version));
|
|
|
|
XDocument centralVersions = XDocument.Load(Path.Combine(root, "Directory.Packages.props"));
|
|
(string Name, string Version)[] centralAvaloniaPackages = centralVersions
|
|
.Descendants("PackageVersion")
|
|
.Select(element => (
|
|
element.Attribute("Include")?.Value ?? string.Empty,
|
|
element.Attribute("Version")?.Value ?? string.Empty))
|
|
.Where(package => ExpectedAvaloniaPackages.Contains(package.Item1, StringComparer.Ordinal))
|
|
.OrderBy(package => package.Item1, StringComparer.Ordinal)
|
|
.ToArray();
|
|
|
|
Assert.Equal(ExpectedAvaloniaPackages, centralAvaloniaPackages.Select(package => package.Name));
|
|
Assert.All(centralAvaloniaPackages, package => Assert.Equal("12.1.1", package.Version));
|
|
}
|
|
|
|
[Fact]
|
|
public void LauncherAndItsTestsAreSolutionMembers()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
XDocument solution = XDocument.Load(Path.Combine(root, "AcDream.slnx"));
|
|
string[] paths = solution.Descendants("Project")
|
|
.Select(element => (element.Attribute("Path")?.Value ?? string.Empty)
|
|
.Replace('\\', '/'))
|
|
.ToArray();
|
|
|
|
Assert.Contains("src/AcDream.Launcher/AcDream.Launcher.csproj", paths);
|
|
Assert.Contains("tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj", paths);
|
|
}
|
|
|
|
[Fact]
|
|
public void LinuxPublishEvaluatesAsSelfContainedSingleFile()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
string projectPath = Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.Launcher",
|
|
"AcDream.Launcher.csproj");
|
|
|
|
Assert.Equal("true", EvaluateProperty(projectPath, "SelfContained"));
|
|
Assert.Equal("true", EvaluateProperty(projectPath, "PublishSingleFile"));
|
|
}
|
|
|
|
[Fact]
|
|
public void RidPublishComposesBakeWithoutAProjectReference()
|
|
{
|
|
string project = File.ReadAllText(Path.Combine(
|
|
FindRepositoryRoot(),
|
|
"src",
|
|
"AcDream.Launcher",
|
|
"AcDream.Launcher.csproj"));
|
|
|
|
Assert.DoesNotContain(
|
|
"ProjectReference Include=\"..\\AcDream.Bake",
|
|
project,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("PublishCoDeployedBakeTool", project, StringComparison.Ordinal);
|
|
Assert.Contains("..\\AcDream.Bake\\AcDream.Bake.csproj", project, StringComparison.Ordinal);
|
|
Assert.Contains("SelfContained=true", project, StringComparison.Ordinal);
|
|
Assert.Contains("PublishSingleFile=true", project, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ModalMarkupAndCodeBehindCarryKeyboardFocusAndAccessibilityGuards()
|
|
{
|
|
string root = FindRepositoryRoot();
|
|
string markup = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.Launcher",
|
|
"MainWindow.axaml"));
|
|
string codeBehind = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.Launcher",
|
|
"MainWindow.axaml.cs"));
|
|
|
|
Assert.Equal(3, Count(markup, "KeyboardNavigation.TabNavigation=\"Cycle\""));
|
|
Assert.Equal(3, Count(markup, "KeyDown=\"OnModalKeyDown\""));
|
|
Assert.True(Count(markup, "AutomationProperties.Name=") >= 13);
|
|
Assert.True(Count(markup, "IsDefault=\"True\"") >= 3);
|
|
Assert.True(Count(markup, "IsCancel=\"True\"") >= 3);
|
|
Assert.Contains("ServerNameTextBox", markup, StringComparison.Ordinal);
|
|
Assert.Contains("AccountNameTextBox", markup, StringComparison.Ordinal);
|
|
Assert.Contains("CharacterNameTextBox", markup, StringComparison.Ordinal);
|
|
Assert.Contains("FocusActiveModal", codeBehind, StringComparison.Ordinal);
|
|
Assert.Contains("_focusBeforeModal", codeBehind, StringComparison.Ordinal);
|
|
Assert.Contains("Key.Escape", codeBehind, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void PortabilityWorkflowBuildsTestsPublishesAndExecutesTheLauncher()
|
|
{
|
|
string workflow = File.ReadAllText(Path.Combine(
|
|
FindRepositoryRoot(),
|
|
".github",
|
|
"workflows",
|
|
"headless-portability.yml"));
|
|
|
|
// The four `**` path filters this used to assert belonged to the
|
|
// push/pull_request triggers that `8be14d39 ci: make GitHub workflows
|
|
// manual only` deliberately removed; the workflow is workflow_dispatch
|
|
// now. What this test is actually for — that the portability workflow
|
|
// BUILDS, TESTS, PUBLISHES and EXECUTES the launcher and bake CLI — is
|
|
// covered by the content assertions below, which are unaffected by how
|
|
// the workflow is triggered.
|
|
Assert.Contains("portable-launcher:", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("tests/AcDream.Launcher.Tests/AcDream.Launcher.Tests.csproj", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("tests/AcDream.Bake.Tests/AcDream.Bake.Tests.csproj", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("-r linux-x64", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("-getProperty:SelfContained", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("DOTNET_ROOT", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("--verify-publish", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("acdream-bake.exe\" --help", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("\"$root/acdream-bake\" --help", workflow, StringComparison.Ordinal);
|
|
Assert.Contains("test -x \"$root/acdream-bake\"", workflow, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"test -x src/AcDream.Headless/bin/Release/net10.0/acdream-headless",
|
|
workflow,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains("test -x \"$root/AcDream.App\"", workflow, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static string EvaluateProperty(string projectPath, string property)
|
|
{
|
|
var startInfo = new ProcessStartInfo("dotnet")
|
|
{
|
|
RedirectStandardError = true,
|
|
RedirectStandardOutput = true,
|
|
UseShellExecute = false,
|
|
};
|
|
startInfo.ArgumentList.Add("msbuild");
|
|
startInfo.ArgumentList.Add(projectPath);
|
|
startInfo.ArgumentList.Add("-nologo");
|
|
startInfo.ArgumentList.Add("-property:RuntimeIdentifier=linux-x64");
|
|
startInfo.ArgumentList.Add($"-getProperty:{property}");
|
|
|
|
using Process process = Process.Start(startInfo)
|
|
?? throw new InvalidOperationException("Could not start dotnet msbuild.");
|
|
string output = process.StandardOutput.ReadToEnd();
|
|
string error = process.StandardError.ReadToEnd();
|
|
Assert.True(process.WaitForExit(30_000), "dotnet msbuild did not exit.");
|
|
Assert.True(
|
|
process.ExitCode == 0,
|
|
$"dotnet msbuild exited {process.ExitCode}: {error}");
|
|
return output.Trim();
|
|
}
|
|
|
|
private static int Count(string text, string value) =>
|
|
text.Split(value, StringSplitOptions.None).Length - 1;
|
|
|
|
private static string FindRepositoryRoot()
|
|
{
|
|
foreach (string start in new[] { AppContext.BaseDirectory, Environment.CurrentDirectory })
|
|
{
|
|
DirectoryInfo? directory = new(start);
|
|
while (directory is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
|
{
|
|
return directory.FullName;
|
|
}
|
|
|
|
directory = directory.Parent;
|
|
}
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
|
}
|
|
}
|