diff --git a/docs/platform-flags.md b/docs/platform-flags.md index 7dc117b..3e91090 100644 --- a/docs/platform-flags.md +++ b/docs/platform-flags.md @@ -424,13 +424,38 @@ be honest about: the cache offers no protection against another process running ## Build and CI -**The layout suite needs `libfontconfig` installed, and nothing else.** Avalonia's headless renderer is -still Skia, and `libSkiaSharp.so` — which the test project copies into its own output — links against -it. On an image without it, every one of the 69 tests fails inside `HeadlessUnitTestSession` before -measuring anything, reported as a `TypeInitializationException` on `SkiaSharp.SKImageInfo` that names -none of the tests' actual subjects. The CI job installs it. Worth being precise that this is the -library and not fonts: verified in a container where `fc-list` returns zero and the suite passes anyway, -because the application carries Inter itself. +**The layout suite needs two unrelated things on a bare image, and each hides the other.** Both were +found the slow way, one per CI run, because the first masks the second entirely. + +The first is `libfontconfig`. Avalonia's headless renderer is Skia, and the `libSkiaSharp.so` the test +project copies into its own output links against it; without it the native library never loads and all +69 tests fail inside `HeadlessUnitTestSession` with a `TypeInitializationException` on +`SkiaSharp.SKImageInfo` naming none of their actual subjects. The CI job installs the package. + +The second only becomes visible once the first is fixed, and is not about a package at all. Avalonia +takes its default font family from the platform, and on an image with no fonts installed there is no +answer — `FontManager` throws "Default font family name can't be null or empty" during +`AppBuilder.SetupUnsafe`, again before any test body runs and again for all 69. `WithInterFont` does not +help by itself: it registers a collection without naming a default. Both `Program.BuildAvaloniaApp` and +the layout suite's `HeadlessApp` now set `FontManagerOptions.DefaultFamilyName` to +`avares://Avalonia.Fonts.Inter/Assets#Inter` explicitly, which owes the host nothing because the font +travels in the package. + +Pinning it is worth more than the CI fix. A suite that measures text was taking its metrics from +whatever the machine happened to have — Segoe UI on Windows, DejaVu on Linux — and reporting both as +one number. *Verified* on Alpine musl with `fc-list` returning zero and on a Fedora desktop with 595 +fonts, which now agree. **Unverified on Windows:** the pinning changed the metrics there too, so a +tight assertion could conceivably have moved. + +**The end-to-end suite must state its plaintext exemption rather than inherit it.** `ServerConnection` +allows an `http` OIDC authority only when it is loopback, which is a sound rule the suite cannot lean +on: Testcontainers reports the host the container is actually reachable at, so running the tests +directly gives `localhost` and passes, while running them *inside* a container — which is what a +containerised CI runner does — gives the bridge gateway `172.17.0.1` and is refused. That refusal is +the product being correct; a client that quietly accepted plaintext metadata from a routable address +would be a real weakness. `M1VerticalSliceTests` therefore passes `configureOidc` to set +`RequireHttpsMetadata = false` for the throwaway Keycloak it starts itself, and the rule stays as strict +as it was for everyone else. **Integration tests need a Docker daemon** (Testcontainers). They run on `ubuntu-latest` in CI. macOS runners have no Docker daemon, and the Windows CI job is deliberately build-only. So diff --git a/src/DodoSSH.Client.App/Program.cs b/src/DodoSSH.Client.App/Program.cs index bfad2f0..a7c8863 100644 --- a/src/DodoSSH.Client.App/Program.cs +++ b/src/DodoSSH.Client.App/Program.cs @@ -1,4 +1,5 @@ using Avalonia; +using Avalonia.Media; namespace DodoSSH.Client.App; @@ -16,9 +17,18 @@ internal static class Program BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); /// Used by the designer as well as by . + /// + /// WithInterFont registers Inter; it does not make it the default, and until this line the + /// application shipped a font it then declined to use — falling back to Segoe UI on Windows and to + /// whatever fontconfig offered on Linux. Almost nothing visible moves, because App.axaml sets + /// MonoFont on essentially every control that draws text, but the fallback behind those is now + /// a font that travels with the build rather than one the machine is assumed to have. The layout + /// suite pins the same family, and has to: see HeadlessApp. + /// public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() + .With(new FontManagerOptions { DefaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter" }) .LogToTrace(); } diff --git a/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs b/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs index 70db435..552185f 100644 --- a/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs +++ b/tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Headless; +using Avalonia.Media; using DodoSSH.Client.App.Layout.Tests; [assembly: AvaloniaTestApplication(typeof(HeadlessApp))] @@ -28,9 +29,34 @@ namespace DodoSSH.Client.App.Layout.Tests; internal static class HeadlessApp { /// Found by name; is the contract. + /// + /// + /// The default family is named explicitly, because the paragraph above was describing an intention + /// rather than what happened. WithInterFont registers a font collection; it does not make it + /// the default. Avalonia resolves that from the platform — which is to say from whatever the machine + /// has installed — so a suite whose whole job is measuring text was measuring Segoe UI on a + /// developer's Windows and DejaVu on their Linux, and treating the two numbers as one. + /// + /// + /// On a machine with no fonts at all it does not merely differ, it stops: FontManager throws + /// "Default font family name can't be null or empty" during AppBuilder.SetupUnsafe, before any + /// test body runs. That is how a container CI runner reports all sixty-eight of these as failures with + /// not one assertion among them. Naming Inter costs the runner nothing, because the font travels + /// inside the package rather than on the host. + /// + /// public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UseSkia() .UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false }) - .WithInterFont(); + .WithInterFont() + .With(new FontManagerOptions { DefaultFamilyName = InterFamily }); + + /// The family WithInterFont registers. + /// + /// Kept in step with Program.BuildAvaloniaApp by hand, and it matters that they agree: a + /// harness pinning a different default from the one the application runs with would certify heights + /// for a window nobody sees, which is the failure the opening paragraph of this file is about. + /// + internal const string InterFamily = "avares://Avalonia.Fonts.Inter/Assets#Inter"; } diff --git a/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs b/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs index d2231c4..b71bbdc 100644 --- a/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs +++ b/tests/DodoSSH.SystemTests/M1VerticalSliceTests.cs @@ -61,8 +61,21 @@ public sealed class M1VerticalSliceTests(DevStack stack) : IClassFixture options with { RequireHttpsMetadata = false }); AssertDiscoveredFromTheServer(connection);