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

nordisk/nornir

src/test_select.rs

//! **Which tests a commit can break — the half that needs no graph.**
//!
//! Incremental tests (owner 2026-10-17: "yes do incremental tests and
//! coverage"): on a commit, run only the tests whose reach holds a changed
//! function. The code room answers the reach (`Test.Affected`,
//! [`crate::code_room::reader`]); this module answers what a COMMIT changed
//! and what that means before any graph is asked:
//!
//! * [`commit_diff`] — the files two commits differ in, and per Rust file the
//!   lines of the NEWER side a hunk touched (a pure removal touches the two
//!   lines around it), read in-process through gix;
//! * [`classify_path`] — the paths the graph cannot see through: a manifest,
//!   a lock file, a build script, a toolchain pin, cargo / nextest config —
//!   every test runs ([`PathRule::All`]);
//! * [`line_kind`] — a changed line no item of the room holds: blank, a
//!   comment, a `use` / `mod` declaration (the graph's own diff carries what
//!   they move), an attribute, a `cfg` (every test runs), or code the graph
//!   does not know;
//! * [`identifiers`] / [`mentions`] — where a name is used AS A VALUE (`.map(f)`,
//!   `Type::f` passed on): the producer records no call through a fn pointer,
//!   so the selection widens over such a use;
//! * [`runner_name`] — the name cargo test / nextest print for a test (its
//!   path without the crate), the spelling a `--skip` takes.
//!
//! The rule everywhere: when the answer cannot be sure, MORE tests run,
//! never fewer — and the one line that says why travels with the answer.

use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;

use anyhow::{Context, Result, anyhow};

/// What happened to a file between the two commits.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileChange {
    Added,
    Deleted,
    Modified,
}

/// One file the two commits differ in.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileDiff {
    /// Repo-relative, `/`-separated.
    pub path: String,
    pub change: FileChange,
    /// 1-based lines of the NEWER side a hunk touched, ascending, merged
    /// (`(start, end)` inclusive). A pure removal touches the line before
    /// and the line after it. Empty for a deleted file and a non-Rust file.
    pub lines: Vec<(u32, u32)>,
}

impl FileDiff {
    /// Every touched line, one by one.
    pub fn each_line(&self) -> impl Iterator<Item = u32> + '_ {
        self.lines.iter().flat_map(|&(a, b)| a..=b)
    }
}

/// The tree of one commit, for reading its files' text.
pub struct Snapshot {
    repo: gix::Repository,
    tree: gix::ObjectId,
    /// Every `.rs` blob of the tree, path → text — read once, on first ask.
    rust: std::sync::OnceLock<BTreeMap<String, String>>,
    /// Every `Cargo.toml` of the tree, path → text — read once, on first ask.
    manifests: std::sync::OnceLock<BTreeMap<String, String>>,
}

impl Snapshot {
    /// The tree of `sha` in the repo at `dir`.
    pub fn at(dir: &Path, sha: &str) -> Result<Self> {
        let repo = gix::open(dir).with_context(|| format!("open {}", dir.display()))?;
        let tree = tree_of(&repo, sha)?;
        Ok(Self { repo, tree, rust: std::sync::OnceLock::new(), manifests: std::sync::OnceLock::new() })
    }

    /// The text of `path` in this tree (`None` = not there, or not text).
    pub fn text(&self, path: &str) -> Option<String> {
        if let Some(t) = self.rust.get().and_then(|m| m.get(path)) {
            return Some(t.clone());
        }
        let tree = self.repo.find_tree(self.tree).ok()?;
        let entry = tree.lookup_entry_by_path(path).ok()??;
        let blob = entry.object().ok()?;
        String::from_utf8(blob.data.clone()).ok()
    }

    /// Every `.rs` file of the tree, path → text.
    pub fn rust_files(&self) -> &BTreeMap<String, String> {
        self.rust.get_or_init(|| self.blobs(|p| p.ends_with(".rs")))
    }

    /// Every `Cargo.toml` of the tree, path → text.
    pub fn manifests(&self) -> &BTreeMap<String, String> {
        self.manifests.get_or_init(|| self.blobs(|p| p == "Cargo.toml" || p.ends_with("/Cargo.toml")))
    }

    /// The packages of the tree (every manifest with a `[package]`).
    pub fn packages(&self) -> Packages {
        Packages::from_manifests(self.manifests())
    }

    fn blobs(&self, want: impl Fn(&str) -> bool) -> BTreeMap<String, String> {
        let mut out = BTreeMap::new();
        let Ok(tree) = self.repo.find_tree(self.tree) else { return out };
        let mut rec = gix::traverse::tree::Recorder::default();
        if tree.traverse().breadthfirst(&mut rec).is_err() {
            return out;
        }
        for e in rec.records {
            let path = e.filepath.to_string();
            if !e.mode.is_blob() || !want(&path) {
                continue;
            }
            if let Ok(obj) = self.repo.find_object(e.oid)
                && let Ok(text) = String::from_utf8(obj.data.clone())
            {
                out.insert(path, text);
            }
        }
        out
    }
}

/// **The cargo packages of a tree**: manifest dir (repo-relative, `""` =
/// the root) → package name. A file belongs to the package of its nearest
/// ancestor dir that holds one.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Packages {
    pub by_dir: BTreeMap<String, String>,
}

impl Packages {
    /// From `Cargo.toml` path → text (a manifest without `[package]` — a
    /// virtual workspace root — names no package; one that does not parse
    /// is read for its `name = "…"` line, else left out).
    pub fn from_manifests(manifests: &BTreeMap<String, String>) -> Self {
        let mut by_dir = BTreeMap::new();
        for (path, text) in manifests {
            let dir = path.strip_suffix("Cargo.toml").unwrap_or("").trim_end_matches('/').to_string();
            let name = match text.parse::<toml::Table>() {
                Ok(t) => t.get("package").and_then(|p| p.get("name")).and_then(|n| n.as_str()).map(str::to_string),
                Err(_) => text.split("[package]").nth(1).and_then(|rest| {
                    rest.lines().take_while(|l| !l.trim_start().starts_with('[')).find_map(|l| {
                        let (k, v) = l.split_once('=')?;
                        (k.trim() == "name").then(|| v.trim().trim_matches('"').to_string())
                    })
                }),
            };
            if let Some(n) = name {
                by_dir.insert(dir, n);
            }
        }
        Self { by_dir }
    }

    /// The package `path` belongs to: `(dir, name)` of its nearest ancestor
    /// manifest dir.
    pub fn of_file(&self, path: &str) -> Option<(&str, &str)> {
        let mut dir = path;
        loop {
            dir = match dir.rfind('/') {
                Some(i) => &dir[..i],
                None if !dir.is_empty() => "",
                None => return None,
            };
            if let Some((d, n)) = self.by_dir.get_key_value(dir) {
                return Some((d.as_str(), n.as_str()));
            }
            if dir.is_empty() {
                return None;
            }
        }
    }

    /// The manifest dir of the package named `name` (`-` and `_` alike).
    pub fn dir_of(&self, name: &str) -> Option<&str> {
        let want = norm_crate(name);
        self.by_dir.iter().find(|(_, n)| norm_crate(n) == want).map(|(d, _)| d.as_str())
    }
}

/// A crate / package name with `-` read as `_` (cargo's two spellings).
pub fn norm_crate(name: &str) -> String {
    name.replace('-', "_")
}

/// Is `path` a file of a target that is no library — a binary
/// (`src/main.rs`, `src/bin/…`), an example, a bench, an integration test,
/// a build script — of the package in `dir`? Nothing links such a file into
/// another crate: only a process (a test that runs the target) reaches it.
pub fn is_non_lib_target(dir: &str, path: &str) -> bool {
    let rel = if dir.is_empty() { path } else { path.strip_prefix(dir).and_then(|r| r.strip_prefix('/')).unwrap_or(path) };
    rel == "src/main.rs" || rel == "build.rs" || ["src/bin/", "examples/", "benches/", "tests/"].iter().any(|p| rel.starts_with(p))
}

/// One `[[package]]` of a `Cargo.lock`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LockPkg {
    pub name: String,
    pub version: String,
    /// `None` = a path package (a workspace member or a path dependency).
    pub source: Option<String>,
    pub checksum: Option<String>,
    /// The names it depends on (the `dependencies` entries' first word).
    pub deps: Vec<String>,
}

/// **Parse a `Cargo.lock`.**
pub fn parse_lock(text: &str) -> Result<Vec<LockPkg>> {
    let t: toml::Table = text.parse().context("Cargo.lock is no TOML")?;
    let Some(pkgs) = t.get("package").and_then(|p| p.as_array()) else { return Ok(Vec::new()) };
    let s = |v: &toml::Value, k: &str| v.get(k).and_then(|x| x.as_str()).map(str::to_string);
    let mut out = Vec::with_capacity(pkgs.len());
    for p in pkgs {
        let name = s(p, "name").ok_or_else(|| anyhow!("a Cargo.lock package without a name"))?;
        let deps = p
            .get("dependencies")
            .and_then(|d| d.as_array())
            .map(|d| d.iter().filter_map(|x| x.as_str()).filter_map(|x| x.split_whitespace().next()).map(str::to_string).collect())
            .unwrap_or_default();
        out.push(LockPkg { name, version: s(p, "version").unwrap_or_default(), source: s(p, "source"), checksum: s(p, "checksum"), deps });
    }
    Ok(out)
}

/// **The packages a lock change moved**: every name whose entries (version,
/// source, checksum, dependencies) differ between `before` and `after` —
/// added, removed, or resolved otherwise.
pub fn lock_moved(before: &[LockPkg], after: &[LockPkg]) -> BTreeSet<String> {
    let by = |l: &[LockPkg]| -> BTreeMap<String, BTreeSet<LockPkg>> {
        l.iter().fold(BTreeMap::new(), |mut m, p| {
            let mut p = p.clone();
            p.deps.sort();
            m.entry(p.name.clone()).or_default().insert(p);
            m
        })
    };
    let (a, b) = (by(before), by(after));
    a.keys().chain(b.keys()).filter(|n| a.get(*n) != b.get(*n)).cloned().collect()
}

/// **Every package that depends on one of `moved`**, directly or not, by
/// the dependency lists of `locks` (all read together: a package that
/// depended on it in either lock counts) — `moved` included.
pub fn lock_dependents(locks: &[&[LockPkg]], moved: &BTreeSet<String>) -> BTreeSet<String> {
    let mut rev: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
    for l in locks {
        for p in l.iter() {
            for d in &p.deps {
                rev.entry(d.as_str()).or_default().insert(p.name.as_str());
            }
        }
    }
    let mut out: BTreeSet<String> = moved.clone();
    let mut todo: Vec<String> = moved.iter().cloned().collect();
    while let Some(n) = todo.pop() {
        for u in rev.get(n.as_str()).into_iter().flatten() {
            if out.insert(u.to_string()) {
                todo.push(u.to_string());
            }
        }
    }
    out
}

/// **The dependency lists of a tree's manifests**, as lock entries: per
/// package its name and the (real, `package = …` read) names of its
/// dependencies, dev-dependencies and build-dependencies, target-specific
/// ones included — what [`lock_dependents`] walks when no lock says it.
pub fn manifest_deps(manifests: &BTreeMap<String, String>) -> Vec<LockPkg> {
    let mut out = Vec::new();
    for text in manifests.values() {
        let Ok(t) = text.parse::<toml::Table>() else { continue };
        let Some(name) = t.get("package").and_then(|p| p.get("name")).and_then(|n| n.as_str()) else { continue };
        let mut deps: Vec<String> = Vec::new();
        let mut read = |tab: &toml::Table| {
            for key in ["dependencies", "dev-dependencies", "build-dependencies"] {
                if let Some(d) = tab.get(key).and_then(|d| d.as_table()) {
                    for (k, v) in d {
                        deps.push(v.get("package").and_then(|p| p.as_str()).unwrap_or(k).to_string());
                    }
                }
            }
        };
        read(&t);
        if let Some(targets) = t.get("target").and_then(|x| x.as_table()) {
            for v in targets.values() {
                if let Some(tab) = v.as_table() {
                    read(tab);
                }
            }
        }
        out.push(LockPkg { name: name.to_string(), version: String::new(), source: None, checksum: None, deps });
    }
    out
}

/// **Where a string literal of `files` names one of `terms`** as a whole
/// word (a neighbour that is no letter, digit, `_` or `-`): `(file, line)`.
/// What reaches a target across a process boundary (`cargo build` in its
/// dir, `cargo run -p` it, a path to its binary) names it in a string.
pub fn named_in_strings(files: &BTreeMap<String, String>, terms: &[String]) -> Vec<(String, u32)> {
    let mut out: BTreeSet<(String, u32)> = BTreeSet::new();
    let word = |c: char| c == '_' || c == '-' || c.is_alphanumeric();
    for (path, text) in files {
        if !terms.iter().any(|t| text.contains(t.as_str())) {
            continue;
        }
        for (line, lit) in string_literals(text) {
            for t in terms {
                for (at, _) in lit.match_indices(t.as_str()) {
                    let before = lit[..at].chars().next_back();
                    let after = lit[at + t.len()..].chars().next();
                    if before.is_some_and(word) || after.is_some_and(word) {
                        continue;
                    }
                    out.insert((path.clone(), line + lit[..at].matches('\n').count() as u32));
                }
            }
        }
    }
    out.into_iter().collect()
}

/// Every string literal of `text` (plain and raw; comments skipped): the
/// 1-based line it starts on and its contents as written.
pub fn string_literals(text: &str) -> Vec<(u32, &str)> {
    let b = text.as_bytes();
    let (mut i, mut line) = (0usize, 1u32);
    let mut out = Vec::new();
    let is_cont = |c: u8| c == b'_' || c.is_ascii_alphanumeric();
    let mut raw_at = usize::MAX;
    while i < b.len() {
        match b[i] {
            b'\n' => {
                line += 1;
                i += 1;
            }
            b'/' if b.get(i + 1) == Some(&b'/') => {
                while i < b.len() && b[i] != b'\n' {
                    i += 1;
                }
            }
            b'/' if b.get(i + 1) == Some(&b'*') => {
                let mut depth = 1;
                i += 2;
                while i < b.len() && depth > 0 {
                    if b[i] == b'\n' {
                        line += 1;
                    }
                    if b[i] == b'/' && b.get(i + 1) == Some(&b'*') {
                        depth += 1;
                        i += 1;
                    } else if b[i] == b'*' && b.get(i + 1) == Some(&b'/') {
                        depth -= 1;
                        i += 1;
                    }
                    i += 1;
                }
            }
            b'"' => {
                let (start, at) = (i + 1, line);
                i += 1;
                while i < b.len() && b[i] != b'"' {
                    if b[i] == b'\\' {
                        if b.get(i + 1) == Some(&b'\n') {
                            line += 1;
                        }
                        i += 1;
                    } else if b[i] == b'\n' {
                        line += 1;
                    }
                    i += 1;
                }
                out.push((at, &text[start..i.min(b.len())]));
                i += 1;
            }
            b'r' if b.get(i + 1).is_some_and(|n| *n == b'#' || *n == b'"') && (i == 0 || i == raw_at || !is_cont(b[i - 1])) => {
                let mut j = i + 1;
                let mut hashes = 0;
                while b.get(j) == Some(&b'#') {
                    hashes += 1;
                    j += 1;
                }
                if b.get(j) != Some(&b'"') {
                    i += 1;
                    continue;
                }
                j += 1;
                let (start, at) = (j, line);
                let mut end = b.len();
                while j < b.len() {
                    if b[j] == b'"' && b.len() >= j + 1 + hashes && b[j + 1..j + 1 + hashes].iter().all(|x| *x == b'#') {
                        end = j;
                        j += 1 + hashes;
                        break;
                    }
                    if b[j] == b'\n' {
                        line += 1;
                    }
                    j += 1;
                }
                out.push((at, &text[start..end.min(b.len())]));
                i = j;
            }
            b'\'' => {
                if b.get(i + 2) == Some(&b'\'') {
                    i += 3;
                } else if b.get(i + 1) == Some(&b'\\') {
                    i += 2;
                    while i < b.len() && b[i] != b'\'' {
                        i += 1;
                    }
                    i += 1;
                } else {
                    i += 1;
                    while i < b.len() && is_cont(b[i]) {
                        i += 1;
                    }
                }
            }
            c if is_cont(c) => {
                // a word; `br"…"` hands its `r` to the raw arm (`b"…"` reads as a plain string)
                let s = i;
                while i < b.len() && is_cont(b[i]) {
                    i += 1;
                }
                if &b[s..i] == b"br" && matches!(b.get(i), Some(b'#') | Some(b'"')) {
                    i -= 1;
                    raw_at = i;
                }
            }
            _ => i += 1,
        }
    }
    out
}

/// What kind of item a brace block opens.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlockKind {
    Impl,
    Trait,
    Mod,
    Fn,
    /// A struct / enum / union body, an `extern` block, a macro body …
    OtherItem,
    /// A block inside an expression or a statement (no item header).
    Expr,
}

/// One `{ … }` block of a source text.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Block {
    pub kind: BlockKind,
    /// 1-based line of the header's first token (attributes left out).
    pub header: u32,
    /// The lines of the `{` and the `}` (the last line when it never closes).
    pub open: u32,
    pub close: u32,
}

/// **The blocks of `text` that hold `line`**, innermost first. A lexer for
/// one purpose (comments, strings and char literals skipped), not Rust's.
pub fn enclosing_blocks(text: &str, line: u32) -> Vec<Block> {
    let b = text.as_bytes();
    let (mut i, mut ln) = (0usize, 1u32);
    // per open block: (kind, header line, open line)
    let mut stack: Vec<(BlockKind, u32, u32)> = Vec::new();
    let mut out: Vec<Block> = Vec::new();
    // where the current header began: the byte after the last `;` `{` `}`
    let mut head_from = 0usize;
    let is_cont = |c: u8| c == b'_' || c.is_ascii_alphanumeric();
    while i < b.len() {
        let c = b[i];
        match c {
            b'\n' => {
                ln += 1;
                i += 1;
            }
            b'/' if b.get(i + 1) == Some(&b'/') => {
                while i < b.len() && b[i] != b'\n' {
                    i += 1;
                }
            }
            b'/' if b.get(i + 1) == Some(&b'*') => {
                let mut depth = 1;
                i += 2;
                while i < b.len() && depth > 0 {
                    if b[i] == b'\n' {
                        ln += 1;
                    }
                    if b[i] == b'/' && b.get(i + 1) == Some(&b'*') {
                        depth += 1;
                        i += 1;
                    } else if b[i] == b'*' && b.get(i + 1) == Some(&b'/') {
                        depth -= 1;
                        i += 1;
                    }
                    i += 1;
                }
            }
            b'"' => {
                i += 1;
                while i < b.len() && b[i] != b'"' {
                    if b[i] == b'\\' {
                        // an escaped newline (a string continued on the next line) is a line too
                        if b.get(i + 1) == Some(&b'\n') {
                            ln += 1;
                        }
                        i += 1;
                    } else if b[i] == b'\n' {
                        ln += 1;
                    }
                    i += 1;
                }
                i += 1;
            }
            b'r' if b.get(i + 1).is_some_and(|n| *n == b'#' || *n == b'"') && (i == 0 || !is_cont(b[i - 1])) => {
                let mut j = i + 1;
                let mut hashes = 0;
                while b.get(j) == Some(&b'#') {
                    hashes += 1;
                    j += 1;
                }
                if b.get(j) != Some(&b'"') {
                    i += 1;
                    continue;
                }
                j += 1;
                while j < b.len() {
                    if b[j] == b'\n' {
                        ln += 1;
                    }
                    if b[j] == b'"' && b.len() >= j + 1 + hashes && b[j + 1..j + 1 + hashes].iter().all(|x| *x == b'#') {
                        j += 1 + hashes;
                        break;
                    }
                    j += 1;
                }
                i = j;
            }
            b'\'' => {
                if b.get(i + 2) == Some(&b'\'') {
                    i += 3;
                } else if b.get(i + 1) == Some(&b'\\') {
                    i += 2;
                    while i < b.len() && b[i] != b'\'' {
                        i += 1;
                    }
                    i += 1;
                } else {
                    i += 1;
                    while i < b.len() && is_cont(b[i]) {
                        i += 1;
                    }
                }
            }
            b'{' => {
                let (kind, header) = block_header(text, head_from, i, ln);
                stack.push((kind, header, ln));
                i += 1;
                head_from = i;
            }
            b'}' => {
                if let Some((kind, header, open)) = stack.pop()
                    && header.min(open) <= line
                    && line <= ln
                {
                    out.push(Block { kind, header, open, close: ln });
                }
                i += 1;
                head_from = i;
            }
            b';' => {
                i += 1;
                head_from = i;
            }
            _ => i += 1,
        }
    }
    // blocks that never close hold everything after them
    while let Some((kind, header, open)) = stack.pop() {
        if header.min(open) <= line {
            out.push(Block { kind, header, open, close: ln });
        }
    }
    out.sort_by_key(|b| std::cmp::Reverse((b.header.min(b.open), u32::MAX - b.close)));
    out
}

/// What the header `text[from..to]` (before a `{` on line `open_line`)
/// opens, and the line its first token is on (attributes and doc comments
/// left out).
fn block_header(text: &str, from: usize, to: usize, open_line: u32) -> (BlockKind, u32) {
    let raw = &text[from..to];
    // the line `from` is on
    let mut line = open_line - raw.matches('\n').count() as u32;
    let mut rest = raw;
    // skip whitespace, comments, attributes (balanced brackets)
    loop {
        let trimmed = rest.trim_start();
        line += rest[..rest.len() - trimmed.len()].matches('\n').count() as u32;
        rest = trimmed;
        if rest.starts_with("//") {
            let end = rest.find('\n').unwrap_or(rest.len());
            rest = &rest[end..];
            continue;
        }
        if rest.starts_with('#') {
            let mut depth = 0i32;
            let mut end = rest.len();
            for (k, ch) in rest.char_indices() {
                match ch {
                    '[' => depth += 1,
                    ']' => {
                        depth -= 1;
                        if depth == 0 {
                            end = k + 1;
                            break;
                        }
                    }
                    _ => {}
                }
            }
            line += rest[..end].matches('\n').count() as u32;
            rest = &rest[end..];
            continue;
        }
        break;
    }
    let words: Vec<&str> = rest.split(|c: char| !(c == '_' || c.is_alphanumeric() || c == '!')).filter(|w| !w.is_empty()).take(8).collect();
    let mut k = 0;
    while k < words.len() && matches!(words[k], "pub" | "crate" | "super" | "self" | "in" | "unsafe" | "default" | "async" | "const" | "extern" | "C" | "auto") {
        k += 1;
    }
    let kind = match words.get(k).copied() {
        Some("impl") => BlockKind::Impl,
        Some("trait") => BlockKind::Trait,
        Some("mod") => BlockKind::Mod,
        Some("fn") => BlockKind::Fn,
        Some("struct" | "enum" | "union" | "macro_rules!") => BlockKind::OtherItem,
        _ if words.first() == Some(&"extern") => BlockKind::OtherItem,
        _ => BlockKind::Expr,
    };
    (kind, line)
}

/// What a `cfg` attribute line gates.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CfgTarget {
    /// `#![cfg(…)]`: the whole module (file) it stands in.
    Inner,
    /// The item that starts on this line.
    Item(u32),
    /// `mod name;`: the module's own file.
    ModFile(String),
    /// A `use` / `extern crate`.
    Use,
    /// A statement / expression inside a body: the item that holds it.
    Body,
}

/// **What the `cfg` attribute on `line` (1-based) of `lines` gates**: the
/// first line after it (and after the attributes and comments that follow
/// it) that is not one.
pub fn cfg_target(lines: &[&str], line: u32) -> CfgTarget {
    let at = lines.get(line as usize - 1).copied().unwrap_or("").trim();
    if at.starts_with("#![") {
        return CfgTarget::Inner;
    }
    // the attribute's text may close on this line with the item after it
    let mut depth = 0i32;
    let mut after: Option<(&str, u32)> = None;
    let mut n = line;
    let mut cur = at;
    'scan: loop {
        let mut rest_from = None;
        for (k, ch) in cur.char_indices() {
            match ch {
                '[' => depth += 1,
                ']' => {
                    depth -= 1;
                    if depth == 0 {
                        rest_from = Some(k + 1);
                    }
                }
                _ => {}
            }
        }
        if depth <= 0 {
            depth = 0;
            if let Some(r) = rest_from {
                let rest = cur[r..].trim();
                if !rest.is_empty() && !rest.starts_with("//") && !rest.starts_with('#') {
                    after = Some((rest, n));
                    break 'scan;
                }
            }
            // the next line that is neither blank, a comment nor an attribute
            loop {
                n += 1;
                let Some(l) = lines.get(n as usize - 1) else { break 'scan };
                let t = l.trim();
                if t.is_empty() || t.starts_with("//") {
                    continue;
                }
                if t.starts_with('#') {
                    cur = t;
                    continue 'scan;
                }
                after = Some((t, n));
                break 'scan;
            }
        }
        n += 1;
        let Some(l) = lines.get(n as usize - 1) else { break };
        cur = l.trim();
    }
    let Some((t, n)) = after else { return CfgTarget::Body };
    let bare = strip_vis(t);
    if bare.starts_with("use ") || bare.starts_with("extern crate ") {
        return CfgTarget::Use;
    }
    if let Some(m) = bare.strip_prefix("mod ")
        && let Some(name) = m.trim().strip_suffix(';')
    {
        return CfgTarget::ModFile(name.trim().to_string());
    }
    let first = bare.split(|c: char| !(c == '_' || c.is_alphanumeric() || c == '!')).find(|w| !w.is_empty()).unwrap_or("");
    if matches!(first, "fn" | "async" | "const" | "unsafe" | "extern" | "static" | "struct" | "enum" | "union" | "trait" | "impl" | "type" | "mod" | "macro_rules!" | "default")
        || t.starts_with("pub ")
        || t.starts_with("pub(")
    {
        return CfgTarget::Item(n);
    }
    CfgTarget::Body
}

fn tree_of(repo: &gix::Repository, sha: &str) -> Result<gix::ObjectId> {
    let id = repo.rev_parse_single(sha).map_err(|e| anyhow!("`{sha}` names no commit here: {e}"))?;
    let commit = id.object().context("read the commit")?.try_into_commit().map_err(|e| anyhow!("`{sha}` is no commit: {e}"))?;
    Ok(commit.tree_id().map_err(|e| anyhow!("the tree of `{sha}`: {e}"))?.detach())
}

/// **What changed from `from` to `to`** in the repo at `dir`: every file
/// added, deleted or modified (a rename is a deletion and an addition), with
/// the touched lines of each `.rs` file's newer side.
pub fn commit_diff(dir: &Path, from: &str, to: &str) -> Result<Vec<FileDiff>> {
    let repo = gix::open(dir).with_context(|| format!("open {}", dir.display()))?;
    let (a, b) = (repo.find_tree(tree_of(&repo, from)?)?, repo.find_tree(tree_of(&repo, to)?)?);
    let changes = repo.diff_tree_to_tree(&a, &b, gix::diff::Options::default()).context("tree diff")?;
    let mut out = Vec::new();
    for c in changes {
        use gix::object::tree::diff::ChangeDetached as C;
        let (path, change, old, new, blob) = match &c {
            C::Addition { location, entry_mode, id, .. } => (location.to_string(), FileChange::Added, None, Some(*id), entry_mode.is_blob()),
            C::Deletion { location, entry_mode, id, .. } => (location.to_string(), FileChange::Deleted, Some(*id), None, entry_mode.is_blob()),
            C::Modification { location, entry_mode, previous_id, id, .. } => (location.to_string(), FileChange::Modified, Some(*previous_id), Some(*id), entry_mode.is_blob()),
            C::Rewrite { location, entry_mode, id, .. } => (location.to_string(), FileChange::Added, None, Some(*id), entry_mode.is_blob()),
        };
        if !blob {
            continue;
        }
        let lines = if path.ends_with(".rs") && change != FileChange::Deleted {
            let read = |id: Option<gix::ObjectId>| -> Result<Vec<u8>> {
                match id {
                    Some(id) => Ok(repo.find_object(id).with_context(|| format!("blob of {path}"))?.data.clone()),
                    None => Ok(Vec::new()),
                }
            };
            touched_lines(&read(old)?, &read(new)?)
        } else {
            Vec::new()
        };
        out.push(FileDiff { path, change, lines });
    }
    out.sort_by(|x, y| x.path.cmp(&y.path));
    Ok(out)
}

/// The newer side's touched lines of a line diff of `before` → `after`
/// (1-based, inclusive, merged). A pure removal touches the line before the
/// gap and the line after it.
pub fn touched_lines(before: &[u8], after: &[u8]) -> Vec<(u32, u32)> {
    use gix::diff::blob::{Algorithm, Diff, InternedInput};
    let input = InternedInput::new(before, after);
    let mut diff = Diff::compute(Algorithm::Histogram, &input);
    diff.postprocess_lines(&input);
    let total = after.split(|b| *b == b'\n').count() as u32;
    let mut ranges: Vec<(u32, u32)> = Vec::new();
    for h in diff.hunks() {
        let (a, b) = (h.after.start, h.after.end);
        let r = if a < b { (a + 1, b) } else { (a.max(1), (a + 1).min(total.max(1))) };
        ranges.push(r);
    }
    merge(ranges)
}

fn merge(mut r: Vec<(u32, u32)>) -> Vec<(u32, u32)> {
    r.sort_unstable();
    let mut out: Vec<(u32, u32)> = Vec::with_capacity(r.len());
    for (a, b) in r {
        match out.last_mut() {
            Some(last) if a <= last.1 + 1 => last.1 = last.1.max(b),
            _ => out.push((a, b.max(a))),
        }
    }
    out
}

/// What a changed path means for the selection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathRule {
    /// Rust source: its hunks are placed on the room's items.
    Rust,
    /// The graph cannot see through it: every test runs, and why.
    All(String),
    /// Not Rust: inert only if no `.rs` file names it; a doc-like file that
    /// no `.rs` file names is inert, anything else that none names runs all
    /// (a test may read it at run time).
    Other { doc_like: bool },
}

/// **Classify a changed path** (repo-relative).
pub fn classify_path(path: &str) -> PathRule {
    let name = path.rsplit('/').next().unwrap_or(path);
    let all = |what: &str| PathRule::All(format!("{path} changed — {what}: the graph cannot see what it changes, every test runs"));
    if name == "Cargo.toml" {
        return all("a manifest (features, dependencies, targets)");
    }
    if name == "Cargo.lock" {
        return all("the lock file (a dependency moved)");
    }
    if name == "build.rs" {
        return all("a build script");
    }
    if name.starts_with("rust-toolchain") {
        return all("the toolchain pin");
    }
    if path.starts_with(".cargo/") || path.contains("/.cargo/") {
        return all("cargo configuration");
    }
    if path.starts_with(".config/") && name.starts_with("nextest") {
        return all("the nextest configuration");
    }
    if name.ends_with(".rs") {
        return PathRule::Rust;
    }
    let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase()).unwrap_or_default();
    let doc_like = matches!(ext.as_str(), "md" | "markdown" | "txt" | "rst" | "adoc" | "png" | "jpg" | "jpeg" | "gif" | "svg" | "ico" | "pdf" | "drawio")
        || name.starts_with("LICENSE")
        || name.starts_with("COPYING")
        || matches!(name, ".gitignore" | ".gitattributes" | ".mailmap")
        || path.starts_with(".github/")
        || path.starts_with(".forgejo/")
        || path.starts_with(".gitea/")
        || path.starts_with(".nornir/");
    PathRule::Other { doc_like }
}

/// What a changed line that no item of the room holds is.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineKind {
    Blank,
    Comment,
    /// `use …;` / `pub use …;` / `extern crate …;` — a name moved; the
    /// graph's own diff (an edge that now points elsewhere) carries it.
    Use,
    /// `mod x;` — the module's items are positioned in their own file.
    ModDecl,
    /// An attribute line (`#[…]`) that is not a `cfg`.
    Attr,
    /// A `cfg` (`#[cfg(…)]`, `#![cfg(…)]`, `#[cfg_attr(…)]`): the set of
    /// compiled items moves with it.
    Cfg,
    /// A brace or a lone delimiter (the end of an item the room spans
    /// differently).
    Delimiter,
    /// Anything else: code the graph does not place.
    Code,
}

/// **Classify one source line** that no item holds.
pub fn line_kind(text: &str) -> LineKind {
    let t = text.trim();
    if t.is_empty() {
        return LineKind::Blank;
    }
    if t.starts_with("//") || t.starts_with("/*") || t.starts_with('*') || t.ends_with("*/") {
        return LineKind::Comment;
    }
    if t.starts_with("#[cfg") || t.starts_with("#![cfg") || t.contains("cfg_attr(") || t.starts_with("#[cfg_attr") {
        return LineKind::Cfg;
    }
    if t.starts_with("#[") || t.starts_with("#![") {
        return LineKind::Attr;
    }
    let bare = strip_vis(t);
    if bare.starts_with("use ") || bare.starts_with("extern crate ") {
        return LineKind::Use;
    }
    if (bare.starts_with("mod ") && bare.ends_with(';')) || bare.starts_with("mod ") && !bare.contains('{') {
        return LineKind::ModDecl;
    }
    if t.chars().all(|c| matches!(c, '{' | '}' | '(' | ')' | '[' | ']' | ';' | ',' | ' ')) {
        return LineKind::Delimiter;
    }
    LineKind::Code
}

fn strip_vis(t: &str) -> &str {
    let t = t.trim_start();
    if let Some(rest) = t.strip_prefix("pub") {
        let rest = rest.trim_start();
        if let Some(r) = rest.strip_prefix('(')
            && let Some(close) = r.find(')')
        {
            return r[close + 1..].trim_start();
        }
        return rest;
    }
    t
}

/// One identifier of a source text and what surrounds it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ident<'a> {
    pub name: &'a str,
    /// 1-based line.
    pub line: u32,
    /// The path segment before it (`Point` in `Point::new`), if any.
    pub qualifier: Option<&'a str>,
    /// The next non-space character after it.
    pub next: Option<char>,
    /// The word before it (`fn` in `fn new`), if any.
    pub prev_word: Option<&'a str>,
}

/// **Every identifier of `text`** outside comments and string literals, in
/// order. A lexer for one purpose (finding where a name is used), not Rust's.
pub fn identifiers(text: &str) -> Vec<Ident<'_>> {
    let b = text.as_bytes();
    let mut out: Vec<Ident<'_>> = Vec::new();
    let (mut i, mut line) = (0usize, 1u32);
    let is_start = |c: u8| c == b'_' || c.is_ascii_alphabetic();
    let is_cont = |c: u8| c == b'_' || c.is_ascii_alphanumeric();
    while i < b.len() {
        let c = b[i];
        match c {
            b'\n' => {
                line += 1;
                i += 1;
            }
            b'/' if b.get(i + 1) == Some(&b'/') => {
                while i < b.len() && b[i] != b'\n' {
                    i += 1;
                }
            }
            b'/' if b.get(i + 1) == Some(&b'*') => {
                let mut depth = 1;
                i += 2;
                while i < b.len() && depth > 0 {
                    if b[i] == b'\n' {
                        line += 1;
                    }
                    if b[i] == b'/' && b.get(i + 1) == Some(&b'*') {
                        depth += 1;
                        i += 1;
                    } else if b[i] == b'*' && b.get(i + 1) == Some(&b'/') {
                        depth -= 1;
                        i += 1;
                    }
                    i += 1;
                }
            }
            b'"' => {
                i += 1;
                while i < b.len() && b[i] != b'"' {
                    if b[i] == b'\\' {
                        // an escaped newline (a string continued on the next line) is a line too
                        if b.get(i + 1) == Some(&b'\n') {
                            line += 1;
                        }
                        i += 1;
                    } else if b[i] == b'\n' {
                        line += 1;
                    }
                    i += 1;
                }
                i += 1;
            }
            b'r' if b.get(i + 1).is_some_and(|n| *n == b'#' || *n == b'"') && (i == 0 || !is_cont(b[i - 1])) => {
                // raw string r#"…"#
                let mut j = i + 1;
                let mut hashes = 0;
                while b.get(j) == Some(&b'#') {
                    hashes += 1;
                    j += 1;
                }
                if b.get(j) != Some(&b'"') {
                    i += 1;
                    continue;
                }
                j += 1;
                loop {
                    if j >= b.len() {
                        break;
                    }
                    if b[j] == b'\n' {
                        line += 1;
                    }
                    if b[j] == b'"' && b[j + 1..].iter().take(hashes).filter(|x| **x == b'#').count() == hashes && b.len() >= j + 1 + hashes {
                        j += 1 + hashes;
                        break;
                    }
                    j += 1;
                }
                i = j;
            }
            b'\'' => {
                // a char literal ('a', '\n') — a lifetime ('a) is skipped as one too
                if b.get(i + 2) == Some(&b'\'') {
                    i += 3;
                } else if b.get(i + 1) == Some(&b'\\') {
                    i += 2;
                    while i < b.len() && b[i] != b'\'' {
                        i += 1;
                    }
                    i += 1;
                } else {
                    i += 1;
                    while i < b.len() && is_cont(b[i]) {
                        i += 1;
                    }
                }
            }
            c if is_start(c) => {
                let s = i;
                while i < b.len() && is_cont(b[i]) {
                    i += 1;
                }
                let name = &text[s..i];
                let next = text[i..].chars().find(|c| !c.is_whitespace());
                let before = text[..s].trim_end();
                let qualifier = before.strip_suffix("::").map(|q| {
                    let q = q.trim_end();
                    &q[word_start(q)..]
                });
                let prev_word = (!before.ends_with("::")).then(|| {
                    &before[word_start(before)..]
                });
                out.push(Ident { name, line, qualifier: qualifier.filter(|q| !q.is_empty()), next, prev_word: prev_word.filter(|w| !w.is_empty()) });
            }
            c if c.is_ascii_digit() => {
                while i < b.len() && is_cont(b[i]) {
                    i += 1;
                }
            }
            _ => i += 1,
        }
    }
    out
}

/// Where the trailing identifier of `s` starts: just past the last character
/// that is no identifier character (its whole width — a `─` is three bytes).
fn word_start(s: &str) -> usize {
    s.char_indices()
        .rev()
        .find(|(_, c)| !(*c == '_' || c.is_ascii_alphanumeric()))
        .map_or(0, |(p, c)| p + c.len_utf8())
}

/// A name to look for as a VALUE (a fn passed on, not called), and what
/// qualifies it when it is not unique.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Wanted {
    pub name: String,
    /// The name of its parent (type, trait or module): `Point` for
    /// `Point::new`. A use qualified by it counts always.
    pub parent: String,
    /// No other fn of the repo has this name: an unqualified use counts too.
    pub unique: bool,
}

/// **Where a wanted fn is used as a value** in `files` (path → text): an
/// identifier equal to its name that is not called (`f(` / `f::<T>(`),
/// not a macro (`f!`), and not its own definition (`fn f`). Returns
/// `(file, line, name)`.
pub fn mentions(files: &BTreeMap<String, String>, wanted: &[Wanted]) -> Vec<(String, u32, String)> {
    let by_name: BTreeMap<&str, Vec<&Wanted>> = wanted.iter().fold(BTreeMap::new(), |mut m, w| {
        m.entry(w.name.as_str()).or_default().push(w);
        m
    });
    let mut out: BTreeSet<(String, u32, String)> = BTreeSet::new();
    for (path, text) in files {
        // the cheap prefilter (few names): a file that holds none of them
        if by_name.len() <= 32 && !by_name.keys().any(|n| text.contains(n)) {
            continue;
        }
        for id in identifiers(text) {
            let Some(ws) = by_name.get(id.name) else { continue };
            if matches!(id.next, Some('(') | Some('!')) || id.prev_word == Some("fn") {
                continue;
            }
            // `f::<T>(…)` is a call too
            if id.next == Some(':') && text_after_is_turbofish_call(text, &id) {
                continue;
            }
            if ws.iter().any(|w| w.unique || id.qualifier == Some(w.parent.as_str())) {
                out.insert((path.clone(), id.line, id.name.to_string()));
            }
        }
    }
    out.into_iter().collect()
}

fn text_after_is_turbofish_call(text: &str, id: &Ident<'_>) -> bool {
    // find the identifier's line and look for `name::<…>(`
    let line = text.lines().nth(id.line as usize - 1).unwrap_or("");
    line.contains(&format!("{}::<", id.name)) && line.split(&format!("{}::<", id.name)).skip(1).all(|rest| {
        let mut depth = 1i32;
        let mut it = rest.chars();
        for c in it.by_ref() {
            match c {
                '<' => depth += 1,
                '>' => {
                    depth -= 1;
                    if depth == 0 {
                        break;
                    }
                }
                _ => {}
            }
        }
        it.as_str().trim_start().starts_with('(')
    })
}

/// **The name a test runner prints for a test**: its path inside its crate
/// — the module path without the crate segment, then the name
/// (`cart::tests::adds_up`; a test at the crate root is its bare name). The
/// spelling `--skip <name> --exact` matches.
pub fn runner_name(module_path: &str, name: &str) -> String {
    match module_path.split_once("::") {
        Some((_, m)) if !m.is_empty() => format!("{m}::{name}"),
        _ => name.to_string(),
    }
}

/// Does a test's own file spawn the crate's binary (`CARGO_BIN_EXE_…`,
/// `assert_cmd`, `cargo_bin(`)? Then its reach crosses a process boundary
/// the graph does not walk: it runs whenever any Rust code changed.
pub fn spawns_binary(text: &str) -> bool {
    text.contains("CARGO_BIN_EXE_") || text.contains("assert_cmd") || text.contains("cargo_bin(") || text.contains("escargot")
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn blocks_name_their_kind_header_and_braces() {
        let src = "//! doc\nimpl<T> Tr for X<T>\nwhere T: Copy {\n    fn f() {\n        let s = \"{\";\n    }\n}\n\n#[cfg(test)]\n// a comment\nmod tests {\n    #[test]\n    fn t() { if true { } }\n}\n";
        let at = |l: u32| enclosing_blocks(src, l).iter().map(|b| (b.kind, b.header, b.open, b.close)).collect::<Vec<_>>();
        // the where clause: inside the impl's header, before its brace
        assert_eq!(at(3), vec![(BlockKind::Impl, 2, 3, 7)]);
        assert_eq!(at(5), vec![(BlockKind::Fn, 4, 4, 6), (BlockKind::Impl, 2, 3, 7)]);
        assert_eq!(at(11), vec![(BlockKind::Mod, 11, 11, 14)]);
        assert_eq!(at(13)[0].0, BlockKind::Expr);
        assert_eq!(at(13)[1].0, BlockKind::Fn);
        assert!(at(8).is_empty());
        // a string continued over a newline keeps the line count
        let cont = "fn a() {\n    let s = \"x\\\n    y\";\n}\nimpl Z {\n}\n";
        assert_eq!(enclosing_blocks(cont, 5)[0].kind, BlockKind::Impl);
    }

    #[test]
    fn a_cfg_stands_on_what_follows_it() {
        let lines = [
            "#[cfg(feature = \"x\")]",
            "/// doc",
            "#[inline]",
            "",
            "pub(crate) async fn go() {}",
            "#[cfg(test)]",
            "mod tests;",
            "#[cfg_attr(",
            "    x, derive(Debug)",
            ")]",
            "struct S;",
            "#![cfg(unix)]",
            "    #[cfg(feature = \"on\")]",
            "    {",
            "#[cfg(x)]",
            "use a::b;",
            "#[cfg(x)] fn same_line() {}",
        ];
        assert_eq!(cfg_target(&lines, 1), CfgTarget::Item(5));
        assert_eq!(cfg_target(&lines, 6), CfgTarget::ModFile("tests".into()));
        assert_eq!(cfg_target(&lines, 8), CfgTarget::Item(11));
        assert_eq!(cfg_target(&lines, 12), CfgTarget::Inner);
        assert_eq!(cfg_target(&lines, 13), CfgTarget::Body);
        assert_eq!(cfg_target(&lines, 15), CfgTarget::Use);
        assert_eq!(cfg_target(&lines, 17), CfgTarget::Item(17));
    }

    #[test]
    fn a_file_belongs_to_the_nearest_package_above_it() {
        let manifests: BTreeMap<String, String> = [
            ("Cargo.toml", "[workspace]\nmembers = [\"crates/x\"]\n[package]\nname = \"root\"\n"),
            ("crates/x/Cargo.toml", "[package]\nname = \"x-y\"\nversion = \"1.0.0\"\n[dependencies]\nroot = { path = \"../..\" }\n[dev-dependencies]\nz = { package = \"zed\", version = \"1\" }\n"),
            ("ws/Cargo.toml", "[workspace]\n"),
        ]
        .into_iter()
        .map(|(a, b)| (a.to_string(), b.to_string()))
        .collect();
        let p = Packages::from_manifests(&manifests);
        assert_eq!(p.of_file("crates/x/src/a/b.rs"), Some(("crates/x", "x-y")));
        assert_eq!(p.of_file("src/lib.rs"), Some(("", "root")));
        // a virtual manifest is no package: the root's is the next one up
        assert_eq!(p.of_file("ws/src/lib.rs"), Some(("", "root")));
        assert_eq!(p.dir_of("x_y"), Some("crates/x"));
        assert!(is_non_lib_target("crates/x", "crates/x/examples/e.rs"));
        assert!(is_non_lib_target("crates/x", "crates/x/src/bin/tool.rs"));
        assert!(is_non_lib_target("", "tests/it.rs"));
        assert!(!is_non_lib_target("crates/x", "crates/x/src/lib.rs"));
        let deps = manifest_deps(&manifests);
        let x = deps.iter().find(|d| d.name == "x-y").unwrap();
        assert_eq!(x.deps, vec!["root".to_string(), "zed".to_string()]);
        // root changed → x-y depends on it
        assert_eq!(lock_dependents(&[&deps], &["root".to_string()].into()), ["root".to_string(), "x-y".to_string()].into());
    }

    #[test]
    fn a_lock_change_reaches_the_packages_that_depend_on_what_moved() {
        let lock = |zed: &str, yon: &str| {
            format!(
                "version = 4\n\n[[package]]\nname = \"a\"\nversion = \"0.1.0\"\ndependencies = [\"zed\"]\n\n[[package]]\nname = \"b-c\"\nversion = \"0.2.0\"\ndependencies = [\"a\", \"yon 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)\"]\n\n[[package]]\nname = \"lone\"\nversion = \"0.1.0\"\n\n[[package]]\nname = \"yon\"\nversion = \"{yon}\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n\n[[package]]\nname = \"zed\"\nversion = \"{zed}\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n"
            )
        };
        let reach = |a: &str, b: &str| -> BTreeSet<String> {
            let (a, b) = (parse_lock(a).unwrap(), parse_lock(b).unwrap());
            lock_dependents(&[&a, &b], &lock_moved(&a, &b))
        };
        let base = lock("1.0.0", "1.0.0");
        assert!(reach(&base, &base).is_empty());
        assert_eq!(reach(&base, &lock("1.0.0", "1.0.1")), ["b-c".to_string(), "yon".to_string()].into());
        assert_eq!(reach(&base, &lock("1.1.0", "1.0.0")), ["a".to_string(), "b-c".to_string(), "zed".to_string()].into());
        // a package that went: who depended on it before is reached
        let gone = base.replace("dependencies = [\"zed\"]\n", "").replace("[[package]]\nname = \"zed\"\nversion = \"1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n", "");
        assert_eq!(reach(&base, &gone), ["a".to_string(), "b-c".to_string(), "zed".to_string()].into());
        assert!(parse_lock("[[package]\nname=").is_err());
    }

    #[test]
    fn a_string_that_names_a_package_is_found() {
        let files: BTreeMap<String, String> = [(
            "src/f.rs".to_string(),
            "fn dir() -> String {\n    concat!(env!(\"X\"), \"/crates/mimir-driver\").into()\n}\n// crates/mimir-driver in a comment\nfn no() { \"mimir-driver-2\"; }\nfn raw() { br#\"run mimir-driver\"#; }\n".to_string(),
        )]
        .into();
        let terms = vec!["crates/mimir-driver".to_string(), "mimir-driver".to_string()];
        assert_eq!(named_in_strings(&files, &terms), vec![("src/f.rs".to_string(), 2), ("src/f.rs".to_string(), 6)]);
    }

    #[test]
    fn a_hunk_touches_its_new_lines_and_a_removal_the_lines_around_it() {
        let before = "a\nb\nc\nd\ne\n";
        // c changed, e removed
        let after = "a\nb\nC\nd\n";
        assert_eq!(touched_lines(before.as_bytes(), after.as_bytes()), vec![(3, 5)]);
        // only a removal between b and d
        assert_eq!(touched_lines(b"a\nb\nc\nd\n", b"a\nb\nd\n"), vec![(2, 3)]);
        // an added line
        assert_eq!(touched_lines(b"a\nb\n", b"a\nx\nb\n"), vec![(2, 2)]);
        assert!(touched_lines(b"a\n", b"a\n").is_empty());
    }

    #[test]
    fn manifests_lock_build_scripts_and_toolchains_run_every_test() {
        for p in ["Cargo.toml", "crates/a/Cargo.toml", "Cargo.lock", "a/build.rs", "rust-toolchain.toml", ".cargo/config.toml", ".config/nextest.toml"] {
            assert!(matches!(classify_path(p), PathRule::All(_)), "{p}");
        }
        assert_eq!(classify_path("src/lib.rs"), PathRule::Rust);
        assert_eq!(classify_path("README.md"), PathRule::Other { doc_like: true });
        assert_eq!(classify_path("tests/fixtures/a.json"), PathRule::Other { doc_like: false });
        let PathRule::All(why) = classify_path("Cargo.toml") else { unreachable!() };
        assert!(why.starts_with("Cargo.toml changed"), "{why}");
    }

    #[test]
    fn unplaced_lines_are_told_apart() {
        assert_eq!(line_kind("   "), LineKind::Blank);
        assert_eq!(line_kind("// note"), LineKind::Comment);
        assert_eq!(line_kind("pub(crate) use a::b;"), LineKind::Use);
        assert_eq!(line_kind("mod inner;"), LineKind::ModDecl);
        assert_eq!(line_kind("#[cfg(feature = \"x\")]"), LineKind::Cfg);
        assert_eq!(line_kind("#![cfg_attr(docsrs, feature(doc_cfg))]"), LineKind::Cfg);
        assert_eq!(line_kind("#[derive(Debug)]"), LineKind::Attr);
        assert_eq!(line_kind("}"), LineKind::Delimiter);
        assert_eq!(line_kind("static X: u8 = 1;"), LineKind::Code);
    }

    #[test]
    fn a_fn_used_as_a_value_is_found_and_a_call_is_not() {
        let src = "fn helper(x: u8) -> u8 { x }\n\
                   fn a() { helper(1); }\n\
                   fn b() { [1u8].map(helper); }\n\
                   fn c() { let s = \"helper\"; } // helper\n\
                   fn d() { [1].map(Point::new); [2].map(Other::new); new(); }\n\
                   fn e() { helper::<u8>(1); }\n";
        let files: BTreeMap<String, String> = [("src/lib.rs".to_string(), src.to_string())].into();
        let wanted = [
            Wanted { name: "helper".into(), parent: "lib".into(), unique: true },
            Wanted { name: "new".into(), parent: "Point".into(), unique: false },
        ];
        let got = mentions(&files, &wanted);
        assert_eq!(got, vec![("src/lib.rs".to_string(), 3, "helper".to_string()), ("src/lib.rs".to_string(), 5, "new".to_string())]);
    }

    /// A name right after a non-ASCII character (a box-drawing `─` in a
    /// banner, an `é` in a path segment) is read, not a panic on a char
    /// boundary — measured on nornir's own sources, 2026-10-17.
    #[test]
    fn a_name_after_a_non_ascii_character_is_read() {
        let ids = identifiers("─f\nv─x::g\n");
        let got: Vec<_> = ids.iter().map(|i| (i.name, i.qualifier, i.prev_word)).collect();
        assert_eq!(got, vec![("f", None, None), ("v", None, Some("f")), ("x", None, None), ("g", Some("x"), None)]);
    }

    #[test]
    fn the_runner_prints_a_test_without_its_crate() {
        assert_eq!(runner_name("a::tests", "adds"), "tests::adds");
        assert_eq!(runner_name("cart::checkout::tests", "pays"), "checkout::tests::pays");
        assert_eq!(runner_name("ingest", "works"), "works");
        assert_eq!(runner_name("", "works"), "works");
    }

    #[test]
    fn a_scratch_repo_diff_names_files_and_touched_lines() {
        let dir = tempfile::tempdir().unwrap();
        let d = dir.path();
        crate::gitio::init(d).unwrap();
        std::fs::create_dir_all(d.join("src")).unwrap();
        std::fs::write(d.join("src/lib.rs"), "fn a() {\n    1;\n}\n\nfn b() {\n    2;\n}\n").unwrap();
        std::fs::write(d.join("README.md"), "x\n").unwrap();
        let c1 = crate::gitio::commit_all(d, "c1").unwrap();
        std::fs::write(d.join("src/lib.rs"), "fn a() {\n    1;\n}\n\nfn b() {\n    3;\n}\n").unwrap();
        std::fs::write(d.join("Cargo.toml"), "[package]\n").unwrap();
        std::fs::remove_file(d.join("README.md")).unwrap();
        let c2 = crate::gitio::commit_all(d, "c2").unwrap();
        let diff = commit_diff(d, &c1, &c2).unwrap();
        let got: Vec<(&str, FileChange, Vec<(u32, u32)>)> = diff.iter().map(|f| (f.path.as_str(), f.change, f.lines.clone())).collect();
        assert_eq!(
            got,
            vec![("Cargo.toml", FileChange::Added, vec![]), ("README.md", FileChange::Deleted, vec![]), ("src/lib.rs", FileChange::Modified, vec![(6, 6)])]
        );
        let snap = Snapshot::at(d, &c2).unwrap();
        assert!(snap.text("src/lib.rs").unwrap().contains("3;"));
        assert_eq!(snap.rust_files().keys().collect::<Vec<_>>(), vec!["src/lib.rs"]);
    }
}