mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-03 06:59:53 -06:00
libtest has no skipped status and hides the output of passing tests, so a test that returns early without a GPU, a corpus file, a craft-fonts face or a model passed silently. `lightcraft-testkit` (the testkit slot the layering table already reserves, dev-dependency only, std only) adds `skip!(Need, "reason")`: it prints the note as before and, when LIGHTCRAFT_SKIP_LOG names a file, appends one JSON line (need, reason, test, binary, file:line). Plain `cargo test` is unchanged. The 61 existing skip sites use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
5.3 KiB
Rust
109 lines
5.3 KiB
Rust
//! [`lightcraft_raw::sparse::header_plan`] over `corpus/raw/**` (git-ignored CC0 samples from
|
|
//! raw.pixls.us, fetched with `cargo xtask corpus --download`; `LIGHTCRAFT_CORPUS` overrides the
|
|
//! corpus root). Skips cleanly when absent.
|
|
//!
|
|
//! For every file the plan reaches `Ready` on, probing the partly loaded buffer must give exactly
|
|
//! what probing the whole file gives — with the skipped bytes zero and again with them 0xFF, so a
|
|
//! reader that looked at skipped data would be caught. CR3 and DNG files must be described from a
|
|
//! small part of the file (`cargo test -p lightcraft-raw --release --test sparse_corpus -- --nocapture`
|
|
//! prints what each format reads).
|
|
|
|
use lightcraft_raw::sparse::{HeaderPlan, Ranges, header_plan, preview_plan};
|
|
use lightcraft_raw::{embedded_preview, probe, probe_info};
|
|
use std::collections::BTreeMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn corpus_root() -> PathBuf {
|
|
std::env::var_os("LIGHTCRAFT_CORPUS").map(PathBuf::from).unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus"))
|
|
}
|
|
|
|
/// Run the plan (with the embedded preview when `preview`), loading from `file` what it asks for
|
|
/// into a buffer whose other bytes are `fill`.
|
|
fn run(file: &[u8], fill: u8, preview: bool) -> (HeaderPlan, Vec<u8>, Ranges) {
|
|
let mut buf = vec![fill; file.len()];
|
|
let mut loaded = Ranges::new();
|
|
let prefix = file.len().min(512 * 1024);
|
|
buf[..prefix].copy_from_slice(&file[..prefix]);
|
|
loaded.insert(0, prefix as u64);
|
|
for _ in 0..32 {
|
|
match if preview { preview_plan(&buf, &loaded) } else { header_plan(&buf, &loaded) } {
|
|
HeaderPlan::Need(r) => {
|
|
for &(a, b) in r.spans() {
|
|
assert!(!loaded.overlaps(a, b), "asked again for {a}..{b}");
|
|
buf[a as usize..b as usize].copy_from_slice(&file[a as usize..b as usize]);
|
|
loaded.insert(a, b);
|
|
}
|
|
}
|
|
done => return (done, buf, loaded),
|
|
}
|
|
}
|
|
panic!("no answer after 32 rounds");
|
|
}
|
|
|
|
/// What an import learns from a file: the raw description and the metadata.
|
|
fn describe(bytes: &[u8]) -> String {
|
|
let e = lightcraft_meta::embedded(bytes);
|
|
format!("{:?}\n{:?}\n{:?}", probe_info(bytes), lightcraft_meta::extract(bytes), e.xmp)
|
|
}
|
|
|
|
#[test]
|
|
fn partial_reads_describe_raws_as_whole_files_do() {
|
|
let dir = corpus_root().join("raw");
|
|
let mut paths: Vec<PathBuf> = std::fs::read_dir(corpus_root().join("leica"))
|
|
.into_iter()
|
|
.chain(std::fs::read_dir(&dir))
|
|
.flatten()
|
|
.flatten()
|
|
.map(|e| e.path())
|
|
.filter(|p| p.is_file() && !p.extension().is_some_and(|e| e == "part" || e == "txt" || e == "md"))
|
|
.collect();
|
|
if paths.is_empty() {
|
|
lightcraft_testkit::skip!(Corpus, "{} absent", dir.display());
|
|
return;
|
|
}
|
|
paths.sort();
|
|
// format → (files ready, bytes read, bytes total, files read whole)
|
|
let mut stats: BTreeMap<String, (usize, u64, u64, usize)> = BTreeMap::new();
|
|
for p in paths {
|
|
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
|
let file = std::fs::read(&p).unwrap();
|
|
let format = probe(&file).map_or("?".to_string(), |f| format!("{f:?}"));
|
|
let (plan, zeros, loaded) = run(&file, 0, false);
|
|
let entry = stats.entry(format.clone()).or_default();
|
|
match plan {
|
|
HeaderPlan::Ready => {
|
|
let (plan_ff, ones, loaded_ff) = run(&file, 0xff, false);
|
|
assert_eq!((plan_ff, &loaded_ff), (HeaderPlan::Ready, &loaded), "{name}: the plan read skipped bytes");
|
|
let whole = describe(&file);
|
|
assert_eq!(describe(&zeros), whole, "{name}: differs from the whole file (skipped bytes 0)");
|
|
assert_eq!(describe(&ones), whole, "{name}: differs from the whole file (skipped bytes 0xFF)");
|
|
entry.0 += 1;
|
|
entry.1 += loaded.total();
|
|
entry.2 += file.len() as u64;
|
|
// with the preview (for a raw shown from it), the preview is what the whole file gives too
|
|
let preview = embedded_preview(&file);
|
|
for fill in [0, 0xff] {
|
|
let (plan, buf, _) = run(&file, fill, true);
|
|
assert_eq!(plan, HeaderPlan::Ready, "{name}: preview plan");
|
|
assert_eq!(describe(&buf), whole, "{name}: preview plan, skipped bytes {fill}");
|
|
assert!(embedded_preview(&buf) == preview, "{name}: a different embedded preview (skipped bytes {fill})");
|
|
}
|
|
if loaded.total() * 4 > file.len() as u64 {
|
|
eprintln!("{name}: read {:.0} %", loaded.total() as f64 * 100.0 / file.len() as f64);
|
|
}
|
|
if matches!(format.as_str(), "Cr3" | "Dng") && file.len() > 8 << 20 {
|
|
assert!(loaded.total() * 4 < file.len() as u64, "{name}: read {} of {} bytes", loaded.total(), file.len());
|
|
}
|
|
}
|
|
HeaderPlan::Whole(why) => {
|
|
entry.3 += 1;
|
|
eprintln!("{name}: whole ({why})");
|
|
}
|
|
HeaderPlan::Need(_) => unreachable!(),
|
|
}
|
|
}
|
|
for (format, (ready, read, total, whole)) in stats {
|
|
eprintln!("{format:>10}: {ready} described from {:.1} % of their bytes, {whole} read whole", read as f64 * 100.0 / total.max(1) as f64);
|
|
}
|
|
}
|