use cli::ingest_git::{run_ingest_git, IngestGitOpts};
use core_api::{Direction, GraphDb, Value};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
static SEQ: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
fn tmp(name: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let seq = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let d = std::env::temp_dir().join(format!(
"mushroomdb-structure-{name}-{}-{nanos}-{seq}",
std::process::id()
));
std::fs::create_dir_all(&d).unwrap();
d
}
fn git(repo: &Path, args: &[&str]) {
let st = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.env("GIT_AUTHOR_DATE", "2026-01-01T00:00:00Z")
.env("GIT_COMMITTER_DATE", "2026-01-01T00:00:00Z")
.status()
.unwrap();
assert!(st.success(), "git {args:?} failed");
}
fn write_files(repo: &Path, files: &[(&str, &str)]) {
for (p, body) in files {
let full = repo.join(p);
std::fs::create_dir_all(full.parent().unwrap()).unwrap();
std::fs::write(full, body).unwrap();
}
}
fn commit_all(repo: &Path, msg: &str) {
git(repo, &["add", "-A"]);
git(
repo,
&[
"-c",
"user.name=alice",
"-c",
"user.email=alice@x.test",
"commit",
"-q",
"-m",
msg,
],
);
}
fn commit(repo: &Path, msg: &str, files: &[(&str, &str)]) {
write_files(repo, files);
commit_all(repo, msg);
}
const LIB_RS: &str = "//! Demo crate root.
mod net;
mod util;
use crate::util::helper;
/// A record kept by the crate.
pub struct Record {
pub id: u32,
}
/// Run the demo.
pub fn run() -> u32 {
helper(1)
}
";
const UTIL_RS: &str = "//! Shared helpers.
/// Double a value.
pub fn helper(n: u32) -> u32 {
n * 2
}
";
const NET_RS: &str = "//! Networking.
use crate::util::helper;
/// Open a connection.
pub fn connect(port: u32) -> u32 {
helper(port)
}
";
const NET_RS_NO_IMPORT: &str = "//! Networking.
/// Open a connection.
pub fn connect(port: u32) -> u32 {
helper(port)
}
";
const GUIDE_MD: &str = "# Demo guide
The networking layer lives in `src/net.rs` and is quicksilver by design.
## Wiring
See [the helpers](../src/util.rs) for the doubling routine.
";
fn seed_repo() -> PathBuf {
let repo = tmp("repo");
git(&repo, &["init", "-q", "-b", "main"]);
commit(
&repo,
"demo crate",
&[
("Cargo.toml", "[package]\nname = \"demo\"\n"),
("src/lib.rs", LIB_RS),
("src/util.rs", UTIL_RS),
("src/net.rs", NET_RS),
("docs/guide.md", GUIDE_MD),
],
);
repo
}
fn opts(repo: &Path) -> IngestGitOpts {
IngestGitOpts {
repo: repo.to_path_buf(),
exclude: vec![],
max_commits_per_file: 200,
recurse_submodules: false,
prs: false,
structure: true,
docs: true,
ensure_gitignore: false,
}
}
fn prop(db: &cli::structure::Db, key: &str, field: &str) -> Option<Value> {
db.node_ref(key).and_then(|n| n.prop(field))
}
fn strings(v: Option<Value>) -> Vec<String> {
match v {
Some(Value::List(l)) => l
.into_iter()
.filter_map(|x| match x {
Value::Str(s) => Some(s),
_ => None,
})
.collect(),
_ => Vec::new(),
}
}
fn out(db: &cli::structure::Db, key: &str, edge: &str) -> Vec<String> {
let mut v = db.neighbors(key, edge, Direction::Out).unwrap_or_default();
v.sort();
v
}
fn all_keys(db: &cli::structure::Db, label: &str) -> Vec<String> {
let rs = db
.query(
&format!("MATCH (n:{label}) RETURN n.id AS id"),
&BTreeMap::new(),
)
.unwrap();
let mut v: Vec<String> = (0..rs.len())
.filter_map(|i| match rs.get(i, "id") {
Some(Value::Str(s)) => Some(s.clone()),
_ => None,
})
.collect();
v.sort();
v
}
#[test]
fn calls_resolve_across_crates_via_imports() {
let repo = tmp("repo");
git(&repo, &["init", "-q", "-b", "main"]);
commit(
&repo,
"workspace",
&[
("Cargo.toml", "[workspace]\nmembers = [\"crates/*\"]\n"),
("crates/alpha/Cargo.toml", "[package]\nname = \"alpha\"\n"),
("crates/alpha/src/lib.rs", "pub mod render;\n"),
(
"crates/alpha/src/render.rs",
"/// Strip control characters.\npub fn sanitize(s: &str) -> String {\n s.to_string()\n}\n",
),
("crates/beta/Cargo.toml", "[package]\nname = \"beta\"\n"),
(
"crates/beta/src/lib.rs",
"/// A different escape entirely.\npub fn sanitize(s: &str) -> String {\n s.to_string()\n}\n",
),
("crates/gamma/Cargo.toml", "[package]\nname = \"gamma\"\n"),
(
"crates/gamma/src/lib.rs",
"use alpha::render::sanitize;\n\n\
pub fn show(t: &str) -> String {\n \
let head = sanitize(t);\n \
format!(\"{} {}\", head, sanitize(t))\n}\n",
),
],
);
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
out(&db, "crates/gamma/src/lib.rs", "IMPORTS"),
vec!["crates/alpha/src/render.rs".to_string()]
);
assert_eq!(
out(&db, "crates/gamma/src/lib.rs#show", "CALLS"),
vec!["crates/alpha/src/render.rs#sanitize".to_string()],
"the imported definition wins; beta's same-named function is not it"
);
assert_eq!(
strings(prop(&db, "crates/gamma/src/lib.rs#show", "call_lines")),
vec![
"crates/alpha/src/render.rs#sanitize\t4".to_string(),
"crates/alpha/src/render.rs#sanitize\t5".to_string(),
]
);
assert!(
db.neighbors("crates/beta/src/lib.rs#sanitize", "CALLS", Direction::In)
.unwrap()
.is_empty(),
"nothing calls beta's sanitize"
);
}
#[test]
fn method_calls_do_not_reach_an_unrelated_function_of_the_same_name() {
let repo = tmp("repo");
git(&repo, &["init", "-q", "-b", "main"]);
commit(
&repo,
"workspace",
&[
("Cargo.toml", "[workspace]\nmembers = [\"crates/*\"]\n"),
("crates/helper/Cargo.toml", "[package]\nname = \"helper\"\n"),
(
"crates/helper/src/lib.rs",
"/// Gather the fixtures.\npub fn collect(dir: &str) -> Vec<String> {\n vec![dir.to_string()]\n}\n\n\
/// The only `take` in the tree.\npub fn take(n: usize) -> usize {\n n\n}\n",
),
("crates/app/Cargo.toml", "[package]\nname = \"app\"\n"),
(
"crates/app/src/lib.rs",
"/// A job the app runs.\npub struct Job;\n\n\
/// Do the work.\npub fn run(job: &Job) -> u32 {\n let _ = job;\n 7\n}\n\n\
pub fn total(items: &[u32], job: &Job) -> u32 {\n \
let picked: Vec<u32> = items.iter().copied().collect();\n \
let moved = std::mem::take(&mut picked.clone());\n \
picked.len() as u32 + moved.len() as u32 + job.run()\n}\n",
),
],
);
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert!(
db.has_node("crates/helper/src/lib.rs#collect"),
"the helper is in the graph; it is the edge to it that must not exist"
);
assert!(
db.neighbors("crates/helper/src/lib.rs#collect", "CALLS", Direction::In)
.unwrap()
.is_empty(),
"`.collect()` is a method on a type from outside the tree"
);
assert!(
db.neighbors("crates/helper/src/lib.rs#take", "CALLS", Direction::In)
.unwrap()
.is_empty(),
"a path call leading with `std` names nothing here"
);
assert_eq!(
out(&db, "crates/app/src/lib.rs#total", "CALLS"),
vec!["crates/app/src/lib.rs#run".to_string()]
);
}
#[test]
fn first_run_creates_symbols_imports_calls_and_mentions() {
let repo = seed_repo();
let db_dir = tmp("db");
let report = run_ingest_git(&db_dir, &opts(&repo)).unwrap();
assert!(report.structure.files_scanned >= 5, "{report:?}");
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
out(&db, "src/lib.rs", "IMPORTS"),
vec!["src/net.rs".to_string(), "src/util.rs".to_string()]
);
assert_eq!(
out(&db, "src/net.rs", "IMPORTS"),
vec!["src/util.rs".to_string()]
);
assert!(db.has_node("src/util.rs#helper"), "helper symbol missing");
assert_eq!(
prop(&db, "src/util.rs#helper", "kind"),
Some(Value::Str("function".into()))
);
assert_eq!(
prop(&db, "src/util.rs#helper", "file_id"),
Some(Value::Str("src/util.rs".into()))
);
assert_eq!(
prop(&db, "src/util.rs#helper", "doc"),
Some(Value::Str("Double a value.".into()))
);
assert_eq!(
out(&db, "src/util.rs#helper", "DEFINES"),
vec!["src/util.rs".to_string()]
);
assert_eq!(
out(&db, "src/net.rs#connect", "CALLS"),
vec!["src/util.rs#helper".to_string()]
);
assert_eq!(
out(&db, "docs/guide.md", "MENTIONS"),
vec!["src/net.rs".to_string(), "src/util.rs".to_string()]
);
assert_eq!(prop(&db, "src/util.rs", "symbols_n"), Some(Value::Int(1)));
assert_eq!(
prop(&db, "src/util.rs", "lang"),
Some(Value::Str("rust".into()))
);
let Some(Value::Str(hash)) = prop(&db, "src/util.rs", "hash") else {
panic!("no hash on src/util.rs");
};
assert_eq!(hash.len(), 32, "hash is 32 hex characters: {hash}");
let ex = db.explain("src/net.rs", "src/util.rs").unwrap();
assert!(
ex.iter()
.any(|e| e.rule == "imports" && e.edge_type == "IMPORTS"),
"explain must name the imports rule: {ex:?}"
);
}
#[test]
fn editing_an_import_retracts_the_edge_on_refresh() {
let repo = seed_repo();
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
{
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
out(&db, "src/net.rs", "IMPORTS"),
vec!["src/util.rs".to_string()]
);
}
commit(&repo, "drop the use", &[("src/net.rs", NET_RS_NO_IMPORT)]);
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert!(
out(&db, "src/net.rs", "IMPORTS").is_empty(),
"the import was deleted, so the edge must retract"
);
assert!(
!strings(prop(&db, "src/net.rs", "imports")).contains(&"src/util.rs".to_string()),
"the imports list must be rewritten, not just the edge"
);
assert_eq!(
out(&db, "src/net.rs#connect", "CALLS"),
vec!["src/util.rs#helper".to_string()]
);
let hist = db.edge_history("src/net.rs", "src/util.rs").unwrap();
let imports: Vec<&core_api::EdgeHistoryEvent> = hist
.items
.iter()
.filter(|e| e.edge_type == "IMPORTS")
.collect();
assert!(
imports
.iter()
.any(|e| e.event == core_api::EdgeEvent::Added && e.rule.as_deref() == Some("imports")),
"history must show the edge being added: {imports:?}"
);
assert!(
imports
.iter()
.any(|e| e.event == core_api::EdgeEvent::Retracted),
"history must show the edge being retracted: {imports:?}"
);
}
#[test]
fn renamed_file_moves_its_symbols() {
let repo = seed_repo();
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
git(&repo, &["mv", "src/util.rs", "src/helpers.rs"]);
commit_all(&repo, "move util to helpers");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert!(!db.has_node("src/util.rs"), "the old path is gone");
assert!(db.has_node("src/helpers.rs"), "the new path exists");
assert!(
!db.has_node("src/util.rs#helper"),
"the old symbol key must not survive the rename"
);
assert!(db.has_node("src/helpers.rs#helper"), "symbol must move");
assert_eq!(
prop(&db, "src/helpers.rs#helper", "file_id"),
Some(Value::Str("src/helpers.rs".into()))
);
assert_eq!(
out(&db, "src/helpers.rs#helper", "DEFINES"),
vec!["src/helpers.rs".to_string()]
);
assert_eq!(
all_keys(&db, "Symbol")
.iter()
.filter(|k| k.starts_with("src/util.rs#"))
.count(),
0,
"no symbol may be left under the old path"
);
assert!(
out(&db, "src/net.rs", "IMPORTS").is_empty(),
"an unresolvable import must leave no edge, stale or retargeted"
);
assert!(
!strings(prop(&db, "src/net.rs", "imports")).contains(&"src/util.rs".to_string()),
"the importer's list must no longer name the old key"
);
assert_eq!(
out(&db, "src/net.rs#connect", "CALLS"),
vec!["src/helpers.rs#helper".to_string()]
);
}
#[test]
fn no_structure_skips_symbols_and_hashes() {
let repo = seed_repo();
let db_dir = tmp("db");
let mut o = opts(&repo);
o.structure = false;
o.docs = false;
let report = run_ingest_git(&db_dir, &o).unwrap();
assert_eq!(report.structure.files_scanned, 0);
let db = GraphDb::open(&db_dir).unwrap();
assert!(db.has_node("src/util.rs"), "history is still ingested");
assert_eq!(prop(&db, "src/util.rs", "hash"), None, "no content hash");
assert_eq!(prop(&db, "src/util.rs", "symbols_n"), None);
assert!(all_keys(&db, "Symbol").is_empty(), "no symbols");
assert!(
out(&db, "src/net.rs", "IMPORTS").is_empty(),
"no derived structure edges"
);
}
#[test]
fn markdown_body_is_fulltext_searchable() {
let repo = seed_repo();
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
strings(prop(&db, "docs/guide.md", "headings")),
vec!["Demo guide".to_string(), "Wiring".to_string()]
);
let hits = db.search("body", "quicksilver");
assert_eq!(
hits.iter().map(|(k, _)| k.clone()).collect::<Vec<_>>(),
vec!["docs/guide.md".to_string()],
"the guide body must be searchable"
);
let by_heading = db.search("headings", "wiring");
assert!(
by_heading.iter().any(|(k, _)| k == "docs/guide.md"),
"headings are indexed too: {by_heading:?}"
);
let db_dir2 = tmp("db-nodocs");
let mut o = opts(&repo);
o.docs = false;
run_ingest_git(&db_dir2, &o).unwrap();
let db2 = GraphDb::open(&db_dir2).unwrap();
assert_eq!(prop(&db2, "docs/guide.md", "body"), None);
assert_eq!(prop(&db2, "docs/guide.md", "headings"), None);
assert!(
out(&db2, "docs/guide.md", "MENTIONS").is_empty(),
"no mentions without --docs"
);
assert!(
prop(&db2, "docs/guide.md", "hash").is_some(),
"the hash is structure, not docs"
);
}
#[test]
fn large_and_binary_files_are_hash_only() {
let repo = seed_repo();
let big = format!("// {}\npub fn huge() {{}}\n", "x".repeat(1024 * 1024));
write_files(&repo, &[("src/big.rs", &big)]);
let mut binary = b"pub fn nope() {}\n".to_vec();
binary.extend_from_slice(&[0u8, 1, 2, 3, 0]);
std::fs::write(repo.join("src/blob.rs"), &binary).unwrap();
commit_all(&repo, "add a huge file and a binary one");
let db_dir = tmp("db");
let report = run_ingest_git(&db_dir, &opts(&repo)).unwrap();
assert_eq!(
report.structure.skipped_large, 2,
"both files are hash-only: {report:?}"
);
let db = GraphDb::open(&db_dir).unwrap();
for path in ["src/big.rs", "src/blob.rs"] {
assert!(
prop(&db, path, "hash").is_some(),
"{path} must still be hashed"
);
assert_eq!(
prop(&db, path, "symbols_n"),
Some(Value::Int(0)),
"{path} must contribute no symbols"
);
assert_eq!(prop(&db, path, "imports"), None, "{path} has no imports");
}
assert!(
!db.has_node("src/big.rs#huge"),
"no symbol from a huge file"
);
assert!(
!db.has_node("src/blob.rs#nope"),
"no symbol from binary bytes"
);
}
#[test]
fn unchanged_files_are_not_rewritten() {
let repo = seed_repo();
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let before_file = {
let db = GraphDb::open(&db_dir).unwrap();
db.node_history("src/util.rs").unwrap().len()
};
let before_commits = core_api::wal_commit_count_at(&db_dir).unwrap();
let report = {
let shared = core_api::SharedDb::open(&db_dir).unwrap();
let mut w = shared.write();
cli::structure::refresh_all(&mut w, &repo, "", true).unwrap()
};
assert_eq!(report.files_scanned, 5, "every file is read again");
assert_eq!(
core_api::wal_commit_count_at(&db_dir).unwrap(),
before_commits,
"a re-scan of an unchanged tree must not add a single WAL commit"
);
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
db.node_history("src/util.rs").unwrap().len(),
before_file,
"an unchanged file must produce no write on a re-scan"
);
}
#[test]
fn the_sync_marker_is_written_after_the_structure_pass() {
let repo = seed_repo();
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
let newest = |key: &str| {
db.node_history(key)
.unwrap()
.iter()
.map(|e| e.commit)
.max()
.unwrap_or_else(|| panic!("no history for {key}"))
};
let marker = newest("__mushroomdb_git_sync__");
for key in [
"src/util.rs",
"src/net.rs",
"docs/guide.md",
"src/util.rs#helper",
] {
assert!(
newest(key) < marker,
"{key} was written at commit {} but the marker landed at {marker}",
newest(key)
);
}
}
#[cfg(unix)]
#[test]
fn an_unreadable_file_is_skipped_rather_than_failing_the_run() {
use std::os::unix::fs::PermissionsExt;
let repo = seed_repo();
let db_dir = tmp("db");
let locked = repo.join("src/net.rs");
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
let readable = std::fs::read(&locked).is_err();
if !readable {
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o644)).unwrap();
return;
}
let report = run_ingest_git(&db_dir, &opts(&repo)).unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(
report.structure.files_scanned, 4,
"the unreadable file is skipped, the other four are read: {report:?}"
);
let db = GraphDb::open(&db_dir).unwrap();
assert!(db.has_node("src/net.rs"), "its history is still ingested");
assert_eq!(
prop(&db, "src/net.rs", "hash"),
None,
"but nothing was read"
);
assert!(!db.has_node("src/net.rs#connect"));
assert!(
db.has_node("__mushroomdb_git_sync__"),
"the run succeeded, so the marker advanced"
);
}
#[test]
fn a_symbol_key_that_collides_with_a_file_leaves_the_file_alone() {
let repo = seed_repo();
write_files(&repo, &[("src/util.rs#helper", "not a symbol\n")]);
commit_all(&repo, "a file named like a symbol key");
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
prop(&db, "src/util.rs#helper", "path"),
Some(Value::Str("src/util.rs#helper".into())),
"the File node keeps its own path"
);
assert_eq!(
prop(&db, "src/util.rs#helper", "file_id"),
None,
"no symbol props were written onto it"
);
assert_eq!(prop(&db, "src/util.rs#helper", "kind"), None);
assert_eq!(prop(&db, "src/util.rs", "symbols_n"), Some(Value::Int(1)));
assert_eq!(
out(&db, "src/net.rs", "IMPORTS"),
vec!["src/util.rs".to_string()]
);
}
#[test]
fn turning_docs_back_on_fills_in_the_prose() {
let repo = seed_repo();
let db_dir = tmp("db");
let mut o = opts(&repo);
o.docs = false;
run_ingest_git(&db_dir, &o).unwrap();
{
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(prop(&db, "docs/guide.md", "body"), None);
}
let report = run_ingest_git(&db_dir, &opts(&repo)).unwrap();
assert_eq!(
report.structure.files_scanned, 5,
"a full re-scan: {report:?}"
);
{
let db = GraphDb::open(&db_dir).unwrap();
assert!(prop(&db, "docs/guide.md", "body").is_some(), "body arrives");
assert_eq!(
out(&db, "docs/guide.md", "MENTIONS"),
vec!["src/net.rs".to_string(), "src/util.rs".to_string()]
);
}
run_ingest_git(&db_dir, &o).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(prop(&db, "docs/guide.md", "body"), None);
assert!(out(&db, "docs/guide.md", "MENTIONS").is_empty());
}
#[test]
fn import_lines_are_recorded() {
let repo = seed_repo();
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
strings(prop(&db, "src/net.rs", "import_lines")),
vec!["src/util.rs\t3".to_string()],
"the evidence line is the line the `use` is written on"
);
assert_eq!(
strings(prop(&db, "src/lib.rs", "import_lines")),
vec![
"src/net.rs\t3".to_string(),
"src/util.rs\t4".to_string(),
"src/util.rs\t6".to_string(),
],
"one entry per import site, sorted"
);
assert_eq!(
strings(prop(&db, "src/net.rs#connect", "call_lines")),
vec!["src/util.rs#helper\t7".to_string()],
"call evidence carries the call site line"
);
}
#[test]
fn touch_reads_only_the_touched_files() {
let repo = seed_repo();
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
{
let shared = core_api::SharedDb::open(&db_dir).unwrap();
let mut w = shared.write();
w.delete_node("src/net.rs").unwrap();
}
assert!(GraphDb::open(&db_dir)
.unwrap()
.has_node("src/net.rs#connect"));
let report = {
let shared = core_api::SharedDb::open(&db_dir).unwrap();
let mut w = shared.write();
cli::structure::refresh_files(&mut w, &repo, "", &["src/util.rs".to_string()], true)
.unwrap()
};
assert_eq!(
report.files_scanned, 1,
"one file was extracted: {report:?}"
);
assert!(
GraphDb::open(&db_dir)
.unwrap()
.has_node("src/net.rs#connect"),
"an orphan the pass was not told about is not its business"
);
{
let db = GraphDb::open(&db_dir).unwrap();
assert_eq!(
db.neighbors("src/lib.rs#run", "CALLS", Direction::Out)
.unwrap_or_default(),
vec!["src/util.rs#helper".to_string()],
"a call out of an untouched file still points at the touched one"
);
}
{
let shared = core_api::SharedDb::open(&db_dir).unwrap();
let mut w = shared.write();
cli::structure::refresh_all(&mut w, &repo, "", true).unwrap();
}
assert!(
!GraphDb::open(&db_dir)
.unwrap()
.has_node("src/net.rs#connect"),
"refresh_all still sweeps every orphan"
);
}
#[test]
fn a_narrowed_refresh_resolves_calls_the_same_as_a_whole_tree_one() {
let repo = seed_repo();
commit(
&repo,
"a second helper, and a caller of both names",
&[
(
"src/dup.rs",
"//! A second definition of the same name.\n\n\
/// Also doubles.\npub fn helper(n: u32) -> u32 {\n n + n\n}\n",
),
(
"src/caller.rs",
"//! Calls one ambiguous name and one unique one.\n\n\
/// Do both.\npub fn both() -> u32 {\n helper(1) + connect(2)\n}\n",
),
],
);
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let calls = |dir: &Path| {
GraphDb::open(dir)
.unwrap()
.neighbors("src/caller.rs#both", "CALLS", Direction::Out)
.unwrap_or_default()
};
let whole_tree = calls(&db_dir);
assert_eq!(
whole_tree,
vec!["src/net.rs#connect".to_string()],
"`connect` is unique so it resolves; `helper` is defined twice so it does not"
);
write_files(
&repo,
&[(
"src/caller.rs",
"//! Calls one ambiguous name and one unique one.\n\n\
/// Do both, twice.\npub fn both() -> u32 {\n helper(1) + connect(2) + connect(3)\n}\n",
)],
);
{
let shared = core_api::SharedDb::open(&db_dir).unwrap();
let mut w = shared.write();
cli::structure::refresh_files(&mut w, &repo, "", &["src/caller.rs".to_string()], true)
.unwrap();
}
assert_eq!(calls(&db_dir), whole_tree, "narrowing moved a call edge");
}
#[test]
fn rust_methods_get_incoming_calls_from_receiver_syntax() {
let repo = tmp("repo");
git(&repo, &["init", "-q", "-b", "main"]);
commit(
&repo,
"a store and its callers",
&[
("Cargo.toml", "[package]\nname = \"demo\"\n"),
(
"src/store.rs",
"//! The store.\n\n\
/// A store.\npub struct Store {\n pub n: u32,\n}\n\n\
impl Store {\n\
\x20 /// Flush it.\n\
\x20 pub fn flush(&self) -> u32 {\n self.n\n }\n\n\
\x20 /// Flush it twice.\n\
\x20 pub fn flush_twice(&self) -> u32 {\n self.flush() + self.flush()\n }\n\
}\n",
),
(
"src/caller.rs",
"//! A sibling that holds a Store.\n\n\
/// Do the thing.\npub fn drive(store: &Store) -> u32 {\n store.flush()\n}\n",
),
(
"src/lib.rs",
"//! Crate root.\n\nmod caller;\nmod store;\n",
),
],
);
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
let callers = |key: &str| {
let mut v = db
.neighbors(key, "CALLS", Direction::In)
.unwrap_or_default();
v.sort();
v
};
assert_eq!(
callers("src/store.rs#Store.flush"),
vec![
"src/caller.rs#drive".to_string(),
"src/store.rs#Store.flush_twice".to_string(),
],
"both the `self.flush()` inside the type and the `store.flush()` next door"
);
assert!(
callers("src/store.rs#Store.flush_twice").is_empty(),
"nothing calls it"
);
}
#[test]
fn a_method_call_on_an_unrelated_receiver_writes_no_edge() {
let repo = tmp("repo");
git(&repo, &["init", "-q", "-b", "main"]);
commit(
&repo,
"a type with a len, and a caller with a vec",
&[
("Cargo.toml", "[package]\nname = \"demo\"\n"),
(
"src/reg.rs",
"//! A registry.\n\n\
/// A registry.\npub struct Registry {\n pub items: Vec<u32>,\n}\n\n\
impl Registry {\n\
\x20 /// How many.\n\
\x20 pub fn len(&self) -> usize {\n self.items.len()\n }\n\
}\n",
),
(
"src/other.rs",
"//! Counts some bytes.\n\n\
/// Count them.\npub fn count(bytes: &[u8]) -> usize {\n bytes.len()\n}\n",
),
("src/lib.rs", "//! Crate root.\n\nmod other;\nmod reg;\n"),
],
);
let db_dir = tmp("db");
run_ingest_git(&db_dir, &opts(&repo)).unwrap();
let db = GraphDb::open(&db_dir).unwrap();
assert!(
db.has_node("src/reg.rs#Registry.len"),
"the method is in the graph"
);
assert!(
db.neighbors("src/other.rs#count", "CALLS", Direction::Out)
.unwrap_or_default()
.is_empty(),
"`bytes.len()` is a slice's length, not the registry's"
);
assert!(
db.neighbors("src/reg.rs#Registry.len", "CALLS", Direction::Out)
.unwrap_or_default()
.is_empty(),
"`self.items.len()` is a vector's length, not a recursive call"
);
}