Add processing pipeline test spec with golden outputs#434
Conversation
Add documentcloud/documents/processing/tests/spec/: an executable version of the processing pipeline output contract researched in MuckRock/research/processing-pipeline-spec. Each test document under documents/ has an input PDF and an expected/ directory containing every file the current pipeline produces for it (processed PDF, concatenated and per-page text, txt.json, position.json, all five page image sizes, index cache) plus the database-facing metadata captured from the API callbacks (page_count, page_spec, file_hash, status). The harness runs the real serverless functions in-process - pdfium rendering, Tesseract OCR, grafting, and text position extraction are not mocked - against local storage and Redis, with pubsub topics dispatched through a FIFO queue to mirror one-Lambda-per-message production behavior. The corpus covers embedded text, scanned, mixed, force-OCR, mixed page sizes, blank page, and redaction cases. test_spec.py validates a fresh pipeline run against the goldens (OCR cases skip when the LFS Tesseract libraries or pinned traineddata are unavailable); generate.py regenerates the goldens after intentional behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Le7iTGKrY5TmE4jL4GhnWU
Pull the Tesseract LFS libraries in the CI test job (only the ocr/tesseract directory, to avoid fetching the large LibreOffice bundle) so the OCR test spec cases run in CI instead of skipping. The pinned eng.traineddata is downloaded automatically on first use. Also fix pylint findings in the spec package and drop an unused compare_case parameter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Le7iTGKrY5TmE4jL4GhnWU
Review: gaps in coverage, measured against the goal of pipeline replaceabilityThe goal this spec serves: be able to swap out parts of the pipeline — or replace it entirely — and have confidence the new system produces the same final outputs. This review evaluates the PR against that goal, not against internal implementation details (callback ordering, message choreography, batch sizes are explicitly not the contract). What holds up well
Gaps, ranked1. The comparison rules encode "same implementation, unchanged" — not "equivalent output." GIF comparison is byte-exact, so a replacement renderer (or even a pdfium version bump) fails on every image; OCR text is byte-exact, so a different Tesseract build fails on every scanned page. Those rules are right for regression-testing the current pipeline, but for replacement confidence a second, looser mode is needed: perceptual/tolerance image comparison, OCR similarity thresholds rather than equality. The pieces are already there — 2. The goldens enshrine implementation quirks as contract without deciding whether they are contract. The README documents them honestly — pdfium's trailing 3. Corpus breadth is the biggest coverage gap for replacement confidence. Eight synthetic PyMuPDF documents exercise the happy path of the exact libraries that generated them. A replacement could match all eight and still diverge on real-world documents: rotated pages, non-Latin/RTL text, ligatures and odd encodings, scanned pages with skew and noise, PDFs with forms or broken xref tables, JPEG2000 images, very large page dimensions. A handful of real (redistributable) documents would carry more weight than any harness improvement. 4. No error-path case, and it's not in the README's "not covered" list. "Given a corrupt/truncated/password-protected input, the system reports an error state to the API" is a final-output contract a replacement must honor, and it's currently untested and undocumented. The harness is already positioned for it: 5. The redacted 6. Make the replacement seam explicit in the README. A short section stating the boundary — the corpus and Harness robustness (minor)
Acknowledged and reasonableThe README's "not covered" list (Textract, non-PDF conversion, page modifications, import, None of these are blockers. Items 1–3 are direction-setting for the replaceability goal; items 4–5 are the concrete contract holes worth closing soonest. 🤖 Generated with Claude Code |
- Compare the full API callback sequence in metadata.json, not just the merged database fields: order, methods, URLs, and payloads must match. page_spec values are compared decoded, file_hash values are loosened to presence + SHA-1 shape for redaction cases (the redacted PDF is rewritten non-reproducibly), and storage bucket prefixes are stripped from error messages. This makes regressions like redaction resending page_spec or dropping its file_hash update detectable. - Add corrupt-1page: an unparseable PDF must produce an error POST to the API and no output files. The runner neutralizes the local sentry stub's re-raise so the error path completes as it does in production. - Add text-4page: four pages exceed TEXT_POSITION_BATCH (3), exercising the multi-batch flush path for text position extraction. - ocr_libraries_available now verifies the libraries actually load (they are Linux x86-64 builds), so non-Linux checkouts skip instead of error. - CI: cache the pinned eng.traineddata and set DC_SPEC_REQUIRE_OCR so an unavailable OCR runtime fails the OCR cases instead of skipping them. - Keep the actual output tree on test failure for easier diffing; default needs_ocr/expects_error in load_case; document access levels, remaining error paths, and the Linux platform pinning as known gaps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Le7iTGKrY5TmE4jL4GhnWU
|
Addressed in 7353e79 — all seven items:
All 10 cases pass locally (and 5 pass + 5 skip cleanly with LFS pointers, matching what a plain checkout sees); pylint 10/10, isort/black clean. Agreed on page modifications as the next corpus addition — the Generated by Claude Code |
compare_case now takes strictness="exact" (default; regression testing the current pipeline, byte-exact) or strictness="equivalent" (validating a modified or replacement pipeline against the same goldens): - trailing control characters (pdfium's NUL, Tesseract's form feed) are normalized away, treating them as implementation accidents rather than contract; OCR text is held to a similarity threshold instead of equality - images require the same dimensions and a small mean pixel delta rather than identical bytes - positions must parse, stay in the 0-1 range, agree on emptiness, and carry approximately the same text - the txt.json ocr field is compared as OCR'd-or-not rather than by engine name; file_hash must be well-formed but is not compared by value; the callback sequence is not compared, though error reports still must be sent Exposed via generate.py --check --equivalent. test_spec.py and CI stay on exact mode. The README documents the replacement seam (corpus + compare are implementation-independent; harness.py is the current pipeline's runner) and lists real-world corpus breadth and quirk verdicts as open items. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Le7iTGKrY5TmE4jL4GhnWU
|
Follow-up for the replaceability framing, in 5fe986f: 1 (strictness knob) — 6 (seam) — README now has a "Validating a modified or replacement pipeline" section stating the boundary explicitly: 2 (quirk verdicts) — took a default position rather than deciding for the team: exact mode pins the quirks (NUL, 3 (real-world corpus) — agreed this is the biggest gap; added to the README's gap list as the highest-value improvement. Picking small redistributable real documents (rotated, RTL, skewed scans, broken xref, JPEG2000) is a curation call — if you point me at candidates (or say "grab N public-domain docs from production"), I'll add them and generate goldens. Items 4–5 and the robustness notes were addressed in 7353e79 (error-path case, shape-checked redaction Generated by Claude Code |
Summary
Adds
documentcloud/documents/processing/tests/spec/: an executable version of the processing pipeline output contract researched in MuckRock/research/processing-pipeline-spec. The goal is to be able to change processing code and validate that the pipeline's outputs stay the same (or that a diff shows exactly what changed).For each test document under
documents/, anexpected/directory contains every file the current pipeline produces — processed PDF, concatenated and per-page text,txt.json,position.json, all five page image sizes, the.indexcache — plusmetadata.jsonrecording the database-facing API callbacks (page_count,page_spec,file_hash,status).How it works
harness.pyruns the real serverless functions (info_and_image,ocr) in-process against local storage and Redis. Nothing in the pipeline is mocked: pdfium rendering, Tesseract OCR (the LFS-bundledlibtesseract), grafting, and pdfplumber positions all run for real. Pubsub topics dispatch through a FIFO queue, mirroring production's one-Lambda-per-message model.test_spec.py(pytest) reruns the pipeline for every case and compares against the goldens. It runs in CI with the existing Redis service; OCR cases skip automatically when the LFS Tesseract libraries or pinned traineddata aren't present (CI doesn't check out LFS, so CI covers the embedded-text cases).generate.pyregenerates the goldens after an intentional behavior change;generate.py --checkvalidates without pytest.compare.pyencodes the normalization rules for known non-determinism:updatedtimestamps, PDF bytes (uuid-named XObjects from the grafter, pikepdf/ID),page_specsegment order (Redis set iteration), and the redacted-PDFfile_hash.Test corpus (8 cases)
Embedded text (1 and 3 pages), scanned/image-only, mixed text+scan,
force_ocr, mixed page dimensions, blank page, and a redaction pass. Inputs are built deterministically bymake_corpus.pyand committed (~1.7 MB total including goldens).Notable pipeline behaviors the goldens capture: pdfium text ends with a trailing
\x00, Tesseract text ends with\f(a blank page yields"\f", not""),force_ocrproducesocr: "tess4_force", and redaction re-sendsfile_hash/statusbut notpage_spec.Not covered yet
Textract OCR, non-PDF conversion (needs the LibreOffice LFS bundle), page modifications (needs
storage.async_downloadin local storage), bulk import,set_page_text, and non-English OCR — all documented in the README.Test plan
pytest documentcloud/documents/processing/tests/spec/test_spec.pyrepeatedly across fresh processesgenerate.py --checkreproduces the committed goldens byte-for-byte (modulo the documented normalizations)🤖 Generated with Claude Code
https://claude.ai/code/session_01Le7iTGKrY5TmE4jL4GhnWU
Generated by Claude Code