diff --git a/binding/IncludeNativeAssets.HarfBuzzSharp.targets b/binding/IncludeNativeAssets.HarfBuzzSharp.targets
index 754e70a9559..7b3eb95157a 100644
--- a/binding/IncludeNativeAssets.HarfBuzzSharp.targets
+++ b/binding/IncludeNativeAssets.HarfBuzzSharp.targets
@@ -6,7 +6,18 @@
$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant)
-
+
+
+
+
+
+
+
diff --git a/binding/IncludeNativeAssets.SkiaSharp.targets b/binding/IncludeNativeAssets.SkiaSharp.targets
index a4f98db59c0..7f60ed4fce3 100644
--- a/binding/IncludeNativeAssets.SkiaSharp.targets
+++ b/binding/IncludeNativeAssets.SkiaSharp.targets
@@ -6,7 +6,18 @@
$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant)
-
+
+
+
+
+
+
+
diff --git a/build.cake b/build.cake
index a62d3f2d369..483dd572449 100644
--- a/build.cake
+++ b/build.cake
@@ -103,6 +103,10 @@ Task ("tests-netcore")
.IsDependentOn ("externals")
.Does (() => RunCake ("./scripts/infra/tests/tests-netcore.cake", "Default"));
+Task ("tests-container")
+ .Description ("Run the console test suite against prebuilt natives (used by the containerized test legs).")
+ .Does (() => RunCake ("./scripts/infra/tests/tests-container.cake", "Default"));
+
Task ("tests-android")
.Description ("Run all Android tests.")
.IsDependentOn ("externals")
diff --git a/documentation/dev/README.md b/documentation/dev/README.md
index 501f747e61a..46cbbca9234 100644
--- a/documentation/dev/README.md
+++ b/documentation/dev/README.md
@@ -54,6 +54,11 @@ C# Wrapper (binding/SkiaSharp/) → P/Invoke → C API (externals/skia/src/c
| [building.md](building.md) | Build on Windows & macOS |
| [building-linux.md](building-linux.md) | Build native libraries for Linux |
+### Testing
+| Document | Description |
+|----------|-------------|
+| [containerized-testing.md](containerized-testing.md) | Run the console test suite inside a Docker container (Linux glibc/Alpine + Nano Server) via the bootstrapper `docker:` feature + the `tests-container` cake target |
+
### Releasing
| Document | Description |
|----------|-------------|
diff --git a/documentation/dev/containerized-testing.md b/documentation/dev/containerized-testing.md
new file mode 100644
index 00000000000..36c86ad80fd
--- /dev/null
+++ b/documentation/dev/containerized-testing.md
@@ -0,0 +1,140 @@
+# Containerized testing (Docker)
+
+The console test suite (`SkiaSharp.Tests.Console`) can be run **inside a Docker container** against a
+chosen native build. This covers container runtimes that the agent-based test legs don't — **Linux
+(Azure Linux, glibc)**, **Linux (Alpine, musl)**, their **No Dependencies** variants, and **Windows
+Nano Server**.
+
+Pieces:
+
+- **Cake target** [`tests-container`](../../scripts/infra/tests/tests-container.cake) — runs the
+ console suite against a prebuilt native library. Builds no externals.
+- **Env images** [`scripts/infra/tests/docker/`](../../scripts/infra/tests/docker) —
+ `azurelinux/`, `azurelinux-nodeps/`, `alpine/`, `alpine-nodeps/`, `nanoserver/`.
+- **CI legs** `tests_container_linux`, `tests_container_linux_nodeps`, `tests_container_alpine_linux`,
+ `tests_container_alpine_nodeps`, `tests_container_nanoserver_windows` in
+ [`scripts/azure-templates-stages-test.yml`](../../scripts/azure-templates-stages-test.yml).
+
+The desktop `Linux` leg already runs on an Ubuntu agent, so the glibc container leg uses **Azure
+Linux** instead — a different, minimal glibc distro rather than a second Ubuntu run.
+
+## How a leg runs
+
+Each leg uses the bootstrapper `docker:` feature (`azure-templates-jobs-bootstrapper.yml`), which:
+
+1. builds the **env image** from `/Dockerfile` with the `Docker@2` task, then
+2. runs `dotnet cake --target=tests-container` **inside** that image against the mounted repo:
+ `docker run --volume : skiasharp … dotnet cake --target=tests-container …`.
+
+The feature runs on both Linux and Windows agents: Linux mounts `/work` and runs via `/bin/sh` (POSIX,
+so the bare No Dependencies images need no shell package); Windows mounts `C:\work` and runs via `cmd`.
+
+The **env images** provide only what each scenario needs. The two fontconfig images add `fontconfig`
++ fonts (see [Fonts](#fonts)); the **No Dependencies** and **Nano Server** images install **nothing**
+beyond the base .NET SDK — that is the point of those legs. There is no `COPY` in these Dockerfiles;
+the repo is mounted at run time.
+
+## What `tests-container` does
+
+1. builds `tests/SkiaSharp.Tests.Console` with `-p:TargetFrameworks=net10.0`, collapsing the
+ multi-targeted binding projects to a single TFM so no Android/iOS workloads are needed;
+2. runs the suite via `dotnet test`, writing `output/logs/testlogs/**/TestResults.trx`.
+
+It builds no externals — the prebuilt `libSkiaSharp` comes from `output/native///`
+in the mounted repo (in CI, the merged `native` artifact; locally, `externals-download`).
+
+## Selecting the native build
+
+`--nativePlatform=` sets the `SkiaSharpNativePlatform` / `HarfBuzzSharpNativePlatform`
+MSBuild properties. In `binding/IncludeNativeAssets.SkiaSharp.targets` (and the HarfBuzz twin), those
+properties make the desktop `Content` include copy the native library from
+`output/native///` only. The `` is derived from the host (`OSArchitecture`).
+
+This is how a container runs against a platform-specific build the OS-derived defaults don't map —
+`nanoserver` (`output/native/nanoserver/x64/libSkiaSharp.dll`) on Windows, and `alpine`
+(`output/native/alpine/x64/libSkiaSharp.so`) on musl.
+
+## Running it locally
+
+Build the env image, then run `tests-container` inside it against the mounted repo:
+
+```bash
+# Linux Azure Linux (glibc). --nativePlatform picks the build; is auto from the host.
+dotnet cake --target=externals-download
+docker build -t skiasharp-tests-env scripts/infra/tests/docker/azurelinux
+docker run --rm --volume "$(pwd):/work" -w /work skiasharp-tests-env \
+ /bin/sh -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=linux"
+```
+
+```bash
+# Linux Alpine (musl).
+docker build -t skiasharp-tests-env-alpine scripts/infra/tests/docker/alpine
+docker run --rm --volume "$(pwd):/work" -w /work skiasharp-tests-env-alpine \
+ /bin/sh -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=alpine"
+```
+
+```bash
+# No Dependencies (bare image, no fontconfig). Swap in alpine-nodeps / --nativePlatform=alpinenodeps
+# for the musl variant.
+docker build -t skiasharp-tests-env-nodeps scripts/infra/tests/docker/azurelinux-nodeps
+docker run --rm --volume "$(pwd):/work" -w /work skiasharp-tests-env-nodeps \
+ /bin/sh -c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=linuxnodeps"
+```
+
+```powershell
+# Nano Server (Windows container host only).
+dotnet cake --target=externals-nanoserver
+docker build -t skiasharp-tests-env scripts/infra/tests/docker/nanoserver
+docker run --rm --volume "${pwd}:C:\work" -w C:\work skiasharp-tests-env `
+ cmd /c "dotnet tool restore && dotnet cake --target=tests-container --nativePlatform=nanoserver"
+```
+
+## CI wiring
+
+Each leg is a bootstrapper job in the `tests` stage that:
+
+- declares `requiredArtifacts: - name: native`, so `output/native/…` is the merged native artifact;
+- sets `docker:` to the env image and `target: tests-container` with
+ `additionalArgs: --nativePlatform=<…>`;
+- publishes the TRX via `PublishTestResults@2` (Azure DevOps Tests tab) and the
+ `testlogs_container_` artifact.
+
+The legs are gating: a genuine test failure fails the build. Environment differences that are not
+SkiaSharp regressions — most notably the fontless `nodeps` and Nano Server builds — are handled by
+the runtime self-skip helpers (see [Fonts](#fonts) below), so those tests skip rather than fail.
+
+The Nano Server leg requires a Windows agent with container support (matching the `ltsc2022` image
+base) and pulls the Nano Server .NET SDK image.
+
+## Fonts
+
+Whether the suite can resolve **system fonts** depends on how the native library was compiled, not
+just on the image:
+
+- **fontconfig builds** (`linux`, `alpine`) enumerate whatever fonts are installed in the image.
+ Base .NET SDK images ship no fonts, so the two fontconfig env images install them: `fontconfig` +
+ DejaVu on both, plus an emoji font for the Unicode tests. Alpine installs `font-noto-emoji`
+ (packaged); Azure Linux has no emoji font in its repos, so its Dockerfile downloads **Noto Emoji**
+ (SIL OFL) pinned by URL commit and verified by SHA-256. These provide the families the test config
+ expects (`DefaultFontFamily`, `UnicodeFontFamilies`).
+- **non-fontconfig / bare** — the **No Dependencies** variants (`linuxnodeps`, `alpinenodeps`, built
+ with `skia_use_fontconfig=false`) run in bare images with nothing installed, and **Nano Server** —
+ all enumerate **no** system fonts regardless of what the image contains. Their font manager
+ (`SkFontMgr_New_Custom_Empty`, a FreeType scanner) can only use fonts loaded explicitly
+ (`SKTypeface.FromFile` / `FromStream` / `FromData`). APIs that resolve a system family or the
+ default typeface (`SKTypeface.FromFamilyName`, `SKFontManager.Default`, a default `SKFont`) have
+ nothing to bind to.
+
+The Linux test config chooses `UnicodeFontFamilies` per libc — `Symbola` (Ubuntu desktop) or
+`Noto Emoji` (Azure Linux container) on glibc, and `Noto Color Emoji` on musl — keyed off
+`PlatformConfiguration.IsGlibc`.
+
+Tests that need system fonts detect the environment at runtime and self-skip, so the same suite
+runs unmodified everywhere:
+
+- `SkipWhenNoSystemFontManager()` — for tests that enumerate or match families
+ (`SKFontManager` / `SKFontStyleSet`, or `MatchCharacter`). Skips where there is no usable font
+ manager: WASM, the NoDependencies builds, and Nano Server.
+- `SkipWhenNoDefaultFont()` — for tests that measure or draw with the **default** typeface. Skips
+ where the default typeface is empty: the NoDependencies builds and Nano Server. WASM keeps a
+ single embedded default font, so these still run there.
diff --git a/scripts/azure-templates-jobs-bootstrapper.yml b/scripts/azure-templates-jobs-bootstrapper.yml
index de022a2ac26..49e3c2d05eb 100644
--- a/scripts/azure-templates-jobs-bootstrapper.yml
+++ b/scripts/azure-templates-jobs-bootstrapper.yml
@@ -377,7 +377,7 @@ jobs:
dockerfile: ${{ parameters.docker }}/Dockerfile
context: ${{ parameters.docker }}
image: skiasharp:skiasharp
- buildArguments: --platform linux/amd64 --tag skiasharp ${{ parameters.dockerArgs }}
+ buildArguments: --tag skiasharp ${{ parameters.dockerArgs }}
enableNetwork: true
- ${{ if ne(parameters.use1ESPipelineTemplates, 'true') }}:
- task: Docker@2
@@ -387,19 +387,34 @@ jobs:
command: build
buildContext: ${{ parameters.docker }}
dockerfile: ${{ parameters.docker }}/Dockerfile
- arguments: --platform linux/amd64 --tag skiasharp ${{ parameters.dockerArgs }}
- - bash: |
- echo dotnet tool restore > cmd.sh
- echo dotnet cake --target=${{ parameters.target }} --verbosity=${{ parameters.verbosity }} --configuration=${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} >> cmd.sh
- sed -i 's/--gnArgs=\" \"//' cmd.sh
- cat cmd.sh
- displayName: Generate the script for the Docker image
- condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
- - bash: |
- docker run --rm --name skiasharp --volume $(pwd):/work skiasharp /bin/bash /work/cmd.sh
- displayName: Run the bootstrapper for ${{ parameters.target }} using the Docker image
- retryCountOnTaskFailure: ${{ parameters.retryCount }}
- condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
+ arguments: --tag skiasharp ${{ parameters.dockerArgs }}
+ # Run the target inside the image against the mounted repo. Linux and Windows containers
+ # differ in shell and mount path, so the run step is branched by agent OS.
+ - ${{ if ne(parameters.buildAgent.pool.os, 'windows') }}:
+ - bash: |
+ echo dotnet tool restore > cmd.sh
+ echo dotnet cake --target=${{ parameters.target }} --verbosity=${{ parameters.verbosity }} --configuration=${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }} >> cmd.sh
+ sed -i 's/--gnArgs=\" \"//' cmd.sh
+ cat cmd.sh
+ displayName: Generate the script for the Docker image
+ condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
+ - bash: |
+ docker run --rm --name skiasharp --volume $(pwd):/work skiasharp /bin/sh /work/cmd.sh
+ displayName: Run the bootstrapper for ${{ parameters.target }} using the Docker image
+ retryCountOnTaskFailure: ${{ parameters.retryCount }}
+ condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
+ - ${{ if eq(parameters.buildAgent.pool.os, 'windows') }}:
+ - pwsh: |
+ 'dotnet tool restore' | Set-Content cmd.cmd -Encoding ascii
+ 'dotnet cake --target=${{ parameters.target }} --verbosity=${{ parameters.verbosity }} --configuration=${{ coalesce(parameters.configuration, 'Release') }} ${{ parameters.additionalArgs }}' | Add-Content cmd.cmd -Encoding ascii
+ Get-Content cmd.cmd
+ displayName: Generate the script for the Docker image
+ condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
+ - pwsh: |
+ docker run --rm --name skiasharp --volume "$(Build.SourcesDirectory):C:\work" -w C:\work skiasharp cmd /c C:\work\cmd.cmd
+ displayName: Run the bootstrapper for ${{ parameters.target }} using the Docker image
+ retryCountOnTaskFailure: ${{ parameters.retryCount }}
+ condition: and(succeeded(), ne(variables['CACHE_SKIP'], 'true'))
- pwsh: .\scripts\infra\native\shared\get-free-space.ps1
displayName: Get the volume information after the build
diff --git a/scripts/azure-templates-stages-test.yml b/scripts/azure-templates-stages-test.yml
index c47897e0547..4e83db88d97 100644
--- a/scripts/azure-templates-stages-test.yml
+++ b/scripts/azure-templates-stages-test.yml
@@ -84,7 +84,7 @@ stages:
${{ else }}:
dependsOn: native
jobs:
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|netfx (Windows)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Windows (.NET Framework)
parameters:
name: tests_netfx_windows
displayName: Windows (.NET Framework)
@@ -110,7 +110,7 @@ stages:
- name: testlogs_netfx_windows
always: true
path: 'output/logs/testlogs'
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|netcore (Windows)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Windows (.NET Core)
parameters:
name: tests_netcore_windows
displayName: Windows (.NET Core)
@@ -138,10 +138,10 @@ stages:
path: 'output/logs/testlogs'
- name: coverage_netcore_windows
path: 'output/coverage'
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|netcore (macOS)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|macOS
parameters:
name: tests_netcore_macos
- displayName: macOS (.NET Core)
+ displayName: macOS
buildAgent: ${{ parameters.buildAgentMac }}
target: tests-netcore
additionalArgs: --skipExternals="all" --coverage=$(ENABLE_CODE_COVERAGE)
@@ -166,10 +166,10 @@ stages:
path: 'output/logs/testlogs'
- name: coverage_netcore_macos
path: 'output/coverage'
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|netcore (Linux)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Linux
parameters:
name: tests_netcore_linux
- displayName: Linux (.NET Core)
+ displayName: Linux
buildAgent: ${{ parameters.buildAgentLinux }}
# Software GL (Mesa llvmpipe) + software Vulkan (Mesa lavapipe) + a
# virtual X server give the headless agent deterministic GPU output so
@@ -217,10 +217,10 @@ stages:
path: 'output/logs/testlogs'
- name: coverage_netcore_linux
path: 'output/coverage'
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|android (Linux)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Android
parameters:
name: tests_android_linux
- displayName: Android (Linux)
+ displayName: Android
buildAgent: ${{ parameters.buildAgentAndroidTests }}
target: tests-android
additionalArgs: --device=android-emulator-64 --deviceVersion=$(ANDROID_TEST_DEVICE_VERSION) --skipExternals="all" --coverage=$(ENABLE_CODE_COVERAGE)
@@ -259,10 +259,10 @@ stages:
- name: testlogs_android_$(ANDROID_TEST_DEVICE_VERSION)
always: true
path: 'output/logs/testlogs'
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|ios (macOS)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|iOS
parameters:
name: tests_ios_macos
- displayName: iOS (macOS)
+ displayName: iOS
buildAgent: ${{ parameters.buildAgentMac }}
target: tests-ios
additionalArgs: --device=ios-simulator-64 --deviceVersion=$(IOS_TEST_DEVICE_VERSION) --skipExternals="all" --coverage=$(ENABLE_CODE_COVERAGE)
@@ -284,10 +284,10 @@ stages:
- name: testlogs_ios_$(IOS_TEST_DEVICE_VERSION)
always: true
path: 'output/logs/testlogs'
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|maccatalyst (macOS)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Mac Catalyst
parameters:
name: tests_maccatalyst_macos
- displayName: Mac Catalyst (macOS)
+ displayName: Mac Catalyst
continueOnError: true
buildAgent: ${{ parameters.buildAgentMac }}
target: tests-maccatalyst
@@ -311,10 +311,10 @@ stages:
- name: testlogs_maccatalyst
always: true
path: 'output/logs/testlogs'
- - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests [WASM] (Linux)
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|WASM
parameters:
name: tests_wasm_linux
- displayName: WASM (Linux)
+ displayName: WASM
buildAgent: ${{ parameters.buildAgentLinux }}
packages: $(MANAGED_LINUX_PACKAGES) ninja-build
target: tests-wasm
@@ -347,7 +347,7 @@ stages:
# mismatch in the native lib, fail CI. installPreviewSdk provisions the preview SDK +
# wasm-tools workload (which bundles the matching emscripten) and pins global.json to it.
name: tests_wasm_preview_linux
- displayName: WASM Preview (Linux)
+ displayName: WASM (.NET Preview)
buildAgent: ${{ parameters.buildAgentLinux }}
packages: $(MANAGED_LINUX_PACKAGES) ninja-build
target: tests-wasm
@@ -370,9 +370,134 @@ stages:
- name: testlogs_wasm_preview
always: true
path: 'output/logs/testlogs'
- # TODO: add tests for linux alpine
- # TODO: add tests for linux no dependencies
- # TODO: add tests for windows nano server
+
+ # Containerized test legs — see documentation/dev/containerized-testing.md
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Linux (Azure Linux)
+ parameters:
+ name: tests_container_linux
+ displayName: Linux (Azure Linux)
+ buildAgent: ${{ parameters.buildAgentLinux }}
+ docker: scripts/infra/tests/docker/azurelinux
+ target: tests-container
+ additionalArgs: --nativePlatform=linux
+ installAndroidSdk: false
+ installXcode: false
+ shouldPublish: false
+ requiredArtifacts:
+ - name: native
+ postBuildSteps:
+ - task: PublishTestResults@2
+ displayName: Publish the Azure Linux container test results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: 'output/logs/testlogs/**/*.trx'
+ testRunTitle: 'Linux (Azure Linux) Tests'
+ publishArtifacts:
+ - name: testlogs_container_linux
+ always: true
+ path: 'output/logs/testlogs'
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Linux (Azure Linux, No Dependencies)
+ parameters:
+ name: tests_container_linux_nodeps
+ displayName: Linux (Azure Linux, No Dependencies)
+ buildAgent: ${{ parameters.buildAgentLinux }}
+ docker: scripts/infra/tests/docker/azurelinux-nodeps
+ target: tests-container
+ additionalArgs: --nativePlatform=linuxnodeps
+ installAndroidSdk: false
+ installXcode: false
+ shouldPublish: false
+ requiredArtifacts:
+ - name: native
+ postBuildSteps:
+ - task: PublishTestResults@2
+ displayName: Publish the Azure Linux NoDeps container test results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: 'output/logs/testlogs/**/*.trx'
+ testRunTitle: 'Linux (Azure Linux, No Dependencies) Tests'
+ publishArtifacts:
+ - name: testlogs_container_linux_nodeps
+ always: true
+ path: 'output/logs/testlogs'
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Linux (Alpine)
+ parameters:
+ name: tests_container_alpine_linux
+ displayName: Linux (Alpine)
+ buildAgent: ${{ parameters.buildAgentLinux }}
+ docker: scripts/infra/tests/docker/alpine
+ target: tests-container
+ additionalArgs: --nativePlatform=alpine
+ installAndroidSdk: false
+ installXcode: false
+ shouldPublish: false
+ requiredArtifacts:
+ - name: native
+ postBuildSteps:
+ - task: PublishTestResults@2
+ displayName: Publish the Alpine container test results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: 'output/logs/testlogs/**/*.trx'
+ testRunTitle: 'Linux (Alpine) Tests'
+ publishArtifacts:
+ - name: testlogs_container_alpine
+ always: true
+ path: 'output/logs/testlogs'
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Linux (Alpine, No Dependencies)
+ parameters:
+ name: tests_container_alpine_nodeps
+ displayName: Linux (Alpine, No Dependencies)
+ buildAgent: ${{ parameters.buildAgentLinux }}
+ docker: scripts/infra/tests/docker/alpine-nodeps
+ target: tests-container
+ additionalArgs: --nativePlatform=alpinenodeps
+ installAndroidSdk: false
+ installXcode: false
+ shouldPublish: false
+ requiredArtifacts:
+ - name: native
+ postBuildSteps:
+ - task: PublishTestResults@2
+ displayName: Publish the Alpine NoDeps container test results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: 'output/logs/testlogs/**/*.trx'
+ testRunTitle: 'Linux (Alpine, No Dependencies) Tests'
+ publishArtifacts:
+ - name: testlogs_container_alpine_nodeps
+ always: true
+ path: 'output/logs/testlogs'
+ - template: /scripts/azure-templates-jobs-bootstrapper.yml@self # Tests|Windows (Nano Server)
+ parameters:
+ name: tests_container_nanoserver_windows
+ displayName: Windows (Nano Server)
+ buildAgent: ${{ parameters.buildAgentWindows }}
+ docker: scripts/infra/tests/docker/nanoserver
+ target: tests-container
+ additionalArgs: --nativePlatform=nanoserver
+ installAndroidSdk: false
+ installXcode: false
+ shouldPublish: false
+ requiredArtifacts:
+ - name: native
+ postBuildSteps:
+ - task: PublishTestResults@2
+ displayName: Publish the Nano Server container test results
+ condition: always()
+ inputs:
+ testResultsFormat: VSTest
+ testResultsFiles: 'output/logs/testlogs/**/*.trx'
+ testRunTitle: 'Nano Server Container Tests'
+ publishArtifacts:
+ - name: testlogs_container_nanoserver
+ always: true
+ path: 'output/logs/testlogs'
+
- job: coverage_reports # Coverage Reports
displayName: Coverage Reports
pool: ${{ parameters.buildAgentHost.pool }}
diff --git a/scripts/infra/tests/docker/alpine-nodeps/Dockerfile b/scripts/infra/tests/docker/alpine-nodeps/Dockerfile
new file mode 100644
index 00000000000..2a55b098a77
--- /dev/null
+++ b/scripts/infra/tests/docker/alpine-nodeps/Dockerfile
@@ -0,0 +1,20 @@
+# Containerized test environment — Linux Alpine, No Dependencies (musl).
+#
+# This image proves the NoDependencies musl native build (skia_use_fontconfig=false) runs on a bare
+# Alpine image with NOTHING installed beyond the base .NET SDK — no fontconfig, no fonts, no extra
+# runtime libraries. That is the whole point of the leg: it fails if libSkiaSharp ever grows an
+# undeclared dependency on an optional system package. The font-dependent tests self-skip at
+# runtime (there is no system font manager), so the suite still passes.
+#
+# Consumed by the bootstrapper's `docker:` feature via:
+#
+# docker run --volume :/work skiasharp /bin/sh /work/cmd.sh
+#
+# The repository (including output/native/alpinenodeps//libSkiaSharp.so from the `native`
+# artifact) is mounted at /work at run time; tests-container selects it via --nativePlatform=alpinenodeps.
+# The base image already provides everything the native library links against (libstdc++, libgcc,
+# musl libc, ICU), so there is deliberately nothing to install here.
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine
+
+WORKDIR /work
diff --git a/scripts/infra/tests/docker/alpine/Dockerfile b/scripts/infra/tests/docker/alpine/Dockerfile
new file mode 100644
index 00000000000..0059db36142
--- /dev/null
+++ b/scripts/infra/tests/docker/alpine/Dockerfile
@@ -0,0 +1,28 @@
+# Containerized test environment — Linux Alpine (musl).
+#
+# This image is the ENVIRONMENT the SkiaSharp console test suite runs inside. It is consumed by the
+# bootstrapper's `docker:` feature (scripts/azure-templates-jobs-bootstrapper.yml), which builds it
+# with the approved container-image task and then runs the tests inside it via:
+#
+# docker run --volume :/work skiasharp /bin/sh /work/cmd.sh
+# # where cmd.sh = "dotnet tool restore" + "dotnet cake --target=tests-container ..."
+#
+# So this Dockerfile only provisions fonts; the repository (including output/native/alpine//
+# libSkiaSharp.so — the musl build — from the `native` artifact) is mounted at /work at run time.
+# There is intentionally no COPY here. The build context is this directory, not the repo. The
+# tests-container cake target selects the musl build via --nativePlatform=alpine.
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine
+
+# Fonts for Skia's fontconfig-backed default font manager (the base .NET SDK image already provides
+# the C++/ICU runtime the native library links against, so only fonts are added):
+# - fontconfig: font discovery used by Skia at runtime
+# - font-dejavu: "DejaVu Sans" (DefaultFontFamily on Linux tests)
+# - font-noto-emoji: "Noto Color Emoji" (UnicodeFontFamilies on musl — covers emoji like U+1F680)
+RUN apk add --no-cache \
+ fontconfig \
+ font-dejavu \
+ font-noto-emoji \
+ && fc-cache -f
+
+WORKDIR /work
diff --git a/scripts/infra/tests/docker/azurelinux-nodeps/Dockerfile b/scripts/infra/tests/docker/azurelinux-nodeps/Dockerfile
new file mode 100644
index 00000000000..40434209d43
--- /dev/null
+++ b/scripts/infra/tests/docker/azurelinux-nodeps/Dockerfile
@@ -0,0 +1,20 @@
+# Containerized test environment — Linux Azure Linux, No Dependencies (glibc).
+#
+# This image proves the NoDependencies native build (skia_use_fontconfig=false) runs on a bare
+# glibc image with NOTHING installed beyond the base .NET SDK — no fontconfig, no fonts, no extra
+# runtime libraries. That is the whole point of the leg: it fails if libSkiaSharp ever grows an
+# undeclared dependency on an optional system package. The font-dependent tests self-skip at
+# runtime (there is no system font manager), so the suite still passes.
+#
+# Consumed by the bootstrapper's `docker:` feature via:
+#
+# docker run --volume :/work skiasharp /bin/sh /work/cmd.sh
+#
+# The repository (including output/native/linuxnodeps//libSkiaSharp.so from the `native`
+# artifact) is mounted at /work at run time; tests-container selects it via --nativePlatform=linuxnodeps.
+# The base image already provides everything the native library links against (libstdc++, libgcc,
+# libc, ICU), so there is deliberately nothing to install here.
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0-azurelinux3.0
+
+WORKDIR /work
diff --git a/scripts/infra/tests/docker/azurelinux/Dockerfile b/scripts/infra/tests/docker/azurelinux/Dockerfile
new file mode 100644
index 00000000000..74ce9269371
--- /dev/null
+++ b/scripts/infra/tests/docker/azurelinux/Dockerfile
@@ -0,0 +1,36 @@
+# Containerized test environment — Linux Azure Linux (glibc).
+#
+# This image is the ENVIRONMENT the SkiaSharp console test suite runs inside. It is consumed by the
+# bootstrapper's `docker:` feature (scripts/azure-templates-jobs-bootstrapper.yml), which builds it
+# with the approved container-image task and then runs the tests inside it via:
+#
+# docker run --volume :/work skiasharp /bin/sh /work/cmd.sh
+# # where cmd.sh = "dotnet tool restore" + "dotnet cake --target=tests-container ..."
+#
+# So this Dockerfile only provisions fonts; the repository (including output/native/linux//
+# libSkiaSharp.so from the `native` artifact) is mounted at /work at run time. There is intentionally
+# no COPY here. The build context is this directory, not the repo. The tests-container cake target
+# selects the glibc build via --nativePlatform=linux.
+#
+# Azure Linux is glibc, so it shares the desktop Linux leg's fontconfig behavior and font config
+# (DefaultFontFamily "DejaVu Sans"). It exercises a non-Ubuntu glibc distro (the desktop leg already
+# covers Ubuntu).
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0-azurelinux3.0
+
+# Fonts for Skia's fontconfig-backed default font manager:
+# - fontconfig: font discovery used by Skia at runtime
+# - dejavu-sans-fonts: "DejaVu Sans" (DefaultFontFamily on Linux tests)
+# Azure Linux has no emoji/symbol font in any repo, so download Noto Emoji (SIL OFL, monochrome)
+# for the unicode tests (covers e.g. U+1F680) and verify it by SHA-256. Pinned to a fixed
+# googlefonts/noto-emoji commit so the file is reproducible.
+ARG NOTO_EMOJI_URL=https://raw.githubusercontent.com/googlefonts/noto-emoji/9a5261d871451f9b5183c93483cbd68ed916b1e9/fonts/NotoEmoji-Regular.ttf
+ARG NOTO_EMOJI_SHA256=415dc6290378574135b64c808dc640c1df7531973290c4970c51fdeb849cb0c5
+RUN tdnf install -y fontconfig dejavu-sans-fonts \
+ && tdnf clean all \
+ && mkdir -p /usr/share/fonts/noto \
+ && curl -fSL "$NOTO_EMOJI_URL" -o /usr/share/fonts/noto/NotoEmoji-Regular.ttf \
+ && echo "$NOTO_EMOJI_SHA256 /usr/share/fonts/noto/NotoEmoji-Regular.ttf" | sha256sum -c - \
+ && fc-cache -f
+
+WORKDIR /work
diff --git a/scripts/infra/tests/docker/nanoserver/Dockerfile b/scripts/infra/tests/docker/nanoserver/Dockerfile
new file mode 100644
index 00000000000..f419bae53ee
--- /dev/null
+++ b/scripts/infra/tests/docker/nanoserver/Dockerfile
@@ -0,0 +1,26 @@
+# escape=`
+# Containerized test environment — Windows Nano Server.
+#
+# This image is the ENVIRONMENT the SkiaSharp console test suite runs inside. It is consumed by the
+# bootstrapper's `docker:` feature (scripts/azure-templates-jobs-bootstrapper.yml), which builds it
+# with the approved container-image task and then runs the tests inside it via:
+#
+# docker run --volume :C:\work -w C:\work skiasharp cmd /c C:\work\cmd.cmd
+# # where cmd.cmd = "dotnet tool restore" + "dotnet cake --target=tests-container ..."
+#
+# So this Dockerfile only provisions the toolchain; the repository (including
+# output/native/nanoserver/x64/libSkiaSharp.dll from the `native` artifact) is mounted at C:\work at
+# run time — there is intentionally no COPY here. The build context is this directory, not the repo.
+#
+# It uses the Nano Server .NET *SDK* image so the suite can be built AND run in one place. Because
+# it is Nano Server, it has ZERO system fonts — which is exactly the environment under test. The
+# tests-container cake target overlays output/native/nanoserver/x64/libSkiaSharp.dll (via
+# --nativePlatform=nanoserver), so the suite runs against the nanoserver native build, not the
+# regular windows one.
+#
+# REQUIRES a Windows container host (Windows agent with container support). It cannot be built or
+# run on Linux/macOS Docker.
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0-nanoserver-ltsc2022
+
+WORKDIR C:\work
diff --git a/scripts/infra/tests/tests-container.cake b/scripts/infra/tests/tests-container.cake
new file mode 100644
index 00000000000..d9010d0e16c
--- /dev/null
+++ b/scripts/infra/tests/tests-container.cake
@@ -0,0 +1,71 @@
+DirectoryPath ROOT_PATH = MakeAbsolute(Directory("../../.."));
+
+#load "../shared/shared.cake"
+#load "../shared/msbuild.cake"
+#load "test-shared.cake"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+// CONTAINER TESTS — run ONLY the console test suite (SkiaSharp.Tests.Console)
+//
+// This target is invoked INSIDE a container by the bootstrapper's `docker:` feature
+// (docker run ... dotnet cake --target=tests-container). It does NOT build any native
+// externals — the prebuilt libSkiaSharp is provided in the mounted repo's output/native (populated
+// in CI from the `native` artifact; locally by externals-download).
+//
+// It runs a single assembly (the cross-platform console suite) rather than the full tests-netcore
+// set, so the container image stays minimal (no GTK4 / Vulkan / Direct3D dependencies).
+//
+// --nativePlatform selects which output/native// build to run against. It is passed
+// through to the native-asset targets as the SkiaSharp/HarfBuzzSharp NativePlatform MSBuild property,
+// so the build copies that platform's library. This is how the Windows container runs against the
+// nanoserver build (output/native/nanoserver/x64/libSkiaSharp.dll) and Alpine against the musl build,
+// which the OS-derived defaults don't map.
+//
+// dotnet cake --target=tests-container --nativePlatform=linux
+// dotnet cake --target=tests-container --nativePlatform=alpine
+// dotnet cake --target=tests-container --nativePlatform=nanoserver
+////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Which output/native// build to run against. The is derived from the host
+// (OSArchitecture) by the native-asset targets, so only the platform is specified here.
+var NATIVE_PLATFORM = Argument("nativePlatform", "");
+
+Task ("Default")
+ .Description ("Run the .NET console test suite (for containerized runs).")
+ .Does (() =>
+{
+ CleanDirectories ($"{PACKAGE_CACHE_PATH}/skiasharp*");
+ CleanDirectories ($"{PACKAGE_CACHE_PATH}/harfbuzzsharp*");
+
+ var tfm = "net10.0";
+ var testAssembly = "SkiaSharp.Tests.Console";
+ var csproj = $"{ROOT_PATH}/tests/{testAssembly}/{testAssembly}.csproj";
+
+ var props = new Dictionary {
+ // TargetFrameworks (plural) collapses the multi-targeted binding projects to net10.0 only,
+ // so the Android/iOS/etc. workloads are never restored/built — keeping the image lean.
+ { "TargetFramework", tfm },
+ { "TargetFrameworks", tfm },
+ // The Windows-only sn.exe re-sign step isn't present in the Nano Server SDK image (and isn't
+ // needed to run tests — PublicSign already gives the correct identity for InternalsVisibleTo).
+ { "DisableStrongNameSigning", "true" },
+ // mdoc.exe doesn't run on Nano Server (missing Windows DLL deps) and doc generation is
+ // irrelevant to running the test suite, so skip it in every container build.
+ { "SkipMDocGenerateDocs", "true" },
+ };
+
+ // Select the native build: the native-asset targets copy this platform's library.
+ if (!string.IsNullOrEmpty(NATIVE_PLATFORM)) {
+ props["SkiaSharpNativePlatform"] = NATIVE_PLATFORM;
+ props["HarfBuzzSharpNativePlatform"] = NATIVE_PLATFORM;
+ }
+
+ if (!SKIP_BUILD) {
+ RunDotNetBuild (csproj, properties: props);
+ }
+
+ var results = $"{ROOT_PATH}/output/logs/testlogs/{testAssembly}/{DATE_TIME_STR}/{tfm}";
+ RunDotNetTest (csproj, results, properties: props);
+});
+
+RunTarget(TARGET);
diff --git a/source/SkiaSharp.Build.targets b/source/SkiaSharp.Build.targets
index 4bd5357a81f..4c890d4906d 100644
--- a/source/SkiaSharp.Build.targets
+++ b/source/SkiaSharp.Build.targets
@@ -151,6 +151,10 @@ internal partial class VersionConstants {
_SignAssembly
Sign the assembly using sn.
+
+ Set DisableStrongNameSigning=true to skip this sn.exe re-sign (and its verify) where sn.exe is
+ unavailable, e.g. the Nano Server SDK container. PublicSign still applies, so the assembly keeps
+ its public-key identity for InternalsVisibleTo.
===================================================================================================================
-->
@@ -163,7 +167,7 @@ internal partial class VersionConstants {
+ Condition=" $(IsWindows) and '$(SignAssembly)' == 'true' and '$(TargetPath)' != '' and '$(BuildingInsideVisualStudio)' != 'true' and '$(DisableStrongNameSigning)' != 'true' ">
@@ -182,7 +186,7 @@ internal partial class VersionConstants {
+ Condition=" $(IsWindows) and '$(SignAssembly)' == 'true' and '$(Configuration)' == 'Release' and '$(TargetPath)' != '' and '$(BuildingInsideVisualStudio)' != 'true' and '$(DisableStrongNameSigning)' != 'true' ">
diff --git a/tests/Content/Goldens/raster.nanoserver/Text.png b/tests/Content/Goldens/raster.nanoserver/Text.png
new file mode 100644
index 00000000000..f5e53aaad35
Binary files /dev/null and b/tests/Content/Goldens/raster.nanoserver/Text.png differ
diff --git a/tests/SkiaSharp.Tests.Console/DefaultTestConfig.Console.cs b/tests/SkiaSharp.Tests.Console/DefaultTestConfig.Console.cs
index 1734e3ceeac..a0fa62d85eb 100644
--- a/tests/SkiaSharp.Tests.Console/DefaultTestConfig.Console.cs
+++ b/tests/SkiaSharp.Tests.Console/DefaultTestConfig.Console.cs
@@ -33,7 +33,12 @@ public DefaultTestConfig()
// set the test fields
DefaultFontFamily = IsLinux ? "DejaVu Sans" : "Arial";
UnicodeFontFamilies =
- IsLinux ? new[] { "Symbola" } :
+ IsLinux ? (SkiaSharp.Internals.PlatformConfiguration.IsGlibc
+ // glibc images resolve emoji via "Symbola" (ttf-ancient-fonts, on the desktop
+ // Ubuntu agent) or "Noto Emoji" (downloaded into the Azure Linux container image);
+ // Alpine/musl images install font-noto-emoji, matching "Noto Color Emoji".
+ ? new[] { "Symbola", "Noto Emoji" }
+ : new[] { "Noto Color Emoji" }) :
IsMac ? new[] { "Apple Color Emoji" } :
new[] { "Segoe UI Emoji", "Segoe UI Symbol" };
}
diff --git a/tests/SkiaSharp.Tests.Console/SkiaSharp/SKBitmapThreadingTest.cs b/tests/SkiaSharp.Tests.Console/SkiaSharp/SKBitmapThreadingTest.cs
index a97e2384ab8..df393a13c60 100644
--- a/tests/SkiaSharp.Tests.Console/SkiaSharp/SKBitmapThreadingTest.cs
+++ b/tests/SkiaSharp.Tests.Console/SkiaSharp/SKBitmapThreadingTest.cs
@@ -19,9 +19,13 @@ public static void ImageScalingMultipleThreadsTest(int numThreads, int numIterat
{
// The (100, 1000) variant creates 100K undisposed native allocations to stress
// GC finalizer throughput. On x86 (2GB address space), the GC can't keep up and
- // Skia's native allocator fails. See #3608.
- if (IntPtr.Size == 4 && numThreads >= 100 && numIterationsPerThread >= 1000)
- Assert.Skip("Stress test skipped on x86 due to address space limit.");
+ // Skia's native allocator fails. See #3608. On musl (Alpine) the same allocation
+ // pressure across 100 threads stalls the allocator/finalizer interaction and the
+ // run hangs indefinitely (deterministic on x64 musl; glibc keeps up and passes).
+ // Skip the heavy variant on both; the (10,10) and (10,100) variants still run and
+ // exercise concurrent bitmap scaling on every platform.
+ if (numThreads >= 100 && numIterationsPerThread >= 1000 && (IntPtr.Size == 4 || IsMusl))
+ Assert.Skip("Heavy threading stress variant is skipped on x86 (address space limit) and musl (allocator/finalizer stalls).");
var referenceFile = Path.Combine(PathToImages, "baboon.jpg");
diff --git a/tests/Tests/BaseTest.cs b/tests/Tests/BaseTest.cs
index c38de2bd83c..7ba74f22f71 100644
--- a/tests/Tests/BaseTest.cs
+++ b/tests/Tests/BaseTest.cs
@@ -9,6 +9,13 @@ public abstract class BaseTest
protected static bool IsMac = TestConfig.Current.IsMac;
protected static bool IsUnix = TestConfig.Current.IsUnix;
protected static bool IsWindows = TestConfig.Current.IsWindows;
+ protected static bool IsNanoServer = TestConfig.Current.IsNanoServer;
+ protected static bool IsMusl = TestConfig.Current.IsMusl;
+
+ // XPS requires the Windows XPS Object Model / DirectWrite, present on desktop
+ // and Server Windows but not on Nano Server or non-Windows platforms (where
+ // CreateXps returns null).
+ protected static bool SupportsXps = IsWindows && !IsNanoServer;
protected static string[] UnicodeFontFamilies => TestConfig.Current.UnicodeFontFamilies;
protected static string DefaultFontFamily => TestConfig.Current.DefaultFontFamily;
@@ -94,5 +101,31 @@ protected static void SkipOnPlatform(bool condition, string reason)
{
Assert.SkipWhen(condition, reason);
}
+
+ // True when the platform has no system font manager that can enumerate or match fonts.
+ // WASM has no font manager; the NoDependencies Linux builds (skia_use_fontconfig=false) and
+ // Windows Nano Server have an "empty" font manager that reports one empty family and matches
+ // nothing. Probing a basic Latin character ('a') detects all three (guard IsBrowser first so
+ // we never call into a non-existent manager on WASM).
+ protected static bool HasNoSystemFontManager =>
+ IsBrowser || SkiaSharp.SKFontManager.Default.MatchCharacter('a') is null;
+
+ // True when there is no usable default typeface. The NoDependencies and Nano Server builds
+ // default to an empty typeface (no glyphs). WASM keeps a single embedded default font, so it
+ // is NOT considered fontless here.
+ protected static bool HasNoDefaultFont =>
+ SkiaSharp.SKTypeface.Default.IsEmpty;
+
+ // Skip tests that need the system font manager to enumerate or match fonts.
+ protected static void SkipWhenNoSystemFontManager(string reason = "This platform has no system font manager to enumerate or match fonts.")
+ {
+ Assert.SkipWhen(HasNoSystemFontManager, reason);
+ }
+
+ // Skip tests that need a usable default typeface (system fonts installed and enumerable).
+ protected static void SkipWhenNoDefaultFont(string reason = "This platform has no usable default typeface (no system fonts).")
+ {
+ Assert.SkipWhen(HasNoDefaultFont, reason);
+ }
}
}
diff --git a/tests/Tests/SkiaSharp/SKDocumentTest.cs b/tests/Tests/SkiaSharp/SKDocumentTest.cs
index 76901d8bf34..9d2e56d7717 100644
--- a/tests/Tests/SkiaSharp/SKDocumentTest.cs
+++ b/tests/Tests/SkiaSharp/SKDocumentTest.cs
@@ -44,19 +44,18 @@ public void PdfFileWithNonASCIIPathIsClosed()
[Fact]
public void XpsFileIsClosed()
{
+ Assert.SkipWhen(!SupportsXps, "XPS is only supported on desktop/server Windows");
+
var path = Path.Combine(PathToImages, Guid.NewGuid().ToString("D") + ".xps");
using (new SKAutoCoInitialize())
using (var doc = SKDocument.CreateXps(path))
{
- if (IsWindows)
- {
- Assert.NotNull(doc);
- Assert.NotNull(doc.BeginPage(100, 100));
+ Assert.NotNull(doc);
+ Assert.NotNull(doc.BeginPage(100, 100));
- doc.EndPage();
- doc.Close();
- }
+ doc.EndPage();
+ doc.Close();
}
File.Delete(path);
@@ -134,44 +133,29 @@ public void ManagedStreamDisposeOrder()
[Fact]
public void CanCreateXps()
{
- // XPS is only supported on Windows
+ Assert.SkipWhen(!SupportsXps, "XPS is only supported on desktop/server Windows");
using (var stream = new MemoryStream())
{
using (new SKAutoCoInitialize())
using (var doc = SKDocument.CreateXps(stream))
{
- if (IsWindows)
- {
- Assert.NotNull(doc);
- Assert.NotNull(doc.BeginPage(100, 100));
+ Assert.NotNull(doc);
+ Assert.NotNull(doc.BeginPage(100, 100));
- doc.EndPage();
- doc.Close();
- }
- else
- {
- Assert.Null(doc);
- }
+ doc.EndPage();
+ doc.Close();
}
- if (IsWindows)
- {
- Assert.True(stream.Length > 0);
- Assert.True(stream.Position > 0);
- }
- else
- {
- Assert.True(stream.Length == 0);
- Assert.True(stream.Position == 0);
- }
+ Assert.True(stream.Length > 0);
+ Assert.True(stream.Position > 0);
}
}
[Fact]
public void CanCreateXpsWithOptions()
{
- // XPS is only supported on Windows
+ Assert.SkipWhen(!SupportsXps, "XPS is only supported on desktop/server Windows");
var options = new SKDocumentXpsOptions { Dpi = 150, AllowNoPngs = true };
@@ -180,30 +164,35 @@ public void CanCreateXpsWithOptions()
using (new SKAutoCoInitialize())
using (var doc = SKDocument.CreateXps(stream, options))
{
- if (IsWindows)
- {
- Assert.NotNull(doc);
- Assert.NotNull(doc.BeginPage(100, 100));
+ Assert.NotNull(doc);
+ Assert.NotNull(doc.BeginPage(100, 100));
- doc.EndPage();
- doc.Close();
- }
- else
- {
- Assert.Null(doc);
- }
+ doc.EndPage();
+ doc.Close();
}
- if (IsWindows)
- {
- Assert.True(stream.Length > 0);
- Assert.True(stream.Position > 0);
- }
- else
+ Assert.True(stream.Length > 0);
+ Assert.True(stream.Position > 0);
+ }
+ }
+
+ [Fact]
+ public void CreateXpsReturnsNullWhereUnsupported()
+ {
+ Assert.SkipWhen(SupportsXps, "XPS is supported on this platform");
+
+ var options = new SKDocumentXpsOptions { Dpi = 150, AllowNoPngs = true };
+
+ using (var stream = new MemoryStream())
+ {
+ using (new SKAutoCoInitialize())
{
- Assert.True(stream.Length == 0);
- Assert.True(stream.Position == 0);
+ Assert.Null(SKDocument.CreateXps(stream));
+ Assert.Null(SKDocument.CreateXps(stream, options));
}
+
+ Assert.True(stream.Length == 0);
+ Assert.True(stream.Position == 0);
}
}
diff --git a/tests/Tests/SkiaSharp/SKFontManagerTest.cs b/tests/Tests/SkiaSharp/SKFontManagerTest.cs
index b9f10228781..7aa8463e533 100644
--- a/tests/Tests/SkiaSharp/SKFontManagerTest.cs
+++ b/tests/Tests/SkiaSharp/SKFontManagerTest.cs
@@ -10,7 +10,7 @@ public class SKFontManagerTest : SKTest
[Fact]
public void TestFontManagerMatchCharacter()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
var emoji = "🚀";
@@ -51,7 +51,7 @@ public void TestFamilyCount()
[Fact]
public void TestGetFontStyles()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
@@ -64,7 +64,7 @@ public void TestGetFontStyles()
[Fact]
public void TestMatchFamilyStyle()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
@@ -243,7 +243,7 @@ public unsafe void FromFamilyReturnsSameObject()
[Fact]
public unsafe void FromFamilyDisposeDoesNotDispose()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
@@ -261,7 +261,7 @@ public unsafe void FromFamilyDisposeDoesNotDispose()
[Fact]
public unsafe void TypefaceAndFontManagerReturnsSameObject()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
@@ -279,6 +279,7 @@ public unsafe void TypefaceAndFontManagerReturnsSameObject()
public unsafe void GCStillCollectsTypeface()
{
SkipOnNonWindows("Test uses Windows-specific font path");
+ SkipWhenNoSystemFontManager("Test resolves the 'Times New Roman' system font");
var handle = DoWork();
diff --git a/tests/Tests/SkiaSharp/SKFontStyleSetTest.cs b/tests/Tests/SkiaSharp/SKFontStyleSetTest.cs
index 4c9e9a29ee8..926ab54d3da 100644
--- a/tests/Tests/SkiaSharp/SKFontStyleSetTest.cs
+++ b/tests/Tests/SkiaSharp/SKFontStyleSetTest.cs
@@ -27,7 +27,7 @@ public void TestFindsNothing()
[Fact]
public void TestSetHasAtLeastOne()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
@@ -53,7 +53,7 @@ public void TestCanGetStyles()
[Fact]
public void TestCanCreateBoldFromIndex()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
var set = fonts.GetFontStyles(DefaultFontFamily);
@@ -82,7 +82,7 @@ public void TestCanCreateBoldFromIndex()
[Fact]
public void TestCanCreateBold()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
var set = fonts.GetFontStyles(DefaultFontFamily);
@@ -123,7 +123,7 @@ public void CreateTypefaceReturnsSameTypeface()
[Fact]
public unsafe void CreateTypefaceDisposeDoesNotDispose()
{
- SkipOnPlatform(IsBrowser, "WASM has no system font manager");
+ SkipWhenNoSystemFontManager();
var fonts = SKFontManager.Default;
var set = fonts.GetFontStyles(DefaultFontFamily);
diff --git a/tests/Tests/SkiaSharp/SKFontTest.cs b/tests/Tests/SkiaSharp/SKFontTest.cs
index fa42b14679a..9d957bf65fb 100644
--- a/tests/Tests/SkiaSharp/SKFontTest.cs
+++ b/tests/Tests/SkiaSharp/SKFontTest.cs
@@ -90,7 +90,7 @@ public void PlainGlyphsReturnsTheCorrectNumberOfCharacters()
[Fact]
public void UnicodeGlyphsReturnsTheCorrectNumberOfCharacters()
{
- SkipOnPlatform(IsBrowser, "WASM has no system fonts with emoji support");
+ SkipWhenNoSystemFontManager();
const string text = "🚀";
var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32);
@@ -109,6 +109,7 @@ public void UnicodeGlyphsReturnsTheCorrectNumberOfCharacters()
[Fact]
public void ContainsTextIsCorrect()
{
+ SkipWhenNoDefaultFont();
const string text = "A";
var font = new SKFont();
@@ -121,7 +122,7 @@ public void ContainsTextIsCorrect()
[Fact]
public void ContainsUnicodeTextIsCorrect()
{
- SkipOnPlatform(IsBrowser, "WASM has no system fonts with emoji support");
+ SkipWhenNoSystemFontManager();
const string text = "🚀";
var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32);
@@ -155,6 +156,7 @@ public void CanMeasureBadUnicodeText()
[Fact]
public void MeasureTextMeasuresTheText()
{
+ SkipWhenNoDefaultFont();
var font = new SKFont(SKTypeface.Default);
var width = font.MeasureText("Hello World!");
@@ -165,6 +167,7 @@ public void MeasureTextMeasuresTheText()
[Fact]
public void MeasureTextMeasuresTheTextForBytes()
{
+ SkipWhenNoDefaultFont();
var font = new SKFont(SKTypeface.Default);
var text8 = StringUtilities.GetEncodedText("Hello World!", SKTextEncoding.Utf8);
@@ -184,6 +187,7 @@ public void MeasureTextMeasuresTheTextForBytes()
[Fact]
public void MeasureTextReturnsTheBounds()
{
+ SkipWhenNoDefaultFont();
var font = new SKFont(SKTypeface.Default);
var width = font.MeasureText("Hello World!", out var bounds);
@@ -195,6 +199,7 @@ public void MeasureTextReturnsTheBounds()
[Fact]
public void MeasureTextReturnsTheBoundsForBytes()
{
+ SkipWhenNoDefaultFont();
var font = new SKFont(SKTypeface.Default);
var text8 = StringUtilities.GetEncodedText("Hello World!", SKTextEncoding.Utf8);
@@ -277,6 +282,7 @@ public void GetGlyphWidthsReturnsTheCorrectAmount()
[Fact]
public void GetGlyphWidthsAreCorrect()
{
+ SkipWhenNoDefaultFont();
SkipOnPlatform(IsBrowser, "WASM has no system fonts for glyph width measurement");
var font = new SKFont(SKTypeface.Default);
@@ -303,6 +309,7 @@ public void GetGlyphWidthsAreCorrect()
[Fact]
public unsafe void TextInterceptsAreFoundCorrectly()
{
+ SkipWhenNoDefaultFont();
var text = "|";
var font = new SKFont(SKTypeface.Default);
@@ -335,6 +342,7 @@ public void GetTextPathSucceedsForEmptyString()
[Fact]
public void GetTextPathWithPositionsProducesNonEmptyPath()
{
+ SkipWhenNoDefaultFont();
var font = new SKFont();
var text = "AV";
var glyphCount = font.CountGlyphs(text);
@@ -354,6 +362,7 @@ public void GetTextPathWithPositionsProducesNonEmptyPath()
[Fact]
public void GetTextPathWithPositionsMatchesExpectedBounds()
{
+ SkipWhenNoDefaultFont();
var font = new SKFont();
var text = "A";
@@ -384,6 +393,7 @@ public void GetTextPathWithPositionsMatchesExpectedBounds()
[InlineData(SKTextEncoding.GlyphId, "a", 2)]
public void BreakTextReturnsTheCorrectNumberOfBytes(SKTextEncoding encoding, string text, int expectedRead)
{
+ SkipWhenNoDefaultFont();
var font = new SKFont(SKTypeface.Default);
// get bytes
@@ -442,6 +452,7 @@ public void BreakTextReturnsTheCorrectNumberOfCharacters()
[InlineData(1 << 17)]
public void BreakTextWidthIsEqualToMeasureTextWidth(int textSize)
{
+ SkipWhenNoDefaultFont();
var font = new SKFont(SKTypeface.Default);
if (textSize >= 0)
@@ -465,6 +476,7 @@ public void BreakTextWidthIsEqualToMeasureTextWidth(int textSize)
[InlineData(1 << 17)]
public void BreakTextHandlesLongText(int textSize)
{
+ SkipWhenNoDefaultFont();
var font = new SKFont(SKTypeface.Default);
if (textSize >= 0)
@@ -562,6 +574,7 @@ public void CanSetTypefacesWithoutCrashing(string fontfile)
[Fact]
public void DefaultFontTypefaceIsDefault()
{
+ SkipWhenNoDefaultFont();
using var font = new SKFont();
Assert.NotNull(font.Typeface);
Assert.False(font.Typeface.IsEmpty);
@@ -598,6 +611,7 @@ public void FontTypefaceSetNullDoesNotCrashOnMeasure()
[Fact]
public void FontWithDefaultCanMeasure()
{
+ SkipWhenNoDefaultFont();
using var font = new SKFont(SKTypeface.Default);
var width = font.MeasureText("Hello World!");
Assert.True(width > 0);
diff --git a/tests/Tests/SkiaSharp/SKPaintTest.cs b/tests/Tests/SkiaSharp/SKPaintTest.cs
index df40125aaae..0d7605bc3ae 100644
--- a/tests/Tests/SkiaSharp/SKPaintTest.cs
+++ b/tests/Tests/SkiaSharp/SKPaintTest.cs
@@ -78,6 +78,7 @@ public void GetFillPathIsWorkingWithLine()
[Fact]
public void NonAntiAliasedTextOnScaledCanvasIsCorrect()
{
+ SkipWhenNoDefaultFont();
SkipOnPlatform(IsAndroid, "TODO: figure out why the font has changed");
SkipOnPlatform(IsBrowser, "WASM text rendering produces slightly different pixel values");
diff --git a/tests/Tests/SkiaSharp/SKTypefaceTest.cs b/tests/Tests/SkiaSharp/SKTypefaceTest.cs
index fc902523ac0..71bae11374d 100644
--- a/tests/Tests/SkiaSharp/SKTypefaceTest.cs
+++ b/tests/Tests/SkiaSharp/SKTypefaceTest.cs
@@ -240,7 +240,7 @@ public void PlainGlyphsReturnsTheCorrectNumberOfCharacters()
[Obsolete ("Tests obsolete SKTypeface.CountGlyphs/GetGlyphs — see SKFontTest for non-obsolete equivalents")]
public void UnicodeGlyphsReturnsTheCorrectNumberOfCharacters()
{
- SkipOnPlatform(IsBrowser, "WASM has no system fonts with emoji support");
+ SkipWhenNoSystemFontManager();
const string text = "🚀";
var emojiChar = StringUtilities.GetUnicodeCharacterCode(text, SKTextEncoding.Utf32);
@@ -260,6 +260,7 @@ public void UnicodeGlyphsReturnsTheCorrectNumberOfCharacters()
[Obsolete ("Tests obsolete SKTypeface.ContainsGlyphs — see SKFontTest for non-obsolete equivalents")]
public void ContainsGlyphsWithByteSpanDoesNotStackOverflow ()
{
+ SkipWhenNoDefaultFont();
using var typeface = SKTypeface.Default;
var text = System.Text.Encoding.UTF8.GetBytes ("Hello");
ReadOnlySpan span = text;
@@ -556,6 +557,7 @@ public unsafe void FromFamilyDisposeDoesNotDispose()
public unsafe void GCStillCollectsTypeface()
{
SkipOnNonWindows("Test uses Windows-specific font path");
+ SkipWhenNoSystemFontManager("Test resolves the 'Times New Roman' system font");
var handle = DoWork();
@@ -600,6 +602,7 @@ public void DefaultHasValidNativeHandle()
[Fact]
public void DefaultFamilyNameIsNotNullOrEmpty()
{
+ SkipWhenNoDefaultFont();
Assert.NotNull(SKTypeface.Default.FamilyName);
Assert.NotEmpty(SKTypeface.Default.FamilyName);
}
@@ -607,6 +610,7 @@ public void DefaultFamilyNameIsNotNullOrEmpty()
[Fact]
public void DefaultIsNotEmpty()
{
+ SkipWhenNoDefaultFont();
Assert.False(SKTypeface.Default.IsEmpty);
Assert.True(SKTypeface.Default.GlyphCount > 0);
}
diff --git a/tests/Tests/SkiaSharp/Visual/GoldenStore.cs b/tests/Tests/SkiaSharp/Visual/GoldenStore.cs
index a3eebbaab32..3e71ab61417 100644
--- a/tests/Tests/SkiaSharp/Visual/GoldenStore.cs
+++ b/tests/Tests/SkiaSharp/Visual/GoldenStore.cs
@@ -62,21 +62,22 @@ public ResolvedGolden(byte[] pixels, string location)
///
/// The default golden key for a cell, relative to the Goldens root:
- /// {renderer}.{platform}/{scene}.png. This is the path the
- /// captured-image marker carries and the harvest script writes to by
- /// default; a promoted, platform-portable golden lives at the shared
- /// {renderer}/{scene}.png key instead.
+ /// {renderer}.{platform}/{scene}.png using the most-specific platform
+ /// tag. This is the path the captured-image marker carries and the harvest
+ /// script writes to by default; a promoted, platform-portable golden lives at
+ /// the shared {renderer}/{scene}.png key instead.
///
public static string Key(string rendererName, string sceneName) =>
- $"{rendererName}.{VisualPlatform.Tag}/{sceneName}.png";
+ $"{rendererName}.{VisualPlatform.Tags[0]}/{sceneName}.png";
///
/// Golden keys for a cell in lookup order (most specific first):
- /// the per-platform override, then the platform-portable renderer golden.
+ /// each per-platform tag, then the platform-portable renderer golden.
///
public static IEnumerable Candidates(string rendererName, string sceneName)
{
- yield return $"{rendererName}.{VisualPlatform.Tag}/{sceneName}.png";
+ foreach (var tag in VisualPlatform.Tags)
+ yield return $"{rendererName}.{tag}/{sceneName}.png";
yield return $"{rendererName}/{sceneName}.png";
}
diff --git a/tests/Tests/SkiaSharp/Visual/Tests/VisualMatrixTestsBase.cs b/tests/Tests/SkiaSharp/Visual/Tests/VisualMatrixTestsBase.cs
index 1454d105548..528c8d34323 100644
--- a/tests/Tests/SkiaSharp/Visual/Tests/VisualMatrixTestsBase.cs
+++ b/tests/Tests/SkiaSharp/Visual/Tests/VisualMatrixTestsBase.cs
@@ -158,7 +158,7 @@ private void EmitVisualImage(string rendererName, string sceneName, string kind,
{
var normalized = RendererPixels.NormalizedInfo(info);
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
- var key = $"{rendererName}.{VisualPlatform.Tag}/{sceneName}.{kind}.png";
+ var key = $"{rendererName}.{VisualPlatform.Tags[0]}/{sceneName}.{kind}.png";
WriteOutput(
$"{VisualImageMarker} path={key} " +
$"size={normalized.Width}x{normalized.Height} base64={Convert.ToBase64String(data.ToArray())}");
@@ -216,7 +216,7 @@ private void FailUnseeded(string rendererName, string sceneName, SKImageInfo inf
// marker above), so seed it by harvesting the TRX and committing the
// result, after which the cell compares strictly and goes green.
Assert.Fail(
- $"No golden recorded yet for '{rendererName}/{sceneName}' on '{VisualPlatform.Tag}' " +
+ $"No golden recorded yet for '{rendererName}/{sceneName}' on '{string.Join("' / '", VisualPlatform.Tags)}' " +
$"(looked for {looked}). " +
"The rendered PNG is in the test results as a ##SKIA-GOLDEN-IMAGE## marker; " +
"seed it with scripts/infra/tests/extract-visual-goldens.py and commit. " +
diff --git a/tests/Tests/SkiaSharp/Visual/VisualPlatform.cs b/tests/Tests/SkiaSharp/Visual/VisualPlatform.cs
index 4da9edb7686..02197954706 100644
--- a/tests/Tests/SkiaSharp/Visual/VisualPlatform.cs
+++ b/tests/Tests/SkiaSharp/Visual/VisualPlatform.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
namespace SkiaSharp.Tests.Visual
{
@@ -10,7 +12,25 @@ namespace SkiaSharp.Tests.Visual
///
internal static class VisualPlatform
{
- public static string Tag { get; } = DetermineTag();
+ ///
+ /// Golden directory tags for the current host, most specific first. Usually a
+ /// single entry, but Windows Nano Server rasterizes text with FreeType instead
+ /// of DirectWrite, so it looks up its own nanoserver golden first and
+ /// falls back to the shared windows golden for cells that render
+ /// identically (shapes, gradients).
+ ///
+ public static IReadOnlyList Tags { get; } = DetermineTags().ToList();
+
+ private static IEnumerable DetermineTags()
+ {
+ // Nano Server IS Windows but rasterizes text with FreeType, not DirectWrite,
+ // so it looks up its own golden first and then falls back to the shared
+ // "windows" golden (DetermineTag returns "windows" on Nano).
+ if (TestConfig.Current.IsNanoServer)
+ yield return "nanoserver";
+
+ yield return DetermineTag();
+ }
private static string DetermineTag()
{
diff --git a/tests/Tests/TestConfig.cs b/tests/Tests/TestConfig.cs
index b19b83e2fe1..6b0f3c72a9f 100644
--- a/tests/Tests/TestConfig.cs
+++ b/tests/Tests/TestConfig.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Runtime.CompilerServices;
using SkiaSharp.Internals;
namespace SkiaSharp.Tests
@@ -20,6 +21,40 @@ public static TestConfig Current
public bool IsMac => PlatformConfiguration.IsMac;
public bool IsUnix => PlatformConfiguration.IsUnix;
public bool IsWindows => PlatformConfiguration.IsWindows;
+ public bool IsGlibc => PlatformConfiguration.IsGlibc;
+ public bool IsMusl => PlatformConfiguration.IsLinux && !PlatformConfiguration.IsGlibc;
+ public bool IsNanoServer => _isNanoServer.Value;
+
+ private static readonly Lazy _isNanoServer = new(DetectNanoServer);
+
+ // Windows Nano Server identifies itself in the registry as InstallationType
+ // "Nano Server" (full Windows reports "Client"/"Server", Server Core reports
+ // "Server Core"). Guard on Windows first, and keep the registry read in a
+ // separate non-inlined method so the Microsoft.Win32 types are only resolved
+ // (JIT-compiled) on Windows and never on the mobile/WASM test hosts.
+ private static bool DetectNanoServer() =>
+ PlatformConfiguration.IsWindows && ReadIsNanoServer();
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static bool ReadIsNanoServer()
+ {
+ try
+ {
+ using var key = Microsoft.Win32.Registry.LocalMachine
+ .OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
+ return string.Equals(
+ key?.GetValue("InstallationType") as string,
+ "Nano Server",
+ StringComparison.OrdinalIgnoreCase);
+ }
+ catch
+ {
+ // A constrained host can deny the registry read (SecurityException,
+ // UnauthorizedAccessException, etc.). Treat any failure as "not Nano Server"
+ // so a capability probe never fails the whole test run during config evaluation.
+ return false;
+ }
+ }
public string[] UnicodeFontFamilies { get; protected set; }
public string DefaultFontFamily { get; protected set; }