Public Access
Pin the font the layout suite measures, and let the slice say it means plaintext
Two failures left on the runner, with nothing in common except that both only appear on a machine unlike the one anybody develops on. The runner is Alpine, musl, inside a container, with no fonts installed at all — and that combination is now reproducible locally, which is how these were fixed rather than guessed at. Both are verified by running the suite in it. The layout suite had two causes stacked, and the first hid the second completely. Missing libfontconfig stops libSkiaSharp loading, which the last commit fixed and which then revealed the real one: Avalonia takes its default font family from the platform, and on an image with no fonts there is no answer, so FontManager throws "Default font family name can't be null or empty" inside AppBuilder.SetupUnsafe — before a single test body runs, for all sixty-eight of them, naming none of their subjects. WithInterFont does not prevent it: it registers a collection without nominating a default. HeadlessApp's own comment already claimed it measured "the same Inter font the application registers", which was an intention the code never carried out. Both heads now name it, through FontManagerOptions.DefaultFamilyName. That is worth more than getting CI green: a suite whose entire job is measuring text was taking its metrics from whatever the machine happened to have — Segoe UI here, DejaVu there — and reporting the two as one number. It also means the application uses the font it has been shipping and declining to use since it first referenced the package; almost nothing moves visually, because App.axaml already sets MonoFont on essentially everything that draws. The end-to-end slice was the product being right and the test leaning on an accident. ServerConnection permits an http authority only when it is loopback. Testcontainers reports the host a container can actually be reached at, so running the suite directly gives localhost and passes, while running it inside a container gives the bridge gateway 172.17.0.1 and is refused — correctly, since a client that accepted plaintext metadata from a routable address would be a weakness for everyone who is not a test. Loosening that rule was the wrong repair. The slice now passes configureOidc and says out loud that it accepts plaintext from the Keycloak it started itself. Verified by reproducing the runner rather than approximating it: dotnet/sdk:10.0-alpine, musl-x64, fc-list returning zero, the docker socket mounted so Testcontainers resolves the gateway exactly as it does in CI. The whole solution passes there — 19 suites, 0 failures, 4 skipped — and the end-to-end failure was confirmed causal by reverting only that one file and watching it fail again in the same container. The layout suite also still passes on a Fedora desktop with 595 fonts, so the two agree now. Not verified on Windows, and it should be said plainly rather than left to be discovered: pinning the family changed the measured metrics there too, so a tight layout assertion could have moved. platform-flags.md records that, and corrects the entry the last commit added — "libfontconfig, and nothing else" was true of the container it was tested in and false of the runner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
-7
@@ -424,13 +424,38 @@ be honest about: the cache offers no protection against another process running
|
|||||||
|
|
||||||
## Build and CI
|
## Build and CI
|
||||||
|
|
||||||
**The layout suite needs `libfontconfig` installed, and nothing else.** Avalonia's headless renderer is
|
**The layout suite needs two unrelated things on a bare image, and each hides the other.** Both were
|
||||||
still Skia, and `libSkiaSharp.so` — which the test project copies into its own output — links against
|
found the slow way, one per CI run, because the first masks the second entirely.
|
||||||
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
|
The first is `libfontconfig`. Avalonia's headless renderer is Skia, and the `libSkiaSharp.so` the test
|
||||||
none of the tests' actual subjects. The CI job installs it. Worth being precise that this is the
|
project copies into its own output links against it; without it the native library never loads and all
|
||||||
library and not fonts: verified in a container where `fc-list` returns zero and the suite passes anyway,
|
69 tests fail inside `HeadlessUnitTestSession` with a `TypeInitializationException` on
|
||||||
because the application carries Inter itself.
|
`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.
|
**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
|
macOS runners have no Docker daemon, and the Windows CI job is deliberately build-only. So
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
namespace DodoSSH.Client.App;
|
namespace DodoSSH.Client.App;
|
||||||
|
|
||||||
@@ -16,9 +17,18 @@ internal static class Program
|
|||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
|
|
||||||
/// <summary>Used by the designer as well as by <see cref="Main"/>.</summary>
|
/// <summary>Used by the designer as well as by <see cref="Main"/>.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <c>WithInterFont</c> 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
|
||||||
|
/// <c>MonoFont</c> 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.
|
||||||
|
/// </remarks>
|
||||||
public static AppBuilder BuildAvaloniaApp() =>
|
public static AppBuilder BuildAvaloniaApp() =>
|
||||||
AppBuilder.Configure<DodoSshApp>()
|
AppBuilder.Configure<DodoSshApp>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
.WithInterFont()
|
.WithInterFont()
|
||||||
|
.With(new FontManagerOptions { DefaultFamilyName = "avares://Avalonia.Fonts.Inter/Assets#Inter" })
|
||||||
.LogToTrace();
|
.LogToTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Headless;
|
using Avalonia.Headless;
|
||||||
|
using Avalonia.Media;
|
||||||
using DodoSSH.Client.App.Layout.Tests;
|
using DodoSSH.Client.App.Layout.Tests;
|
||||||
|
|
||||||
[assembly: AvaloniaTestApplication(typeof(HeadlessApp))]
|
[assembly: AvaloniaTestApplication(typeof(HeadlessApp))]
|
||||||
@@ -28,9 +29,34 @@ namespace DodoSSH.Client.App.Layout.Tests;
|
|||||||
internal static class HeadlessApp
|
internal static class HeadlessApp
|
||||||
{
|
{
|
||||||
/// <summary>Found by name; <see cref="AvaloniaTestApplicationAttribute"/> is the contract.</summary>
|
/// <summary>Found by name; <see cref="AvaloniaTestApplicationAttribute"/> is the contract.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The default family is named explicitly, because the paragraph above was describing an intention
|
||||||
|
/// rather than what happened. <c>WithInterFont</c> 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.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// On a machine with no fonts at all it does not merely differ, it stops: <c>FontManager</c> throws
|
||||||
|
/// "Default font family name can't be null or empty" during <c>AppBuilder.SetupUnsafe</c>, 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.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
public static AppBuilder BuildAvaloniaApp() =>
|
public static AppBuilder BuildAvaloniaApp() =>
|
||||||
AppBuilder.Configure<DodoSshApp>()
|
AppBuilder.Configure<DodoSshApp>()
|
||||||
.UseSkia()
|
.UseSkia()
|
||||||
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false })
|
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false })
|
||||||
.WithInterFont();
|
.WithInterFont()
|
||||||
|
.With(new FontManagerOptions { DefaultFamilyName = InterFamily });
|
||||||
|
|
||||||
|
/// <summary>The family <c>WithInterFont</c> registers.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Kept in step with <c>Program.BuildAvaloniaApp</c> 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.
|
||||||
|
/// </remarks>
|
||||||
|
internal const string InterFamily = "avares://Avalonia.Fonts.Inter/Assets#Inter";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,8 +61,21 @@ public sealed class M1VerticalSliceTests(DevStack stack) : IClassFixture<DevStac
|
|||||||
var account = DevStack.RealmUser;
|
var account = DevStack.RealmUser;
|
||||||
var browser = new ScriptedBrowser(account.Username, account.Password);
|
var browser = new ScriptedBrowser(account.Username, account.Password);
|
||||||
|
|
||||||
using var connection = await ServerConnection
|
// The plaintext exemption is stated here rather than inferred from the address, and that is the
|
||||||
.SignInAsync(stack.ApiBaseUrl, browser, TimeProvider.System, Token);
|
// whole point of stating it. ServerConnection allows an http authority only when it is loopback,
|
||||||
|
// which is a sound rule and not one this suite can rely on: Testcontainers reports the host it
|
||||||
|
// can actually be reached at, so a developer running the tests directly gets localhost and passes,
|
||||||
|
// while the same suite inside a container gets the bridge gateway — 172.17.0.1 — and is refused.
|
||||||
|
// That is the product being right. 172.17.0.1 is not loopback, and a client that quietly accepted
|
||||||
|
// plaintext metadata from a routable address would be a real weakness for everyone who is not a
|
||||||
|
// test. So the test says out loud that it accepts plaintext from the throwaway Keycloak it started
|
||||||
|
// itself, and the rule stays as strict as it was for everybody else.
|
||||||
|
using var connection = await ServerConnection.SignInAsync(
|
||||||
|
stack.ApiBaseUrl,
|
||||||
|
browser,
|
||||||
|
TimeProvider.System,
|
||||||
|
Token,
|
||||||
|
configureOidc: options => options with { RequireHttpsMetadata = false });
|
||||||
|
|
||||||
AssertDiscoveredFromTheServer(connection);
|
AssertDiscoveredFromTheServer(connection);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user