use super::*;
use std::fs;
#[test]
fn doctor_link_layout_uses_the_mapped_store_and_leaves_missing_shards_absent() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::test_support::test_config(dir.path().join("main"));
let build = dir.path().join("build");
fs::create_dir(&build).unwrap();
let volume = format!("{}{}", build.display(), std::path::MAIN_SEPARATOR);
#[cfg(windows)]
let volume = volume.replace('/', "\\").to_uppercase();
let mapping = crate::config::VolumeStore {
volume,
store: dir.path().join("shard"),
max_size: Some(1000),
};
config.volume_stores.push(mapping.clone());
let missing = doctor_link_layout(&config, &build);
assert!(!missing.pass);
assert!(missing.detail.contains("unavailable; link probe skipped"));
assert!(!mapping.store.exists());
Store::open(&config.for_volume_store(&mapping, |_| None)).unwrap();
let mapped = doctor_link_layout(&config, &build);
assert!(mapped.pass, "{}", mapped.detail);
let store = crate::link_probe::shown_path(&mapping.store);
assert!(mapped.detail.contains(&store), "{}", mapped.detail);
assert!(mapped.fix.is_none());
config.volume_stores.clear();
let unmapped = doctor_link_layout(&config, &build);
assert!(unmapped.pass, "{}", unmapped.detail);
assert!(
unmapped
.detail
.contains(&crate::link_probe::shown_path(&config.cache_dir))
);
let failed = doctor_link_layout(&config, &dir.path().join("absent-build"));
assert!(!failed.pass);
assert!(failed.fix.unwrap().contains("SAME mount"));
}
#[test]
fn shutdown_summary_status_distinguishes_partial_totals_from_cancellation() {
assert_eq!(summary_status_suffix(false, false), "");
assert_eq!(summary_status_suffix(true, false), ", CANCELLED");
assert_eq!(
summary_status_suffix(false, true),
", INCOMPLETE (partial totals)"
);
assert_eq!(
summary_status_suffix(true, true),
", CANCELLED, INCOMPLETE (partial totals)"
);
}
fn timeline_record(session_id: &str, started_at_ms: u64) -> kache_core::timeline::BuildTimeline {
kache_core::timeline::BuildTimeline {
schema: kache_core::timeline::BUILD_TIMELINE_SCHEMA,
client_record_id: format!("r-{session_id}"),
session_id: session_id.to_string(),
started_at_ms,
..Default::default()
}
}
#[test]
fn the_default_selection_is_the_last_build() {
let records = vec![timeline_record("old", 1), timeline_record("new", 2)];
let selected = select_timelines(records.clone(), &TimelineSelection::Latest);
assert_eq!(
selected
.iter()
.map(|r| r.session_id.as_str())
.collect::<Vec<_>>(),
["new"]
);
assert_eq!(
select_timelines(records.clone(), &TimelineSelection::All).len(),
2
);
assert_eq!(
select_timelines(records, &TimelineSelection::Session("old".to_string()))
.iter()
.map(|r| r.session_id.as_str())
.collect::<Vec<_>>(),
["old"]
);
}
#[test]
fn selecting_from_no_records_sends_nothing() {
for selection in [
TimelineSelection::Latest,
TimelineSelection::All,
TimelineSelection::Session("s".to_string()),
] {
assert!(select_timelines(Vec::new(), &selection).is_empty());
}
assert!(
select_timelines(
vec![timeline_record("s1", 1)],
&TimelineSelection::Session("other".to_string())
)
.is_empty()
);
}
#[test]
fn labels_are_key_value_pairs() {
let parsed = parse_labels(&[
"phase=cold".to_string(),
" scenario = firefox ".to_string(),
"empty=".to_string(),
])
.unwrap();
assert_eq!(parsed["phase"], "cold");
assert_eq!(parsed["scenario"], "firefox");
assert_eq!(parsed["empty"], "");
for bad in ["nope", "=value"] {
assert!(parse_labels(&[bad.to_string()]).is_err(), "{bad}");
}
}
#[test]
fn an_unstamped_log_says_the_wrapper_is_too_old() {
let versions = std::collections::BTreeSet::from(["0.23.1".to_string()]);
let message = nothing_to_send(12, &versions);
assert!(message.contains("12 compiles in the log"), "{message}");
assert!(message.contains("0.23.1"), "{message}");
assert!(message.contains("Upgrade kache"), "{message}");
assert!(nothing_to_send(1, &versions).starts_with("1 compile in the log"));
assert_eq!(
nothing_to_send(0, &Default::default()),
"no builds in the event log"
);
assert!(
nothing_to_send(3, &Default::default()).contains("unknown version"),
"a log with no version still explains itself"
);
}
#[test]
fn a_push_without_logs_reports_that_and_succeeds() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
telemetry_push(&config, &TimelineSelection::Latest, &[], true)
.expect("an empty log directory is not an error");
}
#[test]
fn detail_fields_render_only_nonempty_values() {
let mut lines = Vec::new();
push_nonempty_detail(&mut lines, " Type: ", "");
assert!(lines.is_empty());
push_nonempty_detail(&mut lines, " Type: ", "lib");
push_nonempty_detail(&mut lines, " Features: ", "serde, std");
assert_eq!(
lines,
vec![
" Type: lib".to_string(),
" Features: serde, std".to_string(),
]
);
}
struct FailOnWrite {
fail_on_call: usize,
calls: usize,
}
impl std::io::Write for FailOnWrite {
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
self.calls += 1;
if self.calls == self.fail_on_call {
Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"pager exited",
))
} else {
Ok(buffer.len())
}
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn pager_line_writer_stops_on_content_or_newline_errors() {
let lines = vec!["first".to_string(), "second".to_string()];
let mut output = Vec::new();
assert!(write_pager_lines(&mut output, &lines));
assert_eq!(output, b"first\nsecond\n");
let mut content_error = FailOnWrite {
fail_on_call: 1,
calls: 0,
};
assert!(!write_pager_lines(&mut content_error, &lines));
assert_eq!(content_error.calls, 1);
let mut newline_error = FailOnWrite {
fail_on_call: 2,
calls: 0,
};
assert!(!write_pager_lines(&mut newline_error, &lines));
assert_eq!(newline_error.calls, 2);
}
#[test]
fn pager_resolution_obeys_tty_disable_and_environment_precedence() {
assert_eq!(
resolve_pager_argv(false, true, Some("most -R"), Some("less -S"), false),
Some(vec!["most".to_string(), "-R".to_string()])
);
assert_eq!(
resolve_pager_argv(false, true, None, Some("less -S"), false),
Some(vec!["less".to_string(), "-S".to_string()])
);
assert_eq!(
resolve_pager_argv(true, true, Some("less"), None, false),
None
);
assert_eq!(
resolve_pager_argv(false, false, Some("less"), None, false),
None
);
assert_eq!(
resolve_pager_argv(false, true, Some(""), Some("less"), false),
None
);
assert_eq!(
resolve_pager_argv(false, true, Some("cat"), Some("less"), false),
None
);
assert_eq!(
resolve_pager_argv(false, true, None, Some("cat"), false),
None
);
}
#[test]
fn pager_resolution_uses_platform_defaults() {
assert_eq!(
resolve_pager_argv(false, true, None, None, false),
Some(vec!["less".to_string(), "-FRX".to_string()])
);
assert_eq!(
resolve_pager_argv(false, true, None, None, true),
Some(vec!["more.com".to_string()])
);
}
#[test]
fn pager_resolution_groups_quotes_without_shell_evaluation() {
assert_eq!(
resolve_pager_argv(
false,
true,
Some(r#""C:\Program Files\Git\usr\bin\less.exe" -FRX"#),
None,
true
),
Some(vec![
r"C:\Program Files\Git\usr\bin\less.exe".to_string(),
"-FRX".to_string()
])
);
assert_eq!(
resolve_pager_argv(
false,
true,
Some("less --prompt='literal ; $HOME | text'"),
None,
false
),
Some(vec![
"less".to_string(),
"--prompt=literal ; $HOME | text".to_string()
])
);
assert_eq!(
resolve_pager_argv(false, true, Some("less 'unterminated"), None, false),
None
);
}
#[test]
fn stats_names_the_host_config_only_when_in_effect() {
use crate::config::HostConfigStatus;
let path = std::path::PathBuf::from("/etc/kache/config.toml");
assert_eq!(
host_config_in_effect(&HostConfigStatus::Present {
path: path.clone(),
keys: Vec::new(),
})
.as_deref(),
Some("/etc/kache/config.toml")
);
assert_eq!(host_config_in_effect(&HostConfigStatus::Disabled), None);
assert_eq!(
host_config_in_effect(&HostConfigStatus::Absent { path: path.clone() }),
None
);
assert_eq!(
host_config_in_effect(&HostConfigStatus::Invalid {
path,
error: "parsing host config".to_string(),
}),
None
);
}
#[test]
fn remote_access_reports_the_explained_cause() {
let ok = remote_access_from(Ok(()), None, None);
assert!(ok.pass);
assert_eq!(ok.detail, "reachable; credentials accepted");
assert_eq!(ok.fix, None);
let expired: anyhow::Error = opendal::Error::new(
opendal::ErrorKind::PermissionDenied,
r#"S3Error { code: "ExpiredToken" }"#,
)
.into();
let failed = remote_access_from(Err(expired), Some("us-east-1"), None);
assert!(!failed.pass);
assert!(failed.fix.unwrap().contains("expired"));
let unknown = remote_access_from(Err(anyhow::anyhow!("connection refused")), None, None);
assert!(!unknown.pass);
assert!(unknown.detail.contains("connection refused"));
assert_eq!(
unknown.fix.as_deref(),
Some("check the endpoint, bucket, region and credentials")
);
}
#[test]
fn remote_access_names_the_credential_source_and_a_stopped_chain() {
let ok = remote_access_from(Ok(()), None, Some("EC2 instance metadata"));
assert!(ok.pass);
assert_eq!(
ok.detail,
"reachable; credentials from EC2 instance metadata accepted"
);
let failure = crate::remote_backend::CredentialFailure::Profile {
name: "build-cache".to_string(),
selected_by: "AWS_PROFILE",
problem: "it is not defined in /aws/config or /aws/credentials".to_string(),
};
let stopped = anyhow::Error::new(opendal::Error::new(
opendal::ErrorKind::Unexpected,
"signing http request",
))
.context("GET s3://bucket/kache-doctor-probe")
.context(failure.clone());
let failed = remote_access_from(Err(stopped), Some("us-east-1"), None);
assert!(!failed.pass);
assert_eq!(failed.detail, failure.to_string());
assert_eq!(
failed.fix.as_deref(),
Some(
"fix the profile, or unset AWS_PROFILE so kache can use web identity, ECS or EC2 \
instance credentials"
)
);
}
struct Silent;
#[async_trait::async_trait]
impl crate::remote_backend::RemoteBackend for Silent {
async fn head(&self, _key: &str) -> Result<bool> {
std::future::pending().await
}
async fn get(
&self,
_key: &str,
_max_bytes: Option<u64>,
) -> Result<Option<crate::remote_backend::GetObject>> {
std::future::pending().await
}
async fn put(&self, _key: &str, _body: Vec<u8>, _content_type: Option<&str>) -> Result<()> {
std::future::pending().await
}
async fn list(&self, _prefix: &str) -> Result<Vec<String>> {
std::future::pending().await
}
fn describe(&self, key: &str) -> String {
key.to_string()
}
}
#[tokio::test]
async fn a_remote_that_never_answers_fails_the_probe() {
assert_eq!(REMOTE_PROBE_TIMEOUT, std::time::Duration::from_secs(10));
let error = probe_remote(&Silent, "probe", std::time::Duration::from_millis(20))
.await
.unwrap_err();
assert!(error.to_string().contains("no answer within"), "{error:#}");
let backend = crate::remote_backend::memory_backend();
probe_remote(&backend, "probe", std::time::Duration::from_secs(1))
.await
.unwrap();
}
#[test]
fn doctor_host_config_check_wording() {
use crate::config::{HostConfigKey, HostConfigStatus, HostKeySource};
let path = std::path::PathBuf::from("/etc/kache/config.toml");
assert!(doctor_host_config_check(&HostConfigStatus::Disabled).0);
let (pass, detail, fix) =
doctor_host_config_check(&HostConfigStatus::Absent { path: path.clone() });
assert!(pass);
assert!(
detail.contains("none at /etc/kache/config.toml"),
"{detail}"
);
assert!(fix.is_none());
let (pass, detail, fix) = doctor_host_config_check(&HostConfigStatus::Invalid {
path: path.clone(),
error: "parsing host config".to_string(),
});
assert!(!pass);
assert!(detail.contains("is ignored"), "{detail}");
assert!(fix.unwrap().contains("fix or remove"));
let (pass, detail, _) = doctor_host_config_check(&HostConfigStatus::Present {
path,
keys: vec![
HostConfigKey {
key: "cache.input_predictions".to_string(),
source: HostKeySource::Host,
},
HostConfigKey {
key: "cache.local_max_size".to_string(),
source: HostKeySource::ChosenFile("/job/kache-action.toml".into()),
},
HostConfigKey {
key: "cache.local_only".to_string(),
source: HostKeySource::Env("KACHE_LOCAL_ONLY"),
},
],
});
assert!(pass);
assert!(detail.contains("cache.input_predictions, "), "{detail}");
assert!(
detail.contains("cache.local_max_size (overridden by /job/kache-action.toml)"),
"{detail}"
);
assert!(
detail.contains("cache.local_only (overridden by KACHE_LOCAL_ONLY)"),
"{detail}"
);
}
#[test]
fn doctor_install_path_check_wording() {
let mut selection = kache_shims::Selection {
path: "/home/me/.nix-profile/bin/kache".into(),
kind: kache_shims::Kind::Nix,
stability: kache_shims::Stability::InstallerManaged,
reason:
"the Nix profile in /home/me/.nix-profile/bin, which each upgrade or rollback repoints"
.into(),
};
let (pass, detail, fix) = doctor_install_path_check(&Ok(selection.clone()));
assert!(pass);
assert_eq!(
detail,
"/home/me/.nix-profile/bin/kache (Nix, installer-managed): the Nix profile in \
/home/me/.nix-profile/bin, which each upgrade or rollback repoints"
);
assert_eq!(fix, None);
selection.stability = kache_shims::Stability::Versioned;
let (pass, _, fix) = doctor_install_path_check(&Ok(selection));
assert!(!pass);
assert!(fix.unwrap().contains("kache daemon install"));
let error = kache_shims::Error::Replaced("/usr/bin/kache".into());
let (pass, detail, fix) = doctor_install_path_check(&Err(error));
assert!(!pass);
assert!(detail.contains("was replaced"), "{detail}");
assert_eq!(fix, None);
}
#[test]
fn doctor_check_optionality_truth_table() {
assert!(doctor_check_is_optional("Install path", false, false));
assert!(doctor_check_is_optional(OTHER_CHECKOUTS, false, false));
assert!(doctor_check_is_optional("Daemon version", true, false));
assert!(!doctor_check_is_optional("Daemon version", false, false));
assert!(doctor_check_is_optional("Compiler probe", false, true));
assert!(!doctor_check_is_optional("Compiler probe", false, false));
assert!(!doctor_check_is_optional("Binary", true, true));
assert!(!doctor_check_is_optional("Daemon version", false, true));
assert!(!doctor_check_is_optional("Compiler probe", true, false));
assert!(!doctor_check_is_optional("Remote", true, true));
assert!(doctor_check_is_optional("C/C++ shims", false, false));
assert!(doctor_check_is_optional("C/C++ shims", true, true));
}
#[test]
fn daemon_service_is_satisfied_by_install_or_healthy_on_demand_daemon() {
assert_eq!(daemon_service_check(true, false), (true, None));
assert_eq!(daemon_service_check(false, true), (true, None));
assert_eq!(
daemon_service_check(false, false),
(false, Some("kache daemon install"))
);
}
#[test]
fn daemon_footnote_only_for_downgraded_daemon_failures() {
assert!(daemon_footnote_needed(
true,
&[("Daemon service", false), ("Binary", true)]
));
assert!(!daemon_footnote_needed(false, &[("Daemon service", false)]));
assert!(!daemon_footnote_needed(
true,
&[("Daemon service", true), ("Binary", true)]
));
assert!(!daemon_footnote_needed(
true,
&[("Compiler probe", false), ("Binary", false)]
));
assert!(!daemon_footnote_needed(true, &[]));
}
#[test]
fn daemon_version_check_reports_release_order_and_unknown_legacy_versions() {
for (daemon, client, pass, message, fix) in [
("0.13.0", "0.14.0", false, "remains active", true),
("0.14.0", "0.13.0", true, "newer than binary", false),
(
"0.14.0+build.1",
"0.14.0+build.2",
true,
"v0.14.0+build.1",
false,
),
("", "0.14.0", true, "unknown", false),
("legacy", "0.14.0", true, "unknown", false),
("0.14.0", "development", true, "unknown", false),
] {
let (actual, detail, hint) =
daemon_version_check(Some(daemon), false, client, "/run/kache/daemon.log");
assert_eq!(actual, pass, "{detail}");
assert!(detail.contains(message), "{detail}");
assert_eq!(hint.is_some(), fix, "{hint:?}");
if let Some(hint) = hint {
assert!(hint.contains("doctor --fix"));
}
assert!(!detail.contains("shutting down"));
}
}
#[test]
fn daemon_version_check_reports_pending_start_without_guessing_the_release() {
let (pass, detail, hint) = daemon_version_check(None, true, "0.14.0", "/run/kache/daemon.log");
assert!(pass);
assert!(detail.contains("starting"));
assert!(!detail.contains("v0.14.0"));
assert!(hint.is_none());
}
#[test]
fn only_fix_restarts_a_stale_daemon() {
assert!(should_restart_stale_daemon(true, true));
assert!(!should_restart_stale_daemon(false, true), "plain doctor");
assert!(!should_restart_stale_daemon(true, false), "nothing stale");
assert!(!should_restart_stale_daemon(false, false), "neither");
}
#[test]
fn stale_restart_note_never_claims_a_replacement_that_may_not_exist() {
assert!(stale_restart_note(&Ok(true)).is_none());
let timed_out = stale_restart_note(&Ok(false)).unwrap();
assert!(timed_out.contains("did not bind"), "{timed_out}");
assert!(!timed_out.contains("background"), "{timed_out}");
let failed = stale_restart_note(&Err(anyhow::anyhow!("spawn refused"))).unwrap();
assert!(failed.contains("restart failed"), "{failed}");
assert!(failed.contains("spawn refused"), "{failed}");
}
#[test]
fn daemon_version_check_reports_an_absent_daemon() {
let (pass, detail, fix) = daemon_version_check(None, false, "0.14.0", "/run/kache/daemon.log");
assert!(!pass);
assert_eq!(detail, "daemon not reachable");
let fix = fix.unwrap();
assert!(fix.contains("kache daemon start"), "{fix}");
assert!(fix.contains("/run/kache/daemon.log"), "{fix}");
}
#[test]
fn daemon_version_check_prefers_the_daemon_that_answered() {
let (pass, detail, _) =
daemon_version_check(Some("0.14.0"), true, "0.14.0", "/run/kache/daemon.log");
assert!(pass, "{detail}");
assert_eq!(detail, "v0.14.0");
}
fn gc_stats(evicted: usize, pinned: usize, bytes: u64) -> crate::store::GcStats {
crate::store::GcStats {
entries_evicted: evicted,
bytes_freed: bytes,
entries_pinned: pinned,
..Default::default()
}
}
#[cfg(unix)]
#[test]
fn stats_reads_the_volume_holding_the_cache() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::test_support::test_config(dir.path().join("cache"));
std::fs::create_dir_all(&config.cache_dir).unwrap();
config.auto_recover_min_free_bytes = 1;
let volume = volume_view(&config, None).expect("Unix reads volume usage");
assert!(volume.total_bytes > 0, "{volume:?}");
assert!(volume.free_bytes <= volume.total_bytes, "{volume:?}");
assert_eq!(volume.recovery_floor_bytes, 1);
}
#[test]
fn the_volume_row_says_what_is_free_and_whether_recovery_is_on() {
let gib = 1u64 << 30;
let usage = |free: u64| kache_fs::VolumeUsage {
total: 100 * gib,
free: free * gib,
};
assert_eq!(
row_text(volume_row(&VolumeView::new(usage(2), 0, true))),
"Volume | 2.0 GiB free | of 100.0 GiB · 98% used · free-space recovery off"
);
let below = VolumeView::new(usage(2), 10 * gib, false);
assert!(below.below_floor);
assert_eq!(
row_text(volume_row(&below)),
"Volume | 2.0 GiB free | of 100.0 GiB · 98% used · below the 10.0 GiB recovery floor · cache only"
);
let above = VolumeView::new(usage(40), 10 * gib, true);
assert!(!above.below_floor);
assert!(
row_text(volume_row(&above))
.ends_with("60% used · recovery keeps 10.0 GiB free · cache and idle targets"),
"{above:?}"
);
let off = VolumeView::new(usage(0), 0, true);
assert!(!off.below_floor);
assert!(!off.recovers_targets);
}
#[test]
fn cloned_targets_summary_only_appears_for_retained_blocks() {
let dir = tempfile::tempdir().unwrap();
let mut disk = crate::machine::disk_view(dir.path(), 0, 1024);
assert!(cloned_targets_row(&disk).map(row_text).is_none());
disk.disk_private_bytes = 3;
disk.cloned_into_targets_bytes = 7;
let line = cloned_targets_row(&disk)
.map(row_text)
.expect("cloned blocks need a summary");
assert!(line.contains("3 B"), "{line}");
assert!(
line.contains("7 B cloned into target/, which the limit leaves out"),
"{line}"
);
assert!(!line.contains("snapshots"), "{line}");
disk.snapshot_retained_bytes = 5;
let line = cloned_targets_row(&disk)
.map(row_text)
.expect("both retainers are named");
assert!(line.contains("7 B cloned"), "{line}");
assert!(
line.contains("5 B held only by filesystem snapshots"),
"{line}"
);
disk.cloned_into_targets_bytes = 0;
let line = cloned_targets_row(&disk)
.map(row_text)
.expect("snapshot blocks need a summary");
assert!(!line.contains("cloned"), "{line}");
assert!(line.contains("5 B held only"), "{line}");
}
#[test]
fn auto_gc_worker_retries_after_pins_or_a_lost_lock() {
let swept_clean = crate::store::GcStats::default();
assert!(!auto_gc_retry_wanted(&swept_clean));
assert!(auto_gc_retry_wanted(&skipped_gc_stats()));
let pinned = crate::store::GcStats {
entries_pinned: 1,
..crate::store::GcStats::default()
};
assert!(auto_gc_retry_wanted(&pinned));
}
#[test]
fn gc_machine_output_boundaries_are_explicit() {
assert!(human_gc_output(false));
assert!(!human_gc_output(true));
let stats = skipped_gc_stats();
assert!(stats.skipped);
assert_eq!(stats.entries_evicted, 0);
assert_eq!(DEFAULT_TRACKED_STALE_HOURS, 14 * 24);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("target");
std::fs::create_dir(&path).unwrap();
assert!(!path_was_removed(&path));
std::fs::remove_dir(&path).unwrap();
assert!(path_was_removed(&path));
assert!(human_clean_output(false));
assert!(!human_clean_output(true));
assert!(!doctor_has_issues(0));
assert!(doctor_has_issues(1));
}
#[test]
fn json_list_crate_filter_is_exact() {
assert!(crate_name_matches("serde", "serde"));
assert!(!crate_name_matches("serde", "serde_json"));
}
#[test]
fn daemon_gc_breakdown_sums_every_policy_field() {
fn policy(n: u64) -> crate::daemon::GcPolicyOutcome {
crate::daemon::GcPolicyOutcome {
entries_evicted: n as usize,
bytes_freed: n * 10,
entries_pinned: (n * 100) as usize,
disk_bytes_reclaimed: n * 1_000,
entries_unreclaimable: (n * 10_000) as usize,
entries_failed: (n * 100_000) as usize,
entries_locked: (n * 1_000_000) as usize,
evict_write_ms: n * 10_000_000,
bytes_held: n * 100_000_000,
}
}
let report = crate::daemon::GcBreakdown {
mode: crate::daemon::GcRequestMode::Automatic,
age: policy(1),
duplicate: policy(2),
size: policy(3),
};
let total = gc_stats_from_breakdown(&report);
assert_eq!(total.entries_evicted, 6);
assert_eq!(total.bytes_freed, 60);
assert_eq!(total.entries_pinned, 600);
assert_eq!(total.disk_bytes_reclaimed, 6_000);
assert_eq!(total.entries_unreclaimable, 60_000);
assert_eq!(total.entries_failed, 600_000);
assert_eq!(total.entries_locked, 6_000_000);
assert_eq!(total.evict_write_ms, 60_000_000);
assert_eq!(
total.bytes_held, 300_000_000,
"the largest probe, not a sum"
);
let mut accumulated = crate::store::GcStats {
entries_evicted: 1,
bytes_freed: 2,
entries_pinned: 3,
blobs_removed: 4,
duration_ms: 7,
entries_unreclaimable: 5,
disk_bytes_reclaimed: 6,
skipped: false,
entries_failed: 8,
entries_locked: 9,
entries_busy_snapshot: 0,
entries_recent_prefiltered: 0,
entries_import_pinned: 12,
evict_write_ms: 11,
bytes_held: 13,
housekeeping: None,
};
let part = crate::store::GcStats {
entries_evicted: 10,
bytes_freed: 20,
entries_pinned: 30,
blobs_removed: 40,
duration_ms: 70,
entries_unreclaimable: 50,
disk_bytes_reclaimed: 60,
skipped: true,
entries_failed: 80,
entries_locked: 90,
entries_busy_snapshot: 0,
entries_recent_prefiltered: 0,
entries_import_pinned: 120,
evict_write_ms: 110,
bytes_held: 7,
housekeeping: None,
};
add_gc_stats(&mut accumulated, &part);
assert_eq!(accumulated.bytes_held, 13, "the larger probe, not a sum");
assert_eq!(accumulated.entries_evicted, 11);
assert_eq!(accumulated.bytes_freed, 22);
assert_eq!(accumulated.entries_pinned, 33);
assert_eq!(accumulated.blobs_removed, 44);
assert_eq!(accumulated.duration_ms, 77);
assert_eq!(accumulated.entries_unreclaimable, 55);
assert_eq!(accumulated.disk_bytes_reclaimed, 66);
assert_eq!(accumulated.entries_failed, 88);
assert_eq!(accumulated.entries_locked, 99);
assert_eq!(accumulated.evict_write_ms, 121);
assert_eq!(accumulated.entries_import_pinned, 132);
assert!(accumulated.skipped);
}
#[test]
fn local_gc_records_its_run_with_the_driver_that_ran_it() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
run_gc_local(&config, GcMode::Background).unwrap();
let stats = crate::report::read_gc_stats(&config.cache_dir).expect("worker run recorded");
assert_eq!(stats.source, "auto");
run_gc_local(&config, GcMode::Cli).unwrap();
let stats = crate::report::read_gc_stats(&config.cache_dir).unwrap();
assert_eq!(stats.source, "manual");
}
#[test]
fn local_gc_sweeps_hermetic_build_script_runs() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
let leftover = config.cache_dir.join("out-dirs/v2").join("ab".repeat(16));
std::fs::create_dir_all(leftover.join("debug/build/z-1/out")).unwrap();
run_gc_local(&config, GcMode::Background).unwrap();
assert!(!leftover.exists());
}
#[cfg(unix)]
#[test]
fn purge_keeps_only_linked_build_script_runs() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let v2 = config.cache_dir.join("out-dirs/v2");
let run = |key: &str| {
let root = v2.join(key.repeat(16));
std::fs::create_dir_all(root.join("out")).unwrap();
std::fs::write(root.join(".kache-sealed"), b"{}").unwrap();
root
};
let (linked, unlinked) = (run("ab"), run("cd"));
let link = dir.path().join("target-out");
std::os::unix::fs::symlink(linked.join("out"), &link).unwrap();
let mut line = link.as_os_str().as_encoded_bytes().to_vec();
line.push(b'\n');
std::fs::write(linked.with_extension("refs"), line).unwrap();
purge(&config, None).unwrap();
assert!(linked.exists(), "a target directory links to it");
assert!(!unlinked.exists());
}
#[test]
fn local_gc_appends_to_the_gc_history_only_when_record_sessions_is_on() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().to_path_buf(), None);
run_gc_local(&config, GcMode::Cli).unwrap();
assert!(!config.cache_dir.join("telemetry").exists());
config.record_sessions = true;
run_gc_local(&config, GcMode::Background).unwrap();
let log = std::fs::read_to_string(crate::report::gc_runs_log_path(&config.cache_dir)).unwrap();
let records: Vec<crate::report::GcRunRecord> = log
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
assert_eq!(records.len(), 1, "{log}");
assert_eq!(records[0].source, "auto");
}
#[test]
fn local_gc_runs_store_housekeeping_and_records_it() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
std::fs::create_dir_all(config.store_dir()).unwrap();
let lock_path = |seed: u8| {
config
.store_dir()
.join(format!("{}.lock", blake3::hash(&[seed]).to_hex()))
};
for seed in [1, 2, 3] {
std::fs::write(lock_path(seed), b"1").unwrap();
}
for seed in [1, 2] {
std::fs::OpenOptions::new()
.write(true)
.open(lock_path(seed))
.unwrap()
.set_modified(std::time::SystemTime::now() - std::time::Duration::from_secs(7200))
.unwrap();
}
let stats = run_gc_local(&config, GcMode::Background).unwrap();
assert_eq!(
stats.housekeeping,
Some(crate::store::HousekeepingStats {
key_locks_removed: 2,
key_locks_remaining: 1,
predictions_pruned: 0,
file_hashes_pruned: 0,
})
);
assert!(!lock_path(1).exists());
assert!(!lock_path(2).exists());
assert!(lock_path(3).exists());
let recorded = crate::report::read_gc_stats(&config.cache_dir).expect("run recorded");
assert_eq!(recorded.key_locks_removed, Some(2));
assert_eq!(recorded.key_locks_remaining, Some(1));
assert_eq!(recorded.predictions_pruned, Some(0));
assert_eq!(recorded.file_hashes_pruned, Some(0));
}
#[test]
fn stale_schema_gc_records_its_run() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
gc(&config, None, true, true).unwrap();
let stats = crate::report::read_gc_stats(&config.cache_dir).expect("stale-schema run recorded");
assert_eq!(stats.source, "manual");
}
#[test]
fn age_gc_without_a_daemon_records_its_run() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
let store = Store::open(&config).unwrap();
let _gc_lock = store.try_gc_lock().unwrap().expect("gc lock");
evict_older_than_recorded(&store, &config, 24).unwrap();
let stats = crate::report::read_gc_stats(&config.cache_dir).expect("age run recorded");
assert_eq!(stats.source, "manual");
}
fn put_idle_entry(store: &Store, dir: &std::path::Path, key: &str, size: usize) {
let src = dir.join(format!("{key}.o"));
std::fs::write(&src, &key.as_bytes().repeat(size)[..size]).unwrap();
store
.put(
key,
"test-crate",
&[],
&[],
"host",
"dev",
&[(src.clone(), format!("{key}.o"))],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&src);
store.set_last_accessed_for_test(key, "-1 hour");
}
fn config_with_an_over_budget_shard(dir: &std::path::Path, budget: u64) -> (Config, Config, Store) {
let mut config = save_manifest_config(dir.join("main"), None);
let shard = crate::config::VolumeStore {
volume: "/mnt/shard/".into(),
store: dir.join("shard"),
max_size: Some(budget),
};
let shard_config = config.for_volume_store(&shard, |_| None);
config.volume_stores = vec![
crate::config::VolumeStore {
volume: "/mnt/gone/".into(),
store: dir.join("unmounted"),
max_size: None,
},
shard,
];
let store = Store::open(&shard_config).unwrap();
put_idle_entry(&store, dir, "shard_a", 600);
put_idle_entry(&store, dir, "shard_b", 600);
(config, shard_config, store)
}
#[test]
fn local_gc_sweeps_each_shard_against_its_own_budget() {
let dir = tempfile::tempdir().unwrap();
let (config, shard_config, shard) = config_with_an_over_budget_shard(dir.path(), 1000);
let main = Store::open(&config).unwrap();
put_idle_entry(&main, dir.path(), "main_entry", 1200);
let stats = run_gc_local_with_shards(&config, GcMode::Cli).unwrap();
assert_eq!(stats.entries_evicted, 0, "the stats are the main store's");
assert!(main.contains("main_entry"));
assert_eq!(shard.physical_size().unwrap(), 600);
let recorded = crate::report::read_gc_stats(&shard_config.cache_dir).unwrap();
assert_eq!(
(recorded.source.as_str(), recorded.entries_evicted),
("manual", 1)
);
assert!(!dir.path().join("unmounted").exists());
let dir = tempfile::tempdir().unwrap();
let (config, _, shard) = config_with_an_over_budget_shard(dir.path(), 1_000_000);
run_gc_local_with_shards(&config, GcMode::Background).unwrap();
assert_eq!(
shard.physical_size().unwrap(),
1200,
"under budget: left alone"
);
}
#[test]
fn local_gc_takes_each_stores_lock_on_its_own() {
let dir = tempfile::tempdir().unwrap();
let (config, _, shard) = config_with_an_over_budget_shard(dir.path(), 1000);
let main = Store::open(&config).unwrap();
let main_lock = main.try_gc_lock().unwrap().expect("gc lock");
let shard_lock = shard.try_gc_lock().unwrap().expect("gc lock");
let stats = run_gc_local_with_shards(&config, GcMode::Background).unwrap();
assert!(stats.skipped);
assert_eq!(
shard.physical_size().unwrap(),
1200,
"the shard's lock is held"
);
drop(shard_lock);
run_gc_local_with_shards(&config, GcMode::Background).unwrap();
assert_eq!(
shard.physical_size().unwrap(),
600,
"only the main lock is held"
);
drop(main_lock);
}
#[test]
fn only_a_printing_sweep_names_the_shard_first() {
let shard = std::path::Path::new("/mnt/shard");
assert_eq!(
shard_sweep_heading(GcMode::Cli, shard).as_deref(),
Some("Volume shard /mnt/shard:")
);
assert_eq!(shard_sweep_heading(GcMode::Background, shard), None);
}
#[test]
fn the_auto_gc_worker_sweeps_an_over_budget_shard() {
let dir = tempfile::tempdir().unwrap();
let (config, shard_config, shard) = config_with_an_over_budget_shard(dir.path(), 1000);
run_auto_gc_workers(&config, std::time::Duration::ZERO);
assert_eq!(shard.physical_size().unwrap(), 600);
let recorded = crate::report::read_gc_stats(&shard_config.cache_dir).unwrap();
assert_eq!(recorded.source, "auto");
assert!(!dir.path().join("unmounted").exists());
}
#[test]
fn age_gc_without_a_daemon_reaches_shards_whose_lock_is_free() {
let dir = tempfile::tempdir().unwrap();
let (config, shard_config, shard) = config_with_an_over_budget_shard(dir.path(), 1_000_000);
shard.set_last_accessed_for_test("shard_a", "-48 hours");
let held = shard.try_gc_lock().unwrap().expect("gc lock");
let stats = evict_shards_older_than(&config, 24);
assert_eq!(stats.len(), 1);
assert!(stats[0].skipped);
assert!(shard.contains("shard_a"));
drop(held);
let stats = evict_shards_older_than(&config, 24);
assert_eq!(stats[0].entries_evicted, 1);
assert!(!shard.contains("shard_a") && shard.contains("shard_b"));
let recorded = crate::report::read_gc_stats(&shard_config.cache_dir).unwrap();
assert_eq!(recorded.source, "manual");
}
#[test]
fn the_gc_summary_has_a_line_per_shard_it_swept() {
let dir = tempfile::tempdir().unwrap();
let (config, shard_config, shard) = config_with_an_over_budget_shard(dir.path(), 1000);
assert_eq!(
shard_store_lines(&config),
vec![format!(
"Volume shard {}: {} / {} · 2 entries",
shard_config.cache_dir.display(),
ByteSize(shard.physical_size().unwrap()),
ByteSize(1000),
)]
);
}
#[test]
fn gc_summary_compares_deduplicated_blob_bytes_with_the_limit() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let store = Store::open(&config).unwrap();
let artifact = dir.path().join("shared.o");
fs::write(&artifact, [42; 60]).unwrap();
for key in ["first", "second"] {
store
.put(
key,
key,
&[],
&[],
"",
"",
&[(artifact.clone(), "shared.o".into())],
"",
"",
)
.unwrap();
}
assert_eq!(store.total_size().unwrap(), 120);
assert_eq!(store.physical_size().unwrap(), 60);
assert_eq!(
gc_store_usage(&store, 90, None).unwrap(),
"60 B / 90 B · 2 entries"
);
assert_eq!(
gc_store_usage(&store, 0, None).unwrap(),
"60 B / 0 B · 2 entries"
);
}
#[test]
fn machine_snapshot_reads_a_store_without_creating_one() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
let empty = machine_snapshot(&config);
assert!(empty.index_bytes.is_none());
assert!(empty.wal_bytes.is_none());
assert!(empty.rowid_high_water.is_empty());
assert!(empty.gc.is_none());
assert!(
!config.index_db_path().exists(),
"a snapshot must never create the index"
);
let store = Store::open(&config).unwrap();
let artifact = dir.path().join("physical-size.rlib");
std::fs::write(&artifact, b"payload").unwrap();
store
.put(
"physical-size",
"physical_size",
&["lib".to_string()],
&[],
"",
"dev",
&[(artifact, "libphysical_size.rlib".to_string())],
"",
"",
)
.unwrap();
drop(store);
crate::report::record_gc_run(
&config,
"auto",
crate::store::SweepOrigin::Automatic,
&crate::store::GcStats::default(),
)
.unwrap();
let snap = machine_snapshot(&config);
let db_len = std::fs::metadata(config.index_db_path()).unwrap().len();
let mut wal_path = config.index_db_path().into_os_string();
wal_path.push("-wal");
let wal_len = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
assert_eq!(snap.wal_bytes, Some(wal_len));
assert_eq!(snap.index_bytes, Some(db_len + wal_len));
assert_eq!(snap.store_physical_bytes, Some(7));
assert_eq!(snap.index_free_bytes, Some(0), "a fresh index has no holes");
assert_eq!(
snap.blob_drift,
Some(crate::store::BlobRefcountDrift::default())
);
let db = rusqlite::Connection::open(config.index_db_path()).unwrap();
db.execute_batch(
"UPDATE blobs SET refcount = 5;
INSERT INTO blobs (hash, size, refcount) VALUES ('unowned', 4096, 1);",
)
.unwrap();
drop(db);
assert_eq!(
machine_snapshot(&config).blob_drift,
Some(crate::store::BlobRefcountDrift {
unowned: 1,
unowned_bytes: 4096,
too_high: 1,
too_high_bytes: 7,
..Default::default()
})
);
let tables: Vec<_> = snap
.rowid_high_water
.iter()
.map(|(table, _)| *table)
.collect();
assert!(
tables.contains(&"entries") && tables.contains(&"blobs"),
"{tables:?}"
);
assert_eq!(snap.gc.expect("gc_stats.json read").source, "auto");
}
#[test]
fn machine_snapshot_never_checkpoints_the_wal() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
drop(Store::open(&config).unwrap());
let db_path = config.index_db_path();
let mut wal_path = db_path.clone().into_os_string();
wal_path.push("-wal");
{
let writer = rusqlite::Connection::open(&db_path).unwrap();
writer
.set_db_config(
rusqlite::config::DbConfig::SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE,
true,
)
.unwrap();
writer
.execute_batch(
"PRAGMA wal_autocheckpoint = 0;
CREATE TABLE probe (x INTEGER);
INSERT INTO probe VALUES (1);",
)
.unwrap();
}
let db_before = std::fs::read(&db_path).unwrap();
let wal_before = std::fs::metadata(&wal_path).unwrap().len();
assert!(wal_before > 0, "the setup leaves frames in the WAL");
machine_snapshot(&config);
assert_eq!(
std::fs::read(&db_path).unwrap(),
db_before,
"index.db must not be written"
);
assert_eq!(
std::fs::metadata(&wal_path).map(|m| m.len()).ok(),
Some(wal_before),
"the WAL must be left in place"
);
}
fn row_text(row: StatsRow) -> String {
format!("{} | {} | {}", row.0, row.1, row.2)
}
#[test]
fn stats_gc_note_suffixes_start_at_one() {
let gc_rows = |failed: usize, locked: usize, write_ms: u64, source: &str| {
machine_rows(&crate::otel::MachineSnapshot {
gc: Some(crate::report::GcStatsPersisted {
last_run: "not a time".to_string(),
source: source.to_string(),
entries_evicted: 3,
entries_failed: failed,
entries_locked: locked,
evict_write_ms: write_ms,
..Default::default()
}),
..Default::default()
})
.into_iter()
.map(row_text)
.collect::<Vec<_>>()
};
assert_eq!(
gc_rows(0, 0, 0, "manual"),
vec!["Last GC | not a time | manual, 3 evicted".to_string()]
);
assert_eq!(
gc_rows(1, 1, 1, ""),
vec![
"Last GC | not a time | daemon, 3 evicted, 1 failed (1 lost the index write lock), 1 ms in index writes"
.to_string()
]
);
let index_only = machine_rows(&crate::otel::MachineSnapshot {
index_bytes: Some(4096),
..Default::default()
});
assert_eq!(
index_only.into_iter().map(row_text).collect::<Vec<_>>(),
vec!["Index | 4.0 KiB | ".to_string()]
);
}
#[test]
fn stats_rows_show_the_index_and_a_gc_that_keeps_losing_the_lock() {
let machine = crate::otel::MachineSnapshot {
store_physical_bytes: None,
index_bytes: Some(29_074_419_712),
wal_bytes: Some(1_073_741_824),
index_free_bytes: None,
blob_drift: None,
rowid_high_water: vec![
("entries", 2),
("file_hashes", 13_286_285),
("cc_preprocess_memos", 874_517),
("eviction_tombstones", 5_425_819),
("blobs", 5),
],
gc: Some(crate::report::GcStatsPersisted {
last_run: "2026-09-12T12:11:05+00:00".to_string(),
source: "auto".to_string(),
entries_failed: 40,
entries_locked: 40,
evict_write_ms: 4200,
..Default::default()
}),
};
let rows: Vec<String> = machine_rows(&machine).into_iter().map(row_text).collect();
assert_eq!(
rows[0],
"Index | 27.1 GiB | WAL 1.0 GiB · rowid high-water: file_hashes 13,286,285, eviction_tombstones 5,425,819, cc_preprocess_memos 874,517"
);
assert!(rows[1].starts_with("Last GC | "), "{rows:?}");
assert!(
!rows[1].contains("2026-09-12T"),
"shown in local time: {rows:?}"
);
assert!(
rows[1].contains("auto, 0 evicted, 40 failed (40 lost the index write lock)"),
"{rows:?}"
);
assert!(rows[1].ends_with(", 4200 ms in index writes"), "{rows:?}");
assert_eq!(rows.len(), 2, "{rows:?}");
assert!(machine_rows(&crate::otel::MachineSnapshot::default()).is_empty());
}
fn lifetime_totals() -> crate::savings::Totals {
crate::savings::Totals {
since: chrono::DateTime::parse_from_rfc3339("2026-09-12T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc),
hits: 128_402,
hit_compile_time_ms: 147_600_000,
zero_copy_bytes: 3 << 30,
copied_bytes: 1 << 20,
pruned_automatic_bytes: 2 << 30,
pruned_requested_bytes: 1 << 30,
}
}
fn lifetime_text(totals: Option<&crate::savings::Totals>) -> Vec<String> {
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 1).unwrap();
lifetime_rows(totals, today)
.into_iter()
.map(row_text)
.collect()
}
#[test]
fn lifetime_rows_show_each_recorded_total() {
assert!(
lifetime_text(None).is_empty(),
"nothing recorded: no section"
);
let totals = lifetime_totals();
let rows = lifetime_text(Some(&totals));
assert_eq!(rows.len(), 4, "{rows:?}");
assert!(rows[0].starts_with("Since | Sep "), "{rows:?}");
assert!(rows[0].ends_with(" | "), "{rows:?}");
assert_eq!(
rows[1..],
[
"Time saved | ~41.0 h | compile work avoided over 128,402 hits",
"No-copy | 3.0 GiB | restored without copying, 1.0 MiB copied",
"Pruned | 3.0 GiB | 2.0 GiB automatically, 1.0 GiB on request",
]
);
}
#[test]
fn lifetime_rows_leave_out_zero_totals() {
let mut totals = crate::savings::Totals {
hits: 0,
hit_compile_time_ms: 0,
zero_copy_bytes: 0,
copied_bytes: 0,
pruned_automatic_bytes: 0,
pruned_requested_bytes: 0,
..lifetime_totals()
};
assert!(
lifetime_text(Some(&totals)).is_empty(),
"nothing saved yet: no section"
);
totals.hits = 1;
totals.copied_bytes = 5;
totals.pruned_requested_bytes = 7;
let rows = lifetime_text(Some(&totals));
assert_eq!(
rows[1..],
[
"Time saved | 0 ms | compile work avoided over 1 hit",
"No-copy | 0 B | restored without copying, 5 B copied",
"Pruned | 7 B | 0 B automatically, 7 B on request",
]
);
}
#[test]
fn since_label_adds_the_year_only_for_another_year() {
let date = |y, m, d| chrono::NaiveDate::from_ymd_opt(y, m, d).unwrap();
assert_eq!(since_label(date(2026, 9, 2), date(2026, 10, 1)), "Sep 02");
assert_eq!(
since_label(date(2025, 9, 2), date(2026, 10, 1)),
"Sep 02 2025"
);
}
#[test]
fn render_stats_places_the_lifetime_section_after_the_window() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().to_path_buf());
let extras = StatsExtras {
lifetime: vec![("Since", "Sep 12".to_string(), "lifetime".to_string())],
..Default::default()
};
let lines = render_stats_with(
&StatsSnapshot::default(),
&config,
SinceWindow::DEFAULT,
&extras,
);
let at = |label: &str| {
lines
.iter()
.position(|line| line.trim_start().starts_with(label))
.unwrap_or_else(|| panic!("{label}: {lines:#?}"))
};
assert!(at("Time saved") < at("Since"), "{lines:#?}");
assert!(at("Since") < at("Cache"), "{lines:#?}");
assert_eq!(
lines[at("Since") - 1],
"Lifetime",
"its own section: {lines:#?}"
);
}
#[test]
fn index_free_bytes_counts_freelist_pages() {
let db = rusqlite::Connection::open_in_memory().unwrap();
db.execute_batch("PRAGMA page_size = 4096; CREATE TABLE keep (v BLOB);")
.unwrap();
assert_eq!(index_free_bytes(&db), Some(0));
db.execute_batch(
"CREATE TABLE junk (v BLOB);
INSERT INTO junk VALUES (zeroblob(65536));
DROP TABLE junk;",
)
.unwrap();
let pages: i64 = db
.query_row("PRAGMA freelist_count", [], |row| row.get(0))
.unwrap();
assert!(
pages >= 16,
"a 64 KiB blob spans at least 16 pages: {pages}"
);
assert_eq!(index_free_bytes(&db), Some(pages as u64 * 4096));
db.execute_batch("VACUUM").unwrap();
assert_eq!(index_free_bytes(&db), Some(0));
}
#[test]
fn free_page_bytes_multiplies_and_rejects_nonsense() {
assert_eq!(free_page_bytes(3, 4096), Some(12_288));
assert_eq!(free_page_bytes(0, 4096), Some(0));
assert_eq!(free_page_bytes(-1, 4096), None);
assert_eq!(free_page_bytes(1, -4096), None);
assert_eq!(free_page_bytes(i64::MAX, 4096), None);
}
#[test]
fn rowid_high_water_counts_replacements_not_rows() {
let db = rusqlite::Connection::open_in_memory().unwrap();
db.execute_batch("CREATE TABLE file_hashes (path TEXT PRIMARY KEY, hash TEXT)")
.unwrap();
for hash in ["a", "b", "c"] {
db.execute(
"INSERT OR REPLACE INTO file_hashes VALUES ('src/lib.rs', ?1)",
[hash],
)
.unwrap();
}
let high_water: i64 = db
.query_row(&rowid_high_water_sql("file_hashes"), [], |row| row.get(0))
.unwrap();
let rows: i64 = db
.query_row("SELECT COUNT(*) FROM file_hashes", [], |row| row.get(0))
.unwrap();
assert_eq!((high_water, rows), (3, 1));
}
#[test]
fn index_row_figures_seek_instead_of_scanning() {
let db = rusqlite::Connection::open_in_memory().unwrap();
db.execute_batch(
"CREATE TABLE memos (key TEXT PRIMARY KEY, body BLOB);
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 5000)
INSERT INTO memos SELECT 'k' || i, zeroblob(64) FROM n;",
)
.unwrap();
let mut stmt = db.prepare(&rowid_high_water_sql("memos")).unwrap();
let top: i64 = stmt.query_row([], |row| row.get(0)).unwrap();
assert_eq!(top, 5000);
let steps = stmt.get_status(rusqlite::StatementStatus::VmStep);
assert!(steps < 100, "{steps} VM steps for one high-water mark");
let mut explain = db
.prepare(&format!("EXPLAIN {}", rowid_high_water_sql("memos")))
.unwrap();
let opcodes: Vec<String> = explain
.query_map([], |row| row.get(1))
.unwrap()
.collect::<rusqlite::Result<_>>()
.unwrap();
assert!(opcodes.iter().any(|op| op == "Last"), "{opcodes:?}");
assert!(!opcodes.iter().any(|op| op == "Count"), "{opcodes:?}");
}
#[test]
fn machine_snapshot_skips_row_figures_on_a_locked_index() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
drop(Store::open(&config).unwrap());
let holder = rusqlite::Connection::open(config.index_db_path()).unwrap();
holder
.execute_batch(
"PRAGMA locking_mode = EXCLUSIVE; BEGIN EXCLUSIVE; \
DELETE FROM entries WHERE 0;",
)
.unwrap();
let started = std::time::Instant::now();
let snap = machine_snapshot(&config);
let waited = started.elapsed();
assert!(snap.index_bytes.is_some(), "file sizes need no lock");
assert!(
snap.rowid_high_water.is_empty(),
"{:?}",
snap.rowid_high_water
);
assert!(
waited < std::time::Duration::from_secs(4),
"waited {waited:?} on a locked index"
);
holder.execute_batch("COMMIT").unwrap();
}
fn report_run(config: &Config, out: &std::path::Path, record: bool) {
report(
config,
ReportFormat::Json,
SinceWindow::DEFAULT,
crate::report::ReportFilter {
root: Some(std::path::PathBuf::from("/ci/runner-3/_work/secret-repo")),
last_build: false,
},
Some(out.join("report.json")),
10,
record,
false,
)
.unwrap();
}
fn tree_contents(dir: &std::path::Path) -> Vec<(std::path::PathBuf, Vec<u8>)> {
let mut files = Vec::new();
let mut pending = vec![dir.to_path_buf()];
while let Some(next) = pending.pop() {
for entry in std::fs::read_dir(&next).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
pending.push(path);
} else {
let body = std::fs::read(&path).unwrap();
files.push((path, body));
}
}
}
files.sort();
files
}
#[test]
fn stats_last_build_records_when_record_sessions_is_on() {
let cache = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(cache.path().to_path_buf(), None);
config.record_sessions = true;
plant_last_build(&config);
stats_last_build(&config, None, false).unwrap();
stats_last_build(&config, None, true).unwrap();
let log = std::fs::read_to_string(session_log_path(&config)).unwrap();
assert_eq!(log.lines().count(), 2, "{log}");
let quiet = tempfile::tempdir().unwrap();
let config = save_manifest_config(quiet.path().to_path_buf(), None);
plant_last_build(&config);
stats_last_build(&config, None, true).unwrap();
assert!(
!session_log_path(&config).exists(),
"a last-build summary records only when record_sessions is on"
);
}
fn plant_last_build(config: &Config) {
let mut event = build_event(
"demo",
crate::events::EventResult::LocalHit,
4,
5,
64,
"key",
);
event.root = "/repo".to_string();
event.session_id = "build".to_string();
crate::events::log_event(&config.event_log_path(), &event).unwrap();
}
#[test]
fn report_records_without_the_flag_when_record_sessions_is_on() {
let cache = tempfile::tempdir().unwrap();
let out = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(cache.path().to_path_buf(), None);
config.record_sessions = true;
report_run(&config, out.path(), false);
let log = std::fs::read_to_string(session_log_path(&config)).unwrap();
assert_eq!(log.lines().count(), 1, "{log}");
let record: crate::report::SessionRecord =
serde_json::from_str(log.lines().next().unwrap()).unwrap();
assert_eq!(record.schema, crate::report::SESSION_RECORD_SCHEMA);
}
#[test]
fn report_without_record_leaves_the_cache_dir_unchanged() {
let cache = tempfile::tempdir().unwrap();
let out = tempfile::tempdir().unwrap();
let config = save_manifest_config(cache.path().to_path_buf(), None);
drop(Store::open(&config).unwrap());
let before = tree_contents(cache.path());
report_run(&config, out.path(), false);
assert!(out.path().join("report.json").is_file());
assert!(!cache.path().join("telemetry").exists());
assert_eq!(tree_contents(cache.path()), before);
}
#[test]
fn report_record_appends_one_line_to_the_cache_dir_not_the_runtime_dir() {
let cache = tempfile::tempdir().unwrap();
let runtime = tempfile::tempdir().unwrap();
let out = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(cache.path().to_path_buf(), None);
config.runtime_dir = runtime.path().to_path_buf();
let line_count = || {
std::fs::read_to_string(session_log_path(&config))
.unwrap()
.lines()
.count()
};
report_run(&config, out.path(), true);
assert_eq!(line_count(), 1, "one line per recorded session");
report_run(&config, out.path(), true);
assert_eq!(line_count(), 2, "a second record appends, never rewrites");
assert!(!runtime.path().join("telemetry").exists());
let log = std::fs::read_to_string(session_log_path(&config)).unwrap();
let lines: Vec<_> = log.lines().collect();
assert!(
!log.contains("secret-repo"),
"the root is hashed, never written"
);
let record: crate::report::SessionRecord = serde_json::from_str(lines[0]).unwrap();
assert_eq!(record.schema, crate::report::SESSION_RECORD_SCHEMA);
assert_eq!(record.root_hash.as_deref().map(str::len), Some(16));
assert_eq!(record.summary.total_crates, 0);
assert_eq!(record.machine.store_max, config.max_size);
}
#[test]
fn report_record_that_cannot_write_still_delivers_the_report() {
let cache = tempfile::tempdir().unwrap();
let out = tempfile::tempdir().unwrap();
let config = save_manifest_config(cache.path().to_path_buf(), None);
std::fs::write(cache.path().join("telemetry"), b"").unwrap();
let result = report(
&config,
ReportFormat::Json,
SinceWindow::DEFAULT,
crate::report::ReportFilter {
root: None,
last_build: false,
},
Some(out.path().join("report.json")),
10,
true,
false,
);
assert!(result.is_ok(), "{result:?}");
assert!(out.path().join("report.json").is_file());
}
#[test]
fn evicting_nothing_because_everything_is_pinned_explains_itself() {
let msg = describe_eviction(&gc_stats(0, 24, 0), true);
assert!(
msg.contains("24"),
"must say how many were held back: {msg}"
);
assert!(
msg.contains("within the last ~2 min"),
"must name the grace period so the wait is bounded and knowable: {msg}"
);
assert!(
msg.contains("Run it again once builds and uploads are idle"),
"must tell the user what to do next: {msg}"
);
assert!(
msg.contains("durable remote upload"),
"the shared pin counter must describe upload-backed entries too: {msg}"
);
}
#[test]
fn evicting_nothing_while_over_limit_still_explains_the_grace() {
let msg = describe_eviction(&gc_stats(0, 0, 0), true);
assert!(msg.contains("over its limit"), "{msg}");
assert!(msg.contains("within the last ~2 min"), "{msg}");
}
#[test]
fn an_empty_store_does_not_imply_something_is_wrong() {
let msg = describe_eviction(&gc_stats(0, 0, 0), false);
assert!(msg.contains("nothing to evict"), "{msg}");
assert!(
!msg.contains("over its limit"),
"a healthy store must not be told it is over budget: {msg}"
);
}
#[test]
fn a_successful_eviction_reports_bytes_and_still_flags_pinned_entries() {
let mut stats = gc_stats(12, 3, 5 * 1024 * 1024);
stats.disk_bytes_reclaimed = 4 * 1024 * 1024;
stats.entries_unreclaimable = 2;
let msg = describe_eviction(&stats, false);
assert!(msg.contains("12 entries"), "{msg}");
assert!(msg.contains("MiB") || msg.contains("MB"), "{msg}");
assert!(
msg.contains('3'),
"entries left behind matter even on a successful sweep — they are \
why the store may still be over budget: {msg}"
);
assert!(msg.contains("became free on disk"), "{msg}");
assert!(msg.contains("is still held by clones"), "{msg}");
assert!(msg.contains("2 entries left in place"), "{msg}");
}
#[test]
fn an_unsuccessful_eviction_still_flags_pinned_entries_when_clones_remain() {
let mut stats = gc_stats(0, 7, 0);
stats.entries_unreclaimable = 2;
let msg = describe_eviction(&stats, false);
assert!(
msg.contains("7 more entries"),
"must report the 7 pinned entries even when clones retain others: {msg}"
);
stats.entries_pinned = 0;
let msg = describe_eviction(&stats, false);
assert!(!msg.contains(" more "), "{msg}");
}
#[test]
fn a_successful_eviction_without_pinned_entries_has_no_pinned_note() {
let msg = describe_eviction(&gc_stats(3, 0, 1024), false);
assert!(!msg.contains(" more "), "{msg}");
}
#[test]
fn fully_retained_eviction_has_a_cleanup_path() {
let mut stats = gc_stats(0, 0, 0);
stats.entries_unreclaimable = 1;
let msg = describe_eviction(&stats, false);
assert!(msg.contains("1 entry cloned"), "{msg}");
assert!(msg.contains("kache clean --stale 14d --dry-run"), "{msg}");
assert!(msg.contains("then run it again"), "{msg}");
stats.bytes_held = 3 * 1024 * 1024 * 1024;
let msg = describe_eviction(&stats, false);
assert!(msg.contains("1 entry (3.0 GiB) cloned"), "{msg}");
}
#[test]
fn a_successful_eviction_sizes_the_entries_clones_hold() {
let mut stats = gc_stats(1, 0, 1024);
stats.entries_unreclaimable = 2;
stats.bytes_held = 2048;
let msg = describe_eviction(&stats, false);
assert!(msg.contains("2 entries (2.0 KiB) left in place"), "{msg}");
}
#[test]
fn eviction_messages_are_singular_for_one_entry() {
let mut stats = gc_stats(1, 0, 1024);
stats.disk_bytes_reclaimed = 1024;
let msg = describe_eviction(&stats, false);
assert!(msg.contains("1 entry"), "{msg}");
assert!(!msg.contains("1 entries"), "{msg}");
assert!(!msg.contains("is still held by clones"), "{msg}");
assert!(!msg.contains("more 0 entries"), "{msg}");
assert!(!msg.contains("0 entries left in place"), "{msg}");
}
#[test]
fn a_clean_sweep_says_nothing_about_entries_left_behind() {
let msg = describe_eviction(&gc_stats(7, 0, 4096), false);
assert!(msg.contains("7 entries"), "{msg}");
assert!(
!msg.contains("in use within the last"),
"no entries were held back, so the grace note must not appear: {msg}"
);
}
#[test]
fn over_limit_is_decided_by_a_strict_comparison_against_the_budget() {
assert!(store_over_limit(Some(1025), 1024));
assert!(
!store_over_limit(Some(1024), 1024),
"exactly at budget is within it: a store that fits must not be told \
it is over"
);
assert!(!store_over_limit(Some(512), 1024));
assert!(
!store_over_limit(None, 1024),
"an unreadable size must not be reported as over budget — that sends \
the user chasing an eviction problem they may not have"
);
}
#[cfg(unix)]
#[test]
fn a_hardlinked_artifact_records_its_link_group() {
let dir = tempfile::tempdir().unwrap();
let blob = dir.path().join("blob.bin");
let linked = dir.path().join("target-copy.bin");
fs::write(&blob, vec![0u8; 4096]).unwrap();
fs::hard_link(&blob, &linked).unwrap();
let meta = fs::metadata(&linked).unwrap();
let observation = observe_storage(&linked, &meta);
let hardlink = observation.hardlink.expect("nlink > 1 records a group");
assert_eq!(hardlink.total_links, 2);
let mut reclaim = ReclaimEstimator::default();
reclaim.record(meta.len(), observation);
assert_eq!(reclaim.estimated_reclaimable_bytes(), 0);
assert!(reclaim.hardlink_has_external_ref(hardlink.id));
let plain = dir.path().join("plain.bin");
fs::write(&plain, vec![0u8; 4096]).unwrap();
let plain_meta = fs::metadata(&plain).unwrap();
let plain_observation = observe_storage(&plain, &plain_meta);
assert!(plain_observation.hardlink.is_none());
assert!(!plain_observation.sharing.shared);
}
fn zero_event_stats() -> daemon::EventStatsResponse {
daemon::EventStatsResponse {
local_hits: 0,
prefetch_hits: 0,
remote_hits: 0,
dups: 0,
misses: 0,
errors: 0,
total_elapsed_ms: 0,
hit_elapsed_ms: 0,
miss_elapsed_ms: 0,
hit_compile_time_ms: 0,
miss_compile_time_ms: 0,
store_output_blobs: 0,
store_duplicate_blobs: 0,
store_new_blobs: 0,
}
}
#[test]
fn test_daemon_needed_when_remote_configured() {
assert!(daemon_needed(true, false));
}
#[test]
fn test_daemon_needed_when_planner_configured() {
assert!(daemon_needed(false, true));
}
#[test]
fn test_daemon_needed_when_both_configured() {
assert!(daemon_needed(true, true));
}
#[test]
fn test_daemon_not_needed_when_local_only_or_unconfigured() {
assert!(!daemon_needed(false, false));
}
#[test]
fn test_optional_failing_check_is_not_an_issue() {
assert!(!is_doctor_issue(false, true));
}
#[test]
fn test_genuine_failing_check_is_an_issue() {
assert!(is_doctor_issue(false, false));
}
#[test]
fn test_passing_check_is_never_an_issue() {
assert!(!is_doctor_issue(true, false));
assert!(!is_doctor_issue(true, true));
}
#[test]
fn test_count_hit_rate_zero_total_is_zero() {
assert_eq!(count_hit_rate(&zero_event_stats()), 0.0);
}
#[test]
fn test_count_hit_rate_counts_all_hit_kinds() {
let es = daemon::EventStatsResponse {
local_hits: 3,
prefetch_hits: 2,
remote_hits: 1,
dups: 0,
misses: 4,
..zero_event_stats()
};
assert!((count_hit_rate(&es) - 60.0).abs() < 1e-9);
}
#[test]
fn test_count_hit_rate_all_hits_is_hundred() {
let es = daemon::EventStatsResponse {
local_hits: 5,
..zero_event_stats()
};
assert!((count_hit_rate(&es) - 100.0).abs() < 1e-9);
}
#[test]
fn test_compile_weighted_hit_rate_none_when_no_compile_time() {
assert_eq!(compile_weighted_hit_rate(&zero_event_stats()), None);
}
#[test]
fn test_compile_weighted_hit_rate_weights_by_time() {
let es = daemon::EventStatsResponse {
hit_compile_time_ms: 750,
miss_compile_time_ms: 250,
..zero_event_stats()
};
let r = compile_weighted_hit_rate(&es).unwrap();
assert!((r - 75.0).abs() < 1e-9);
}
#[test]
fn test_format_duration_ms_buckets() {
assert_eq!(format_duration_ms(0), "~0ms");
assert_eq!(format_duration_ms(500), "~500ms");
assert_eq!(format_duration_ms(1_000), "~1s");
assert_eq!(format_duration_ms(59_000), "~59s");
assert_eq!(format_duration_ms(60_000), "~1min");
assert_eq!(format_duration_ms(3_600_000), "~1.0h");
assert_eq!(format_duration_ms(7_200_000), "~2.0h");
}
#[test]
fn test_format_relative_time_invalid_passes_through() {
assert_eq!(format_relative_time("not a date"), "not a date");
}
#[test]
fn test_format_relative_time_buckets() {
let now = chrono::Utc::now();
let fmt = |dt: chrono::DateTime<chrono::Utc>| {
format_relative_time(&dt.format("%Y-%m-%d %H:%M:%S").to_string())
};
assert_eq!(fmt(now - chrono::Duration::seconds(10)), "just now");
assert_eq!(fmt(now - chrono::Duration::minutes(5)), "5m ago");
assert_eq!(fmt(now - chrono::Duration::hours(3)), "3h ago");
assert_eq!(fmt(now - chrono::Duration::days(2)), "2d ago");
assert_eq!(fmt(now + chrono::Duration::hours(1)), "just now");
}
#[test]
fn test_is_binary_artifact_extensions() {
assert!(!is_binary_artifact(std::path::Path::new("libfoo.d")));
assert!(!is_binary_artifact(std::path::Path::new("libfoo.rmeta")));
assert!(!is_binary_artifact(std::path::Path::new("libfoo.rlib")));
assert!(is_binary_artifact(std::path::Path::new("myapp")));
assert!(is_binary_artifact(std::path::Path::new("libfoo.dylib")));
assert!(is_binary_artifact(std::path::Path::new("libfoo.so")));
assert!(is_binary_artifact(std::path::Path::new("myapp.exe")));
assert!(is_binary_artifact(std::path::Path::new("mylib.dll")));
assert!(!is_binary_artifact(std::path::Path::new("file.txt")));
}
#[test]
fn test_detect_profiles_empty() {
let dir = tempfile::tempdir().unwrap();
let profiles = detect_profiles(dir.path());
assert!(profiles.is_empty());
}
#[test]
fn test_detect_profiles_with_dirs() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir(dir.path().join("debug")).unwrap();
fs::create_dir(dir.path().join("release")).unwrap();
let profiles = detect_profiles(dir.path());
assert!(profiles.contains(&"debug".to_string()));
assert!(profiles.contains(&"release".to_string()));
assert!(!profiles.contains(&"profiling".to_string()));
}
#[test]
fn test_detect_profiles_all() {
let dir = tempfile::tempdir().unwrap();
for name in &["debug", "release", "profiling", "coverage"] {
fs::create_dir(dir.path().join(name)).unwrap();
}
let profiles = detect_profiles(dir.path());
assert_eq!(profiles.len(), 4);
}
#[test]
fn profiles_are_found_by_what_cargo_wrote_into_them() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path();
fs::create_dir_all(target.join("release")).unwrap();
fs::create_dir_all(target.join("ci/.fingerprint")).unwrap();
fs::create_dir_all(target.join("x86_64-unknown-linux-gnu/release/deps")).unwrap();
fs::create_dir_all(target.join("aarch64-apple-darwin/debug")).unwrap();
fs::create_dir_all(target.join("doc/some_crate")).unwrap();
fs::create_dir_all(target.join("tmp")).unwrap();
assert_eq!(
detect_profiles(target),
[
"release",
"aarch64-apple-darwin/debug",
"ci",
"x86_64-unknown-linux-gnu/release",
]
);
fs::write(
target.join("x86_64-unknown-linux-gnu/release/deps/libfoo.rlib"),
vec![0u8; 4096],
)
.unwrap();
let (stats, _) = compute_project_stats(target);
assert!(
stats.total_bytes >= 4096,
"cross-compiled output is counted"
);
}
#[test]
fn targets_inside_worktree_containers_are_found() {
let dir = tempfile::tempdir().unwrap();
for workspace in [
".worktrees/feature",
".claude/worktrees/agent-1",
".cache/other",
] {
let root = dir.path().join(workspace);
fs::create_dir_all(root.join("target/debug")).unwrap();
fs::write(root.join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();
fs::write(root.join("target/debug/out.rlib"), vec![0u8; 1024]).unwrap();
}
let mut results = Vec::new();
find_target_dirs(dir.path(), &mut results);
let mut found: Vec<_> = results
.iter()
.map(|entry| entry.path.strip_prefix(dir.path()).unwrap().to_path_buf())
.collect();
found.sort();
assert_eq!(
found,
[
std::path::PathBuf::from(".claude/worktrees/agent-1/target"),
std::path::PathBuf::from(".worktrees/feature/target"),
]
);
}
#[test]
fn test_sccache_program_detection_accepts_paths() {
assert!(is_sccache_program("sccache"));
assert!(is_sccache_program("/opt/homebrew/bin/sccache"));
assert!(is_sccache_program("sccache.exe"));
assert!(!is_sccache_program("kache"));
assert!(!is_sccache_program("sccache-wrapper"));
}
#[test]
fn test_sccache_rc_detection_ignores_fallback_setting() {
assert!(!active_sccache_migration_line("# RUSTC_WRAPPER=sccache"));
assert!(!active_sccache_migration_line(
"export KACHE_FALLBACK=sccache"
));
assert!(active_sccache_migration_line(
"export RUSTC_WRAPPER=sccache"
));
assert!(active_sccache_migration_line("rustc-wrapper = \"sccache\""));
}
#[test]
fn test_fallback_is_sccache() {
let dir = tempfile::tempdir().unwrap();
let mut cfg = save_manifest_config(dir.path().to_path_buf(), None);
assert!(!fallback_is_sccache(None));
assert!(!fallback_is_sccache(Some(&cfg)));
cfg.fallback = Some("sccache".to_string());
assert!(fallback_is_sccache(Some(&cfg)));
cfg.fallback = Some("/usr/local/bin/sccache".to_string());
assert!(fallback_is_sccache(Some(&cfg)));
cfg.fallback = Some("/usr/bin/gcc".to_string());
assert!(!fallback_is_sccache(Some(&cfg)));
}
#[test]
fn test_dir_size_empty() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(dir_size(dir.path()), 0);
}
#[test]
fn test_dir_size_with_files() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.txt"), vec![0u8; 100]).unwrap();
fs::write(dir.path().join("b.txt"), vec![0u8; 200]).unwrap();
let size = dir_size(dir.path());
assert!(size >= 300, "expected >= 300, got {}", size);
}
#[test]
fn test_dir_size_recursive() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("sub");
fs::create_dir(&sub).unwrap();
fs::write(sub.join("file.txt"), vec![0u8; 50]).unwrap();
let size = dir_size(dir.path());
assert!(size >= 50);
}
#[test]
fn test_dir_size_nonexistent() {
assert_eq!(dir_size(std::path::Path::new("/nonexistent/path")), 0);
}
#[test]
fn test_find_target_dirs_empty() {
let dir = tempfile::tempdir().unwrap();
let mut results = Vec::new();
find_target_dirs(dir.path(), &mut results);
assert!(results.is_empty());
}
#[test]
fn test_find_target_dirs_with_cargo_project() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path().join("myproject");
fs::create_dir(&project).unwrap();
fs::write(project.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
let target = project.join("target");
fs::create_dir(&target).unwrap();
let debug = target.join("debug");
fs::create_dir(&debug).unwrap();
fs::write(debug.join("test.rlib"), vec![0u8; 100]).unwrap();
let mut results = Vec::new();
find_target_dirs(dir.path(), &mut results);
assert_eq!(results.len(), 1);
assert!(results[0].size >= 100);
assert!(results[0].profiles.contains(&"debug".to_string()));
}
#[test]
fn find_target_dirs_ignores_an_empty_cargo_target() {
let dir = tempfile::tempdir().unwrap();
let project = dir.path().join("empty-project");
fs::create_dir_all(project.join("target")).unwrap();
fs::write(project.join("Cargo.toml"), "[package]\nname = \"empty\"").unwrap();
let mut results = Vec::new();
find_target_dirs(dir.path(), &mut results);
assert!(
results.is_empty(),
"an empty target has no reclaimable bytes and must not be offered for deletion"
);
}
#[test]
fn test_find_target_dirs_skips_hidden() {
let dir = tempfile::tempdir().unwrap();
let hidden = dir.path().join(".hidden");
fs::create_dir(&hidden).unwrap();
fs::write(hidden.join("Cargo.toml"), "[package]").unwrap();
fs::create_dir(hidden.join("target")).unwrap();
let mut results = Vec::new();
find_target_dirs(dir.path(), &mut results);
assert!(results.is_empty());
}
#[test]
fn test_find_target_dirs_skips_node_modules() {
let dir = tempfile::tempdir().unwrap();
let nm = dir.path().join("node_modules");
fs::create_dir(&nm).unwrap();
fs::write(nm.join("Cargo.toml"), "[package]").unwrap();
fs::create_dir(nm.join("target")).unwrap();
let mut results = Vec::new();
find_target_dirs(dir.path(), &mut results);
assert!(results.is_empty());
}
#[test]
fn test_compute_project_stats_empty_dir() {
let dir = tempfile::tempdir().unwrap();
let (stats, breakdown) = compute_project_stats(dir.path());
assert_eq!(stats.total_bytes, 0);
assert_eq!(stats.cached_bytes, 0);
assert_eq!(stats.estimated_reclaimable_bytes, 0);
assert_eq!(breakdown.incremental, 0);
}
#[test]
fn reclaim_estimator_uses_private_bytes_for_partial_reflinks() {
let mut reclaim = ReclaimEstimator::default();
reclaim.record(
10_000,
StorageObservation {
sharing: crate::sharing::Sharing {
shared: true,
private_bytes: 4_000,
snapshot_bytes: 0,
},
hardlink: None,
},
);
assert_eq!(
reclaim.estimated_reclaimable_bytes(),
4_000,
"a partially shared reflink reclaims its private extents, not zero"
);
}
#[cfg(unix)]
#[test]
fn reclaim_estimator_clamps_private_bytes_to_allocated_storage() {
assert_eq!(clamp_private_bytes(1 << 30, 1 << 30, Some(0)), 0);
assert_eq!(clamp_private_bytes(10_000, 8_000, Some(4_096)), 4_096);
assert_eq!(clamp_private_bytes(10_000, 4_000, Some(8_192)), 4_000);
}
#[test]
fn reclaim_estimator_collapses_internal_hardlink_groups() {
let id = FileIdentity {
device: 7,
inode: 11,
};
let observation = StorageObservation {
sharing: crate::sharing::Sharing::unknown_for(100),
hardlink: Some(HardlinkObservation { id, total_links: 2 }),
};
let mut stats = ProjectStats::default();
let mut breakdown = CategoryBreakdown::default();
let mut reclaim = ReclaimEstimator::default();
let mut candidates = Vec::new();
for _ in 0..2 {
record_scanned_file(
&mut stats,
&mut breakdown,
&mut reclaim,
&mut candidates,
100,
ProjectBucket::Deps,
true,
observation,
);
}
finalize_cache_candidates(&mut stats, &mut breakdown, &reclaim, &candidates);
assert_eq!(
reclaim.estimated_reclaimable_bytes(),
100,
"two names for one inode reclaim that inode only once"
);
assert_eq!(
stats.cached_bytes, 0,
"hardlinks wholly inside target/ are not evidence of a store link"
);
assert_eq!(stats.local_bytes, 200, "both apparent paths stay local");
}
#[test]
fn cache_candidates_accept_each_independent_store_sharing_signal() {
let external_id = FileIdentity {
device: 17,
inode: 23,
};
let mut reclaim = ReclaimEstimator::default();
reclaim.record(
200,
StorageObservation {
sharing: crate::sharing::Sharing::unknown_for(200),
hardlink: Some(HardlinkObservation {
id: external_id,
total_links: 2,
}),
},
);
let candidates = [
CacheCandidate {
size: 100,
bucket: ProjectBucket::Deps,
reflink_shared: true,
hardlink_id: None,
},
CacheCandidate {
size: 200,
bucket: ProjectBucket::Deps,
reflink_shared: false,
hardlink_id: Some(external_id),
},
];
let mut stats = ProjectStats::default();
let mut breakdown = CategoryBreakdown::default();
finalize_cache_candidates(&mut stats, &mut breakdown, &reclaim, &candidates);
assert_eq!(stats.cached_bytes, 300);
assert_eq!(stats.local_bytes, 0);
assert_eq!(breakdown.deps_local, 0);
}
#[test]
fn reclaim_estimator_counts_binary_reflink_private_bytes() {
let mut stats = ProjectStats::default();
let mut breakdown = CategoryBreakdown::default();
let mut reclaim = ReclaimEstimator::default();
let mut candidates = Vec::new();
record_scanned_file(
&mut stats,
&mut breakdown,
&mut reclaim,
&mut candidates,
100,
ProjectBucket::Binaries,
false,
StorageObservation {
sharing: crate::sharing::Sharing {
shared: true,
private_bytes: 25,
snapshot_bytes: 0,
},
hardlink: None,
},
);
finalize_cache_candidates(&mut stats, &mut breakdown, &reclaim, &candidates);
assert_eq!(reclaim.estimated_reclaimable_bytes(), 25);
assert_eq!(stats.cached_bytes, 0, "binary bucketing stays unchanged");
assert_eq!(breakdown.binaries, 100);
}
#[test]
fn windows_identity_parts_preserve_volume_and_both_file_index_halves() {
let identity = windows_file_identity_from_parts(0x1020_3040, 0x1122_3344, 0x5566_7788);
assert_eq!(identity.device, 0x1020_3040);
assert_eq!(identity.inode, 0x1122_3344_5566_7788);
assert_ne!(
identity,
windows_file_identity_from_parts(0x1020_3040, 0x1122_3344, 0x5566_7789),
"the low DWORD remains part of the stable identity"
);
}
#[test]
fn windows_storage_observation_covers_identity_and_link_count_boundaries() {
let id = FileIdentity {
device: 5,
inode: 8,
};
let single = windows_storage_observation(4096, Some((id, 1)));
assert_eq!(single.sharing.private_bytes, 4096);
assert!(
single.hardlink.is_none(),
"one link is not a hardlink group"
);
let linked = windows_storage_observation(4096, Some((id, 2)));
assert_eq!(
linked.hardlink,
Some(HardlinkObservation { id, total_links: 2 })
);
let unavailable = windows_storage_observation(4096, None);
assert_eq!(unavailable.sharing.private_bytes, 0);
assert!(!unavailable.sharing.shared);
assert!(unavailable.hardlink.is_none());
}
#[test]
fn unsupported_storage_observation_treats_the_whole_file_as_private() {
let observation = unsupported_storage_observation(4096);
assert_eq!(observation.sharing.private_bytes, 4096);
assert!(!observation.sharing.shared);
assert!(observation.hardlink.is_none());
}
#[cfg(windows)]
#[test]
fn windows_hardlinks_share_one_reclaimable_file() {
let dir = tempfile::tempdir().unwrap();
let first = dir.path().join("first.rlib");
let second = dir.path().join("second.rlib");
fs::write(&first, vec![0u8; 4096]).unwrap();
fs::hard_link(&first, &second).unwrap();
let first_meta = fs::metadata(&first).unwrap();
let second_meta = fs::metadata(&second).unwrap();
let (first_identity, first_links) =
query_windows_file_identity(&first).expect("first hardlink has a Windows identity");
let (second_identity, second_links) =
query_windows_file_identity(&second).expect("second hardlink has a Windows identity");
assert_eq!(first_identity, second_identity);
assert_eq!((first_links, second_links), (2, 2));
let distinct = dir.path().join("distinct.rlib");
fs::write(&distinct, vec![0u8; 4096]).unwrap();
let (distinct_identity, distinct_links) =
query_windows_file_identity(&distinct).expect("a plain file has a Windows identity");
assert_ne!(first_identity, distinct_identity);
assert_eq!(distinct_links, 1);
assert!(
query_windows_file_identity(&dir.path().join("missing.rlib")).is_none(),
"a failed open must not invent an identity"
);
let first_observation = observe_storage(&first, &first_meta);
let second_observation = observe_storage(&second, &second_meta);
assert_eq!(first_observation.hardlink, second_observation.hardlink);
let distinct_observation = observe_storage(&distinct, &fs::metadata(&distinct).unwrap());
assert!(distinct_observation.hardlink.is_none());
let mut reclaim = ReclaimEstimator::default();
reclaim.record(first_meta.len(), first_observation);
reclaim.record(second_meta.len(), second_observation);
assert_eq!(reclaim.estimated_reclaimable_bytes(), 4096);
}
#[test]
fn test_compute_project_stats_with_profiles() {
let dir = tempfile::tempdir().unwrap();
let debug = dir.path().join("debug");
fs::create_dir(&debug).unwrap();
let incr = debug.join("incremental");
fs::create_dir(&incr).unwrap();
fs::write(incr.join("data"), vec![0u8; 100]).unwrap();
let fp = debug.join(".fingerprint");
fs::create_dir(&fp).unwrap();
fs::write(fp.join("hash"), vec![0u8; 50]).unwrap();
let build = debug.join("build");
fs::create_dir(&build).unwrap();
fs::write(build.join("script"), vec![0u8; 30]).unwrap();
let deps = debug.join("deps");
fs::create_dir(&deps).unwrap();
fs::write(deps.join("libfoo.rlib"), vec![0u8; 200]).unwrap();
let (stats, breakdown) = compute_project_stats(dir.path());
assert!(stats.total_bytes > 0);
assert!(breakdown.incremental >= 100);
assert!(breakdown.fingerprints >= 50);
assert!(breakdown.build_scripts >= 30);
}
#[cfg(unix)]
#[test]
fn compute_project_stats_classifies_an_externally_hardlinked_rlib_as_cached() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target");
let debug = target.join("debug");
fs::create_dir_all(&debug).unwrap();
let retained_blob = dir.path().join("store-blob.rlib");
fs::write(&retained_blob, vec![0u8; 4096]).unwrap();
fs::hard_link(&retained_blob, debug.join("libcached.rlib")).unwrap();
let (stats, breakdown) = compute_project_stats(&target);
assert_eq!(stats.total_bytes, 4096);
assert_eq!(stats.cached_bytes, 4096);
assert_eq!(stats.local_bytes, 0);
assert_eq!(breakdown.other, 0);
}
#[cfg(unix)]
#[test]
fn compute_project_stats_does_not_follow_profile_symlinks() {
let dir = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
fs::write(outside.path().join("outside.rlib"), vec![0u8; 4096]).unwrap();
std::os::unix::fs::symlink(outside.path(), dir.path().join("debug")).unwrap();
let (stats, _) = compute_project_stats(dir.path());
assert_eq!(stats.total_bytes, 0);
assert_eq!(stats.estimated_reclaimable_bytes, 0);
assert!(detect_profiles(dir.path()).is_empty());
}
#[test]
fn compute_project_stats_reads_cargos_per_unit_build_layout() {
let dir = tempfile::tempdir().unwrap();
let build = dir.path().join("debug/build");
let write = |path: &str, len: usize| {
let path = build.join(path);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, vec![0u8; len]).unwrap();
};
write("foo/0123456789abcdef/fingerprint/lib-foo", 5);
write("foo/0123456789abcdef/out/libfoo-0123456789abcdef.rlib", 7);
write("foo/1111111111111111/out/build_script_build", 11);
write("foo/2222222222222222/out/gen.rs", 13);
write("foo/2222222222222222/run/stdout", 17);
write("bar-0123456789abcdef/output", 19);
write("loose", 23);
write("foo/0123456789abcdef/extra", 29);
write("foo/not-a-unit/out/libfoo.rlib", 31);
write("foo/fedcba9876543210", 37);
let (stats, breakdown) = compute_project_stats(dir.path());
assert_eq!(stats.total_bytes, 95 + 29 + 31 + 37);
assert_eq!(breakdown.fingerprints, 5);
assert_eq!(breakdown.deps_local, 7, "a compiled unit's out is deps");
assert_eq!(
breakdown.build_scripts,
11 + 13 + 17 + 19 + 23 + 29 + 31 + 37,
"a build script's binary, a run, the legacy layout, and anything \
unrecognized, as the whole directory used to"
);
assert_eq!(breakdown.other, 0);
}
#[test]
fn test_compute_project_stats_classifies_remaining_buckets() {
let dir = tempfile::tempdir().unwrap();
let debug = dir.path().join("debug");
let deps_nested = debug.join("deps").join("nested");
let other_dir = debug.join("examples");
fs::create_dir_all(&deps_nested).unwrap();
fs::create_dir_all(&other_dir).unwrap();
fs::write(debug.join("runner"), vec![0u8; 11]).unwrap();
fs::write(deps_nested.join("libdep.rmeta"), vec![0u8; 13]).unwrap();
fs::write(other_dir.join("note.txt"), vec![0u8; 17]).unwrap();
fs::write(dir.path().join("CACHEDIR.TAG"), vec![0u8; 19]).unwrap();
let (stats, breakdown) = compute_project_stats(dir.path());
assert_eq!(stats.total_bytes, 60);
assert!(breakdown.binaries >= 11, "got {}", breakdown.binaries);
assert!(breakdown.deps_local >= 13, "got {}", breakdown.deps_local);
assert!(breakdown.other >= 36, "got {}", breakdown.other);
assert_eq!(
stats.local_files, 4,
"nested local files are counted individually"
);
}
#[test]
fn test_parse_cargo_lock_crate_names_nonexistent() {
let _ = parse_cargo_lock_crate_names();
}
#[test]
fn test_parse_cargo_lock_crate_names_from_valid_missing_and_bad_files() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("Cargo.lock");
std::fs::write(
&lock,
"version = 3\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\n\n\
[[package]]\nname = \"tokio\"\nversion = \"1.0.0\"\n",
)
.unwrap();
let names = parse_cargo_lock_crate_names_from(&lock).unwrap();
assert!(names.contains("serde"));
assert!(names.contains("tokio"));
assert_eq!(
parse_cargo_lock_crate_names_from(&dir.path().join("missing.lock")),
None
);
std::fs::write(&lock, "not valid toml [[[[").unwrap();
assert_eq!(parse_cargo_lock_crate_names_from(&lock), None);
}
#[test]
fn test_is_macos_protected() {
if !cfg!(target_os = "macos") {
assert!(!is_macos_protected(std::path::Path::new("/System/Library")));
assert!(!is_macos_protected(std::path::Path::new("/tmp/build")));
return;
}
assert!(is_macos_protected(std::path::Path::new("/System/Library")));
assert!(is_macos_protected(std::path::Path::new(
"/Library/Preferences"
)));
assert!(is_macos_protected(std::path::Path::new(
"/Applications/Xcode.app"
)));
assert!(is_macos_protected(std::path::Path::new(
"/Volumes/External"
)));
assert!(is_macos_protected(std::path::Path::new("/private/var")));
assert!(is_macos_protected(std::path::Path::new("/Network/Servers")));
if let Some(home) = dirs::home_dir() {
assert!(is_macos_protected(&home.join("Desktop")));
assert!(is_macos_protected(&home.join("Documents")));
assert!(is_macos_protected(&home.join("Downloads")));
assert!(is_macos_protected(&home.join("Library")));
assert!(is_macos_protected(&home.join("Pictures")));
assert!(is_macos_protected(&home.join("Music")));
assert!(is_macos_protected(&home.join("Movies")));
assert!(is_macos_protected(&home.join("Applications")));
assert!(is_macos_protected(&home.join("Public")));
assert!(is_macos_protected(&home.join("Documents/subfolder")));
assert!(!is_macos_protected(&home.join("projects")));
assert!(!is_macos_protected(&home.join("src")));
assert!(!is_macos_protected(&home.join("work")));
assert!(!is_macos_protected(&home.join(".config")));
}
assert!(!is_macos_protected(std::path::Path::new("/tmp/build")));
assert!(!is_macos_protected(std::path::Path::new("/Users/dev/code")));
}
#[test]
fn test_category_breakdown_default() {
let b = CategoryBreakdown::default();
assert_eq!(b.incremental, 0);
assert_eq!(b.build_scripts, 0);
assert_eq!(b.fingerprints, 0);
assert_eq!(b.binaries, 0);
assert_eq!(b.deps_local, 0);
assert_eq!(b.other, 0);
}
#[cfg(unix)]
#[test]
fn init_reports_whether_this_terminal_paces_tests() {
assert_eq!(test_pacing(Some("kache test-runner")), TestPacing::Active);
assert_eq!(
test_pacing(Some("qemu-x86_64")),
TestPacing::Overridden("qemu-x86_64".into())
);
assert_eq!(test_pacing(Some("")), TestPacing::Pending);
assert_eq!(test_pacing(None), TestPacing::Pending);
}
#[cfg(unix)]
#[test]
fn init_reads_the_host_triple_from_rustc() {
let host = rustc_host().expect("rustc on the test PATH");
assert!(host.contains(std::env::consts::ARCH), "{host}");
}
#[test]
fn test_cargo_wrapper_edit_create() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
let plan = plan_cargo_wrapper_edit(&path).unwrap();
assert!(matches!(plan, CargoWrapperPlan::Create));
let new = apply_cargo_wrapper_edit("", &plan);
assert_eq!(new, "[build]\nrustc-wrapper = \"kache\"\n");
}
#[test]
fn rustdoc_assignment_ignores_a_longer_key_and_a_comment() {
assert!(is_rustdoc_assignment("rustdoc = \"/shim/rustdoc\""));
assert!(is_rustdoc_assignment("rustdoc=\"/shim/rustdoc\""));
assert!(is_rustdoc_assignment(" rustdoc = \"/shim/rustdoc\""));
assert!(!is_rustdoc_assignment("rustdoc-extra = \"/shim/rustdoc\""));
assert!(!is_rustdoc_assignment("# rustdoc = \"/shim/rustdoc\""));
assert!(!is_rustdoc_assignment("build-rustdoc = \"x\""));
}
#[test]
fn rustdoc_edit_decision_writes_only_for_kache_without_a_key() {
let shim = "/home/user/.local/lib/kache/shims/rustdoc";
assert_eq!(rustdoc_edit_decision(None, None, shim), RustdocEdit::Skip);
assert_eq!(
rustdoc_edit_decision(Some("sccache"), None, shim),
RustdocEdit::Skip
);
assert_eq!(
rustdoc_edit_decision(Some("kache"), None, shim),
RustdocEdit::Write
);
assert_eq!(
rustdoc_edit_decision(Some("kache"), Some(shim), shim),
RustdocEdit::Already
);
assert_eq!(
rustdoc_edit_decision(Some("kache"), Some("/usr/bin/rustdoc"), shim),
RustdocEdit::Leave
);
}
#[test]
fn apply_rustdoc_edit_inserts_replaces_and_leaves_a_correct_line() {
let shim = "/shim/rustdoc";
assert_eq!(
apply_rustdoc_edit("", shim),
"[build]\nrustdoc = \"/shim/rustdoc\"\n"
);
assert_eq!(
apply_rustdoc_edit("[build]\nrustc-wrapper = \"kache\"\n", shim),
"[build]\nrustdoc = \"/shim/rustdoc\"\nrustc-wrapper = \"kache\"\n"
);
assert_eq!(
apply_rustdoc_edit(
"[build]\nrustdoc = \"/old/rustdoc\"\nrustc-wrapper = \"kache\"\n",
shim
),
"[build]\nrustdoc = \"/shim/rustdoc\"\nrustc-wrapper = \"kache\"\n"
);
let correct = "[build]\nrustdoc = \"/shim/rustdoc\"";
assert_eq!(apply_rustdoc_edit(correct, shim), correct);
assert_eq!(
apply_rustdoc_edit(
"[build]\nrustc-wrapper = \"kache\"\nrustdoc = \"/old\"\n",
shim
),
"[build]\nrustc-wrapper = \"kache\"\nrustdoc = \"/shim/rustdoc\"\n"
);
assert_eq!(
apply_rustdoc_edit("[net]\noffline = true\n", shim),
"[net]\noffline = true\n\n[build]\nrustdoc = \"/shim/rustdoc\"\n"
);
let extra = "[build]\nrustdoc-extra = \"keep\"\n# rustdoc = \"no\"\nrustdoc = \"/a\"\nrustdoc = \"/b\"\n";
let replaced = apply_rustdoc_edit(extra, shim);
assert!(replaced.contains("rustdoc-extra = \"keep\""));
assert!(replaced.contains("# rustdoc = \"no\""));
assert_eq!(
replaced
.lines()
.filter(|line| is_rustdoc_assignment(line))
.count(),
1,
"{replaced}"
);
let outside = "[net]\nrustdoc = \"/old\"\n[build]\nrustc-wrapper = \"kache\"\n";
let edited = apply_rustdoc_edit(outside, shim);
assert!(edited.contains("rustdoc = \"/old\""), "{edited}");
assert!(
edited.contains("[build]\nrustdoc = \"/shim/rustdoc\"\nrustc-wrapper = \"kache\"\n"),
"{edited}"
);
let later = "[build]\nrustc-wrapper = \"kache\"\n[env]\nrustdoc = \"/keep\"\n";
let edited = apply_rustdoc_edit(later, shim);
assert_eq!(
edited,
"[build]\nrustdoc = \"/shim/rustdoc\"\nrustc-wrapper = \"kache\"\n[env]\nrustdoc = \"/keep\"\n",
"{edited}"
);
for distractor in ["note]", "[not-closed"] {
let body =
format!("[build]\nrustc-wrapper = \"kache\"\n{distractor}\nrustdoc = \"/old\"\n");
let edited = apply_rustdoc_edit(&body, shim);
assert!(!edited.contains("/old"), "{distractor}: {edited}");
assert_eq!(
edited.matches("rustdoc =").count(),
1,
"{distractor}: {edited}"
);
}
}
#[test]
fn apply_rustdoc_edit_preserves_commented_table_boundaries() {
let original = "[build] # compiler settings\nrustc-wrapper = \"kache\"\n\
[env] # retained environment\nrustdoc = \"keep\"\n";
toml::from_str::<toml::Value>(original).expect("valid original Cargo config");
let edited = checked_rustdoc_edit(original, "/shim/rustdoc").unwrap();
let parsed: toml::Value =
toml::from_str(&edited).expect("valid Cargo config after rustdoc edit");
assert_eq!(parsed["build"]["rustc-wrapper"].as_str(), Some("kache"));
assert_eq!(parsed["build"]["rustdoc"].as_str(), Some("/shim/rustdoc"));
assert_eq!(parsed["env"]["rustdoc"].as_str(), Some("keep"));
assert!(edited.contains("[build] # compiler settings"));
assert!(edited.contains("[env] # retained environment"));
assert_eq!(
checked_rustdoc_edit(&edited, "/shim/rustdoc").unwrap(),
edited
);
}
#[test]
fn checked_rustdoc_edit_refuses_unsupported_layouts_and_changes_to_other_values() {
for original in [
"[ build ]\nrustc-wrapper = 'kache'\n",
"[\"build\"]\nrustc-wrapper = 'kache'\n",
"build.rustc-wrapper = 'kache'\n",
"build = { rustc-wrapper = 'kache' }\n",
"[metadata]\nexample = '''\n[build] # an example, inside a string\n'''\n\
[build]\nrustc-wrapper = 'kache'\n",
] {
toml::from_str::<toml::Value>(original).expect("valid original Cargo config");
assert!(
checked_rustdoc_edit(original, "/shim/rustdoc").is_err(),
"an unsafe text edit must be rejected: {original}"
);
}
for original in ["not TOML", "[env]\nCC = 'cc'\n", "build = 'other'\n"] {
assert!(checked_rustdoc_edit(original, "/shim/rustdoc").is_err());
}
}
#[test]
fn rustdoc_doctor_fix_is_present_only_when_the_check_fails() {
assert_eq!(rustdoc_doctor_fix(true), None);
assert_eq!(
rustdoc_doctor_fix(false).as_deref(),
Some("point build.rustdoc at the rustdoc shim from kache init")
);
}
#[test]
fn rustdoc_doctor_fields_cover_windows_unset_and_a_bad_path() {
assert_eq!(
rustdoc_doctor_fields(false, Some("/usr/bin/rustdoc"), false),
(true, "not cached on Windows")
);
assert_eq!(rustdoc_doctor_fields(true, None, false), (true, "not set"));
assert_eq!(
rustdoc_doctor_fields(true, Some("/shim/rustdoc"), true),
(true, "shim")
);
assert_eq!(
rustdoc_doctor_fields(true, Some("/usr/bin/rustdoc"), false),
(false, "not a rustdoc shim")
);
}
#[cfg(any(unix, windows))]
#[test]
fn rustdoc_shim_path_requires_an_absolute_rustdoc_file() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("rustdoc");
let missing = missing.to_string_lossy().into_owned();
assert!(rustdoc_shim_path_shape(&missing));
assert!(!rustdoc_shim_path_ok(&missing));
assert!(!rustdoc_shim_path_shape("rustdoc"));
assert!(!rustdoc_shim_path_shape(
&std::env::temp_dir().join("rustdoc-extra").to_string_lossy()
));
assert!(!rustdoc_shim_path_shape(
&std::env::temp_dir().join("kache").to_string_lossy()
));
let file = dir.path().join("rustdoc");
std::fs::write(&file, b"kache").unwrap();
assert!(
!rustdoc_shim_path_ok(&file.to_string_lossy()),
"an absolute file named rustdoc is the real toolchain until it is a shim"
);
std::fs::write(dir.path().join(".kache-shims"), b"kache shims\n").unwrap();
assert!(rustdoc_shim_path_ok(&file.to_string_lossy()));
let other = dir.path().join("kache");
std::fs::write(&other, b"kache").unwrap();
assert!(!rustdoc_shim_path_ok(&other.to_string_lossy()));
let as_dir = dir.path().join("nested/rustdoc");
std::fs::create_dir_all(&as_dir).unwrap();
assert!(!rustdoc_shim_path_ok(&as_dir.to_string_lossy()));
}
#[cfg(unix)]
#[test]
fn rustdoc_shim_path_accepts_a_symlink_to_kache() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("kache");
std::fs::write(&target, b"bin").unwrap();
let link = dir.path().join("rustdoc");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(rustdoc_shim_path_ok(&link.to_string_lossy()));
std::fs::remove_file(&link).unwrap();
let other = dir.path().join("not-kache");
std::fs::write(&other, b"bin").unwrap();
std::os::unix::fs::symlink(&other, &link).unwrap();
assert!(!rustdoc_shim_path_ok(&link.to_string_lossy()));
}
#[test]
fn test_cargo_wrapper_edit_replace() {
let existing = "[build]\nrustc-wrapper = \"sccache\"\n";
let plan = CargoWrapperPlan::Replace("sccache".into());
let new = apply_cargo_wrapper_edit(existing, &plan);
assert_eq!(new, "[build]\nrustc-wrapper = \"kache\"\n");
}
#[test]
fn test_cargo_wrapper_edit_replace_quote_styles_and_miss() {
let single = apply_cargo_wrapper_edit(
"[build]\nrustc-wrapper = 'sccache'\n",
&CargoWrapperPlan::Replace("sccache".into()),
);
assert_eq!(single, "[build]\nrustc-wrapper = \"kache\"\n");
let compact = apply_cargo_wrapper_edit(
"[build]\nrustc-wrapper=\"sccache\"\n",
&CargoWrapperPlan::Replace("sccache".into()),
);
assert_eq!(compact, "[build]\nrustc-wrapper = \"kache\"\n");
let unchanged = apply_cargo_wrapper_edit(
"[build]\nrustc-wrapper = \"other\"\n",
&CargoWrapperPlan::Replace("sccache".into()),
);
assert_eq!(unchanged, "[build]\nrustc-wrapper = \"other\"\n");
}
#[test]
fn doctor_fix_changes_only_an_sccache_wrapper_and_keeps_a_backup() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
let source = "# sccache since 2023\n[build]\nrustc-wrapper = \"/opt/homebrew/bin/sccache\" # mine\njobs = 4\n\n[env]\nSCCACHE_DIR = \"/data/sccache\"\n";
fs::write(&path, source).unwrap();
let replaced = migrate_cargo_wrapper(&path).unwrap();
assert_eq!(replaced.as_deref(), Some("/opt/homebrew/bin/sccache"));
assert_eq!(
fs::read_to_string(&path).unwrap(),
source.replace("\"/opt/homebrew/bin/sccache\"", "\"kache\"")
);
let backups: Vec<String> = fs::read_dir(dir.path())
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|backup| backup != &path)
.map(|backup| fs::read_to_string(backup).unwrap())
.collect();
assert_eq!(backups, [source]);
}
#[test]
fn doctor_fix_leaves_other_cargo_configs_alone() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
for source in [
"[build]\nrustc-wrapper = \"other-wrapper\"\n# sccache was here\n",
"[build]\njobs = 2\n[env]\nSCCACHE_DIR = \"/data/sccache\"\n",
"[build\nrustc-wrapper = \"sccache\"\n",
] {
fs::write(&path, source).unwrap();
assert!(matches!(migrate_cargo_wrapper(&path), Ok(None) | Err(_)));
assert_eq!(fs::read_to_string(&path).unwrap(), source);
}
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1, "no backups");
}
#[test]
fn migrate_with_nothing_to_do_points_at_init() {
let summary = migration_summary(&[], &[], false);
assert!(summary.contains("kache init"), "{summary}");
assert!(!summary.contains(".zshrc"), "{summary}");
let rc = vec![(
".bashrc".to_string(),
vec![(3, "export RUSTC_WRAPPER=sccache".to_string())],
)];
let summary = migration_summary(&[], &rc, true);
assert!(
summary.contains("+ export RUSTC_WRAPPER=kache"),
"{summary}"
);
assert!(!summary.contains("source ~/.zshrc"), "{summary}");
assert!(!summary.contains("--purge-sccache"), "{summary}");
let summary = migration_summary(&[], &rc, false);
assert!(summary.contains("--purge-sccache"), "{summary}");
}
#[test]
fn doctor_suggests_fix_only_for_failures_fix_repairs() {
let argv = |next: &[crate::machine::NextAction]| {
next.iter()
.map(|action| action.argv.join(" "))
.collect::<Vec<_>>()
};
assert!(doctor_next_actions(&["Remote access", "Config file"], &[], &[]).is_empty());
assert_eq!(
argv(&doctor_next_actions(
&["Cargo config", "Daemon version"],
&["Cargo config", "Daemon version"],
&[],
)),
["kache doctor --fix"]
);
assert_eq!(
argv(&doctor_next_actions(
&["RUSTC_WRAPPER", "cargo doc"],
&[],
&["RUSTC_WRAPPER", "cargo doc"]
)),
["kache init"]
);
assert!(doctor_next_actions(&["Remote access"], &["sccache"], &["cargo doc"]).is_empty());
}
#[test]
fn cargo_wrapper_repair_names_fix_for_sccache_and_init_otherwise() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path().join("cargo-home");
let project = dir.path().join("project/.cargo");
let in_home = home.join("config.toml");
assert_eq!(cargo_wrapper_repair("sccache", &in_home, &home), Some(true));
assert_eq!(
cargo_wrapper_repair("/usr/local/bin/sccache", &in_home, &home),
Some(true)
);
assert_eq!(cargo_wrapper_repair("other", &in_home, &home), Some(false));
assert_eq!(cargo_wrapper_repair("", &in_home, &home), Some(false));
assert_eq!(
cargo_wrapper_repair("sccache", &project.join("config.toml"), &home),
None
);
}
#[test]
fn doctor_config_file_check_fails_on_a_file_that_does_not_parse() {
let path = std::path::Path::new("/etc/elsewhere/kache.toml");
let (pass, detail, fix) =
doctor_config_file_check(path, true, Some("TOML parse error at line 2, column 3: x"));
assert!(!pass);
assert!(detail.contains("/etc/elsewhere/kache.toml"), "{detail}");
assert!(detail.contains("line 2"), "{detail}");
assert!(fix.unwrap().contains("defaults"));
let shown = crate::wrapper_config::display_path(path);
assert_eq!(
doctor_config_file_check(path, true, None),
(true, shown.clone(), None)
);
assert_eq!(
doctor_config_file_check(path, false, None),
(true, format!("{shown} (not created; defaults apply)"), None)
);
}
#[test]
fn doctor_files_each_wrapper_repair_under_the_right_command() {
use crate::wrapper_config::WrapperSetting;
let home = std::path::Path::new("/home/me/.cargo");
let in_home = |value: &str| WrapperSetting::CargoConfig {
value: value.into(),
path: home.join("config.toml"),
};
assert_eq!(wrapper_setting_repair(None, home), Some(false));
assert_eq!(
wrapper_setting_repair(Some(&in_home("sccache")), home),
Some(true)
);
assert_eq!(
wrapper_setting_repair(Some(&in_home("other")), home),
Some(false)
);
let env = WrapperSetting::Environment {
value: "sccache".into(),
};
assert_eq!(wrapper_setting_repair(Some(&env), home), None);
let (mut fix, mut init) = (Vec::new(), Vec::new());
note_repair("a", Some(true), &mut fix, &mut init);
note_repair("b", Some(false), &mut fix, &mut init);
note_repair("c", None, &mut fix, &mut init);
assert_eq!((fix, init), (vec!["a"], vec!["b"]));
}
#[test]
fn doctor_fix_refuses_a_wrapper_it_cannot_rewrite() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
let source = "[build]\nrustc-wrapper = \"sccache\"\n";
fs::write(&path, source).unwrap();
let error = migrate_cargo_wrapper(&path).unwrap_err().to_string();
assert!(error.contains("edit"), "{error}");
assert_eq!(fs::read_to_string(&path).unwrap(), source);
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 1, "no backup");
assert!(ensure_cargo_wrapper_is_kache("[build]\nrustc-wrapper = \"kache\"\n", &path).is_ok());
assert!(ensure_cargo_wrapper_is_kache(source, &path).is_err());
}
#[test]
fn test_cargo_wrapper_edit_add_under_build() {
let existing = "[build]\njobs = 4\n";
let plan = CargoWrapperPlan::AddUnderBuild;
let new = apply_cargo_wrapper_edit(existing, &plan);
assert!(new.contains("rustc-wrapper = \"kache\""));
assert!(new.contains("jobs = 4"));
}
#[test]
fn test_cargo_wrapper_edit_append_section() {
let existing = "[net]\nretry = 3\n";
let plan = CargoWrapperPlan::AppendSection;
let new = apply_cargo_wrapper_edit(existing, &plan);
assert!(new.contains("[net]"));
assert!(new.trim_end().ends_with("rustc-wrapper = \"kache\""));
}
#[test]
fn test_cargo_wrapper_edit_already_set() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "[build]\nrustc-wrapper = \"kache\"\n").unwrap();
let plan = plan_cargo_wrapper_edit(&path).unwrap();
assert!(matches!(plan, CargoWrapperPlan::AlreadySet));
}
#[test]
fn test_plan_cargo_wrapper_edit_replace_from_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "[build]\nrustc-wrapper = \"sccache\"\n").unwrap();
let plan = plan_cargo_wrapper_edit(&path).unwrap();
assert_eq!(plan, CargoWrapperPlan::Replace("sccache".into()));
let new = apply_cargo_wrapper_edit(&std::fs::read_to_string(&path).unwrap(), &plan);
assert_eq!(new, "[build]\nrustc-wrapper = \"kache\"\n");
}
#[test]
fn test_plan_cargo_wrapper_edit_add_under_build_from_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "[build]\njobs = 8\n").unwrap();
let plan = plan_cargo_wrapper_edit(&path).unwrap();
assert_eq!(plan, CargoWrapperPlan::AddUnderBuild);
let new = apply_cargo_wrapper_edit(&std::fs::read_to_string(&path).unwrap(), &plan);
assert!(new.contains("jobs = 8"));
assert!(new.contains("rustc-wrapper = \"kache\""));
}
#[test]
fn test_plan_cargo_wrapper_edit_append_section_from_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "[net]\nretry = 2\n").unwrap();
let plan = plan_cargo_wrapper_edit(&path).unwrap();
assert_eq!(plan, CargoWrapperPlan::AppendSection);
let new = apply_cargo_wrapper_edit(&std::fs::read_to_string(&path).unwrap(), &plan);
assert!(new.contains("[net]"));
assert!(new.contains("[build]"));
assert!(new.contains("rustc-wrapper = \"kache\""));
}
#[test]
fn test_plan_cargo_wrapper_edit_rejects_malformed_toml() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "this = = not valid toml\n").unwrap();
let err = plan_cargo_wrapper_edit(&path).unwrap_err();
assert!(
err.to_string().contains("parsing"),
"expected a parse-context error, got: {err}"
);
}
#[test]
fn test_get_workspace_crate_names_lists_members() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"a\", \"b\"]\nresolver = \"2\"\n",
)
.unwrap();
for m in ["a", "b"] {
std::fs::create_dir_all(root.join(m).join("src")).unwrap();
std::fs::write(
root.join(m).join("Cargo.toml"),
format!("[package]\nname = \"{m}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
)
.unwrap();
std::fs::write(root.join(m).join("src/lib.rs"), "").unwrap();
}
let names = get_workspace_crate_names(root.join("Cargo.toml").to_str().unwrap()).unwrap();
assert!(names.contains(&"a".to_string()), "got {names:?}");
assert!(names.contains(&"b".to_string()), "got {names:?}");
}
#[test]
fn test_get_workspace_crate_names_errors_on_bad_manifest() {
let dir = tempfile::tempdir().unwrap();
let bad = dir.path().join("Cargo.toml");
std::fs::write(&bad, "this is not valid toml [[[").unwrap();
assert!(get_workspace_crate_names(bad.to_str().unwrap()).is_err());
}
#[test]
fn test_workspace_filter_bad_manifest_is_empty_set() {
let dir = tempfile::tempdir().unwrap();
let bad = dir.path().join("Cargo.toml");
std::fs::write(&bad, "not toml [[[[").unwrap();
let filter = workspace_filter(Some(bad.to_str().unwrap())).unwrap();
assert!(filter.is_empty());
}
#[derive(Default)]
struct BackendCalls {
gets: Vec<String>,
puts: Vec<String>,
lists: Vec<String>,
}
struct TestBackend {
inner: crate::remote_backend::OpenDalBackend,
calls: std::sync::Mutex<BackendCalls>,
fail_put: bool,
listing: Option<Arc<ControlledListing>>,
}
impl TestBackend {
fn memory() -> Arc<Self> {
Arc::new(Self {
inner: crate::remote_backend::memory_backend(),
calls: std::sync::Mutex::new(BackendCalls::default()),
fail_put: false,
listing: None,
})
}
fn failing_put() -> Arc<Self> {
Arc::new(Self {
inner: crate::remote_backend::memory_backend(),
calls: std::sync::Mutex::new(BackendCalls::default()),
fail_put: true,
listing: None,
})
}
async fn seed(&self, key: &str, body: impl Into<Vec<u8>>) {
crate::remote_backend::RemoteBackend::put(&self.inner, key, body.into(), None)
.await
.expect("seed remote object");
}
fn get_calls(&self) -> Vec<String> {
self.calls.lock().unwrap().gets.clone()
}
fn put_calls(&self) -> Vec<String> {
self.calls.lock().unwrap().puts.clone()
}
fn list_calls(&self) -> Vec<String> {
self.calls.lock().unwrap().lists.clone()
}
}
fn as_remote_backend(backend: &Arc<TestBackend>) -> Arc<dyn crate::remote_backend::RemoteBackend> {
backend.clone()
}
fn as_cache_remote(
backend: Arc<dyn crate::remote_backend::RemoteBackend>,
remote: &crate::config::RemoteConfig,
) -> crate::cache_remote::V3Remote {
crate::cache_remote::V3Remote::new(backend, remote.clone())
}
#[async_trait::async_trait]
impl crate::remote_backend::RemoteBackend for TestBackend {
async fn head(&self, key: &str) -> Result<bool> {
crate::remote_backend::RemoteBackend::head(&self.inner, key).await
}
async fn get(
&self,
key: &str,
max_bytes: Option<u64>,
) -> Result<Option<crate::remote_backend::GetObject>> {
self.calls.lock().unwrap().gets.push(key.to_string());
crate::remote_backend::RemoteBackend::get(&self.inner, key, max_bytes).await
}
async fn put(&self, key: &str, body: Vec<u8>, content_type: Option<&str>) -> Result<()> {
self.calls.lock().unwrap().puts.push(key.to_string());
if self.fail_put {
anyhow::bail!("injected PUT failure for {key}");
}
crate::remote_backend::RemoteBackend::put(&self.inner, key, body, content_type).await
}
async fn list(&self, prefix: &str) -> Result<Vec<String>> {
self.calls.lock().unwrap().lists.push(prefix.to_string());
let _active = if let Some(listing) = &self.listing {
listing
.active
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let active = ActiveListing(listing);
listing.started.add_permits(1);
listing.release.acquire().await.unwrap().forget();
if listing.fail {
anyhow::bail!("injected LIST failure for {prefix}");
}
Some(active)
} else {
None
};
crate::remote_backend::RemoteBackend::list(&self.inner, prefix).await
}
fn describe(&self, key: &str) -> String {
crate::remote_backend::RemoteBackend::describe(&self.inner, key)
}
}
struct ControlledListing {
active: std::sync::atomic::AtomicUsize,
started: tokio::sync::Semaphore,
release: tokio::sync::Semaphore,
fail: bool,
}
struct ActiveListing<'a>(&'a ControlledListing);
impl Drop for ActiveListing<'_> {
fn drop(&mut self) {
self.0
.active
.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
}
}
fn controlled_listing(fail: bool) -> (Arc<TestBackend>, Arc<ControlledListing>) {
let listing = Arc::new(ControlledListing {
active: std::sync::atomic::AtomicUsize::new(0),
started: tokio::sync::Semaphore::new(0),
release: tokio::sync::Semaphore::new(0),
fail,
});
let mut backend = TestBackend::memory();
Arc::get_mut(&mut backend).unwrap().listing = Some(listing.clone());
(backend, listing)
}
#[tokio::test]
async fn scoped_sync_lists_overlap_within_the_configured_limit() {
use std::sync::atomic::Ordering::SeqCst;
for (concurrency, batches) in [
(0, vec![1, 1, 1]),
(1, vec![1, 1, 1]),
(2, vec![2, 1]),
(8, vec![3]),
] {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
config.s3_concurrency = concurrency;
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let crates = std::collections::HashSet::from(["foo".into(), "bar".into(), "baz".into()]);
let (backend, listing) = controlled_listing(false);
let cache_remote = as_cache_remote(as_remote_backend(&backend), &remote);
for (name, key) in [
("foo", "a".repeat(64)),
("bar", "b".repeat(64)),
("baz", "c".repeat(64)),
("unrelated", "d".repeat(64)),
] {
backend
.seed(
&format!("prefix/v3/manifests/{name}/{key}.json"),
b"{}".to_vec(),
)
.await;
}
for workspace in [false, true] {
backend.calls.lock().unwrap().lists.clear();
tokio::time::timeout(std::time::Duration::from_secs(2), async {
let (result, ()) = tokio::join!(
sync_with_client(
&cache_remote,
&config,
&store,
Some(&crates),
true,
false,
true,
false,
Some(&crates),
workspace,
false
),
async {
for &batch in &batches {
listing.started.acquire_many(batch).await.unwrap().forget();
assert_eq!(listing.active.load(SeqCst), batch as usize);
listing.release.add_permits(batch as usize);
}
}
);
result.unwrap();
})
.await
.expect("scoped listing must fill and release each configured batch");
let calls = backend.list_calls();
assert_eq!(calls.len(), 3);
for name in &crates {
assert!(calls.contains(&format!("prefix/v3/manifests/{name}/")));
}
assert_eq!(listing.active.load(SeqCst), 0);
}
let keys = list_remote_crates(
&cache_remote,
&std::collections::HashSet::new(),
concurrency,
)
.await
.unwrap();
assert!(keys.is_empty());
assert_eq!(backend.list_calls().len(), 3);
}
}
#[tokio::test]
async fn scoped_listing_propagates_failures_and_drops_cancelled_requests() {
use std::sync::atomic::Ordering::SeqCst;
let remote = test_remote_cfg();
let crates = std::collections::HashSet::from(["foo".into(), "bar".into(), "baz".into()]);
let (backend, listing) = controlled_listing(true);
let cache_remote = as_cache_remote(as_remote_backend(&backend), &remote);
let result = tokio::time::timeout(std::time::Duration::from_secs(2), async {
let (result, ()) = tokio::join!(list_remote_crates(&cache_remote, &crates, 2), async {
listing.started.acquire_many(2).await.unwrap().forget();
assert_eq!(listing.active.load(SeqCst), 2);
listing.release.add_permits(1);
});
result
})
.await
.expect("a failed LIST must not wait for the other requests");
assert!(format!("{:#}", result.unwrap_err()).contains("injected LIST failure"));
assert_eq!(listing.active.load(SeqCst), 0);
assert_eq!(backend.list_calls().len(), 2);
}
#[tokio::test]
async fn scoped_listing_merges_every_requested_crates_keys() {
let remote = test_remote_cfg();
let backend = TestBackend::memory();
let expected = std::collections::HashMap::from([
("a".repeat(64), "foo".to_string()),
("b".repeat(64), "foo".to_string()),
("c".repeat(64), "bar".to_string()),
]);
for (key, name) in &expected {
backend
.seed(
&format!("prefix/v3/manifests/{name}/{key}.json"),
b"{}".to_vec(),
)
.await;
}
backend
.seed(
&format!("prefix/v3/manifests/unrelated/{}.json", "d".repeat(64)),
b"{}".to_vec(),
)
.await;
let cache_remote = as_cache_remote(as_remote_backend(&backend), &remote);
let crates = std::collections::HashSet::from(["foo".into(), "bar".into()]);
assert_eq!(
list_remote_crates(&cache_remote, &crates, 2).await.unwrap(),
expected
);
}
#[tokio::test]
async fn upload_shards_uploads_one_shard_per_nonempty_bucket() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("Cargo.lock");
std::fs::write(
&lock,
"version = 3\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\n\n\
[[package]]\nname = \"tokio\"\nversion = \"1.0.0\"\n",
)
.unwrap();
let entries = vec![
crate::remote::ManifestEntry {
cache_key: "k-serde".to_string(),
crate_name: "serde".to_string(),
compile_time_ms: 1,
artifact_size: 1,
},
crate::remote::ManifestEntry {
cache_key: "k-tokio".to_string(),
crate_name: "tokio".to_string(),
compile_time_ms: 1,
artifact_size: 1,
},
];
let deps = crate::shards::parse_cargo_lock(&lock).unwrap();
let shard_set = crate::shards::compute_shards(&deps);
let expected = shard_set.shards.len();
let backend = TestBackend::memory();
let client = as_remote_backend(&backend);
let remote = test_remote_cfg();
let remote_cache: Arc<crate::cache_remote::V3Remote> =
Arc::new(crate::cache_remote::V3Remote::new(client, remote));
let uploaded = upload_shards(&remote_cache, "ns", &lock, &entries, None)
.await
.expect("upload_shards should succeed");
assert_eq!(uploaded, expected);
let puts = backend.put_calls();
assert_eq!(puts.len(), expected);
assert!(
puts.iter()
.all(|key| key.starts_with("prefix/_manifests/v3/ns/shards/"))
);
}
#[tokio::test]
async fn upload_shards_skips_when_no_entries_match() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("Cargo.lock");
std::fs::write(
&lock,
"version = 3\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\n",
)
.unwrap();
let backend = TestBackend::memory();
let client = as_remote_backend(&backend);
let remote = test_remote_cfg();
let remote_cache: Arc<crate::cache_remote::V3Remote> =
Arc::new(crate::cache_remote::V3Remote::new(client, remote));
let uploaded = upload_shards(&remote_cache, "ns", &lock, &[], None)
.await
.expect("should succeed with nothing to upload");
assert_eq!(uploaded, 0);
assert!(backend.put_calls().is_empty());
}
#[tokio::test]
async fn upload_shards_errors_on_malformed_lockfile() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("Cargo.lock");
std::fs::write(&lock, "not valid toml [[[[").unwrap();
let backend = TestBackend::memory();
let client = as_remote_backend(&backend);
let remote = test_remote_cfg();
let remote_cache: Arc<crate::cache_remote::V3Remote> =
Arc::new(crate::cache_remote::V3Remote::new(client, remote));
let err = upload_shards(&remote_cache, "ns", &lock, &[], None)
.await
.expect_err("bad lockfile should error");
assert!(
err.to_string().contains("TOML") || err.to_string().contains("parse"),
"got {err}"
);
assert!(backend.put_calls().is_empty());
}
fn save_manifest_config(
cache_dir: std::path::PathBuf,
remote: Option<crate::config::RemoteConfig>,
) -> Config {
use crate::config::{
DEFAULT_DAEMON_IDLE_TIMEOUT_SECS, DEFAULT_REMOTE_NEGATIVE_TTL_SECS,
DEFAULT_REMOTE_RESTORE_TIMEOUT_SECS, DEFAULT_S3_POOL_IDLE_SECS,
};
Config {
remote_error: None,
socket_path_override: None,
fallback: None,
key_salt: None,
cc_extra_allowlist_flags: Vec::new(),
local_only: false,
remote_readonly: false,
readonly_store: None,
pull_request_prefix: None,
modified_input_guard: false,
input_predictions: false,
record_sessions: false,
volume_stores: Vec::new(),
windows_hardlink: false,
shared_hardlink_restores: false,
deferred_discovery: true,
out_dir_alias: true,
deferred_durability: false,
daemon_publish: false,
project_rules: crate::config::ProjectRules::default(),
auto_gc: true,
index_auto_compact: true,
auto_clean_orphaned_targets: false,
auto_share_target_files: false,
auto_clean_idle_targets_days: 0,
auto_recover_min_free_bytes: 0,
auto_recover_targets: false,
scheduler_memory_pressure: true,
auto_clean_unused_units_days: 0,
target_liveness: false,
seed_new_targets: false,
build_script_hermetic: false,
gc_evict_shared: false,
storage_layout_advice: true,
heartbeat_secs: 30,
explain_miss: false,
scheduler: true,
test_lease: None,
path_only_env_vars: Vec::new(),
incremental_crates: Vec::new(),
key_env_vars: Vec::new(),
base_dirs: Vec::new(),
runtime_dir: cache_dir.clone(),
cache_dir,
max_size: 1024 * 1024,
remote,
disabled: false,
cache_executables: false,
cache_cc_links: false,
trust_codegen_backends: false,
clean_incremental: true,
preserve_incremental: false,
adaptive_incremental: true,
event_log_max_size: 1024 * 1024,
event_log_keep_lines: 1000,
compression_level: 3,
s3_concurrency: 16,
prefetch_enabled: crate::config::DEFAULT_PREFETCH_ENABLED,
remote_key_listing: false,
remote_key_cache_refresh_secs: crate::config::DEFAULT_REMOTE_KEY_CACHE_REFRESH_SECS,
prefetch_max_keys: crate::config::DEFAULT_PREFETCH_MAX_KEYS,
prefetch_max_bytes: crate::config::DEFAULT_PREFETCH_MAX_BYTES,
prefetch_deadline_secs: crate::config::DEFAULT_PREFETCH_DEADLINE_SECS,
min_store_compile_ms: crate::config::DEFAULT_MIN_STORE_COMPILE_MS,
gc_max_age_hours: crate::config::DEFAULT_GC_MAX_AGE_HOURS,
daemon_idle_timeout_secs: DEFAULT_DAEMON_IDLE_TIMEOUT_SECS,
s3_pool_idle_secs: DEFAULT_S3_POOL_IDLE_SECS,
remote_restore_timeout_secs: DEFAULT_REMOTE_RESTORE_TIMEOUT_SECS,
remote_negative_ttl_secs: DEFAULT_REMOTE_NEGATIVE_TTL_SECS,
}
}
fn put_entry(config: &Config, key: &str, crate_name: &str, dir: &std::path::Path) {
let store = Store::open(config).unwrap();
let src = dir.join(format!("{key}.rlib"));
std::fs::write(&src, format!("artifact bytes for {key}")).unwrap();
store
.put(
key,
crate_name,
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(src, format!("{key}.rlib"))],
"",
"",
)
.unwrap();
}
fn overwrite_entry_meta(
config: &Config,
key: &str,
crate_name: &str,
mut meta: crate::store::EntryMeta,
) {
meta.cache_key = key.to_string();
meta.crate_name = crate_name.to_string();
std::fs::write(
config.store_dir().join(key).join("meta.json"),
serde_json::to_vec(&meta).unwrap(),
)
.unwrap();
}
#[test]
fn why_miss_no_events_prints_tip() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
why_miss(&config, "ghost", false).expect("why_miss with no events should succeed");
}
#[test]
fn why_miss_shows_stored_metadata_for_a_missed_key() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "serdemisskey", "serde", dir.path());
crate::events::log_event(
&config.event_log_path(),
&build_event(
"serde",
crate::events::EventResult::Miss,
1234,
1300,
4096,
"serdemisskey",
),
)
.unwrap();
why_miss(&config, "serde", false).expect("why_miss should succeed for a missed key");
}
#[test]
fn why_miss_leads_with_a_store_failure_when_there_was_one() {
let mut miss = build_event(
"lint_crate",
crate::events::EventResult::Miss,
1000,
900,
4096,
"somekey",
);
assert!(
store_failure_banner(&miss).is_none(),
"a normal miss gets no banner"
);
miss.store_error = "creating blob shard directory: Permission denied (os error 13)".into();
let banner = store_failure_banner(&miss).expect("failed store should produce a banner");
assert!(banner.contains("NOT CACHED"));
assert!(banner.contains("Permission denied (os error 13)"));
assert!(!banner.contains("Diagnosis:"), "banner: {banner}");
}
#[test]
fn why_miss_prioritizes_same_key_lookup_rejection() {
let mut miss = build_event(
"foo.c",
crate::events::EventResult::Miss,
10,
20,
30,
"same-key",
);
assert!(lookup_rejection_banner(&miss, true).is_none());
miss.lookup_rejection = "matching entry lacks dep-info required by this invocation".to_string();
let banner = lookup_rejection_banner(&miss, true).unwrap();
assert!(banner.contains("matching key was found but rejected"));
assert!(banner.contains("lacks dep-info required"));
assert!(banner.contains("currently present under the same key"));
assert!(!banner.contains("key mismatch"), "banner: {banner}");
}
#[test]
fn why_miss_legacy_same_key_repeat_does_not_claim_key_mismatch() {
let mut miss = build_event(
"foo.c",
crate::events::EventResult::Miss,
10,
20,
30,
"same-key",
);
miss.schema = 14;
assert!(legacy_repeated_same_key_banner(&miss, false).is_none());
let banner = legacy_repeated_same_key_banner(&miss, true).unwrap();
assert!(banner.contains("repeated miss for the same cache key"));
assert!(banner.contains("older event did not record"));
assert!(banner.contains("not caused by a cache-key change"));
assert!(
!banner.contains("Diagnosis: key mismatch"),
"banner: {banner}"
);
miss.schema = 15;
assert!(legacy_repeated_same_key_banner(&miss, true).is_none());
}
#[test]
fn why_miss_all_hits_reports_no_misses() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
crate::events::log_event(
&config.event_log_path(),
&build_event(
"tokio",
crate::events::EventResult::LocalHit,
0,
5,
4096,
"tokiohitkey",
),
)
.unwrap();
why_miss(&config, "tokio", false).expect("why_miss with only hits should succeed");
}
#[test]
fn why_miss_reports_many_stored_diffs() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "misskeymanydiffs", "serde", dir.path());
overwrite_entry_meta(
&config,
"misskeymanydiffs",
"serde",
diff_meta("wasm32", "release", &[], &["lib"]),
);
for i in 0..3 {
let key = format!("otherkeymanydiffs{i}");
let feature = format!("feat{i}");
put_entry(&config, &key, "serde", dir.path());
overwrite_entry_meta(
&config,
&key,
"serde",
diff_meta(
&format!("target{i}"),
&format!("profile{i}"),
&[&feature],
&["bin"],
),
);
}
crate::events::log_event(
&config.event_log_path(),
&build_event(
"serde",
crate::events::EventResult::Miss,
123,
130,
4096,
"misskeymanydiffs",
),
)
.unwrap();
why_miss(&config, "serde", false).expect("why_miss should print capped diffs");
}
#[test]
fn why_miss_diff_messages_cap_feature_and_type_diffs() {
let miss = diff_meta("wasm32", "release", &[], &["lib"]);
let others: Vec<_> = (0..3)
.map(|i| {
(
format!("other{i}"),
diff_meta("x86_64", "debug", &[&format!("feat{i}")], &["bin"]),
)
})
.collect();
let (messages, extra) = why_miss_diff_messages(
&miss,
others.iter().map(|(key, meta)| (key.as_str(), meta)),
5,
);
assert_eq!(messages.len(), 5);
assert!(extra > 0, "expected capped messages, got {messages:?}");
assert!(messages.iter().any(|m| m.contains("different target")));
assert!(messages.iter().any(|m| m.contains("different profile")));
assert!(
messages.iter().any(|m| m.contains("[(none)] vs [feat0]")),
"got {messages:?}"
);
assert!(messages.iter().any(|m| m.contains("different crate types")));
}
#[test]
fn why_miss_diff_messages_dedupes_same_config_and_empty_other_features() {
let miss = diff_meta("x86_64", "debug", &["feat"], &["lib"]);
let others = [
(
"empty-features",
diff_meta("x86_64", "debug", &[], &["lib"]),
),
("same-a", diff_meta("x86_64", "debug", &["feat"], &["lib"])),
("same-b", diff_meta("x86_64", "debug", &["feat"], &["lib"])),
];
let (messages, extra) =
why_miss_diff_messages(&miss, others.iter().map(|(key, meta)| (*key, meta)), 5);
assert_eq!(extra, 0);
assert!(
messages.iter().any(|m| m.contains("[feat] vs [(none)]")),
"got {messages:?}"
);
assert_eq!(
messages
.iter()
.filter(|m| m.contains("likely a source"))
.count(),
1
);
}
#[test]
fn explain_crate_last_miss_rows_name_the_key_its_config_and_what_shifts_it() {
use crate::events::EventResult;
let miss = build_event("dep", EventResult::Miss, 10, 20, 30, "abcdef0123456789");
let mut hit = build_event("dep", EventResult::LocalHit, 0, 1, 30, "fedcba9876543210");
hit.ts = miss.ts - chrono::Duration::seconds(5);
let meta = diff_meta("x86_64", "", &["a", "b"], &["lib"]);
let rows = last_miss_rows(&miss, Some(&meta), Some(&hit), Some("salt"));
let labels: Vec<&str> = rows.iter().map(|row| row.0).collect();
assert_eq!(
labels,
[
"Result", "Key", "Target", "Features", "Last hit", "Key salt"
]
);
assert_eq!(rows[0].1, "miss");
assert_eq!(rows[1].1, "abcdef012345");
assert_eq!(rows[3].1, "a, b");
assert_eq!(rows[4].1, "fedcba987654");
assert_eq!(rows[5].1, "\"salt\"");
let mut same = hit.clone();
same.cache_key = miss.cache_key.clone();
let mut later = hit.clone();
later.ts = miss.ts + chrono::Duration::seconds(5);
let mut simultaneous = hit.clone();
simultaneous.ts = miss.ts;
for hit in [&same, &later, &simultaneous] {
let rows = last_miss_rows(&miss, None, Some(hit), Some(""));
let labels: Vec<&str> = rows.iter().map(|row| row.0).collect();
assert_eq!(labels, ["Result", "Key"]);
}
let mut keyless = miss.clone();
keyless.cache_key.clear();
assert_eq!(last_miss_rows(&keyless, None, None, None)[1].1, "none");
}
#[test]
fn explain_crate_diagnosis_lines_lead_with_the_cause() {
use crate::events::EventResult;
let miss = build_event("dep", EventResult::Miss, 10, 20, 30, "abcdef0123456789");
let never = MissDiagnosis::new(&miss, false, 0, false, None, false);
assert_eq!(
miss_diagnosis_lines(&miss, None, &[], &never),
[" never cached: first build of this crate"]
);
assert_eq!(
extern_chain_lines(&never),
[
" not analyzed: no per-dependency digests recorded",
" set explain_miss = true under [cache], then rebuild",
]
);
let recorded = MissDiagnosis::new(&miss, false, 0, false, None, true);
assert!(extern_chain_lines(&recorded).is_empty());
let now_cached = MissDiagnosis::new(&miss, false, 1, true, None, false);
assert_eq!(
miss_diagnosis_lines(&miss, None, &[], &now_cached),
[" first build with these inputs: the entry is now cached"]
);
for (stored, others) in [(2, "1 other stored entry"), (3, "2 other stored entries")] {
let mismatch = MissDiagnosis::new(&miss, false, stored, true, None, false);
assert_eq!(
miss_diagnosis_lines(&miss, None, &[], &mismatch),
[format!(
" key mismatch: key abcdef012345 differs from {others}"
)]
);
}
let mut failed = miss.clone();
failed.store_error = "disk full".into();
let not_cached = MissDiagnosis::new(&failed, false, 0, false, None, false);
let lines = miss_diagnosis_lines(&failed, None, &[], ¬_cached);
assert_eq!(lines.len(), 1, "{lines:?}");
assert!(lines[0].starts_with(" NOT CACHED: "), "{lines:?}");
assert!(lines[0].ends_with("reason: disk full"), "{lines:?}");
}
fn stats_entry(key: &str, profile: &str, crate_type: &str) -> crate::daemon::StatsEntry {
crate::daemon::StatsEntry {
store_dirs: vec!["/nonexistent-store".into()],
cache_key: key.to_string(),
crate_name: "dep".to_string(),
crate_type: crate_type.to_string(),
profile: profile.to_string(),
size: 1024,
hit_count: 3,
created_at: String::new(),
last_accessed: "not a date".to_string(),
content_hash: None,
}
}
#[test]
fn explain_crate_finds_the_missed_entry_only_by_its_own_key() {
let other = stats_entry("other-key", "", "");
let keyless = stats_entry("", "", "");
let missed = stats_entry("missed-key", "", "");
let stored = [&other, &keyless, &missed];
assert_eq!(
missed_entry(&stored, "missed-key").map(|e| e.cache_key.as_str()),
Some("missed-key")
);
assert!(
missed_entry(&stored, "").is_none(),
"a keyless miss matches nothing"
);
assert!(missed_entry(&stored, "absent").is_none());
}
#[test]
fn explain_crate_warns_once_per_unreadable_store() {
let stores = [
crate::store_view::StoreSummary {
path: "/fine".into(),
..Default::default()
},
crate::store_view::StoreSummary {
path: "/broken".into(),
error: Some("index locked".into()),
..Default::default()
},
];
assert_eq!(
unavailable_store_warnings(&stores),
["warning: store /broken is unavailable (index locked)"]
);
}
#[test]
fn explain_crate_entry_rows_name_what_sets_each_entry_apart() {
let entry = stats_entry("abcdef0123456789", "debug", "lib");
let meta = diff_meta("x86_64", "debug", &["a", "b"], &["lib"]);
let row = stored_entry_row(&entry, Some(&meta), Some("abcdef0123456789"));
assert_eq!(
row,
[
"abcdef012345",
"not a date",
"1.0 KiB",
"3",
"/nonexistent-store",
"profile debug, type lib, target x86_64, features [a, b], the missed key, stored \
after the compile",
]
);
let bare = stats_entry("abcdef0123456789", "", "");
let empty = diff_meta("", "", &[], &[]);
assert_eq!(stored_entry_row(&bare, Some(&empty), Some("other"))[5], "");
assert_eq!(stored_entry_row(&bare, None, None)[5], "");
let entries: Vec<_> = (0..11)
.map(|i| stats_entry(&format!("key{i:02}"), "", ""))
.collect();
let refs: Vec<_> = entries.iter().collect();
let ten = stored_entry_lines(None, &refs[..10]);
assert_eq!(ten.len(), 11, "a header and ten rows: {ten:?}");
let eleven = stored_entry_lines(None, &refs);
assert_eq!(eleven.len(), 12, "{eleven:?}");
assert_eq!(eleven[11], " and 1 older entry");
}
#[test]
fn explain_crate_differences_stop_at_five_and_count_the_rest() {
let miss = diff_meta("wasm32", "debug", &[], &["lib"]);
let others = |n: usize| -> Vec<(String, crate::store::EntryMeta)> {
(0..n)
.map(|i| {
(
format!("k{i}"),
diff_meta(&format!("t{i}"), "debug", &[], &["lib"]),
)
})
.collect()
};
let five = difference_lines(&miss, &others(5));
assert_eq!(five.len(), 5, "{five:?}");
assert!(
five[0].starts_with(" - different target vs k0"),
"{five:?}"
);
let seven = difference_lines(&miss, &others(7));
assert_eq!(seven.len(), 6, "{seven:?}");
assert_eq!(seven[5], " and 2 more");
}
#[test]
fn explain_crate_cascade_notes_uncached_compiles_and_unresolved_endpoints() {
use crate::events::EventResult;
use crate::miss_chain::{Chain, ChangedDep, PassthroughGroup, Root, RootKind};
let miss = build_event("app", EventResult::Miss, 10, 20, 30, "abcdef0123456789");
let chain = |kind: RootKind, passthroughs: Vec<PassthroughGroup>| Chain {
roots: vec![Root {
crate_name: "leaf".to_string(),
unit: None,
kind,
passthroughs,
branches: 1,
path: Vec::new(),
}],
direct: vec![ChangedDep {
name: "leaf".to_string(),
from: Some("a".to_string()),
to: Some("b".to_string()),
unit: None,
}],
truncated: None,
baseline_root: None,
};
let resolved = MissDiagnosis::new(
&miss,
false,
0,
false,
Some(chain(
RootKind::Groups(vec!["sources".to_string()]),
vec![PassthroughGroup {
reason: "unsupported flag".to_string(),
count: 2,
}],
)),
true,
);
assert_eq!(
extern_chain_lines(&resolved),
[
" this miss is downstream of a dependency that changed (leaf)",
" root: leaf, its own inputs changed: sources",
" 2x uncached compile in leaf: unsupported flag",
" (uncached compiles make the artifact vary per checkout; attributed by package \
directory)",
]
);
let unresolved = MissDiagnosis::new(
&miss,
false,
0,
false,
Some(chain(RootKind::NoBaseline, Vec::new())),
true,
);
assert_eq!(
extern_chain_lines(&unresolved),
[
" this miss is downstream of a dependency that changed (leaf)",
" unresolved: leaf, nothing earlier to compare it against",
" note: no endpoint could be resolved; the recorded history does not explain this \
miss",
]
);
}
#[test]
fn explain_crate_sections_skip_empty_bodies_and_tabulate_events() {
use crate::events::EventResult;
assert!(section("Hints", Vec::new()).is_empty());
assert_eq!(
section("Hints", vec![" a".to_string()]),
["", "Hints", " a"]
);
assert_eq!(stored_entry_lines(None, &[]), [" none"]);
let miss = build_event("dep", EventResult::Miss, 10, 20, 2048, "abcdef0123456789");
let lines = recent_event_lines(&[&miss]);
assert_eq!(lines.len(), 2, "{lines:?}");
assert!(lines[0].starts_with(" TIME"), "{lines:?}");
assert!(
lines[1].ends_with("miss abcdef012345 20 ms 2.0 KiB"),
"{lines:?}"
);
}
#[test]
fn checkout_comparison_lines_separate_path_only_from_input_changes() {
use crate::miss_chain::{ChangedDep, CheckoutComparison, CheckoutVerdict};
let dep = |name: &str| ChangedDep {
name: name.to_string(),
from: Some("a".to_string()),
to: Some("b".to_string()),
unit: None,
};
let compared = |verdict, groups: &[&str], dependencies: Vec<ChangedDep>| {
checkout_comparison_lines(&CheckoutComparison {
root: "/b".to_string(),
baseline_root: "/a".to_string(),
groups: groups.iter().map(|g| g.to_string()).collect(),
dependencies,
same_key: verdict == CheckoutVerdict::PathOnly,
verdict,
})
};
let lines = compared(CheckoutVerdict::PathOnly, &[], vec![]);
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("the key is not why this missed"));
let lines = compared(CheckoutVerdict::OwnInputs, &["env_deps"], vec![]);
assert_eq!(lines[0], "own inputs differ: env_deps");
assert_eq!(lines.len(), 2, "no dependency or remap line: {lines:?}");
assert!(lines[1].contains("real input difference"));
let lines = compared(
CheckoutVerdict::OwnInputs,
&["args", "remap"],
vec![dep("mid"), dep("util")],
);
assert_eq!(lines[0], "own inputs differ: args, remap");
assert_eq!(lines[1], "dependencies differ: mid, util");
assert!(lines[2].contains("real input difference"), "{lines:?}");
assert!(lines[3].starts_with("(remap:"), "{lines:?}");
let lines = compared(CheckoutVerdict::OwnInputs, &["remap"], vec![]);
assert_eq!(lines.len(), 2, "{lines:?}");
assert!(lines[1].starts_with("(remap:"), "{lines:?}");
let lines = compared(CheckoutVerdict::Dependencies, &[], vec![dep("mid")]);
assert_eq!(
lines,
vec!["own inputs match after path normalization; dependencies differ: mid"]
);
let lines = compared(CheckoutVerdict::Untraced, &[], vec![]);
assert!(lines[0].contains("key salt or extra inputs"));
}
#[test]
fn telemetry_push_refuses_a_malformed_label_before_reading_logs() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let err = telemetry_push(
&config,
&TimelineSelection::Latest,
&["no-equals-sign".to_string()],
true,
)
.unwrap_err();
assert!(
err.to_string().contains("not key=value"),
"the label parse error must reach the caller: {err}"
);
}
#[test]
fn collect_timelines_reports_only_stamped_wrapper_versions() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
std::fs::create_dir_all(&config.runtime_dir).unwrap();
let mut unstamped =
crate::events::BuildEvent::new_for_test("serde", crate::events::EventResult::LocalHit);
unstamped.version = String::new();
let mut stamped =
crate::events::BuildEvent::new_for_test("syn", crate::events::EventResult::LocalHit);
stamped.version = "0.23.0".to_string();
let log = config.event_log_path();
crate::events::log_event(&log, &unstamped).unwrap();
crate::events::log_event(&log, &stamped).unwrap();
let (records, seen, versions) = collect_timelines(
&config,
&TimelineSelection::All,
std::collections::BTreeMap::new(),
)
.unwrap();
assert!(records.is_empty());
assert_eq!(seen, 2);
assert_eq!(
versions.into_iter().collect::<Vec<_>>(),
vec!["0.23.0".to_string()]
);
}
#[test]
fn telemetry_write_emits_cache_scope_not_bench() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let out = dir.path().join("otlp");
telemetry_write(&config, &out, Some("bench-firefox"), Some("warm")).expect("telemetry write");
let body = std::fs::read_to_string(out.join("metrics.otlp.json")).unwrap();
assert!(
body.contains("\"kache.cache\""),
"cache OTLP must use the kache.cache scope"
);
assert!(
body.contains("\"kache.cache.scenario\""),
"bench dumps must name the scenario"
);
assert!(
body.contains("bench-firefox"),
"scenario value must match kache.bench.project"
);
assert!(
body.contains("\"kache.cache.phase\""),
"bench dumps must name the phase"
);
assert!(
body.contains("warm"),
"phase value must match the bench phase"
);
assert!(
!body.contains("kache.bench."),
"cache dump must not mix bench gauges"
);
assert!(
body.contains("AGGREGATION_TEMPORALITY_CUMULATIVE"),
"daemon counters must ride as cumulative sums, not gauges"
);
assert_eq!(
std::fs::read_to_string(out.join("schema_version"))
.unwrap()
.trim(),
"1"
);
}
#[test]
fn purge_skips_corrupt_filtered_entry() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "badpurgekey", "bad", dir.path());
std::fs::write(
config.store_dir().join("badpurgekey").join("meta.json"),
b"{ not valid json",
)
.unwrap();
purge(&config, Some("bad")).expect("purge skips corrupt entries");
assert!(
config
.store_dir()
.join("badpurgekey")
.join("meta.json")
.exists(),
"corrupt entry remains accounted-for after skipped purge"
);
}
#[test]
fn purge_waits_for_the_gc_lock() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "purgelockkey", "locked", dir.path());
let store = crate::store::Store::open(&config).unwrap();
let gc_lock = store.try_gc_lock().unwrap().expect("uncontended lock");
let (done_tx, done_rx) = std::sync::mpsc::channel();
let cfg = config.clone();
let worker = std::thread::spawn(move || {
let result = purge(&cfg, None);
let _ = done_tx.send(());
result
});
assert!(
done_rx
.recv_timeout(std::time::Duration::from_millis(300))
.is_err(),
"purge completed while the GC lock was held"
);
drop(gc_lock);
done_rx
.recv_timeout(std::time::Duration::from_secs(30))
.expect("purge proceeds once the GC lock is released");
worker.join().unwrap().expect("purge succeeds");
let store = crate::store::Store::open(&config).unwrap();
assert_eq!(store.entry_count().unwrap(), 0, "store cleared");
}
#[test]
fn verify_reports_valid_entries_on_a_clean_store() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "validkey1", "serde", dir.path());
verify(&config, true, false).expect("verify of a clean store should succeed");
}
#[test]
fn doctor_groups_each_label_by_its_component() {
for label in ["Daemon socket", "Service file", "Remote cache"] {
assert_eq!(doctor_section(label), "Services", "{label}");
}
for label in [
"Cache directory",
"Store index",
"Shard mount",
"Link layout",
] {
assert_eq!(doctor_section(label), "Storage", "{label}");
}
assert_eq!(doctor_section("RUSTC_WRAPPER"), "Setup");
assert_eq!(doctor_section("Other"), "Setup");
}
#[test]
fn login_service_configuration_covers_existing_and_new_installations() {
assert!(!login_service_configured(false, false));
assert!(login_service_configured(true, false));
assert!(login_service_configured(false, true));
assert!(login_service_configured(true, true));
}
fn stats_row(lines: &[String], label: &str) -> Option<String> {
let line = lines.iter().find(|line| {
line.strip_prefix(" ")
.and_then(|rest| rest.strip_prefix(label))
.is_some_and(|rest| rest.is_empty() || rest.starts_with(" "))
})?;
let mut cells = line
.split(" ")
.map(str::trim)
.filter(|cell| !cell.is_empty())
.skip(1);
let value = cells.next().unwrap_or_default();
let note = cells.collect::<Vec<_>>().join(" ");
Some(format!("{label} | {value} | {note}"))
}
#[test]
#[allow(clippy::field_reassign_with_default)] fn render_stats_rich_snapshot_covers_all_lines() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().join("cache"), Some(test_remote_cfg()));
config.remote.as_mut().unwrap().prefix = "artifacts".to_string();
let mut snap = StatsSnapshot::default();
snap.total_size = 5000;
snap.max_size = 10000;
snap.entry_count = 3;
snap.daemon_connected = true;
snap.daemon_version = "9.9.9".to_string();
snap.daemon_build_epoch = crate::daemon::build_epoch(); snap.event_stats.local_hits = 8;
snap.event_stats.misses = 2;
snap.event_stats.total_elapsed_ms = 1000;
snap.event_stats.miss_elapsed_ms = 300;
snap.event_stats.hit_compile_time_ms = 5000;
snap.event_stats.miss_compile_time_ms = 2000;
snap.blob_stats = Some(crate::store::BlobStats {
total_blobs: 4,
total_blob_size: 2048,
total_logical_size: 4096,
savings: 2048,
});
let lines = render_stats(&snap, &config, SinceWindow::DEFAULT);
let row = |label| stats_row(&lines, label).unwrap_or_else(|| panic!("{label}: {lines:#?}"));
assert_eq!(row("Cache"), "Cache | 2.0 KiB | blob bytes · 3 entries");
assert!(row("Limit").starts_with("Limit | 9.8 KiB | registered blob budget"));
assert_eq!(
row("Dedup"),
"Dedup | 4 blobs | 4.0 KiB logical, 50.0% saved"
);
assert_eq!(
row("Hit rate"),
"Hit rate | 80.0% | 8 of 10 crates from cache"
);
assert_eq!(
row("By time"),
"By time | 71.4% | compile time served from cache"
);
assert_eq!(
row("Miss time"),
"Miss time | 300 ms | 30.0% of wrapper time"
);
assert_eq!(
row("Time saved"),
"Time saved | 5.0 s | compile work avoided"
);
assert!(row("Daemon").starts_with("Daemon | v9.9.9 | "));
assert!(
!row("Daemon").contains("restart pending"),
"matching epoch -> no mismatch note"
);
assert!(row("Remote").starts_with("Remote | s3://"));
}
#[test]
fn render_stats_cache_fallback_labels_logical_bytes() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let snap = StatsSnapshot {
total_size: 8192,
blob_stats: None,
..Default::default()
};
let lines = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&lines, "Cache").unwrap(),
"Cache | 8.0 KiB | logical (blob size unavailable) · 0 entries"
);
}
#[test]
fn render_stats_cache_preserves_logical_size_without_registered_blobs() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let snap = StatsSnapshot {
total_size: 8192,
blob_stats: Some(crate::store::BlobStats {
total_logical_size: 8192,
..Default::default()
}),
..Default::default()
};
let lines = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&lines, "Cache").unwrap(),
"Cache | 0 B | blob bytes · 0 entries"
);
assert_eq!(
stats_row(&lines, "Dedup").unwrap(),
"Dedup | 0 blobs | 8.0 KiB logical, 0.0% saved"
);
}
#[test]
fn render_stats_cache_shards_use_blob_bytes_and_label_missing_metadata() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let snap = StatsSnapshot {
stores: vec![
crate::store_view::StoreSummary {
path: "/cache-one".into(),
bytes: 8192,
max_size: 4096,
entries: 2,
blob_stats: Some(crate::store::BlobStats {
total_blob_size: 1024,
..Default::default()
}),
..Default::default()
},
crate::store_view::StoreSummary {
path: "/cache-two".into(),
bytes: 2048,
max_size: 4096,
entries: 1,
..Default::default()
},
],
..Default::default()
};
let stores = cache_rows(&snap, &config)
.into_iter()
.filter(|row| row.0 == "Store")
.collect::<Vec<_>>();
assert_eq!(
stores,
vec![
(
"Store",
"1.0 KiB".into(),
"blob bytes · of 4.0 KiB · 2 entries · /cache-one".into()
),
(
"Store",
"2.0 KiB".into(),
"logical (blob size unavailable) · of 4.0 KiB · 1 entry · /cache-two".into()
),
]
);
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn render_stats_resilience_requires_both_sources_and_any_single_signal() {
let dir = tempfile::tempdir().unwrap();
let remote_config =
save_manifest_config(dir.path().join("remote-cache"), Some(test_remote_cfg()));
let local_config = save_manifest_config(dir.path().join("local-cache"), None);
let mut quiet = StatsSnapshot::default();
quiet.daemon_connected = true;
assert!(
render_stats(&quiet, &remote_config, SinceWindow::DEFAULT)
.iter()
.all(|line| !line.starts_with(" Resilience "))
);
let signals = [
(
"round trips",
StatsSnapshot {
remote_check_roundtrips: 1,
..Default::default()
},
),
(
"negative hits",
StatsSnapshot {
negative_hits: 1,
..Default::default()
},
),
(
"download suppression",
StatsSnapshot {
downloads_suppressed: 1,
..Default::default()
},
),
(
"upload suppression",
StatsSnapshot {
uploads_suppressed: 1,
..Default::default()
},
),
(
"degraded state",
StatsSnapshot {
remote_degraded: true,
..Default::default()
},
),
];
for (signal, mut snap) in signals {
snap.daemon_connected = true;
assert!(
render_stats(&snap, &remote_config, SinceWindow::DEFAULT)
.iter()
.any(|line| line.starts_with(" Resilience ")),
"{signal} must independently render the resilience section"
);
}
let disconnected = StatsSnapshot {
negative_hits: 1,
..Default::default()
};
assert!(
render_stats(&disconnected, &remote_config, SinceWindow::DEFAULT)
.iter()
.all(|line| !line.starts_with(" Resilience "))
);
let connected_without_remote = StatsSnapshot {
daemon_connected: true,
negative_hits: 1,
..Default::default()
};
assert!(
render_stats(
&connected_without_remote,
&local_config,
SinceWindow::DEFAULT
)
.iter()
.all(|line| !line.starts_with(" Resilience "))
);
}
#[test]
fn render_stats_says_when_the_daemon_does_not_list_the_remote() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), Some(test_remote_cfg()));
let render = |listing: bool| {
let mut eff = effective_config_like(&config);
eff.remote_key_listing = listing;
let snap = StatsSnapshot {
daemon_connected: true,
daemon_effective_config: Some(eff),
..Default::default()
};
stats_row(
&render_stats(&snap, &config, SinceWindow::DEFAULT),
"Listing",
)
};
assert!(render(false).unwrap().starts_with("Listing | off | "));
assert_eq!(render(true), None);
}
#[test]
fn prefetch_session_lines_follow_the_summaries() {
assert!(prefetch_session_lines(&[]).is_empty());
let summary: crate::events::BuildSummaryEvent =
serde_json::from_str(r#"{"ts":"2026-09-30T12:00:00Z","schema":1,"session_id":"sess"}"#)
.unwrap();
let lines = prefetch_session_lines(&[summary]);
let text = lines.join("\n");
assert!(text.contains("Prefetch sessions"), "{text}");
assert!(text.contains("sess"), "{text}");
}
#[test]
fn hit_rate_row_adds_every_outcome() {
let row = |local, prefetch, remote, dups, misses| {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let mut snap = StatsSnapshot::default();
snap.event_stats.local_hits = local;
snap.event_stats.prefetch_hits = prefetch;
snap.event_stats.remote_hits = remote;
snap.event_stats.dups = dups;
snap.event_stats.misses = misses;
let lines = render_stats(&snap, &config, SinceWindow::DEFAULT);
stats_row(&lines, "Hit rate").unwrap_or_default()
};
assert!(
row(1, 4, 0, 0, 0).contains("5 of 5 crates from cache (1 local, 0 remote, 4 prefetched)"),
"{}",
row(1, 4, 0, 0, 0)
);
assert!(
row(1, 0, 4, 0, 0).contains("5 of 5 crates from cache (1 local, 4 remote, 0 prefetched)"),
"{}",
row(1, 0, 4, 0, 0)
);
assert!(
row(3, 1, 1, 2, 0).contains("5 of 7 crates from cache (3 local, 1 remote, 1 prefetched)"),
"{}",
row(3, 1, 1, 2, 0)
);
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn render_stats_prefetch_section_gated_on_activity() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), Some(test_remote_cfg()));
let mut quiet = StatsSnapshot::default();
quiet.daemon_connected = true;
let out = render_stats(&quiet, &config, SinceWindow::DEFAULT);
assert_eq!(stats_row(&out, "Prefetch"), None);
assert_eq!(stats_row(&out, "Planning"), None);
let mut snap = StatsSnapshot::default();
snap.daemon_connected = true;
snap.prefetch = crate::daemon::PrefetchStatsSnapshot {
downloads_completed: 4,
bytes_downloaded: 2048,
keys_used: 2,
keys_cancelled: 3,
keys_over_budget: 0,
cancelled: true,
plans_advisory: 1,
plans_fallback: 2,
last_plan_candidates: 7,
dedup_join_waits: 5,
dedup_join_wait_ms: 1234,
last_list_duration_ms: 88,
last_list_key_count: 250_000,
list_requests_total: 0,
list_failures_total: 0,
list_duration_ms_total: 0,
list_keys_total: 0,
pack_requests_total: 3,
pack_bytes_downloaded: 4096,
v3_requests_total: 1,
v3_bytes_downloaded: 1024,
pack_validation_failures: 1,
pack_fallback_entries: 1,
last_plan_wall_ms: 250,
plan_wall_ms_total: 500,
};
let mut eff = effective_config_like(&config);
eff.remote_key_cache_refresh_secs = 7;
snap.daemon_effective_config = Some(eff);
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
let row = |out: &[String], label| stats_row(out, label).unwrap_or_default();
let prefetch = row(&out, "Prefetch");
assert!(
prefetch.starts_with("Prefetch | 4 downloads | "),
"{prefetch}"
);
assert!(prefetch.contains("2 used (50%)"), "{prefetch}");
assert!(prefetch.ends_with(", CANCELLED"), "{prefetch}");
assert_eq!(
row(&out, "Planning"),
"Planning | 1 advisory | 2 fallback plans, last had 7 candidates"
);
let transport = row(&out, "Transport");
assert!(
transport.starts_with("Transport | 3 pack requests | "),
"{transport}"
);
assert!(transport.contains("v3 1 requests"), "{transport}");
assert_eq!(
row(&out, "Plan time"),
"Plan time | 250 ms | last plan; 500 ms in all"
);
assert_eq!(
row(&out, "Key LIST"),
"Key LIST | 250,000 keys | in 88 ms (refreshes every 7s)"
);
assert_eq!(
row(&out, "Join wait"),
"Join wait | 5 waits | 1234 ms in all, waiting on a download already in flight"
);
assert_eq!(stats_row(&out, "LIST total"), None);
snap.prefetch.list_requests_total = 2;
snap.prefetch.list_failures_total = 1;
snap.prefetch.list_duration_ms_total = 40;
snap.prefetch.list_keys_total = 9;
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(
row(&out, "LIST total"),
"LIST total | 2 requests | 1 failed, 40 ms, 9 keys returned"
);
snap.prefetch.list_requests_total = 0;
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(stats_row(&out, "LIST total"), None);
snap.prefetch.dedup_join_waits = 0;
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(stats_row(&out, "Join wait"), None);
snap.prefetch.dedup_join_waits = 5;
let mut initial_only = config.clone();
initial_only.remote_key_cache_refresh_secs = 0;
let mut eff = effective_config_like(&initial_only);
eff.remote_key_cache_refresh_secs = 0;
snap.daemon_effective_config = Some(eff);
let out = render_stats(&snap, &initial_only, SinceWindow::DEFAULT);
assert_eq!(
row(&out, "Key LIST"),
"Key LIST | 250,000 keys | in 88 ms (one initial population; periodic refresh disabled)"
);
let mut disabled = config.clone();
disabled.prefetch_enabled = false;
let out = render_stats(&quiet, &disabled, SinceWindow::DEFAULT);
assert!(
row(&out, "Prefetch")
.starts_with("Prefetch | disabled | exact remote lookup and uploads stay on")
);
assert_eq!(stats_row(&out, "Planning"), None);
assert_eq!(stats_row(&out, "Key LIST"), None);
snap.prefetch.last_list_key_count = 0;
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert!(row(&out, "Prefetch").starts_with("Prefetch | 4 downloads | "));
assert_eq!(
stats_row(&out, "Key LIST"),
None,
"zero listed keys must not render a LIST status row"
);
snap.prefetch.last_list_key_count = 250_000;
snap.prefetch.pack_requests_total = 0;
snap.prefetch.v3_requests_total = 0;
snap.prefetch.last_plan_wall_ms = 0;
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(stats_row(&out, "Transport"), None);
assert_eq!(stats_row(&out, "Plan time"), None);
snap.prefetch.pack_requests_total = 1;
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert!(row(&out, "Transport").starts_with("Transport | 1 pack requests | "));
snap.prefetch.pack_requests_total = 0;
snap.prefetch.v3_requests_total = 1;
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert!(row(&out, "Transport").contains("v3 1 requests"));
}
fn effective_config_like(config: &Config) -> crate::daemon::EffectiveConfig {
crate::daemon::EffectiveConfig {
max_size: config.max_size,
cache_dir: config.cache_dir.display().to_string(),
runtime_dir: config.runtime_dir.display().to_string(),
config_path: "/daemon-home/.config/kache/config.toml".to_string(),
config_fingerprint: Some("daemon-fingerprint".to_string()),
prefetch_enabled: config.prefetch_enabled,
remote_key_listing: config.remote_key_listing,
remote_description: config.remote.as_ref().map(|remote| remote.describe()),
local_only: config.local_only,
remote_readonly: Some(config.remote_readonly),
remote_error: config.remote_error.clone(),
remote_key_cache_refresh_secs: config.remote_key_cache_refresh_secs,
socket_path: config.socket_path().display().to_string(),
started_at_ms: 1_700_000_000_000,
auto_recover_min_free_bytes: None,
auto_recover_targets: None,
}
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn render_stats_prefetch_policy_prefers_daemon_effective() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().join("cache"), Some(test_remote_cfg()));
config.prefetch_enabled = false;
let mut snap = StatsSnapshot::default();
snap.daemon_connected = true;
let mut eff = effective_config_like(&config);
eff.prefetch_enabled = true; snap.daemon_effective_config = Some(eff);
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert!(
!stats_row(&out, "Prefetch").is_some_and(|row| row.starts_with("Prefetch | disabled")),
"must not claim disabled while the daemon reports enabled: {out:#?}"
);
assert!(
stats_row(&out, "Daemon")
.unwrap()
.contains(" · /daemon-home/.config/kache/config.toml"),
"{out:#?}"
);
config.prefetch_enabled = true;
let mut eff = effective_config_like(&config);
eff.prefetch_enabled = false;
snap.daemon_effective_config = Some(eff);
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&out, "Prefetch").unwrap(),
"Prefetch | disabled | exact remote lookup and uploads stay on"
);
assert!(!out.join("\n").contains("client config"));
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn render_stats_prefetch_policy_labels_client_fallback_for_old_daemon() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().join("cache"), Some(test_remote_cfg()));
config.prefetch_enabled = false;
let mut snap = StatsSnapshot::default();
snap.daemon_connected = true; snap.daemon_build_epoch = crate::daemon::build_epoch();
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&out, "Prefetch").unwrap(),
"Prefetch | disabled | exact remote lookup and uploads stay on \
[client config — daemon did not report its policy]"
);
assert_eq!(
stats_row(&out, "Daemon").unwrap(),
"Daemon | release unknown | running",
"no daemon config path to show without a report"
);
}
#[test]
fn config_mismatch_warnings_name_both_sides_per_field() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let same = crate::config::ConfigFileProvenance {
path: "/daemon-home/.config/kache/config.toml".into(),
fingerprint: "daemon-fingerprint".to_string(),
};
let other_path = crate::config::ConfigFileProvenance {
path: "/cli-home/kache-repro.toml".into(),
fingerprint: "client-fingerprint".to_string(),
};
let eff = effective_config_like(&config);
assert!(config_mismatch_warnings(&config, &same, &eff).is_empty());
let mut cadence_only = eff.clone();
cadence_only.remote_key_cache_refresh_secs += 1;
assert!(config_mismatch_warnings(&config, &same, &cadence_only).is_empty());
let warnings = config_mismatch_warnings(&config, &other_path, &eff);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("daemon loaded config"), "{warnings:?}");
assert!(warnings[0].contains("values may diverge"), "{warnings:?}");
let changed = crate::config::ConfigFileProvenance {
path: same.path.clone(),
fingerprint: "changed-fingerprint".to_string(),
};
let warnings = config_mismatch_warnings(&config, &changed, &eff);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("different snapshots"), "{warnings:?}");
let mut old_eff = eff.clone();
old_eff.config_fingerprint = None;
assert!(config_mismatch_warnings(&config, &same, &old_eff).is_empty());
let mut runtime_eff = effective_config_like(&config);
runtime_eff.runtime_dir = "/somewhere/runtime".to_string();
let warnings = config_mismatch_warnings(&config, &same, &runtime_eff);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("runtime_dir=/somewhere/runtime"));
runtime_eff.runtime_dir.clear();
assert!(config_mismatch_warnings(&config, &same, &runtime_eff).is_empty());
let mut eff = effective_config_like(&config);
eff.max_size = config.max_size * 2;
let warnings = config_mismatch_warnings(&config, &same, &eff);
assert_eq!(warnings.len(), 1);
assert!(
warnings[0].contains("local_max_size=2.0 MiB"),
"{warnings:?}"
);
assert!(warnings[0].contains("says 1.0 MiB"), "{warnings:?}");
assert!(
warnings[0].contains(
"daemon (started 2023-11-14 22:13 UTC, config /daemon-home/.config/kache/config.toml)"
),
"{warnings:?}"
);
assert!(
warnings[0].contains("this process's config (/daemon-home/.config/kache/config.toml)"),
"{warnings:?}"
);
assert!(
warnings[0].contains("the daemon's value is in effect"),
"{warnings:?}"
);
let mut eff = effective_config_like(&config);
eff.max_size += 1;
eff.cache_dir = "/somewhere/else".to_string();
eff.prefetch_enabled = !config.prefetch_enabled;
eff.remote_key_listing = !config.remote_key_listing;
eff.remote_description = Some("s3://daemon-bucket/artifacts".to_string());
eff.remote_key_cache_refresh_secs += 1;
eff.started_at_ms = 0; let warnings = config_mismatch_warnings(&config, &same, &eff);
assert_eq!(warnings.len(), 6);
assert!(
warnings[1].contains("local_store=/somewhere/else"),
"{warnings:?}"
);
assert!(
warnings[1].contains("the daemon's numbers describe ITS store"),
"{warnings:?}"
);
assert!(
warnings[2].contains("prefetch_enabled=false"),
"{warnings:?}"
);
assert!(
warnings[3].contains("remote_key_listing=true"),
"{warnings:?}"
);
assert!(warnings[4].contains("remote=s3://daemon-bucket/artifacts"));
assert!(warnings[5].contains("remote_key_cache_refresh_secs=61"));
assert!(warnings[0].contains("started unknown time"), "{warnings:?}");
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn render_stats_remote_state_prefers_daemon_effective() {
let dir = tempfile::tempdir().unwrap();
let client_remote =
save_manifest_config(dir.path().join("client-remote"), Some(test_remote_cfg()));
let mut snap = StatsSnapshot::default();
snap.daemon_connected = true;
let mut eff = effective_config_like(&client_remote);
eff.remote_description = None;
eff.remote_key_cache_refresh_secs = 7;
snap.daemon_effective_config = Some(eff);
let out = render_stats(&snap, &client_remote, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&out, "Remote").unwrap(),
"Remote | not configured | "
);
let client_local = save_manifest_config(dir.path().join("client-local"), None);
let mut eff = effective_config_like(&client_local);
eff.remote_description = Some("s3://daemon-bucket/artifacts".to_string());
snap.daemon_effective_config = Some(eff);
let out = render_stats(&snap, &client_local, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&out, "Remote").unwrap(),
"Remote | s3://daemon-bucket/artifacts | "
);
assert!(!out.join("\n").contains("client config"), "{out:#?}");
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn stats_show_a_read_only_daemon_to_a_writable_process() {
let dir = tempfile::tempdir().unwrap();
let writable = save_manifest_config(dir.path().join("cache"), Some(test_remote_cfg()));
let provenance = crate::config::ConfigFileProvenance {
path: "/daemon-home/.config/kache/config.toml".into(),
fingerprint: "daemon-fingerprint".to_string(),
};
let mut snap = StatsSnapshot::default();
snap.daemon_connected = true;
let mut eff = effective_config_like(&writable);
eff.remote_readonly = Some(true);
snap.daemon_effective_config = Some(eff.clone());
let out = render_stats(&snap, &writable, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&out, "Remote").unwrap(),
"Remote | s3://bucket/prefix | "
);
assert_eq!(
stats_row(&out, "Uploads").unwrap(),
"Uploads | off | the daemon is read-only and skips remote writes"
);
let warnings = config_mismatch_warnings(&writable, &provenance, &eff);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(
warnings[0].contains("skips this process's uploads"),
"{warnings:?}"
);
let mut read_only = writable.clone();
read_only.remote_readonly = true;
let out = render_stats(&snap, &read_only, SinceWindow::DEFAULT);
assert_eq!(
action_remote_value(&out).as_deref(),
Some("s3://bucket/prefix")
);
assert!(stats_row(&out, "Uploads").is_some());
eff.remote_readonly = Some(false);
assert!(config_mismatch_warnings(&read_only, &provenance, &eff).is_empty());
eff.remote_readonly = None;
snap.daemon_effective_config = Some(eff.clone());
assert!(config_mismatch_warnings(&writable, &provenance, &eff).is_empty());
let out = render_stats(&snap, &writable, SinceWindow::DEFAULT);
assert_eq!(
stats_row(&out, "Remote").unwrap(),
"Remote | s3://bucket/prefix | "
);
assert!(stats_row(&out, "Uploads").is_none());
}
#[test]
fn doctor_flags_a_read_only_daemon_for_a_writable_process() {
let check = remote_writes_check(None, false, || Some(true));
assert!(!check.pass, "{}", check.detail);
assert!(
check.detail.contains("daemon is read-only"),
"{}",
check.detail
);
assert!(
check
.fix
.as_deref()
.is_some_and(|fix| fix.contains("kache daemon restart"))
);
for daemon in [Some(false), None] {
let check = remote_writes_check(None, false, || daemon);
assert!(check.pass, "{daemon:?}");
assert_eq!(check.detail, "read-write");
assert!(check.fix.is_none());
}
for forced in [None, Some("pull request".to_string())] {
let check = remote_writes_check(forced.clone(), true, || {
panic!("a process that cannot upload must not ask the daemon")
});
assert!(check.pass);
assert!(check.detail.starts_with("read-only"), "{}", check.detail);
if let Some(reason) = forced {
assert!(check.detail.contains(&reason), "{}", check.detail);
}
}
}
fn action_remote_value(lines: &[String]) -> Option<String> {
lines.iter().find_map(|line| {
let rest = line.trim_start().strip_prefix("Remote")?;
let value = match rest.strip_prefix(':') {
Some(value) => value,
None if rest.starts_with(" ") => rest,
None => return None,
};
Some(value.trim().to_string())
})
}
#[test]
#[allow(clippy::field_reassign_with_default)] fn render_stats_daemon_mismatch_and_local_only() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().join("cache"), None);
config.local_only = true;
let mut snap = StatsSnapshot::default();
snap.daemon_connected = true;
snap.daemon_version = "0.0.1".to_string();
snap.daemon_build_epoch = crate::daemon::build_epoch().wrapping_add(1);
let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert!(
stats_row(&out, "Daemon")
.unwrap()
.ends_with(" · older release; upgrade available")
);
assert!(
stats_row(&out, "Remote")
.unwrap()
.starts_with("Remote | local-only mode")
);
}
#[test]
fn render_stats_offline_and_not_configured() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let snap = StatsSnapshot::default(); let out = render_stats(&snap, &config, SinceWindow::DEFAULT);
assert_eq!(stats_row(&out, "Daemon").unwrap(), "Daemon | offline | ");
assert_eq!(
stats_row(&out, "Remote").unwrap(),
"Remote | not configured | "
);
assert_eq!(stats_row(&out, "Dedup"), None);
}
#[test]
fn cache_limit_note_names_the_disk_share_budget() {
const GIB: u64 = 1 << 30;
let share = format!(
" (5% of {}, floor 5.0 GiB, cap 100.0 GiB)",
ByteSize(200 * GIB)
);
assert_eq!(cache_limit_note(0, 10 * GIB, Some(200 * GIB)), share);
assert_eq!(cache_limit_note(1, 10 * GIB, Some(200 * GIB)), share);
assert_eq!(cache_limit_note(2, 10 * GIB, Some(200 * GIB)), "");
assert_eq!(cache_limit_note(0, 7 * GIB, Some(200 * GIB)), "");
let floor = cache_limit_note(0, 5 * GIB, Some(GIB));
assert!(floor.contains("floor"), "{floor}");
assert!(floor.contains("floor 5.0 GiB"), "{floor}");
assert!(floor.contains(&ByteSize(GIB).to_string()), "{floor}");
let cap = cache_limit_note(0, 100 * GIB, Some(4000 * GIB));
assert!(cap.contains("cap"), "{cap}");
assert!(cap.contains("cap 100.0 GiB"), "{cap}");
assert_eq!(
cache_limit_note(0, 50 * GIB, None),
" (default; disk size unknown)"
);
assert_eq!(
cache_limit_note(1, 50 * GIB, Some(0)),
" (default; disk size unknown)"
);
assert_eq!(cache_limit_note(0, 0, Some(0)), "");
assert_eq!(cache_limit_note(0, 49 * GIB, None), "");
}
#[test]
fn render_stats_handles_zero_limits_and_zero_logical_dedup() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let snap = StatsSnapshot {
total_size: 500,
max_size: 0,
blob_stats: Some(crate::store::BlobStats {
total_blobs: 2,
total_blob_size: 500,
total_logical_size: 0,
savings: 0,
}),
..Default::default()
};
let window = SinceWindow::parse("15m").unwrap();
let out = render_stats(&snap, &config, window);
assert_eq!(
stats_row(&out, "Cache").unwrap(),
"Cache | 500 B | blob bytes · 0 entries"
);
assert_eq!(
stats_row(&out, "Dedup").unwrap(),
"Dedup | 2 blobs | 0 B logical, 0.0% saved"
);
assert_eq!(out[0], "kache · last 15m");
assert_eq!(
stats_row(&out, "Time saved").unwrap(),
"Time saved | none yet | "
);
let no_blobs = StatsSnapshot {
blob_stats: Some(crate::store::BlobStats {
total_blobs: 0,
total_blob_size: 0,
total_logical_size: 0,
savings: 0,
}),
..Default::default()
};
let out = render_stats(&no_blobs, &config, window);
assert_eq!(
stats_row(&out, "Dedup"),
None,
"an empty blob snapshot must not render a dedup row"
);
}
#[test]
fn snapshot_from_direct_reads_returns_only_the_newest_five_summaries() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let summary_path = config.summary_log_path();
std::fs::create_dir_all(summary_path.parent().unwrap()).unwrap();
let mut summaries = (0..7)
.map(|index| {
format!(
"{{\"ts\":\"2026-08-09T00:00:0{index}Z\",\"schema\":1,\"session_id\":\"s{index}\"}}"
)
})
.collect::<Vec<_>>()
.join("\n");
summaries.push('\n');
std::fs::write(summary_path, summaries).unwrap();
let snap = snapshot_from_direct_reads(&config, false, "size", SinceWindow::DEFAULT, true);
let ids = snap
.recent_summaries
.iter()
.map(|summary| summary.session_id.as_str())
.collect::<Vec<_>>();
assert_eq!(ids, ["s2", "s3", "s4", "s5", "s6"]);
}
#[test]
fn snapshot_from_direct_reads_reflects_store_and_events() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "serdekey", "serde", dir.path());
crate::events::log_event(
&config.event_log_path(),
&build_event(
"serde",
crate::events::EventResult::LocalHit,
0,
5,
4096,
"serdekey",
),
)
.unwrap();
crate::events::log_event(
&config.event_log_path(),
&build_event(
"tokio",
crate::events::EventResult::Miss,
900,
950,
8192,
"tk",
),
)
.unwrap();
let snap = snapshot_from_direct_reads(&config, true, "name", SinceWindow::DEFAULT, false);
assert!(!snap.daemon_connected, "direct reads report no daemon");
assert_eq!(snap.entry_count, 1);
assert_eq!(snap.entries.len(), 1);
assert_eq!(snap.entries[0].crate_name, "serde");
assert_eq!(snap.event_stats.local_hits, 1);
assert_eq!(snap.event_stats.misses, 1);
assert_eq!(snap.max_size, config.max_size);
}
#[test]
fn snapshot_from_direct_reads_honors_a_sub_hour_window() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let now = chrono::Utc::now();
let mut recent = build_event(
"serde",
crate::events::EventResult::LocalHit,
0,
5,
4096,
"a",
);
recent.ts = now - chrono::Duration::minutes(10);
let mut old = build_event(
"tokio",
crate::events::EventResult::Miss,
900,
950,
8192,
"b",
);
old.ts = now - chrono::Duration::hours(3);
crate::events::log_event(&config.event_log_path(), &recent).unwrap();
crate::events::log_event(&config.event_log_path(), &old).unwrap();
let narrow = SinceWindow::parse("15m").unwrap();
let snap = snapshot_from_direct_reads(&config, false, "size", narrow, false);
assert_eq!(snap.event_stats.local_hits, 1);
assert_eq!(snap.event_stats.misses, 0, "a 3h-old miss is outside 15m");
let wide = SinceWindow::parse("24h").unwrap();
let snap = snapshot_from_direct_reads(&config, false, "size", wide, false);
assert_eq!(snap.event_stats.local_hits, 1);
assert_eq!(snap.event_stats.misses, 1);
}
#[test]
fn snapshot_from_direct_reads_without_entries_skips_listing() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "k1", "serde", dir.path());
let snap = snapshot_from_direct_reads(&config, false, "size", SinceWindow::DEFAULT, false);
assert!(
snap.entries.is_empty(),
"entries omitted when not requested"
);
assert_eq!(snap.entry_count, 1, "count still reflects the store");
}
#[test]
fn sync_without_remote_errors() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let err = sync(&config, None, false, false, false, false, false, false)
.expect_err("sync without a remote must error");
assert!(
err.to_string().contains("no remote configured"),
"got: {err}"
);
}
#[test]
fn repair_tip_and_reconciliation_message_cover_every_boundary() {
assert!(!should_print_repair_tip(0, 0, 0, false));
assert!(should_print_repair_tip(1, 0, 0, false));
assert!(should_print_repair_tip(0, 1, 0, false));
assert!(should_print_repair_tip(0, 0, 1, false));
assert!(!should_print_repair_tip(1, 1, 1, true));
assert_eq!(
reconciled_index_message(crate::store::BlobIndexDrift::default()),
None
);
assert_eq!(
reconciled_index_message(crate::store::BlobIndexDrift {
entry_mappings: 1,
blobs: 2,
})
.as_deref(),
Some("Repairing: reconciled 1 entry mappings and 2 blob rows.")
);
}
#[test]
fn verify_detects_missing_blob_and_missing_meta_then_repairs() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let store = Store::open(&config).unwrap();
put_entry(&config, "missingblobkey", "aaa", dir.path());
let meta_a = store.get("missingblobkey").unwrap().unwrap();
let blob_a = store.blob_path(&meta_a.files[0].hash);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(
blob_a.parent().unwrap(),
std::fs::Permissions::from_mode(0o755),
);
}
let _ = std::fs::remove_file(&blob_a);
put_entry(&config, "missingmetakey", "bbb", dir.path());
std::fs::remove_file(config.store_dir().join("missingmetakey").join("meta.json")).unwrap();
put_entry(&config, "badmetakey", "ccc", dir.path());
std::fs::write(
config.store_dir().join("badmetakey").join("meta.json"),
b"{ not valid json",
)
.unwrap();
put_entry(&config, "validkey", "ddd", dir.path());
verify(&config, false, true).expect("verify --repair should succeed");
let store2 = Store::open(&config).unwrap();
assert!(store2.get("validkey").unwrap().is_some());
assert!(store2.get("missingblobkey").unwrap().is_none());
verify(&config, false, false).expect("a second verify pass should succeed");
}
#[test]
fn verify_reports_and_repairs_blob_index_drift() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "driftkey", "drift", dir.path());
let store = Store::open(&config).unwrap();
let hash = store.get("driftkey").unwrap().unwrap().files[0]
.hash
.clone();
drop(store);
let db = crate::store::open_index_db(&config.index_db_path()).unwrap();
db.execute(
"UPDATE blobs SET refcount = 77 WHERE hash = ?1",
rusqlite::params![hash],
)
.unwrap();
drop(db);
let drifted = verify(&config, false, false).unwrap();
assert_eq!(drifted.index_drift, 1, "{drifted:?}");
assert_eq!(drifted.unresolved_integrity_findings(), 1, "{drifted:?}");
let repaired = verify(&config, false, true).unwrap();
assert_eq!(repaired.index_drift, 0, "{repaired:?}");
assert_eq!(repaired.unresolved_integrity_findings(), 0, "{repaired:?}");
let clean = verify(&config, false, false).unwrap();
assert_eq!(clean.index_drift, 0, "{clean:?}");
assert_eq!(clean.unresolved_integrity_findings(), 0, "{clean:?}");
}
#[test]
fn verify_accepts_build_script_out_dir_artifact_names() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let store = Store::open(&config).unwrap();
let output = dir.path().join("asm.s");
std::fs::write(&output, b"asm bytes").unwrap();
store
.put(
"buildscriptkey",
"build_script_run",
&["build-script".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(output, "out/asm.s".to_string())],
"",
"",
)
.unwrap();
drop(store);
let outcome = verify(&config, false, false).expect("verify must succeed");
assert_eq!(outcome.corrupted_entries, 0, "{outcome:?}");
assert_eq!(outcome.index_drift, 0, "{outcome:?}");
assert_eq!(outcome.unresolved_integrity_findings(), 0, "{outcome:?}");
}
#[test]
fn verify_detects_checksum_mismatch_with_checksums_enabled() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let store = Store::open(&config).unwrap();
put_entry(&config, "corruptblobkey", "eee", dir.path());
let meta = store.get("corruptblobkey").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&blob, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[cfg(not(unix))]
{
let mut p = std::fs::metadata(&blob).unwrap().permissions();
p.set_readonly(false);
std::fs::set_permissions(&blob, p).unwrap();
}
std::fs::write(&blob, vec![b'X'; meta.files[0].size as usize]).unwrap();
let outcome = verify(&config, true, false).expect("verify with checksums should succeed");
assert_eq!(outcome.checksum_failures, 1, "{outcome:?}");
assert_eq!(outcome.corrupted_entries, 1, "{outcome:?}");
assert_eq!(
outcome.unresolved_integrity_findings(),
1,
"without --repair the finding stands: {outcome:?}"
);
let repaired = verify(&config, true, true).expect("verify --repair should succeed");
assert_eq!(repaired.corrupted_removed, repaired.corrupted_entries);
assert_eq!(repaired.unresolved_integrity_findings(), 0, "{repaired:?}");
let clean = verify(&config, true, false).expect("verify after repair should succeed");
assert_eq!(clean.corrupted_entries, 0, "{clean:?}");
assert_eq!(clean.unresolved_integrity_findings(), 0, "{clean:?}");
}
#[test]
fn verify_scrubs_each_unique_blob_once_and_marks_every_referencing_entry() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let store = Store::open(&config).unwrap();
for key in ["sharedkey_a", "sharedkey_b"] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, b"shared artifact bytes").unwrap();
store
.put(
key,
"shared",
&["lib".to_string()],
&[],
"",
"dev",
&[(src, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
}
let meta = store.get("sharedkey_a").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
assert_eq!(
store.get("sharedkey_b").unwrap().unwrap().files[0].hash,
meta.files[0].hash,
"the two entries must share one blob for this test to mean anything"
);
let mut perms = std::fs::metadata(&blob).unwrap().permissions();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
perms.set_mode(0o644);
}
#[cfg(not(unix))]
perms.set_readonly(false);
std::fs::set_permissions(&blob, perms).unwrap();
std::fs::write(&blob, vec![b'X'; meta.files[0].size as usize]).unwrap();
let unchecked = verify(&config, false, false).expect("verify should succeed");
assert_eq!(
unchecked.checksum_failures, 0,
"checksums=false must not hash anything: {unchecked:?}"
);
assert_eq!(
unchecked.unresolved_integrity_findings(),
0,
"same-size corruption is undetectable without --checksums: {unchecked:?}"
);
let outcome = verify(&config, true, false).expect("verify should succeed");
assert_eq!(
outcome.checksum_failures, 1,
"one shared blob is one failure, not one per referencing entry: {outcome:?}"
);
assert_eq!(
outcome.corrupted_entries, 2,
"but both entries referencing it are corrupt: {outcome:?}"
);
assert_eq!(outcome.unresolved_integrity_findings(), 2, "{outcome:?}");
}
#[test]
fn verify_reports_no_findings_for_a_clean_store() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
put_entry(&config, "cleanstorekey", "ggg", dir.path());
let outcome = verify(&config, true, false).expect("verify should succeed");
assert_eq!(outcome.corrupted_entries, 0, "{outcome:?}");
assert_eq!(outcome.checksum_failures, 0, "{outcome:?}");
assert_eq!(outcome.missing_blobs, 0, "{outcome:?}");
assert_eq!(outcome.unresolved_integrity_findings(), 0, "{outcome:?}");
assert!(outcome.valid_entries >= 1, "{outcome:?}");
}
#[test]
fn verify_detects_blob_size_mismatch() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().join("cache"), None);
let store = Store::open(&config).unwrap();
put_entry(&config, "sizemismatchkey", "fff", dir.path());
let meta = store.get("sizemismatchkey").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&blob, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[cfg(not(unix))]
{
let mut p = std::fs::metadata(&blob).unwrap().permissions();
p.set_readonly(false);
std::fs::set_permissions(&blob, p).unwrap();
}
std::fs::write(&blob, vec![b'Y'; meta.files[0].size as usize + 1]).unwrap();
verify(&config, false, false).expect("verify with size mismatch should succeed");
}
#[test]
fn save_manifest_without_remote_errors() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
let err = save_manifest(&config, None, None).expect_err("no remote -> error");
assert!(
err.to_string().contains("no remote configured"),
"got {err}"
);
}
#[test]
fn automatic_manifest_save_without_remote_errors() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), None);
let err =
save_manifest_auto_for_session(&config, "key", "session").expect_err("no remote -> error");
assert!(
err.to_string().contains("no remote configured"),
"got {err}"
);
}
#[test]
fn only_the_primary_manifest_key_publishes_shards() {
assert_eq!(shard_namespace_for_publish_key(0, Some("ns")), Some("ns"));
assert_eq!(shard_namespace_for_publish_key(1, Some("ns")), None);
assert_eq!(shard_namespace_for_publish_key(0, None), None);
}
#[test]
fn save_manifest_with_no_events_returns_ok_before_touching_remote() {
let dir = tempfile::tempdir().unwrap();
let remote = crate::config::RemoteConfig::test_s3("b", "p");
let config = save_manifest_config(dir.path().to_path_buf(), Some(remote));
save_manifest(&config, Some("mykey"), None).expect("empty events -> Ok");
}
fn build_event(
crate_name: &str,
result: crate::events::EventResult,
compile_time_ms: u64,
elapsed_ms: u64,
size: u64,
cache_key: &str,
) -> crate::events::BuildEvent {
crate::events::BuildEvent {
ts: chrono::Utc::now(),
session_id: String::new(),
demands: Vec::new(),
crate_name: crate_name.to_string(),
version: "0.1.0".to_string(),
result,
elapsed_ms,
compile_time_ms,
size,
cache_key: cache_key.to_string(),
schema: 8,
key_ms: 0,
key_hash_hits: 0,
key_hash_misses: 0,
key_hash_bytes: 0,
lookup_ms: 0,
restore_ms: 0,
store_ms: 0,
startup_ms: 0,
dep_info_ms: 0,
dep_info_runs: 0,
prediction_mismatches: 0,
flight_wait_ms: 0,
permit_wait_ms: 0,
store_output_blobs: 0,
store_duplicate_blobs: 0,
store_new_blobs: 0,
compiler_runs: 0,
preprocessor_runs: 0,
probe_runs: 0,
reflinked_bytes: 0,
hardlinked_bytes: 0,
copied_bytes: 0,
store_reflinked_bytes: 0,
store_hardlinked_bytes: 0,
store_copied_bytes: 0,
store_copy_cross_device_bytes: 0,
store_copy_permission_bytes: 0,
store_copy_ineligible_bytes: 0,
store_copy_other_bytes: 0,
restore_copy_cross_device_bytes: 0,
restore_copy_permission_bytes: 0,
restore_copy_exclusive_bytes: 0,
restore_copy_other_bytes: 0,
root: String::new(),
passthrough_reason: String::new(),
skip_reason: String::new(),
store_error: String::new(),
store_handed_off: false,
daemon_store_ms: 0,
lookup_rejection: String::new(),
verify_compare: String::new(),
fallback: false,
fallback_attempt: None,
exit_code: None,
key_fields: Default::default(),
key_diff: Vec::new(),
key_externs: Default::default(),
key_env_deps: None,
key_externs_recorded: false,
unit_id: String::new(),
extern_units: Default::default(),
miss_reason: crate::events::MissReason::None,
object_output: String::new(),
package: String::new(),
rebuilt_package: None,
rebuilt_fingerprint: None,
rebuilt_path: None,
rebuilt_paths: Vec::new(),
}
}
fn diff_meta(
target: &str,
profile: &str,
features: &[&str],
crate_types: &[&str],
) -> crate::store::EntryMeta {
crate::store::EntryMeta {
cache_key: "k".to_string(),
key_schema: crate::cache_key::CACHE_KEY_VERSION,
crate_name: "c".to_string(),
crate_types: crate_types.iter().map(|v| (*v).to_string()).collect(),
files: Vec::new(),
stdout: String::new(),
stderr: String::new(),
features: features.iter().map(|v| (*v).to_string()).collect(),
target: target.to_string(),
profile: profile.to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
}
}
#[test]
fn manifest_commit_prefers_github_then_gitlab_and_skips_daemon_sessions() {
let env = |vars: &'static [(&'static str, &'static str)]| {
move |name: &str| {
vars.iter()
.find(|(key, _)| *key == name)
.map(|(_, value)| value.to_string())
}
};
let both = env(&[("GITHUB_SHA", " gh "), ("CI_COMMIT_SHA", "gl")]);
assert_eq!(manifest_commit(None, both).as_deref(), Some("gh"));
assert_eq!(manifest_commit(Some("session"), both), None);
let blank_github = env(&[("GITHUB_SHA", " "), ("CI_COMMIT_SHA", "gl")]);
assert_eq!(manifest_commit(None, blank_github).as_deref(), Some("gl"));
assert_eq!(manifest_commit(None, env(&[])), None);
}
#[test]
fn manifest_entries_from_events_dedups_and_filters() {
use crate::events::EventResult;
let events = vec![
build_event("serde", EventResult::Miss, 100, 0, 10, "k-serde"),
build_event("serde", EventResult::LocalHit, 900, 0, 10, "k-serde"),
build_event("tokio", EventResult::Dup, 50, 0, 20, "k-tokio"),
build_event("nokey", EventResult::Miss, 5, 0, 0, ""),
build_event("passth", EventResult::Passthrough, 5, 0, 0, "k-p"),
build_event("skip", EventResult::Skipped, 5, 0, 0, "k-s"),
build_event(
crate::build_script::CRATE_NAME,
EventResult::Miss,
500,
0,
30,
"k-run",
),
];
let mut entries = manifest_entries_from_events(&events, None);
entries.sort_by(|a, b| a.crate_name.cmp(&b.crate_name));
assert_eq!(entries.len(), 2, "only the two cacheable keys survive");
let serde = entries.iter().find(|e| e.crate_name == "serde").unwrap();
assert_eq!(serde.compile_time_ms, 900, "larger compile time wins");
assert!(entries.iter().any(|e| e.crate_name == "tokio"));
}
#[test]
fn manifest_entry_ties_keep_the_first_observation() {
use crate::events::EventResult;
let events = vec![
build_event("first", EventResult::Miss, 100, 0, 10, "same-key"),
build_event("second", EventResult::Miss, 100, 0, 20, "same-key"),
];
let entries = manifest_entries_from_events(&events, None);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].crate_name, "first");
assert_eq!(entries[0].artifact_size, 10);
}
#[test]
fn automatic_manifest_entries_are_scoped_to_the_build_session() {
use crate::events::EventResult;
let mut current = build_event("current", EventResult::Miss, 100, 0, 10, "current-key");
current.session_id = "current-session".into();
let mut other = build_event("other", EventResult::Miss, 200, 0, 20, "other-key");
other.session_id = "other-session".into();
let entries = manifest_entries_from_events(&[other, current], Some("current-session"));
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].crate_name, "current");
assert_eq!(entries[0].cache_key, "current-key");
}
#[test]
fn manifest_entries_from_events_falls_back_to_elapsed_when_no_compile_time() {
use crate::events::EventResult;
let events = vec![build_event("x", EventResult::Miss, 0, 77, 1, "k")];
let entries = manifest_entries_from_events(&events, None);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].compile_time_ms, 77);
}
#[test]
fn manifest_entries_from_events_accepts_remote_and_prefetch_hits() {
use crate::events::EventResult;
let events = vec![
build_event(
"prefetch",
EventResult::PrefetchHit,
12,
3,
10,
"k-prefetch",
),
build_event("remote", EventResult::RemoteHit, 34, 5, 20, "k-remote"),
build_event("error", EventResult::Error, 99, 99, 99, "k-error"),
];
let mut entries = manifest_entries_from_events(&events, None);
entries.sort_by(|a, b| a.crate_name.cmp(&b.crate_name));
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].crate_name, "prefetch");
assert_eq!(entries[0].artifact_size, 10);
assert_eq!(entries[1].crate_name, "remote");
assert_eq!(entries[1].compile_time_ms, 34);
}
fn test_remote_cfg() -> crate::config::RemoteConfig {
crate::config::RemoteConfig::test_s3("bucket", "prefix")
}
#[tokio::test]
async fn upload_manifest_and_shards_uploads_manifest_only_without_namespace() {
let backend = TestBackend::memory();
let client = as_remote_backend(&backend);
let remote = test_remote_cfg();
let remote_cache: Arc<crate::cache_remote::V3Remote> =
Arc::new(crate::cache_remote::V3Remote::new(client, remote));
let entries = vec![crate::remote::ManifestEntry {
cache_key: "k".to_string(),
crate_name: "c".to_string(),
compile_time_ms: 1,
artifact_size: 1,
}];
upload_manifest_and_shards(
&remote_cache,
"mykey",
None,
std::path::Path::new("/nonexistent/Cargo.lock"),
entries,
None,
)
.await
.expect("manifest-only upload should succeed");
assert_eq!(backend.put_calls(), vec!["prefix/_manifests/mykey.json"]);
}
#[tokio::test]
async fn upload_manifest_and_shards_skips_shards_when_lock_missing() {
let backend = TestBackend::memory();
let client = as_remote_backend(&backend);
let remote = test_remote_cfg();
let remote_cache: Arc<crate::cache_remote::V3Remote> =
Arc::new(crate::cache_remote::V3Remote::new(client, remote));
let entries = vec![crate::remote::ManifestEntry {
cache_key: "k".to_string(),
crate_name: "c".to_string(),
compile_time_ms: 1,
artifact_size: 1,
}];
upload_manifest_and_shards(
&remote_cache,
"mykey",
Some("ns"),
std::path::Path::new("/nonexistent/Cargo.lock"),
entries,
None,
)
.await
.expect("upload should succeed, shards skipped");
assert_eq!(backend.put_calls(), vec!["prefix/_manifests/mykey.json"]);
}
#[tokio::test]
async fn upload_manifest_and_shards_uploads_shards_when_lock_present() {
let dir = tempfile::tempdir().unwrap();
let lock = dir.path().join("Cargo.lock");
std::fs::write(
&lock,
"version = 3\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\n",
)
.unwrap();
let entries = vec![crate::remote::ManifestEntry {
cache_key: "k-serde".to_string(),
crate_name: "serde".to_string(),
compile_time_ms: 1,
artifact_size: 1,
}];
let deps = crate::shards::parse_cargo_lock(&lock).unwrap();
let expected_shards = crate::shards::compute_shards(&deps).shards.len();
let backend = TestBackend::memory();
let client = as_remote_backend(&backend);
let remote = test_remote_cfg();
let remote_cache: Arc<crate::cache_remote::V3Remote> =
Arc::new(crate::cache_remote::V3Remote::new(client, remote));
upload_manifest_and_shards(&remote_cache, "mykey", Some("ns"), &lock, entries, None)
.await
.expect("upload with shards should succeed");
let puts = backend.put_calls();
assert_eq!(puts.len(), expected_shards + 1);
assert!(puts.contains(&"prefix/_manifests/mykey.json".to_string()));
assert_eq!(
puts.iter()
.filter(|key| key.starts_with("prefix/_manifests/v3/ns/shards/"))
.count(),
expected_shards
);
}
fn sync_test_cache_key(seed: &str) -> String {
blake3::hash(seed.as_bytes()).to_hex().to_string()
}
#[tokio::test]
async fn sync_with_client_dry_run_empty_remote_reports_nothing() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let backend = TestBackend::memory();
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
false,
false,
true,
false,
None,
false,
false,
)
.await
.expect("dry-run sync over empty remote should succeed");
assert_eq!(backend.list_calls(), vec!["prefix/v3/manifests/"]);
}
#[tokio::test]
async fn sync_with_client_workspace_pull_scopes_listing_to_workspace_members() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let workspace: std::collections::HashSet<String> = ["wsfoo".to_string()].into_iter().collect();
let lock: std::collections::HashSet<String> = ["dep_a".to_string(), "dep_b".to_string()]
.into_iter()
.collect();
let backend = TestBackend::memory();
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
Some(&workspace), true, false, true, false, Some(&lock), true, false, )
.await
.expect("workspace-scoped pull should list only the workspace member(s)");
assert_eq!(backend.list_calls(), vec!["prefix/v3/manifests/wsfoo/"]);
}
#[tokio::test]
async fn sync_with_client_workspace_pull_errors_when_no_workspace_resolved() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let empty: std::collections::HashSet<String> = std::collections::HashSet::new();
let cases: [Option<&std::collections::HashSet<String>>; 2] = [None, Some(&empty)];
for workspace_crates in cases {
let backend = TestBackend::memory();
let err = sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
workspace_crates, true, false, true, false, None, true, false, )
.await
.expect_err("--workspace with no resolved members must error, not scan the bucket");
assert!(
err.to_string().contains("no workspace members resolved"),
"unexpected error: {err}"
);
assert!(
backend.list_calls().is_empty(),
"guard must run before listing the remote"
);
}
}
#[tokio::test]
async fn sync_with_client_push_uploads_local_only_entry() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let artifact = src_dir.join("libfoo.rlib");
std::fs::write(&artifact, b"artifact bytes").unwrap();
store
.put(
"pushkey123",
"foo",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(artifact, "libfoo.rlib".to_string())],
"",
"",
)
.unwrap();
let remote = test_remote_cfg();
let backend = TestBackend::memory();
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
false,
true,
false,
false,
None,
false,
false,
)
.await
.expect("push sync should succeed");
let puts = backend.put_calls();
assert_eq!(puts.len(), 2);
assert!(puts.contains(&"prefix/v3/packs/foo/pushkey123.tar.zst".to_string()));
assert!(puts.contains(&"prefix/v3/manifests/foo/pushkey123.json".to_string()));
}
#[tokio::test]
async fn sync_with_client_push_throttles_with_low_concurrency() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
config.s3_concurrency = 1;
let store = Store::open(&config).unwrap();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
for (key, cn) in [("pusha1", "aaa"), ("pushb2", "bbb")] {
let artifact = src_dir.join(format!("{cn}.rlib"));
std::fs::write(&artifact, format!("{cn} bytes")).unwrap();
store
.put(
key,
cn,
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(artifact, format!("{cn}.rlib"))],
"",
"",
)
.unwrap();
}
let remote = test_remote_cfg();
let backend = TestBackend::memory();
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
false,
true,
false,
false,
None,
false,
false,
)
.await
.expect("throttled push sync should succeed");
assert_eq!(backend.put_calls().len(), 4);
}
#[tokio::test]
async fn sync_with_client_dry_run_plans_pull_for_remote_only_key() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key = sync_test_cache_key("dry-run-remote-only");
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/serde/{key}.json"),
b"{}".to_vec(),
)
.await;
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
false,
false,
true,
false,
None,
false,
false,
)
.await
.expect("dry-run sync planning a pull should succeed");
assert!(
backend.get_calls().is_empty(),
"dry-run must not download the pack"
);
}
#[tokio::test]
async fn sync_with_client_pull_loop_records_failure_and_returns_err_by_default() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key = sync_test_cache_key("failed-pull");
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/serde/{key}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(
&format!("prefix/v3/packs/serde/{key}.tar.zst"),
b"not a valid pack".to_vec(),
)
.await;
let err = sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
false,
)
.await
.expect_err("pull sync should return Err when a download fails by default");
assert!(
err.to_string()
.contains("1 transfer(s) or import(s) failed")
);
assert_eq!(
backend.get_calls(),
vec![format!("prefix/v3/packs/serde/{key}.tar.zst")]
);
}
#[tokio::test]
async fn sync_with_client_pull_allows_partial_when_flag_set() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key = sync_test_cache_key("failed-pull-partial");
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/serde/{key}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(
&format!("prefix/v3/packs/serde/{key}.tar.zst"),
b"not a valid pack".to_vec(),
)
.await;
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
true,
)
.await
.expect("pull sync with allow_partial should complete Ok despite failed download");
assert_eq!(
backend.get_calls(),
vec![format!("prefix/v3/packs/serde/{key}.tar.zst")]
);
}
#[tokio::test]
async fn sync_with_client_push_reports_failed_uploads_and_returns_err_by_default() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let artifact = src_dir.join("foo.rlib");
std::fs::write(&artifact, b"foo bytes").unwrap();
store
.put(
"pushfail1aaaa",
"foo",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(artifact, "foo.rlib".to_string())],
"",
"",
)
.unwrap();
let backend = TestBackend::failing_put();
let err = sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
false,
true,
false,
false,
None,
false,
false,
)
.await
.expect_err("push sync should return Err when an upload fails by default");
assert!(
err.to_string()
.contains("1 transfer(s) or import(s) failed")
);
assert_eq!(
backend.put_calls(),
vec!["prefix/v3/packs/foo/pushfail1aaaa.tar.zst"]
);
}
#[tokio::test]
async fn sync_with_client_push_allows_partial_when_flag_set() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let artifact = src_dir.join("foo.rlib");
std::fs::write(&artifact, b"foo bytes").unwrap();
store
.put(
"pushfail1aaaa",
"foo",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(artifact, "foo.rlib".to_string())],
"",
"",
)
.unwrap();
let backend = TestBackend::failing_put();
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
false,
true,
false,
false,
None,
false,
true,
)
.await
.expect("push sync with allow_partial should complete Ok even when an upload fails");
assert_eq!(
backend.put_calls(),
vec!["prefix/v3/packs/foo/pushfail1aaaa.tar.zst"]
);
}
#[tokio::test]
async fn sync_with_client_pull_throttles_with_low_concurrency() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
config.s3_concurrency = 1;
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let backend = TestBackend::memory();
for (crate_name, key) in [
("aaa", sync_test_cache_key("throttled-pull-a")),
("bbb", sync_test_cache_key("throttled-pull-b")),
] {
backend
.seed(
&format!("prefix/v3/manifests/{crate_name}/{key}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(
&format!("prefix/v3/packs/{crate_name}/{key}.tar.zst"),
b"not a pack".to_vec(),
)
.await;
}
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
true,
)
.await
.expect("throttled pull sync should complete Ok with allow_partial");
assert_eq!(backend.get_calls().len(), 2);
}
fn build_entry_pack(key: &str, crate_name: &str) -> Vec<u8> {
let tmp = tempfile::tempdir().unwrap();
let cfg = save_manifest_config(tmp.path().to_path_buf(), None);
let store = Store::open(&cfg).unwrap();
let src = tmp.path().join("src");
std::fs::create_dir_all(&src).unwrap();
let artifact = src.join("libfoo.rlib");
std::fs::write(&artifact, b"real artifact bytes").unwrap();
store
.put(
key,
crate_name,
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(artifact, "libfoo.rlib".to_string())],
"",
"",
)
.unwrap();
let entry_dir = store.entry_dir(key);
let meta: crate::store::EntryMeta =
serde_json::from_slice(&std::fs::read(entry_dir.join("meta.json")).unwrap()).unwrap();
crate::remote_layout::create_entry_pack_zstd(&entry_dir, &store.blobs_dir(), &meta, 3).unwrap()
}
#[tokio::test]
async fn sync_with_client_pull_downloads_and_imports_entry() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key = sync_test_cache_key("successful-pull");
let pack = build_entry_pack(&key, "serde");
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/serde/{key}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(&format!("prefix/v3/packs/serde/{key}.tar.zst"), pack)
.await;
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
false,
)
.await
.expect("pull sync should succeed");
assert!(
config.store_dir().join(&key).join("meta.json").exists(),
"pulled entry should be materialized in the local store"
);
assert_eq!(
backend.get_calls(),
vec![format!("prefix/v3/packs/serde/{key}.tar.zst")]
);
}
struct DisappearingBackend {
inner: Arc<TestBackend>,
on_put_delete: std::path::PathBuf,
}
#[async_trait::async_trait]
impl crate::remote_backend::RemoteBackend for DisappearingBackend {
async fn head(&self, key: &str) -> Result<bool> {
self.inner.as_ref().head(key).await
}
async fn get(
&self,
key: &str,
max_bytes: Option<u64>,
) -> Result<Option<crate::remote_backend::GetObject>> {
self.inner.as_ref().get(key, max_bytes).await
}
async fn put(&self, key: &str, body: Vec<u8>, content_type: Option<&str>) -> Result<()> {
if self.on_put_delete.exists() {
let _ = std::fs::remove_dir_all(&self.on_put_delete);
}
self.inner.as_ref().put(key, body, content_type).await
}
async fn list(&self, prefix: &str) -> Result<Vec<String>> {
self.inner.as_ref().list(prefix).await
}
fn describe(&self, key: &str) -> String {
self.inner.as_ref().describe(key)
}
}
#[tokio::test]
async fn sync_with_client_push_fails_when_local_entry_disappears() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
config.s3_concurrency = 1;
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
for (k, cn) in [("keep_first", "aaa"), ("disappear_second", "zzz")] {
let artifact = src_dir.join(format!("{cn}.rlib"));
std::fs::write(&artifact, format!("{cn} bytes")).unwrap();
store
.put(
k,
cn,
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(artifact, format!("{cn}.rlib"))],
"",
"",
)
.unwrap();
}
let disappear_dir = config.store_dir().join("disappear_second");
let backend: Arc<dyn crate::remote_backend::RemoteBackend> = Arc::new(DisappearingBackend {
inner: TestBackend::memory(),
on_put_delete: disappear_dir,
});
let err = sync_with_client(
&as_cache_remote(backend, &remote),
&config,
&store,
None,
false,
true,
false,
false,
None,
false,
false,
)
.await
.expect_err("disappeared local entry must cause non-zero exit by default");
assert!(
err.to_string()
.contains("1 transfer(s) or import(s) failed")
);
}
fn build_invalid_meta_pack() -> Vec<u8> {
let mut tar_builder = tar::Builder::new(Vec::new());
let meta_bytes = b"{\"not\": \"valid meta\"}";
let mut header = tar::Header::new_gnu();
header.set_path("meta.json").unwrap();
header.set_size(meta_bytes.len() as u64);
header.set_mode(0o644);
header.set_cksum();
tar_builder.append(&header, &meta_bytes[..]).unwrap();
let tar_data = tar_builder.into_inner().unwrap();
zstd::encode_all(&tar_data[..], 3).unwrap()
}
#[tokio::test]
async fn sync_with_client_pull_fails_when_import_fails_by_default() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key = sync_test_cache_key("invalid-meta-pull");
let pack = build_invalid_meta_pack();
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/foo/{key}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(&format!("prefix/v3/packs/foo/{key}.tar.zst"), pack)
.await;
let err = sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
false,
)
.await
.expect_err("pull sync should return Err when import fails by default");
assert!(
err.to_string()
.contains("1 transfer(s) or import(s) failed")
);
}
#[tokio::test]
async fn sync_with_client_pull_allows_partial_when_import_fails() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key = sync_test_cache_key("invalid-meta-pull-allow");
let pack = build_invalid_meta_pack();
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/foo/{key}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(&format!("prefix/v3/packs/foo/{key}.tar.zst"), pack)
.await;
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
true,
)
.await
.expect("pull sync with allow_partial should return Ok even when import fails");
}
#[tokio::test]
async fn sync_with_client_mixed_success_and_failure_completes_all_and_fails_default() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key_good = sync_test_cache_key("mixed-good");
let key_bad = sync_test_cache_key("mixed-bad");
let pack_good = build_entry_pack(&key_good, "serde");
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/serde/{key_good}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(
&format!("prefix/v3/packs/serde/{key_good}.tar.zst"),
pack_good,
)
.await;
backend
.seed(
&format!("prefix/v3/manifests/tokio/{key_bad}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(
&format!("prefix/v3/packs/tokio/{key_bad}.tar.zst"),
b"corrupted bytes".to_vec(),
)
.await;
let err = sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
false,
)
.await
.expect_err("mixed sync should complete all transfers and return Err by default");
assert!(
err.to_string()
.contains("1 transfer(s) or import(s) failed")
);
assert_eq!(backend.get_calls().len(), 2);
assert!(
config
.store_dir()
.join(&key_good)
.join("meta.json")
.exists(),
"good entry should be materialized"
);
}
#[tokio::test]
async fn sync_with_client_mixed_success_and_failure_with_allow_partial_returns_ok() {
let dir = tempfile::tempdir().unwrap();
let config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
let store = Store::open(&config).unwrap();
let remote = test_remote_cfg();
let key_good = sync_test_cache_key("mixed-good-2");
let key_bad = sync_test_cache_key("mixed-bad-2");
let pack_good = build_entry_pack(&key_good, "serde");
let backend = TestBackend::memory();
backend
.seed(
&format!("prefix/v3/manifests/serde/{key_good}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(
&format!("prefix/v3/packs/serde/{key_good}.tar.zst"),
pack_good,
)
.await;
backend
.seed(
&format!("prefix/v3/manifests/tokio/{key_bad}.json"),
b"{}".to_vec(),
)
.await;
backend
.seed(
&format!("prefix/v3/packs/tokio/{key_bad}.tar.zst"),
b"corrupted bytes".to_vec(),
)
.await;
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
true,
false,
false,
false,
None,
false,
true,
)
.await
.expect("mixed sync with allow_partial should complete Ok");
assert_eq!(backend.get_calls().len(), 2);
assert!(
config
.store_dir()
.join(&key_good)
.join("meta.json")
.exists(),
"good entry should be materialized"
);
}
#[test]
fn draw_clean_renders_target_table() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let targets = vec![
TargetEntry {
path: std::path::PathBuf::from("/work/proj-a/target"),
size: 5_000_000,
cached_bytes: 3_000_000,
estimated_reclaimable_bytes: 2_000_000,
scan_identity: None,
profiles: vec!["debug".to_string(), "release".to_string()],
breakdown: CategoryBreakdown::default(),
stale: false,
},
TargetEntry {
path: std::path::PathBuf::from("/work/proj-b/target"),
size: 2_000_000,
cached_bytes: 0,
estimated_reclaimable_bytes: 2_000_000,
scan_identity: None,
profiles: vec![],
breakdown: CategoryBreakdown::default(),
stale: false,
},
];
let selected = vec![true, false];
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
terminal
.draw(|frame| draw_clean(frame, &targets, &selected, 0, std::path::Path::new("/work")))
.expect("clean selector draw should succeed");
let buffer = terminal.backend().buffer().clone();
let rendered: String = buffer.content().iter().map(|c| c.symbol()).collect();
assert!(rendered.contains("kache clean"), "header should render");
assert!(
rendered.contains("proj-a") && rendered.contains("proj-b"),
"both target rows should render"
);
assert!(rendered.contains("[x]"), "selected row shows a checked box");
assert!(
rendered.contains("Selected: 1 (est. 1.9 MiB)"),
"selected total subtracts cached bytes, got: {rendered}"
);
assert!(
!rendered.contains("Selected: 1 (est. 4.8 MiB)"),
"selected total must not report the raw size"
);
assert!(
rendered.contains("2 dirs (6.7 MiB total, 2.9 MiB cached)"),
"header totals remain raw sizes"
);
}
#[test]
fn render_clean_dry_run_formats_plural_profiles_and_fallback_paths() {
let root = std::path::Path::new("/work");
let single = vec![TargetEntry {
path: std::path::PathBuf::from("/work/proj/target"),
size: 1024,
cached_bytes: 512,
estimated_reclaimable_bytes: 512,
scan_identity: None,
profiles: vec!["debug".to_string()],
breakdown: CategoryBreakdown::default(),
stale: false,
}];
let none = std::collections::HashSet::new();
let single_out = render_clean_dry_run(&single, root, &none).join("\n");
assert!(
single_out.starts_with(
"Remove 1 target directory under /work · 1.0 KiB on disk · 512 B freeable"
),
"{single_out}"
);
assert!(
single_out.contains(" 512 B 1.0 KiB 512 B proj/target [debug]"),
"{single_out}"
);
assert!(
single_out.ends_with("`kache clean /work --yes` removes them."),
"{single_out}"
);
let many = vec![
TargetEntry {
path: std::path::PathBuf::from("/work/proj-a/target"),
size: 10,
cached_bytes: 0,
estimated_reclaimable_bytes: 10,
scan_identity: None,
profiles: Vec::new(),
breakdown: CategoryBreakdown::default(),
stale: false,
},
TargetEntry {
path: std::path::PathBuf::from("/outside/proj-b/target"),
size: 20,
cached_bytes: 5,
estimated_reclaimable_bytes: 15,
scan_identity: None,
profiles: vec!["release".to_string()],
breakdown: CategoryBreakdown::default(),
stale: false,
},
];
let many_out = render_clean_dry_run(&many, root, &none).join("\n");
assert!(!many_out.contains("worktree deleted"));
let orphaned = [std::path::PathBuf::from("/outside/proj-b/target")].into();
let marked = render_clean_dry_run(&many, root, &orphaned).join("\n");
assert_eq!(marked.matches("(worktree deleted)").count(), 1, "{marked}");
assert!(
many_out.starts_with(
"Remove 2 target directories under /work · 30 B on disk · 25 B freeable"
),
"{many_out}"
);
assert!(
many_out.contains(" proj-a/target\n"),
"relative under the root: {many_out}"
);
assert!(
many_out.contains("/outside/proj-b/target [release]"),
"{many_out}"
);
}
#[test]
fn clean_handle_key_navigation_and_selection() {
use crossterm::event::KeyCode;
let mut selected = vec![false, false, false];
let mut cursor = 0usize;
let len = 3;
assert_eq!(
clean_handle_key(KeyCode::Down, &mut selected, &mut cursor, len),
CleanStep::Continue
);
assert_eq!(cursor, 1);
clean_handle_key(KeyCode::Up, &mut selected, &mut cursor, len);
assert_eq!(cursor, 0);
clean_handle_key(KeyCode::Up, &mut selected, &mut cursor, len);
assert_eq!(cursor, 0, "up saturates at 0");
clean_handle_key(KeyCode::Char(' '), &mut selected, &mut cursor, len);
assert!(selected[0]);
assert_eq!(cursor, 1);
clean_handle_key(KeyCode::Char('a'), &mut selected, &mut cursor, len);
assert!(selected.iter().all(|s| *s));
clean_handle_key(KeyCode::Char('n'), &mut selected, &mut cursor, len);
assert!(selected.iter().all(|s| !*s));
}
#[test]
fn clean_handle_key_handles_boundaries() {
use crossterm::event::KeyCode;
let mut empty = Vec::new();
let mut empty_cursor = 0usize;
assert_eq!(
clean_handle_key(KeyCode::Char(' '), &mut empty, &mut empty_cursor, 0),
CleanStep::Continue
);
assert_eq!(empty_cursor, 0);
let mut selected = vec![false, false];
let mut cursor = 1usize;
clean_handle_key(KeyCode::Down, &mut selected, &mut cursor, 2);
assert_eq!(cursor, 1, "down clamps at last row");
clean_handle_key(KeyCode::Char(' '), &mut selected, &mut cursor, 2);
assert!(selected[1]);
assert_eq!(cursor, 1, "space on last row does not advance");
cursor = 10;
clean_handle_key(KeyCode::Char(' '), &mut selected, &mut cursor, 2);
assert_eq!(cursor, 10, "out-of-range cursor is ignored");
}
#[test]
fn remove_targets_deletes_all_and_reports_estimates() {
let root = tempfile::tempdir().unwrap();
let a = root.path().join("proj-a/target");
let b = root.path().join("proj-b/target");
std::fs::create_dir_all(&a).unwrap();
std::fs::create_dir_all(&b).unwrap();
let to_remove = vec![
RemovalTarget {
path: a.clone(),
scanned_identity: directory_identity(&a),
estimated_reclaimable: 60,
apparent_gap: 40,
},
RemovalTarget {
path: b.clone(),
scanned_identity: directory_identity(&b),
estimated_reclaimable: 200,
apparent_gap: 0,
},
];
let (removed, estimated_reclaimed, apparent_gap) = remove_targets(
&to_remove,
root.path(),
root.path(),
false,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
);
assert_eq!(removed, 2, "both target/ dirs removed");
assert_eq!(estimated_reclaimed, 260);
assert_eq!(apparent_gap, 40);
assert!(!a.exists() && !b.exists(), "directories are gone from disk");
}
#[test]
fn remove_targets_skips_failures_without_aborting() {
let root = tempfile::tempdir().unwrap();
let missing = root.path().join("gone/target");
let real = root.path().join("proj/target");
std::fs::create_dir_all(&real).unwrap();
let to_remove = vec![
RemovalTarget {
path: missing,
scanned_identity: None,
estimated_reclaimable: 90,
apparent_gap: 10,
},
RemovalTarget {
path: real.clone(),
scanned_identity: directory_identity(&real),
estimated_reclaimable: 150,
apparent_gap: 50,
},
];
let mut failed = Vec::new();
let (removed, estimated_reclaimed, apparent_gap) = remove_targets(
&to_remove,
root.path(),
root.path(),
false,
&mut Vec::new(),
&mut Vec::new(),
&mut failed,
);
assert_eq!(removed, 1, "only the existing dir counts as removed");
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].path, to_remove[0].path.display().to_string());
assert_eq!(
failed[0].reason,
"directory changed since scan; refusing to remove"
);
assert_eq!(
estimated_reclaimed, 150,
"failed dir's estimate is not counted"
);
assert_eq!(apparent_gap, 50, "failed dir's gap is not counted");
assert!(!real.exists(), "the reachable dir was still removed");
}
#[test]
fn a_target_whose_build_lock_is_held_is_in_use() {
let root = tempfile::tempdir().unwrap();
let target = root.path().join("target");
let debug = target.join("debug");
let cross = target.join("x86_64-unknown-linux-gnu/release");
let too_deep = target.join("a/b/c/d");
for dir in [&debug, &cross, &too_deep] {
std::fs::create_dir_all(dir).unwrap();
std::fs::write(dir.join(".cargo-lock"), b"").unwrap();
std::fs::write(dir.join("libfoo.rlib"), b"rlib").unwrap();
}
let mut found = cargo_lock_files(&target, 3);
found.sort();
let mut expected = vec![debug.join(".cargo-lock"), cross.join(".cargo-lock")];
expected.sort();
assert_eq!(found, expected, "profiles and triple profiles, not deeper");
assert!(!target_in_use(&target));
let held = std::fs::File::open(cross.join(".cargo-lock")).unwrap();
held.lock().unwrap();
assert!(target_in_use(&target));
held.unlock().unwrap();
drop(held);
assert!(!target_in_use(&target));
}
#[test]
fn probing_a_free_lock_leaves_it_free_while_a_duplicate_lives() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(".cargo-lock");
std::fs::write(&path, b"").unwrap();
let probe = std::fs::File::open(&path).unwrap();
let duplicate = probe.try_clone().unwrap();
assert!(!held_elsewhere(&probe));
drop(probe);
let next = std::fs::File::open(&path).unwrap();
assert!(!held_elsewhere(&next), "the probe left its lock behind");
next.lock().unwrap();
assert!(held_elsewhere(&std::fs::File::open(&path).unwrap()));
drop(duplicate);
}
#[test]
fn remove_targets_leaves_a_target_a_build_is_writing() {
let root = tempfile::tempdir().unwrap();
let busy = root.path().join("busy/target");
let idle = root.path().join("idle/target");
for target in [&busy, &idle] {
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("debug/.cargo-lock"), b"").unwrap();
}
let held = std::fs::File::open(busy.join("debug/.cargo-lock")).unwrap();
held.lock().unwrap();
let to_remove = [&busy, &idle].map(|path| RemovalTarget {
path: path.clone(),
scanned_identity: directory_identity(path),
estimated_reclaimable: 10,
apparent_gap: 0,
});
let mut skipped = Vec::new();
let (removed, reclaimed, _) = remove_targets(
&to_remove,
root.path(),
root.path(),
true,
&mut skipped,
&mut Vec::new(),
&mut Vec::new(),
);
assert_eq!((removed, reclaimed), (1, 10));
assert!(busy.exists() && !idle.exists());
assert_eq!(skipped.len(), 1);
assert_eq!(skipped[0].path, busy.display().to_string());
assert_eq!(skipped[0].reason, TARGET_IN_USE);
}
#[test]
fn remove_targets_keeps_a_target_while_cargo_is_running() {
let root = tempfile::tempdir().unwrap();
let target = root.path().join("project/target");
std::fs::create_dir_all(&target).unwrap();
let to_remove = [RemovalTarget {
path: target.clone(),
scanned_identity: directory_identity(&target),
estimated_reclaimable: 10,
apparent_gap: 0,
}];
let command =
crate::target_use::shared(root.path(), "cargo build", &mut std::io::sink()).unwrap();
let mut skipped = Vec::new();
let mut waiting = Vec::new();
assert_eq!(
remove_targets(
&to_remove,
root.path(),
root.path(),
true,
&mut skipped,
&mut waiting,
&mut Vec::new()
)
.0,
0
);
assert!(target.exists());
let pid = std::process::id();
assert_eq!(waiting.len(), 1);
assert_eq!(
(waiting[0].pid, waiting[0].what.as_str()),
(pid, "cargo build")
);
assert!(
skipped[0]
.reason
.starts_with(&format!("waiting for `cargo build` (pid {pid}, running ")),
"{}",
skipped[0].reason
);
let paths = [target.as_path()];
let notes = removal_notes(root.path(), &paths);
assert_eq!(notes.len(), 1);
assert!(
notes[0].starts_with("Not now: waiting for `cargo build`"),
"{notes:?}"
);
drop(command);
assert!(crate::target_use::holders(root.path()).is_empty());
assert!(removal_notes(root.path(), &paths).is_empty());
assert_eq!(
remove_targets(
&to_remove,
root.path(),
root.path(),
true,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new()
)
.0,
1
);
assert!(!target.exists());
}
#[cfg(unix)]
#[test]
fn removing_a_target_never_follows_a_link_out_of_it() {
let root = tempfile::tempdir().unwrap();
let outside = root.path().join("outside");
std::fs::create_dir_all(&outside).unwrap();
std::fs::write(outside.join("keep.txt"), b"keep").unwrap();
let target = root.path().join("app/target");
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::os::unix::fs::symlink(&outside, target.join("debug/linked-dir")).unwrap();
std::os::unix::fs::symlink(outside.join("keep.txt"), target.join("linked-file")).unwrap();
let alias = root.path().join("alias");
std::os::unix::fs::symlink(&outside, &alias).unwrap();
let to_remove = [&target, &alias].map(|path| RemovalTarget {
path: path.clone(),
scanned_identity: directory_identity(path),
estimated_reclaimable: 0,
apparent_gap: 0,
});
let mut failed = Vec::new();
let (removed, _, _) = remove_targets(
&to_remove,
root.path(),
root.path(),
true,
&mut Vec::new(),
&mut Vec::new(),
&mut failed,
);
assert_eq!(removed, 1);
assert!(!target.exists());
assert_eq!(failed.len(), 1);
assert_eq!(failed[0].path, alias.display().to_string());
assert_eq!(std::fs::read(outside.join("keep.txt")).unwrap(), b"keep");
}
#[test]
fn only_targets_this_clean_deleted_count_as_removed() {
let root = tempfile::tempdir().unwrap();
let gone = root.path().join("moved/target");
let target = |path: &std::path::Path| RemovalTarget {
path: path.to_path_buf(),
scanned_identity: None,
estimated_reclaimable: 0,
apparent_gap: 0,
};
let to_remove = [target(&gone), target(root.path())];
let deleted = |failed: &[CleanFailed], skipped: &[CleanSkipped]| {
deleted_targets(&to_remove, failed, skipped)
.map(|target| target.path.clone())
.collect::<Vec<_>>()
};
assert_eq!(
deleted(&[], &[]),
std::slice::from_ref(&gone),
"an existing path is not removed"
);
let failed = CleanFailed {
path: gone.display().to_string(),
reason: "denied".to_owned(),
};
assert!(deleted(std::slice::from_ref(&failed), &[]).is_empty());
let skipped = CleanSkipped {
path: gone.display().to_string(),
reason: TARGET_IN_USE.to_owned(),
missing: false,
};
assert!(deleted(&[], std::slice::from_ref(&skipped)).is_empty());
}
#[test]
fn held_reason_names_each_command_and_how_long_it_ran() {
use crate::target_use::Holder;
let holder = |pid, what: &str, since| Holder {
pid,
what: what.to_owned(),
since,
};
assert_eq!(
held_reason(&[], 100),
"another kache cleanup or a running Cargo command holds kache's target lock"
);
assert_eq!(
held_reason(&[holder(7, "cargo run", 100 - 12)], 100),
"waiting for `cargo run` (pid 7, running 12.0 s) to exit"
);
assert_eq!(
held_reason(
&[
holder(7, "cargo run", 0),
holder(8, "kunobi", 0),
holder(9, "cargo test", 0)
],
7_200
),
"waiting for `cargo run` (pid 7, running ~2.0 h), `kunobi` (pid 8, running ~2.0 h) and `cargo test` (pid 9, running ~2.0 h) to exit"
);
}
#[test]
fn held_summary_counts_only_the_targets_the_lock_held_back() {
let skip = |reason: &str| CleanSkipped {
path: "/t".to_owned(),
reason: reason.to_owned(),
missing: false,
};
let held = "waiting for `cargo run` (pid 7, running 12.0 s) to exit";
let skipped = [skip(held), skip(TARGET_IN_USE), skip(held)];
assert_eq!(
held_summary(Some(held), &skipped).as_deref(),
Some(format!(" skipped 2 targets — {held}").as_str())
);
assert_eq!(held_summary(Some(held), &[skip(TARGET_IN_USE)]), None);
assert_eq!(held_summary(None, &skipped), None);
}
#[test]
fn a_dry_run_names_targets_a_build_holds() {
let root = tempfile::tempdir().unwrap();
let busy = root.path().join("busy/target");
std::fs::create_dir_all(busy.join("debug")).unwrap();
std::fs::write(busy.join("debug/.cargo-lock"), b"").unwrap();
let paths = [busy.as_path()];
assert!(removal_notes(root.path(), &paths).is_empty());
assert!(removal_notes(root.path(), &[]).is_empty());
let held = std::fs::File::open(busy.join("debug/.cargo-lock")).unwrap();
held.lock().unwrap();
assert_eq!(
removal_notes(root.path(), &paths),
[format!("Not now: 1 target {TARGET_IN_USE}.")]
);
}
#[test]
fn idle_times_use_the_largest_whole_unit() {
assert_eq!(format_idle(59), "59s");
assert_eq!(format_idle(60), "1m");
assert_eq!(format_idle(3_599), "59m");
assert_eq!(format_idle(3_600), "1h");
assert_eq!(format_idle(86_399), "23h");
assert_eq!(format_idle(86_400), "1d");
}
const CARGO_CACHEDIR_TAG: &str = "Signature: 8a477f597d28d172789f06886806bc55";
fn row(workspace: &str, state: TargetState, reclaimable: u64) -> TargetRow {
TargetRow {
path: format!("{workspace}/target"),
workspace: workspace.to_string(),
state,
idle_seconds: Some(3 * 86_400),
discovered: false,
next_pass: Default::default(),
unit_cleanup: Default::default(),
profiles: vec!["debug".to_string()],
apparent_bytes: reclaimable * 2,
reclaimable_bytes: reclaimable,
cached_bytes: reclaimable,
}
}
#[test]
fn json_targets_suggest_the_orphan_clean_only_when_a_worktree_is_gone() {
let live = [row("/wt/a", TargetState::Live, 1)];
assert!(targets_next_actions(&live, true).is_empty());
let one = [
row("/wt/a", TargetState::Live, 1),
row("/wt/b", TargetState::WorktreeDeleted, 1),
];
let next = targets_next_actions(&one, true);
assert_eq!(next.len(), 1);
assert_eq!(next[0].argv, ["kache", "clean", "--orphans", "--yes"]);
assert_eq!(
next[0].why,
"1 target belongs to a deleted worktree; the daemon removes it a day after its last \
build. This removes it now"
);
let manual = targets_next_actions(&one, false);
assert_eq!(
manual[0].why,
"1 target belongs to a deleted worktree; automatic removal is off"
);
}
#[test]
fn the_orphan_note_agrees_with_the_count_and_the_setting() {
assert_eq!(
orphan_note(2, true),
"2 targets belong to deleted worktrees; the daemon removes them a day after their \
last build"
);
assert_eq!(
orphan_note(2, false),
"2 targets belong to deleted worktrees"
);
assert_eq!(
orphan_note(1, false),
"1 target belongs to a deleted worktree"
);
let mut found = row("/wt/b", TargetState::WorktreeDeleted, 1);
found.discovered = true;
assert_eq!(
recorded_orphans(&[found, row("/wt/c", TargetState::WorktreeDeleted, 1)]),
1,
"a target found through Git is not one a build recorded"
);
}
#[test]
fn a_target_says_what_the_next_pass_does_to_it() {
use crate::target_cleanup::{Plan, Reason};
use crate::unit_prune::Pruned;
let one_unit = Plan {
remove: None,
units: Pruned {
units: 1,
bytes: 2048,
..Pruned::default()
},
};
assert_eq!(
next_pass_note(&one_unit),
" (next pass prunes 1 unused unit (2.0 KiB))"
);
let both = Plan {
remove: Some(Reason::Pressure),
units: Pruned {
units: 3,
bytes: 1024,
..Pruned::default()
},
};
assert_eq!(
next_pass_note(&both),
" (next pass prunes 3 unused units (1.0 KiB), then removes it if the volume is still below the free-space floor)"
);
let idle = Plan {
remove: Some(Reason::Idle),
units: Pruned::default(),
};
assert_eq!(
next_pass_note(&idle),
" (next pass removes it: no build used it within auto_clean_idle_targets_days)"
);
assert_eq!(next_pass_note(&Plan::default()), "");
let mut planned = row("/wt/a", TargetState::Live, 1024);
planned.next_pass = idle;
let rendered = render_targets(&[planned], true).join("\n");
assert!(
rendered.contains("/wt/a/target (next pass removes it: no build used it"),
"{rendered}"
);
}
#[test]
fn the_targets_table_says_nothing_about_unit_cleanup_unless_it_is_on() {
let mut off = row("/wt/a", TargetState::Live, 1024);
off.unit_cleanup.status = "off".into();
let rendered =
render_targets(&[off.clone(), row("/wt/b", TargetState::Live, 1024)], true).join("\n");
assert!(!rendered.contains("unit cleanup"), "{rendered}");
off.unit_cleanup.status = "unavailable: no receipt yet".into();
let rendered = render_targets(&[off], true).join("\n");
assert!(
rendered.contains("(unit cleanup: unavailable: no receipt yet)"),
"{rendered}"
);
}
#[test]
fn the_targets_table_totals_and_points_at_deleted_worktrees() {
let one = render_targets(&[row("/wt/a", TargetState::Live, 1024)], true).join("\n");
assert!(
one.starts_with("1 tracked target directory: 2.0 KiB on disk, 1.0 KiB freeable"),
"{one}"
);
assert!(one.contains("3d /wt/a/target"), "{one}");
let mut beside = row("/wt/a", TargetState::Live, 1024);
beside.path = "/wt/a/target-b".into();
beside.discovered = true;
let both = render_targets(&[row("/wt/a", TargetState::Live, 1024), beside], true).join("\n");
assert!(both.contains("/wt/a/target\n"), "{both}");
assert!(
both.contains("/wt/a/target-b (found through Git)"),
"{both}"
);
assert!(!one.contains("deleted"), "{one}");
assert!(!one.contains("next pass"), "{one}");
let rows = [
row("/wt/a", TargetState::Live, 1024),
row("/wt/b", TargetState::WorktreeDeleted, 2048),
];
let many = render_targets(&rows, true).join("\n");
assert!(
many.starts_with("2 tracked target directories: 6.0 KiB on disk, 3.0 KiB freeable"),
"{many}"
);
assert_eq!(many.matches("(worktree deleted)").count(), 1, "{many}");
assert!(
many.ends_with(
"\n1 target belongs to a deleted worktree; the daemon removes it a day after its \
last build. To remove it now: `kache clean --orphans --yes`"
),
"{many}"
);
let two = [
row("/wt/b", TargetState::WorktreeDeleted, 1),
row("/wt/c", TargetState::WorktreeDeleted, 1),
];
let rendered = render_targets(&two, true).join("\n");
assert!(
rendered.contains("removes them a day after their last build. To remove them now:"),
"{rendered}"
);
let manual = render_targets(&two, false).join("\n");
assert!(
manual.ends_with(
"2 targets belong to deleted worktrees. To remove them: `kache clean --orphans --yes`"
),
"{manual}"
);
}
#[test]
fn target_rows_report_each_worktree_and_sort_by_what_frees_most() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
let mut targets = Vec::new();
for (name, bytes) in [("small", 4096usize), ("large", 64 * 1024)] {
let workspace = dir.path().join("wt").join(name);
let target = dir.path().join("targets").join(name);
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(target.join("debug/deps")).unwrap();
let mut artifact = vec![0; bytes];
blake3::Hasher::new()
.update(name.as_bytes())
.finalize_xof()
.fill(&mut artifact);
std::fs::write(target.join("debug/deps/libx.rlib"), artifact).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
targets.push((workspace, target));
}
std::fs::remove_dir_all(&targets[1].0).unwrap();
std::fs::create_dir_all(dir.path().join("targets/gone/debug")).unwrap();
std::fs::write(
dir.path().join("targets/gone/CACHEDIR.TAG"),
CARGO_CACHEDIR_TAG,
)
.unwrap();
store
.remember_target_root(&dir.path().join("targets/gone"), &targets[0].0)
.unwrap();
assert!(
store
.tracked_target_roots(0)
.unwrap()
.iter()
.any(|tracked| { tracked.path == dir.path().join("targets/gone") })
);
std::fs::rename(
dir.path().join("targets/gone"),
dir.path().join("targets/old-gone"),
)
.unwrap();
std::fs::create_dir_all(dir.path().join("targets/gone/debug")).unwrap();
std::fs::write(
dir.path().join("targets/gone/CACHEDIR.TAG"),
CARGO_CACHEDIR_TAG,
)
.unwrap();
let now = kache_store::markers::now_epoch_secs() as i64 + 120;
let rows = target_rows(&config, now).unwrap();
assert_eq!(rows.len(), 2, "a replaced target is not listed: {rows:?}");
assert!(rows[0].path.ends_with("large") && rows[1].path.ends_with("small"));
assert_eq!(rows[0].state, TargetState::WorktreeDeleted);
assert_eq!(rows[1].state, TargetState::Live);
assert!(
rows[0].reclaimable_bytes > rows[1].reclaimable_bytes,
"{rows:?}"
);
assert!(
rows.iter().all(|row| row
.idle_seconds
.is_some_and(|secs| (120..=125).contains(&secs))),
"{rows:?}"
);
assert_eq!(rows[1].profiles, ["debug"]);
assert!(rows.iter().all(|row| row.next_pass.remove.is_none()));
assert!(
rows.iter().all(|row| row.unit_cleanup.status == "off"),
"{rows:?}"
);
#[cfg(unix)]
{
let mut pressure = config.clone();
pressure.auto_recover_min_free_bytes =
kache_fs::volume_usage(&targets[0].1).unwrap().total - 1;
pressure.auto_recover_targets = true;
let empty_workspace = dir.path().join("wt/empty");
let empty_target = dir.path().join("targets/empty");
std::fs::create_dir_all(&empty_workspace).unwrap();
std::fs::create_dir_all(empty_target.join("debug")).unwrap();
let shared_marker = dir.path().join("shared-marker");
std::fs::write(&shared_marker, CARGO_CACHEDIR_TAG).unwrap();
std::fs::hard_link(&shared_marker, empty_target.join("CACHEDIR.TAG")).unwrap();
store
.remember_target_root(&empty_target, &empty_workspace)
.unwrap();
let aged = target_rows(&pressure, now + 2 * 86_400).unwrap();
assert!(
aged.iter()
.any(|row| row.next_pass.remove == Some(crate::target_cleanup::Reason::Pressure))
);
let empty = aged
.iter()
.find(|row| row.path == empty_target.display().to_string())
.unwrap();
assert_eq!(empty.reclaimable_bytes, 0);
assert_eq!(empty.next_pass.remove, None);
}
}
#[test]
fn a_workspace_is_gone_only_when_its_parent_is_still_there() {
let root = tempfile::tempdir().unwrap();
let workspace = root.path().join("wt");
std::fs::create_dir(&workspace).unwrap();
assert!(!workspace_is_gone(&workspace));
std::fs::remove_dir(&workspace).unwrap();
assert!(workspace_is_gone(&workspace));
assert!(!workspace_is_gone(&root.path().join("offline/wt")));
}
#[test]
fn tracked_selection_takes_orphans_at_any_age() {
let stale = TrackedSelection::StaleOrOrphaned(1);
assert!(!stale.includes(false, 3599));
assert!(stale.includes(false, 3600));
assert!(stale.includes(true, 0));
assert!(!TrackedSelection::Orphaned.includes(false, i64::MAX));
assert!(TrackedSelection::Orphaned.includes(true, 0));
}
#[test]
fn a_freshly_built_target_is_offered_once_its_worktree_is_deleted() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("worktrees/feature");
let target = dir.path().join("targets/feature");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(
target.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
store.remember_target_root(&target, &workspace).unwrap();
let target = std::path::absolute(&target).unwrap();
let (live, _, _) =
tracked_target_entries(&config, TrackedSelection::StaleOrOrphaned(24), false).unwrap();
assert!(live.is_empty(), "seen just now and the worktree is there");
std::fs::remove_dir_all(&workspace).unwrap();
for selection in [
TrackedSelection::StaleOrOrphaned(24),
TrackedSelection::Orphaned,
] {
let (targets, _, orphans) = tracked_target_entries(&config, selection, false).unwrap();
assert_eq!(
targets.iter().map(|t| t.path.clone()).collect::<Vec<_>>(),
std::slice::from_ref(&target)
);
assert!(orphans.contains(&target));
}
}
#[test]
fn an_orphans_dry_run_keeps_a_missing_target_registered() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("worktrees/feature");
let target = dir.path().join("targets/feature");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(
target.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
store.remember_target_root(&target, &workspace).unwrap();
assert_eq!(store.tracked_target_roots(0).unwrap().len(), 1);
std::fs::remove_dir_all(&workspace).unwrap();
std::fs::remove_dir_all(&target).unwrap();
let orphans = CleanScope::Tracked(TrackedSelection::Orphaned);
clean(&config, true, false, false, orphans).unwrap();
assert_eq!(
store.tracked_target_roots(0).unwrap().len(),
1,
"a dry run must not forget the registry row"
);
}
#[test]
fn tracked_clean_json_previews_keep_missing_targets_registered() {
for selection in [
TrackedSelection::Orphaned,
TrackedSelection::StaleOrOrphaned(24),
] {
for (dry_run, yes, json, expected_rows) in [
(false, false, true, 1),
(true, true, true, 1),
(true, true, false, 1),
(false, true, true, 0),
(false, true, false, 0),
(false, false, false, 1),
] {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("worktrees/feature");
let target = dir.path().join("targets/feature");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
std::fs::remove_dir_all(&workspace).unwrap();
std::fs::remove_dir_all(&target).unwrap();
let scope = CleanScope::Tracked(selection);
let outcome = clean_with_terminal(&config, dry_run, yes, json, scope, false);
let selector = !dry_run && !yes && !json;
assert_eq!(outcome.is_err(), selector);
assert_eq!(
store.tracked_target_roots(0).unwrap().len(),
expected_rows,
"dry_run={dry_run}, yes={yes}, json={json}, selection={selection:?}"
);
}
}
}
#[test]
fn tracked_clean_says_when_registry_removal_is_only_a_preview() {
for missing in [true, false] {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("worktrees/feature");
let target = dir.path().join("targets/feature");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
std::fs::remove_dir_all(&workspace).unwrap();
let reason = if missing {
std::fs::remove_dir_all(&target).unwrap();
"path no longer exists"
} else {
std::fs::rename(&target, target.with_file_name("old-feature")).unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
"directory identity changed"
};
let (_, skipped, _) =
tracked_target_entries(&config, TrackedSelection::Orphaned, true).unwrap();
assert_eq!(skipped.len(), 1);
assert_eq!(
skipped[0].reason,
format!("{reason}; registry entry would be removed")
);
assert_eq!(store.tracked_target_roots(0).unwrap().len(), 1);
let (_, skipped, _) =
tracked_target_entries(&config, TrackedSelection::Orphaned, false).unwrap();
assert_eq!(skipped.len(), 1);
assert_eq!(
skipped[0].reason,
format!("{reason}; registry entry removed")
);
assert!(store.tracked_target_roots(0).unwrap().is_empty());
}
}
#[test]
fn remove_targets_refuses_a_directory_replaced_after_scan() {
let root = tempfile::tempdir().unwrap();
let target = root.path().join("proj/target");
let moved = root.path().join("proj/scanned-target");
std::fs::create_dir_all(&target).unwrap();
let scanned_identity = directory_identity(&target);
std::fs::rename(&target, &moved).unwrap();
std::fs::create_dir(&target).unwrap();
let to_remove = vec![RemovalTarget {
path: target.clone(),
scanned_identity,
estimated_reclaimable: 100,
apparent_gap: 0,
}];
let (removed, estimated_reclaimed, apparent_gap) = remove_targets(
&to_remove,
root.path(),
root.path(),
false,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
);
assert_eq!((removed, estimated_reclaimed, apparent_gap), (0, 0, 0));
assert!(
target.exists(),
"replacement directory must be left untouched"
);
assert!(
moved.exists(),
"the scanned directory was moved, not removed"
);
}
#[test]
fn clean_handle_key_cancel_and_confirm() {
use crossterm::event::KeyCode;
let mut selected = vec![true];
let mut cursor = 0usize;
assert_eq!(
clean_handle_key(KeyCode::Char('q'), &mut selected, &mut cursor, 1),
CleanStep::Cancel
);
assert_eq!(
clean_handle_key(KeyCode::Esc, &mut selected, &mut cursor, 1),
CleanStep::Cancel
);
assert_eq!(
clean_handle_key(KeyCode::Enter, &mut selected, &mut cursor, 1),
CleanStep::Confirm
);
assert_eq!(
clean_handle_key(KeyCode::Char('z'), &mut selected, &mut cursor, 1),
CleanStep::Continue
);
}
#[test]
fn clean_event_applies_press_but_ignores_release() {
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
let key_event = |kind| {
Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('q'),
KeyModifiers::NONE,
kind,
))
};
let mut selected = vec![false];
let mut cursor = 0;
assert_eq!(
clean_handle_event(
key_event(KeyEventKind::Release),
&mut selected,
&mut cursor,
1,
),
CleanStep::Continue,
"key release must not repeat the action"
);
assert_eq!(
clean_handle_event(
key_event(KeyEventKind::Repeat),
&mut selected,
&mut cursor,
1,
),
CleanStep::Continue,
"key repeat must not repeat the action"
);
assert_eq!(
clean_handle_event(
key_event(KeyEventKind::Press),
&mut selected,
&mut cursor,
1,
),
CleanStep::Cancel,
"key press must apply the action"
);
}
#[tokio::test]
async fn sync_with_client_push_skipped_when_remote_readonly() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
config.remote_readonly = true;
let store = Store::open(&config).unwrap();
let src_dir = dir.path().join("src");
std::fs::create_dir_all(&src_dir).unwrap();
let artifact = src_dir.join("libfoo.rlib");
std::fs::write(&artifact, b"artifact bytes").unwrap();
store
.put(
"pushkey123",
"foo",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"debug",
&[(artifact, "libfoo.rlib".to_string())],
"",
"",
)
.unwrap();
let remote = test_remote_cfg();
let backend = TestBackend::memory();
sync_with_client(
&as_cache_remote(as_remote_backend(&backend), &remote),
&config,
&store,
None,
false,
true,
false,
false,
None,
false,
false,
)
.await
.expect("push sync should succeed (by skipping pushes)");
assert!(backend.put_calls().is_empty());
}
#[tokio::test]
async fn save_manifest_skipped_when_remote_readonly() {
let dir = tempfile::tempdir().unwrap();
let mut config = save_manifest_config(dir.path().to_path_buf(), Some(test_remote_cfg()));
config.remote_readonly = true;
let event_log = config.event_log_path();
std::fs::create_dir_all(event_log.parent().unwrap()).unwrap();
let event = serde_json::json!({
"ts": chrono::Utc::now().to_rfc3339(),
"crate_name": "foo",
"result": "Miss",
"elapsed_ms": 100,
"compile_time_ms": 100,
"size": 10,
"cache_key": "key123"
});
let mut file = std::fs::File::create(&event_log).unwrap();
use std::io::Write;
writeln!(file, "{event}").unwrap();
save_manifest(&config, Some("mykey"), None)
.expect("save_manifest should succeed by doing nothing");
}
#[test]
fn save_manifest_warns_a_writable_job_about_a_read_only_daemon() {
let dir = tempfile::tempdir().unwrap();
let writable = save_manifest_config(dir.path().join("writable"), Some(test_remote_cfg()));
let warning =
readonly_daemon_warning(&writable, true, || Some(true)).expect("writable job warns");
assert!(warning.contains("kache daemon restart"), "{warning}");
for daemon_readonly in [Some(false), None] {
assert_eq!(
readonly_daemon_warning(&writable, true, || daemon_readonly),
None
);
}
assert_eq!(
readonly_daemon_warning(&writable, false, || panic!(
"no manifest, nothing to warn about"
)),
None
);
let mut read_only = writable.clone();
read_only.remote_readonly = true;
let local = save_manifest_config(dir.path().join("local"), None);
for config in [&read_only, &local] {
assert_eq!(
readonly_daemon_warning(config, true, || {
panic!("a job that cannot upload must not ask")
}),
None
);
}
}
#[test]
fn a_quiet_gc_the_user_asked_for_is_requested() {
use crate::store::SweepOrigin;
assert_eq!(GcMode::Cli.sweep_origin(), SweepOrigin::Requested);
assert_eq!(GcMode::CliQuiet.sweep_origin(), SweepOrigin::Requested);
assert_eq!(GcMode::Background.sweep_origin(), SweepOrigin::Automatic);
}
#[test]
fn clean_names_its_window_and_the_command_that_repeats_it() {
assert_eq!(window_label(336), "14d");
assert_eq!(window_label(24), "1d");
assert_eq!(window_label(36), "36h");
assert_eq!(window_label(0), "0h");
let default = TrackedSelection::StaleOrOrphaned(DEFAULT_TRACKED_STALE_HOURS);
assert_eq!(clean_argv(default).join(" "), "kache clean --yes");
assert_eq!(
clean_argv(TrackedSelection::StaleOrOrphaned(48)).join(" "),
"kache clean --stale 2d --yes"
);
assert_eq!(
clean_argv(TrackedSelection::Orphaned).join(" "),
"kache clean --orphans --yes"
);
assert_eq!(selection_words(TrackedSelection::Orphaned), "worktree gone");
assert_eq!(
selection_words(default),
"worktree gone, or no build in 14d"
);
assert!(can_widen(default));
assert!(can_widen(TrackedSelection::Orphaned));
assert!(!can_widen(TrackedSelection::StaleOrOrphaned(7 * 24)));
assert!(!can_widen(TrackedSelection::StaleOrOrphaned(48)));
}
#[test]
fn a_removal_summary_counts_directories_and_names_shared_bytes_only_when_there_are_some() {
assert_eq!(
removal_summary(1, 2048, 0),
"Removed 1 target directory, about 2.0 KiB freed."
);
assert_eq!(
removal_summary(2, 0, 512),
"Removed 2 target directories, about 0 B freed. Another 512 B of their size was shared, sparse, or duplicate."
);
}
#[test]
fn the_clean_preview_splits_what_goes_from_what_stays() {
let gone = row("/wt/gone", TargetState::WorktreeDeleted, 1024);
let busy = row("/wt/busy", TargetState::Live, 2048);
let empty = row("/wt/empty", TargetState::Live, 0);
let rows = vec![busy.clone(), gone.clone(), empty];
let remove: std::collections::HashSet<String> = [gone.path.clone()].into();
let skipped = vec![CleanSkipped {
path: "/wt/here/target".into(),
reason: "belongs to the current workspace".into(),
missing: false,
}];
let default = TrackedSelection::StaleOrOrphaned(DEFAULT_TRACKED_STALE_HOURS);
let plan = TrackedPlan::from_parts(
default,
rows.clone(),
&remove,
true,
test_cache_line(),
&beside_its_worktree,
);
let lines = plan_lines(&plan, &skipped, &[], PlanEnd::DryRun);
assert_eq!(
lines[0],
"Remove 1 target · 1.0 KiB freeable · worktree gone, or no build in 14d"
);
assert!(
lines[2].ends_with("/wt/gone/target (worktree deleted)"),
"{lines:#?}"
);
let keep = lines
.iter()
.position(|line| line.starts_with("Keep"))
.unwrap();
assert_eq!(lines[keep], "Keep 2 targets · 2.0 KiB freeable");
assert!(lines[keep + 2].ends_with("/wt/busy/target"), "{lines:#?}");
assert!(lines.contains(&" + 1 more with nothing to free".to_string()));
assert!(lines.contains(&" /wt/here/target belongs to the current workspace".to_string()));
assert!(lines.contains(&"Cache 3.0 MiB of 5.0 GiB · 1,234 entries · kept under its limit automatically; `kache clean --cache` empties it".to_string()));
assert_eq!(
lines[lines.len() - 2..],
[
"Dry run, nothing removed.",
"`kache clean --yes` removes the 1 target above."
]
);
let orphans = TrackedSelection::Orphaned;
let plan = TrackedPlan::from_parts(
orphans,
rows,
&Default::default(),
true,
test_cache_line(),
&beside_its_worktree,
);
let none = plan_lines(&plan, &[], &[], PlanEnd::DryRun);
assert_eq!(
none[0],
"Remove nothing: no target matches (worktree gone)"
);
assert!(!none.iter().any(|line| line == "Skipped"));
assert_eq!(
none[none.len() - 2..],
[
"Dry run, nothing removed.",
"`kache clean --stale 7d --dry-run` also selects targets no build used for 7 days."
]
);
}
fn beside_its_worktree(row: &TargetRow) -> bool {
row.path == format!("{}/target", row.workspace)
}
#[test]
fn naming_a_worktree_reaches_only_the_target_beside_its_manifest() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path().join("agent");
std::fs::create_dir_all(workspace.join("target")).unwrap();
let mut found = row(&workspace.display().to_string(), TargetState::Live, 1);
assert!(
!names_its_target(&found),
"no Cargo.toml: the scan finds nothing"
);
std::fs::write(workspace.join("Cargo.toml"), "[package]").unwrap();
assert!(names_its_target(&found));
found.path = dir.path().join("shared/target").display().to_string();
assert!(!names_its_target(&found), "a target elsewhere is not found");
}
#[test]
fn suggested_commands_read_back_as_the_same_words() {
let argv = |words: &[&str]| words.iter().map(|w| w.to_string()).collect::<Vec<_>>();
assert_eq!(
shell_line(&argv(&["kache", "clean", "/wt/agent", "--dry-run"])),
"kache clean /wt/agent --dry-run"
);
assert_eq!(
shell_line(&argv(&["kache", "clean", "/wt/agent work", "--dry-run"])),
"kache clean '/wt/agent work' --dry-run"
);
assert_eq!(
shell_line(&argv(&["it's", "~/x", ""])),
r"'it'\''s' '~/x' ''"
);
}
fn test_cache_line() -> CacheLine {
CacheLine {
bytes: 3 << 20,
limit: 5 << 30,
entries: 1_234,
}
}
#[test]
fn a_tracked_clean_reads_every_target_only_when_it_shows_them() {
assert!(builds_plan(true, 3), "a preview shows what it keeps");
assert!(
builds_plan(false, 0),
"so does a clean that selected nothing"
);
assert!(
!builds_plan(false, 3),
"a clean that removes something skips it"
);
assert!(prints_plan(false, true, 3), "a dry run prints it");
assert!(
prints_plan(false, false, 0),
"so does a clean with nothing to remove"
);
assert!(
!prints_plan(false, false, 3),
"a clean with work opens the selector"
);
assert!(!prints_plan(true, true, 0), "JSON never prints text");
let stale = TrackedSelection::StaleOrOrphaned(DEFAULT_TRACKED_STALE_HOURS);
assert_eq!(plan_selection(Some(stale), false, 0), Some(stale));
assert_eq!(plan_selection(Some(stale), true, 3), Some(stale));
assert_eq!(
plan_selection(Some(stale), false, 3),
None,
"`--yes` with work reports what it removed, not every target"
);
assert_eq!(
plan_selection(None, true, 0),
None,
"a named directory has no plan"
);
}
#[test]
fn a_clean_that_removes_nothing_shows_targets_found_through_git_and_counts_gone_entries() {
let mut found = row("/wt/agent", TargetState::Live, 4 << 30);
found.discovered = true;
found.idle_seconds = None;
let mut small = row("/wt/spike", TargetState::Live, 1 << 30);
small.discovered = true;
let mut elsewhere = row("/wt/shared", TargetState::Live, 8 << 30);
elsewhere.discovered = true;
elsewhere.path = "/targets/shared".into();
let built = row("/wt/main", TargetState::Live, 2 << 30);
let gone = |path: &str| CleanSkipped {
path: path.into(),
reason: "path no longer exists; registry entry removed".into(),
missing: true,
};
let skipped = [gone("/wt/old-a/target"), gone("/wt/old-b/target")];
let default = TrackedSelection::StaleOrOrphaned(DEFAULT_TRACKED_STALE_HOURS);
let mut empty = row("/wt/empty", TargetState::Live, 0);
empty.discovered = true;
let only_empty = clean_next_actions(
default,
false,
0,
std::slice::from_ref(&empty),
&beside_its_worktree,
);
assert!(
!only_empty
.iter()
.any(|action| action.argv.contains(&"/wt/empty".to_string())),
"{only_empty:?}"
);
let rows = vec![built, small, found, elsewhere, empty];
let plan = TrackedPlan::from_parts(
default,
rows,
&Default::default(),
false,
test_cache_line(),
&beside_its_worktree,
);
let argv: Vec<String> = plan
.next
.iter()
.map(|action| action.argv.join(" "))
.collect();
assert_eq!(
argv,
[
"kache clean --stale 7d --dry-run",
"kache clean /wt/agent --dry-run"
]
);
assert!(
plan.next[1]
.why
.contains("largest of 3 targets found through Git (13.0 GiB freeable)"),
"{:?}",
plan.next[1]
);
assert_eq!(plan.kept.len(), 5);
let lines = plan_lines(&plan, &skipped, &[], PlanEnd::NothingRemoved);
assert!(
lines
.iter()
.any(|line| line.ends_with("/wt/agent/target (found through Git)")),
"{lines:#?}"
);
assert!(
!lines.iter().any(|line| line.contains("old-a")),
"gone entries are counted, not listed: {lines:#?}"
);
assert!(
lines.contains(
&"Forgot 2 registry entries for directories that no longer exist".to_string()
)
);
assert!(!lines.iter().any(|line| line == "Skipped"));
let end = lines
.iter()
.position(|line| line == "Nothing removed.")
.unwrap();
assert_eq!(lines.len() - end - 1, plan.next.len());
for (line, action) in lines[end + 1..].iter().zip(&plan.next) {
assert_eq!(
*line,
format!("`{}` {}.", shell_line(&action.argv), action.why)
);
}
}
#[test]
fn a_tracked_clean_forgets_a_target_found_through_git_once_its_worktree_is_deleted() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("worktrees/agent");
let target = workspace.join("target");
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
store
.remember_discovered_target_root(&target, &workspace)
.unwrap();
let default = TrackedSelection::StaleOrOrphaned(DEFAULT_TRACKED_STALE_HOURS);
let (targets, skipped, _) = tracked_target_entries(&config, default, false).unwrap();
assert!(targets.is_empty() && skipped.is_empty(), "{skipped:?}");
std::fs::remove_dir_all(&workspace).unwrap();
let (targets, skipped, _) = tracked_target_entries(&config, default, true).unwrap();
assert!(targets.is_empty());
assert_eq!(skipped.len(), 1);
assert!(skipped[0].missing, "{skipped:?}");
assert_eq!(
store.tracked_target_roots(0).unwrap().len(),
1,
"a preview keeps the entry"
);
let (_, skipped, _) = tracked_target_entries(&config, default, false).unwrap();
assert!(skipped[0].missing);
assert!(store.tracked_target_roots(0).unwrap().is_empty());
}
#[test]
fn cache_cleaning_previews_without_removing_and_removes_on_yes() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
put_entry(&config, "k1", "serde", dir.path());
put_entry(&config, "k2", "serde", dir.path());
put_entry(&config, "k3", "tokio", dir.path());
let entries = || Store::open(&config).unwrap().entry_count().unwrap();
clean_cache(
&config,
CacheClean::Crate("serde".into()),
true,
false,
false,
)
.unwrap();
clean_cache(&config, CacheClean::All, true, false, false).unwrap();
clean_cache(&config, CacheClean::OlderThan(24), true, false, false).unwrap();
clean_cache(&config, CacheClean::StaleSchema, true, false, false).unwrap();
assert_eq!(entries(), 3, "a dry run removes nothing");
assert!(clean_cache(&config, CacheClean::All, false, true, true).is_err());
assert_eq!(entries(), 3, "JSON is refused before anything is removed");
clean_cache(
&config,
CacheClean::Crate("missing".into()),
false,
true,
false,
)
.unwrap();
clean_cache(
&config,
CacheClean::Crate("serde".into()),
false,
true,
false,
)
.unwrap();
assert_eq!(entries(), 1, "only serde's entries go");
clean_cache(&config, CacheClean::All, false, true, false).unwrap();
assert_eq!(entries(), 0);
}
#[test]
fn a_cache_preview_says_what_each_clean_would_do() {
assert_eq!(
cache_preview(&CacheClean::OlderThan(48), 3, 2048),
"Cache is 2.0 KiB in 3 entries. This would evict the entries older than 2d."
);
assert!(cache_preview(&CacheClean::StaleSchema, 3, 2048).contains("not keyed by schema"));
assert_eq!(
cache_preview(&CacheClean::All, 1, 1024),
"Would remove all 1 cache entry (1.0 KiB)."
);
assert_eq!(
cache_preview(&CacheClean::Crate("serde".into()), 2, 1024),
"Would remove 2 cache entries for 'serde' (1.0 KiB)."
);
assert_eq!(
cache_preview(&CacheClean::Crate("serde".into()), 0, 0),
"No cache entries for 'serde'."
);
}
#[test]
fn crate_entries_counts_only_that_crate() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
put_entry(&config, "k1", "serde", dir.path());
put_entry(&config, "k2", "serde", dir.path());
put_entry(&config, "k3", "tokio", dir.path());
let store = Store::open(&config).unwrap();
let (serde, serde_bytes) = crate_entries(&store, "serde").unwrap();
assert_eq!(serde, 2);
assert!(serde_bytes > 0);
assert_eq!(crate_entries(&store, "tokio").unwrap().0, 1);
assert_eq!(crate_entries(&store, "missing").unwrap(), (0, 0));
}
#[test]
fn clean_yes_removes_a_selected_tracked_target() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let workspace = dir.path().join("ws");
let target = workspace.join("target");
std::fs::create_dir_all(target.join("debug/deps")).unwrap();
std::fs::write(workspace.join("Cargo.toml"), "[package]\nname = \"p\"\n").unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::fs::write(target.join("debug/deps/libp.rlib"), vec![1; 4096]).unwrap();
Store::open(&config)
.unwrap()
.remember_target_root(&target, &workspace)
.unwrap();
let every = CleanScope::Tracked(TrackedSelection::StaleOrOrphaned(0));
clean(&config, true, true, false, every.clone()).unwrap();
assert!(target.exists(), "a dry run removes nothing");
clean(&config, false, true, false, every).unwrap();
assert!(!target.exists(), "--yes removes what was selected");
}
#[test]
fn removing_a_scanned_target_forgets_its_registry_row() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let workspace = dir.path().join("ws");
let target = workspace.join("target");
std::fs::create_dir_all(target.join("debug/deps")).unwrap();
std::fs::write(workspace.join("Cargo.toml"), "[package]\nname = \"p\"\n").unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::fs::write(target.join("debug/deps/libp.rlib"), vec![1; 4096]).unwrap();
let store = Store::open(&config).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
clean_with_terminal(
&config,
false,
true,
true,
CleanScope::Scan(workspace),
false,
)
.unwrap();
assert!(!target.exists());
assert!(store.tracked_target_roots(0).unwrap().is_empty());
}
#[cfg(unix)]
#[test]
fn a_failed_removal_fails_the_clean() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipping: running as root, mode 0555 does not deny removal");
return;
}
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let workspace = dir.path().join("worktrees/feature");
let target = dir.path().join("targets/feature");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
let locked = target.join("debug/build");
std::fs::create_dir_all(&locked).unwrap();
std::fs::write(locked.join("out.rlib"), b"rlib").unwrap();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o555)).unwrap();
let store = Store::open(&config).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
std::fs::remove_dir_all(&workspace).unwrap();
let orphans = CleanScope::Tracked(TrackedSelection::Orphaned);
let error = clean_with_terminal(&config, false, true, true, orphans, false).unwrap_err();
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(error.downcast_ref::<crate::machine::Reported>().is_some());
assert_eq!(error.to_string(), "could not remove 1 target directory");
assert_eq!(
store.tracked_target_roots(0).unwrap().len(),
1,
"what is left on disk stays registered"
);
}
#[cfg(unix)]
#[test]
fn only_a_missing_path_counts_as_removed() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipping: running as root, mode 0000 does not deny lookups");
return;
}
let dir = tempfile::tempdir().unwrap();
let sealed = dir.path().join("sealed");
std::fs::create_dir_all(sealed.join("target")).unwrap();
assert!(!path_was_removed(&sealed.join("target")));
assert!(path_was_removed(&sealed.join("gone")));
std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o000)).unwrap();
let unreadable = path_was_removed(&sealed.join("target"));
std::fs::set_permissions(&sealed, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(!unreadable, "a permission error is not a removal");
}
#[test]
fn a_declined_unit_clean_says_nothing_was_removed() {
assert_eq!(unit_clean_summary(true, None, 0, 0), None);
assert_eq!(
unit_clean_summary(false, Some(false), 0, 0).as_deref(),
Some("Nothing removed.")
);
for answer in [Some(true), None] {
assert_eq!(
unit_clean_summary(false, answer, 3, 2048).as_deref(),
Some("Removed 3 obsolete units (2.0 KiB).")
);
}
assert_eq!(
unit_clean_summary(false, Some(true), 1, 1024).as_deref(),
Some("Removed 1 obsolete unit (1.0 KiB).")
);
}
#[test]
fn command_lines_quote_for_the_platform_shell() {
let argv = |words: &[&str]| words.iter().map(|w| w.to_string()).collect::<Vec<_>>();
let windows_path = argv(&["kache", "clean", r"C:\Users\RUNNER~1\Temp\.tmp1", "--yes"]);
assert_eq!(
command_line(&windows_path, true),
r"kache clean C:\Users\RUNNER~1\Temp\.tmp1 --yes"
);
let spaced = argv(&["kache", "clean", r"C:\My Dir", r#"say "hi""#]);
assert_eq!(
command_line(&spaced, true),
r#"kache clean "C:\My Dir" "say \"hi\"""#
);
assert_eq!(command_line(&argv(&["kache", ""]), true), r#"kache """#);
assert_eq!(
command_line(&argv(&["kache", "clean", "/tmp/my dir"]), false),
"kache clean '/tmp/my dir'"
);
}
#[test]
fn no_terminal_names_the_same_command_with_yes() {
let args = ["clean".to_string(), "/tmp/my dir".to_string()];
let quoted = if cfg!(windows) {
"\"/tmp/my dir\""
} else {
"'/tmp/my dir'"
};
assert_eq!(
no_terminal("nothing removed", &args, true).to_string(),
format!(
"nothing removed: there is no terminal to ask. Run `kache clean {quoted} --yes` \
to go ahead without asking, or --dry-run to preview."
)
);
assert_eq!(
no_terminal("init changed nothing", &["init".to_string()], false).to_string(),
"init changed nothing: there is no terminal to ask. Run `kache init --yes` to go ahead \
without asking."
);
}
#[test]
fn removal_asks_only_at_a_terminal() {
let never = |_: &str| -> Result<bool> { panic!("asked without a terminal") };
assert!(confirm_removal_with("Remove?", true, false, never).unwrap());
let refused = confirm_removal_with("Remove?", false, false, never).unwrap_err();
assert!(refused.to_string().contains("--yes"), "{refused}");
assert!(confirm_removal_with("Remove?", false, true, |_| Ok(true)).unwrap());
assert!(!confirm_removal_with("Remove?", false, true, |_| Ok(false)).unwrap());
}
#[test]
fn a_cache_clean_decides_from_what_it_counted() {
let serde = || CacheClean::Crate("serde".into());
assert_eq!(
cache_action(serde(), 2, 1024, true, false).unwrap(),
CacheAction::Say("Would remove 2 cache entries for 'serde' (1.0 KiB).".into())
);
assert_eq!(
cache_action(serde(), 0, 0, false, false).unwrap(),
CacheAction::Say("No cache entries for 'serde'.".into())
);
assert_eq!(
cache_action(serde(), 2, 1024, false, false).unwrap(),
CacheAction::Purge {
crate_name: Some("serde".into()),
question: "Remove 2 cache entries for 'serde' (1.0 KiB)?".into(),
}
);
assert_eq!(
cache_action(CacheClean::All, 3, 2048, false, false).unwrap(),
CacheAction::Purge {
crate_name: None,
question: "Remove all 3 cache entries (2.0 KiB)?".into(),
}
);
assert!(cache_action(CacheClean::All, 3, 2048, false, true).is_err());
assert!(cache_action(serde(), 2, 1024, false, true).is_err());
assert_eq!(
cache_action(CacheClean::OlderThan(24), 3, 0, false, true).unwrap(),
CacheAction::Gc {
max_age_hours: Some(24),
stale_schema: false
}
);
assert_eq!(
cache_action(CacheClean::StaleSchema, 3, 0, false, false).unwrap(),
CacheAction::Gc {
max_age_hours: None,
stale_schema: true
}
);
}
#[test]
fn cache_counts_cover_one_crate_or_the_whole_cache() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
put_entry(&config, "k1", "serde", dir.path());
put_entry(&config, "k2", "tokio", dir.path());
let store = Store::open(&config).unwrap();
assert_eq!(
cache_counts(&store, &CacheClean::Crate("serde".into()))
.unwrap()
.0,
1
);
assert_eq!(cache_counts(&store, &CacheClean::All).unwrap().0, 2);
assert!(cache_counts(&store, &CacheClean::All).unwrap().1 > 0);
}
#[test]
fn checkout_check_wording() {
let clean = checkout_check("/w/b", 0, None);
assert!(clean.pass);
assert_eq!(clean.fix, None);
let one = checkout_check("/w/b", 1, Some("/w/a"));
assert!(!one.pass);
assert_eq!(one.label, OTHER_CHECKOUTS);
assert_eq!(one.detail, "1 miss in /w/b differs from the build in /w/a");
assert!(one.fix.as_deref().unwrap().contains("kache explain"));
let many = checkout_check("/w/b", 3, None);
assert_eq!(
many.detail,
"3 misses in /w/b differ from the build in other checkouts"
);
}
#[test]
fn doctor_reads_cross_checkout_misses_from_the_event_log() {
use crate::events::{BuildEvent, EventResult};
let dir = tempfile::tempdir().unwrap();
let log = dir.path().join("events.jsonl");
assert!(doctor_checkout_check(&log).is_none());
let built = |root: &str, session: &str, sources: &str| {
let mut e = BuildEvent::new_for_test("one", EventResult::Miss);
e.root = root.to_string();
e.session_id = session.to_string();
e.unit_id = "uone".to_string();
e.key_fields = [("sources".to_string(), sources.to_string())]
.into_iter()
.collect();
e.key_externs_recorded = true;
e
};
for event in [built("/first", "a", "1111"), built("/second", "b", "2222")] {
crate::events::log_event(&log, &event).unwrap();
}
let check = doctor_checkout_check(&log).unwrap();
assert!(!check.pass);
assert_eq!(
check.detail,
"1 miss in /second differs from the build in /first"
);
}
#[test]
fn doctor_and_full_clean_report_quarantined_indexes_without_deleting_them() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let absent = doctor_index_quarantines(&config);
assert!(absent.pass);
assert_eq!(absent.detail, "no retained corrupt index files");
assert!(absent.fix.is_none());
assert!(
cache_quarantine_notice(&config, &CacheClean::All)
.unwrap()
.is_none()
);
fs::create_dir_all(&config.cache_dir).unwrap();
for (name, bytes) in [
("index.db.corrupt-123-456", 1000),
("index.db.corrupt-123-456-wal", 2000),
] {
fs::write(config.cache_dir.join(name), vec![0; bytes]).unwrap();
}
let check = doctor_index_quarantines(&config);
assert!(!check.pass);
assert_eq!(check.label, "Store quarantine");
assert_eq!(doctor_section(check.label), "Storage");
assert_eq!(
check.detail,
format!(
"2 files ({}) retained in {} after index corruption",
ByteSize(3000),
term::home_path(&config.cache_dir)
)
);
assert!(check.fix.unwrap().contains("`kache clean --cache`"));
let notice = cache_quarantine_notice(&config, &CacheClean::All)
.unwrap()
.unwrap();
assert_eq!(
notice,
format!(
"Full cache removal also deletes 2 quarantined index files ({}). Save these copies first if needed for investigation.",
term::bytes(3000)
)
);
for what in [
CacheClean::Crate("serde".to_string()),
CacheClean::OlderThan(24),
CacheClean::StaleSchema,
] {
assert!(cache_quarantine_notice(&config, &what).unwrap().is_none());
}
assert_eq!(
crate::store::quarantined_index_files(&config.index_db_path())
.unwrap()
.len(),
2
);
}
#[test]
fn doctor_quarantine_inspection_reports_unreadable_cache_directory() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
fs::write(&config.cache_dir, b"not a directory").unwrap();
let check = doctor_index_quarantines(&config);
assert!(!check.pass);
assert!(
check
.detail
.starts_with("could not inspect retained corrupt index files: ")
);
assert_eq!(
check.fix,
Some(format!("ensure {} is readable", config.cache_dir.display()))
);
assert!(cache_quarantine_notice(&config, &CacheClean::All).is_err());
}
#[test]
fn unit_cleanup_preview_keeps_json_without_yes_and_all_dry_runs_read_only() {
assert!(unit_preview(true, true, false));
assert!(unit_preview(true, true, true));
assert!(unit_preview(false, false, true));
assert!(!unit_preview(false, true, true));
assert!(!unit_preview(false, false, false));
}
#[test]
fn unit_cleanup_selects_only_matching_intact_live_targets() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::fs::write(workspace.join("Cargo.toml"), "[workspace]\n").unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
let root = store.tracked_target_roots(0).unwrap().pop().unwrap();
assert!(unit_target_matches(&root, None));
assert!(unit_target_matches(&root, Some(&target)));
assert!(unit_target_matches(&root, Some(&workspace)));
assert!(!unit_target_matches(&root, Some(dir.path())));
assert!(unit_target_intact(&root));
let missing_workspace = crate::store::TrackedTargetRoot {
workspace_root: dir.path().join("missing-workspace"),
..root.clone()
};
assert!(!unit_target_intact(&missing_workspace));
std::fs::remove_file(target.join("CACHEDIR.TAG")).unwrap();
assert!(
!unit_target_intact(&root),
"missing Cargo markers must be excluded"
);
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::fs::rename(&target, workspace.join("old-target")).unwrap();
std::fs::create_dir_all(&target).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
assert!(
!unit_target_intact(&root),
"a replaced directory must be excluded"
);
std::fs::remove_dir_all(&workspace).unwrap();
assert!(!unit_target_intact(&root));
}
#[test]
fn unit_cleanup_rendering_explains_missing_evidence_and_ready_counts() {
let ready = crate::target_liveness::Plan {
status: "ready".into(),
units: 2,
bytes: 4096,
protected: 3,
unknown: 4,
command: vec!["check".into()],
};
let summary = render_unit_plan("target", &ready);
assert!(summary.contains("2 obsolete units"));
assert!(summary.contains("3 live and 4 unknown units kept"));
assert!(unit_cleanup_note(&ready).contains("2 obsolete units"));
let one = crate::target_liveness::Plan {
units: 1,
..ready.clone()
};
assert!(render_unit_plan("target", &one).contains("1 obsolete unit ("));
assert_eq!(
unit_cleanup_note(&one),
" (1 obsolete unit: `kache clean --units --dry-run`)"
);
let empty = crate::target_liveness::Plan { units: 0, ..ready };
assert!(unit_cleanup_note(&empty).is_empty());
let missing = crate::target_liveness::Plan {
status: "receipt unavailable".into(),
..Default::default()
};
let summary = render_unit_plan("target", &missing);
assert!(summary.contains("everything kept"));
assert!(summary.contains("KACHE_TARGET_LIVENESS=1 kache cargo check"));
assert!(unit_cleanup_note(&missing).contains("receipt unavailable"));
}
#[test]
fn clean_units_without_a_receipt_keeps_files_in_preview_and_confirmed_modes() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
let artifact = target.join("debug/deps/libold-deadbeef.rlib");
std::fs::create_dir_all(artifact.parent().unwrap()).unwrap();
std::fs::write(&artifact, "still needed until proven obsolete").unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::fs::write(workspace.join("Cargo.toml"), "[workspace]\n").unwrap();
let mut config = crate::test_support::test_config(dir.path().join("cache"));
config.target_liveness = true;
let store = Store::open(&config).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
for (dry_run, yes, json) in [
(true, true, false),
(false, false, true),
(false, true, true),
] {
clean_units(&config, Some(target.clone()), dry_run, yes, json).unwrap();
assert_eq!(
std::fs::read_to_string(&artifact).unwrap(),
"still needed until proven obsolete"
);
assert_eq!(store.tracked_target_roots(0).unwrap().len(), 1);
}
clean_units(&config, Some(workspace), true, false, false).unwrap();
clean_units(&config, Some(dir.path().to_path_buf()), true, false, false).unwrap();
assert!(
clean_units(
&config,
Some(dir.path().join("missing")),
true,
false,
false
)
.is_err()
);
}
#[test]
fn unit_cleanup_excludes_other_workspaces_and_replaced_selected_targets() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
let first_workspace = dir.path().join("first");
let first_target = first_workspace.join("target");
let other_workspace = dir.path().join("other");
let other_target = other_workspace.join("target");
for (workspace, target) in [
(&first_workspace, &first_target),
(&other_workspace, &other_target),
] {
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::fs::write(workspace.join("Cargo.toml"), "[workspace]\n").unwrap();
store.remember_target_root(target, workspace).unwrap();
}
let roots = store.tracked_target_roots(0).unwrap();
assert_eq!(unit_cleanup_roots(roots.clone(), None).len(), 2);
for selected in [&first_target, &first_workspace] {
let selected_roots = unit_cleanup_roots(roots.clone(), Some(selected));
assert_eq!(
selected_roots.len(),
1,
"an intact target in another workspace must remain excluded"
);
assert_eq!(selected_roots[0].path, first_target);
}
assert!(unit_cleanup_roots(roots.clone(), Some(dir.path())).is_empty());
std::fs::rename(&first_target, first_workspace.join("retained-old-target")).unwrap();
std::fs::create_dir_all(first_target.join("debug")).unwrap();
std::fs::write(first_target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
assert!(unit_cleanup_roots(roots.clone(), Some(&first_target)).is_empty());
let retained = unit_cleanup_roots(roots, None);
assert_eq!(retained.len(), 1);
assert_eq!(retained[0].path, other_target);
}
#[test]
fn unit_cleanup_human_messages_do_not_leak_into_json_or_claim_preview_removal() {
let dir = tempfile::tempdir().unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
for (dry_run, yes, json, expected_count, removal_summary) in [
(true, true, true, 0, false),
(false, false, true, 0, false),
(false, true, true, 0, false),
(true, true, false, 1, false),
(false, true, false, 2, true),
] {
let mut messages = Vec::new();
clean_units_with_output(&config, None, dry_run, yes, json, &mut |line| {
messages.push(line)
})
.unwrap();
assert_eq!(
messages.len(),
expected_count,
"dry_run={dry_run} yes={yes} json={json}"
);
assert_eq!(
messages.iter().any(|line| line.starts_with("Removed ")),
removal_summary
);
if !json {
assert!(messages[0].starts_with("No intact tracked target matches."));
}
}
}
#[test]
fn unit_cleanup_requires_both_ready_evidence_and_current_target_identity() {
let dir = tempfile::tempdir().unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
std::fs::write(workspace.join("Cargo.toml"), "[workspace]\n").unwrap();
let config = crate::test_support::test_config(dir.path().join("cache"));
let store = Store::open(&config).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
let root = store.tracked_target_roots(0).unwrap().pop().unwrap();
let ready = crate::target_liveness::Plan {
status: "ready".into(),
units: 1,
..Default::default()
};
let unavailable = crate::target_liveness::Plan {
status: "receipt unavailable".into(),
units: 1,
..Default::default()
};
assert!(unit_cleanup_ready(&root, &ready));
assert!(!unit_cleanup_ready(&root, &unavailable));
std::fs::rename(&target, workspace.join("retained-old-target")).unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(target.join("CACHEDIR.TAG"), CARGO_CACHEDIR_TAG).unwrap();
assert!(!unit_cleanup_ready(&root, &ready));
assert!(!unit_cleanup_ready(&root, &unavailable));
}
#[test]
fn unit_cleanup_confirmation_requires_candidates_and_nonpreview_mode() {
for (preview, units, expected) in [
(false, 0, false),
(false, 1, true),
(true, 0, false),
(true, 1, false),
] {
assert_eq!(
unit_cleanup_needs_confirmation(preview, units),
expected,
"preview={preview} units={units}"
);
}
}
#[test]
fn init_adds_target_recovery_without_rewriting_the_config() {
assert_eq!(
enable_target_recovery("").as_deref(),
Some("[cache]\nauto_recover_targets = true\n")
);
let commented = "# my settings\n[cache]\nlocal_max_size = \"50G\" # keep\n";
assert_eq!(
enable_target_recovery(commented).as_deref(),
Some(
"# my settings\n[cache]\nauto_recover_targets = true\nlocal_max_size = \"50G\" # keep\n"
)
);
assert_eq!(
enable_target_recovery("[remote]\nbucket = \"b\"").as_deref(),
Some("[remote]\nbucket = \"b\"\n\n[cache]\nauto_recover_targets = true\n")
);
assert_eq!(
enable_target_recovery("cache.local_max_size = \"5G\"\n"),
None
);
assert_eq!(enable_target_recovery("[cache] # mine\n"), None);
assert!(!sets_target_recovery(""));
assert!(!sets_target_recovery("[cache]\nlocal_max_size = \"5G\"\n"));
assert!(sets_target_recovery(
"[cache]\nauto_recover_targets = false\n"
));
assert!(sets_target_recovery("cache.auto_recover_targets = true\n"));
}
#[test]
fn init_offers_recovery_with_the_floor_its_file_and_environment_set() {
let none = |_: &str| None;
let defaults = RecoverySettings::of("", &none, false);
assert_eq!(defaults.floor, crate::config::DEFAULT_RECOVERY_FLOOR);
assert_eq!(defaults.targets_from_env, None);
assert!(!defaults.file_decides_targets);
assert_eq!(RecoverySettings::of("", &none, true).floor, 0, "CI default");
let file = "[cache]\nauto_recover_min_free_bytes = 107374182400\n";
assert_eq!(RecoverySettings::of(file, &none, false).floor, 100 << 30);
let env = |name: &str| match name {
"KACHE_AUTO_RECOVER_MIN_FREE_BYTES" => Some("5368709120".to_string()),
"KACHE_AUTO_RECOVER_TARGETS" => Some("0".to_string()),
_ => None,
};
let overridden = RecoverySettings::of(file, &env, false);
assert_eq!(overridden.floor, 5 << 30, "the environment wins");
assert_eq!(overridden.targets_from_env, Some(false));
for (value, on) in [("1", true), ("TRUE", true), ("yes", false)] {
let env =
move |name: &str| (name == "KACHE_AUTO_RECOVER_TARGETS").then(|| value.to_string());
assert_eq!(
RecoverySettings::of("", &env, false).targets_from_env,
Some(on),
"{value}"
);
}
let ignored = RecoverySettings::of(&format!("{file}ignore_env = true\n"), &env, false);
assert_eq!(ignored.floor, 100 << 30, "unless the file ignores it");
assert_eq!(ignored.targets_from_env, None);
assert!(
RecoverySettings::of("[cache]\nauto_recover_targets = false\n", &none, false)
.file_decides_targets
);
}
#[test]
fn stats_shows_the_recovery_the_daemon_runs() {
let dir = tempfile::tempdir().unwrap();
let mut config = crate::test_support::test_config(dir.path().to_path_buf());
config.auto_recover_min_free_bytes = 1;
config.auto_recover_targets = false;
assert_eq!(recovery_in_effect(&config, None), (1, false));
let mut daemon = effective_config_like(&config);
daemon.auto_recover_min_free_bytes = Some(7);
daemon.auto_recover_targets = Some(true);
assert_eq!(recovery_in_effect(&config, Some(&daemon)), (7, true));
daemon.auto_recover_targets = None;
assert_eq!(recovery_in_effect(&config, Some(&daemon)), (1, false));
}
#[test]
fn init_asks_only_when_nothing_else_decides() {
let settings = |floor, env, file| RecoverySettings {
floor,
targets_from_env: env,
file_decides_targets: file,
};
let gib = 1u64 << 30;
assert_eq!(
recovery_offer(&settings(gib, None, false), false),
RecoveryOffer::Ask(gib)
);
assert_eq!(
recovery_offer(&settings(gib, None, false), true),
RecoveryOffer::Explain(gib)
);
assert_eq!(
recovery_offer(&settings(gib, None, true), false),
RecoveryOffer::Nothing
);
assert_eq!(
recovery_offer(&settings(0, None, false), false),
RecoveryOffer::Nothing
);
assert_eq!(
recovery_offer(&settings(gib, Some(true), true), false),
RecoveryOffer::FromEnv(true),
"the environment overrides the file"
);
assert_eq!(
recovery_offer(&settings(0, Some(false), false), true),
RecoveryOffer::FromEnv(false)
);
}
#[cfg(unix)]
#[test]
fn init_saves_target_recovery_safely() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("kache/config.toml");
assert_eq!(read_config_text(&path).unwrap(), "", "no file yet");
assert!(save_target_recovery(&path, "").unwrap());
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"[cache]\nauto_recover_targets = true\n"
);
let text = "[cache]\nlocal_max_size = \"5G\"\n";
std::fs::write(&path, text).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(save_target_recovery(&path, text).unwrap());
assert!(
std::fs::read_to_string(&path)
.unwrap()
.contains("auto_recover_targets = true")
);
assert_eq!(
std::fs::metadata(&path).unwrap().permissions().mode() & 0o777,
0o600
);
std::fs::write(&path, "[cache]\nlocal_max_size = \"9G\"\n").unwrap();
assert!(!save_target_recovery(&path, text).unwrap());
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"[cache]\nlocal_max_size = \"9G\"\n"
);
std::fs::write(&path, "cache.local_max_size = \"5G\"\n").unwrap();
assert!(!save_target_recovery(&path, "cache.local_max_size = \"5G\"\n").unwrap());
assert!(read_config_text(dir.path()).is_err());
}
#[test]
fn doctor_checks_the_cargo_wrapper_once_and_names_init() {
use crate::wrapper_config::WrapperSetting;
let config = std::path::Path::new("/home/me/.cargo/config.toml");
let (pass, detail, fix) = wrapper_check(None, Some(false), config);
assert!(!pass);
assert_eq!(detail, "not set");
let fix = fix.unwrap();
assert!(fix.contains("kache init"), "{fix}");
assert!(fix.contains("config.toml"), "{fix}");
let env = |value: &str| WrapperSetting::Environment {
value: value.into(),
};
assert_eq!(
wrapper_check(Some(&env("/bin/kache")), None, config),
(true, "kache via env".to_string(), None)
);
let (pass, detail, fix) = wrapper_check(Some(&env("sccache")), None, config);
assert!(!pass);
assert_eq!(detail, "sccache (sccache)");
assert!(fix.unwrap().contains("export RUSTC_WRAPPER=kache"));
let (_, detail, _) = wrapper_check(Some(&env("other")), None, config);
assert_eq!(detail, "other (not kache)");
let file = |value: &str| WrapperSetting::CargoConfig {
value: value.into(),
path: config.to_path_buf(),
};
let (pass, detail, fix) = wrapper_check(Some(&file("kache")), Some(false), config);
assert!(pass, "{detail}");
assert!(detail.starts_with("kache via "), "{detail}");
assert!(fix.is_none());
let fix_for = |repair| {
wrapper_check(Some(&file("sccache")), repair, config)
.2
.unwrap()
};
assert!(fix_for(Some(true)).contains("kache doctor --fix"));
assert!(fix_for(Some(true)).contains("kache init"));
assert!(!fix_for(Some(false)).contains("--fix"));
assert!(fix_for(Some(false)).contains("kache init"));
assert!(fix_for(None).starts_with("replace `rustc-wrapper = \"sccache\"`"));
}
#[test]
fn doctor_suggests_the_running_binary_directory_for_path() {
let dir = std::path::Path::new("/opt/kache/bin");
assert_eq!(binary_fix(true, Some(dir)), None);
assert_eq!(
binary_fix(false, Some(dir)).as_deref(),
Some("add /opt/kache/bin to PATH")
);
let fallback = binary_fix(false, None).unwrap();
assert!(!fallback.contains("cargo install"), "{fallback}");
}
#[test]
fn doctor_lists_volume_stores_under_storage_and_only_live_daemon_labels() {
assert_eq!(doctor_section("Volume store"), "Storage");
for label in DAEMON_CHECK_LABELS {
assert_eq!(doctor_section(label), "Services", "{label}");
}
}
#[test]
fn init_preview_ends_with_the_flags_given_or_nothing_to_change() {
assert_eq!(
preview_end(true, false, false, false),
"Preview only. Run kache init to apply."
);
assert_eq!(
preview_end(true, true, true, true),
"Preview only. Run kache init --no-service --no-shell --yes to apply."
);
assert_eq!(
preview_end(true, false, true, false),
"Preview only. Run kache init --no-shell to apply."
);
assert_eq!(preview_end(false, true, false, false), "Nothing to change.");
}
#[test]
fn init_steps_follow_the_plans() {
assert_eq!(edit_action(false, false), "keep");
assert_eq!(edit_action(true, false), "configure");
assert_eq!(edit_action(false, true), "configure");
for action in ["configure", "update"] {
assert!(step_changes(action), "{action}");
}
for action in [
"keep",
"unsupported",
"unavailable",
"manual",
"ensure_running",
"create",
] {
assert!(!step_changes(action), "{action}");
}
assert_eq!(rustdoc_action(RustdocEdit::Write), Some("configure"));
assert_eq!(rustdoc_action(RustdocEdit::Already), Some("keep"));
assert_eq!(rustdoc_action(RustdocEdit::Leave), Some("keep"));
assert_eq!(rustdoc_action(RustdocEdit::Skip), None);
assert_eq!(service_action(true, true, true, true), None);
assert_eq!(
service_action(false, false, true, true),
Some("unavailable")
);
assert_eq!(service_action(false, true, true, true), Some("update"));
assert_eq!(service_action(false, true, true, false), Some("keep"));
assert_eq!(service_action(false, true, false, false), Some("configure"));
}
#[test]
fn init_never_backs_up_a_file_it_created() {
let mut created = Vec::new();
let config = std::path::Path::new("/home/me/.cargo/config.toml");
let rc = std::path::Path::new("/home/me/.bashrc");
assert!(backup_step(&mut created, config, false).is_none());
assert!(backup_step(&mut created, config, true).is_none());
assert!(backup_step(&mut created, config, false).is_none());
assert_eq!(created, [config]);
for _ in 0..2 {
let step = backup_step(&mut created, rc, true).unwrap();
assert_eq!((step.component, step.action), ("backup", "create"));
assert_eq!(step.paths, [rc]);
}
assert_eq!(created, [config]);
}
#[test]
fn init_steps_list_each_edit_with_the_backup_before_it() {
use std::path::PathBuf;
let cargo = PathBuf::from("/home/me/.cargo/config.toml");
let rc = PathBuf::from("/home/me/.bashrc");
let facts = |cargo_ready: bool, cargo_exists: bool, rc_exists: bool| SetupFacts {
cargo_path: cargo.clone(),
cargo: if cargo_ready { "keep" } else { "configure" },
cargo_exists,
shell: Some(SetupStep::new("shell", "configure", vec![rc.clone()])),
shell_changes: vec![(rc.clone(), rc_exists)],
rustdoc: Some("configure"),
test_runner: Some(SetupStep::new("test_runner", "configure", vec![rc.clone()])),
test_changes: vec![(rc.clone(), rc_exists)],
recovery: "keep",
recovery_path: PathBuf::from("/home/me/.config/kache/config.toml"),
service: Some("keep"),
service_path: Some(PathBuf::from("/svc")),
};
let summary = |steps: Vec<SetupStep>| {
steps
.into_iter()
.map(|step| {
let path = step
.paths
.first()
.map(|path| path.display().to_string())
.unwrap_or_default();
format!("{} {} {path}", step.component, step.action)
})
.collect::<Vec<_>>()
};
assert_eq!(
summary(setup_steps(&facts(false, false, false))),
[
"cargo configure /home/me/.cargo/config.toml",
"shell configure /home/me/.bashrc",
"rustdoc configure /home/me/.cargo/config.toml",
"test_runner configure /home/me/.bashrc",
"free_space_recovery keep /home/me/.config/kache/config.toml",
"login_service keep /svc",
"daemon ensure_running ",
]
);
assert_eq!(
summary(setup_steps(&facts(false, true, true))),
[
"cargo configure /home/me/.cargo/config.toml",
"backup create /home/me/.cargo/config.toml",
"shell configure /home/me/.bashrc",
"backup create /home/me/.bashrc",
"rustdoc configure /home/me/.cargo/config.toml",
"backup create /home/me/.cargo/config.toml",
"test_runner configure /home/me/.bashrc",
"backup create /home/me/.bashrc",
"free_space_recovery keep /home/me/.config/kache/config.toml",
"login_service keep /svc",
"daemon ensure_running ",
]
);
let kept = SetupFacts {
shell: None,
shell_changes: Vec::new(),
rustdoc: Some("keep"),
test_runner: None,
test_changes: Vec::new(),
recovery: "ask",
service: None,
..facts(true, true, true)
};
assert_eq!(
summary(setup_steps(&kept)),
[
"cargo keep /home/me/.cargo/config.toml",
"rustdoc keep /home/me/.cargo/config.toml",
"free_space_recovery ask /home/me/.config/kache/config.toml",
"daemon ensure_running ",
]
);
}
fn explain_event(result: events::EventResult, secs: i64, key: &str) -> events::BuildEvent {
let mut event = events::BuildEvent::new_for_test("app", result);
event.ts = chrono::DateTime::from_timestamp(1_760_454_000 + secs, 0).unwrap();
event.cache_key = key.to_string();
event.elapsed_ms = 1_989;
event
}
#[test]
fn the_newest_compile_decides_whether_a_crate_is_cached() {
use events::EventResult;
let miss = explain_event(EventResult::Miss, 1, "missmissmissmiss");
let mut seed = explain_event(EventResult::Passthrough, 2, "seedseedseedseed");
seed.passthrough_reason = "adaptive seed".to_string();
let mut probe = explain_event(EventResult::Passthrough, 3, "");
probe.passthrough_reason = "not-a-compile|--print cfg".to_string();
let error = explain_event(EventResult::Error, 4, "");
let skipped = explain_event(EventResult::Skipped, 5, "");
let hit = explain_event(EventResult::LocalHit, 6, "hithithithithit");
let latest = |events: &[&events::BuildEvent]| {
latest_uncached_compile(events).map(|event| event.cache_key.clone())
};
assert_eq!(
latest(&[&miss, &seed, &probe, &error, &skipped]).as_deref(),
Some("seedseedseedseed")
);
assert_eq!(latest(&[&miss, &seed, &hit]), None);
assert_eq!(latest(&[&seed, &miss]), None);
assert_eq!(latest(&[&probe]), None);
assert_eq!(latest(&[]), None);
let lines = uncached_compile_lines("app", &seed, Some(&miss), &[&miss, &seed]);
assert_eq!(lines[0], "kache · explain · app", "{lines:?}");
assert_eq!(lines[2], "`app` was compiled, not cached.", "{lines:?}");
assert_eq!(lines[4], "Latest compile", "{lines:?}");
assert!(
lines.contains(&format!(" When {}", term::local_datetime(seed.ts))),
"{lines:?}"
);
assert!(
lines.contains(&" Key seedseedseed".to_string()),
"{lines:?}"
);
assert!(
lines.contains(
&" Why edit rebuild: rustc rebuilt the edited crate incrementally (adaptive seed)"
.to_string()
),
"{lines:?}"
);
assert!(
lines.contains(&format!(
" Earlier miss {}, key missmissmiss",
term::local_datetime(miss.ts)
)),
"{lines:?}"
);
let joined = lines.join("\n");
assert!(!joined.contains("now cached"), "{joined}");
let alone = uncached_compile_lines("app", &seed, None, &[&seed]).join("\n");
assert!(!alone.contains("Earlier"), "{alone}");
assert!(alone.contains("\nRecent events\n"), "{alone}");
}
#[test]
fn a_crate_without_misses_is_not_called_all_hits_unless_it_is() {
use events::EventResult;
let hit = explain_event(EventResult::LocalHit, 1, "k");
let remote = explain_event(EventResult::RemoteHit, 2, "k");
let mut pass = explain_event(EventResult::Passthrough, 3, "");
pass.passthrough_reason = "unsupported|cc link mode".to_string();
assert_eq!(
no_miss_headline("app", &[&hit, &remote]),
"No misses for `app`: all events are hits."
);
assert_eq!(
no_miss_headline("app", &[&pass, &hit]),
"No misses for `app`: its latest lookup hit the cache."
);
assert_eq!(
no_miss_headline("app", &[&pass]),
"No misses or hits for `app`."
);
let lines = no_miss_lines("app", &[], &[&hit]);
assert_eq!(lines[0], "kache · explain · app");
assert_eq!(lines[2], "No misses for `app`: all events are hits.");
assert!(
lines.contains(&"Stored entries (0)".to_string()),
"{lines:?}"
);
assert!(lines.contains(&"Recent events".to_string()), "{lines:?}");
}
#[test]
fn a_run_reports_declined_and_failed_steps_as_not_done() {
use std::path::PathBuf;
let cargo = PathBuf::from("/home/me/.cargo/config.toml");
let plan = SetupFacts {
cargo_path: cargo.clone(),
cargo: "configure",
cargo_exists: true,
shell: None,
shell_changes: Vec::new(),
rustdoc: Some("configure"),
test_runner: None,
test_changes: Vec::new(),
recovery: "ask",
recovery_path: PathBuf::from("/home/me/.config/kache/config.toml"),
service: Some("configure"),
service_path: Some(PathBuf::from("/svc")),
};
let components = |facts: &SetupFacts| {
setup_steps(facts)
.into_iter()
.map(|step| format!("{} {}", step.component, step.action))
.collect::<Vec<_>>()
};
assert_eq!(
components(&plan),
[
"cargo configure",
"backup create",
"rustdoc configure",
"backup create",
"free_space_recovery ask",
"login_service configure",
"daemon ensure_running",
]
);
let declined = run_facts(
&plan,
RunOutcome {
cargo_applied: false,
rustdoc: None,
test_runner: None,
test_changes: Vec::new(),
recovery: "keep",
service_installed_now: false,
},
);
assert_eq!(
components(&declined),
[
"cargo skip",
"free_space_recovery keep",
"login_service failed",
"daemon ensure_running",
]
);
let done = run_facts(
&plan,
RunOutcome {
cargo_applied: true,
rustdoc: Some("configure"),
test_runner: None,
test_changes: Vec::new(),
recovery: "configure",
service_installed_now: true,
},
);
assert_eq!(
components(&done),
[
"cargo configure",
"backup create",
"rustdoc configure",
"backup create",
"free_space_recovery configure",
"login_service configure",
"daemon ensure_running",
]
);
let kept = run_facts(
&SetupFacts {
cargo: "keep",
service: Some("keep"),
..plan.clone()
},
RunOutcome {
cargo_applied: false,
rustdoc: Some("failed"),
test_runner: None,
test_changes: Vec::new(),
recovery: "keep",
service_installed_now: false,
},
);
assert_eq!(
components(&kept)[..4],
[
"cargo keep",
"rustdoc failed",
"backup create",
"free_space_recovery keep"
]
);
assert_eq!(components(&kept)[4], "login_service keep");
}
#[test]
fn recovery_steps_say_when_init_asks() {
assert_eq!(recovery_action(&RecoveryOffer::Ask(1)), "ask");
for offer in [
RecoveryOffer::Nothing,
RecoveryOffer::FromEnv(true),
RecoveryOffer::Explain(1),
] {
assert_eq!(recovery_action(&offer), "keep", "{offer:?}");
}
}
#[test]
fn only_a_preview_assumes_the_shims_and_wrapper_it_describes() {
assert_eq!(rustdoc_assumes(true, true, true), (true, true));
assert_eq!(rustdoc_assumes(true, false, true), (false, true));
assert_eq!(rustdoc_assumes(true, true, false), (true, false));
assert_eq!(rustdoc_assumes(false, true, true), (false, false));
}
#[test]
fn a_run_reports_the_test_runner_it_planned_after_the_shell_edits() {
use std::path::PathBuf;
let rc = PathBuf::from("/home/me/.bashrc");
let plan = SetupFacts {
cargo_path: PathBuf::from("/home/me/.cargo/config.toml"),
cargo: "keep",
cargo_exists: true,
shell: Some(SetupStep::new("shell", "configure", vec![rc.clone()])),
shell_changes: vec![(rc.clone(), true)],
rustdoc: None,
test_runner: Some(SetupStep::new("test_runner", "unsupported", Vec::new())),
test_changes: Vec::new(),
recovery: "keep",
recovery_path: PathBuf::from("/home/me/.config/kache/config.toml"),
service: None,
service_path: None,
};
let run = run_facts(
&plan,
RunOutcome {
cargo_applied: false,
rustdoc: None,
test_runner: Some(SetupStep::new("test_runner", "configure", vec![rc.clone()])),
test_changes: vec![(rc.clone(), true)],
recovery: "keep",
service_installed_now: false,
},
);
let steps: Vec<_> = setup_steps(&run)
.into_iter()
.map(|step| format!("{} {}", step.component, step.action))
.collect();
assert_eq!(
steps,
[
"cargo keep",
"shell configure",
"backup create",
"test_runner configure",
"backup create",
"free_space_recovery keep",
"daemon ensure_running",
]
);
}
#[test]
fn recent_events_are_the_newest_five_in_local_time() {
use events::EventResult;
let events: Vec<events::BuildEvent> = (0..7)
.map(|i| explain_event(EventResult::LocalHit, i, &format!("key{i}-0123456789")))
.collect();
let refs: Vec<&events::BuildEvent> = events.iter().collect();
let lines = recent_event_lines(&refs);
assert_eq!(lines.len(), 6, "a header and five events: {lines:?}");
assert!(lines[0].starts_with(" TIME"), "{lines:?}");
assert!(
lines[1].starts_with(&format!(
" {} local_hit",
term::local_clock(events[2].ts)
)),
"{lines:?}"
);
assert!(lines[1].contains("key2-0123456"), "{lines:?}");
assert!(
lines[1].contains(&term::duration_ms(events[2].elapsed_ms)),
"{lines:?}"
);
assert!(lines[5].contains("key6-0123456"), "{lines:?}");
assert_eq!(recent_event_lines(&refs[..2]).len(), 3);
}
#[test]
fn list_details_use_the_short_key_and_local_time() {
let entry = daemon::StatsEntry {
store_dirs: vec![std::path::PathBuf::from("/s")],
cache_key: "96b68103bea22193ffeeddcc".to_string(),
crate_name: "app".to_string(),
crate_type: String::new(),
profile: "debug".to_string(),
size: 1536,
hit_count: 3,
created_at: "2026-10-14 15:18:56".to_string(),
last_accessed: "not a time".to_string(),
content_hash: None,
};
let lines = entry_detail_lines(&entry);
assert_eq!(lines[0], "Cache key: 96b68103bea2");
assert!(
!lines.iter().any(|line| line.contains("Type:")),
"{lines:?}"
);
assert!(
lines.contains(&" Profile: debug".to_string()),
"{lines:?}"
);
assert!(
lines.contains(&format!(
" Created: {}",
term::local_store_time("2026-10-14 15:18:56")
)),
"{lines:?}"
);
assert!(
lines.contains(&" Accessed: not a time".to_string()),
"{lines:?}"
);
assert_eq!(
local_store_date("2026-10-14 15:18:56"),
term::local_store_time("2026-10-14 15:18:56")[..10]
);
}
#[test]
fn the_sync_plan_prints_short_keys() {
let lines = sync_plan_lines(
&[("96b68103bea22193ffee".to_string(), "serde".to_string())],
&[("abc".to_string(), "tiny".to_string())],
);
assert_eq!(
lines,
[" pull 96b68103bea2 (serde)", " push abc (tiny)"]
);
}
#[test]
fn a_declined_store_is_the_latest_compile_and_a_fallback_run_is_not() {
use events::EventResult;
let miss = explain_event(EventResult::Miss, 1, "missmissmissmiss");
let mut seed = explain_event(EventResult::Passthrough, 2, "seedseedseedseed");
seed.passthrough_reason = "adaptive seed".to_string();
let mut declined = explain_event(EventResult::Skipped, 3, "skipskipskipskip");
declined.skip_reason = "admission-threshold".to_string();
let mut fallback = explain_event(EventResult::Passthrough, 4, "fbfbfbfbfbfbfbfb");
fallback.passthrough_reason = "unsupported|assembly".to_string();
fallback.fallback = true;
let legacy_skip = explain_event(EventResult::Skipped, 5, "oldskipoldskip00");
let latest = |events: &[&events::BuildEvent]| {
latest_uncached_compile(events).map(|event| event.cache_key.clone())
};
assert_eq!(
latest(&[&miss, &seed, &declined, &fallback, &legacy_skip]).as_deref(),
Some("skipskipskipskip")
);
assert_eq!(latest(&[&miss, &fallback]), None);
let lines = uncached_compile_lines("app", &declined, Some(&miss), &[&miss, &declined]);
assert!(
lines.contains(&" Why compiled, not stored: admission-threshold".to_string()),
"{lines:?}"
);
}