How PeachPDF Is Tested
PeachPDF is a rendering engine, and rendering bugs are easy to ship and hard to notice — a PDF can contain exactly the right operators and still look wrong. This page describes how the project guards against that: the automated test suite, the continuous-integration pipeline that runs it, the coverage gate on every change, and the rasterization-based checks that verify output actually looks right rather than merely containing the expected tokens.
If you’re contributing and want the exact local commands and conventions, see CONTRIBUTING.md — this page is the higher-level overview of what runs and why.
The test suite
The tests live in two xUnit projects that share a namespace: src/PeachBrackets.Engine.Tests (the HTML parser, CSS cascade, layout engines, painting and SVG subsystem, asserted on directly) and src/PeachPDF.Tests (PDF output, the declarative API, fonts and the rest), together 17,729 tests covering the HTML parser, CSS cascade, layout engines (block, inline, flex, table, multi-column), painting, the SVG subsystem, the font pipeline, and PDF output. The font and text engine, PeachDrawing.Text, has its own xUnit project, src/PeachDrawing.Text.Tests: the font parsers and tables, shaping, bidirectional text and script itemization, hyphenation, the Unicode conformance suites, and the public API of the package.
The suite multi-targets net8.0, net10.0, and net11.0, and all three target frameworks are first-class: continuous integration builds and runs the whole suite against each. (For routine local iteration you can run a single target framework to halve build/test time — see CONTRIBUTING.md — but that’s a local convenience, not the canonical way the project is validated.)
Continuous integration
Every pull request and every push to main runs the test workflow across a three-OS matrix — windows-latest, ubuntu-latest, and macos-latest — so platform-specific behavior (most often font discovery and text metrics) is exercised everywhere PeachPDF is expected to run. The work is split into parallel jobs so a run takes as long as its slowest leg rather than the sum of every step. On each OS:
- three test legs run side by side:
main(buildsPeachPDF.TestsandPeachBrackets.Engine.Testsin Release, installs the Playwright Chromium browser the engine project’s browser-backed tests use, and runs each project on each target framework in turn),engine(the font and text engine’s tests; every target framework on Ubuntu, net10.0 on Windows and macOS) andsmall(the CLI, drawing-abstraction, raster-backend, source-generator and managed Brotli codec tests). All of them collect code coverage. macOS does not install the .NET 11 release-candidate SDK, so net11.0 is covered on Windows and Ubuntu; - a coverage job merges that OS’s legs into one report and, on pull requests, enforces the diff-coverage gate; and
- a separate job builds the TestHarness showcase generator (see below) and the Blazor WebAssembly demo; and
- a conformance job (Ubuntu) renders a sample document at every PDF/A level — 1a through 4f — with the Release
peachpdfcommand-line tool and validates each file with veraPDF, failing the run if any level does not validate. veraPDF is pinned to one version and checksum, and the PDFs and its XML reports are uploaded as build artifacts. PDF/X has no veraPDF profile, so it is covered by the unit tests only.
A documentation-only change shouldn’t pay for the full build/test cycle, but the workflow also feeds a required status check — so it always runs, and the heavy jobs above are gated on whether anything under src/** actually changed. When only docs change, they are skipped and the final test-gate job still reports success. That test-gate job is the single check to require: it fails if any test, coverage or build job failed or was cancelled. A newer push to the same pull request cancels its in-flight run, and every job has a timeout so a hung test host cannot hold a runner for hours.
Coverage gate
Pull requests must meet 90% diff coverage — the coverage of the lines the change actually touches, not the whole codebase. This is enforced by diff-cover comparing the change against its base branch, using coverlet output in Cobertura format (collected through its Microsoft.Testing.Platform integration). The HTML coverage report and the diff-coverage report are uploaded as build artifacts on every run, so a shortfall is easy to inspect.
The intent is simple: new and changed code arrives with tests, rather than leaving CI to discover the gap after merge.
Rasterization-based verification (a development practice, not a CI gate)
A test that only asserts on substrings of the PDF content stream (/SMask, Tj, /ShadingType, and the like) is not proof that a feature renders correctly. A token can be fully present while the composed, positioned result is visually broken or blank — an entirely non-functional <mask> implementation once passed every substring test it had. For anything touching PDF graphics state — soft masks, patterns, clip paths, gradients, transparency groups, transforms — the automated suite prefers structural/adjacency assertions (for example, checking that a graphics-state operator and the drawing operator it modifies appear together) rather than a bare token search.
Beyond those automated assertions, the strongest check is to rasterize the output and inspect the pixels — and where transparency, soft-mask, or blend-mode output is involved, to rasterize with two independent renderers, not one:
- PDFium — the engine inside Chrome and Edge, and the stricter, more representative check.
- MuPDF — useful as a second opinion, but unusually lenient about transparency-group conformance: it will happily render content “correctly” that PDFium refuses.
Agreement between both is real evidence; a single lenient render that happens to look right is not. This two-renderer cross-check is not part of the automated CI pipeline — it’s a manual verification step performed during development when a change touches graphics-state output, and it’s a practice we recommend contributors run themselves before submitting such a change. (One way to do it is a short Python script using pymupdf and pypdfium2 to render the same page with each engine and compare the images.)
The showcase harness
src/PeachPDF.TestHarness is a small runnable app that renders a gallery of feature showcases. It serves two purposes: it generates the PDFs behind the Feature Showcase on the documentation site, and it’s a place to visually exercise a new capability. CI builds it on every source change so a break is caught before release day, not on it.
This matters because automated assertions alone have real blind spots. Several genuine rendering bugs — a paint-order regression, a broken soft mask, a gradient spread-method that was silently a no-op at render time — were caught only by looking at a showcase render, not by the token-level tests that were passing at the time.
Rendering the showcases through the SVG backend
--svg makes the harness render every HTML showcase a second time, through PeachBrackets onto the SvgCanvas of PeachDrawing.Svg instead of onto a PDF page, writing <slug>.svg beside the PDF, as one continuous sheet cut to the content rather than a stack of paper pages (the harness makes the sheet endless, turns off forced page breaks and named pages, and measures where the content ends); --svg-text writes selectable <text> with embedded subset fonts instead of glyph outlines. It is the same engine, layout and paint code that produce the PDF, so the result is a direct, whole-document look at what the SVG backend does - the site’s showcase cards link it as View SVG. A showcase the SVG backend cannot render is reported and skipped; it never fails the run. The unit tests of the backend itself are in src/PeachDrawing.Svg.Tests, and src/PeachDrawing.Svg.Example is a small runnable sample.
Round-tripping the showcases through the PDF importer
The showcase PDFs double as a corpus for the PDF importer. --import-roundtrip <showcaseDir> <roundtripDir> opens every PDF in a showcase output directory with PdfPageSource, paints each page onto a new document and saves it under the same name, writing a report.txt that names the features the importer could not draw for each file. src/PeachPDF.TestHarness/compare-roundtrip.py then rasterizes each original and its round trip with both MuPDF and PDFium and lists the pages whose pixels differ by more than a threshold in either renderer, optionally with side-by-side images. It is a development tool, not a CI gate: it needs Python with pymupdf, pypdfium2, pillow and numpy, and is run after a change to the importer.
Benchmarks
src/PeachDrawing.Text.Benchmarks is a BenchmarkDotNet project for measuring the font and text engine’s hot paths and catching performance regressions. Today it covers TrueType and CFF hinting: a cold benchmark that loads every glyph of a bundled font at several sizes into an empty cache (with allocations reported), and a hot benchmark that looks glyphs up in a warm cache from one, four and eight threads at once. It reads the engine only through the public API, like the engine’s tests, and isn’t part of the per-PR gate or any CI job; it’s run deliberately when a change is expected to affect performance. Run it from src/, in Release, with nothing else building or testing on the machine:
dotnet run -c Release --project PeachDrawing.Text.Benchmarks -- --filter "*HintingCold*"
dotnet run -c Release --project PeachDrawing.Text.Benchmarks -- --filter "*HintingHot*"
For timing whole-document rendering, the showcase harness has a --benchmark mode that times and measures the allocations of each showcase’s render.
See also
- Architecture — what each of these tests is verifying: the HTML → DOM → CSS → layout → paint → PDF pipeline.
- CONTRIBUTING.md — exact local commands, testing conventions (layout-property assertions,
Canvasrecording mocks), and how to reproduce the coverage gate locally.