Skip to content

Add containerized test legs (Azure Linux, Alpine/musl, Nano Server) - #4515

Merged
mattleibow merged 23 commits into
mainfrom
mattleibow-nano-server-ci-tests
Jul 23, 2026
Merged

Add containerized test legs (Azure Linux, Alpine/musl, Nano Server)#4515
mattleibow merged 23 commits into
mainfrom
mattleibow-nano-server-ci-tests

Conversation

@mattleibow

@mattleibow mattleibow commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

What

Runs the SkiaSharp console test suite (SkiaSharp.Tests.Console) inside a Docker container against a chosen native build, covering container runtimes the agent-based test legs don't:

Leg (display name) Image Native build
Linux (Azure Linux) Azure Linux SDK + fonts linux
Linux (Azure Linux, No Dependencies) Azure Linux SDK, bare linuxnodeps
Linux (Alpine) Alpine/musl SDK + fonts alpine
Linux (Alpine, No Dependencies) Alpine/musl SDK, bare alpinenodeps
Windows (Nano Server) Nano Server .NET SDK nanoserver

It reuses the repo's existing bootstrapper docker: feature — the same mechanism the native Linux builds use: build an env image with the Docker@2 task, then run dotnet cake --target=tests-container against the mounted repo. Native libraries come from the merged native pipeline artifact — nothing is downloaded or rebuilt in the leg. The legs are gating: a genuine failure fails the build.

How

  • tests-container cake target (scripts/infra/tests/tests-container.cake + build.cake) — builds only the console suite with -p:TargetFrameworks=net10.0 (single-TFM, so no Android/iOS workloads), then runs it. Builds no externals.
  • Native selection--nativePlatform=<platform> sets a SkiaSharpNativePlatform / HarfBuzzSharpNativePlatform MSBuild property in binding/IncludeNativeAssets.*.targets, so the build copies only the library from output/native/<platform>/<arch>/ (mirroring the existing OS-derived arch selection: $(Platform) when set, else host arch). This is how the Windows container runs the nanoserver build and Alpine runs the musl build (neither is mapped by the OS-derived defaults). Empty property = unchanged default behavior.
  • Env imagesscripts/infra/tests/docker/{azurelinux,azurelinux-nodeps,alpine,alpine-nodeps,nanoserver}/Dockerfile. Each image provisions only what its scenario needs (see Images below).
  • Bootstrapper — the docker: feature now runs on Windows agents too, branching the run step's shell and mount path by agent OS. The Linux path runs the suite via /bin/sh (POSIX, so the bare images need no shell package); the Windows path writes and self-prints a cmd.cmd mirroring the Linux cmd.sh.
  • Nano Server build steps — the Nano SDK image lacks sn.exe and can't run mdoc.exe, both Windows-only build steps. The container build sets DisableStrongNameSigning (gates the sn -R re-sign; PublicSign still gives the correct identity for InternalsVisibleTo) and SkipMDocGenerateDocs (doc generation is irrelevant to running tests).

Images

The glibc container uses Azure Linux rather than a second Ubuntu run (the desktop Linux leg already runs on Ubuntu) — a different, minimal glibc distro.

The No Dependencies legs run in bare images that install nothing beyond the base .NET SDK. That is the point of those legs: they exercise the skia_use_fontconfig=false native build against zero optional system packages, and fail if libSkiaSharp ever grows an undeclared dependency. The base SDK images already provide everything the native library links against (libstdc++, libgcc, libc, ICU), so nothing is installed — and the Alpine (full) image was trimmed to fonts only for the same reason.

Fonts

The nodeps and Nano Server builds have no enumerable system fonts (skia_use_fontconfig=false / the custom-empty font manager), so font-dependent tests self-skip at runtime rather than fail. Two probes in BaseTest capture the two distinct cases:

  • SkipWhenNoSystemFontManager() — no manager to enumerate or match fonts (SKFontManager.Default.MatchCharacter('a') is null; guards IsBrowser first). Covers WASM, the nodeps builds, and Nano Server.
  • SkipWhenNoDefaultFont() — no usable default typeface (SKTypeface.Default.IsEmpty). Covers the nodeps builds and Nano Server, but not WASM (which keeps a single embedded default font).

For the fontconfig legs: Alpine installs font-noto-emoji (packaged). Azure Linux has no emoji font in any repo, so its Dockerfile downloads Noto Emoji (SIL OFL), pinned by URL commit and verified by SHA-256, for the Unicode tests (covers U+1F680). The glibc UnicodeFontFamilies list accepts both Symbola (Ubuntu desktop) and Noto Emoji (Azure Linux).

Nano Server test fixes

The first full Nano run surfaced six failures, all understood environment differences (not harness bugs), each addressed properly:

  • XPS (SKDocumentTest) — Nano has no XPS Object Model / DirectWrite, so CreateXps returns null even though it IsWindows. Split the three tests that branched on IsWindows into straight-line supported tests plus one CreateXpsReturnsNullWhereUnsupported for the null contract, gated by SupportsXps = IsWindows && !IsNanoServer.
  • System fonts (SKTypefaceTest / SKFontManagerTest GCStillCollectsTypeface) — both resolve Times New Roman / C:\Windows\Fonts\times.ttf, absent on fontless Nano; add SkipWhenNoSystemFontManager alongside the existing Windows-path guard.
  • Visual text (VisualMatrixTests raster/Text) — Nano rasterizes text with FreeType, not DirectWrite, so it gets a nanoserver golden tag that falls back to the shared windows golden for cells that render identically (shapes). Committed the harvested raster.nanoserver/Text.png.

Nano is detected via TestConfig.IsNanoServer, which reads the registry InstallationType value ("Nano Server") — Windows-guarded, wrapped in try/catch (a constrained host can deny the read), and isolated so the Win32 types are only resolved on Windows.

Job display names

Test-job display names now lead with the platform, with brackets only for real variations: Windows (.NET Framework) / Windows (.NET Core) / Windows (Nano Server), macOS, Linux / Linux (Azure Linux[, No Dependencies]) / Linux (Alpine[, No Dependencies]), Android, iOS, Mac Catalyst, WASM / WASM (.NET Preview).

Validation

All five container legs run and pass in CI (Nano Server: 5689 passed / 0 failed / 225 skipped). Locally: Azure Linux 5718 passed / 0 failed / 185 skipped with the downloaded emoji font resolving U+1F680; the bare nodeps images load the native library clean and self-skip the font tests; the trimmed Alpine image resolves fonts and runs with no bash.

Musl stress-test skip

Adding the first musl (Alpine) legs surfaced a latent hang in SKBitmapThreadingTest.ImageScalingMultipleThreadsTest — the (100 threads, 1000 iterations) variant, which deliberately creates ~100K undisposed native allocations to stress GC-finalizer throughput (#3608). It already self-skips on x86 (where "the GC can't keep up and Skia's native allocator fails"); on x64 musl the same pressure stalls the allocator/finalizer interaction and the run hangs until hangdump kills the leg. The existing self-skip is extended to musl (glibc keeps up and still runs it), so the lighter (10,10) / (10,100) variants — and the #3608 regression guard — keep running there. Reproduced deterministically against the real CI x64 musl native lib and fixed.

Copilot AI added 3 commits July 22, 2026 11:57
Runs the SkiaSharp console test suite inside a container using the repo's
existing bootstrapper `docker:` feature — the same compliant mechanism the
native Linux builds use (build the env image with the approved container-image
task, then run `dotnet cake --target=<target>` against the mounted repo).
No bespoke multi-stage Dockerfiles, no wrapper script, no raw docker build.

- scripts/infra/tests/tests-container.cake + build.cake 'tests-container' target:
  runs ONLY the console suite (no externals build). Builds with
  -p:TargetFrameworks=net10.0 to avoid the Android workload, and selects the
  native build under test with --nativePlatform=<linux|alpine|nanoserver>.
- Real native swap (not an overlay): --nativePlatform flows to the
  SkiaSharp/HarfBuzzSharp NativePlatform MSBuild property in
  binding/IncludeNativeAssets.*.targets, so the build copies ONLY that
  platform's library. This is what lets the Windows container run the
  nanoserver lib and Alpine run the musl lib (neither is mapped by the
  OS-derived defaults). Empty property = unchanged default behavior.
- scripts/infra/tests/docker/{glibc,alpine,nanoserver}/Dockerfile: environment
  images (Linux glibc + fonts; Alpine/musl + fonts; Nano Server .NET SDK with
  no fonts — the env under test).
- azure-templates-jobs-bootstrapper.yml: extend the docker feature to support
  Windows containers (branch shell/mount by agent OS; only pass
  --platform linux/amd64 on non-Windows). Native docker legs unaffected.
- azure-templates-stages-test.yml: tests_container_linux,
  tests_container_alpine_linux, tests_container_nanoserver_windows legs —
  docker: env image + target: tests-container, consuming the `native` artifact
  (never externals-download), publishing the TRX. Enabled by default,
  non-gating. Intentionally do NOT flow use1ESPipelineTemplates (tests never
  run on 1ES). TRX goes to the standard ADO Tests tab via PublishTestResults.
- documentation/dev/containerized-testing.md: full design + real proofs.

Proven end-to-end locally via the actual feature commands (build env image,
docker run cake target on the mounted repo):
- Linux glibc: 5902 tests, 5720 passed, 182 skipped, 0 failed; the swap put
  ONLY the linux .so in the output dir (no .dll/.dylib).
- Alpine musl: 5715 passed, 182 skipped, 5 failed — 4 are missing-Symbola-font
  environment gaps on Alpine, 1 is a known GC-timing flake; exactly the kind of
  per-platform result these non-gating legs exist to catalogue.
Nano Server is the same path on a Windows container host (cannot run on this
macOS host).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
Rewrite documentation/dev/containerized-testing.md and the related YAML/cake/
targets comments to describe what the feature is and how it works, without
narrating prior iterations, contrasts with removed approaches, or transient
test results.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
@github-actions

Copy link
Copy Markdown
Contributor

📦 Try the packages from this PR

Warning

Do not run these scripts without first reviewing the code in this PR.

Step 1 — Download the packages

bash / macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 4515

PowerShell / Windows:

iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4515"

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-4515/packages --name skiasharp-pr-4515
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-4515

@github-actions

Copy link
Copy Markdown
Contributor

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

Alpine's font-dejavu doesn't cover the emoji (U+1F680) the unicode font tests
need, and Alpine has no Symbola package. Install font-noto-emoji (family
"Noto Color Emoji") in the Alpine env image, and pick UnicodeFontFamilies per
libc (Symbola on glibc, Noto Color Emoji on musl) via PlatformConfiguration.IsGlibc.
Explain the fontconfig vs non-fontconfig font behavior in the docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

📊 SkiaSharp benchmarks — PR #4515

this PR (full source build) vs 🌙 nightly · Linux · Windows · macOS

Informational only — this never blocks the PR. 🟢 faster / less allocation · 🔴 slower / more allocation; moves under 5% are hidden as noise.

⏱️ Times are raw BenchmarkDotNet means, and the ⭐ PR and baseline legs run on separate CI runners, so microbenchmarks can swing run-to-run — treat small time deltas as noise. Allocations are deterministic and the reliable signal. The interactive perf-dashboard (linked below) applies smoothing for the trend view.

Highlights

⏱️ Time — 🔴 54 slower · 🟢 24 faster

  • 🔴 ColorMathBenchmark.ToColor(Colors: 4096) · Windows · 5.74 µs → 16.81 µs (+193%)
  • 🔴 MatrixMapPointsBenchmark.MapPoints(Points: 4096) · Windows · 1.42 µs → 3.77 µs (+166%)
  • 🔴 MatrixMapPointsBenchmark.MapPoints(Points: 256) · Windows · 104.7 ns → 242.6 ns (+132%)
  • 🔴 PathBoundsBenchmark.TightBounds(Points: 1024) · Windows · 9.45 µs → 20.35 µs (+115%)
  • 🔴 PathBoundsBenchmark.TightBounds(Points: 64) · Windows · 916.9 ns → 1.90 µs (+107%)
  • …and 73 more (see details below)
Full per-OS benchmark deltas

Linux

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.90)

Benchmark baseline this PR Δ
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 97.23 µs 124.69 µs 🔴 +28%
MatrixOpsBenchmark.Invert(Count: 4096) 153.26 µs 127.74 µs 🟢 -17%
MatrixMapPointsBenchmark.MapRect(Points: 256) 13.00 µs 11.10 µs 🟢 -15%
MatrixMapPointsBenchmark.MapRect(Points: 4096) 207.50 µs 177.58 µs 🟢 -14%
ColorMathBenchmark.UnPreMultiply(Colors: 4096) 12.96 µs 11.40 µs 🟢 -12%
ColorMathBenchmark.PreMultiply(Colors: 4096) 14.48 µs 13.07 µs 🟢 -10%
MatrixMapPointsBenchmark.MapRadius(Points: 4096) 246.99 µs 223.42 µs 🟢 -10%
PathBoundsBenchmark.TightBounds(Points: 1024) 11.27 µs 10.23 µs 🟢 -9%
MatrixMapPointsBenchmark.MapPoints(Points: 4096) 2.69 µs 2.94 µs 🔴 +9%
MatrixOpsBenchmark.Concat(Count: 4096) 239.45 µs 218.50 µs 🟢 -9%
MatrixMapPointsBenchmark.MapVector(Points: 4096) 191.77 µs 175.09 µs 🟢 -9%
MatrixMapPointsBenchmark.MapPoint(Points: 4096) 80.43 µs 73.58 µs 🟢 -9%
MatrixMapPointsBenchmark.MapRadius(Points: 256) 15.24 µs 13.97 µs 🟢 -8%
MatrixMapPointsBenchmark.MapVector(Points: 256) 11.88 µs 11.01 µs 🟢 -7%
MatrixMapPointsBenchmark.MapPoint(Points: 256) 4.99 µs 4.63 µs 🟢 -7%
RuntimeEffectShaderBenchmark.DrawFrame 3.03 ms 2.83 ms 🟢 -6%
ColorMathBenchmark.ToColorF(Colors: 4096) 14.02 µs 13.14 µs 🟢 -6%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 64) 266.73 µs 251.16 µs 🟢 -6%
CanvasDrawBenchmark.Draw(Shapes: 64) 4.44 ms 4.18 ms 🟢 -6%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 256) 994.13 µs 939.05 µs 🟢 -6%
CanvasDrawBenchmark.Draw(Shapes: 512) 35.26 ms 33.34 ms 🟢 -5%
RasterImageLifecycleBenchmark.CreateRasterImage(Count: 256) 256.94 µs 270.70 µs 🔴 +5%
MatrixMapPointsBenchmark.MapPoints(Points: 256) 226.8 ns 238.7 ns 🔴 +5%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) 1.41 ms 1.34 ms 🟢 -5%

Windows

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.90)

Benchmark baseline this PR Δ
ColorMathBenchmark.ToColor(Colors: 4096) 5.74 µs 16.81 µs 🔴 +193%
MatrixMapPointsBenchmark.MapPoints(Points: 4096) 1.42 µs 3.77 µs 🔴 +166%
MatrixMapPointsBenchmark.MapPoints(Points: 256) 104.7 ns 242.6 ns 🔴 +132%
PathBoundsBenchmark.TightBounds(Points: 1024) 9.45 µs 20.35 µs 🔴 +115%
PathBoundsBenchmark.TightBounds(Points: 64) 916.9 ns 1.90 µs 🔴 +107%
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 67.93 µs 136.29 µs 🔴 +101%
CanvasDrawBenchmark.Draw(Shapes: 512) 11.51 ms 22.61 ms 🔴 +96%
CanvasDrawBenchmark.Draw(Shapes: 64) 1.47 ms 2.84 ms 🔴 +93%
RuntimeEffectShaderBenchmark.DrawFrame 604.24 µs 1.13 ms 🔴 +87%
ColorParseBenchmark.Parse(Iterations: 1000) 44.71 µs 83.11 µs 🔴 +86%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 64) 185.55 µs 344.92 µs 🔴 +86%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 256) 525.31 µs 965.70 µs 🔴 +84%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) 703.67 µs 1.29 ms 🔴 +84%
RasterImageLifecycleBenchmark.CreateRasterImage(Count: 256) 164.06 µs 299.08 µs 🔴 +82%
SceneRenderBenchmark.RenderFrame(Complexity: 1) 1.22 ms 2.18 ms 🔴 +78%
ColorMathBenchmark.PreMultiply(Colors: 4096) 7.48 µs 13.19 µs 🔴 +76%
MatrixMapPointsBenchmark.MapRadius(Points: 4096) 94.86 µs 166.63 µs 🔴 +76%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 64) 143.52 µs 252.09 µs 🔴 +76%
SceneRenderBenchmark.RenderFrame(Complexity: 4) 6.03 ms 10.54 ms 🔴 +75%
MatrixMapPointsBenchmark.MapRadius(Points: 256) 5.96 µs 10.39 µs 🔴 +74%
MatrixOpsBenchmark.Concat(Count: 4096) 146.70 µs 252.58 µs 🔴 +72%
ColorMathBenchmark.ToColorF(Colors: 4096) 7.95 µs 13.56 µs 🔴 +71%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 2048) 6.63 ms 11.21 ms 🔴 +69%
MatrixMapPointsBenchmark.MapVector(Points: 256) 5.23 µs 8.82 µs 🔴 +69%
ColorMathBenchmark.UnPreMultiply(Colors: 4096) 6.96 µs 11.54 µs 🔴 +66%
…and 9 more

macOS

⏱️ Time (vs 🌙 nightly 4.151.0-nightly.90)

Benchmark baseline this PR Δ
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 256) 979.39 µs 699.74 µs 🟢 -29%
RasterImageLifecycleBenchmark.CreateDataWithReleaseProc(Count: 256) 77.21 µs 98.93 µs 🔴 +28%
BitmapDrawBenchmark.DrawUnscaledTiles(Tiles: 64) 212.09 µs 174.14 µs 🟢 -18%
RasterImageLifecycleBenchmark.CreateRasterImage(Count: 256) 159.13 µs 187.17 µs 🔴 +18%
MatrixMapPointsBenchmark.MapPoints(Points: 256) 137.4 ns 160.9 ns 🔴 +17%
MatrixOpsBenchmark.Concat(Count: 4096) 120.27 µs 139.47 µs 🔴 +16%
RuntimeEffectShaderBenchmark.DrawFrame 580.43 µs 670.43 µs 🔴 +16%
BitmapDrawBenchmark.DrawScaledTiles(Tiles: 64) 228.60 µs 200.94 µs 🟢 -12%
PathBoundsBenchmark.TightBounds(Points: 1024) 9.16 µs 10.15 µs 🔴 +11%
LargeImageScaleBenchmark.UpscaleCrossfade(Size: 2048) 5.70 ms 6.27 ms 🔴 +10%
MatrixMapPointsBenchmark.MapRect(Points: 256) 3.28 µs 3.61 µs 🔴 +10%
ColorMathBenchmark.PreMultiply(Colors: 4096) 9.52 µs 8.68 µs 🟢 -9%
SceneRenderBenchmark.RenderFrame(Complexity: 1) 1.44 ms 1.56 ms 🔴 +8%
ColorParseBenchmark.Parse(Iterations: 1000) 68.80 µs 74.33 µs 🔴 +8%
LargeImageScaleBenchmark.UpscaleOpaque(Size: 2048) 2.68 ms 2.87 ms 🔴 +7%
CanvasDrawBenchmark.Draw(Shapes: 512) 12.65 ms 13.44 ms 🔴 +6%
MatrixMapPointsBenchmark.MapPoints(Points: 4096) 1.70 µs 1.80 µs 🔴 +6%
MatrixOpsBenchmark.Invert(Count: 4096) 72.70 µs 76.88 µs 🔴 +6%
MatrixMapPointsBenchmark.MapVector(Points: 256) 2.65 µs 2.80 µs 🔴 +6%
PathBoundsBenchmark.TightBounds(Points: 64) 968.7 ns 1.02 µs 🔴 +5%

📈 Full interactive perf-dashboard & run details →

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

📦 Artifact size report

Packages from this PR (build 1524177) vs the latest nightly baseline 4.151.0-nightly.90 (observed 2026-07-23).

Total .nupkg size: 537.4 MB → 535.4 MB (−2.0 MB, -0.4%)

Packages

⚠️ marks growth over 500.0 KB or 2%. Changes under 50.0 KB are treated as noise.

Package baseline this PR Δ Δ%
SkiaSharp.NativeAssets.WinUI 108.9 MB 107.1 MB 🟢 −1.8 MB -1.6%
HarfBuzzSharp.NativeAssets.Win32 20.8 MB 20.5 MB 🟢 −267.0 KB -1.3%

+38 package(s) unchanged (< 50.0 KB).

Per-file changes

HarfBuzzSharp.NativeAssets.Win32

File Size
runtimes/win-x86/native/libHarfBuzzSharp.pdb 22.2 MB → 21.9 MB (🟢 −272.0 KB)
runtimes/win-x64/native/libHarfBuzzSharp.pdb 22.0 MB → 21.8 MB (🟢 −240.0 KB)
runtimes/win-arm64/native/libHarfBuzzSharp.pdb 22.3 MB → 22.3 MB (🟢 −8.0 KB)

SkiaSharp.NativeAssets.WinUI

File Size
runtimes/win-arm64/native/libGLESv2.pdb 64.6 MB → 64.3 MB (🟢 −272.0 KB)
runtimes/win-x86/native/libGLESv2.pdb 67.3 MB → 67.0 MB (🟢 −256.0 KB)
runtimes/win-x64/native/libGLESv2.pdb 66.6 MB → 66.4 MB (🟢 −240.0 KB)
runtimes/win-x64/native/libEGL.pdb 6.4 MB → 6.3 MB (🟢 −32.0 KB)
runtimes/win-x86/native/libEGL.pdb 6.5 MB → 6.5 MB (🟢 −16.0 KB)
runtimes/win-arm64/native/SkiaSharp.Views.WinUI.Native.pdb 67.7 MB → 67.7 MB (🟢 −8.0 KB)
runtimes/win-x64/native/SkiaSharp.Views.WinUI.Native.pdb 68.0 MB → 68.0 MB (🟢 −8.0 KB)
🧬 win-x64 (runtimes/win-x64/native/libGLESv2.dll) 4.9 MB → 4.9 MB (🔴 +2.0 KB)
🧬 win-arm64 (runtimes/win-arm64/native/libGLESv2.dll) 4.4 MB → 4.4 MB (🔴 +1.0 KB)

Informational only — this never blocks the PR. Native binaries are labelled by os/arch.

Copilot AI added 7 commits July 22, 2026 14:53
Tests that need enumerable system fonts are tagged
[Trait("Category", "RequiresSystemFonts")]. The container legs whose native
build has no enumerable system fonts (linuxnodeps/alpinenodeps, built
skia_use_fontconfig=false, and Nano Server) pass --skipSystemFonts, which
forwards --filter-not-trait "Category=RequiresSystemFonts" to the runner.

- tests/Tests/Traits.cs: add the RequiresSystemFonts category.
- tag the 30 font-dependent methods (39 cases with theory data) across
  SKFontManagerTest, SKFontStyleSetTest, SKFontTest, SKPaintTest, SKTypefaceTest.
- tests-container.cake: add --skipSystemFonts; test-shared.cake RunDotNetTest
  gains an extraRunnerArgs passthrough.
- azure-templates-stages-test.yml: add Linux NoDeps + Alpine NoDeps legs and
  pass --skipSystemFonts=true on the fontless legs (interim filter, removable
  from the legs once the tests self-skip on a fontless environment).

Verified in-container: linuxnodeps without the filter fails 39 font tests; with
--skipSystemFonts it excludes exactly those 39 and is green (5681 passed, 182
skipped, 0 failed).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
Replaces the interim [Trait("Category","RequiresSystemFonts")] + --skipSystemFonts
approach with runtime capability probes, so the same suite runs unmodified
everywhere and each platform skips what it genuinely can't do.

Two capabilities, probed at runtime (BaseTest):
- SkipWhenNoSystemFontManager(): no manager to enumerate/match fonts — WASM
  (no manager), NoDependencies Linux builds and Nano Server (empty manager,
  MatchCharacter returns null). Applied to the SKFontManager/SKFontStyleSet and
  MatchCharacter (emoji) tests; supersedes their prior IsBrowser-only skips.
- SkipWhenNoDefaultFont(): default typeface is empty — NoDependencies and Nano
  Server (SKTypeface.Default.IsEmpty). Applied to the measure/break/draw tests
  that use the default typeface. WASM keeps its single embedded default font, so
  these keep running there.

Reverts the RequiresSystemFonts trait, the --skipSystemFonts cake switch and its
RunDotNetTest passthrough, and the --skipSystemFonts=true leg args.

Capability matrix confirmed by probing each build (SKFontManager.Default /
SKTypeface.Default): fontconfig linux/alpine have real fonts; linuxnodeps and
alpinenodeps report one empty family, an empty default typeface and null
MatchCharacter. Verified in-container: linuxnodeps self-skips and is green
(5681 passed, 221 skipped, 0 failed) with no filter; linux still runs the font
tests unchanged (5720 passed, 182 skipped).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
The Windows container run now writes each command on its own line to cmd.cmd and
prints it (Get-Content), then runs that file in the container — the same
self-printing script-file pattern as the Linux cmd.sh path, instead of an inline
'cmd /c a && b'.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
Remove continueOnError from the container legs so a failure fails the
build. If these platforms are worth building and testing, a regression
should block, not be silently ignored.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
The CI Linux agents are x64, so the base image already resolves to
linux/amd64 by default; the explicit flag was a carryover from local
arm64 testing and is a no-op on CI. Removing it collapses the per-OS
ternary to a plain tag argument.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new CI/testing capability to run SkiaSharp.Tests.Console inside Docker containers against selected prebuilt native outputs, extending coverage to container-only environments (Linux glibc, Alpine/musl, and Windows Nano Server) and enabling explicit native platform selection during managed builds.

Changes:

  • Introduces a tests-container Cake target plus container environment Dockerfiles, and wires new Azure Pipelines legs to run/publish TRX results.
  • Adds MSBuild native-asset overrides (SkiaSharpNativePlatform / HarfBuzzSharpNativePlatform) so container jobs can select output/native/<platform>/<arch>/ explicitly.
  • Updates font-related tests/config to be more container/font-manager aware (glibc vs musl font family selection; runtime skips for fontless environments).

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/Tests/SkiaSharp/SKTypefaceTest.cs Adjusts emoji/system-font test skipping behavior for container/fontless environments.
tests/Tests/SkiaSharp/SKPaintTest.cs Skips a pixel-assert text test when no default font exists (fontless containers).
tests/Tests/SkiaSharp/SKFontTest.cs Adds font-manager/default-font skips to stabilize tests across container/fontless environments.
tests/Tests/SkiaSharp/SKFontStyleSetTest.cs Replaces browser-only skips with “no system font manager” detection.
tests/Tests/SkiaSharp/SKFontManagerTest.cs Replaces browser-only skips with “no system font manager” detection.
tests/Tests/BaseTest.cs Adds shared helpers for detecting “no system font manager” / “no default font” and skipping accordingly.
tests/SkiaSharp.Tests.Console/DefaultTestConfig.Console.cs Selects Linux unicode font family based on glibc vs musl (Symbola vs Noto Color Emoji).
scripts/infra/tests/tests-container.cake New Cake script to build+run only the console tests in a container against prebuilt natives.
scripts/infra/tests/docker/nanoserver/Dockerfile New Windows Nano Server SDK env image for running console tests in a fontless Windows container.
scripts/infra/tests/docker/glibc/Dockerfile New Linux glibc SDK env image with required font packages.
scripts/infra/tests/docker/alpine/Dockerfile New Alpine/musl SDK env image with required runtime deps + fonts.
scripts/azure-templates-stages-test.yml Adds new containerized test legs and publishes TRX/artifacts.
scripts/azure-templates-jobs-bootstrapper.yml Extends docker: bootstrapper feature to run on Windows agents (cmd + Windows volume mount).
documentation/dev/README.md Adds dev docs link for containerized testing.
documentation/dev/containerized-testing.md New documentation describing how containerized test legs work and how to run locally.
build.cake Adds tests-container target entrypoint.
binding/IncludeNativeAssets.SkiaSharp.targets Adds SkiaSharpNativePlatform override to copy natives from output/native/<platform>/<arch>/.
binding/IncludeNativeAssets.HarfBuzzSharp.targets Adds HarfBuzzSharpNativePlatform override to copy natives from output/native/<platform>/<arch>/.
Comments suppressed due to low confidence (2)

tests/Tests/SkiaSharp/SKFontTest.cs:103

  • The SKTypeface returned by SKFontManager.Default.MatchCharacter should be disposed. Other tests use using var for this call; leaving it undisposed can leak native handles in long test runs.
			var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32);

			var typeface = SKFontManager.Default.MatchCharacter(emojiChar);
			Assert.NotNull(typeface);

			using var font = new SKFont();
			font.Typeface = typeface;

tests/Tests/SkiaSharp/SKTypefaceTest.cs:249

  • The SKTypeface returned by SKFontManager.Default.MatchCharacter should be disposed (it owns a native handle). Use using var here to avoid leaking the typeface across the test run.
			SkipWhenNoSystemFontManager();

			const string text = "🚀";
			var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32);

			var typeface = SKFontManager.Default.MatchCharacter(emojiChar);
			Assert.NotNull(typeface);

Comment thread tests/Tests/BaseTest.cs
Comment thread tests/Tests/SkiaSharp/SKFontTest.cs
Comment thread scripts/azure-templates-stages-test.yml
Comment thread scripts/azure-templates-stages-test.yml
Comment thread scripts/azure-templates-stages-test.yml
Comment thread scripts/azure-templates-stages-test.yml
Comment thread scripts/azure-templates-stages-test.yml
Copilot AI added 8 commits July 22, 2026 23:03
The doc still described the container legs as non-gating with
continueOnError; the legs are now gating and rely on the runtime
self-skip helpers for fontless environments. Correct the description.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
The Nano Server .NET SDK image has no sn.exe, so the Windows-only
_SignAssembly / _SignAssemblyVerify targets failed the container build
with exit code 9009 before any test could run. Gate both targets behind
a new DisableStrongNameSigning property and set it from the tests-container
cake target. PublicSign still applies, so the assemblies keep the public
key identity that InternalsVisibleTo requires (same as the Linux legs,
where these targets never run). Default behavior is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
With the sn.exe re-sign skipped, the Nano leg progressed to the next
Windows-only build step: MDocGenerateDocs runs mdoc.exe, which cannot
start on Nano Server (exit -1073741515 / STATUS_DLL_NOT_FOUND — missing
Windows DLL deps). Doc generation is irrelevant to running the test
suite, so set the existing SkipMDocGenerateDocs opt-out for all
container builds. These are the only two external-tool build steps
gated on IsWindows+Release, so no further whack-a-mole expected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
Build 158986 failed due to a lost tvOS test agent (infra flake); the
still-queued container legs were skipped as collateral and never ran.
Empty commit to get a clean run for the Nano Server leg verification.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
The first full Nano Server container run passed 5687/220 with six real
failures, all understood environment differences rather than harness
bugs. Address each properly:

XPS (SKDocumentTest) — Nano has no XPS Object Model / DirectWrite, so
CreateXps returns null even though it IsWindows. Split the three tests
that branched on IsWindows into straight-line supported tests (gated by
a SupportsXps capability probe) plus one CreateXpsReturnsNullWhereUnsupported
test for the null contract on Nano and non-Windows.

System fonts (SKTypefaceTest / SKFontManagerTest GCStillCollectsTypeface)
— both resolve 'Times New Roman' / C:\Windows\Fonts\times.ttf, absent on
fontless Nano. Add SkipWhenNoSystemFontManager alongside the existing
Windows-path guard.

Visual text (VisualMatrixTests raster/Text) — Nano rasterizes text with
FreeType, not DirectWrite, so it needs its own golden. Give Nano a
'nanoserver' golden tag that falls back to the shared 'windows' golden
for cells that render identically (shapes), and commit the harvested
raster.nanoserver/Text.png. Only divergent cells need a nano golden.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
Replace the create-and-check-null XPS probe with a real platform
indicator: TestConfig.IsNanoServer reads the registry InstallationType
value, which is 'Nano Server' on Nano and 'Client'/'Server'/'Server Core'
elsewhere. The registry read is Windows-guarded and isolated in a
non-inlined method so the Microsoft.Win32 types are only JIT-resolved on
Windows, never on the mobile/WASM test hosts (the type is framework-
provided, so no package reference is needed).

XPS support is now the declarative IsWindows && !IsNanoServer, and the
visual golden 'nanoserver' tag keys off IsNanoServer directly instead of
inferring it from an empty font manager.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
SupportsXps is a platform capability, so it belongs alongside IsWindows /
IsNanoServer in BaseTest rather than as a private field on SKDocumentTest.

Drop the singular VisualPlatform.Tag convenience accessor and use the
plural Tags everywhere: Tags[0] for the primary write key (golden/diff
marker paths, GoldenStore.Key) and the full Tags list in the unseeded-
golden message so it names every candidate platform.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
Fold the two-branch DetermineTags into one yield iterator: prepend the
'nanoserver' tag when on Nano Server, then yield the base OS tag from
DetermineTag (which is 'windows' on Nano). This drops the duplicated
'windows' literal so the shared tag has a single source, while keeping
OS identity (DetermineTag) separate from the golden-fallback policy.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 25 changed files in this pull request and generated 3 comments.

Comment thread binding/IncludeNativeAssets.SkiaSharp.targets
Comment thread binding/IncludeNativeAssets.HarfBuzzSharp.targets
Comment thread tests/Tests/TestConfig.cs
Copilot AI added 2 commits July 23, 2026 18:29
Three review-driven robustness fixes:

- TestConfig.ReadIsNanoServer wraps the registry read in try/catch. A
  constrained Windows host can deny the read (SecurityException, etc.),
  and because the result is cached via Lazy<bool> that would re-throw on
  every IsNanoServer access and fail the whole test config. Treat any
  failure as 'not Nano Server'.
- The native-platform override in IncludeNativeAssets.{SkiaSharp,
  HarfBuzzSharp}.targets now mirrors the existing OS-derived selection:
  use $(Platform) when it is set and not AnyCPU, else $(BuildArch).
  Previously it always used $(BuildArch), which could copy the wrong
  arch (e.g. x64 instead of x86) if the override were ever used with an
  explicit Platform. No behavior change for the container legs (they
  build AnyCPU), but it removes the inconsistency with the default logic.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
…arer names

Rework the Linux container test images so each leg tests what it claims,
and rename the test-job display names to lead with the platform.

Images:
- Replace the glibc container image (Ubuntu) with Azure Linux. The desktop
  Linux leg already runs on Ubuntu, so this exercises a different, minimal
  glibc distro instead of a second Ubuntu run.
- Give the No Dependencies legs their own bare images (azurelinux-nodeps,
  alpine-nodeps) that install NOTHING beyond the base .NET SDK. Previously
  they ran in the font-equipped images, which defeated the point: the leg
  exists to prove the nodeps native build (skia_use_fontconfig=false) works
  with no optional system packages. Verified the native libs load with only
  what the base image already provides.
- Trim the Alpine image: drop bash, libstdc++, libgcc and icu-libs. Those
  are all already in the base .NET SDK image (the native library never
  needed them installed separately); only the fonts remain.

Fonts:
- Azure Linux ships no emoji font in any repo, so its Dockerfile downloads
  Noto Emoji (SIL OFL), pinned by URL commit and verified by SHA-256, for
  the Unicode tests. The glibc UnicodeFontFamilies list accepts both
  Symbola (Ubuntu desktop) and Noto Emoji (Azure Linux).

Harness:
- Run the in-container suite via /bin/sh instead of /bin/bash so the bare
  images need no shell package; the generated script is POSIX.

Naming: display names now lead with the platform (macOS, Linux, Android,
iOS, WASM), with brackets only for real variations — Windows (.NET
Framework/.NET Core), WASM (.NET Preview), Linux (Azure Linux[, No
Dependencies]) / (Alpine[, No Dependencies]), Windows (Nano Server).

Validated locally: Azure Linux 5718 passed / 0 failed / 185 skipped with
the downloaded emoji font resolving U+1F680; bare nodeps images load the
native lib clean and self-skip the font tests; trimmed Alpine resolves
fonts and runs with no bash.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
@mattleibow mattleibow changed the title Add containerized test legs (Linux glibc, Alpine/musl, Nano Server) Add containerized test legs (Azure Linux, Alpine/musl, Nano Server) Jul 23, 2026
The Linux (Azure Linux, No Dependencies) and Linux (Alpine, No
Dependencies) container legs cover it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
mattleibow pushed a commit that referenced this pull request Jul 23, 2026
The new Alpine container legs intermittently killed the whole test run:
the leg would stall on "2 tests running" and MTP's hangdump force-killed
it (exit 7), or the process took SIGSEGV inside GPU shader compilation.
It was flaky — CI retries sometimes passed — which made it look like a
machine blip rather than a real fault.

Root cause: on Linux the GL context comes from GLX via libX11, and the
container has no X server. Normally XOpenDisplay(NULL) fails fast and
CreateGlContext()'s catch skips the test. But running that from parallel
GPU tests races the native GLX/GL init on musl: instead of failing fast
it intermittently hangs or crashes in downstream GPU SkSL compilation.
glibc fails fast reliably; musl does not, so only the Alpine legs flaked.
It is not a regression in this PR's code — the PR merely added the Alpine
legs that first exercise this path under load.

Fix: skip in CreateGlContext() before touching any native code when there
is no display (IsLinux and DISPLAY unset). The desktop Linux leg runs
these under Xvfb with DISPLAY set, so its GPU coverage is unchanged;
macOS/Windows use CGL/WGL rather than X11 and are unaffected. On headless
Linux these tests already skipped in the common case, so no coverage is
lost — only the racy native path is now never entered.

Verified in-container (parallel + hangdump): both Alpine legs green with
zero failures and no hang — alpine full 5718 passed / 185 skipped,
alpine nodeps 5679 passed / 224 skipped.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
The new Alpine (No Dependencies) leg intermittently failed with the leg
stalling on "2 tests running" until MTP's hangdump force-killed it (exit
code 7) — reported as flaky since CI retries sometimes squeaked through.

Root cause: SKBitmapThreadingTest.ImageScalingMultipleThreadsTest, the
(100 threads, 1000 iterations) variant. It intentionally creates ~100K
undisposed native allocations to stress GC-finalizer throughput (#3608)
and already self-skips on x86, where "the GC can't keep up and Skia's
native allocator fails". On x64 musl the same pressure across 100 threads
stalls the allocator/finalizer interaction and the run hangs indefinitely
instead of failing — deterministic once reproduced against the real CI
x64 musl native lib (glibc keeps up and passes, which is why the existing
glibc legs never hit this). It is not caused by this PR's code: the PR
merely adds the first musl test legs, so this musl-only stress path is
exercised here for the first time.

Extend the existing x86 self-skip to also cover musl for the heavy
variant. The lighter (10,10) and (10,100) variants still run everywhere,
so concurrent-scaling coverage — and the #3608 regression guard — remains
on musl. Add IsGlibc/IsMusl to TestConfig and IsMusl to BaseTest for the
guard (musl == Linux and not glibc, via the existing gnu_get_libc_version
probe already used for font selection).

Verified against the real CI x64 musl nodeps native lib under x86-64
emulation: the heavy variant now skips (0 ms) and the full suite completes
with no hang; and natively on arm64 musl the full suite is green
(0 failed, 5678 passed, 225 skipped).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 611d76bf-f08f-44ab-85de-681c29a3bda9
@mattleibow
mattleibow force-pushed the mattleibow-nano-server-ci-tests branch from 0b09270 to 04c143c Compare July 23, 2026 20:54
@mattleibow
mattleibow merged commit a90f1e1 into main Jul 23, 2026
86 of 91 checks passed
@mattleibow
mattleibow deleted the mattleibow-nano-server-ci-tests branch July 23, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants