Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

xtask/tests/pack_pipeline.rs

//! **The pack-pipeline source guard, seen to go red.**
//!
//! Its first subject does not exist on the serve path yet (pack-copy has not
//! landed), and a guard whose subject is absent is the most dangerous state a
//! guard can be in, because "found no violations" and "cannot find anything"
//! print the same thing. So it is asserted in both directions, permanently:
//!
//! * **red**, against source samples that call `iter_from_counts` with no
//!   `thread_limit`, with `thread_limit: None`, with a pool-sized limit and no
//!   declaration, and against a `FromEntriesIter::new` with nothing said about
//!   its trailer.
//! * **green**, against the two declared forms, and against gunnar's own
//!   shipped sources, with the file count asserted so an empty scan cannot
//!   masquerade as a clean one.
//!
//! Every red also asserts **which** rule fired. A guard that answered
//! "violation" to everything would satisfy a bare `assert!(!findings
//! .is_empty())` and prove nothing: the identity-value trap.

use xtask::pack_pipeline::{
    check_impls, check_source, object_store_sources, scan_object_stores, scan_workspace,
    shipped_sources, Rule, NOT_FORWARDED_MARKER, OPTIONAL_BUT_LOAD_BEARING,
};

/// The list rules 3 and 4 are PROVEN against, which is deliberately not the live
/// [`OPTIONAL_BUT_LOAD_BEARING`].
///
/// The live list is empty, and correctly so: `GitOps` has no defaulted method,
/// so the class of defect is unrepresentable on the contract. But when it was
/// emptied on 2026-08-10 these four proofs were left asserting findings the rule
/// could no longer produce, and **six of the fifteen tests in this file were
/// RED** until 2026-08-10. A guard against silent-green had gone silently red.
///
/// So the proofs drive [`check_impls`] with names of their own. What they hold
/// is the MECHANISM — omission caught, `None`-from-a-wrapper caught, leaf
/// backend spared, one marker excusing exactly one method — and none of that
/// should depend on which names happen to be load-bearing this week.
/// [`the_live_list_is_empty_and_that_is_a_declaration`] is what holds the other
/// half: that the emptiness is a decision somebody wrote down.
const PROVING_LIST: &[&str] = &["fn packed(", "fn bitmap("];

/// The shape the pack-copy fix produces if the second half of the rule is missed.
const UNLIMITED: &str = r#"
fn upload_pack(counts: Vec<Count>) -> Result<()> {
    let entries = output::entry::iter_from_counts(
        counts,
        db,
        progress,
        output::entry::iter_from_counts::Options {
            mode: Mode::PackCopyAndBaseObjects,
            chunk_size: 1000,
            version: Version::V2,
            ..Default::default()
        },
    );
    Ok(())
}
"#;

/// The field present and defaulted: the case a guard that only looked for the
/// identifier `thread_limit` would wave through.
const EXPLICIT_NONE: &str = r#"
    let entries = iter_from_counts(counts, db, progress, Options {
        thread_limit: None,
        version: Version::V2,
    });
"#;

/// A pool-sized limit with nothing said. Legal on the batch tier, forbidden on
/// serve, and text cannot tell them apart, so the author has to.
const WIDE_UNDECLARED: &str = r#"
    let entries = iter_from_counts(counts, db, progress, Options {
        thread_limit: options.thread_limit,
        version: Version::V2,
    });
"#;

/// What the serve path must look like.
const SERVE: &str = r#"
    let entries = iter_from_counts(counts, db, progress, Options {
        thread_limit: Some(1),
        version: Version::V2,
    });
"#;

/// What the batch tier must look like.
const BATCH: &str = r#"
    // pack-pipeline: batch tier — repack is a gatling batch job, not a
    // per-request path, so the pool is the right answer here.
    let entries = iter_from_counts(counts, db, progress, Options {
        thread_limit: options.thread_limit,
        version: Version::V2,
    });
"#;

/// The trailer rule's subject, undeclared.
const TRAILER_UNDECLARED: &str = r#"
    let mut pack = output::bytes::FromEntriesIter::new(
        InOrderIter::from(entries),
        &mut writer,
        num_entries,
        Version::V2,
        hash_kind,
    );
"#;

/// The trailer rule's subject, declared.
const TRAILER_DECLARED: &str = r#"
    // pack-pipeline: sha1dc trailer accepted — a repack runs once, off the request path.
    let mut pack = output::bytes::FromEntriesIter::new(
        InOrderIter::from(entries),
        &mut writer,
        num_entries,
        Version::V2,
        hash_kind,
    );
"#;

fn one(sample: &str, rule: Rule) {
    let findings = check_source("sample.rs", sample);
    assert_eq!(
        findings.len(),
        1,
        "expected exactly one {rule:?} finding, got {findings:?}"
    );
    assert_eq!(findings[0].rule, rule, "the wrong rule fired");
    assert!(
        !findings[0].why.is_empty(),
        "a finding that does not explain itself sends the reader to xtask instead \
         of to the call site"
    );
}

#[test]
fn a_fan_out_call_with_no_declaration_is_a_violation() {
    one(UNLIMITED, Rule::PerRequestFanOut);
    assert_eq!(
        check_source("sample.rs", UNLIMITED)[0].line,
        3,
        "the call line was misreported. A multi-line call must be counted once, \
         at its head"
    );
}

#[test]
fn thread_limit_none_is_a_violation() {
    one(EXPLICIT_NONE, Rule::PerRequestFanOut);
}

#[test]
fn a_pool_sized_limit_must_declare_its_tier() {
    one(WIDE_UNDECLARED, Rule::PerRequestFanOut);
}

#[test]
fn an_undeclared_from_entries_iter_is_a_violation() {
    one(TRAILER_UNDECLARED, Rule::Sha1dcTrailer);
}

#[test]
fn both_declared_forms_are_accepted() {
    for (name, sample) in [
        ("serve", SERVE),
        ("batch", BATCH),
        ("trailer", TRAILER_DECLARED),
    ] {
        assert!(
            check_source("sample.rs", sample).is_empty(),
            "the guard rejects the `{name}` form the rule itself prescribes: {:?}",
            check_source("sample.rs", sample)
        );
    }
}

/// Prose naming either function is not a call. Without this the guard fires on
/// its own documentation and on every `.nornir` quotation in a doc comment.
#[test]
fn a_mention_in_a_comment_is_not_a_call() {
    for sample in [
        "// iter_from_counts( fans out above 4000 counts",
        "/// See `output::bytes::FromEntriesIter::new(`.",
        "//! `iter_from_counts(` is the function pack-copy needs.",
    ] {
        assert!(
            check_source("sample.rs", sample).is_empty(),
            "the guard fired on a comment: {sample}"
        );
    }
}

/// Green against the real tree, and the scan is asserted to have read
/// something. Zero findings over zero files is not a pass.
#[test]
fn gunnars_shipped_sources_declare_what_they_cost() {
    let root = xtask::guard::workspace_root();
    let (findings, files) = scan_workspace(&root);
    assert!(
        files > 50,
        "the scan read only {files} file(s) under {}/crates, and an empty scan is \
         shaped exactly like a clean one",
        root.display()
    );
    // ── the blindness anchor, re-aimed 2026-08-10 ────────────────────────────
    //
    // This named `gunnar-store/src/repack.rs`, "the one file in the tree that
    // uses all three gix output stages" — and that file **does not exist**. It
    // went with the packfile engine, and the assertion went with it: the test
    // had been failing on a missing anchor rather than passing on a clean scan,
    // which is a guard reporting on itself and being ignored.
    //
    // Where the subject went: `iter_from_counts` and `FromEntriesIter::new` have
    // **no live call site left in gunnar**. The four remaining mentions
    // (`gunnar-wire/src/{upload_pack,limits}.rs`, `gunnar-server/src/serve.rs`,
    // `bench/examples/forge/parity.rs`) are all comments, and comments are
    // stripped before the call is looked for. The engine emits the pack now
    // (`upload_pack.rs`: *"the engine emits; this streams"*), so gunnar no longer
    // drives gix's output pipeline at all.
    //
    // That makes the rule dormant, not dead — `gunnar_pack` still exists and the
    // day anyone reaches for those stages again the rule must be watching. So the
    // anchor is now the two files that would hold the call if it came back, and
    // the emptiness of the finding list is checked against a scan proven to have
    // read them.
    for anchor in [
        "gunnar-wire/src/upload_pack.rs",
        "gunnar-server/src/serve.rs",
    ] {
        assert!(
            shipped_sources(&root).iter().any(|p| p.ends_with(anchor)),
            "the scan did not reach `{anchor}`, which is where pack production lives. If \
             this guard cannot see the pack path, it is green because it is blind"
        );
    }
    assert!(
        findings.is_empty(),
        "undeclared gix pack-pipeline costs:\n{}",
        findings
            .iter()
            .map(|f| format!("  {f}\n"))
            .collect::<String>()
    );
}

// ── rule 3: a storage impl that opts out of an optimisation in silence ──────
//
// A defaulted trait method returning `None` is not "I did not answer" — it is
// "turn it off". A wrapper that forwards the read methods and omits one of these
// makes everything served or measured through it take the walk path, or the
// re-deflate path, while every wire byte, object count and fidelity digest stays
// identical. Five instances in one tree on 2026-08-04; two of them were the
// BEFORE column of a published comparison.
//
// ⚠ The samples below are `impl GitOps` — the live contract, so that the
// impl-detection under test is the real one — but the two methods they hang the
// proof on are `packed()` and `bitmap()`, the historical pair from
// `gunnar_store::ObjectStore`. That is deliberate and it is why [`PROVING_LIST`]
// exists: `GitOps` has no defaulted method for the rule to fire on, so a proof
// written against today's real names could not go red at all, and this whole
// section would quietly stop testing anything — which is exactly what it did
// between the list being emptied and 2026-08-10. The names are a reconstruction
// of a defect class; the mechanism they exercise is current.

/// The shape the rule exists to catch: a forwarding wrapper missing `bitmap`.
const WRAPPER_WITHOUT_BITMAP: &str = r#"
impl<S: GitOps> GitOps for Counting<S> {
    fn put_pack(&self, bytes: &[u8]) -> R<TxId> { self.inner.put_pack(bytes) }
    fn extents(&self, oids: &[Oid<'_>]) -> R<Vec<Option<Extent>>> { self.inner.extents(oids) }
    fn update_ref(&self, n: &str, o: Option<Oid<'_>>, w: Option<Oid<'_>>) -> R<TxId> { self.inner.update_ref(n, o, w) }
    fn packed(&self) -> Option<PackedObjects> { self.inner.packed() }
}
"#;

/// The same wrapper, complete.
const WRAPPER_COMPLETE: &str = r#"
impl<S: GitOps> GitOps for Counting<S> {
    fn extents(&self, oids: &[Oid<'_>]) -> R<Vec<Option<Extent>>> { self.inner.extents(oids) }
    fn packed(&self) -> Option<PackedObjects> { self.inner.packed() }
    fn bitmap(&self) -> Option<Arc<PackBitmap>> { self.inner.bitmap() }
}
"#;

#[test]
fn rule_three_fires_on_a_wrapper_that_omits_bitmap() {
    let findings = check_impls("sample.rs", WRAPPER_WITHOUT_BITMAP, PROVING_LIST);
    assert_eq!(findings.len(), 1, "expected exactly the `bitmap` finding");
    assert_eq!(findings[0].rule, Rule::SilentOptOut);
    assert!(
        findings[0].why.contains("fn bitmap("),
        "the message must name the method that is missing: {}",
        findings[0].why
    );
    assert!(
        check_impls("sample.rs", WRAPPER_COMPLETE, PROVING_LIST).is_empty(),
        "the guard fired on a wrapper that answers both methods"
    );
}

/// The rule must read the block's **own** braces, not the file's.
///
/// Written because the obvious implementation — "does the file contain
/// `fn bitmap(`" — is green on a file holding one complete impl and one
/// incomplete one, which is exactly `bench/examples/nornir-bench.rs`.
#[test]
fn rule_three_is_per_impl_and_not_per_file() {
    let text = format!("{WRAPPER_COMPLETE}\n{WRAPPER_WITHOUT_BITMAP}");
    let findings = check_impls("sample.rs", &text, PROVING_LIST);
    assert_eq!(
        findings.len(),
        1,
        "one complete impl in the same file hid the incomplete one: {findings:?}"
    );
}

/// The trait's own definition is not an `impl` and must not fire.
#[test]
fn rule_three_leaves_the_trait_definition_alone() {
    let trait_text = r#"
pub trait GitOps: Send + Sync {
    fn extents(&self, oids: &[Oid<'_>]) -> R<Vec<Option<Extent>>>;
    fn packed(&self) -> Option<PackedObjects> { None }
    fn bitmap(&self) -> Option<Arc<PackBitmap>> { None }
}
"#;
    assert!(check_impls("object.rs", trait_text, PROVING_LIST).is_empty());
}

// ── rule 4: a wrapper that reaches the same disabled state by typing `None` ──
//
// Rule 3 is satisfied by *writing the method out*. A forwarding wrapper can
// satisfy it with `{ None }`, which is byte-for-byte the behaviour the omission
// had: same disabled bitmap, same identical wire bytes, same green fidelity
// check — and now a guard has signed off on it. Rule 4 is the one that makes
// the class impossible: a forwarder forwards, or it says why not.

/// The shape rule 4 exists to catch: rule 3 satisfied, optimisation still off.
const WRAPPER_WITH_NONE: &str = r#"
impl<S: GitOps> GitOps for Counting<S> {
    fn extents(&self, oids: &[Oid<'_>]) -> R<Vec<Option<Extent>>> { self.inner.extents(oids) }
    fn gc(&self) -> R<GcReport> { self.inner.gc() }
    fn packed(&self) -> Option<PackedObjects> { self.inner.packed() }
    fn bitmap(&self) -> Option<Arc<PackBitmap>> { None }
}
"#;

/// A **leaf** backend over a `RwLock<HashMap>`. It has nothing to forward to,
/// and `self.objects.read()` is a lock, not a store.
///
/// Written because the first version of rule 4 counted `read`/`write` as trait
/// methods and demanded a marker from `MemObjectStore` — the one store in the
/// tree with genuinely no inner store. A rule that cries wolf gets deleted.
const LEAF_WITH_NONE: &str = r#"
impl GitOps for MemGitStore {
    fn has(&self, oid: Oid<'_>) -> R<bool> { Ok(self.objects.read().unwrap().contains_key(oid)) }
    fn put_pack(&self, bytes: &[u8]) -> R<TxId> {
        let mut objects = self.objects.write().unwrap();
        Ok(TxId::default())
    }
    fn packed(&self) -> Option<PackedObjects> { None }
    fn bitmap(&self) -> Option<Arc<PackBitmap>> { None }
}
"#;

#[test]
fn rule_four_fires_on_a_forwarder_that_answers_none() {
    let findings = check_impls("sample.rs", WRAPPER_WITH_NONE, PROVING_LIST);
    assert_eq!(
        findings.len(),
        1,
        "expected exactly the `bitmap` finding, got {findings:?}"
    );
    assert_eq!(
        findings[0].rule,
        Rule::NotForwarded,
        "rule 3 must NOT be what fires here: the method IS written out"
    );
    assert!(
        findings[0].why.contains("fn bitmap("),
        "the message must name the method: {}",
        findings[0].why
    );
}

/// The identity-value trap, in this rule's own shape: a rule 4 that fired on
/// every `None` would be satisfied by this test and would still be wrong,
/// because it would also fire on every leaf backend in the tree.
#[test]
fn rule_four_leaves_a_leaf_backend_alone() {
    let findings = check_impls("mem.rs", LEAF_WITH_NONE, PROVING_LIST);
    assert!(
        findings.is_empty(),
        "rule 4 fired on a store with no inner store to forward to. `self.objects.read()` \
         is an `RwLock`, not a `GitOps`: {findings:?}"
    );
}

#[test]
fn rule_four_accepts_a_declared_opt_out() {
    let declared = WRAPPER_WITH_NONE.replace(
        "    fn bitmap(&self) -> Option<Arc<PackBitmap>> { None }",
        &format!(
            "    /// {NOT_FORWARDED_MARKER}. This wrapper's whole job is to cut \
             the bitmap seam.\n    fn bitmap(&self) -> Option<Arc<PackBitmap>> {{ None }}"
        ),
    );
    assert!(
        check_impls("sample.rs", &declared, PROVING_LIST).is_empty(),
        "the declared form the rule itself prescribes was rejected: {:?}",
        check_impls("sample.rs", &declared, PROVING_LIST)
    );
}

/// One marker must not excuse the other method. The marker is read from the
/// method's own doc comment, not from anywhere in the block.
#[test]
fn a_marker_on_one_method_does_not_excuse_the_other() {
    let both_none = WRAPPER_WITH_NONE.replace(
        "    fn packed(&self) -> Option<PackedObjects> { self.inner.packed() }",
        &format!(
            "    /// {NOT_FORWARDED_MARKER}. Only this one.\n    \
             fn packed(&self) -> Option<PackedObjects> {{ None }}"
        ),
    );
    let findings = check_impls("sample.rs", &both_none, PROVING_LIST);
    assert_eq!(
        findings.len(),
        1,
        "a marker on `packed` excused `bitmap` too, which is one declaration \
         covering two decisions: {findings:?}"
    );
    assert!(findings[0].why.contains("fn bitmap("));
}

/// **The live list is empty, and that is a decision rather than an oversight.**
///
/// The rest of this section proves the mechanism against [`PROVING_LIST`], which
/// would stay green forever if somebody quietly emptied the real list — the
/// state this file was actually in between 2026-08-10 and 2026-08-10. So the
/// emptiness itself is asserted, with the reason in the failure message: the day
/// a name is added, this test goes red and whoever added it has to come here and
/// say why it is load-bearing.
#[test]
fn the_live_list_is_empty_and_that_is_a_declaration() {
    assert!(
        OPTIONAL_BUT_LOAD_BEARING.is_empty(),
        "`OPTIONAL_BUT_LOAD_BEARING` gained {:?}. That is allowed — but the emptiness was a \
         PROPERTY, not an accident: `GitOps` has no defaulted method, so no implementer can \
         inherit a behaviour it did not write and this class of defect is unrepresentable on \
         the contract. If a defaulted, optimisation-toggling method now exists, say here which \
         trait grew it and why the default could not be removed instead — fixing by \
         construction beats a guard over two copies",
        OPTIONAL_BUT_LOAD_BEARING
    );
}

/// Green against the real tree, over a set asserted to include the places the
/// five real instances were found — `bench/` and `tests/`, which
/// [`shipped_sources`] deliberately excludes.
#[test]
fn every_object_store_in_the_tree_says_what_it_turns_off() {
    let root = xtask::guard::workspace_root();
    let (findings, files) = scan_object_stores(&root);
    assert!(
        files > 100,
        "the scan read only {files} file(s), and an empty scan is shaped exactly like a \
         clean one"
    );
    let sources = object_store_sources(&root);
    // ── DIRECTORIES, not files, since 2026-08-10 ─────────────────────────────
    //
    // This was a list of eight FILES, and two of them —
    // `gunnar-store/src/object.rs` and the deleted engine crate’s objects.rs —
    // had been deleted, so the test failed on a missing anchor instead of
    // passing on a clean scan. That is this guard's own disease: it was red, and
    // being red it said nothing about the tree.
    //
    // The claim was never really about those files. It is about **coverage of
    // the directories a scan can silently omit**, which is what the 2026-08-04
    // widening was: the original set was `crates/*/{src,tests}` plus
    // `bench/{src,examples}`, which quietly missed every `crates/*/examples`,
    // every `benches/`, `bench/tests`, `installtest/`, and
    // the old znippy engine bench — a whole second harness on a store the
    // workspace `exclude` already hides from `cargo test`. A rule about
    // instruments cannot be scoped to the directories the last five instruments
    // happened to be in.
    //
    // Stated as directories, the assertion survives any single file being
    // renamed or deleted and still goes red the moment a whole area stops being
    // read — which is the only failure it was ever able to catch.
    // The three deleted-crate entries (`crates/gunnar-znippy/{src,examples}`
    // and its detached bench) left this list on 2026-08-16. The crate was
    // deleted 2026-08-10 (the engine is `gunnar-server`'s `engine-znippy`
    // feature now), and a mass comment
    // rename had already replaced the crate's NAME inside these string
    // literals with prose, so the guard was demanding directories that never
    // existed under any name — no scan of any tree can contain a path spelled
    // `/the old znippy engine crate/src/`, so this guard had been **red ever
    // since**, which is the disease the comment above describes caught a
    // second time by the same mistake in the same list: "it was red, and being
    // red it said nothing about the tree."
    //
    // They are replaced rather than merely dropped, three for three.
    // `crates/gunnar-server/src/` is the directory the engine seam actually
    // moved to, and it holds `storage/znippy_engine.rs` — the instrument the
    // deleted entries were covering. `crates/gunnar-server/examples/` and
    // `crates/gunnar-store/tests/` stand in for the other two: both are
    // scanned by `object_store_sources` and both hold Rust today, so the
    // count of areas a scan can silently stop reading is unchanged.
    for must in [
        "/bench/src/",
        "/bench/examples/",
        "/crates/gunnar-wire/src/",
        "/crates/gunnar-wire/tests/",
        "/crates/gunnar-store/src/",
        "/crates/gunnar-store/tests/",
        "/crates/gunnar-server/src/",
        "/crates/gunnar-server/examples/",
        "/installtest/src/",
    ] {
        assert!(
            sources.iter().any(|p| p.to_string_lossy().contains(must)),
            "the scan read no file under `{must}`. Every entry in this list is a directory \
             that held one of the known instances, or one the first version of this scan \
             could not see at all. A guard that cannot see them is green because it is blind"
        );
    }
    assert!(
        findings.is_empty(),
        "an `ObjectStore` turns an optimisation off in silence:\n{}",
        findings
            .iter()
            .map(|f| format!("  {f}\n"))
            .collect::<String>()
    );
}