use std::path::Path;
use std::process::Command;
fn sinter(repo: &Path, args: &[&str]) -> (bool, String) {
let out = Command::new(env!("CARGO_BIN_EXE_sinter"))
.args(args)
.env("HOME", repo)
.env("USERPROFILE", repo)
.current_dir(repo)
.output()
.expect("run sinter");
(
out.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
fn git(repo: &Path, args: &[&str]) {
let ok = Command::new("git")
.args(args)
.current_dir(repo)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@t")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@t")
.output()
.expect("run git")
.status
.success();
assert!(ok, "git {args:?} failed");
}
#[test]
fn query_affected_path_impact() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(
repo.join("src/lib.rs"),
"mod util;\nuse crate::util::core_fn;\n\n/// Entry point.\npub fn entry() -> u32 {\n core_fn()\n}\n\npub fn test_entry() -> u32 {\n entry()\n}\n",
)
.unwrap();
std::fs::write(
repo.join("src/util.rs"),
"/// The core.\npub fn core_fn() -> u32 {\n 41\n}\n",
)
.unwrap();
git(repo, &["init", "-q"]);
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (ok, out) = sinter(repo, &["query", "core_fn"]);
assert!(ok, "{out}");
assert!(out.contains("fn core_fn()"), "{out}");
assert!(out.contains("The core."), "{out}");
let (ok, out) = sinter(repo, &["affected", "core_fn"]);
assert!(ok, "{out}");
assert!(out.contains("entry"), "{out}");
assert!(out.contains("test_entry"), "{out}");
assert!(out.contains("3 dependents of core_fn"), "{out}");
assert!(out.contains("2 direct in 1 file(s), 1 transitive"), "{out}");
let (_, out) = sinter(repo, &["affected", "core_fn", "--depth", "1"]);
assert!(
out.contains("2 dependents of core_fn") && !out.contains("test_entry"),
"{out}"
);
let (_, out) = sinter(repo, &["affected", "core_fn", "--json"]);
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(
(
v["total"].as_u64(),
v["direct"].as_u64(),
v["direct_files"].as_u64()
),
(Some(3), Some(2), Some(1)),
"{out}"
);
let (ok, out) = sinter(repo, &["affected", "core_fn", "--evidence", "scip"]);
assert!(!ok, "{out}");
assert!(out.contains("0 dependents"), "{out}");
let (ok, out) = sinter(repo, &["path", "test_entry", "core_fn"]);
assert!(ok, "{out}");
assert!(out.contains("-[calls/"), "{out}");
assert!(out.trim_end().ends_with("core_fn"), "{out}");
std::fs::write(
repo.join("src/util.rs"),
"/// The core.\npub fn core_fn() -> u32 {\n 42\n}\n",
)
.unwrap();
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "change core"]);
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (ok, out) = sinter(repo, &["impact", "HEAD~1..HEAD"]);
assert!(ok, "{out}");
assert!(out.contains("core_fn"), "{out}");
assert!(out.contains("entry"), "{out}");
assert!(out.contains("test_entry"), "{out}");
std::fs::write(
repo.join("src/util.rs"),
"/// The core.\npub fn core_fn() -> u32 {\n 43\n}\n",
)
.unwrap();
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (ok, out) = sinter(repo, &["affected", "core_fn"]);
assert!(ok, "{out}");
assert!(out.contains("entry"), "{out}");
assert!(out.contains("test_entry"), "{out}");
}
#[test]
fn affected_through_dyn_dispatch() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(
repo.join("src/lib.rs"),
"mod cat;\nmod dog;\n\npub trait Speak {\n fn speak(&self);\n}\n\npub fn announce(s: &dyn Speak) {\n Speak::speak(s);\n}\n",
)
.unwrap();
let dog =
"use crate::Speak;\n\npub struct Dog;\n\nimpl Speak for Dog {\n fn speak(&self) {}\n}\n";
std::fs::write(repo.join("src/dog.rs"), dog).unwrap();
std::fs::write(
repo.join("src/cat.rs"),
"use crate::Speak;\n\npub struct Cat;\n\nimpl Speak for Cat {\n fn speak(&self) {}\n}\n",
)
.unwrap();
git(repo, &["init", "-q"]);
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (ok, out) = sinter(repo, &["affected", "Dog::speak"]);
assert!(ok, "{out}");
assert!(out.contains("announce"), "{out}");
assert!(out.contains("/dynamic"), "{out}");
std::fs::write(repo.join("src/dog.rs"), dog.replace("{}", "{ }")).unwrap();
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (ok, out) = sinter(repo, &["affected", "Cat::speak"]);
assert!(ok, "{out}");
assert!(out.contains("announce"), "{out}");
assert!(out.contains("/dynamic"), "{out}");
std::fs::write(
repo.join("src/lib.rs"),
"mod cat;\nmod dog;\n\npub trait Speak {\n fn speak(&self);\n}\n\npub fn announce(s: &dyn Speak) {\n Speak::speak(s)\n}\n",
)
.unwrap();
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (ok, out) = sinter(repo, &["affected", "Dog::speak"]);
assert!(ok, "{out}");
assert!(out.contains("announce"), "{out}");
assert!(out.contains("/dynamic"), "{out}");
let (_, out) = sinter(
repo,
&["affected", "Dog::speak", "--evidence", "import,scope,scip"],
);
assert!(!out.contains("announce"), "{out}");
let (_, out) = sinter(repo, &["affected", "Dog::speak", "--certain"]);
assert!(!out.contains("announce"), "{out}");
}
fn sinter_code(repo: &Path, args: &[&str]) -> (Option<i32>, String) {
let out = Command::new(env!("CARGO_BIN_EXE_sinter"))
.args(args)
.env("HOME", repo)
.env("USERPROFILE", repo)
.current_dir(repo)
.output()
.expect("run sinter");
(
out.status.code(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
#[test]
fn json_flags_exit_codes_and_repo_flag() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(
repo.join("src/lib.rs"),
"mod util;\nuse crate::util::core_fn;\n\n/// Entry point.\npub fn entry() -> u32 {\n core_fn()\n}\n\npub fn other_entry() -> u32 {\n core_fn()\n}\n",
)
.unwrap();
std::fs::write(
repo.join("src/util.rs"),
"/// The core.\npub fn core_fn() -> u32 {\n 41\n}\n",
)
.unwrap();
git(repo, &["init", "-q"]);
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
let (code, out) = sinter_code(repo, &["build", "--repo", "."]);
assert_eq!(code, Some(0), "{out}");
let (code, _) = sinter_code(repo, &["build", ".", "--repo", "."]);
assert_eq!(code, Some(2));
let (code, out) = sinter_code(repo, &["query", "core_fn", "--json"]);
assert_eq!(code, Some(0), "{out}");
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["exact"], serde_json::json!(true), "{out}");
assert_eq!(v["results"][0]["name"], serde_json::json!("core_fn"));
let (code, out) = sinter_code(repo, &["show", "core_fn", "--json"]);
assert_eq!(code, Some(0), "{out}");
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["symbol"]["name"], serde_json::json!("core_fn"));
assert!(v["incoming"].as_array().is_some(), "{out}");
let entry_call = v["incoming"]
.as_array()
.unwrap()
.iter()
.find(|e| {
e["symbol"].as_str().is_some_and(|s| s.ends_with("entry")) && e["relation"] == "calls"
})
.unwrap_or_else(|| panic!("no call edge from entry: {out}"));
assert_eq!(
entry_call["site"],
serde_json::json!("src/lib.rs:6"),
"{out}"
);
let (code, out) = sinter_code(repo, &["affected", "core_fn", "--json"]);
assert_eq!(code, Some(0), "{out}");
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert!(v["total"].as_u64().unwrap() >= 2, "{out}");
assert!(v["dependents"][0]["s"].is_string(), "{out}");
let (code, out) = sinter_code(repo, &["affected", "core_fn", "--limit", "1"]);
assert_eq!(code, Some(0), "{out}");
assert!(out.contains("more dependents below cutoff"), "{out}");
assert!(out.contains("--limit"), "{out}");
let (code, out) = sinter_code(repo, &["path", "entry", "core_fn", "--json"]);
assert_eq!(code, Some(0), "{out}");
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["found"], serde_json::json!(true), "{out}");
assert!(v["steps"][0]["relation"].is_string(), "{out}");
assert_eq!(
v["steps"][0]["site"],
serde_json::json!("src/lib.rs:6"),
"{out}"
);
let (code, out) = sinter_code(repo, &["impact", "HEAD", "--json"]);
assert_eq!(code, Some(0), "{out}");
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert!(v["changed_symbols"].is_array(), "{out}");
let (code, out) = sinter_code(repo, &["show", "no_such_symbol_zzqx"]);
assert_eq!(code, Some(1), "{out}");
assert!(out.contains("sinter ask"), "no concept-search hint: {out}");
let (code, _) = sinter_code(repo, &["query", "zzqxzzqx"]);
assert_eq!(code, Some(1));
let (code, out) = sinter_code(repo, &["path", "core_fn", "entry"]);
assert_eq!(code, Some(1), "{out}");
assert!(out.contains("no path"), "{out}");
let (code, _) = sinter_code(repo, &["affected", "core_fn", "--evidence", "bogus"]);
assert_eq!(code, Some(2));
let (code, out) = sinter_code(repo, &["map", "--repo", "."]);
assert_eq!(code, Some(0), "{out}");
assert!(out.contains("nodes"), "{out}");
}
#[test]
fn empty_graph_says_so() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::write(repo.join("notes.txt"), "no source here\n").unwrap();
let (code, out) = sinter_code(repo, &["build"]);
assert_eq!(code, Some(0), "{out}");
assert!(out.contains("0 source files found under"), "{out}");
assert!(out.contains("wrong directory?"), "{out}");
let (code, out) = sinter_code(repo, &["query", "anything"]);
assert_eq!(code, Some(2), "{out}");
assert!(out.contains("graph") && out.contains("is empty"), "{out}");
assert!(out.contains("right directory"), "{out}");
}
#[test]
fn negative_answers_flag_stale_scip() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(
repo.join("src/lib.rs"),
"pub fn entry() -> u32 {\n core_fn()\n}\n\npub fn core_fn() -> u32 {\n 41\n}\n\npub fn orphan() {}\n",
)
.unwrap();
git(repo, &["init", "-q"]);
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (_, out) = sinter(repo, &["path", "core_fn", "entry"]);
assert!(
out.contains("no path")
&& out.contains("status: not proven")
&& out.contains("compiler index missing for rust"),
"{out}"
);
let index = repo.join(".sinter/index.scip");
std::fs::write(&index, b"").unwrap();
let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000);
std::fs::OpenOptions::new()
.write(true)
.open(&index)
.unwrap()
.set_modified(old)
.unwrap();
let (_, out) = sinter(repo, &["path", "core_fn", "entry"]);
assert!(
out.contains("no path") && out.contains("compiler index is stale"),
"{out}"
);
let (_, out) = sinter(repo, &["affected", "orphan"]);
assert!(
out.contains("0 dependents") && out.contains("compiler index is stale"),
"{out}"
);
let (_, out) = sinter(repo, &["path", "entry", "core_fn"]);
assert!(!out.contains("status: not proven"), "{out}");
std::fs::OpenOptions::new()
.write(true)
.open(&index)
.unwrap()
.set_modified(std::time::SystemTime::now())
.unwrap();
let (_, out) = sinter(repo, &["path", "core_fn", "entry"]);
assert!(
out.contains("no path")
&& out.contains("status: not proven")
&& !out.contains("compiler index is stale")
&& !out.contains("compiler index missing"),
"{out}"
);
}
#[test]
fn query_verbs_never_nag_about_stale_artifacts() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(
repo.join("src/lib.rs"),
"pub fn entry() -> u32 {\n 41\n}\n",
)
.unwrap();
std::fs::write(
repo.join("AGENTS.md"),
"<!-- BEGIN sinter (managed by `sinter install`; edits inside are overwritten) -->\nold\n<!-- END sinter -->\n",
)
.unwrap();
git(repo, &["init", "-q"]);
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
assert!(out.contains("AGENTS.md sinter block is stale"), "{out}");
for verb in [
&["show", "entry"][..],
&["ask", "entry"],
&["affected", "entry"],
&["path", "entry", "entry"],
] {
let (_, out) = sinter(repo, verb);
assert!(!out.contains("is stale"), "{verb:?} nagged: {out}");
}
}
#[test]
fn show_lists_implementations_and_dispatch() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
std::fs::write(
repo.join("src/lib.rs"),
"pub trait Speak {\n fn speak(&self);\n}\n\npub struct Dog;\npub struct Cat;\n\nimpl Speak for Dog {\n fn speak(&self) {}\n}\n\nimpl Speak for Cat {\n fn speak(&self) {}\n}\n\npub fn announce(s: &dyn Speak) {\n Speak::speak(s);\n}\n",
)
.unwrap();
git(repo, &["init", "-q"]);
git(repo, &["add", "."]);
git(repo, &["commit", "-qm", "init"]);
let (ok, out) = sinter(repo, &["build"]);
assert!(ok, "{out}");
let (_, out) = sinter(repo, &["show", "Speak"]);
assert!(out.contains("implemented by (2) Cat, Dog"), "{out}");
let (_, out) = sinter(repo, &["show", "Speak::speak"]);
assert!(
out.contains("dispatches to (2) Cat::speak, Dog::speak"),
"{out}"
);
assert!(
!out.contains("calls ("),
"dynamic edges leaked into calls: {out}"
);
let (_, out) = sinter(repo, &["show", "Dog"]);
assert!(out.contains("implements Speak"), "{out}");
let (_, out) = sinter(repo, &["path", "Dog::speak", "announce"]);
assert!(out.contains("no path Dog::speak -> announce"), "{out}");
assert!(
out.contains("forward search from Dog::speak reached 0 symbol(s)"),
"{out}"
);
assert!(
out.contains("nothing reaches announce under this filter"),
"{out}"
);
let (_, out) = sinter(repo, &["path", "Dog", "Dog::speak"]);
assert!(out.contains("Dog::speak is reached by (1):"), "{out}");
assert!(out.contains("Speak::speak [calls/dynamic]"), "{out}");
let (_, out) = sinter(repo, &["path", "announce", "Dog::speak", "--certain"]);
assert!(
out.contains("1 incoming edge(s) excluded by --evidence/--certain"),
"{out}"
);
}