Files
photocraft/xtask/src/corpus.rs
T
callmeBallochandClaude a8d7c6ef3f AF.3: put the Affinity fuzz targets and corpus back in CI
The maintainers approved the new crate, its fuzz targets and the corpus
upstreams in principle (storytold/photocraft#1607), so this undoes the
interim opt-in: ci.yml fuzzes preview and import_preview again, corpus
--all fetches the Affinity set, and its test runs under the normal
corpus feature. The provenance section stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
2026-10-09 06:38:40 +00:00

224 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `cargo xtask corpus [...]` (fetch test corpora) and `cargo xtask test-corpus [...]` (fetch,
//! then run the corpus tests). Pins: `corpus_pins.rs`. Docs: `docs/development.md` › Test corpora.
use std::path::PathBuf;
use std::process::Command;
use crate::corpus_pins::{self, AG_PSD_COMMIT, HEIC_RS_COMMIT, OPENEXR_COMMIT, PHOTOCRAFT_CORPUS_COMMIT, PILLOW_HEIF_COMMIT, PNGSUITE_URL, PSD_TOOLS_COMMIT};
use crate::pinned::USER_AGENT;
use crate::{cargo, root, run};
/// Crates with corpus tests (behind their `corpus` feature).
pub const CORPUS_CRATES: &[&str] = &["photocraft-psd", "photocraft-codecs", "photocraft-io", "photocraft-engine"];
/// Corpus crates with a `heif` feature: test-corpus enables it so the HEIF corpus tests run.
const HEIF_CRATES: &[&str] = &["photocraft-codecs", "photocraft-io"];
/// Paths whose changes make `test-corpus --changed` run (the file-format and rendering crates).
const CRITICAL: &[&str] = &["crates/psd/", "crates/io/", "crates/codecs/", "crates/heif/", "crates/compose/", "crates/gpu/", "crates/text/", "crates/format/"];
/// `cargo xtask corpus [--all | --pngsuite | --download | --psd | --psd-tools | --heif | --exr | --affinity | --photoshop [--local]] [--update-manifest]`
pub fn cmd(args: &[&str]) -> Result<(), String> {
let update = args.contains(&"--update-manifest");
let mut did = false;
if args.contains(&"--all") {
return fetch_all(args.contains(&"--local"));
}
if args.contains(&"--pngsuite") || args.contains(&"--download") {
fetch_pngsuite()?;
did = true;
}
if args.contains(&"--psd") {
corpus_pins::PSD_MIXED.fetch(update)?;
did = true;
}
if args.contains(&"--psd-tools") {
corpus_pins::PSD_TOOLS.fetch(update)?;
did = true;
}
if args.contains(&"--heif") {
corpus_pins::HEIF.fetch(update)?;
did = true;
}
if args.contains(&"--exr") {
corpus_pins::EXR.fetch(update)?;
did = true;
}
if args.contains(&"--affinity") {
corpus_pins::AFFINITY.fetch(update)?;
did = true;
}
if args.contains(&"--photoshop") {
if args.contains(&"--local") {
corpus_pins::PHOTOSHOP.fetch_local(&local_clone()?, update)?;
} else {
corpus_pins::PHOTOSHOP.fetch(update)?;
}
did = true;
}
if !did {
list();
}
Ok(())
}
/// The authoring clone of photocraft-corpus: `$PHOTOCRAFT_CORPUS_REPO`, else `../photocraft-corpus`.
fn local_clone() -> Result<PathBuf, String> {
if let Some(p) = std::env::var_os("PHOTOCRAFT_CORPUS_REPO").filter(|v| !v.is_empty()) {
return Ok(PathBuf::from(p));
}
let sibling = root().parent().map(|p| p.join("photocraft-corpus"));
sibling.filter(|s| s.join("photoshop").is_dir()).ok_or_else(|| {
"no local photocraft-corpus clone: expected ../photocraft-corpus next to this checkout, or set PHOTOCRAFT_CORPUS_REPO=<path>".to_string()
})
}
/// Fetches every corpus that is missing or stale (verified ones are left alone). With `local`,
/// `corpus/photoshop` is copied from the authoring clone of photocraft-corpus instead.
pub fn fetch_all(local: bool) -> Result<(), String> {
fetch_pngsuite()?;
for c in corpus_pins::ALL {
if local && std::ptr::eq(*c, &corpus_pins::PHOTOSHOP) {
c.fetch_local(&local_clone()?, false)?;
} else {
c.fetch(false)?;
}
}
println!("all corpora present and verified under {}", root().join("corpus").display());
Ok(())
}
fn status(present: bool) -> &'static str {
if present { "present" } else { "missing" }
}
fn list() {
let corpus = root().join("corpus");
let png = corpus.join("pngsuite");
let png_ok = pngsuite_count(&png) > 0;
println!(
"Test corpora live under {} (gitignored, never committed). The corpus tests are opt-in
(cargo feature `corpus`) and fail, not skip, when a corpus is missing. CI always runs them.
cargo xtask corpus --all fetch every corpus below that is missing or stale (sha256-verified)
cargo xtask test-corpus fetch, then run every corpus test (--release --features corpus)
corpus/photoshop/ [{}] our Photoshop-authored oracles (MIT OR Apache-2.0), from
https://github.com/storytold/photocraft-corpus at {PHOTOCRAFT_CORPUS_COMMIT}
manifest xtask/photoshop-corpus.sha256. Fetch: --photoshop
(--photoshop --local copies from ../photocraft-corpus or $PHOTOCRAFT_CORPUS_REPO)
corpus/psd/ [{}] 170 small psd-tools + ag-psd files (MIT), psd-tools@{} and
ag-psd@{}, manifest xtask/psd-corpus.sha256. Fetch: --psd
corpus/psd-tools/ [{}] the full psd-tools test set (MIT) at {PSD_TOOLS_COMMIT}
manifest xtask/psd-tools-corpus.sha256. Fetch: --psd-tools
corpus/heif/ [{}] a few HEIC/HEIF files from heic-rs@{} (MIT OR Apache-2.0) and
pillow-heif@{} (BSD-3-Clause), manifest xtask/heif-corpus.sha256. Fetch: --heif
corpus/exr/ [{}] the deep OpenEXR test images (BSD-3-Clause), openexr@{}
manifest xtask/exr-corpus.sha256. Fetch: --exr
corpus/affinity/ [{}] 21 public Affinity 1–3 documents (CC0, MIT): vector-art, AFDesignLoad,
Jac21/Branding, AssetStoreTemplate; manifest xtask/affinity-corpus.sha256. Fetch: --affinity
corpus/pngsuite/ [{}] PngSuite (public domain), {PNGSUITE_URL}. Fetch: --pngsuite
corpus/tiff/, corpus/raw/ optional, copied in by hand
Pins: xtask/src/corpus_pins.rs. Moving one: change it, then --<name> --update-manifest.",
corpus.display(),
status(corpus_pins::PHOTOSHOP.is_current()),
status(corpus_pins::PSD_MIXED.is_current()),
&PSD_TOOLS_COMMIT[..12],
&AG_PSD_COMMIT[..12],
status(corpus_pins::PSD_TOOLS.is_current()),
status(corpus_pins::HEIF.is_current()),
&HEIC_RS_COMMIT[..12],
&PILLOW_HEIF_COMMIT[..12],
status(corpus_pins::EXR.is_current()),
&OPENEXR_COMMIT[..12],
status(corpus_pins::AFFINITY.is_current()),
status(png_ok),
);
}
fn pngsuite_count(dir: &std::path::Path) -> usize {
std::fs::read_dir(dir).map(|d| d.flatten().filter(|e| e.path().extension().is_some_and(|x| x == "png")).count()).unwrap_or(0)
}
/// PngSuite: 175 PNGs from a fixed release archive (skipped when already present).
fn fetch_pngsuite() -> Result<(), String> {
let corpus = root().join("corpus");
let dest = corpus.join("pngsuite");
let have = pngsuite_count(&dest);
if have >= 175 {
println!("pngsuite: {have} PNG files in {} already", dest.display());
return Ok(());
}
std::fs::create_dir_all(&dest).map_err(|e| format!("create {}: {e}", dest.display()))?;
let tgz = corpus.join("PngSuite-2017jul19.tgz");
let mut curl = Command::new("curl");
curl.args(["-fsSL", "--retry", "3", "-A", USER_AGENT, "-o"]).arg(&tgz).arg(PNGSUITE_URL);
run(curl, &format!("curl {PNGSUITE_URL}"))?;
let mut tar = Command::new("tar");
tar.arg("-xzf").arg(&tgz).arg("-C").arg(&dest);
run(tar, "tar -xzf PngSuite-2017jul19.tgz")?;
let _ = std::fs::remove_file(&tgz);
let n = pngsuite_count(&dest);
println!("pngsuite: {n} PNG files in {}", dest.display());
if n == 0 { Err("no PNG files extracted".into()) } else { Ok(()) }
}
/// Files changed relative to `origin/main` (committed, staged or not).
fn changed_files() -> Result<Vec<String>, String> {
let out = Command::new("git").current_dir(root()).args(["diff", "--name-only", "origin/main"]).output().map_err(|e| format!("git diff: {e}"))?;
if !out.status.success() {
return Err(format!("git diff origin/main failed: {}", String::from_utf8_lossy(&out.stderr)));
}
Ok(String::from_utf8_lossy(&out.stdout).lines().map(str::to_string).collect())
}
/// `cargo xtask test-corpus [-p <crate>]... [--changed] [--local] [-- <test args>]`
pub fn test_cmd(args: &[&str]) -> Result<(), String> {
let (ours, passthrough) = match args.iter().position(|a| *a == "--") {
Some(i) => (&args[..i], &args[i + 1..]),
None => (args, &[][..]),
};
if ours.contains(&"--changed") {
let changed = changed_files()?;
let hits: Vec<&String> = changed.iter().filter(|f| CRITICAL.iter().any(|c| f.starts_with(c))).collect();
if hits.is_empty() {
println!("test-corpus --changed: nothing under {} changed since origin/main; skipping", CRITICAL.join(", "));
return Ok(());
}
println!("test-corpus --changed: {} critical files changed (e.g. {}); running the corpus tests", hits.len(), hits[0]);
}
let mut crates: Vec<String> = Vec::new();
let mut it = ours.iter();
while let Some(a) = it.next() {
match *a {
"-p" | "--package" => {
let name = it.next().ok_or("-p needs a crate name")?;
let full = if name.starts_with("photocraft-") { (*name).to_string() } else { format!("photocraft-{name}") };
if !CORPUS_CRATES.contains(&full.as_str()) {
return Err(format!("{full} has no corpus tests (crates: {})", CORPUS_CRATES.join(", ")));
}
crates.push(full);
}
"--changed" | "--local" => {}
other => return Err(format!("test-corpus: unknown argument `{other}`")),
}
}
if crates.is_empty() {
crates = CORPUS_CRATES.iter().map(|s| (*s).to_string()).collect();
}
fetch_all(ours.contains(&"--local"))?;
let mut c = cargo();
c.args(["test", "--release", "--lib", "--tests"]);
for k in &crates {
c.args(["-p", k]);
}
let mut features: Vec<String> = crates.iter().map(|k| format!("{k}/corpus")).collect();
features.extend(crates.iter().filter(|k| HEIF_CRATES.contains(&k.as_str())).map(|k| format!("{k}/heif")));
c.args(["--features", &features.join(",")]);
if !passthrough.is_empty() {
c.arg("--").args(passthrough);
}
run(c, &format!("cargo test --release --features corpus,heif ({})", crates.join(", ")))
}