use assert_cmd::Command;
use predicates::prelude::PredicateBooleanExt;
use std::path::Path;
use tempfile::TempDir;
#[allow(dead_code)]
mod common;
use common::hermetic_command;
const KACHE_BIN: &str = env!("CARGO_BIN_EXE_kache");
fn kache(home: &Path, cache_dir: &Path) -> Command {
let mut cmd = Command::from(hermetic_command(
KACHE_BIN,
cache_dir,
Some(&cache_dir.join("config.toml")),
));
cmd.env("KACHE_LOG", "off")
.env("HOME", home)
.env("CARGO_HOME", home.join(".cargo"))
.env("SHELL", "/bin/bash")
.env_remove("ZDOTDIR")
.env("XDG_CONFIG_HOME", home.join(".config"))
.env("KACHE_DAEMON_IDLE_TIMEOUT", "3");
cmd
}
struct Env {
_home: TempDir,
_cache: TempDir,
home: std::path::PathBuf,
cache: std::path::PathBuf,
}
fn env() -> Env {
let home = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
Env {
home: home.path().to_path_buf(),
cache: cache.path().to_path_buf(),
_home: home,
_cache: cache,
}
}
impl Env {
fn cmd(&self) -> Command {
kache(&self.home, &self.cache)
}
fn wrapper_build(&self, project: &Path, target_dir: &Path) -> std::process::Output {
hermetic_command(
env!("CARGO"),
&self.cache,
Some(&self.cache.join("config.toml")),
)
.args(["build", "--lib"])
.current_dir(project)
.env("RUSTC_WRAPPER", KACHE_BIN)
.env("KACHE_LOG", "off")
.env("HOME", &self.home)
.env("KACHE_DAEMON_IDLE_TIMEOUT", "3")
.env("CARGO_TARGET_DIR", target_dir)
.env("CARGO_INCREMENTAL", "0")
.output()
.expect("run cargo build with kache wrapper")
}
}
fn write_legacy_events(e: &Env, crate_name: &str, events: &[(&str, &str)]) {
let mut lines = events
.iter()
.enumerate()
.map(|(index, (result, key))| {
serde_json::to_string(&serde_json::json!({
"ts": format!("2026-01-01T00:00:{index:02}Z"),
"crate_name": crate_name,
"result": result,
"elapsed_ms": 1,
"size": 1,
"cache_key": key,
"schema": 14
}))
.unwrap()
})
.collect::<Vec<_>>()
.join("\n");
lines.push('\n');
std::fs::write(e.cache.join("events.jsonl"), lines).unwrap();
}
fn write_legacy_miss_events(e: &Env, crate_name: &str, keys: &[&str]) {
let events = keys.iter().map(|key| ("miss", *key)).collect::<Vec<_>>();
write_legacy_events(e, crate_name, &events);
}
fn scaffold_lib(name: &str, body: &str) -> TempDir {
let project = TempDir::new().unwrap();
std::fs::create_dir_all(project.path().join("src")).unwrap();
std::fs::write(
project.path().join("Cargo.toml"),
format!(
"[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
[lib]\npath = \"src/lib.rs\"\n\n[workspace]\n"
),
)
.unwrap();
std::fs::write(project.path().join("src/lib.rs"), body).unwrap();
project
}
fn cc_available() -> bool {
std::process::Command::new("cc")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[test]
fn version_prints_package_version() {
let e = env();
e.cmd()
.arg("--version")
.assert()
.success()
.stdout(predicates::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn help_lists_subcommands() {
let e = env();
e.cmd()
.arg("--help")
.assert()
.success()
.stdout(predicates::str::contains("list"))
.stdout(predicates::str::contains("report"))
.stdout(predicates::str::contains("doctor"))
.stdout(predicates::str::contains("completions"));
}
#[test]
fn unknown_subcommand_is_a_usage_error() {
let e = env();
e.cmd()
.arg("definitely-not-a-command")
.assert()
.failure()
.code(2);
}
#[test]
fn interactive_commands_refuse_captured_non_tty_output() {
let e = env();
e.cmd()
.arg("config")
.assert()
.failure()
.stderr(predicates::str::contains("needs a terminal"));
e.cmd()
.arg("monitor")
.assert()
.failure()
.stderr(predicates::str::contains("kache stats --json"));
}
#[test]
fn json_rejects_unsupported_or_mutating_commands() {
let e = env();
e.cmd()
.args(["--json", "report"])
.assert()
.failure()
.stderr(predicates::str::contains("is supported on"));
e.cmd()
.args(["--json", "doctor", "--fix"])
.assert()
.failure()
.stderr(predicates::str::contains("reports diagnostics only"));
}
#[cfg(unix)]
#[test]
fn path_resolvable_unknown_subcommand_is_still_a_usage_error() {
let e = env();
e.cmd().arg("true").assert().failure().code(2);
}
#[cfg(unix)]
#[test]
fn kani_compiler_passthrough_executes_every_time() {
use std::os::unix::fs::PermissionsExt;
let e = env();
let driver_dir = TempDir::new().unwrap();
let driver = driver_dir.path().join("kani-compiler");
let invocations = driver_dir.path().join("invocations.txt");
std::fs::write(
&driver,
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$KANI_INVOCATIONS\"\nprintf 'kani:%s\\n' \"$*\"\n",
)
.unwrap();
std::fs::set_permissions(&driver, std::fs::Permissions::from_mode(0o755)).unwrap();
for _ in 0..2 {
e.cmd()
.arg(&driver)
.arg("-vV")
.env("RUSTC", &driver)
.env("KANI_INVOCATIONS", &invocations)
.assert()
.success()
.stdout(predicates::str::diff("kani:-vV\n"));
}
assert_eq!(std::fs::read_to_string(invocations).unwrap(), "-vV\n-vV\n");
}
#[test]
fn subcommand_help_is_available() {
for sub in [
"list",
"report",
"gc",
"purge",
"doctor",
"stats",
"sync",
"completions",
] {
let e = env();
e.cmd()
.args([sub, "--help"])
.assert()
.success()
.stdout(predicates::str::contains("Usage"));
}
}
#[test]
fn completions_zsh_emits_script() {
let e = env();
e.cmd()
.args(["completions", "zsh"])
.assert()
.success()
.stdout(predicates::str::contains("#compdef"))
.stdout(predicates::str::contains("kache"));
}
#[test]
fn completions_bash_emits_script() {
let e = env();
e.cmd()
.args(["completions", "bash"])
.assert()
.success()
.stdout(predicates::str::contains("complete"))
.stdout(predicates::str::contains("kache"));
}
#[test]
fn completions_fish_emits_script() {
let e = env();
e.cmd()
.args(["completions", "fish"])
.assert()
.success()
.stdout(predicates::str::contains("complete"))
.stdout(predicates::str::contains("kache"));
}
#[test]
fn completions_invalid_shell_is_a_usage_error() {
let e = env();
e.cmd()
.args(["completions", "not-a-shell"])
.assert()
.failure()
.code(2);
}
#[test]
fn completions_work_with_broken_config() {
let e = env();
std::fs::write(e.cache.join("config.toml"), "{{{{ not toml").unwrap();
e.cmd()
.args(["completions", "zsh"])
.assert()
.success()
.stdout(predicates::str::contains("#compdef"));
}
#[test]
fn list_on_empty_cache_reports_no_entries() {
let e = env();
e.cmd()
.arg("list")
.assert()
.success()
.stdout(predicates::str::contains("No cached entries"));
}
#[test]
fn list_accepts_all_sort_modes() {
for sort in ["name", "size", "hits", "age"] {
let e = env();
e.cmd().args(["list", "--sort", sort]).assert().success();
}
}
#[test]
fn list_no_pager_flag_is_accepted() {
let e = env();
e.cmd().args(["list", "--no-pager"]).assert().success();
}
#[test]
fn list_for_specific_missing_crate_succeeds() {
let e = env();
e.cmd().args(["list", "serde"]).assert().success();
}
#[test]
fn stats_on_empty_cache_succeeds() {
let e = env();
e.cmd().arg("stats").assert().success();
let e = env();
e.cmd().args(["stats", "--since", "1h"]).assert().success();
}
#[test]
fn stats_and_report_label_the_requested_window() {
let e = env();
e.cmd()
.args(["stats", "--since", "15m"])
.assert()
.success()
.stdout(predicates::str::contains("last 15m)"));
e.cmd()
.args(["report", "--format", "text", "--since", "90m"])
.assert()
.success()
.stdout(predicates::str::contains("kache build report (last 90m)"));
e.cmd()
.args(["stats", "--since", "soon"])
.assert()
.failure()
.stderr(predicates::str::contains("invalid --since \"soon\""));
e.cmd()
.args(["report", "--since", "1.5h"])
.assert()
.failure()
.stderr(predicates::str::contains("invalid --since"));
}
#[test]
fn stats_announces_auto_start_and_warns_on_daemon_config_mismatch() {
let e = env();
std::fs::write(
e.cache.join("config.toml"),
"[cache]\nlocal_max_size = \"1GiB\"\n",
)
.unwrap();
e.cmd()
.env("KACHE_DAEMON_IDLE_TIMEOUT", "30")
.arg("stats")
.assert()
.success()
.stderr(
predicates::str::contains("no daemon reachable at").and(predicates::str::contains(
"starting one inheriting this process's environment",
)),
)
.stdout(predicates::str::contains("/ 1.0 GiB"));
let other = e.cache.join("other-config.toml");
std::fs::write(&other, "[cache]\nlocal_max_size = \"2GiB\"\n").unwrap();
e.cmd()
.env("KACHE_DAEMON_IDLE_TIMEOUT", "30")
.env("KACHE_CONFIG", &other)
.arg("stats")
.assert()
.success()
.stdout(predicates::str::contains("/ 1.0 GiB"))
.stderr(
predicates::str::contains("local_max_size=1.0 GiB")
.and(predicates::str::contains("says 2.0 GiB"))
.and(predicates::str::contains("the daemon's value is in effect"))
.and(predicates::str::contains(other.display().to_string()))
.and(predicates::str::contains(
e.cache.join("config.toml").display().to_string(),
)),
);
let unusable_store = e.cache.join("not-a-directory");
std::fs::write(&unusable_store, b"file, not a cache directory").unwrap();
e.cmd()
.env("KACHE_DAEMON_IDLE_TIMEOUT", "30")
.env("KACHE_CONFIG", &other)
.env("KACHE_CACHE_DIR", &unusable_store)
.env("KACHE_SOCKET_PATH", e.cache.join("daemon.sock"))
.arg("stats")
.assert()
.success()
.stdout(predicates::str::contains("/ 1.0 GiB"))
.stderr(predicates::str::contains("local_store="));
e.cmd().args(["daemon", "stop"]).assert().success();
}
#[test]
fn stats_warns_when_daemon_prefetch_policy_differs() {
let e = env();
e.cmd()
.env("KACHE_DAEMON_IDLE_TIMEOUT", "30")
.arg("stats")
.assert()
.success();
e.cmd()
.env("KACHE_DAEMON_IDLE_TIMEOUT", "30")
.env("KACHE_PREFETCH_ENABLED", "0")
.arg("stats")
.assert()
.success()
.stdout(predicates::str::contains("Prefetch: disabled").not())
.stderr(
predicates::str::contains("prefetch_enabled=true")
.and(predicates::str::contains("says false")),
);
e.cmd().args(["daemon", "stop"]).assert().success();
}
#[test]
fn report_emits_each_format() {
for format in ["text", "json", "markdown", "github"] {
let e = env();
e.cmd()
.args(["report", "--format", format])
.assert()
.success();
}
}
#[test]
fn report_json_on_empty_cache_is_valid_json() {
let e = env();
let out = e
.cmd()
.args(["report", "--format", "json", "--since", "24h"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).expect("valid json");
assert_eq!(v["schema_version"].as_u64(), Some(1));
assert_eq!(v["summary"]["misses"].as_u64(), Some(0));
assert_eq!(v["summary"]["local_hits"].as_u64(), Some(0));
let schema_text = include_str!("../docs/commands/report.schema.json");
let schema: serde_json::Value = serde_json::from_str(schema_text).expect("valid schema json");
assert_eq!(
schema["properties"]["schema_version"]["enum"],
serde_json::json!([1])
);
}
#[test]
fn last_build_commands_select_one_root_and_describe_session_scope() {
for recorded in [false, true] {
let e = env();
let root = e.home.canonicalize().unwrap();
let other = e.cache.canonicalize().unwrap();
let events = [
(3, &root, "local_hit", "selected"),
(1, &root, "miss", "older"),
(4, &other, "miss", "selected"),
(2, &root, "miss", "selected"),
]
.map(|(minute, path, result, session)| {
serde_json::json!({
"ts": format!("2026-01-01T00:{:02}:00Z", minute * 10),
"root": path,
"session_id": if recorded { session } else { "" },
"crate_name": format!("fixture{minute}"),
"result": result,
"elapsed_ms": if minute == 3 { 660_000 } else { 0 },
"compile_time_ms": 100,
"size": 128,
"cache_key": "fixture-key",
"schema": 18,
})
.to_string()
});
std::fs::write(e.cache.join("events.jsonl"), events.join("\n") + "\n").unwrap();
for command in ["stats", "report"] {
e.cmd()
.args([command, "--last-build", "--root"])
.arg(&root)
.assert()
.success()
.stdout(predicates::str::contains("last build session"))
.stdout(predicates::str::contains(
"May include multiple Cargo commands",
))
.stdout(predicates::str::contains(if recorded {
"recorded selected"
} else {
"inferred from activity"
}));
let mut cmd = e.cmd();
cmd.args([command, "--last-build", "--root"]).arg(&root);
if command == "stats" {
cmd.arg("--json");
} else {
cmd.args(["--format", "json"]);
}
let output = cmd.assert().success().get_output().stdout.clone();
let raw: serde_json::Value = serde_json::from_slice(&output).unwrap();
let report = if command == "stats" {
assert_eq!(raw["command"], "stats");
assert_eq!(raw["schema_version"], 1);
&raw["report"]
} else {
&raw
};
assert_eq!(report["summary"]["local_hits"], 1);
assert_eq!(report["summary"]["misses"], 1);
assert_eq!(report["timeline"]["event_count"], 2);
assert_eq!(report["meta"]["session"]["root"], root.to_str().unwrap());
assert_eq!(report["meta"]["session"]["inferred"], !recorded);
assert!(report["network"].is_null());
}
let output = e
.cmd()
.args(["report", "--last-build", "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let report: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(report["meta"]["session"]["root"], other.to_str().unwrap());
assert_eq!(report["timeline"]["event_count"], 1);
}
}
#[test]
fn last_build_requires_recorded_events_and_conflicts_with_since() {
let e = env();
for command in ["stats", "report"] {
e.cmd()
.args([command, "--last-build"])
.assert()
.failure()
.stderr(predicates::str::contains("No recorded compiler events"));
e.cmd()
.args([command, "--last-build", "--since", "24h"])
.assert()
.failure()
.stderr(predicates::str::contains("cannot be used with"));
}
e.cmd()
.args(["stats", "--root", "."])
.assert()
.failure()
.stderr(predicates::str::contains("--last-build"));
}
#[test]
fn report_writes_to_output_file() {
let e = env();
let out = e.cache.join("report.json");
e.cmd()
.args(["report", "--format", "json", "--top", "3", "-o"])
.arg(&out)
.assert()
.success();
let body = std::fs::read_to_string(&out).expect("report file written");
let _: serde_json::Value = serde_json::from_str(&body).expect("valid json on disk");
}
#[test]
fn gc_on_empty_cache_succeeds() {
let e = env();
e.cmd().arg("gc").assert().success();
let e = env();
e.cmd().args(["gc", "--max-age", "7d"]).assert().success();
let e = env();
e.cmd()
.args(["gc", "--stale-schema"])
.assert()
.success()
.stdout(predicates::str::contains("Stale-schema GC:"));
}
#[test]
fn gc_rejects_overlapping_explicit_policies() {
let e = env();
e.cmd()
.args(["gc", "--max-age", "7d", "--stale-schema"])
.assert()
.failure()
.stderr(predicates::str::contains("cannot be used with"));
}
#[test]
fn purge_on_empty_cache_succeeds() {
let e = env();
e.cmd().arg("purge").assert().success();
let e = env();
e.cmd()
.args(["purge", "--crate-name", "serde"])
.assert()
.success();
}
#[test]
fn why_miss_on_unknown_crate_succeeds() {
let e = env();
e.cmd().args(["why-miss", "serde"]).assert().success();
}
#[test]
fn why_miss_json_reports_the_last_recorded_miss() {
let e = env();
write_legacy_miss_events(&e, "serde", &["miss-key"]);
let output = e
.cmd()
.args(["--json", "why-miss", "serde"])
.output()
.expect("run why-miss JSON");
assert!(output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(value["command"], "why-miss");
assert_eq!(value["crate_name"], "serde");
assert_eq!(value["cache_key"], "miss-key");
assert_eq!(value["diagnosis"], "never_cached");
assert_eq!(value["stored_entries"], 0);
}
#[test]
fn machine_readable_commands_emit_one_json_document() {
let e = env();
let workdir = TempDir::new().unwrap();
let commands: &[&[&str]] = &[
&["--json", "list"],
&["--json", "stats"],
&["--json", "gc"],
&["--json", "clean"],
&["--json", "doctor"],
&["--json", "why-miss", "serde"],
&["--json", "daemon", "status"],
];
for args in commands {
let output = e
.cmd()
.current_dir(workdir.path())
.args(*args)
.output()
.expect("run JSON command");
assert!(
output.status.success(),
"{args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let value: serde_json::Value = serde_json::from_slice(&output.stdout)
.unwrap_or_else(|error| panic!("{args:?} did not emit clean JSON: {error}"));
assert_eq!(value["schema_version"], 1, "{args:?}");
assert!(value["command"].is_string(), "{args:?}");
}
}
#[test]
fn tracked_clean_previews_and_removes_only_the_remembered_target() {
let e = env();
let project = scaffold_lib("trackedclean", "pub fn value() -> u8 { 1 }\n");
let external = TempDir::new().unwrap();
let target = external.path().join("cargo-output");
let build = e.wrapper_build(project.path(), &target);
assert!(
build.status.success(),
"wrapper build failed: {}",
String::from_utf8_lossy(&build.stderr)
);
assert!(target.is_dir());
let elsewhere = TempDir::new().unwrap();
let preview = e
.cmd()
.current_dir(elsewhere.path())
.args(["--json", "clean", "--tracked", "--stale", "0h"])
.output()
.expect("preview tracked clean");
assert!(preview.status.success());
let preview: serde_json::Value = serde_json::from_slice(&preview.stdout).unwrap();
assert_eq!(preview["changed"], false);
assert_eq!(preview["targets"][0]["path"], target.display().to_string());
let clean = e
.cmd()
.current_dir(elsewhere.path())
.args(["--json", "clean", "--tracked", "--stale", "0h", "--yes"])
.output()
.expect("run tracked clean");
assert!(clean.status.success());
let clean: serde_json::Value = serde_json::from_slice(&clean.stdout).unwrap();
assert_eq!(clean["changed"], true);
assert!(!target.exists(), "tracked target should be removed");
assert!(
project.path().join("Cargo.toml").is_file(),
"tracked cleanup must not remove the source workspace"
);
let after = e
.cmd()
.current_dir(elsewhere.path())
.args(["--json", "clean", "--tracked", "--stale", "0h"])
.output()
.expect("recheck tracked clean");
assert!(after.status.success());
let after: serde_json::Value = serde_json::from_slice(&after.stdout).unwrap();
assert_eq!(after["targets"].as_array().unwrap().len(), 0);
}
#[test]
fn clean_dry_run_reports_no_targets_in_empty_dir() {
let e = env();
let workdir = TempDir::new().unwrap();
e.cmd()
.current_dir(workdir.path())
.args(["clean", "--dry-run"])
.assert()
.success()
.stdout(predicates::str::contains("No target/ directories found"));
}
#[cfg(not(target_os = "macos"))]
#[test]
fn clean_dry_run_reports_a_populated_target_dir() {
let e = env();
let project = TempDir::new().unwrap();
let root = project.path();
std::fs::write(root.join("Cargo.toml"), "[package]\nname=\"p\"\n").unwrap();
for profile in ["debug", "release"] {
let deps = root.join("target").join(profile).join("deps");
std::fs::create_dir_all(&deps).unwrap();
std::fs::write(deps.join("libfoo.rlib"), vec![0u8; 4096]).unwrap();
std::fs::write(deps.join("libfoo.rmeta"), vec![0u8; 1024]).unwrap();
std::fs::write(deps.join("foo.d"), b"foo.o: foo.rs\n").unwrap();
}
e.cmd()
.current_dir(root)
.args(["clean", "--dry-run"])
.assert()
.success()
.stdout(predicates::str::contains("Found 1 target"))
.stdout(predicates::str::contains(
"Dry run: estimated to free 10.0 KiB",
))
.stdout(predicates::str::contains("shared, sparse, or duplicate").not());
}
#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn clean_dry_run_collapses_hardlinks_wholly_inside_target() {
let e = env();
let project = TempDir::new().unwrap();
let root = project.path();
std::fs::write(root.join("Cargo.toml"), "[package]\nname=\"p\"\n").unwrap();
let deps = root.join("target/debug/deps");
std::fs::create_dir_all(&deps).unwrap();
let first = deps.join("libfoo.rlib");
let second = deps.join("libfoo-copy.rlib");
std::fs::write(&first, vec![0u8; 4096]).unwrap();
std::fs::hard_link(&first, &second).unwrap();
e.cmd()
.current_dir(root)
.args(["clean", "--dry-run"])
.assert()
.success()
.stdout(predicates::str::contains("8.0 KiB total, 0 B cached"))
.stdout(predicates::str::contains(
"Dry run: estimated to free 4.0 KiB",
))
.stdout(predicates::str::contains(
"4.0 KiB of apparent size is shared, sparse, or duplicate",
));
}
#[test]
fn doctor_runs_readonly_diagnostics() {
let e = env();
e.cmd().arg("doctor").assert().success();
}
#[test]
fn doctor_does_not_prescribe_obsolete_build_script_for_extra_inputs() {
let e = env();
let project = TempDir::new().unwrap();
std::fs::write(
project.path().join("Cargo.toml"),
"[package]\nname='doctor-extra-inputs'\nversion='0.1.0'\n",
)
.unwrap();
std::fs::write(
project.path().join("kache.toml"),
"extra_inputs = [\"data/**/*.txt\"]\n",
)
.unwrap();
e.cmd()
.current_dir(project.path())
.arg("doctor")
.assert()
.success()
.stdout(predicates::str::contains("cargo:rerun-if-changed").not())
.stdout(predicates::str::contains("no build.rs").not());
}
#[test]
fn doctor_verify_exit_status_gates_on_corruption() {
let e = env();
e.cmd().args(["doctor", "--verify"]).assert().success();
let project = TempDir::new().unwrap();
std::fs::create_dir_all(project.path().join("src")).unwrap();
std::fs::write(
project.path().join("Cargo.toml"),
"[package]\nname = \"verifygate\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[workspace]\n",
)
.unwrap();
std::fs::write(
project.path().join("src/lib.rs"),
"pub fn f() -> u32 { 1 }\n",
)
.unwrap();
let target = TempDir::new().unwrap();
let out = e.wrapper_build(project.path(), target.path());
assert!(out.status.success(), "fixture build must succeed");
let blobs = e.cache.join("store").join("blobs");
let mut corrupted = false;
for shard in std::fs::read_dir(&blobs).into_iter().flatten().flatten() {
for blob in std::fs::read_dir(shard.path())
.into_iter()
.flatten()
.flatten()
{
let path = blob.path();
let Ok(meta) = std::fs::metadata(&path) else {
continue;
};
if !meta.is_file() || meta.len() == 0 {
continue;
}
let mut perms = meta.permissions();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
perms.set_mode(0o644);
}
#[cfg(not(unix))]
perms.set_readonly(false);
std::fs::set_permissions(&path, perms).unwrap();
std::fs::write(&path, vec![b'X'; meta.len() as usize]).unwrap();
corrupted = true;
break;
}
if corrupted {
break;
}
}
assert!(corrupted, "the fixture build should have stored a blob");
e.cmd()
.args(["doctor", "--checksums"])
.assert()
.failure()
.stderr(predicates::str::contains("cache integrity check failed"));
e.cmd()
.args(["doctor", "--checksums", "--repair"])
.assert()
.success();
e.cmd().args(["doctor", "--checksums"]).assert().success();
}
#[test]
fn doctor_reports_wrapper_configured_via_env() {
let e = env();
e.cmd()
.env("RUSTC_WRAPPER", "kache")
.arg("doctor")
.assert()
.success()
.stdout(predicates::str::contains("RUSTC_WRAPPER"));
}
#[test]
fn doctor_reports_wrapper_configured_via_cargo_config() {
let e = env();
let cargo_dir = e.home.join(".cargo");
std::fs::create_dir_all(&cargo_dir).unwrap();
std::fs::write(
cargo_dir.join("config.toml"),
"[build]\nrustc-wrapper = \"kache\"\n",
)
.unwrap();
e.cmd()
.current_dir(&e.home) .arg("doctor")
.assert()
.success()
.stdout(predicates::str::contains("RUSTC_WRAPPER"));
}
#[test]
fn doctor_reports_local_only_mode() {
let e = env();
e.cmd()
.env("KACHE_LOCAL_ONLY", "1")
.arg("doctor")
.assert()
.success()
.stdout(predicates::str::contains("local-only"));
}
#[test]
fn doctor_reports_configured_remote() {
let e = env();
e.cmd()
.env("KACHE_S3_BUCKET", "my-remote-bucket")
.arg("doctor")
.assert()
.success()
.stdout(predicates::str::contains("my-remote-bucket"));
}
#[test]
fn doctor_reports_reachable_daemon_version() {
let e = env();
let project = scaffold_lib("kachedoc", "pub fn d() -> u8 { 1 }\n");
let target_dir = project.path().join("target");
let build = hermetic_command(env!("CARGO"), &e.cache, Some(&e.cache.join("config.toml")))
.args(["build", "--lib"])
.current_dir(project.path())
.env("RUSTC_WRAPPER", KACHE_BIN)
.env("KACHE_LOG", "off")
.env("HOME", &e.home)
.env("KACHE_DAEMON_IDLE_TIMEOUT", "30") .env("CARGO_TARGET_DIR", &target_dir)
.env("CARGO_INCREMENTAL", "0")
.output()
.expect("run build");
assert!(build.status.success(), "build failed");
e.cmd()
.arg("doctor")
.assert()
.success()
.stdout(predicates::str::contains("Daemon version"));
}
#[test]
fn doctor_verify_on_empty_cache_succeeds() {
let e = env();
e.cmd().args(["doctor", "--verify"]).assert().success();
let e = env();
e.cmd()
.args(["doctor", "--verify", "--checksums"])
.assert()
.success();
}
#[test]
fn doctor_fix_in_isolated_home_succeeds() {
let e = env();
std::fs::create_dir_all(e.home.join(".cargo")).unwrap();
e.cmd().args(["doctor", "--fix"]).assert().success();
}
#[cfg(unix)]
#[test]
fn doctor_fix_migrates_sccache_cargo_config_to_kache() {
let e = env();
let cargo_dir = e.home.join(".cargo");
std::fs::create_dir_all(&cargo_dir).unwrap();
std::fs::write(
cargo_dir.join("config.toml"),
"[build]\nrustc-wrapper = \"sccache\"\n",
)
.unwrap();
std::fs::write(
e.home.join(".zshrc"),
"export RUSTC_WRAPPER=sccache\n# sccache comment line\n",
)
.unwrap();
e.cmd().args(["doctor", "--fix"]).assert().success();
let rewritten = std::fs::read_to_string(cargo_dir.join("config.toml")).unwrap();
assert!(
rewritten.contains("kache") && !rewritten.contains("sccache"),
"migrate should replace sccache with kache: {rewritten}"
);
}
#[test]
fn doctor_fix_migrates_sccache_config_in_custom_cargo_home() {
let e = env();
let cargo_home = e.home.join("custom-cargo-home");
std::fs::create_dir_all(&cargo_home).unwrap();
std::fs::write(
cargo_home.join("config.toml"),
"[build]\nrustc-wrapper = \"sccache\"\n",
)
.unwrap();
e.cmd()
.env("CARGO_HOME", &cargo_home)
.args(["doctor", "--fix"])
.assert()
.success();
let rewritten = std::fs::read_to_string(cargo_home.join("config.toml")).unwrap();
assert!(
rewritten.contains("kache") && !rewritten.contains("sccache"),
"migrate should rewrite the $CARGO_HOME config: {rewritten}"
);
assert!(
!e.home.join(".cargo").join("config.toml").exists(),
"migrate must not touch ~/.cargo when CARGO_HOME points elsewhere"
);
}
#[test]
fn init_check_is_a_dry_run() {
let e = env();
std::fs::create_dir_all(e.home.join(".cargo")).unwrap();
let output = e
.cmd()
.args(["init", "--check", "--yes"])
.assert()
.success()
.get_output()
.stdout
.clone();
let output = String::from_utf8(output).unwrap();
assert!(!output.contains("[Y/n]"));
assert!(!e.home.join(".cargo/config.toml").exists());
assert!(!e.home.join(".bashrc").exists());
assert!(!e.home.join(".bash_profile").exists());
assert!(!e.home.join(".local/lib/kache/shims").exists());
assert!(!e.cache.join("daemon.sock").exists());
}
#[test]
fn init_eof_does_not_accept_changes() {
let e = env();
e.cmd().args(["init", "--no-service"]).assert().success();
assert!(!e.home.join(".cargo/config.toml").exists());
assert!(!e.home.join(".bashrc").exists());
assert!(!e.home.join(".local/lib/kache/shims").exists());
assert!(!e.cache.join("daemon.sock").exists());
}
#[cfg(unix)]
#[test]
fn init_saves_shell_setup_preserves_cargo_choices_and_is_idempotent() {
let e = env();
let cargo = e.home.join(".cargo/config.toml");
std::fs::create_dir_all(cargo.parent().unwrap()).unwrap();
let original = "[build]\nrustc-wrapper = \"sccache\"\n[env]\nHOST_CC = \"custom-cc\"\n";
std::fs::write(&cargo, original).unwrap();
std::fs::write(e.home.join(".bashrc"), "# user rc\n").unwrap();
std::fs::write(e.home.join(".profile"), "# user profile\n").unwrap();
let output = e
.cmd()
.args(["init", "--no-service"])
.write_stdin("y\ny\nn\n")
.assert()
.success()
.get_output()
.stdout
.clone();
let output = String::from_utf8(output).unwrap();
assert!(output.contains("Replace sccache with Kache for Cargo builds?"));
assert!(output.contains("Open a new terminal"));
assert!(!output.contains("Setup complete"));
assert!(!output.contains("PKGBUILD"));
assert!(
!e.home.join(".bash_profile").exists(),
"must not shadow .profile"
);
let configured = std::fs::read_to_string(&cargo).unwrap();
assert!(configured.contains("rustc-wrapper = \"kache\""));
assert!(configured.contains("HOST_CC = \"custom-cc\""));
assert!(configured.contains("HOST_CXX = \"kache c++\""));
let backup = std::fs::read_dir(cargo.parent().unwrap())
.unwrap()
.map(|entry| entry.unwrap().path())
.find(|path| {
path.file_name()
.unwrap()
.to_string_lossy()
.starts_with(".kache-cargo-backup-")
})
.unwrap();
assert_eq!(std::fs::read_to_string(backup).unwrap(), original);
let before = std::fs::read_to_string(e.home.join(".bashrc")).unwrap();
assert!(before.starts_with("# user rc\n"));
assert_eq!(before.matches("# >>> kache compiler cache >>>").count(), 1);
for name in ["cc", "c++", "gcc", "g++", "clang", "clang++"] {
assert_eq!(
std::fs::canonicalize(e.home.join(".local/lib/kache/shims").join(name)).unwrap(),
std::fs::canonicalize(KACHE_BIN).unwrap()
);
}
e.cmd()
.args(["init", "--no-service"])
.write_stdin("n\n")
.assert()
.success();
assert_eq!(
std::fs::read_to_string(e.home.join(".bashrc")).unwrap(),
before
);
assert_eq!(std::fs::read_to_string(&cargo).unwrap(), configured);
let shell = std::process::Command::new("/bin/bash")
.args([
"--noprofile",
"--rcfile",
e.home.join(".bashrc").to_str().unwrap(),
"-ic",
"command -v cc",
])
.env("HOME", &e.home)
.env("PATH", "/usr/bin:/bin")
.env_remove("BASH_ENV")
.output()
.unwrap();
assert!(shell.status.success());
assert_eq!(
String::from_utf8(shell.stdout).unwrap().trim(),
e.home.join(".local/lib/kache/shims/cc").to_str().unwrap()
);
}
#[cfg(unix)]
#[test]
fn init_zsh_uses_zdotdir_and_no_shell_skips_terminal_changes() {
let e = env();
let zdotdir = e.home.join("custom-zsh");
e.cmd()
.env("SHELL", "/bin/zsh")
.env("ZDOTDIR", &zdotdir)
.args(["init", "--no-service"])
.write_stdin("y\ny\nn\n")
.assert()
.success();
assert!(zdotdir.join(".zshrc").exists());
assert!(!e.home.join(".zshrc").exists());
if Path::new("/bin/zsh").exists() {
let output = std::process::Command::new("/bin/zsh")
.args(["-ic", "command -v cc"])
.env("HOME", &e.home)
.env("ZDOTDIR", &zdotdir)
.env("PATH", "/usr/bin:/bin")
.output()
.unwrap();
assert!(output.status.success());
assert_eq!(
String::from_utf8(output.stdout).unwrap().trim(),
e.home.join(".local/lib/kache/shims/cc").to_str().unwrap()
);
}
let skipped = env();
skipped
.cmd()
.args(["init", "--no-service", "--no-shell"])
.write_stdin("y\nn\n")
.assert()
.success();
assert!(skipped.home.join(".cargo/config.toml").exists());
assert!(!skipped.home.join(".local/lib/kache/shims").exists());
assert!(!skipped.home.join(".bashrc").exists());
}
#[test]
fn init_noninteractive_writes_isolated_cargo_config() {
let e = env();
let cargo_home = e.home.join(".cargo");
std::fs::create_dir_all(&cargo_home).unwrap();
e.cmd()
.args(["init", "--no-service"])
.write_stdin("y\nn\n")
.assert()
.success();
assert!(
cargo_home.join("config.toml").exists(),
"init should have written an isolated cargo config"
);
}
#[cfg(unix)]
#[test]
fn init_repairs_each_half_of_terminal_setup_and_reports_activation() {
let e = env();
let shims = e.home.join(".local/lib/kache/shims");
e.cmd()
.args(["init", "--no-service"])
.write_stdin("\ny\nn\n")
.assert()
.success();
let rc = e.home.join(".bashrc");
let configured = std::fs::read_to_string(&rc).unwrap();
std::fs::write(&rc, "# new user configuration\n").unwrap();
e.cmd()
.args(["init", "--no-service"])
.write_stdin("yes\nn\n")
.assert()
.success()
.stdout(predicates::str::contains(
"Enable C/C++ caching in new terminals?",
));
assert!(std::fs::read_to_string(&rc).unwrap().ends_with(&configured));
let repaired = std::fs::read_to_string(&rc).unwrap();
std::fs::remove_file(shims.join("cc")).unwrap();
e.cmd()
.args(["init", "--no-service"])
.write_stdin("y\nn\n")
.assert()
.success()
.stdout(predicates::str::contains(
"Enable C/C++ caching in new terminals?",
));
assert_eq!(std::fs::read_to_string(&rc).unwrap(), repaired);
assert_eq!(
std::fs::canonicalize(shims.join("cc")).unwrap(),
std::fs::canonicalize(KACHE_BIN).unwrap()
);
e.cmd()
.env(
"PATH",
std::env::join_paths([shims.as_path(), Path::new("/usr/bin"), Path::new("/bin")])
.unwrap(),
)
.args(["init", "--no-service"])
.write_stdin("n\n")
.assert()
.success()
.stdout(predicates::str::contains("Terminal C/C++ caching: active"))
.stdout(predicates::str::contains("Open a new terminal").not());
std::fs::remove_file(shims.join("cc")).unwrap();
std::fs::write(shims.join("cc"), "user-owned compiler").unwrap();
e.cmd()
.args(["init", "--no-service"])
.write_stdin("y\n")
.assert()
.failure()
.stderr(predicates::str::contains("existing files"));
assert_eq!(
std::fs::read_to_string(shims.join("cc")).unwrap(),
"user-owned compiler"
);
assert_eq!(std::fs::read_to_string(rc).unwrap(), repaired);
}
#[cfg(unix)]
#[test]
fn init_leaves_unsupported_shells_and_managed_dotfiles_alone() {
let e = env();
e.cmd()
.env("SHELL", "/bin/unsupported")
.args(["init", "--no-service"])
.write_stdin("n\nn\n")
.assert()
.success()
.stdout(predicates::str::contains("shell not supported"));
assert!(!e.home.join(".local/lib/kache/shims").exists());
let managed = e.home.join("managed-rc");
std::fs::write(&managed, "# managed config\n").unwrap();
std::os::unix::fs::symlink(&managed, e.home.join(".bashrc")).unwrap();
e.cmd()
.args(["init", "--no-service"])
.write_stdin("n\nn\n")
.assert()
.success()
.stdout(predicates::str::contains("No shell files were changed."));
assert_eq!(
std::fs::read_to_string(managed).unwrap(),
"# managed config\n"
);
assert!(!e.home.join(".bash_profile").exists());
assert!(!e.home.join(".local/lib/kache/shims").exists());
}
#[test]
fn init_writes_config_to_custom_cargo_home() {
let e = env();
let cargo_home = e.home.join("custom-cargo-home");
e.cmd()
.env("CARGO_HOME", &cargo_home)
.args(["init", "--no-service"])
.write_stdin("y\nn\n")
.assert()
.success();
assert!(
cargo_home.join("config.toml").exists(),
"init should write the wrapper config under $CARGO_HOME"
);
assert!(
!e.home.join(".cargo").join("config.toml").exists(),
"init must not touch ~/.cargo when CARGO_HOME points elsewhere"
);
}
#[test]
fn init_adds_native_caching_to_an_existing_rust_setup() {
let e = env();
let cargo = e.home.join(".cargo/config.toml");
std::fs::create_dir_all(cargo.parent().unwrap()).unwrap();
std::fs::write(&cargo, "[build]\nrustc-wrapper = \"kache\"\n").unwrap();
e.cmd()
.args(["init", "--no-service", "--no-shell"])
.write_stdin("y\nn\n")
.assert()
.success();
let config: toml::Value = toml::from_str(&std::fs::read_to_string(cargo).unwrap()).unwrap();
assert_eq!(config["build"]["rustc-wrapper"].as_str(), Some("kache"));
assert_eq!(
config["env"]["CC_KNOWN_WRAPPER_CUSTOM"].as_str(),
Some("kache")
);
#[cfg(unix)]
{
assert_eq!(config["env"]["HOST_CC"].as_str(), Some("kache cc"));
assert_eq!(config["env"]["HOST_CXX"].as_str(), Some("kache c++"));
}
#[cfg(windows)]
{
assert!(config["env"].get("HOST_CC").is_none());
assert!(config["env"].get("HOST_CXX").is_none());
}
}
#[cfg(unix)]
#[test]
fn init_ignores_empty_shell_config_directory_overrides() {
for (shell, config) in [
("/bin/zsh", ".zshrc"),
("/bin/fish", ".config/fish/config.fish"),
] {
let e = env();
e.cmd()
.env("SHELL", shell)
.env("ZDOTDIR", "")
.env("XDG_CONFIG_HOME", "")
.args(["init", "--no-service"])
.write_stdin("y\ny\nn\n")
.assert()
.success();
assert!(
std::fs::read_to_string(e.home.join(config))
.unwrap()
.contains("# >>> kache compiler cache >>>")
);
}
}
#[test]
fn daemon_without_subcommand_succeeds() {
let e = env();
e.cmd().arg("daemon").assert().success();
}
#[test]
fn daemon_status_subcommand_succeeds() {
let e = env();
let bare = e.cmd().arg("daemon").output().unwrap();
let explicit = e.cmd().args(["daemon", "status"]).output().unwrap();
assert!(bare.status.success(), "bare `kache daemon` failed");
assert_eq!(explicit.status, bare.status);
assert_eq!(explicit.stdout, bare.stdout);
let stdout = String::from_utf8_lossy(&bare.stdout);
assert!(stdout.contains("kache:"), "status body missing: {stdout}");
assert!(stdout.contains("Daemon:"), "daemon state missing: {stdout}");
}
#[test]
fn commands_operate_on_a_populated_cache() {
let e = env();
let project = scaffold_lib("kachetestlib", "pub fn answer() -> u8 { 42 }\n");
let target_dir = project.path().join("target");
let cold = e.wrapper_build(project.path(), &target_dir);
assert!(
cold.status.success(),
"cold build failed: {}",
String::from_utf8_lossy(&cold.stderr)
);
std::fs::remove_dir_all(&target_dir).ok();
let warm = e.wrapper_build(project.path(), &target_dir);
assert!(
warm.status.success(),
"warm build failed: {}",
String::from_utf8_lossy(&warm.stderr)
);
e.cmd()
.arg("list")
.env("KACHE_PAGER", "definitely-not-a-real-pager")
.assert()
.success()
.stdout(predicates::str::contains("kachetestlib"));
let out = e
.cmd()
.args(["report", "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).expect("valid json");
assert_eq!(v["schema_version"].as_u64(), Some(1));
assert_eq!(v["summary"]["local_hits"].as_u64(), Some(1), "report: {v}");
assert_eq!(v["summary"]["misses"].as_u64(), Some(1), "report: {v}");
assert!(
v["top_hits"]
.as_array()
.map(|a| !a.is_empty())
.unwrap_or(false),
"expected a non-empty top_hits list: {v}"
);
assert!(
v["storage"]["store_blobs"].as_u64().unwrap_or(0) >= 1,
"expected stored blobs: {v}"
);
e.cmd()
.arg("stats")
.assert()
.success()
.stdout(predicates::str::contains("Store:"))
.stdout(predicates::str::contains("Hit rate:"))
.stdout(predicates::str::contains("Dedup:"))
.stdout(predicates::str::contains("Time saved:"));
e.cmd()
.args(["doctor", "--verify", "--checksums"])
.assert()
.success();
e.cmd()
.args(["why-miss", "kachetestlib"])
.assert()
.success()
.stdout(predicates::str::contains("kachetestlib"))
.stdout(predicates::str::contains(
"Diagnosis: first build with these inputs -- entry is now cached",
));
e.cmd()
.args(["list", "kachetestlib"])
.env("KACHE_PAGER", "definitely-not-a-real-pager")
.assert()
.success()
.stdout(predicates::str::contains("Cache key:"))
.stdout(predicates::str::contains("kachetestlib"));
e.cmd()
.args(["purge", "--crate-name", "kachetestlib"])
.assert()
.success();
}
#[test]
fn why_miss_reports_key_mismatch_after_source_edit() {
let e = env();
let project = scaffold_lib("kachediff", "pub fn v() -> u8 { 1 }\n");
let target_dir = project.path().join("target");
assert!(
e.wrapper_build(project.path(), &target_dir)
.status
.success(),
"first build failed"
);
std::fs::remove_dir_all(&target_dir).ok();
std::fs::write(
project.path().join("src/lib.rs"),
"pub fn v() -> u32 { 123456 }\n",
)
.unwrap();
assert!(
e.wrapper_build(project.path(), &target_dir)
.status
.success(),
"second build failed"
);
e.cmd()
.args(["why-miss", "kachediff"])
.assert()
.success()
.stdout(predicates::str::contains("Why `kachediff` missed"))
.stdout(predicates::str::contains("Diagnosis: key mismatch"));
e.cmd()
.args(["list", "kachediff"])
.assert()
.success()
.stdout(predicates::str::contains("kachediff"));
}
#[test]
fn why_miss_legacy_repeated_same_key_does_not_claim_key_mismatch() {
let e = env();
write_legacy_miss_events(&e, "legacy-cc", &["same-key", "same-key"]);
let output = e
.cmd()
.args(["why-miss", "legacy-cc"])
.output()
.expect("run why-miss");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Diagnosis: repeated miss for the same cache key"),
"stdout: {stdout}"
);
assert!(
stdout.contains("the miss was not caused by a cache-key change"),
"stdout: {stdout}"
);
assert!(
!stdout.contains("Diagnosis: key mismatch"),
"stdout: {stdout}"
);
}
#[test]
fn why_miss_legacy_different_keys_remain_a_cold_miss() {
let e = env();
write_legacy_miss_events(&e, "legacy-cc", &["old-key", "new-key"]);
let output = e
.cmd()
.args(["why-miss", "legacy-cc"])
.output()
.expect("run why-miss");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Diagnosis: never cached -- first build of this crate"),
"stdout: {stdout}"
);
assert!(
!stdout.contains("repeated miss for the same cache key"),
"stdout: {stdout}"
);
}
#[test]
fn why_miss_legacy_nonconsecutive_same_key_does_not_claim_a_repeat() {
let e = env();
write_legacy_miss_events(&e, "legacy-cc", &["key-x", "key-y", "key-x"]);
e.cmd()
.args(["why-miss", "legacy-cc"])
.assert()
.success()
.stdout(predicates::str::contains("repeated miss for the same cache key").not());
}
#[test]
fn why_miss_legacy_prior_hit_does_not_claim_a_repeated_miss() {
let e = env();
write_legacy_events(
&e,
"legacy-cc",
&[("local_hit", "same-key"), ("miss", "same-key")],
);
e.cmd()
.args(["why-miss", "legacy-cc"])
.assert()
.success()
.stdout(predicates::str::contains("repeated miss for the same cache key").not());
}
#[test]
fn cc_link_invocation_passes_through() {
if !cc_available() {
eprintln!("skipping cc_link_invocation_passes_through: no cc on PATH");
return;
}
let e = env();
let project = TempDir::new().unwrap();
std::fs::write(project.path().join("main.c"), "int main(void){return 0;}\n").unwrap();
e.cmd()
.current_dir(project.path())
.args(["cc", "main.c", "-o", "app"])
.assert()
.success();
assert!(
project.path().join("app").exists() || project.path().join("app.exe").exists(),
"passthrough should have produced the linked binary"
);
let out = e
.cmd()
.args(["report", "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).expect("valid json");
assert!(
v["summary"]["passthroughs"].as_u64().unwrap_or(0) >= 1,
"report should record a passthrough: {v}"
);
}
#[test]
fn cc_compile_roundtrips_through_cache() {
if !cc_available() {
eprintln!("skipping cc_compile_roundtrips_through_cache: no cc on PATH");
return;
}
let e = env();
let project = TempDir::new().unwrap();
std::fs::write(
project.path().join("add.c"),
"int add(int a, int b){return a + b;}\n",
)
.unwrap();
e.cmd()
.current_dir(project.path())
.args(["cc", "-c", "add.c", "-o", "add.o"])
.assert()
.success();
assert!(
project.path().join("add.o").exists(),
"first compile output"
);
std::fs::remove_file(project.path().join("add.o")).unwrap();
e.cmd()
.current_dir(project.path())
.args(["cc", "-c", "add.c", "-o", "add.o"])
.assert()
.success();
assert!(
project.path().join("add.o").exists(),
"second compile should restore the object from cache"
);
let out = e
.cmd()
.args(["report", "--format", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let v: serde_json::Value = serde_json::from_slice(&out).expect("valid json");
assert!(
v["summary"]["local_hits"].as_u64().unwrap_or(0) >= 1,
"report should record a local hit after the second compile: {v}"
);
assert_eq!(
v["all_events"]
.as_array()
.and_then(|events| events.last())
.and_then(|event| event["preprocessor_runs"].as_u64()),
Some(0),
"plain warm compiles should reuse the private dependency memo: {v}"
);
}
#[test]
fn cc_compile_with_depinfo_roundtrips() {
if !cc_available() {
eprintln!("skipping cc_compile_with_depinfo_roundtrips: no cc on PATH");
return;
}
let e = env();
let project = TempDir::new().unwrap();
std::fs::write(project.path().join("dep.c"), "int dep(void){return 7;}\n").unwrap();
let args = ["cc", "-c", "dep.c", "-MMD", "-MF", "dep.d", "-o", "dep.o"];
e.cmd()
.current_dir(project.path())
.args(args)
.assert()
.success();
assert!(project.path().join("dep.o").exists(), "object produced");
assert!(project.path().join("dep.d").exists(), "depfile produced");
std::fs::remove_file(project.path().join("dep.o")).unwrap();
std::fs::remove_file(project.path().join("dep.d")).unwrap();
e.cmd()
.current_dir(project.path())
.args(args)
.assert()
.success();
assert!(
project.path().join("dep.o").exists(),
"object restored from cache"
);
assert!(
project.path().join("dep.d").exists(),
"depfile restored from cache"
);
}
#[test]
fn cc_entry_lacking_depfile_is_evicted_then_recached() {
if !cc_available() {
eprintln!("skipping cc_entry_lacking_depfile_is_evicted_then_recached: no cc on PATH");
return;
}
let e = env();
let project = TempDir::new().unwrap();
std::fs::write(project.path().join("ev.c"), "int ev(void){return 9;}\n").unwrap();
e.cmd()
.current_dir(project.path())
.args(["cc", "-c", "ev.c", "-o", "ev.o"])
.assert()
.success();
e.cmd()
.current_dir(project.path())
.args(["cc", "-c", "ev.c", "-MMD", "-MF", "ev.d", "-o", "ev.o"])
.assert()
.success();
assert!(
project.path().join("ev.d").exists(),
"recompile after eviction should produce the depfile"
);
std::fs::remove_file(project.path().join("ev.o")).unwrap();
std::fs::remove_file(project.path().join("ev.d")).unwrap();
e.cmd()
.current_dir(project.path())
.args(["cc", "-c", "ev.c", "-MMD", "-MF", "ev.d", "-o", "ev.o"])
.assert()
.success();
assert!(
project.path().join("ev.d").exists() && project.path().join("ev.o").exists(),
"re-cached entry should satisfy the depfile invocation on hit"
);
}
fn first_file_under(dir: &Path) -> Option<std::path::PathBuf> {
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for entry in std::fs::read_dir(&d).ok()?.flatten() {
let p = entry.path();
if p.is_dir() {
stack.push(p);
} else if p.is_file() {
return Some(p);
}
}
}
None
}
#[test]
fn doctor_repair_removes_corrupted_entries() {
let e = env();
let project = scaffold_lib("repairlib", "pub fn r() -> u8 { 5 }\n");
let target_dir = project.path().join("target");
assert!(
e.wrapper_build(project.path(), &target_dir)
.status
.success(),
"build failed"
);
let blobs = e.cache.join("store").join("blobs");
let blob = first_file_under(&blobs).expect("a stored blob should exist");
let mut perms = std::fs::metadata(&blob).unwrap().permissions();
perms.set_readonly(false);
std::fs::set_permissions(&blob, perms).unwrap();
std::fs::write(&blob, b"this is not the original blob content").unwrap();
e.cmd()
.args(["doctor", "--checksums", "--repair"])
.assert()
.success()
.stdout(predicates::str::contains("corrupted"))
.stdout(predicates::str::contains("Repairing: removing"));
e.cmd()
.arg("list")
.assert()
.success()
.stdout(predicates::str::contains("No cached entries"));
}
#[test]
fn gc_max_age_runs_eviction_sweep_on_populated_cache() {
let e = env();
let project = scaffold_lib("kachegc", "pub fn g() -> u8 { 7 }\n");
let target_dir = project.path().join("target");
assert!(
e.wrapper_build(project.path(), &target_dir)
.status
.success(),
"build failed"
);
e.cmd()
.arg("list")
.assert()
.success()
.stdout(predicates::str::contains("kachegc"));
e.cmd()
.args(["gc", "--max-age", "0h"])
.assert()
.success()
.stdout(predicates::str::contains("Store:"));
}
fn create_wrapper_script(dir: &Path, name: &str) -> std::path::PathBuf {
if cfg!(windows) {
let script_path = dir.join(format!("{}.bat", name));
std::fs::write(&script_path, "@%*\n").unwrap();
script_path
} else {
let script_path = dir.join(name);
std::fs::write(&script_path, "#!/bin/sh\nexec \"$@\"\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&script_path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script_path, perms).unwrap();
}
script_path
}
}
#[test]
fn workspace_wrapper_via_cargo_build_env_and_path() {
let e = env();
let project = scaffold_lib("kachewrap1", "pub fn w() -> u8 { 1 }\n");
let target_dir = project.path().join("target");
let path_dir = TempDir::new().unwrap();
let _script = create_wrapper_script(path_dir.path(), "mydriver");
let prev_path = std::env::var_os("PATH");
let mut new_path = std::ffi::OsString::new();
new_path.push(path_dir.path());
if let Some(ref p) = prev_path {
new_path.push(if cfg!(windows) { ";" } else { ":" });
new_path.push(p);
}
let build = hermetic_command(env!("CARGO"), &e.cache, Some(&e.cache.join("config.toml")))
.args(["build", "--lib"])
.current_dir(project.path())
.env("PATH", new_path)
.env("RUSTC_WRAPPER", KACHE_BIN)
.env("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", "mydriver")
.env_remove("RUSTC_WORKSPACE_WRAPPER")
.env("KACHE_LOG", "off")
.env("HOME", &e.home)
.env("KACHE_DAEMON_IDLE_TIMEOUT", "3")
.env("CARGO_TARGET_DIR", &target_dir)
.env("CARGO_INCREMENTAL", "0")
.output()
.expect("run cargo build");
assert!(
build.status.success(),
"build failed with stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&build.stdout),
String::from_utf8_lossy(&build.stderr)
);
}
#[test]
fn workspace_wrapper_via_cargo_config_and_path() {
let e = env();
let project = scaffold_lib("kachewrap2", "pub fn w() -> u8 { 2 }\n");
let target_dir = project.path().join("target");
let path_dir = TempDir::new().unwrap();
let _script = create_wrapper_script(path_dir.path(), "mydriver");
let cargo_dir = project.path().join(".cargo");
std::fs::create_dir_all(&cargo_dir).unwrap();
std::fs::write(
cargo_dir.join("config.toml"),
"[build]\nrustc-workspace-wrapper = \"mydriver\"\n",
)
.unwrap();
let prev_path = std::env::var_os("PATH");
let mut new_path = std::ffi::OsString::new();
new_path.push(path_dir.path());
if let Some(ref p) = prev_path {
new_path.push(if cfg!(windows) { ";" } else { ":" });
new_path.push(p);
}
let build = hermetic_command(env!("CARGO"), &e.cache, Some(&e.cache.join("config.toml")))
.args(["build", "--lib"])
.current_dir(project.path())
.env("PATH", new_path)
.env("RUSTC_WRAPPER", KACHE_BIN)
.env_remove("RUSTC_WORKSPACE_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER")
.env("KACHE_LOG", "off")
.env("HOME", &e.home)
.env("KACHE_DAEMON_IDLE_TIMEOUT", "3")
.env("CARGO_TARGET_DIR", &target_dir)
.env("CARGO_INCREMENTAL", "0")
.output()
.expect("run cargo build");
assert!(
build.status.success(),
"build failed with stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&build.stdout),
String::from_utf8_lossy(&build.stderr)
);
}
#[test]
fn sync_help_includes_allow_partial() {
let e = env();
e.cmd()
.args(["sync", "--help"])
.assert()
.success()
.stdout(predicates::str::contains("--allow-partial"));
}