use assert_cmd::Command;
use predicates::prelude::PredicateBooleanExt;
use std::path::Path;
use tempfile::TempDir;
#[allow(dead_code)]
mod common;
use common::{hermetic_command, stop_daemon};
const KACHE_BIN: &str = env!("CARGO_BIN_EXE_kache");
const DAEMON_IDLE_BACKSTOP_SECS: &str = "60";
fn kache(home: &Path, cache_dir: &Path) -> Command {
kache_as(Path::new(KACHE_BIN), home, cache_dir)
}
fn kache_as(program: &Path, home: &Path, cache_dir: &Path) -> Command {
Command::from(kache_process_as(program, home, cache_dir))
}
fn kache_process_as(program: &Path, home: &Path, cache_dir: &Path) -> std::process::Command {
let mut cmd = hermetic_command(program, 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", DAEMON_IDLE_BACKSTOP_SECS);
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 Drop for Env {
fn drop(&mut self) {
stop_daemon(
kache_process_as(Path::new(KACHE_BIN), &self.home, &self.cache)
.args(["daemon", "stop"]),
&self.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", DAEMON_IDLE_BACKSTOP_SECS)
.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 login_and_logout_need_a_planner() {
let e = env();
for command in ["login", "logout"] {
kache(&e.home, &e.cache)
.arg(command)
.env_remove("KACHE_PLANNER_ENDPOINT")
.assert()
.failure()
.stderr(predicates::str::contains("no planner is configured"));
}
}
#[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("stats"))
.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)
.stdout(predicates::str::is_empty());
}
#[test]
fn test_runner_passes_the_exit_code_through() {
let e = env();
let dir = TempDir::new_in(env!("CARGO_TARGET_TMPDIR")).unwrap();
let probe = dir.path().join(format!(
"probe-0123456789abcdef{}",
std::env::consts::EXE_SUFFIX
));
std::fs::hard_link(KACHE_BIN, &probe)
.or_else(|_| std::fs::copy(KACHE_BIN, &probe).map(drop))
.unwrap();
for scheduler in ["1", "0"] {
e.cmd()
.arg("test-runner")
.arg(&probe)
.arg("definitely-not-a-command")
.env("KACHE_SCHEDULER", scheduler)
.env_remove("KACHE_TEST_LEASE")
.env_remove("RUST_TEST_THREADS")
.env_remove("NEXTEST_EXECUTION_MODE")
.assert()
.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_interactive_or_mutating_commands() {
let e = env();
e.cmd()
.args(["--json", "monitor"])
.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() {
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");
kache_fs::testutil::write_executable(
&driver,
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$KANI_INVOCATIONS\"\nprintf 'kani:%s\\n' \"$*\"\n",
);
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::starts_with("kache · last 15m\n"));
e.cmd()
.args(["stats", "--full", "--since", "90m"])
.assert()
.success()
.stdout(predicates::str::starts_with(
"kache · last 90m · full report\n",
));
e.cmd()
.args(["report", "--format", "text", "--since", "90m"])
.assert()
.success()
.stdout(predicates::str::starts_with(
"kache · last 90m · full report\n",
));
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()
.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")
.and(predicates::str::contains("registered blob budget")),
);
let other = e.cache.join("other-config.toml");
std::fs::write(&other, "[cache]\nlocal_max_size = \"2GiB\"\n").unwrap();
e.cmd()
.env("KACHE_CONFIG", &other)
.arg("stats")
.assert()
.success()
.stdout(
predicates::str::contains("1.0 GiB")
.and(predicates::str::contains("registered blob budget")),
)
.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_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")
.and(predicates::str::contains("registered blob budget")),
)
.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().arg("stats").assert().success();
e.cmd()
.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_formats_and_full_index_diagnostics_are_distinct() {
let e = env();
let github = e
.cmd()
.args(["report", "--format", "github"])
.output()
.unwrap();
assert!(github.status.success());
let github = String::from_utf8(github.stdout).unwrap();
assert!(github.starts_with("### kache build cache\n"), "{github}");
assert!(!github.contains("Index diagnostics"), "{github}");
let text = e
.cmd()
.args(["report", "--format", "text"])
.output()
.unwrap();
assert!(text.status.success());
let text = String::from_utf8(text.stdout).unwrap();
assert!(text.contains("Index diagnostics"), "{text}");
assert!(!text.starts_with("### kache build cache\n"), "{text}");
}
#[test]
fn doctor_human_groups_each_check_once() {
let e = env();
let output = e.cmd().arg("doctor").output().unwrap();
assert!(output.status.success());
let text = String::from_utf8(output.stdout).unwrap();
let setup = text.find("\nSetup\n").unwrap();
let storage = text.find("\nStorage\n").unwrap();
let services = text.find("\nServices\n").unwrap();
let rows = text
.lines()
.filter(|line| line.starts_with(" RUSTC_WRAPPER "))
.collect::<Vec<_>>();
assert_eq!(rows.len(), 1, "{text}");
assert!(text[setup..storage].contains(rows[0]), "{text}");
assert!(!text[storage..].contains(rows[0]), "{text}");
assert!(text[storage..services].contains(" Cache dir "), "{text}");
assert!(text[services..].contains(" Daemon version "), "{text}");
}
#[test]
fn init_plan_matches_the_requested_components_and_keeps_prompt_lines_separate() {
let e = env();
let output = e
.cmd()
.args(["init", "--no-shell", "--no-service"])
.write_stdin("n\n")
.output()
.unwrap();
assert!(output.status.success());
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("[Y/n] \n Setup cancelled."), "{text}");
assert!(!text.contains(" Shell "), "{text}");
assert!(!text.contains(" Service "), "{text}");
assert!(!e.home.join(".cargo").join("config.toml").exists());
let output = e
.cmd()
.args(["init", "--check", "--no-shell", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let plan: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let steps = plan["steps"].as_array().unwrap();
assert!(!steps.iter().any(|step| step["component"] == "shell"));
assert!(
steps
.iter()
.any(|step| step["component"] == "login_service")
);
let text = String::from_utf8(output.stderr).unwrap();
assert!(text.contains(" Service "), "{text}");
assert!(!text.contains(" Shell "), "{text}");
}
#[cfg(unix)]
#[test]
fn init_shell_plan_lists_only_the_shell_paths() {
let e = env();
let output = e
.cmd()
.args(["init", "--check", "--no-service", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let plan: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let steps = plan["steps"].as_array().unwrap();
assert!(
!steps
.iter()
.any(|step| step["component"] == "login_service")
);
let shell = steps
.iter()
.find(|step| step["component"] == "shell")
.unwrap();
let text = String::from_utf8(output.stderr).unwrap();
assert!(text.contains(" Shell "), "{text}");
assert!(!text.contains(" Service "), "{text}");
let shell_files = text
.lines()
.filter(|line| line.starts_with(" Shell file "))
.collect::<Vec<_>>();
let paths = shell["paths"].as_array().unwrap();
assert_eq!(shell_files.len(), paths.len(), "{text}");
for path in paths {
let path = std::path::Path::new(path.as_str().unwrap());
assert!(
shell_files
.iter()
.any(|line| line.contains(path.file_name().unwrap().to_str().unwrap())),
"{text}"
);
}
assert!(!e.home.join(".bashrc").exists());
}
#[cfg(unix)]
fn terminal_output(mut command: std::process::Command, columns: u16) -> String {
use std::io::Read;
use std::os::fd::FromRawFd;
use std::os::unix::process::CommandExt;
use std::process::Stdio;
let (mut master, mut slave) = (-1, -1);
let mut size = libc::winsize {
ws_row: 24,
ws_col: columns,
ws_xpixel: 0,
ws_ypixel: 0,
};
assert_eq!(
unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
&raw mut size,
)
},
0
);
unsafe {
assert_eq!(libc::fcntl(master, libc::F_SETFD, libc::FD_CLOEXEC), 0);
assert_eq!(libc::fcntl(slave, libc::F_SETFD, libc::FD_CLOEXEC), 0);
}
let (mut master, slave) = unsafe {
(
std::fs::File::from_raw_fd(master),
std::fs::File::from_raw_fd(slave),
)
};
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1
|| libc::ioctl(libc::STDOUT_FILENO, libc::TIOCSCTTY as _, 0) == -1
{
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = command
.stdin(Stdio::null())
.stdout(slave.try_clone().unwrap())
.stderr(Stdio::piped())
.spawn()
.unwrap();
drop(command);
drop(slave);
let mut bytes = Vec::new();
let mut buffer = [0; 4096];
loop {
match master.read(&mut buffer) {
Ok(0) => break,
Ok(n) => bytes.extend_from_slice(&buffer[..n]),
Err(error) if error.raw_os_error() == Some(libc::EIO) => break,
Err(error) => panic!("reading terminal output: {error}"),
}
}
let output = child.wait_with_output().unwrap();
assert!(output.status.success(), "{output:?}");
String::from_utf8(bytes).unwrap().replace("\r\n", "\n")
}
#[cfg(unix)]
#[test]
fn terminal_color_policy_and_wrapping_match_the_actual_command() {
let e = env();
for (no_color, term, colored) in [
(None, "xterm", true),
(Some(""), "xterm", true),
(Some("1"), "xterm", false),
(None, "dumb", false),
] {
let mut command = kache_process_as(Path::new(KACHE_BIN), &e.home, &e.cache);
command
.args(["init", "--check", "--no-service", "--no-shell"])
.env_remove("NO_COLOR")
.env("TERM", term);
if let Some(no_color) = no_color {
command.env("NO_COLOR", no_color);
}
let text = terminal_output(command, 80);
assert_eq!(
text.contains('\x1b'),
colored,
"{no_color:?}, {term}: {text:?}"
);
assert!(text.contains("Setup plan"), "{text}");
}
let mut command = kache_process_as(Path::new(KACHE_BIN), &e.home, &e.cache);
command
.args(["init", "--check", "--no-service", "--no-shell"])
.env("NO_COLOR", "1")
.env("TERM", "xterm");
let text = terminal_output(command, 35);
assert!(text.contains("start or restart the\n"), "{text}");
assert!(text.contains("cache if needed"), "{text}");
}
#[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 stats_reports_lifetime_totals_from_the_savings_ledger() {
let e = env();
let stats_json = |e: &Env| -> serde_json::Value {
let output = e.cmd().args(["--json", "stats"]).output().unwrap();
assert!(output.status.success(), "{output:?}");
serde_json::from_slice(&output.stdout).unwrap()
};
assert!(stats_json(&e).get("lifetime").is_none());
e.cmd()
.arg("stats")
.assert()
.success()
.stdout(predicates::str::contains(" Since ").not());
std::fs::write(
e.cache.join("savings.json"),
r#"{"schema":1,"since":"2026-09-12T08:00:00Z","hits":10,"hit_compile_time_ms":5000,
"zero_copy_bytes":4096,"copied_bytes":1024,
"pruned_automatic_bytes":3072,"pruned_requested_bytes":2048}"#,
)
.unwrap();
let lifetime = stats_json(&e)["lifetime"].clone();
assert_eq!(lifetime["since"], "2026-09-12T08:00:00Z");
assert_eq!(lifetime["hits"], 10);
assert_eq!(lifetime["hit_compile_time_ms"], 5000);
assert_eq!(lifetime["zero_copy_bytes"], 4096);
assert_eq!(lifetime["copied_bytes"], 1024);
assert_eq!(lifetime["pruned_automatic_bytes"], 3072);
assert_eq!(lifetime["pruned_requested_bytes"], 2048);
e.cmd().arg("stats").assert().success().stdout(
predicates::str::contains("Lifetime")
.and(predicates::str::contains(
"compile work avoided over 10 hits",
))
.and(predicates::str::contains(
"3.0 KiB automatically, 2.0 KiB on request",
)),
);
}
#[test]
fn machine_readable_commands_emit_one_json_document() {
let e = env();
let workdir = TempDir::new().unwrap();
let commands: &[&[&str]] = &[
&["--json", "list"],
&["--json", "stats"],
&["stats", "--full", "--json"],
&["report", "--json"],
&["init", "--check", "--no-service", "--no-shell", "--json"],
&["--json", "gc"],
&["--json", "clean"],
&["--json", "targets"],
&["--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_eq!(value["success"], true, "{args:?}");
assert!(value["command"].is_string(), "{args:?}");
}
}
#[test]
fn json_failures_emit_one_document_and_nonzero_exit() {
let e = env();
for (args, exit, code) in [
(vec!["--json", "unknown-command"], 2, "invalid_arguments"),
(
vec!["stats", "--json", "--unknown-option"],
2,
"invalid_arguments",
),
(
vec!["stats", "--json", "--since", "soon"],
1,
"command_failed",
),
(vec!["init", "--json"], 1, "command_failed"),
(vec!["monitor", "--json"], 1, "command_failed"),
(
vec!["stats", "--json", "--format", "github"],
1,
"command_failed",
),
] {
let output = e.cmd().args(&args).output().unwrap();
assert_eq!(output.status.code(), Some(exit), "{args:?}: {output:?}");
let body: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(body["schema_version"], 1);
assert_eq!(body["success"], false);
assert_eq!(body["error"]["code"], code);
assert!(
body["error"]["message"]
.as_str()
.is_some_and(|message| !message.is_empty())
);
assert!(body["error"]["causes"].is_array());
assert!(body["command"].is_string());
}
let output = e
.cmd()
.args(["report", "--json", "--output"])
.arg(&e.home)
.output()
.unwrap();
assert!(!output.status.success());
let body: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(body["command"], "report");
assert_eq!(body["success"], false);
}
#[test]
fn json_init_previews_without_writes_and_applies_without_prompts() {
let e = env();
let args = ["init", "--json", "--no-service", "--no-shell"];
let preview = e.cmd().args(args).arg("--check").output().unwrap();
assert!(preview.status.success(), "{preview:?}");
let body: serde_json::Value = serde_json::from_slice(&preview.stdout).unwrap();
assert_eq!(body["command"], "init");
assert_eq!(body["preview"], true);
assert_eq!(body["steps"][0]["action"], "configure");
assert_eq!(
body["steps"][0]["paths"][0],
e.home.join(".cargo").join("config.toml").to_str().unwrap()
);
assert!(body["daemon_running"].is_null());
assert!(!e.home.join(".cargo/config.toml").exists());
assert!(!e.cache.join("daemon.sock").exists());
let applied = e.cmd().args(args).arg("--yes").output().unwrap();
assert!(applied.status.success(), "{applied:?}");
let body: serde_json::Value = serde_json::from_slice(&applied.stdout).unwrap();
assert_eq!(body["success"], true);
assert_eq!(body["preview"], false);
assert_eq!(body["cargo_configured"], true);
assert_eq!(body["daemon_running"], true);
assert_eq!(body["service_installed"], false);
assert!(e.home.join(".cargo/config.toml").exists());
assert!(!e.home.join(".bashrc").exists());
assert!(!String::from_utf8_lossy(&applied.stderr).contains("[Y/n]"));
}
#[test]
fn redirected_human_output_has_no_ansi_sequences() {
let e = env();
for args in [
vec!["stats"],
vec!["doctor"],
vec!["daemon", "status"],
vec!["init", "--check"],
] {
let output = e.cmd().args(&args).output().unwrap();
assert!(output.status.success(), "{args:?}: {output:?}");
assert!(!output.stdout.contains(&0x1b), "{args:?}: {output:?}");
}
}
#[test]
fn json_report_file_returns_a_receipt_on_stdout() {
let e = env();
let path = e.home.join("report.json");
let output = e
.cmd()
.args(["stats", "--full", "--json", "--output"])
.arg(&path)
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
let receipt: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(receipt["command"], "stats");
assert_eq!(receipt["output"], path.to_str().unwrap());
assert_eq!(receipt["format"], "json");
let report: serde_json::Value = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
assert!(report["summary"].is_object());
}
#[test]
fn json_help_and_version_are_machine_readable() {
let e = env();
let output = e.cmd().args(["--json", "--help"]).output().unwrap();
assert!(output.status.success());
let help: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(help["command"], "help");
assert_eq!(help["cli"]["name"], "kache");
let commands = help["cli"]["subcommands"].as_array().unwrap();
assert!(commands.iter().any(|command| command["name"] == "init"));
assert!(
!commands
.iter()
.any(|command| command["name"] == "test-runner")
);
let init = commands
.iter()
.find(|command| command["name"] == "init")
.unwrap();
assert!(
init["arguments"]
.as_array()
.unwrap()
.iter()
.any(|arg| arg["long"] == "yes")
);
let output = e.cmd().args(["--json", "--version"]).output().unwrap();
assert!(output.status.success());
let version: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(version["command"], "version");
assert!(version["version"].is_string());
}
#[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 targets_lists_a_tracked_target_and_flags_its_deleted_worktree() {
let e = env();
let project = scaffold_lib("ledger", "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)
);
let elsewhere = TempDir::new().unwrap();
let targets = || {
let output = e
.cmd()
.current_dir(elsewhere.path())
.args(["--json", "targets"])
.output()
.expect("run kache targets");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap()
};
let live = targets();
assert_eq!(live["command"], "targets");
assert_eq!(live["targets"][0]["path"], target.display().to_string());
assert_eq!(live["targets"][0]["state"], "live");
assert!(live["apparent_bytes"].as_u64().unwrap() > 0, "{live}");
assert!(live.get("next").is_none(), "{live}");
let text = e
.cmd()
.current_dir(elsewhere.path())
.arg("targets")
.output()
.expect("run kache targets");
assert!(
String::from_utf8_lossy(&text.stdout).contains("1 tracked target directory"),
"{}",
String::from_utf8_lossy(&text.stdout)
);
std::fs::remove_dir_all(project.path()).unwrap();
let orphaned = targets();
assert_eq!(orphaned["targets"][0]["state"], "worktree_deleted");
assert_eq!(orphaned["next"][0]["argv"][2], "--orphans", "{orphaned}");
}
#[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::starts_with(
"Remove nothing: no target matches",
));
e.cmd()
.arg("clean")
.arg(workdir.path())
.arg("--dry-run")
.assert()
.success()
.stdout(predicates::str::contains("No target/ directories under"));
}
#[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()
.arg("clean")
.arg(root)
.arg("--dry-run")
.assert()
.success()
.stdout(predicates::str::starts_with(
"Remove 1 target directory under",
))
.stdout(predicates::str::contains(
" · 10.0 KiB on disk · 10.0 KiB freeable",
))
.stdout(predicates::str::contains("target [debug, release]"));
}
#[cfg(unix)]
#[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()
.arg("clean")
.arg(root)
.arg("--dry-run")
.assert()
.success()
.stdout(predicates::str::contains(
" · 8.0 KiB on disk · 4.0 KiB freeable",
));
}
#[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", DAEMON_IDLE_BACKSTOP_SECS)
.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()
.failure()
.stderr(predicates::str::contains("rerun with --yes"));
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_rejects_unavailable_daemon_replacement() {
use std::io::{BufRead, Write};
use std::os::unix::net::{UnixListener, UnixStream};
let e = env();
let socket = e.cache.join("daemon.sock");
let listener = UnixListener::bind(&socket).unwrap();
let run_lock = std::fs::File::create(e.cache.join("daemon.run.lock")).unwrap();
run_lock.lock().unwrap();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
stream
.set_read_timeout(Some(std::time::Duration::from_secs(10)))
.unwrap();
let mut request = String::new();
std::io::BufReader::new(&stream)
.read_line(&mut request)
.unwrap();
assert!(request.contains("health"), "{request}");
drop(listener);
let response = serde_json::json!({
"ok": true, "health": { "version": "old", "build_epoch": 1 }
});
writeln!(stream, "{response}").unwrap();
});
let output = e
.cmd()
.args(["init", "--yes", "--no-service", "--no-shell"])
.env("KACHE_SOCKET_PATH", &socket)
.env("KACHE_LOG", "warn")
.timeout(std::time::Duration::from_secs(60))
.output()
.unwrap();
drop(UnixStream::connect(&socket));
server.join().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(!output.status.success(), "{stdout}");
assert!(!stdout.contains("Background cache: running"), "{stdout}");
assert!(stdout.contains("Background cache setup failed"), "{stdout}");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("daemon did not start after recovery"),
"{stderr}"
);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn init_upgrade_keeps_the_service_manager_in_charge() {
use std::io::{BufRead, Write};
use std::os::unix::net::UnixListener;
let e = env();
let socket = e.cache.join("daemon.sock");
let listener = UnixListener::bind(&socket).unwrap();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
stream
.set_read_timeout(Some(std::time::Duration::from_secs(10)))
.unwrap();
let mut request = String::new();
std::io::BufReader::new(&stream)
.read_line(&mut request)
.unwrap();
assert!(request.contains("health"), "{request}");
drop(listener);
writeln!(
stream,
"{{\"ok\":true,\"health\":{{\"version\":\"old\",\"build_epoch\":1}}}}"
)
.unwrap();
});
let (tool, service_file) = if cfg!(target_os = "macos") {
(
"launchctl",
e.home.join("Library/LaunchAgents/ninja.kunobi.kache.plist"),
)
} else {
(
"systemctl",
e.home.join(".config/systemd/user/kache.service"),
)
};
std::fs::create_dir_all(service_file.parent().unwrap()).unwrap();
std::fs::write(service_file, "installed test service").unwrap();
let user_config = e.home.join(".config/kache/config.toml");
std::fs::create_dir_all(user_config.parent().unwrap()).unwrap();
std::fs::write(
&user_config,
format!("[cache]\nlocal_store = {:?}\n", e.cache.to_str().unwrap()),
)
.unwrap();
let bin = e.home.join("bin");
std::fs::create_dir_all(&bin).unwrap();
let manager = bin.join(tool);
kache_fs::testutil::write_executable(
&manager,
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$KACHE_TEST_SERVICE_ARGS\"\n\"$KACHE_TEST_BIN\" daemon run >/dev/null 2>&1 &\necho $! > \"$KACHE_TEST_SERVICE_PID\"\n",
);
let path = std::env::join_paths(std::iter::once(bin).chain(std::env::split_paths(
&std::env::var_os("PATH").unwrap_or_default(),
)))
.unwrap();
let marker = e.home.join("service.pid");
let output = e
.cmd()
.args(["init", "--yes", "--no-service", "--no-shell"])
.env("PATH", path)
.env("KACHE_TEST_BIN", KACHE_BIN)
.env("KACHE_TEST_SERVICE_PID", &marker)
.env("KACHE_TEST_SERVICE_ARGS", e.home.join("service.args"))
.timeout(std::time::Duration::from_secs(30))
.output()
.unwrap();
drop(std::os::unix::net::UnixStream::connect(&socket));
server.join().unwrap();
let state = std::fs::read(e.cache.join("daemon.state.json"));
e.cmd().args(["daemon", "stop"]).output().unwrap();
assert!(output.status.success(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stdout).contains("Background cache: running"));
let pid: u32 = std::fs::read_to_string(marker)
.expect("upgrade bypassed service manager")
.trim()
.parse()
.unwrap();
let state: serde_json::Value = serde_json::from_slice(&state.unwrap()).unwrap();
assert_eq!(state["pid"], pid);
let args = std::fs::read_to_string(e.home.join("service.args")).unwrap();
assert!(!args.lines().any(|arg| arg == "-k" || arg == "restart"));
if cfg!(target_os = "macos") {
assert_eq!(args.lines().next(), Some("kickstart"));
} else {
assert_eq!(
args.lines().collect::<Vec<_>>(),
["--user", "start", "kache.service"]
);
}
}
#[test]
fn daemon_restart_preserves_another_cache_daemon() {
let a = env();
let b = env();
b.cmd().args(["daemon", "start"]).assert().success();
let before = std::fs::read(b.cache.join("daemon.state.json")).unwrap();
let before: serde_json::Value = serde_json::from_slice(&before).unwrap();
a.cmd().args(["daemon", "start"]).assert().success();
let own_before: serde_json::Value =
serde_json::from_slice(&std::fs::read(a.cache.join("daemon.state.json")).unwrap()).unwrap();
let restart = a.cmd().args(["daemon", "restart"]).output().unwrap();
let other = b
.cmd()
.args(["daemon", "status", "--json"])
.output()
.unwrap();
let after = std::fs::read(b.cache.join("daemon.state.json"));
let own_after = std::fs::read(a.cache.join("daemon.state.json"));
a.cmd().args(["daemon", "stop"]).output().unwrap();
b.cmd().args(["daemon", "stop"]).output().unwrap();
assert!(
restart.status.success(),
"{}",
String::from_utf8_lossy(&restart.stderr)
);
let other: serde_json::Value = serde_json::from_slice(&other.stdout).unwrap();
assert_eq!(other["daemon_running"], true, "{other}");
let after: serde_json::Value = serde_json::from_slice(&after.unwrap()).unwrap();
assert_eq!(before["pid"], after["pid"]);
let own_after: serde_json::Value = serde_json::from_slice(&own_after.unwrap()).unwrap();
assert_ne!(
own_before["pid"], own_after["pid"],
"explicit restart must replace a compatible owner"
);
}
#[test]
fn a_failing_test_still_stops_its_daemon() {
let (lock_tx, lock_rx) = std::sync::mpsc::channel();
let failed = std::thread::spawn(move || {
let e = env();
e.cmd().args(["daemon", "start"]).assert().success();
let run_lock = std::fs::OpenOptions::new()
.write(true)
.open(e.cache.join("daemon.run.lock"))
.expect("opening the live daemon's lifetime lock");
assert!(
matches!(run_lock.try_lock(), Err(std::fs::TryLockError::WouldBlock)),
"the running daemon must hold its lifetime lock"
);
lock_tx.send(run_lock).unwrap();
panic!("simulated test failure while the daemon runs");
})
.join();
assert!(failed.is_err(), "the fixture thread must have panicked");
let run_lock = lock_rx
.recv()
.expect("the daemon never started; see the fixture's panic above");
run_lock
.try_lock()
.expect("Env's Drop must stop the daemon while the test unwinds");
run_lock.unlock().unwrap();
}
#[cfg(target_os = "linux")]
fn fake_systemd_path(dir: &Path, script: &str) -> std::ffi::OsString {
std::fs::create_dir_all(dir).unwrap();
for (name, body) in [("systemctl", script), ("loginctl", "exit 0")] {
let tool = dir.join(name);
kache_fs::testutil::write_executable(&tool, format!("#!/bin/sh\n{body}\n"));
}
let path = std::env::var_os("PATH").unwrap_or_default();
std::env::join_paths(std::iter::once(dir.to_path_buf()).chain(std::env::split_paths(&path)))
.unwrap()
}
#[cfg(target_os = "linux")]
#[test]
fn init_without_user_systemd_skips_the_login_service() {
let e = env();
e.cmd()
.env(
"PATH",
fake_systemd_path(&e.home.join("fake-bin"), "exit 1"),
)
.args(["init", "--no-shell"])
.write_stdin("y\nn\n")
.assert()
.success()
.stdout(predicates::str::contains("Login service: unavailable"))
.stdout(predicates::str::contains("Start Kache automatically at login?").not());
assert!(e.home.join(".cargo/config.toml").exists());
assert!(!e.home.join(".config/systemd/user/kache.service").exists());
}
#[cfg(target_os = "linux")]
#[test]
fn init_with_user_systemd_installs_the_login_service() {
let e = env();
e.cmd()
.env(
"PATH",
fake_systemd_path(&e.home.join("fake-bin"), "exit 0"),
)
.args(["init", "--no-shell"])
.write_stdin("y\ny\nn\n")
.assert()
.success()
.stdout(predicates::str::contains("Service installed"))
.stdout(predicates::str::contains("still starting"));
assert!(e.home.join(".config/systemd/user/kache.service").exists());
}
#[cfg(target_os = "linux")]
#[test]
fn init_updates_a_login_service_that_runs_another_binary() {
let e = env();
let unit = e.home.join(".config/systemd/user/kache.service");
std::fs::create_dir_all(unit.parent().unwrap()).unwrap();
std::fs::write(&unit, "[Service]\nExecStart=/old/kache daemon run\n").unwrap();
e.cmd()
.env(
"PATH",
fake_systemd_path(&e.home.join("fake-bin"), "exit 0"),
)
.args(["init", "--no-shell"])
.write_stdin("y\ny\n")
.assert()
.success()
.stdout(predicates::str::contains("Background service: update"))
.stdout(predicates::str::contains("Service installed"));
let updated = std::fs::read_to_string(&unit).unwrap();
assert!(
!updated.contains("/old/kache"),
"unit was not rewritten: {updated}"
);
}
#[cfg(target_os = "linux")]
#[test]
fn init_continues_when_the_login_service_cannot_be_installed() {
let e = env();
let refuse_unit = r#"[ "$2" = show-environment ] && exit 0; echo refused >&2; exit 1"#;
e.cmd()
.env(
"PATH",
fake_systemd_path(&e.home.join("fake-bin"), refuse_unit),
)
.args(["init", "--no-shell"])
.write_stdin("y\ny\nn\n")
.assert()
.success()
.stdout(predicates::str::contains(
"Login service: not installed (systemctl --user daemon-reload failed: refused)",
))
.stdout(predicates::str::contains("Background cache: running"));
assert!(e.home.join(".cargo/config.toml").exists());
assert!(!e.home.join(".config/systemd/user/kache.service").exists());
}
#[cfg(unix)]
#[test]
fn init_exports_the_test_runner_for_the_host() {
let sysroot = std::process::Command::new("rustc")
.args(["--print", "sysroot"])
.output()
.unwrap();
let rustc = Path::new(String::from_utf8(sysroot.stdout).unwrap().trim()).join("bin/rustc");
let version = std::process::Command::new(&rustc)
.arg("-vV")
.output()
.unwrap();
let version = String::from_utf8(version.stdout).unwrap();
let host = version
.lines()
.find_map(|line| line.strip_prefix("host: "))
.unwrap();
let var = format!(
"CARGO_TARGET_{}_RUNNER",
host.to_ascii_uppercase().replace(['-', '.'], "_")
);
let export = format!("{var}='kache test-runner'; export {var}");
let init = |e: &Env, args: &[&str], runner: Option<&str>, stdin: &str| {
let mut command = e.cmd();
command
.arg("init")
.args(args)
.env("RUSTC", &rustc)
.env_remove(&var)
.write_stdin(stdin.to_owned());
if let Some(runner) = runner {
command.env(&var, runner);
}
let output = command.assert().success().get_output().stdout.clone();
String::from_utf8(output).unwrap()
};
let bashrc = |e: &Env| std::fs::read_to_string(e.home.join(".bashrc")).unwrap_or_default();
let e = env();
let out = init(&e, &["--no-service", "--no-shell"], None, "y\nn\n");
assert!(out.contains("Test pacing: skipped (--no-shell)"), "{out}");
let out = init(&e, &["--no-service", "--check"], None, "");
assert!(
out.contains(&format!("Would set {var} in new terminals.")),
"{out}"
);
assert!(!e.home.join(".bashrc").exists());
let out = init(&e, &["--no-service"], None, "y\n");
assert_eq!(out.matches("[Y/n]").count(), 1, "{out}");
assert!(
out.contains("Test pacing: configured for new terminals"),
"{out}"
);
assert!(
out.contains("Open a new terminal to pace test binaries."),
"{out}"
);
let saved = bashrc(&e);
assert!(saved.contains("# >>> kache test runner >>>"), "{saved}");
assert!(saved.contains(&export), "{saved}");
assert!(saved.contains("kache compiler cache"), "{saved}");
let profile = std::fs::read_to_string(e.home.join(".bash_profile")).unwrap();
assert!(profile.contains(&export), "{profile}");
let out = init(&e, &["--no-service"], Some("kache test-runner"), "y\n");
assert!(out.contains("Test pacing: active"), "{out}");
assert!(!out.contains("Pace test binaries"), "{out}");
assert!(!out.contains("pace test binaries."), "{out}");
let out = init(&e, &["--no-service"], Some("qemu-user"), "y\n");
assert!(
out.contains(&format!("sets {var}=\"qemu-user\", which stays")),
"{out}"
);
assert_eq!(bashrc(&e), saved);
let declined = env();
let out = init(&declined, &["--no-service"], None, "n\n");
assert!(out.contains("Setup cancelled"), "{out}");
assert!(!bashrc(&declined).contains("kache test runner"));
let other = env();
let mut command = other.cmd();
command
.args(["init", "--no-service"])
.env("RUSTC", other.home.join("no-rustc"))
.write_stdin("y\nn\nn\n");
let out = command.assert().success().get_output().stdout.clone();
let out = String::from_utf8(out).unwrap();
assert!(
out.contains("rustc did not report its host target"),
"{out}"
);
let mut command = other.cmd();
command
.args(["init", "--no-service"])
.env("RUSTC", &rustc)
.env("SHELL", "/bin/tcsh")
.write_stdin("y\n");
let out = command.assert().success().get_output().stdout.clone();
let out = String::from_utf8(out).unwrap();
assert!(
out.contains(&format!("Export {var}=\"kache test-runner\"")),
"{out}"
);
std::fs::write(
other.home.join(".bashrc"),
"# >>> kache test runner >>>\nno end marker\n",
)
.unwrap();
let out = init(&other, &["--no-service"], None, "y\n");
assert!(out.contains("Test pacing changed no shell files."), "{out}");
assert!(!bashrc(&other).contains(&export));
}
#[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\nprintf 'user shell startup\\n'\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\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_eq!(output.matches("[Y/n]").count(), 2, "{output}");
assert!(!output.contains("Cache cargo doc as well?"));
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 parsed: toml::Value = toml::from_str(&configured).unwrap();
let rustdoc = parsed["build"]["rustdoc"]
.as_str()
.expect("init sets build.rustdoc when the shim exists");
let rustdoc_path = std::path::Path::new(rustdoc);
assert!(rustdoc_path.is_absolute(), "{rustdoc}");
assert_eq!(
rustdoc_path.file_name().and_then(|name| name.to_str()),
Some("rustdoc")
);
assert!(rustdoc_path.is_file(), "{rustdoc}");
let mut backups: Vec<String> = std::fs::read_dir(cargo.parent().unwrap())
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| {
path.file_name()
.unwrap()
.to_string_lossy()
.starts_with(".kache-cargo-backup-")
})
.map(|path| std::fs::read_to_string(path).unwrap())
.collect();
backups.sort_by_key(|backup| backup.contains("rustc-wrapper = \"kache\""));
assert_eq!(backups.len(), 2, "{backups:?}");
assert_eq!(backups[0], original);
assert!(backups[1].contains("rustc-wrapper = \"kache\""));
assert!(!backups[1].contains("rustdoc"));
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 resolved_cc = e.home.join("resolved-cc");
let bash = std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default())
.map(|dir| dir.join("bash"))
.find(|path| path.is_file())
.expect("Bash on test PATH");
let shell = std::process::Command::new(bash)
.args([
"--noprofile",
"--rcfile",
e.home.join(".bashrc").to_str().unwrap(),
"-ic",
"command -v cc > \"$KACHE_TEST_CC_PATH\"",
])
.env("HOME", &e.home)
.env("PATH", "/usr/bin:/bin")
.env("KACHE_TEST_CC_PATH", &resolved_cc)
.env_remove("BASH_ENV")
.output()
.unwrap();
assert!(shell.status.success());
assert_eq!(
std::fs::read_to_string(resolved_cc).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("Apply this setup?"));
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("Apply this setup?"));
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(
std::iter::once(shims.clone())
.chain(std::env::split_paths(&std::env::var_os("PATH").unwrap())),
)
.unwrap(),
)
.args(["init", "--no-service"])
.write_stdin("y\n")
.assert()
.success()
.stdout(predicates::str::contains(
"Cargo target protection and C/C++ caching: active",
))
.stdout(predicates::str::contains("Open a new terminal to activate C/C++ caching.").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("y\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("y\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());
}
#[cfg(unix)]
fn second_kache_binary(link: &Path) {
std::fs::create_dir_all(link.parent().unwrap()).unwrap();
if std::fs::hard_link(KACHE_BIN, link).is_err() {
std::fs::copy(KACHE_BIN, link).unwrap();
}
}
#[cfg(unix)]
fn shim_test_path(root: &Path, dirs: &[&Path]) -> std::ffi::OsString {
let real = root.join("real");
std::fs::create_dir_all(&real).unwrap();
let cc = real.join("cc");
kache_fs::testutil::write_executable(&cc, "#!/bin/sh\necho \"real cc: $*\"\n");
let mut path: Vec<&Path> = dirs.to_vec();
path.extend([real.as_path(), Path::new("/usr/bin"), Path::new("/bin")]);
std::env::join_paths(path).unwrap()
}
#[cfg(unix)]
fn run_shim_command(mut cmd: std::process::Command) -> assert_cmd::assert::Assert {
use assert_cmd::assert::OutputAssertExt;
use std::os::unix::process::CommandExt;
use std::time::{Duration, Instant};
let out = TempDir::new().unwrap();
let stdout = out.path().join("stdout");
let stderr = out.path().join("stderr");
cmd.process_group(0)
.stdin(std::process::Stdio::null())
.stdout(std::fs::File::create(&stdout).unwrap())
.stderr(std::fs::File::create(&stderr).unwrap());
let mut child = cmd.spawn().unwrap();
let timeout = Duration::from_secs(60);
let deadline = Instant::now() + timeout;
let status = loop {
if let Some(status) = child.try_wait().unwrap() {
break status;
}
if Instant::now() >= deadline {
unsafe { libc::kill(-(child.id() as libc::pid_t), libc::SIGKILL) };
child.wait().unwrap();
panic!(
"still running after {timeout:?}, killed its process group. stderr:\n{}",
std::fs::read_to_string(&stderr).unwrap_or_default()
);
}
std::thread::sleep(Duration::from_millis(20));
};
std::process::Output {
status,
stdout: std::fs::read(&stdout).unwrap(),
stderr: std::fs::read(&stderr).unwrap(),
}
.assert()
}
#[cfg(unix)]
#[test]
fn two_kache_installs_on_path_do_not_run_each_other() {
let e = env();
let root = tempfile::Builder::new()
.tempdir_in(Path::new(KACHE_BIN).parent().unwrap())
.unwrap();
let root = root.path();
let other_kache = root.join("other-install/bin/kache");
second_kache_binary(&other_kache);
let shims_a = root.join("shims-a");
let shims_b = root.join("shims-b");
std::fs::create_dir_all(&shims_a).unwrap();
std::fs::create_dir_all(&shims_b).unwrap();
std::os::unix::fs::symlink(KACHE_BIN, shims_a.join("cc")).unwrap();
std::os::unix::fs::symlink(&other_kache, shims_b.join("cc")).unwrap();
let path = shim_test_path(root, &[&shims_a, &shims_b]);
for shim in [&shims_a, &shims_b] {
let mut cmd = kache_process_as(&shim.join("cc"), &e.home, &e.cache);
cmd.env("PATH", &path).arg("--version");
run_shim_command(cmd)
.success()
.stdout(predicates::str::contains("real cc: --version"));
}
}
#[cfg(unix)]
#[test]
fn unidentifiable_shims_stop_at_the_depth_bound_until_marked() {
let e = env();
let root = tempfile::Builder::new()
.tempdir_in(Path::new(KACHE_BIN).parent().unwrap())
.unwrap();
let root = root.path();
let copies = root.join("copies");
second_kache_binary(&copies.join("cc"));
let hidden = root.join("hidden/cc");
second_kache_binary(&hidden);
let scripts = root.join("scripts");
std::fs::create_dir_all(&scripts).unwrap();
let script = scripts.join("cc");
kache_fs::testutil::write_executable(
&script,
format!(
"#!/bin/sh\n\
n=$(cat '{rounds}' 2>/dev/null || echo 0)\n\
n=$((n + 1))\n\
echo \"$n\" > '{rounds}'\n\
if [ \"$n\" -gt 32 ]; then echo 'shim test: gave up after 32 rounds' >&2; exit 97; fi\n\
exec '{hidden}' \"$@\"\n",
rounds = root.join("rounds").display(),
hidden = hidden.display(),
),
);
let path = shim_test_path(root, &[&copies, &scripts]);
let run = || {
let mut cmd = kache_process_as(&copies.join("cc"), &e.home, &e.cache);
cmd.env("PATH", &path).arg("--version");
run_shim_command(cmd)
};
run()
.failure()
.stderr(predicates::str::contains("loop"))
.stderr(predicates::str::contains(".kache-shims"));
std::fs::write(scripts.join(".kache-shims"), "").unwrap();
run()
.success()
.stdout(predicates::str::contains("real cc: --version"));
}
#[cfg(unix)]
#[test]
fn a_self_spawned_gc_behind_a_shim_runs_gc() {
let e = env();
let root = TempDir::new().unwrap();
let shims = root.path().join("shims");
std::fs::create_dir_all(&shims).unwrap();
std::os::unix::fs::symlink(KACHE_BIN, shims.join("cc")).unwrap();
let path = shim_test_path(root.path(), &[&shims]);
kache_as(&shims.join("cc"), &e.home, &e.cache)
.env("PATH", &path)
.env("KACHE_AUTO_GC_WORKER", "1")
.env("KACHE_SELF_SPAWN", "gc")
.arg("gc")
.timeout(std::time::Duration::from_secs(60))
.assert()
.success()
.stdout(predicates::str::contains("real cc").not());
kache_as(&shims.join("cc"), &e.home, &e.cache)
.env("PATH", &path)
.env("KACHE_AUTO_GC_WORKER", "1")
.arg("gc")
.timeout(std::time::Duration::from_secs(60))
.assert()
.stdout(predicates::str::contains("real cc: gc"));
}
#[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++"));
assert!(config["build"].get("rustdoc").is_none());
}
#[cfg(windows)]
{
assert!(config["env"].get("HOST_CC").is_none());
assert!(config["env"].get("HOST_CXX").is_none());
}
}
#[cfg(unix)]
#[test]
fn init_check_does_not_write_build_rustdoc() {
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(["install-shims"]).assert().success();
let before = std::fs::read_to_string(&cargo).unwrap();
e.cmd()
.args(["init", "--check", "--no-service", "--yes"])
.assert()
.success();
assert_eq!(std::fs::read_to_string(&cargo).unwrap(), before);
assert!(!before.contains("rustdoc"));
}
#[cfg(unix)]
#[test]
fn init_without_a_shell_edit_leaves_rustdoc_unset_even_when_the_shim_exists() {
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(["install-shims"]).assert().success();
e.cmd()
.args(["init", "--no-service", "--no-shell", "--yes"])
.assert()
.success();
let config: toml::Value = toml::from_str(&std::fs::read_to_string(&cargo).unwrap()).unwrap();
assert!(config["build"].get("rustdoc").is_none());
}
#[cfg(unix)]
#[test]
fn init_leaves_an_existing_rustdoc_path_in_place() {
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\"\nrustdoc = \"/usr/bin/rustdoc\"\n",
)
.unwrap();
e.cmd().args(["install-shims"]).assert().success();
e.cmd()
.args(["init", "--no-service", "--yes"])
.assert()
.success();
let config = std::fs::read_to_string(&cargo).unwrap();
assert!(config.contains("rustdoc = \"/usr/bin/rustdoc\""));
assert_eq!(config.matches("rustdoc =").count(), 1);
}
#[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("\n Cache "))
.stdout(predicates::str::contains("\n Hit rate "))
.stdout(predicates::str::contains("\n Dedup "))
.stdout(predicates::str::contains("\n 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"
);
let first_diagnosis = e
.cmd()
.args(["--json", "why-miss", "kachediff"])
.output()
.unwrap();
assert!(first_diagnosis.status.success());
let first_diagnosis: serde_json::Value =
serde_json::from_slice(&first_diagnosis.stdout).unwrap();
let first_key = first_diagnosis["cache_key"].as_str().unwrap();
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"))
.stdout(predicates::str::contains(format!(
"same config as {} -- likely source code, dependency, or rustc version change",
&first_key[..12],
)));
e.cmd()
.args(["list", "kachediff"])
.assert()
.success()
.stdout(predicates::str::contains("kachediff"));
}
#[test]
fn why_miss_compares_features_from_a_volume_shard() {
let e = env();
let project = scaffold_lib("shardfeatures", "pub fn value() -> u32 { 42 }\n");
let target = project.path().join("target");
let shard = e.cache.join("volume-store");
let config = "[cache]\nlocal_only = true\nscheduler = false\ndaemon_publish = false\n";
std::fs::write(e.cache.join("config.toml"), config).unwrap();
let manifest_path = project.path().join("Cargo.toml");
let manifest = std::fs::read_to_string(&manifest_path).unwrap();
for feature in ["old", "new"] {
if feature == "new" {
std::fs::write(
e.cache.join("config.toml"),
format!(
"{config}[cache.volumes]\n{} = {}\n",
serde_json::to_string(&project.path().display().to_string()).unwrap(),
serde_json::to_string(&shard.display().to_string()).unwrap(),
),
)
.unwrap();
}
std::fs::write(
&manifest_path,
format!("{manifest}\n[features]\ndefault = [\"{feature}\"]\nold = []\nnew = []\n"),
)
.unwrap();
let build = e.wrapper_build(project.path(), &target);
assert!(
build.status.success(),
"{feature}: {}",
String::from_utf8_lossy(&build.stderr)
);
}
e.cmd()
.args(["why-miss", "shardfeatures"])
.assert()
.success()
.stdout(predicates::str::contains("different features vs"))
.stdout(predicates::str::contains(" features: default, new"))
.stdout(predicates::str::contains(shard.display().to_string()));
let output = e
.cmd()
.args(["why-miss", "shardfeatures", "--json"])
.output()
.unwrap();
assert!(output.status.success());
let diagnosis: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(diagnosis["stored_entries"], 2);
assert_eq!(diagnosis["store_dirs"], serde_json::json!([shard]));
}
#[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 doctor_repair_drops_entries_with_unparseable_metadata() {
let e = env();
let project = scaffold_lib("metarepair", "pub fn m() -> u8 { 6 }\n");
let target_dir = project.path().join("target");
assert!(
e.wrapper_build(project.path(), &target_dir)
.status
.success(),
"build failed"
);
let store = e.cache.join("store");
let metas: Vec<_> = std::fs::read_dir(&store)
.unwrap()
.flatten()
.map(|entry| entry.path().join("meta.json"))
.filter(|meta| meta.is_file())
.collect();
assert!(!metas.is_empty(), "the build should have stored an entry");
for meta in &metas {
std::fs::write(meta, b"not json").unwrap();
}
e.cmd()
.args(["doctor", "--repair"])
.assert()
.success()
.stdout(predicates::str::contains(format!(
"Repairing: removed {} entries that failed verification.",
metas.len()
)));
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);
kache_fs::testutil::write_executable(&script_path, "#!/bin/sh\nexec \"$@\"\n");
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", DAEMON_IDLE_BACKSTOP_SECS)
.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", DAEMON_IDLE_BACKSTOP_SECS)
.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"));
}
#[test]
fn why_miss_json_preserves_legacy_repeat_uncertainty() {
let e = env();
write_legacy_miss_events(&e, "legacy-cc", &["same-key", "same-key"]);
let output = e
.cmd()
.args(["--json", "why-miss", "legacy-cc"])
.output()
.unwrap();
assert!(output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(value["diagnosis"], "repeated_same_key");
assert_eq!(value["dependency_recording_missing"], true);
assert!(value["dependency_chain"].is_null());
}
#[test]
fn why_miss_both_formats_prioritize_store_failure_over_lookup_and_cold_history() {
let e = env();
let event = serde_json::json!({
"ts": "2026-01-01T00:00:00Z", "crate_name": "example",
"result": "miss", "elapsed_ms": 1, "size": 1,
"cache_key": "same-key", "schema": 15,
"store_error": "disk full", "lookup_rejection": "invalid metadata"
});
std::fs::write(e.cache.join("events.jsonl"), format!("{event}\n")).unwrap();
e.cmd()
.args(["why-miss", "example"])
.assert()
.success()
.stdout(predicates::str::contains("NOT CACHED"))
.stdout(predicates::str::contains("disk full"))
.stdout(predicates::str::contains("Diagnosis: never cached").not())
.stdout(predicates::str::contains("Diagnosis: matching key").not());
let output = e
.cmd()
.args(["--json", "why-miss", "example"])
.output()
.unwrap();
assert!(output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(value["diagnosis"], "not_cached");
assert_eq!(value["store_error"], "disk full");
assert_eq!(value["lookup_rejection"], "invalid metadata");
}
#[test]
fn why_miss_both_formats_include_unresolved_dependency_cascade() {
let e = env();
let events: Vec<_> = ["before", "after"]
.iter()
.enumerate()
.map(|(index, digest)| {
serde_json::json!({
"ts": format!("2026-01-01T00:00:0{index}Z"), "crate_name": "example",
"result": "miss", "elapsed_ms": 1, "size": 1,
"cache_key": digest, "schema": 15, "root": "/project",
"key_externs_recorded": true, "key_externs": {"dep": digest}
})
.to_string()
})
.collect();
std::fs::write(
e.cache.join("events.jsonl"),
format!("{}\n", events.join("\n")),
)
.unwrap();
e.cmd()
.args(["why-miss", "example"])
.assert()
.success()
.stdout(predicates::str::contains("Dependency cascade:"))
.stdout(predicates::str::contains("unresolved: dep"));
let output = e
.cmd()
.args(["--json", "why-miss", "example"])
.output()
.unwrap();
assert!(output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(value["dependency_recording_missing"], false);
assert_eq!(value["dependency_chain"]["direct"][0]["name"], "dep");
assert_eq!(value["dependency_chain"]["direct"][0]["from"], "before");
assert_eq!(value["dependency_chain"]["direct"][0]["to"], "after");
assert_eq!(value["dependency_chain"]["roots"][0]["crate_name"], "dep");
assert_eq!(
value["dependency_chain"]["roots"][0]["kind"]["kind"],
"no_miss_recorded"
);
assert!(value["checkout_comparison"].is_null());
assert!(value["dependency_chain"]["baseline_root"].is_null());
}
fn write_checkout_builds(e: &Env, builds: &[(&str, &str, &str)]) {
let mut events = Vec::new();
for (minute, (root, leaf_env, leaf_out)) in builds.iter().enumerate() {
let mid_out = format!("mid-out-{leaf_out}");
for (offset, name, env_deps, externs, extern_units, key) in [
(
0,
"leaf",
*leaf_env,
serde_json::json!({}),
serde_json::json!({}),
format!("leaf-{leaf_env}"),
),
(
1,
"mid",
"e",
serde_json::json!({"leaf": leaf_out}),
serde_json::json!({"leaf": "uleaf"}),
format!("mid-{leaf_out}"),
),
(
2,
"app",
"e",
serde_json::json!({"mid": mid_out}),
serde_json::json!({"mid": "umid"}),
format!("app-{mid_out}"),
),
] {
events.push(
serde_json::json!({
"ts": format!("2026-01-01T00:0{minute}:0{offset}Z"), "crate_name": name,
"result": "miss", "elapsed_ms": 1, "size": 1,
"cache_key": key, "schema": 19, "root": root,
"key_fields": {"env_deps": env_deps, "sources": "s"},
"key_externs_recorded": true, "key_externs": externs,
"unit_id": format!("u{name}"), "extern_units": extern_units
})
.to_string(),
);
}
}
std::fs::write(
e.cache.join("events.jsonl"),
format!("{}\n", events.join("\n")),
)
.unwrap();
}
#[test]
fn why_miss_compares_a_second_checkout_with_the_first() {
let e = env();
write_checkout_builds(
&e,
&[
("/checkouts/a", "e1", "leaf-out-e1"),
("/checkouts/b", "e2", "leaf-out-e2"),
],
);
e.cmd()
.args(["why-miss", "app"])
.assert()
.success()
.stdout(predicates::str::contains(
"Other build tree: no earlier build of this crate in /checkouts/b",
))
.stdout(predicates::str::contains(
"compared with the same unit built in: /checkouts/a",
))
.stdout(predicates::str::contains("dependencies differ: mid"))
.stdout(predicates::str::contains(
"root: leaf -- own inputs changed: env_deps",
))
.stdout(predicates::str::contains("via: app <- mid <- leaf"));
let output = e
.cmd()
.args(["--json", "why-miss", "leaf"])
.output()
.unwrap();
assert!(output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
value["checkout_comparison"]["baseline_root"],
"/checkouts/a"
);
assert_eq!(value["checkout_comparison"]["verdict"], "own_inputs");
assert_eq!(value["checkout_comparison"]["groups"][0], "env_deps");
assert_eq!(value["dependency_recording_missing"], false);
let output = e
.cmd()
.args(["--json", "why-miss", "app"])
.output()
.unwrap();
assert!(output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(value["checkout_comparison"]["verdict"], "dependencies");
assert_eq!(value["dependency_chain"]["baseline_root"], "/checkouts/a");
assert_eq!(value["dependency_chain"]["roots"][0]["crate_name"], "leaf");
}
#[test]
fn why_miss_reports_a_path_only_root_across_checkouts() {
let e = env();
write_checkout_builds(
&e,
&[
("/checkouts/a", "e1", "leaf-out-a"),
("/checkouts/b", "e1", "leaf-out-b"),
],
);
e.cmd()
.args(["why-miss", "app"])
.assert()
.success()
.stdout(predicates::str::contains(
"root: leaf -- same key in both build trees, but its artifact differs",
));
e.cmd()
.args(["why-miss", "leaf"])
.assert()
.success()
.stdout(predicates::str::contains(
"same cache key in both build trees: no key input differs",
));
let output = e
.cmd()
.args(["--json", "why-miss", "app"])
.output()
.unwrap();
assert!(output.status.success());
let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(
value["dependency_chain"]["roots"][0]["kind"]["kind"],
"path_only"
);
}
#[cfg(unix)]
#[test]
fn daemon_start_refuses_a_socket_path_too_long_for_a_unix_socket() {
let home = TempDir::new().unwrap();
let root = TempDir::new().unwrap();
let deep = root.path().join("x".repeat(60)).join("y".repeat(60));
std::fs::create_dir_all(&deep).unwrap();
let output = kache(home.path(), &deep)
.args(["daemon", "start"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success(), "daemon start must fail: {stderr}");
assert!(
stderr.contains("a Unix socket path can be at most"),
"{stderr}"
);
assert!(stderr.contains("KACHE_SOCKET_PATH"), "{stderr}");
assert!(!stderr.contains("exited before readiness"), "{stderr}");
assert!(
!deep.join("daemon.state.json").exists(),
"no daemon may have started"
);
}
#[cfg(unix)]
fn longest_bindable_socket_path(dir: &Path) -> usize {
let mut longest = 0;
for total in dir.as_os_str().len() + 2..=200 {
let path = dir.join("s".repeat(total - dir.as_os_str().len() - 1));
let Ok(listener) = std::os::unix::net::UnixListener::bind(&path) else {
break;
};
drop(listener);
std::fs::remove_file(&path).unwrap();
longest = total;
}
assert!(longest > 0, "no socket binds under {}", dir.display());
longest
}
#[cfg(unix)]
#[test]
fn daemon_start_needs_no_more_path_than_its_own_socket() {
let home = TempDir::new().unwrap();
let root = TempDir::new().unwrap();
let limit = longest_bindable_socket_path(root.path());
let name = limit - root.path().as_os_str().len() - 1 - "/daemon.sock".len();
let runtime = root.path().join("d".repeat(name));
std::fs::create_dir_all(&runtime).unwrap();
assert_eq!(runtime.join("daemon.sock").as_os_str().len(), limit);
let start = kache(home.path(), &runtime)
.args(["daemon", "start"])
.output()
.unwrap();
let bound = runtime.join("daemon.sock").exists();
kache(home.path(), &runtime)
.args(["daemon", "stop"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&start.stderr);
assert!(start.status.success(), "{stderr}");
assert!(bound, "the daemon must have bound its socket: {stderr}");
}
#[test]
fn the_target_info_probe_seeds_a_new_checkout() {
let e = env();
e.cmd().args(["daemon", "start"]).assert().success();
let root = TempDir::new().unwrap();
let donor = root.path().join("donor");
std::fs::create_dir_all(donor.join("src")).unwrap();
let manifest = "[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n";
std::fs::write(donor.join("Cargo.toml"), manifest).unwrap();
std::fs::write(donor.join("src/lib.rs"), "pub fn f() {}\n").unwrap();
let donor_target = donor.join("target");
let built = e.wrapper_build(&donor, &donor_target);
assert!(built.status.success(), "{built:?}");
let profile = donor_target.join("debug");
let hash = "0123456789abcdef";
let per_unit = !profile.join(".fingerprint").is_dir();
let marker = |target: &Path| {
if per_unit {
target.join("debug/build/dep").join(hash)
} else {
target
.join("debug/.fingerprint")
.join(format!("dep-{hash}"))
}
};
let donor_marker = marker(&donor_target);
std::fs::create_dir_all(donor_marker.join("fingerprint")).unwrap();
std::fs::write(donor_marker.join("fingerprint/lib"), "fp").unwrap();
let lock = "[[package]]\nname = \"app\"\nversion = \"0.1.0\"\n\n\
[[package]]\nname = \"dep\"\nversion = \"1.0.0\"\n\
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n";
let probe = |checkout: &Path, seed: &str| {
std::fs::create_dir_all(checkout).unwrap();
std::fs::write(checkout.join("Cargo.toml"), manifest).unwrap();
std::fs::write(checkout.join("Cargo.lock"), lock).unwrap();
e.cmd()
.current_dir(checkout)
.env("KACHE_SEED_NEW_TARGETS", seed)
.env_remove("CARGO_TARGET_DIR")
.env_remove("CARGO_BUILD_TARGET_DIR")
.env_remove("CARGO_BUILD_BUILD_DIR")
.args(["rustc", "-", "--crate-name", "___", "--print=file-names"])
.args(["--crate-type", "lib"])
.write_stdin("")
.assert()
.success();
};
let seeded = root.path().join("seeded");
probe(&seeded, "1");
if !marker(&seeded.join("target")).is_dir() {
let recorded = std::fs::read_to_string(donor_target.join(".rustc_info.json"))
.unwrap_or_else(|error| format!("unreadable: {error}"));
let probed = std::process::Command::new("rustc")
.arg("-vV")
.current_dir(&seeded)
.env("HOME", &e.home)
.env("CARGO_HOME", e.home.join(".cargo"))
.output()
.map(|out| String::from_utf8_lossy(&out.stdout).into_owned())
.unwrap_or_else(|error| format!("failed: {error}"));
let tracked = e.cmd().args(["targets", "--json"]).output().unwrap();
panic!(
"the unit was not copied\n\
donor .rustc_info.json: {recorded}\n\
probe rustc -vV: {probed}\n\
tracked targets: {}",
String::from_utf8_lossy(&tracked.stdout)
);
}
let off = root.path().join("off");
probe(&off, "0");
assert!(!off.join("target").exists());
}
#[test]
fn daemon_readiness_channel_precedes_a_fresh_running_status() {
use kunobi_daemon::launch::DaemonCommand;
use std::time::{Duration, Instant};
let e = env();
let configured = kache_process_as(Path::new(KACHE_BIN), &e.home, &e.cache);
let mut command = DaemonCommand::new(KACHE_BIN);
command.args(["daemon", "run"]).readiness_channel();
for (name, value) in configured.get_envs() {
match value {
Some(value) => {
command.env(name, value);
}
None => {
command.env_remove(name);
}
}
}
let mut child = command.spawn().unwrap();
let mut readiness = child.take_readiness().unwrap();
readiness.wait(Duration::from_secs(10)).unwrap();
assert!(readiness.progress_reports() >= 1);
let status = e
.cmd()
.args(["daemon", "status", "--json"])
.output()
.unwrap();
let stopped = e.cmd().args(["daemon", "stop"]).output().unwrap();
assert!(
stopped.status.success(),
"{}",
String::from_utf8_lossy(&stopped.stderr)
);
assert!(
child
.wait_until(Instant::now() + Duration::from_secs(5))
.unwrap()
.unwrap()
.success()
);
assert!(
status.status.success(),
"{}",
String::from_utf8_lossy(&status.stderr)
);
let status: serde_json::Value = serde_json::from_slice(&status.stdout).unwrap();
assert_eq!(status["daemon_running"], true, "{status}");
}
fn write_diff_sessions(e: &Env, sessions: &[(&str, &[&str])]) {
let mut lines = Vec::new();
let mut second = 1u32;
for &(session, crates) in sessions {
for crate_name in crates {
lines.push(
serde_json::json!({
"ts": format!("2026-01-01T00:00:{second:02}Z"),
"root": "/w",
"session_id": session,
"crate_name": crate_name,
"result": "miss",
"elapsed_ms": 1,
"size": 1,
"cache_key": crate_name,
"schema": 22
})
.to_string(),
);
second += 1;
}
}
std::fs::write(e.cache.join("events.jsonl"), lines.join("\n") + "\n").unwrap();
}
#[test]
fn diff_exits_when_the_later_session_misses_more() {
let e = env();
write_diff_sessions(&e, &[("s1", &["a"]), ("s2", &["b", "c"])]);
e.cmd()
.arg("diff")
.assert()
.failure()
.stdout(predicates::str::contains("EXCESS later misses 2 earlier 1"));
}
#[test]
fn diff_is_quiet_when_the_sessions_match() {
let e = env();
write_diff_sessions(&e, &[("s1", &["a"]), ("s2", &["b"])]);
e.cmd()
.arg("diff")
.assert()
.success()
.stdout(predicates::str::contains("EXCESS").not())
.stdout(predicates::str::contains("misses 1 -> 1"));
}