From c37d5e082d302f19ae943d8cf1d4390a02842f15 Mon Sep 17 00:00:00 2001 From: terbin Date: Wed, 26 Aug 2026 15:36:56 +0200 Subject: [PATCH 1/3] test: replace silent corpus skips with an enforced fail-loud harness Tests keyed on a sample corpus that CI never has returned early and still reported success, so the suite looked green while asserting nothing. Move the corpus resolver into a shared support crate that fails loudly when a run declares the data required, mark the corpus-only tests ignored so they read as skipped rather than passed, and add decoder property tests that need no corpus. Enforce it: run under nextest, publish coverage without gating on a number, fail the build if a silent skip reappears, and require a green suite before release. Set the minimum Rust to 1.89, the true floor for the current sources and dependencies. --- .config/nextest.toml | 5 + .github/workflows/ci.yml | 37 ++++- .github/workflows/release.yml | 15 ++ Cargo.lock | 156 ++++++++++++++++++ Cargo.toml | 3 +- crates/test-support/Cargo.toml | 9 + crates/test-support/src/corpus.rs | 58 +++++++ crates/test-support/src/lib.rs | 4 + crates/test-support/src/synth.rs | 40 +++++ crates/test-support/src/trees.rs | 26 +++ crates/xaero-core/Cargo.toml | 2 + crates/xaero-core/tests/corpus.rs | 21 +-- crates/xaero-core/tests/legacy.rs | 6 +- crates/xaero-core/tests/parsers.rs | 21 +-- crates/xaero-core/tests/proptest_roundtrip.rs | 18 ++ crates/xaero-db/Cargo.toml | 3 + crates/xaero-db/src/drawing.rs | 5 +- crates/xaero-db/src/lib.rs | 15 +- crates/xaero-merge/Cargo.toml | 1 + crates/xaero-merge/tests/fixture.rs | 17 +- crates/xaero-scan/Cargo.toml | 3 + crates/xaero-scan/src/lib.rs | 21 +-- crates/xaerotools-cli/Cargo.toml | 3 + crates/xaerotools-cli/tests/cli.rs | 40 ++--- crates/xaerotools-server/Cargo.toml | 3 + crates/xaerotools-server/src/ingest.rs | 15 +- 26 files changed, 423 insertions(+), 124 deletions(-) create mode 100644 .config/nextest.toml create mode 100644 crates/test-support/Cargo.toml create mode 100644 crates/test-support/src/corpus.rs create mode 100644 crates/test-support/src/lib.rs create mode 100644 crates/test-support/src/synth.rs create mode 100644 crates/test-support/src/trees.rs create mode 100644 crates/xaero-core/tests/proptest_roundtrip.rs diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..816356a --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,5 @@ +[profile.default] +# Keep running the rest of the suite after a failure, so one broken test +# doesn't hide the results of every test that would have run after it. +# (Process-per-test isolation is nextest's own default, not set here.) +fail-fast = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16a6971..8ad6512 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo fmt --all --check - run: cargo clippy --workspace -- -D warnings - # Corpus-driven tests skip gracefully when the sample data is absent. - - run: cargo test --workspace + # Corpus/archive tests are #[ignore]d and need real data; run with --ignored locally. + - uses: taiki-e/install-action@nextest + - run: cargo nextest run --workspace + - run: cargo test --workspace --doc wasm-core: runs-on: ubuntu-latest @@ -52,3 +54,34 @@ jobs: working-directory: webui - run: npm run build working-directory: webui + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + # Instrumented builds use different RUSTFLAGS, so they get their own cache key. + - uses: Swatinem/rust-cache@v2 + with: + key: llvm-cov + - uses: taiki-e/install-action@cargo-llvm-cov + - uses: taiki-e/install-action@nextest + - run: cargo llvm-cov nextest --workspace --lcov --output-path lcov.info + - run: cargo llvm-cov report --summary-only >> "$GITHUB_STEP_SUMMARY" + - uses: actions/upload-artifact@v4 + with: + name: coverage-lcov + path: lcov.info + + no-silent-skips: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Fail if a test silently returns on missing corpus + run: | + if grep -rEn 'corpus_root\(\)\s*else\s*\{|(std::)?env::var\("XAERO_[A-Z_]*CORPUS"\)\s*else\s*\{' crates; then + echo "::error::Found a silent corpus/env else-skip. Use #[ignore] + .expect(), not a silent return." + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4bbf410..42dcc02 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,22 @@ permissions: attestations: write jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@nextest + - run: cargo fmt --all --check + - run: cargo clippy --workspace -- -D warnings + - run: cargo nextest run --workspace + - run: cargo test --workspace --doc + build: + needs: test strategy: fail-fast: false matrix: diff --git a/Cargo.lock b/Cargo.lock index 153456c..8e7899d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "axum" version = "0.8.9" @@ -87,6 +93,21 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.13.1" @@ -233,6 +254,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "fdeflate" version = "0.3.7" @@ -269,6 +296,12 @@ dependencies = [ "zlib-rs", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -596,6 +629,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "lock_api" version = "0.4.14" @@ -687,6 +726,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -765,6 +813,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.47" @@ -815,6 +888,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.12.0" @@ -844,6 +926,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "ring" version = "0.17.14" @@ -872,6 +960,19 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.43" @@ -907,6 +1008,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -1089,6 +1202,27 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "test-support" +version = "0.4.0" +dependencies = [ + "tempfile", + "xaero-core", +] + [[package]] name = "thiserror" version = "2.0.20" @@ -1225,6 +1359,12 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1284,6 +1424,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -1510,8 +1659,10 @@ dependencies = [ name = "xaero-core" version = "0.4.0" dependencies = [ + "proptest", "rayon", "smallvec", + "test-support", "thiserror", "walkdir", "zip", @@ -1524,6 +1675,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "test-support", "xaero-core", ] @@ -1534,6 +1686,7 @@ dependencies = [ "filetime", "rayon", "serde", + "test-support", "xaero-core", "xaero-db", "xaero-scan", @@ -1543,6 +1696,7 @@ dependencies = [ name = "xaero-scan" version = "0.4.0" dependencies = [ + "test-support", "xaero-core", ] @@ -1553,6 +1707,7 @@ dependencies = [ "png", "rayon", "serde_json", + "test-support", "tokio", "xaero-core", "xaero-db", @@ -1576,6 +1731,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "test-support", "tokio", "xaero-core", "xaero-db", diff --git a/Cargo.toml b/Cargo.toml index 91a5540..05e3388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [workspace] resolver = "2" -members = ["crates/xaero-core", "crates/xaero-db", "crates/xaero-merge", "crates/xaero-scan", "crates/xaerotools-server", "crates/xaerotools-cli", "tools/xaero-colorgen"] +members = ["crates/xaero-core", "crates/xaero-db", "crates/xaero-merge", "crates/xaero-scan", "crates/xaerotools-server", "crates/xaerotools-cli", "tools/xaero-colorgen", "crates/test-support"] [workspace.package] version = "0.4.0" edition = "2021" +rust-version = "1.89" license = "MIT" authors = ["XaeroTools contributors"] repository = "https://github.com/dekrom/xaerotools" diff --git a/crates/test-support/Cargo.toml b/crates/test-support/Cargo.toml new file mode 100644 index 0000000..67fe734 --- /dev/null +++ b/crates/test-support/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "test-support" +version.workspace = true +edition = "2021" +publish = false + +[dependencies] +xaero-core = { path = "../xaero-core" } +tempfile = "3" diff --git a/crates/test-support/src/corpus.rs b/crates/test-support/src/corpus.rs new file mode 100644 index 0000000..719b182 --- /dev/null +++ b/crates/test-support/src/corpus.rs @@ -0,0 +1,58 @@ +use std::path::{Path, PathBuf}; + +/// Pure core: decides the corpus path from inputs. `required` forces a hard +/// error (not a skip) when data is missing (set via XAERO_CORPUS_REQUIRED). +pub fn resolve_corpus( + env_value: Option, + required: bool, + directory_exists: impl Fn(&Path) -> bool, +) -> Result, String> { + if let Some(p) = env_value { + let p = PathBuf::from(p); + if directory_exists(&p) { + return Ok(Some(p)); + } + if required { + return Err(format!( + "XAERO_CORPUS points at a missing directory: {}", + p.display() + )); + } + return Ok(None); + } + if required { + return Err("XAERO_CORPUS_REQUIRED=1 but XAERO_CORPUS is unset".into()); + } + Ok(None) +} + +/// Test-facing resolver. Reads real env. Panics loudly in required mode so a +/// misconfigured corpus job fails instead of silently passing. +pub fn corpus_root() -> Option { + let required = std::env::var("XAERO_CORPUS_REQUIRED").ok().as_deref() == Some("1"); + match resolve_corpus(std::env::var("XAERO_CORPUS").ok(), required, |p| p.is_dir()) { + Ok(v) => v, + Err(e) => panic!("{e}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn required_but_absent_is_an_error() { + let r = resolve_corpus(None, true, |_| false); + assert!(r.is_err(), "required + no data must fail, not skip"); + } + #[test] + fn absent_and_not_required_is_none() { + assert_eq!(resolve_corpus(None, false, |_| false).unwrap(), None); + } + #[test] + fn explicit_env_dir_is_used() { + let r = resolve_corpus(Some("/x".into()), false, |p| { + p == std::path::Path::new("/x") + }); + assert_eq!(r.unwrap(), Some(PathBuf::from("/x"))); + } +} diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs new file mode 100644 index 0000000..9a48fd0 --- /dev/null +++ b/crates/test-support/src/lib.rs @@ -0,0 +1,4 @@ +pub mod corpus; +pub mod synth; +pub mod trees; +pub use corpus::{corpus_root, resolve_corpus}; diff --git a/crates/test-support/src/synth.rs b/crates/test-support/src/synth.rs new file mode 100644 index 0000000..979ee44 --- /dev/null +++ b/crates/test-support/src/synth.rs @@ -0,0 +1,40 @@ +use xaero_core::{encode_region, DecodedRegion}; + +/// Re-encode a decoded region to the canonical 7.8 stream. +pub fn reencode(region: &DecodedRegion) -> Vec { + encode_region(region) +} + +/// First `keep` bytes of `bytes`, an error-injection or seed input. +pub fn truncate_prefix(bytes: &[u8], keep: usize) -> Vec { + bytes[..keep.min(bytes.len())].to_vec() +} + +#[cfg(test)] +mod tests { + use super::*; + + // The committed legacy fixture the core crate already ships. + const FIXTURE: &[u8] = + include_bytes!("../../xaero-core/tests/fixtures/legacy/v0.7_-10000_-1.zip"); + + #[test] + fn reencode_of_a_decoded_region_decodes_again() { + let stream = xaero_core::read_region_container(FIXTURE).expect("unzip"); + let region = xaero_core::decode_region(&stream).expect("decode"); + let bytes = reencode(®ion); + assert!( + xaero_core::decode_region(&bytes).is_ok(), + "re-encoded region must decode" + ); + } + + #[test] + fn truncate_prefix_never_panics_on_decode() { + let stream = xaero_core::read_region_container(FIXTURE).expect("unzip"); + for keep in [0usize, 1, 8, stream.len() / 2] { + let t = truncate_prefix(&stream, keep); + let _ = xaero_core::decode_region(&t); // must not panic (Ok or Err both fine) + } + } +} diff --git a/crates/test-support/src/trees.rs b/crates/test-support/src/trees.rs new file mode 100644 index 0000000..76688d9 --- /dev/null +++ b/crates/test-support/src/trees.rs @@ -0,0 +1,26 @@ +use std::fs; +use tempfile::TempDir; + +/// Build a minimal world-map tree. Each region = (world_id, rx, rz, bytes), +/// placed at world-map//null/0/_.zip (dim "null", mw "0"). +pub fn world_map_tree(regions: &[(&str, i32, i32, &[u8])]) -> TempDir { + let directory = TempDir::new().expect("tempdir"); + for (world, rx, rz, bytes) in regions { + let region_directory = directory.path().join(format!("world-map/{world}/null/0")); + fs::create_dir_all(®ion_directory).unwrap(); + fs::write(region_directory.join(format!("{rx}_{rz}.zip")), bytes).unwrap(); + } + directory +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn builds_the_expected_layout() { + let directory = world_map_tree(&[("srv", -10, -1, b"zzz")]); + let p = directory.path().join("world-map/srv/null/0/-10_-1.zip"); + assert!(p.is_file(), "expected region file at {}", p.display()); + assert_eq!(std::fs::read(p).unwrap(), b"zzz"); + } +} diff --git a/crates/xaero-core/Cargo.toml b/crates/xaero-core/Cargo.toml index 17c9f41..e395cbd 100644 --- a/crates/xaero-core/Cargo.toml +++ b/crates/xaero-core/Cargo.toml @@ -11,3 +11,5 @@ zip = { version = "8.6.0", default-features = false, features = ["deflate"] } [dev-dependencies] rayon = "1.12.0" walkdir = "2.5.0" +test-support = { path = "../test-support" } +proptest = "1" diff --git a/crates/xaero-core/tests/corpus.rs b/crates/xaero-core/tests/corpus.rs index b3c50c6..f7f31d1 100644 --- a/crates/xaero-core/tests/corpus.rs +++ b/crates/xaero-core/tests/corpus.rs @@ -16,16 +16,6 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use rayon::prelude::*; use xaero_core::{decode_region, encode_region, read_region_container}; -fn corpus_root() -> Option { - if let Ok(p) = std::env::var("XAERO_CORPUS") { - let p = PathBuf::from(p); - return p.is_dir().then_some(p); - } - // repo layout: /xaerotools/crates/xaero-core ; corpus at /sample data - let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../sample data"); - fallback.is_dir().then(|| fallback.canonicalize().unwrap()) -} - fn is_region_file(path: &Path) -> bool { if path.extension().and_then(|e| e.to_str()) != Some("zip") { return false; @@ -48,11 +38,9 @@ fn in_cache_dir(path: &Path) -> bool { } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn corpus_round_trip() { - let Some(root) = corpus_root() else { - eprintln!("corpus not found; set XAERO_CORPUS to run this test"); - return; - }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let mut files: Vec = walkdir::WalkDir::new(&root) .into_iter() .filter_map(|e| e.ok()) @@ -160,10 +148,9 @@ fn corpus_round_trip() { } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn truncation_never_panics() { - let Some(root) = corpus_root() else { - return; - }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); // One small region per major version. let picks = [ "xaero1.21.4/world-map/Multiplayer_2b2t/DIM-1/mw$default/0_-24.zip", diff --git a/crates/xaero-core/tests/legacy.rs b/crates/xaero-core/tests/legacy.rs index cb24d96..c75ffe5 100644 --- a/crates/xaero-core/tests/legacy.rs +++ b/crates/xaero-core/tests/legacy.rs @@ -167,11 +167,9 @@ fn legacy_regions_re_encode_to_modern_and_survive_a_round_trip() { /// Opt-in sweep over a private archive: every region must decode to exact EOF. #[test] +#[ignore = "requires XAERO_LEGACY_CORPUS"] fn optional_corpus_decodes_to_exact_eof() { - let Ok(dir) = std::env::var("XAERO_LEGACY_CORPUS") else { - eprintln!("XAERO_LEGACY_CORPUS unset — skipping"); - return; - }; + let dir = std::env::var("XAERO_LEGACY_CORPUS").expect("XAERO_LEGACY_CORPUS"); let mut checked = 0usize; let mut exact = 0usize; let mut by_version: std::collections::BTreeMap = Default::default(); diff --git a/crates/xaero-core/tests/parsers.rs b/crates/xaero-core/tests/parsers.rs index fd9c020..aa6b0a0 100644 --- a/crates/xaero-core/tests/parsers.rs +++ b/crates/xaero-core/tests/parsers.rs @@ -1,24 +1,12 @@ //! Parser checks against the real sample corpus (skipped when absent). -use std::path::{Path, PathBuf}; - use xaero_core::dimconfig::{parse_dimension_config, parse_minimap_config}; use xaero_core::waypoints::parse_waypoints_file; -fn corpus_root() -> Option { - if let Ok(p) = std::env::var("XAERO_CORPUS") { - let p = PathBuf::from(p); - return p.is_dir().then_some(p); - } - let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../sample data"); - fallback.is_dir().then(|| fallback.canonicalize().unwrap()) -} - #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn parses_all_sample_waypoint_files() { - let Some(root) = corpus_root() else { - return; - }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let mut files = 0; let mut total = 0; for entry in walkdir::WalkDir::new(&root) @@ -54,10 +42,9 @@ fn parses_all_sample_waypoint_files() { } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn parses_all_sample_configs() { - let Some(root) = corpus_root() else { - return; - }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let mut dimcfgs = 0; let mut mmcfgs = 0; for entry in walkdir::WalkDir::new(&root) diff --git a/crates/xaero-core/tests/proptest_roundtrip.rs b/crates/xaero-core/tests/proptest_roundtrip.rs new file mode 100644 index 0000000..1d75a53 --- /dev/null +++ b/crates/xaero-core/tests/proptest_roundtrip.rs @@ -0,0 +1,18 @@ +use proptest::prelude::*; +use xaero_core::{decode_region, encode_region}; + +proptest! { + #[test] + fn decode_never_panics(bytes in proptest::collection::vec(any::(), 0..4096)) { + let _ = decode_region(&bytes); + } + + #[test] + fn encode_is_a_fixed_point(bytes in proptest::collection::vec(any::(), 0..4096)) { + if let Ok(region) = decode_region(&bytes) { + let once = encode_region(®ion); + let twice = encode_region(&decode_region(&once).expect("re-decode canonical")); + prop_assert_eq!(once, twice); + } + } +} diff --git a/crates/xaero-db/Cargo.toml b/crates/xaero-db/Cargo.toml index 51b39cd..2d572ec 100644 --- a/crates/xaero-db/Cargo.toml +++ b/crates/xaero-db/Cargo.toml @@ -9,3 +9,6 @@ rusqlite = { version = "0.37", features = ["bundled", "hooks"] } serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" xaero-core = { path = "../xaero-core" } + +[dev-dependencies] +test-support = { path = "../test-support" } diff --git a/crates/xaero-db/src/drawing.rs b/crates/xaero-db/src/drawing.rs index c683d77..66bc49d 100644 --- a/crates/xaero-db/src/drawing.rs +++ b/crates/xaero-db/src/drawing.rs @@ -685,10 +685,9 @@ mod tests { } #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn reads_corpus_drawing_dbs() { - let Some(root) = crate::tests::corpus_root() else { - return; - }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let mut checked = 0; for version_dir in ["xaero1.21.4", "xaero1.21.8"] { let wm = root.join(version_dir).join("world-map"); diff --git a/crates/xaero-db/src/lib.rs b/crates/xaero-db/src/lib.rs index 937095a..0ab5e74 100644 --- a/crates/xaero-db/src/lib.rs +++ b/crates/xaero-db/src/lib.rs @@ -651,18 +651,10 @@ mod tests { use super::*; use std::path::PathBuf; - pub(crate) fn corpus_root() -> Option { - if let Ok(p) = std::env::var("XAERO_CORPUS") { - let p = PathBuf::from(p); - return p.is_dir().then_some(p); - } - let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../sample data"); - fallback.is_dir().then(|| fallback.canonicalize().unwrap()) - } - #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn reads_sample_newchunks() { - let Some(root) = corpus_root() else { return }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let db = open_readonly( &root.join("xaero1.21.8/world-map/Multiplayer_2b2t/XaeroPlusNewChunks.db"), ) @@ -679,8 +671,9 @@ mod tests { } #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn reads_all_sample_dbs() { - let Some(root) = corpus_root() else { return }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let mut checked = 0; for version_dir in ["xaero1.21.4", "xaero1.21.8"] { let wm = root.join(version_dir).join("world-map"); diff --git a/crates/xaero-merge/Cargo.toml b/crates/xaero-merge/Cargo.toml index efdfefb..f8fe966 100644 --- a/crates/xaero-merge/Cargo.toml +++ b/crates/xaero-merge/Cargo.toml @@ -14,3 +14,4 @@ serde = { version = "1", features = ["derive"] } [dev-dependencies] xaero-core = { path = "../xaero-core" } +test-support = { path = "../test-support" } diff --git a/crates/xaero-merge/tests/fixture.rs b/crates/xaero-merge/tests/fixture.rs index 960407a..341f54a 100644 --- a/crates/xaero-merge/tests/fixture.rs +++ b/crates/xaero-merge/tests/fixture.rs @@ -3,19 +3,8 @@ //! overlap — null 307 vs 90 regions (20 conflicts), DIM-1 296 vs 794 (71), //! DIM1 0 vs 4. Expected merged totals: null 377, DIM-1 1019, DIM1 4. -use std::path::{Path, PathBuf}; - use xaero_merge::{merge_to_output, MergeOptions}; -fn corpus_root() -> Option { - if let Ok(p) = std::env::var("XAERO_CORPUS") { - let p = PathBuf::from(p); - return p.is_dir().then_some(p); - } - let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../sample data"); - fallback.is_dir().then(|| fallback.canonicalize().unwrap()) -} - fn unit<'a>( report: &'a xaero_merge::MergeReport, dim: &str, @@ -29,11 +18,9 @@ fn unit<'a>( } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn merges_the_2b2t_fixture() { - let Some(root) = corpus_root() else { - eprintln!("corpus not found; skipping"); - return; - }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let a = root.join("xaero1.21.4"); let b = root.join("xaero1.21.8"); let out = std::env::temp_dir().join(format!("xt-merge-fixture-{}", std::process::id())); diff --git a/crates/xaero-scan/Cargo.toml b/crates/xaero-scan/Cargo.toml index 9986519..a72aa50 100644 --- a/crates/xaero-scan/Cargo.toml +++ b/crates/xaero-scan/Cargo.toml @@ -6,3 +6,6 @@ license.workspace = true [dependencies] xaero-core = { path = "../xaero-core" } + +[dev-dependencies] +test-support = { path = "../test-support" } diff --git a/crates/xaero-scan/src/lib.rs b/crates/xaero-scan/src/lib.rs index bcef769..7a7cd44 100644 --- a/crates/xaero-scan/src/lib.rs +++ b/crates/xaero-scan/src/lib.rs @@ -749,18 +749,10 @@ fn scan_minimap_world( mod tests { use super::*; - fn corpus_root() -> Option { - if let Ok(p) = std::env::var("XAERO_CORPUS") { - let p = PathBuf::from(p); - return p.is_dir().then_some(p); - } - let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../sample data"); - fallback.is_dir().then(|| fallback.canonicalize().unwrap()) - } - #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn discovers_sample_worlds() { - let Some(root) = corpus_root() else { return }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let worlds = discover_root(&root.join("xaero1.21.8")); assert!(worlds.iter().any(|w| w.id == "Multiplayer_2b2t")); let w2 = worlds.iter().find(|w| w.id == "Multiplayer_2b2t").unwrap(); @@ -789,8 +781,9 @@ mod tests { } #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn discovers_minimap_only_worlds() { - let Some(root) = corpus_root() else { return }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let worlds = discover_root(&root.join("xaero1.21.4")); // Worlds the player only ever had the minimap for: waypoints, no map. let pvp = worlds @@ -824,8 +817,9 @@ mod tests { } #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn scans_every_waypoint_file_layout() { - let Some(root) = corpus_root() else { return }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let files = scan_waypoint_files(&root.join("xaero1.21.4")); assert!(files.iter().all(|f| f.kind == WaypointSourceKind::Live)); // Every mw$*_.txt under a dim% folder of the corpus. @@ -936,8 +930,9 @@ mod tests { } #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn indexes_sample_regions() { - let Some(root) = corpus_root() else { return }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); let dir = root.join("xaero1.21.8/world-map/Multiplayer_2b2t/DIM-1/mw$default"); let idx = index_regions(&dir).unwrap(); assert_eq!(idx.entries.len(), 794); diff --git a/crates/xaerotools-cli/Cargo.toml b/crates/xaerotools-cli/Cargo.toml index 896d12a..b102336 100644 --- a/crates/xaerotools-cli/Cargo.toml +++ b/crates/xaerotools-cli/Cargo.toml @@ -13,3 +13,6 @@ xaero-db = { path = "../xaero-db" } xaero-merge = { path = "../xaero-merge" } xaero-scan = { path = "../xaero-scan" } xaerotools-server = { path = "../xaerotools-server" } + +[dev-dependencies] +test-support = { path = "../test-support" } diff --git a/crates/xaerotools-cli/tests/cli.rs b/crates/xaerotools-cli/tests/cli.rs index 0ac43fb..9023c54 100644 --- a/crates/xaerotools-cli/tests/cli.rs +++ b/crates/xaerotools-cli/tests/cli.rs @@ -11,19 +11,11 @@ use std::path::{Path, PathBuf}; use std::process::Command; -fn corpus_root() -> Option { - if let Ok(p) = std::env::var("XAERO_CORPUS") { - let p = PathBuf::from(p); - return p.is_dir().then_some(p); - } - let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../sample data"); - fallback.is_dir().then(|| fallback.canonicalize().unwrap()) -} - /// The 1.21.8 half of the corpus: one world with a 4-region End dimension. -fn corpus_root_1218() -> Option { - let p = corpus_root()?.join("xaero1.21.8"); - p.is_dir().then_some(p) +fn corpus_root_1218() -> PathBuf { + test_support::corpus_root() + .expect("XAERO_CORPUS") + .join("xaero1.21.8") } fn scratch(name: &str) -> PathBuf { @@ -51,11 +43,9 @@ fn read_png(path: &Path) -> (u32, u32, Vec) { } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn render_stitch_matches_render_region() { - let Some(root) = corpus_root_1218() else { - eprintln!("skipping: sample corpus not found"); - return; - }; + let root = corpus_root_1218(); let dir = scratch("render"); let stitched = dir.join("end.png"); let single = dir.join("region.png"); @@ -100,11 +90,9 @@ fn render_stitch_matches_render_region() { } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn render_refuses_an_oversized_box() { - let Some(root) = corpus_root_1218() else { - eprintln!("skipping: sample corpus not found"); - return; - }; + let root = corpus_root_1218(); let dir = scratch("cap"); let out = xt(&[ "render", @@ -167,11 +155,9 @@ fn render_leaves_a_hole_for_a_region_it_cannot_decode() { } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn stats_json_describes_the_corpus() { - let Some(root) = corpus_root_1218() else { - eprintln!("skipping: sample corpus not found"); - return; - }; + let root = corpus_root_1218(); let out = xt(&[ "stats", "--root", @@ -221,11 +207,9 @@ fn stats_json_describes_the_corpus() { } #[test] +#[ignore = "requires corpus (XAERO_CORPUS)"] fn doctor_is_quiet_on_a_healthy_corpus() { - let Some(root) = corpus_root_1218() else { - eprintln!("skipping: sample corpus not found"); - return; - }; + let root = corpus_root_1218(); let out = xt(&[ "doctor", "--root", diff --git a/crates/xaerotools-server/Cargo.toml b/crates/xaerotools-server/Cargo.toml index 183fc02..88549bc 100644 --- a/crates/xaerotools-server/Cargo.toml +++ b/crates/xaerotools-server/Cargo.toml @@ -21,3 +21,6 @@ xaero-merge = { path = "../xaero-merge" } xaero-db = { path = "../xaero-db" } getrandom = "0.4.3" rusqlite = { version = "0.37", features = ["bundled"] } + +[dev-dependencies] +test-support = { path = "../test-support" } diff --git a/crates/xaerotools-server/src/ingest.rs b/crates/xaerotools-server/src/ingest.rs index 593580e..5eef81e 100644 --- a/crates/xaerotools-server/src/ingest.rs +++ b/crates/xaerotools-server/src/ingest.rs @@ -428,21 +428,10 @@ mod tests { ); } - fn corpus_root() -> Option { - if let Ok(p) = std::env::var("XAERO_CORPUS") { - let p = PathBuf::from(p); - return p.is_dir().then_some(p); - } - let fallback = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../sample data"); - fallback.is_dir().then(|| fallback.canonicalize().unwrap()) - } - #[test] + #[ignore = "requires corpus (XAERO_CORPUS)"] fn stores_backup_and_merges_shared_from_corpus() { - let Some(root) = corpus_root() else { - eprintln!("corpus not found; skipping"); - return; - }; + let root = test_support::corpus_root().expect("XAERO_CORPUS"); // Two real copies of the same coordinate: major-6 (1.21.4) and // major-7 (1.21.8) — a genuine cross-version merge. let a = std::fs::read( From d2527479770da972a84a76ca28516422a3d2fade Mon Sep 17 00:00:00 2001 From: terbin Date: Fri, 28 Aug 2026 10:54:30 +0200 Subject: [PATCH 2/3] test: route the legacy corpus variable through the shared resolver `optional_corpus_decodes_to_exact_eof` read XAERO_LEGACY_CORPUS with a bare env::var().expect(), so it was the one corpus test that did not go through resolve_corpus. Give resolve_corpus the variable name, add legacy_corpus_root() beside corpus_root(), and point the legacy sweep at it, so required mode names the variable the caller actually reads instead of always naming XAERO_CORPUS. The sweep still stops rather than skipping. It reads a private archive that no CI runner can reach, so nothing can ever declare it required, and a skip there would report a pass while asserting nothing, which is the shape this branch exists to remove. The panic now names the flag that excludes this one test, so running the ignored set against the public corpus does not trip over the private one. Also correct four module docs and the developer setup guide, which all still described the pre-#[ignore] silent-skip behaviour, and ignore lcov.info so the documented coverage command leaves no untracked file behind. --- .gitignore | 3 ++ crates/test-support/src/corpus.rs | 49 +++++++++++++++++++++++------- crates/test-support/src/lib.rs | 2 +- crates/xaero-core/tests/corpus.rs | 5 +-- crates/xaero-core/tests/legacy.rs | 20 ++++++++++-- crates/xaero-core/tests/parsers.rs | 3 +- crates/xaerotools-cli/tests/cli.rs | 4 +-- docs/START-HERE.md | 21 +++++++++---- 8 files changed, 81 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index f7a3ebc..8c0c1af 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ vault.db pyramid.db *.db-shm *.db-wal + +# Coverage output (cargo llvm-cov) +lcov.info diff --git a/crates/test-support/src/corpus.rs b/crates/test-support/src/corpus.rs index 719b182..6d424ed 100644 --- a/crates/test-support/src/corpus.rs +++ b/crates/test-support/src/corpus.rs @@ -1,8 +1,11 @@ use std::path::{Path, PathBuf}; /// Pure core: decides the corpus path from inputs. `required` forces a hard -/// error (not a skip) when data is missing (set via XAERO_CORPUS_REQUIRED). +/// error (not a skip) when data is missing. `var` names the environment +/// variable the caller reads, so the message points at that variable and not +/// at whichever one this helper happened to be written for first. pub fn resolve_corpus( + var: &str, env_value: Option, required: bool, directory_exists: impl Fn(&Path) -> bool, @@ -14,45 +17,69 @@ pub fn resolve_corpus( } if required { return Err(format!( - "XAERO_CORPUS points at a missing directory: {}", + "{var} points at a missing directory: {}", p.display() )); } return Ok(None); } if required { - return Err("XAERO_CORPUS_REQUIRED=1 but XAERO_CORPUS is unset".into()); + return Err(format!("{var}_REQUIRED=1 but {var} is unset")); } Ok(None) } -/// Test-facing resolver. Reads real env. Panics loudly in required mode so a -/// misconfigured corpus job fails instead of silently passing. -pub fn corpus_root() -> Option { - let required = std::env::var("XAERO_CORPUS_REQUIRED").ok().as_deref() == Some("1"); - match resolve_corpus(std::env::var("XAERO_CORPUS").ok(), required, |p| p.is_dir()) { +/// Reads real env for one corpus variable. Panics loudly in required mode so a +/// misconfigured corpus job fails instead of silently passing. Callers still +/// `.expect()` the `None`, so neither mode can turn into a silent skip. +fn root_for(var: &str) -> Option { + let required = std::env::var(format!("{var}_REQUIRED")).ok().as_deref() == Some("1"); + match resolve_corpus(var, std::env::var(var).ok(), required, |p| p.is_dir()) { Ok(v) => v, Err(e) => panic!("{e}"), } } +/// The public sample corpus (`XAERO_CORPUS`), shared by most corpus tests. +pub fn corpus_root() -> Option { + root_for("XAERO_CORPUS") +} + +/// The private legacy archive (`XAERO_LEGACY_CORPUS`). Same contract as +/// [`corpus_root`]; it is a separate variable because the two data sets have +/// different availability, not because they need different rules. +pub fn legacy_corpus_root() -> Option { + root_for("XAERO_LEGACY_CORPUS") +} + #[cfg(test)] mod tests { use super::*; #[test] fn required_but_absent_is_an_error() { - let r = resolve_corpus(None, true, |_| false); + let r = resolve_corpus("XAERO_CORPUS", None, true, |_| false); assert!(r.is_err(), "required + no data must fail, not skip"); } #[test] fn absent_and_not_required_is_none() { - assert_eq!(resolve_corpus(None, false, |_| false).unwrap(), None); + assert_eq!( + resolve_corpus("XAERO_CORPUS", None, false, |_| false).unwrap(), + None + ); } #[test] fn explicit_env_dir_is_used() { - let r = resolve_corpus(Some("/x".into()), false, |p| { + let r = resolve_corpus("XAERO_CORPUS", Some("/x".into()), false, |p| { p == std::path::Path::new("/x") }); assert_eq!(r.unwrap(), Some(PathBuf::from("/x"))); } + #[test] + fn the_message_names_the_variable_the_caller_reads() { + let e = resolve_corpus("XAERO_LEGACY_CORPUS", None, true, |_| false).unwrap_err(); + assert!( + e.contains("XAERO_LEGACY_CORPUS_REQUIRED") && !e.contains("XAERO_CORPUS_REQUIRED=1"), + "message must name the legacy variable, got: {e}" + ); + } } diff --git a/crates/test-support/src/lib.rs b/crates/test-support/src/lib.rs index 9a48fd0..be9d5bf 100644 --- a/crates/test-support/src/lib.rs +++ b/crates/test-support/src/lib.rs @@ -1,4 +1,4 @@ pub mod corpus; pub mod synth; pub mod trees; -pub use corpus::{corpus_root, resolve_corpus}; +pub use corpus::{corpus_root, legacy_corpus_root, resolve_corpus}; diff --git a/crates/xaero-core/tests/corpus.rs b/crates/xaero-core/tests/corpus.rs index f7f31d1..88e749f 100644 --- a/crates/xaero-core/tests/corpus.rs +++ b/crates/xaero-core/tests/corpus.rs @@ -7,8 +7,9 @@ //! 7.8 share the same body layout), and survive decode(encode(x)) with //! semantic equality //! -//! The corpus lives outside the repo; the test is skipped (with a notice) -//! when it cannot be found. Override with XAERO_CORPUS=/path/to/sample-data. +//! The corpus lives outside the repo. These tests are `#[ignore]`d, so a plain +//! `cargo test` reports them as skipped and never as passed. Run them with +//! `--ignored` and XAERO_CORPUS=/path/to/sample-data. use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/crates/xaero-core/tests/legacy.rs b/crates/xaero-core/tests/legacy.rs index c75ffe5..ab18a9d 100644 --- a/crates/xaero-core/tests/legacy.rs +++ b/crates/xaero-core/tests/legacy.rs @@ -7,7 +7,9 @@ //! (0.4 and 0.7). //! //! Point `XAERO_LEGACY_CORPUS` at a directory of region zips to run the same -//! assertions across a larger private archive; the test is a no-op without it. +//! assertions across a larger private archive. That sweep is `#[ignore]`d, so +//! it never runs by accident, and it stops with a clear error rather than +//! reporting a pass when the archive is absent. use std::path::{Path, PathBuf}; @@ -169,7 +171,15 @@ fn legacy_regions_re_encode_to_modern_and_survive_a_round_trip() { #[test] #[ignore = "requires XAERO_LEGACY_CORPUS"] fn optional_corpus_decodes_to_exact_eof() { - let dir = std::env::var("XAERO_LEGACY_CORPUS").expect("XAERO_LEGACY_CORPUS"); + let dir = test_support::legacy_corpus_root().unwrap_or_else(|| { + panic!( + "XAERO_LEGACY_CORPUS is unset. This sweep reads a private archive \ + that is not part of the public sample corpus, so `--ignored` on \ + the public data alone cannot satisfy it. Either set the variable, \ + or exclude this one test with \ + `--skip optional_corpus_decodes_to_exact_eof`." + ) + }); let mut checked = 0usize; let mut exact = 0usize; let mut by_version: std::collections::BTreeMap = Default::default(); @@ -205,6 +215,10 @@ fn optional_corpus_decodes_to_exact_eof() { *by_version.entry(dr.version.to_string()).or_default() += 1; checked += 1; } - assert!(checked > 0, "corpus dir {dir} had no region zips"); + assert!( + checked > 0, + "corpus dir {} had no region zips", + dir.display() + ); eprintln!("decoded {checked} regions cleanly ({exact} byte-identical): {by_version:?}"); } diff --git a/crates/xaero-core/tests/parsers.rs b/crates/xaero-core/tests/parsers.rs index aa6b0a0..2afa2bb 100644 --- a/crates/xaero-core/tests/parsers.rs +++ b/crates/xaero-core/tests/parsers.rs @@ -1,4 +1,5 @@ -//! Parser checks against the real sample corpus (skipped when absent). +//! Parser checks against the real sample corpus. `#[ignore]`d, so they report +//! as skipped rather than as passed when XAERO_CORPUS is unset. use xaero_core::dimconfig::{parse_dimension_config, parse_minimap_config}; use xaero_core::waypoints::parse_waypoints_file; diff --git a/crates/xaerotools-cli/tests/cli.rs b/crates/xaerotools-cli/tests/cli.rs index 9023c54..4a2100b 100644 --- a/crates/xaerotools-cli/tests/cli.rs +++ b/crates/xaerotools-cli/tests/cli.rs @@ -3,8 +3,8 @@ //! `render` is asserted pixel-for-pixel against `render-region` so the stitch //! path can never silently drift from the single-region renderer, and `doctor` //! is pointed at a scratch tree of deliberately broken files. The corpus lives -//! outside the repo; corpus tests are skipped (with a notice) when it is -//! absent. Override with XAERO_CORPUS=/path/to/sample-data. +//! outside the repo; the corpus tests are `#[ignore]`d, so they report as +//! skipped and never as passed. Point XAERO_CORPUS at a copy to run them. //! //! Nothing here ever writes inside the corpus. diff --git a/docs/START-HERE.md b/docs/START-HERE.md index 762986f..1c1da82 100644 --- a/docs/START-HERE.md +++ b/docs/START-HERE.md @@ -52,12 +52,21 @@ vault backup of every account's waypoints. ## Developing -- `cargo test --workspace` — the full suite. The byte-identical codec - round-trip runs against the 2b2t sample corpus, which is not part of this - repo: point `XAERO_CORPUS` at a copy - (`XAERO_CORPUS=/path/to/sample-data cargo test --workspace`). Without it - those tests **skip rather than fail**, so a green run does not by itself - prove the corpus was read. +- `cargo test --workspace` — the full suite. The corpus-backed tests (the + byte-identical codec round-trip and friends) are `#[ignore]`d, so this run + reports them as **skipped**, never as passed. A green run therefore makes no + claim about the corpus either way. +- To actually run them, point `XAERO_CORPUS` at a copy of the 2b2t sample + corpus, which is not part of this repo, and ask for the ignored tests: + + ``` + XAERO_CORPUS=/path/to/sample-data cargo test --workspace -- --ignored \ + --skip optional_corpus_decodes_to_exact_eof + ``` + + Without `XAERO_CORPUS` they stop with an error rather than passing quietly. + The `--skip` excludes one sweep over a **private** legacy archive that is not + part of the public corpus; drop it if you have set `XAERO_LEGACY_CORPUS`. - If you change the web UI: `cd webui && npm install && npm run build`, then `cargo clean -p xaerotools-server && cargo build` (the UI is embedded at compile time). From f428e0e8ef2ecc77070e30941d8b1d49603bed81 Mon Sep 17 00:00:00 2001 From: terbin Date: Fri, 28 Aug 2026 10:54:31 +0200 Subject: [PATCH 3/3] test: fail truncation_never_panics when its corpus picks are absent The test read two hardcoded region paths and continued past either one it could not open. With both absent the loop body never ran, so the test reported a pass having asserted nothing. That is the defect this branch exists to remove: the harness closed the missing-variable case and left the missing-file case open. Found by pointing XAERO_CORPUS at an empty directory and running the ignored set. Of the 17 corpus tests, this was the only one that passed. It now names the missing file and stops. The no-silent-skips guard cannot catch this shape, because it looks for a missing environment variable rather than a missing file. Also record that the legacy sweep's test name is quoted in the developer setup guide and in its own panic message, so a rename updates both, and document the nextest form of the ignored-test command alongside the cargo test form. --- crates/xaero-core/tests/corpus.rs | 13 ++++++++++--- crates/xaero-core/tests/legacy.rs | 4 ++++ docs/START-HERE.md | 7 +++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/xaero-core/tests/corpus.rs b/crates/xaero-core/tests/corpus.rs index 88e749f..7059245 100644 --- a/crates/xaero-core/tests/corpus.rs +++ b/crates/xaero-core/tests/corpus.rs @@ -159,9 +159,16 @@ fn truncation_never_panics() { ]; for rel in picks { let path = root.join(rel); - let Ok(bytes) = std::fs::read(&path) else { - continue; - }; + // Reading these two is the whole test. Skipping a missing one leaves + // the loop body unexecuted and the test green, so name it and stop. + let bytes = std::fs::read(&path).unwrap_or_else(|e| { + panic!( + "corpus pick is missing: {} ({e}). If the corpus layout moved, \ + update the pick rather than letting this test pass without \ + decoding anything.", + path.display() + ) + }); let stream = read_region_container(&bytes).unwrap(); for cut in 0..stream.len().min(4000) { let _ = decode_region(&stream[..cut]); diff --git a/crates/xaero-core/tests/legacy.rs b/crates/xaero-core/tests/legacy.rs index ab18a9d..91f02f1 100644 --- a/crates/xaero-core/tests/legacy.rs +++ b/crates/xaero-core/tests/legacy.rs @@ -168,6 +168,10 @@ fn legacy_regions_re_encode_to_modern_and_survive_a_round_trip() { } /// Opt-in sweep over a private archive: every region must decode to exact EOF. +/// +/// The name below is quoted in `docs/START-HERE.md` and in the panic message, +/// because running the ignored set against the public corpus alone means +/// excluding this one by name. Renaming it means updating both. #[test] #[ignore = "requires XAERO_LEGACY_CORPUS"] fn optional_corpus_decodes_to_exact_eof() { diff --git a/docs/START-HERE.md b/docs/START-HERE.md index 1c1da82..b7395d3 100644 --- a/docs/START-HERE.md +++ b/docs/START-HERE.md @@ -64,6 +64,13 @@ vault backup of every account's waypoints. --skip optional_corpus_decodes_to_exact_eof ``` + Or under nextest, which is what CI runs: + + ``` + XAERO_CORPUS=/path/to/sample-data cargo nextest run --workspace \ + --run-ignored ignored-only -- --skip optional_corpus_decodes_to_exact_eof + ``` + Without `XAERO_CORPUS` they stop with an error rather than passing quietly. The `--skip` excludes one sweep over a **private** legacy archive that is not part of the public corpus; drop it if you have set `XAERO_LEGACY_CORPUS`.