mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 10:39:01 -06:00
55 files hand-rolled the same serialiser -- number the objects, record each one's offset, emit a classic cross-reference table and a trailer -- 111 times over. The object bodies are properly the business of the crate under test; the envelope around them is not, and every copy is a place for a fixture to be quietly wrong in a way that makes a test prove less than it appears to. `pdfcraft-testkit` holds that envelope. It has no workspace dependencies on purpose, so the crates at the bottom of the layering can use it without a cycle, and it returns bytes rather than a parsed document for the same reason. The builder is byte-identical to the serialiser it replaces, asserted against a verbatim copy of the old code across versions and trailer variations, because a migration that changes a fixture's bytes silently changes what the test using it proves. Watched failing by breaking the free-entry generation: three tests caught it. The one intended difference is that an empty stream dictionary gives `<< /Length N >>` rather than a doubled space. Migrates cos, measure and redact: 111 copies in 55 files becomes 108 in 53. The rest are follow-ups rather than one sweep -- ui-egui alone is 27 of those files and is the most contested area in the repository. Some copies should stay: a test about a malformed cross-reference table needs to write its own bytes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
58 lines
2.8 KiB
Markdown
58 lines
2.8 KiB
Markdown
# pdfcraft-testkit
|
|
|
|
Synthetic PDF fixtures: **one way to write the bytes, so tests differ only where they mean to.**
|
|
|
|
- **Layer:** test support (`Class::Testkit` in `xtask/src/layers.rs`). A dev-dependency only, never
|
|
compiled into the product.
|
|
- **No workspace dependencies**, deliberately. The crates at the bottom of the layering (`cos`,
|
|
`geom`, `filters`, `crypt`, `arlington`) can use it in their own tests without a cycle, which is
|
|
also why it hands back bytes rather than a parsed `Document`.
|
|
|
|
## Why it exists
|
|
|
|
55 files hand-rolled the same serialiser — number the objects, record each one's offset, emit a
|
|
classic cross-reference table and a trailer — **111 times over**. The object *bodies* are properly
|
|
the business of the crate under test. The envelope around them is not, and every copy is a place
|
|
for a fixture to be quietly wrong in a way that makes a test prove less than it appears to.
|
|
|
|
## API
|
|
|
|
```rust
|
|
use pdfcraft_testkit::Pdf;
|
|
|
|
let bytes = Pdf::new()
|
|
.version("1.4") // default 1.7
|
|
.obj("<< /Type /Catalog /Pages 2 0 R >>") // becomes 1 0 R
|
|
.obj("<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
|
|
.obj("<< /Type /Page /Parent 2 0 R /Contents 4 0 R >>")
|
|
.stream("", b"BT /F1 12 Tf (hi) Tj ET") // /Length computed
|
|
.trailer_entry("/Info 5 0 R")
|
|
.build();
|
|
```
|
|
|
|
Objects are numbered from 1 in the order they are added, all at generation 0; the trailer always
|
|
names `1 0 R` as `/Root` and carries the right `/Size`. `next_obj()` gives the number the next
|
|
object will get, for forward references. `stream_obj(dict, data)` builds a stream body on its own,
|
|
for callers that assemble their object list first.
|
|
|
|
## Byte-identical by construction
|
|
|
|
`the_builder_is_byte_identical_to_the_serialiser_it_replaces` keeps a verbatim copy of the
|
|
hand-rolled serialiser and asserts the builder agrees with it, across versions and trailer
|
|
variations. Migrating a crate must not change a single byte of its fixtures — otherwise the test
|
|
that fixture feeds silently starts testing something else.
|
|
|
|
The one intended difference: an empty stream dictionary gives `<< /Length N >>` rather than the
|
|
doubled space `<< /Length N >>` that some copies produced. Parsers accept both.
|
|
|
|
## Migration status
|
|
|
|
Three crates are migrated (`cos`, `measure`, `redact`), taking the count from 111 copies in 55
|
|
files to **108 in 53**. The rest are follow-ups, deliberately not done in one sweep: `ui-egui`
|
|
alone accounts for 27 of those files and is the most contested area in the repository, so a single
|
|
large mechanical change there would rot before it landed.
|
|
|
|
Not every copy should move. A test that is *about* a malformed or unusual cross-reference table
|
|
needs to write its own bytes; this builder is for the fixtures that are merely scaffolding for
|
|
something else.
|