use std::process::Command;
use std::time::{Duration, Instant};
fn sinter(repo: &std::path::Path, args: &[&str]) -> String {
let out = Command::new(env!("CARGO_BIN_EXE_sinter"))
.args(args)
.env("HOME", repo)
.env("USERPROFILE", repo)
.arg(repo)
.output()
.expect("run sinter");
assert!(
out.status.success(),
"sinter {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn one_file_edit_is_incremental_and_fast() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
for i in 0..300 {
std::fs::write(
repo.join("src").join(format!("m{i}.rs")),
format!("pub fn f{i}() -> u32 {{ {i} }}\npub fn g{i}() -> u32 {{ f{i}() }}\n"),
)
.unwrap();
}
let full_started = Instant::now();
let first = sinter(repo, &["build"]);
let full_elapsed = full_started.elapsed();
assert!(first.contains("300 scanned, 300 changed"), "{first}");
let noop_started = Instant::now();
let noop = sinter(repo, &["build"]);
let noop_elapsed = noop_started.elapsed();
assert!(noop.contains("300 scanned, 0 changed"), "{noop}");
std::fs::write(
repo.join("src/m7.rs"),
"pub fn f7() -> u32 { 77 }\npub fn g7() -> u32 { f7() }\n",
)
.unwrap();
let started = Instant::now();
let edit = sinter(repo, &["build"]);
let elapsed = started.elapsed();
assert!(edit.contains("300 scanned, 1 changed"), "{edit}");
let edit_work = elapsed.saturating_sub(noop_elapsed);
let full_work = full_elapsed.saturating_sub(noop_elapsed);
assert!(
edit_work < full_work.mul_f64(0.5).max(Duration::from_millis(250)),
"incremental work {edit_work:?} vs full-build work {full_work:?} \
(edit {elapsed:?}, full {full_elapsed:?}, scan floor {noop_elapsed:?})"
);
std::fs::remove_file(repo.join("src/m8.rs")).unwrap();
let removed = sinter(repo, &["build"]);
assert!(removed.contains("0 changed, 1 removed"), "{removed}");
}
#[test]
fn touched_but_unchanged_file_stays_clean_and_restamps() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
let content = "pub fn f() -> u32 { 1 }\n";
std::fs::write(repo.join("src/a.rs"), content).unwrap();
std::fs::write(repo.join("src/b.rs"), "pub fn g() -> u32 { 2 }\n").unwrap();
let first = sinter(repo, &["build"]);
assert!(first.contains("2 scanned, 2 changed"), "{first}");
std::thread::sleep(Duration::from_millis(20));
std::fs::write(repo.join("src/a.rs"), content).unwrap();
let touched = sinter(repo, &["build"]);
assert!(touched.contains("2 scanned, 0 changed"), "{touched}");
let clean = sinter(repo, &["build"]);
assert!(clean.contains("2 scanned, 0 changed"), "{clean}");
std::thread::sleep(Duration::from_millis(20));
std::fs::write(repo.join("src/a.rs"), "pub fn f() -> u32 { 9 }\n").unwrap();
let edited = sinter(repo, &["build"]);
assert!(edited.contains("2 scanned, 1 changed"), "{edited}");
}
#[cfg(unix)]
#[test]
fn preserved_mtime_and_length_still_detects_changed_content() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src")).unwrap();
let path = repo.join("src/a.rs");
std::fs::write(&path, "pub fn f() -> u32 { 1 }\n").unwrap();
sinter(repo, &["build"]);
let original_mtime = std::fs::metadata(&path).unwrap().modified().unwrap();
std::fs::write(&path, "pub fn f() -> u32 { 9 }\n").unwrap();
std::fs::File::options()
.write(true)
.open(&path)
.unwrap()
.set_times(std::fs::FileTimes::new().set_modified(original_mtime))
.unwrap();
let rebuilt = sinter(repo, &["build"]);
assert!(rebuilt.contains("1 scanned, 1 changed"), "{rebuilt}");
}
#[test]
fn manifest_rename_invalidates_resolution() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../harness/golden/fixtures/rust-workspace-crates/crates");
for (src, dst) in [("util", "util"), ("app", "app")] {
let out = repo.join("crates").join(dst).join("src");
std::fs::create_dir_all(&out).unwrap();
for entry in std::fs::read_dir(fixture.join(src).join("src")).unwrap() {
let entry = entry.unwrap();
std::fs::copy(entry.path(), out.join(entry.file_name())).unwrap();
}
std::fs::copy(
fixture.join(src).join("Cargo.toml"),
repo.join("crates").join(dst).join("Cargo.toml"),
)
.unwrap();
}
sinter(repo, &["build"]);
let before = sinter(repo, &["affected", "double", "--repo"]);
assert!(before.contains("main"), "expected dependents: {before}");
let manifest = repo.join("crates/util/Cargo.toml");
let renamed = std::fs::read_to_string(&manifest)
.unwrap()
.replace("acme-util", "renamed-util");
std::fs::write(&manifest, renamed).unwrap();
let rebuild = sinter(repo, &["build"]);
assert!(
!rebuild.contains(" 0 files re-resolved"),
"manifest change must re-resolve: {rebuild}"
);
let out = Command::new(env!("CARGO_BIN_EXE_sinter"))
.args(["affected", "double", "--repo"])
.arg(repo)
.env("HOME", repo)
.env("USERPROFILE", repo)
.output()
.expect("run sinter");
let after = String::from_utf8_lossy(&out.stdout).into_owned();
assert!(
!after.contains("main"),
"stale dependents survived the rename: {after}"
);
}
#[test]
fn subdirectory_invocation_discovers_graph_root() {
let dir = tempfile::tempdir().unwrap();
let repo = dir.path();
std::fs::create_dir_all(repo.join("src/deep")).unwrap();
std::fs::write(repo.join("src/deep/m.rs"), "pub fn f() {}\n").unwrap();
sinter(repo, &["build"]);
let out = Command::new(env!("CARGO_BIN_EXE_sinter"))
.args(["query", "f"])
.current_dir(repo.join("src/deep"))
.output()
.expect("run sinter");
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
String::from_utf8_lossy(&out.stdout).contains("src/deep/m.rs"),
"query from subdirectory missed the root graph"
);
assert!(
!repo.join("src/deep/.sinter").exists(),
"subdirectory build created a nested graph"
);
}