use std::path::Path;
use std::process::{Command, Output};
use std::time::{Duration, SystemTime};
fn sinter(repo: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_sinter"))
.args(args)
.env("HOME", repo)
.env("USERPROFILE", repo)
.current_dir(repo)
.stdin(std::process::Stdio::null())
.output()
.expect("run sinter")
}
fn set_mtime(path: &Path, t: SystemTime) {
std::fs::File::options()
.write(true)
.open(path)
.unwrap()
.set_modified(t)
.unwrap();
}
#[test]
fn check_reports_freshness_without_indexing() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::write(repo.join("a.rs"), "pub fn f() {}\n").unwrap();
std::fs::write(
repo.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(repo.join("b.rs"), "pub fn g() {}\n").unwrap();
let out = sinter(repo, &["scip", "check"]);
assert!(
!out.status.success(),
"missing index must fail `scip check`"
);
assert!(
String::from_utf8_lossy(&out.stderr).contains("no SCIP index"),
"{}",
String::from_utf8_lossy(&out.stderr)
);
std::fs::create_dir_all(repo.join(".sinter")).unwrap();
std::fs::write(repo.join(".sinter/index.scip"), b"stub").unwrap();
let past = SystemTime::now() - Duration::from_secs(60);
set_mtime(&repo.join("a.rs"), past);
set_mtime(&repo.join("b.rs"), past);
let out = sinter(repo, &["scip", "check"]);
assert!(out.status.success(), "fresh index must pass `scip check`");
assert!(
String::from_utf8_lossy(&out.stdout).contains("index fresh"),
"{}",
String::from_utf8_lossy(&out.stdout)
);
set_mtime(
&repo.join("a.rs"),
SystemTime::now() + Duration::from_secs(60),
);
let out = sinter(repo, &["scip", "check"]);
assert!(!out.status.success(), "newer source must fail `scip check`");
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("1 source/config input"), "{err}");
set_mtime(&repo.join("a.rs"), past);
set_mtime(
&repo.join("Cargo.toml"),
SystemTime::now() + Duration::from_secs(60),
);
let out = sinter(repo, &["scip", "check"]);
assert!(
!out.status.success(),
"newer project configuration must make SCIP stale"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("1 source/config input"), "{err}");
set_mtime(&repo.join("Cargo.toml"), past);
std::fs::write(repo.join("AGENTS.md"), "# agents\n").unwrap();
set_mtime(
&repo.join("AGENTS.md"),
SystemTime::now() + Duration::from_secs(60),
);
let out = sinter(repo, &["scip", "check"]);
assert!(
out.status.success(),
"non-indexable file must not stale the index: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn negative_path_reports_snapshot_and_coverage_gaps() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::write(
repo.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
)
.unwrap();
std::fs::write(repo.join("lib.rs"), "pub fn from() {}\npub fn to() {}\n").unwrap();
std::fs::write(repo.join("bad.rs"), [0xff, 0xfe]).unwrap();
let git = |args: &[&str]| {
let out = 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()
.unwrap();
assert!(out.status.success(), "git {args:?}");
};
git(&["init", "-q"]);
git(&["add", "Cargo.toml", "lib.rs", "bad.rs"]);
git(&["commit", "-qm", "fixture"]);
let out = sinter(repo, &["build"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
std::fs::write(
repo.join("lib.rs"),
"pub fn from() {}\npub fn to() {}\n// dirty, but query self-syncs it\n",
)
.unwrap();
let out = sinter(repo, &["path", "from", "to", "--json"]);
assert!(!out.status.success(), "a miss uses grep exit 1");
let value: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let coverage = &value["coverage"];
assert_eq!(coverage["status"], "not_proven", "{value}");
assert_eq!(coverage["conclusive"], false, "{value}");
assert_eq!(coverage["snapshot"]["dirty"], true, "{value}");
assert_eq!(
coverage["snapshot"]["head"].as_str().unwrap().len(),
40,
"{value}"
);
assert_eq!(coverage["snapshot"]["working_tree_indexed"], true);
assert_eq!(coverage["compiler_index"]["state"], "missing");
assert!(
coverage["compiler_index"]["missing_index_for"]
.as_array()
.unwrap()
.iter()
.any(|language| language == "rust"),
"{value}"
);
assert!(
coverage["graph"]["unindexed_files"]
.as_array()
.unwrap()
.iter()
.any(|file| file == "bad.rs"),
"{value}"
);
}
#[cfg(unix)]
#[test]
fn bare_scip_skips_indexers_when_fresh() {
let dir = tempfile::tempdir().unwrap();
let bin = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::write(repo.join("a.rs"), "pub fn f() {}\n").unwrap();
std::fs::write(
repo.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let marker = bin.path().join("executed");
std::fs::write(
bin.path().join("rust-analyzer"),
format!("#!/bin/sh\ntouch {}\n", marker.display()),
)
.unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
bin.path().join("rust-analyzer"),
std::fs::Permissions::from_mode(0o755),
)
.unwrap();
let path_env = format!(
"{}:{}",
bin.path().display(),
std::env::var("PATH").unwrap_or_default()
);
let run = |args: &[&str]| {
Command::new(env!("CARGO_BIN_EXE_sinter"))
.args(args)
.current_dir(repo)
.env("PATH", &path_env)
.stdin(std::process::Stdio::null())
.output()
.expect("run sinter")
};
std::fs::create_dir_all(repo.join(".sinter")).unwrap();
std::fs::write(repo.join(".sinter/index.scip"), b"stub").unwrap();
set_mtime(
&repo.join("a.rs"),
SystemTime::now() - Duration::from_secs(60),
);
let out = run(&["scip"]);
assert!(out.status.success());
assert!(!marker.exists(), "fresh bare `scip` executed an indexer");
assert!(
String::from_utf8_lossy(&out.stdout).contains("nothing to do"),
"{}",
String::from_utf8_lossy(&out.stdout)
);
set_mtime(
&repo.join("a.rs"),
SystemTime::now() + Duration::from_secs(60),
);
run(&["scip"]);
assert!(marker.exists(), "stale bare `scip` must run the indexer");
}
#[cfg(unix)]
#[test]
fn scip_skips_language_without_project_configuration() {
let dir = tempfile::tempdir().unwrap();
let bin = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::write(repo.join("fixture.ts"), "export const value = 1;\n").unwrap();
let marker = bin.path().join("typescript-executed");
std::fs::write(
bin.path().join("scip-typescript"),
format!("#!/bin/sh\ntouch {}\n", marker.display()),
)
.unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
bin.path().join("scip-typescript"),
std::fs::Permissions::from_mode(0o755),
)
.unwrap();
let path_env = format!(
"{}:{}",
bin.path().display(),
std::env::var("PATH").unwrap_or_default()
);
let out = Command::new(env!("CARGO_BIN_EXE_sinter"))
.arg("scip")
.current_dir(repo)
.env("PATH", path_env)
.stdin(std::process::Stdio::null())
.output()
.unwrap();
assert!(
!out.status.success(),
"no configured project can produce an index"
);
assert!(
!marker.exists(),
"extension-only fixture launched the indexer"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("no tsconfig*.json found"), "{stderr}");
}
#[cfg(unix)]
#[test]
fn scip_runs_indexer_from_nested_project_root() {
let dir = tempfile::tempdir().unwrap();
let bin = tempfile::tempdir().unwrap();
let repo = dir.path();
let project = repo.join("services/worker");
std::fs::create_dir_all(&project).unwrap();
std::fs::write(project.join("go.mod"), "module example.com/worker\n").unwrap();
std::fs::write(project.join("main.go"), "package main\nfunc main() {}\n").unwrap();
let marker = bin.path().join("working-directory");
std::fs::write(
bin.path().join("scip-go"),
format!("#!/bin/sh\npwd > '{}'\n", marker.display()),
)
.unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(
bin.path().join("scip-go"),
std::fs::Permissions::from_mode(0o755),
)
.unwrap();
let path_env = format!(
"{}:{}",
bin.path().display(),
std::env::var("PATH").unwrap_or_default()
);
let out = Command::new(env!("CARGO_BIN_EXE_sinter"))
.arg("scip")
.current_dir(repo)
.env("PATH", path_env)
.stdin(std::process::Stdio::null())
.output()
.unwrap();
assert!(!out.status.success(), "fake indexer writes no SCIP output");
assert_eq!(
std::fs::read_to_string(marker).unwrap().trim(),
project.canonicalize().unwrap().to_string_lossy()
);
}
#[test]
fn build_names_an_ingested_stale_index() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join(".sinter")).unwrap();
std::fs::write(repo.join("a.rs"), "pub fn f() {}\n").unwrap();
let index = repo.join(".sinter/index.scip");
std::fs::write(&index, b"").unwrap();
set_mtime(&index, SystemTime::now() - Duration::from_secs(60));
let out = sinter(repo, &["build"]);
let text = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "{text}");
assert!(text.contains("SCIP index is older"), "{text}");
assert!(text.contains("rerun `sinter scip`"), "{text}");
set_mtime(&index, SystemTime::now() + Duration::from_secs(60));
let out = sinter(repo, &["build"]);
let text = String::from_utf8_lossy(&out.stdout);
assert!(!text.contains("SCIP index is older"), "{text}");
}