#[test]
fn lock_polls_back_off_from_a_millisecond_to_the_interval() {
let naps: Vec<u64> = (0..10)
.map(|attempt| lock_poll_interval(attempt).as_millis() as u64)
.collect();
assert_eq!(naps, vec![1, 2, 4, 8, 16, 32, 64, 100, 100, 100]);
}
#[test]
fn index_ddl_runs_once_per_schema_generation() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let db = open_index_db(&path).unwrap();
let generation: i64 = db
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(generation, INDEX_SCHEMA_GENERATION);
db.pragma_update(None, "user_version", 0_i64).unwrap();
db.execute_batch("DROP TABLE target_roots").unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
let generation: i64 = db
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(generation, INDEX_SCHEMA_GENERATION);
let tables: i64 = db
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name = 'target_roots'",
[],
|row| row.get(0),
)
.unwrap();
db.pragma_update(None, "user_version", 1_i64).unwrap();
db.execute_batch(
"ALTER TABLE entries DROP COLUMN durable;
DROP TABLE cc_mapped_hashes;
DROP TABLE cc_asm_scans;",
)
.unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
let durable_column: i64 = db
.query_row(
"SELECT count(*) FROM pragma_table_info('entries') WHERE name = 'durable'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(durable_column, 1, "generation 1 gains the durable column");
let memo_tables: i64 = db
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name IN ('cc_mapped_hashes', 'cc_asm_scans')",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(memo_tables, 2, "generation 1 gains the C memo tables");
assert_eq!(
tables, 1,
"the dropped table was recreated by the migration"
);
}
#[test]
fn the_cc_memo_rule_columns_arrive_once_and_older_rows_are_never_served() {
const MEMO_TABLES: [&str; 5] = [
"cc_preprocess_memos",
"cc_memo_inputs",
"cc_memo_input_refs",
"cc_mapped_hashes",
"cc_asm_scans",
];
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let stamp = |mtime_ns| crate::file_hash::FileFingerprint {
path: "/checkout/a.h".into(),
size: 1,
mtime_ns,
ctime_ns: mtime_ns,
inode: 1,
};
let record_as_older_release = |db: &Connection, memo_key: &str, mtime_ns: i64| {
let memo: i64 = db
.query_row(
"INSERT INTO cc_preprocess_memos(memo_key, preprocessed_hash, input_count)
VALUES (?1, 'older', 0) ON CONFLICT(memo_key) DO UPDATE SET
preprocessed_hash = excluded.preprocessed_hash, last_used = unixepoch()
RETURNING id",
params![memo_key],
|row| row.get(0),
)
.unwrap();
let input: i64 = db
.query_row(
"INSERT INTO cc_memo_inputs(name, content, mapped, local_path, size, mtime_ns, ctime_ns, inode)
VALUES ('a.h', 'c', 'm', '/checkout/a.h', 1, ?1, ?1, 1)
ON CONFLICT(name, content, mapped) DO UPDATE SET
local_path = excluded.local_path, size = excluded.size,
mtime_ns = excluded.mtime_ns, ctime_ns = excluded.ctime_ns, inode = excluded.inode
RETURNING id",
params![mtime_ns],
|row| row.get(0),
)
.unwrap();
db.execute(
"INSERT OR IGNORE INTO cc_memo_input_refs(memo_id, input_id) VALUES (?1, ?2)",
params![memo, input],
)
.unwrap();
db.execute(
"UPDATE cc_preprocess_memos SET input_count =
(SELECT count(*) FROM cc_memo_input_refs WHERE memo_id = ?1) WHERE id = ?1",
params![memo],
)
.unwrap();
db.execute_batch(
"INSERT OR IGNORE INTO cc_mapped_hashes(content, maps, mapped)
VALUES ('c', 'maps', 'older'), ('c2', 'maps', 'older');
INSERT OR IGNORE INTO cc_asm_scans(content, construct)
VALUES ('c', 'older'), ('c2', 'older');",
)
.unwrap();
};
let rows = |db: &Connection| {
MEMO_TABLES.map(|table| {
db.query_row(&format!("SELECT count(*) FROM {table}"), [], |row| {
row.get::<_, i64>(0)
})
.unwrap()
})
};
let generation = |db: &Connection| -> i64 {
db.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap()
};
let db = open_index_db(&path).unwrap();
db.execute_batch(
"ALTER TABLE cc_memo_inputs DROP COLUMN proof;
ALTER TABLE cc_mapped_hashes DROP COLUMN rule;
ALTER TABLE cc_asm_scans DROP COLUMN rule;",
)
.unwrap();
record_as_older_release(&db, "memo", 1);
db.pragma_update(None, "user_version", 8_i64).unwrap();
assert_eq!(rows(&db), [1, 1, 1, 2, 2]);
drop(db);
let db = open_index_db(&path).unwrap();
assert_eq!(generation(&db), INDEX_SCHEMA_GENERATION);
assert_eq!(
rows(&db),
[0; 5],
"rows from before the columns are dropped"
);
let cache = crate::file_hash::FileHashCache::Borrowed(&db);
let input = crate::file_hash::CcPreprocessMemoInput {
name: "a.h".into(),
fingerprint: stamp(1),
content: "c".into(),
mapped: "m".into(),
observed_ns: 10 * crate::file_hash::HASH_SETTLE_NS,
};
cache
.put_cc_preprocess_memo_inputs("memo", "current", std::slice::from_ref(&input))
.unwrap();
cache
.put_cc_mapped_hashes("maps", &[("c".into(), "current".into())])
.unwrap();
cache
.put_cc_asm_scans(&[("c".into(), String::new())])
.unwrap();
db.pragma_update(None, "user_version", 8_i64).unwrap();
record_as_older_release(&db, "older-memo", 1);
drop(db);
let db = open_index_db(&path).unwrap();
assert_eq!(
generation(&db),
INDEX_SCHEMA_GENERATION,
"the schema ran again"
);
assert_eq!(
rows(&db),
[2, 1, 2, 2, 2],
"nothing was dropped the second time"
);
let cache = crate::file_hash::FileHashCache::Borrowed(&db);
let memo = cache.get_cc_preprocess_memo("memo").unwrap().unwrap();
assert_eq!(memo.preprocessed_hash, "current");
assert_eq!(
memo.inputs[0].fingerprint,
stamp(1),
"rewritten unchanged, the stamp still proves the input"
);
let found = cache.get_cc_mapped_hashes("maps", &["c", "c2"]).unwrap();
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found["c"], "current");
let verdicts = cache.get_cc_asm_scans(&["c", "c2"]).unwrap();
assert_eq!(verdicts.len(), 1, "{verdicts:?}");
assert_eq!(verdicts["c"], "");
record_as_older_release(&db, "older-memo", 7);
let memo = cache.get_cc_preprocess_memo("memo").unwrap().unwrap();
assert_eq!(
memo.inputs[0].fingerprint.size,
crate::cc_memo::UNPROVEN_SIZE
);
assert_eq!(memo.inputs[0].fingerprint.path, "/checkout/a.h");
}
#[test]
fn index_from_generation_three_gains_the_crate_name_index() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let db = open_index_db(&path).unwrap();
db.execute_batch("DROP INDEX idx_entries_crate_name;")
.unwrap();
db.pragma_update(None, "user_version", 3_i64).unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
let indexes: i64 = db
.query_row(
"SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name = 'idx_entries_crate_name'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(indexes, 1);
let generation: i64 = db
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(generation, INDEX_SCHEMA_GENERATION);
}
#[test]
fn index_from_generation_four_gains_the_unit_column() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let db = open_index_db(&path).unwrap();
db.execute_batch(
"DROP INDEX idx_entries_crate_unit;
ALTER TABLE entries DROP COLUMN unit_id;",
)
.unwrap();
db.pragma_update(None, "user_version", 4_i64).unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
let indexes: i64 = db
.query_row(
"SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name = 'idx_entries_crate_unit'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(indexes, 1);
db.execute(
"INSERT INTO entries (cache_key, crate_name, unit_id) VALUES ('k', 'c', 'u')",
[],
)
.unwrap();
let generation: i64 = db
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(generation, INDEX_SCHEMA_GENERATION);
}
#[test]
fn an_older_release_reopening_the_index_does_not_empty_the_file_hash_memo() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let stamp = |file: &str| crate::file_hash::FileFingerprint {
path: file.to_string(),
size: 1,
mtime_ns: 1,
ctime_ns: 1,
inode: 1,
};
let record_as_older_release = |db: &Connection, file: &str| {
db.execute(
"INSERT OR REPLACE INTO file_hashes
(path, size, mtime_ns, ctime_ns, inode, hash, updated_at)
VALUES (?1, 1, 1, 1, 1, 'older', datetime('now'))",
params![file],
)
.unwrap();
};
let rows = |db: &Connection| -> i64 {
db.query_row("SELECT count(*) FROM file_hashes", [], |row| row.get(0))
.unwrap()
};
let served = |db: &Connection, file: &str| {
crate::file_hash::FileHashCache::Borrowed(db)
.get(&stamp(file))
.unwrap()
};
let db = open_index_db(&path).unwrap();
db.execute_batch(
"DROP TABLE file_hashes;
CREATE TABLE file_hashes (
path TEXT PRIMARY KEY, size INTEGER NOT NULL, mtime_ns INTEGER NOT NULL,
ctime_ns INTEGER NOT NULL DEFAULT 0, inode INTEGER NOT NULL DEFAULT 0,
hash TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now'))
) WITHOUT ROWID;",
)
.unwrap();
record_as_older_release(&db, "/before-upgrade");
db.pragma_update(None, "user_version", 8_i64).unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
assert_eq!(rows(&db), 0, "rows from before the column are dropped");
crate::file_hash::FileHashCache::Borrowed(&db)
.put(&stamp("/memoised"), "current")
.unwrap();
db.pragma_update(None, "user_version", 8_i64).unwrap();
record_as_older_release(&db, "/older-release");
drop(db);
let db = open_index_db(&path).unwrap();
let generation: i64 = db
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(generation, INDEX_SCHEMA_GENERATION, "the schema ran again");
assert_eq!(rows(&db), 2, "nothing was dropped the second time");
assert_eq!(served(&db, "/memoised").as_deref(), Some("current"));
assert_eq!(served(&db, "/older-release"), None);
}
#[test]
fn file_hashes_rebuilt_by_an_older_gc_after_the_upgrade_get_their_rule_back() {
const OLDER_COLUMNS: &str = "path TEXT PRIMARY KEY, size INTEGER NOT NULL,
mtime_ns INTEGER NOT NULL, ctime_ns INTEGER NOT NULL DEFAULT 0,
inode INTEGER NOT NULL DEFAULT 0, hash TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))";
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let stamp = crate::file_hash::FileFingerprint {
path: "/memoised".into(),
size: 1,
mtime_ns: 1,
ctime_ns: 1,
inode: 1,
};
let db = open_index_db(&path).unwrap();
db.execute_batch(&format!(
"DROP TABLE file_hashes; CREATE TABLE file_hashes ({OLDER_COLUMNS});"
))
.unwrap();
db.pragma_update(None, "user_version", 8_i64).unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
let cache = crate::file_hash::FileHashCache::Borrowed(&db);
cache.put(&stamp, "current").unwrap();
db.execute_batch(&format!(
"CREATE TABLE file_hashes_rebuilt ({OLDER_COLUMNS}) WITHOUT ROWID;
INSERT INTO file_hashes_rebuilt
(path, size, mtime_ns, ctime_ns, inode, hash, updated_at)
SELECT path, size, mtime_ns, ctime_ns, inode, hash, updated_at
FROM file_hashes;
DROP TABLE file_hashes;
ALTER TABLE file_hashes_rebuilt RENAME TO file_hashes;"
))
.unwrap();
assert!(cache.get(&stamp).is_err(), "the rebuild dropped the column");
let generation: i64 = db
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(generation, INDEX_SCHEMA_GENERATION);
drop(db);
let db = open_index_db(&path).unwrap();
let cache = crate::file_hash::FileHashCache::Borrowed(&db);
assert_eq!(cache.get(&stamp).unwrap(), None, "copied rows name no rule");
cache.put(&stamp, "current").unwrap();
assert_eq!(cache.get(&stamp).unwrap().as_deref(), Some("current"));
}
#[test]
fn an_index_stamped_current_without_a_rule_column_gains_it() {
for (table, column) in [
("file_hashes", "rule"),
("cc_memo_inputs", "proof"),
("cc_mapped_hashes", "rule"),
("cc_asm_scans", "rule"),
] {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let db = open_index_db(&path).unwrap();
db.execute_batch(&format!("ALTER TABLE {table} DROP COLUMN {column}"))
.unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
let present: bool = db
.query_row(
"SELECT EXISTS(SELECT 1 FROM pragma_table_info(?1) WHERE name = ?2)",
params![table, column],
|row| row.get(0),
)
.unwrap();
assert!(present, "{table}.{column}");
}
}
#[test]
fn opening_a_current_index_does_not_need_the_write_lock() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
drop(open_index_db(&path).unwrap());
let writer = Connection::open(&path).unwrap();
writer.execute_batch("BEGIN IMMEDIATE").unwrap();
open_index_db(&path).unwrap();
}
#[test]
fn record_entry_unit_updates_only_that_entry() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
store
.db
.execute_batch("INSERT INTO entries (cache_key, crate_name) VALUES ('a', 'x'), ('b', 'x');")
.unwrap();
store.record_entry_unit("a", "unit-a").unwrap();
store.record_entry_unit("b", "").unwrap();
let units: Vec<(String, String)> = store
.db
.prepare("SELECT cache_key, unit_id FROM entries ORDER BY cache_key")
.unwrap()
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
.unwrap()
.collect::<rusqlite::Result<_>>()
.unwrap();
assert_eq!(
units,
vec![
("a".to_string(), "unit-a".to_string()),
("b".to_string(), String::new())
]
);
assert!(
store
.file_hash_cache()
.has_entry_for_unit("x", "unit-a")
.unwrap()
);
assert!(
store
.file_hash_cache()
.has_entry_for_unit("x", "unit-z")
.unwrap(),
"row b has no unit"
);
}
#[test]
fn index_from_generation_one_moves_to_the_versioned_env_use_memo() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.db");
let db = open_index_db(&path).unwrap();
db.execute_batch(
"DROP TABLE source_env_dep_uses;
CREATE TABLE source_env_runtime_uses (
content_hash TEXT NOT NULL,
env_var TEXT NOT NULL,
has_runtime_use INTEGER NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (content_hash, env_var)
);",
)
.unwrap();
db.pragma_update(None, "user_version", 1_i64).unwrap();
drop(db);
let db = open_index_db(&path).unwrap();
let tables: Vec<String> = db
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table'
AND name IN ('source_env_runtime_uses', 'source_env_dep_uses')",
)
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<rusqlite::Result<_>>()
.unwrap();
assert_eq!(tables, vec!["source_env_dep_uses".to_string()]);
}
type Store = ArtifactStore<TestPolicy>;
struct TestPolicy;
impl ArtifactPolicy for TestPolicy {
fn allow_hardlink(name: &str) -> bool {
matches!(
std::path::Path::new(name)
.extension()
.and_then(|ext| ext.to_str()),
Some("rlib" | "rmeta" | "o" | "obj" | "a" | "lib" | "pdb" | "dwo" | "tar" | "unknown")
)
}
fn allow_empty(name: &str, kinds: &[String]) -> bool {
name.ends_with(".rmeta")
&& kinds
.iter()
.all(|kind| matches!(kind.as_str(), "bin" | "cdylib" | "staticlib"))
}
fn emit_kind(name: &str) -> Option<&'static str> {
match std::path::Path::new(name)
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("")
{
"rlib" | "so" | "dylib" | "dll" | "exe" | "a" | "lib" | "wasm" | "" => Some("link"),
"rmeta" => Some("metadata"),
"o" | "obj" => Some("obj"),
"d" | "pp" => Some("dep-info"),
"s" | "asm" => Some("asm"),
"ll" => Some("llvm-ir"),
"bc" => Some("llvm-bc"),
"mir" => Some("mir"),
_ => None,
}
}
fn stable_after_store(name: &str) -> bool {
!name.ends_with(".d") && !name.ends_with(".pp")
}
}
#[test]
fn test_positive_or_none_treats_non_positive_as_unknown() {
assert_eq!(positive_or_none(0), None, "0 is unknown, not Some(0)");
assert_eq!(positive_or_none(-1), None, "a negative is unknown");
assert_eq!(
positive_or_none(1),
Some(1),
"the smallest real value survives"
);
assert_eq!(positive_or_none(4200), Some(4200));
assert_eq!(positive_or_none(i64::MAX), Some(i64::MAX as u64));
}
use super::*;
use crate::eviction::EvictionPolicy as _;
#[test]
fn readonly_regular_metadata_requires_both_properties() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("artifact");
fs::write(&file, b"artifact").unwrap();
assert!(!metadata_is_readonly_regular(&fs::metadata(&file).unwrap()));
let mut permissions = fs::metadata(&file).unwrap().permissions();
permissions.set_readonly(true);
fs::set_permissions(&file, permissions).unwrap();
assert!(metadata_is_readonly_regular(&fs::metadata(&file).unwrap()));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let readonly_dir = dir.path().join("readonly-dir");
fs::create_dir(&readonly_dir).unwrap();
fs::set_permissions(&readonly_dir, fs::Permissions::from_mode(0o555)).unwrap();
assert!(!metadata_is_readonly_regular(
&fs::metadata(&readonly_dir).unwrap()
));
}
}
#[test]
fn content_hash_distinguishes_transposition_exec_bit_and_is_order_independent() {
let cf = |name: &str, hash: &str, executable: bool| CachedFile {
name: name.to_string(),
size: 10,
hash: hash.to_string(),
executable,
};
let a = vec![cf("a.rlib", "H1", false), cf("b.rlib", "H2", false)];
let swapped = vec![cf("a.rlib", "H2", false), cf("b.rlib", "H1", false)];
assert_ne!(
compute_content_hash(&a),
compute_content_hash(&swapped),
"a name<->hash transposition must change the content hash"
);
let exec_a = vec![cf("a.rlib", "H1", true), cf("b.rlib", "H2", false)];
let exec_b = vec![cf("a.rlib", "H1", false), cf("b.rlib", "H2", true)];
assert_ne!(
compute_content_hash(&exec_a),
compute_content_hash(&exec_b),
"moving the exec-bit to a different file must change the content hash"
);
let reordered = vec![cf("b.rlib", "H2", false), cf("a.rlib", "H1", false)];
assert_eq!(
compute_content_hash(&a),
compute_content_hash(&reordered),
"content hash must not depend on file order"
);
}
#[test]
fn hardlink_eligibility_mirrors_restore_strategy_with_insert_exclusions() {
let gate_open = !cfg!(windows);
for name in [
"libserde-abc123.rlib",
"libserde-abc123.rmeta",
"foo.rcgu.o",
"foo.obj",
"foo.dwo",
] {
assert_eq!(
hardlink_eligible::<TestPolicy>(name, false),
gate_open,
"{name} should be hardlink-eligible on insert (behind the Windows gate)"
);
}
assert!(!hardlink_eligible::<TestPolicy>("libfoo.dylib", false));
assert!(!hardlink_eligible::<TestPolicy>("libfoo.so", false));
assert!(!hardlink_eligible::<TestPolicy>("foo.exe", false));
assert!(!hardlink_eligible::<TestPolicy>("serde-abc123.d", false));
assert!(!hardlink_eligible::<TestPolicy>("my-binary", false));
assert!(!hardlink_eligible::<TestPolicy>(
"libserde-abc123.rlib",
true
));
}
#[test]
fn source_hardlink_policy_honors_independent_storage() {
assert!(!source_hardlink_allowed::<TestPolicy>(
false, "foo.o", false
));
assert_eq!(
source_hardlink_allowed::<TestPolicy>(true, "foo.o", false),
hardlink_eligible::<TestPolicy>("foo.o", false)
);
}
#[cfg(unix)]
#[test]
fn independent_put_never_hardlinks_or_marks_source_readonly() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("compiler-output.o");
fs::write(&output, b"independent cc artifact").unwrap();
fs::set_permissions(&output, fs::Permissions::from_mode(0o660)).unwrap();
store
.put_with_compile_time_independent(
"cc-independent",
"foo.c",
&[],
&[],
"x86_64-unknown-linux-gnu",
"",
&[(output.clone(), "foo.o".to_string())],
"",
"",
1,
)
.unwrap();
let meta = store.get("cc-independent").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let output_meta = fs::metadata(&output).unwrap();
let blob_meta = fs::metadata(&blob).unwrap();
assert_eq!(output_meta.permissions().mode() & 0o777, 0o660);
assert!(!output_meta.permissions().readonly());
assert_ne!(
(output_meta.dev(), output_meta.ino()),
(blob_meta.dev(), blob_meta.ino()),
"independent ingest must never share the compiler output inode"
);
assert!(blob_meta.permissions().readonly());
}
#[cfg(unix)]
#[test]
fn put_records_the_executable_bit_from_the_compiler_output_not_the_staging_snapshot() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("harness-a1b2c3d4e5f60718");
fs::write(&output, b"\x7fELF harness=false test binary").unwrap();
fs::set_permissions(&output, fs::Permissions::from_mode(0o755)).unwrap();
{
let _forced = ModeDroppingIngest::enable();
store
.put_with_compile_time_independent(
"harness-entry",
"harness",
&[],
&[],
"x86_64-unknown-linux-gnu",
"",
&[(output.clone(), "harness-a1b2c3d4e5f60718".to_string())],
"",
"",
1,
)
.unwrap();
}
let meta = store.get("harness-entry").unwrap().unwrap();
let blob_mode = fs::metadata(store.blob_path(&meta.files[0].hash))
.unwrap()
.permissions()
.mode();
assert_eq!(
blob_mode & 0o111,
0,
"emulated reflink ingest should have staged without the bit, got {blob_mode:o}"
);
assert!(
meta.files[0].executable,
"the recorded mode must come from the compiler's 0o755 output, \
not from the staging snapshot"
);
}
#[cfg(unix)]
#[test]
fn put_records_no_executable_bit_for_a_non_executable_output() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("libfoo.rlib");
fs::write(&output, b"rlib bytes").unwrap();
fs::set_permissions(&output, fs::Permissions::from_mode(0o644)).unwrap();
{
let _forced = ModeDroppingIngest::enable();
store
.put_with_compile_time_independent(
"rlib-entry",
"foo",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"",
&[(output.clone(), "libfoo.rlib".to_string())],
"",
"",
1,
)
.unwrap();
}
let meta = store.get("rlib-entry").unwrap().unwrap();
assert!(
!meta.files[0].executable,
"a 0o644 compiler output must not be recorded executable"
);
}
#[cfg(unix)]
#[test]
fn mode_dropping_ingest_guard_clears_on_drop() {
assert!(
!FORCE_MODE_DROPPING_INGEST.with(std::cell::Cell::get),
"ingest emulation must start off"
);
{
let _forced = ModeDroppingIngest::enable();
assert!(
FORCE_MODE_DROPPING_INGEST.with(std::cell::Cell::get),
"enable must turn ingest emulation on"
);
}
assert!(
!FORCE_MODE_DROPPING_INGEST.with(std::cell::Cell::get),
"Drop must turn ingest emulation off so a later put on this thread is not forced"
);
}
#[cfg(unix)]
#[test]
fn put_keeps_mutable_kind_blobs_inode_independent_from_the_source() {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("libfoo.dylib");
fs::write(&source, b"dylib bytes").unwrap();
store
.put(
"key-dylib",
"foo",
&["dylib".to_string()],
&[],
"host",
"dev",
&[(source.clone(), "libfoo.dylib".to_string())],
"",
"",
)
.unwrap();
let hash = crate::file_hash::hash_file(&source).unwrap();
let blob = store.blob_path(&hash);
assert_ne!(
fs::metadata(&blob).unwrap().ino(),
fs::metadata(&source).unwrap().ino(),
"a mutable-kind blob must not share an inode with the build output"
);
}
#[cfg(unix)]
#[test]
fn put_ingests_immutable_kinds_zero_copy_where_the_filesystem_allows() {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("libfoo-abc.rlib");
fs::write(&source, b"rlib bytes").unwrap();
store
.put(
"key-rlib",
"foo",
&["rlib".to_string()],
&[],
"host",
"dev",
&[(source.clone(), "libfoo-abc.rlib".to_string())],
"",
"",
)
.unwrap();
let hash = crate::file_hash::hash_file(&source).unwrap();
let blob = store.blob_path(&hash);
assert_eq!(fs::read(&blob).unwrap(), b"rlib bytes");
assert!(
fs::metadata(&blob).unwrap().permissions().readonly(),
"store blob must be read-only"
);
if fs::metadata(&blob).unwrap().ino() == fs::metadata(&source).unwrap().ino() {
assert!(
fs::metadata(&source).unwrap().permissions().readonly(),
"a hardlinked source must carry the blob's read-only mode"
);
}
}
#[cfg(unix)]
#[test]
fn put_never_stores_a_symlink_as_a_blob() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let target = dir.path().join("real-artifact.rlib");
fs::write(&target, b"real artifact bytes").unwrap();
let symlink = dir.path().join("linked.rlib");
std::os::unix::fs::symlink(&target, &symlink).unwrap();
store
.put(
"key-symlink",
"foo",
&["rlib".to_string()],
&[],
"host",
"dev",
&[(symlink.clone(), "linked.rlib".to_string())],
"",
"",
)
.unwrap();
let hash = crate::file_hash::hash_file(&symlink).unwrap();
let blob = store.blob_path(&hash);
let meta = fs::symlink_metadata(&blob).unwrap();
assert!(
meta.file_type().is_file(),
"blob must be a regular file, not a symlink"
);
assert_eq!(fs::read(&blob).unwrap(), b"real artifact bytes");
}
#[test]
fn should_try_store_reflink_is_the_inverse_of_the_force_flag() {
assert!(should_try_store_reflink(false));
assert!(!should_try_store_reflink(true));
}
#[test]
fn allow_store_hardlink_requires_permission_and_a_regular_file() {
assert!(allow_store_hardlink(true, true));
assert!(!allow_store_hardlink(false, true));
assert!(!allow_store_hardlink(true, false));
assert!(!allow_store_hardlink(false, false));
}
#[test]
fn force_store_hardlink_defaults_off_and_follows_the_guard() {
assert!(!force_store_hardlink());
{
let _guard = ForceStoreHardlink::enable();
assert!(force_store_hardlink());
}
assert!(!force_store_hardlink());
}
#[cfg(unix)]
#[test]
fn materialize_blob_without_hardlink_permission_does_not_share_inode() {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("libx.rlib");
fs::write(&source, b"ineligible-materialize").unwrap();
let blob = dir.path().join("blobs").join("aa").join("a".repeat(64));
let _force = ForceStoreHardlink::enable();
assert!(materialize_blob(&source, &blob, false).unwrap());
assert_eq!(
fs::metadata(&blob).unwrap().nlink(),
1,
"allow_hardlink=false must copy, not hardlink"
);
assert_eq!(fs::metadata(&source).unwrap().nlink(), 1);
}
#[test]
fn materialize_blob_errors_when_source_cannot_be_copied() {
let dir = tempfile::tempdir().unwrap();
let hash = "a".repeat(64);
let source = dir.path().join("missing.rlib");
let blob = dir.path().join("blobs").join("aa").join(&hash);
let err = materialize_blob(&source, &blob, false).unwrap_err();
assert!(
err.to_string().contains("copying"),
"expected copy context, got: {err:#}"
);
assert!(!blob.exists());
}
#[test]
fn materialize_blob_removes_tmp_when_atomic_rename_fails() {
let dir = tempfile::tempdir().unwrap();
let hash = "b".repeat(64);
let source = dir.path().join("source.rlib");
fs::write(&source, b"blob bytes").unwrap();
let blob = dir.path().join("blobs").join("bb").join(&hash);
fs::create_dir_all(&blob).unwrap();
let err = materialize_blob(&source, &blob, false).unwrap_err();
assert!(
err.to_string().contains("atomic rename"),
"expected rename context, got: {err:#}"
);
let tmp_left = fs::read_dir(blob.parent().unwrap())
.unwrap()
.flatten()
.filter(|entry| entry.file_name().to_string_lossy().ends_with(".tmp"))
.count();
assert_eq!(tmp_left, 0, "failed rename must remove its temp file");
assert!(blob.is_dir(), "the conflicting destination dir remains");
}
#[test]
fn materialize_blob_failure_does_not_leave_source_readonly() {
let dir = tempfile::tempdir().unwrap();
let hash = "c".repeat(64);
let source = dir.path().join("source.rlib");
fs::write(&source, b"blob bytes").unwrap();
let blob = dir.path().join("blobs").join("cc").join(&hash);
fs::create_dir_all(&blob).unwrap();
let err = materialize_blob(&source, &blob, true).unwrap_err();
assert!(
err.to_string().contains("atomic rename"),
"expected rename context, got: {err:#}"
);
assert!(
!fs::metadata(&source).unwrap().permissions().readonly(),
"failed hardlink ingest must restore a writable build output"
);
}
#[test]
fn put_stores_snapshot_bytes_matching_recorded_digest() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("out.rlib");
let original = b"artifact-bytes-v1";
fs::write(&output_file, original).unwrap();
store
.put_with_compile_time_independent(
"snapshot_key",
"snapshot_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file.clone(), "libout.rlib".to_string())],
"",
"",
0,
)
.unwrap();
fs::write(
&output_file,
b"mutated-after-put-with-a-much-longer-payload",
)
.unwrap();
let meta = store.get("snapshot_key").unwrap().unwrap();
assert_eq!(meta.files.len(), 1);
let blob = store.blob_path(&meta.files[0].hash);
let stored = fs::read(&blob).unwrap();
assert_eq!(
stored, original,
"stored blob must be byte-identical to what was hashed at put time"
);
assert_eq!(meta.files[0].size, original.len() as u64);
}
#[test]
fn successful_put_leaves_staging_dir_empty() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("out.rlib");
fs::write(&output_file, b"artifact").unwrap();
store
.put(
"staging_clean_key",
"crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file, "libout.rlib".to_string())],
"",
"",
)
.unwrap();
let staging = dir.path().join("staging");
if staging.exists() {
let leftovers: Vec<_> = fs::read_dir(&staging).unwrap().flatten().collect();
assert!(leftovers.is_empty(), "staging litter: {leftovers:?}");
}
}
#[test]
fn stored_meta_reads_the_entry_a_put_wrote() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("out.rlib");
fs::write(&output_file, b"rlib bytes").unwrap();
store
.put(
"stored_key",
"crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file, "libout.rlib".to_string())],
"",
"",
)
.unwrap();
let meta = store.stored_meta("stored_key").unwrap();
assert_eq!(meta.files.len(), 1);
assert_eq!(meta.files[0].name, "libout.rlib");
assert!(store.stored_meta("never_stored").is_none());
}
#[test]
fn zero_byte_refusal_cleans_up_staged_snapshot() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("empty.rlib");
fs::write(&output_file, b"").unwrap();
let result = store.put(
"zero_key",
"crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file, "libout.rlib".to_string())],
"",
"",
);
assert!(result.is_err(), "zero-byte rlib must be refused");
let staging = dir.path().join("staging");
if staging.exists() {
let leftovers: Vec<_> = fs::read_dir(&staging).unwrap().flatten().collect();
assert!(leftovers.is_empty(), "refused put left staging litter");
}
}
#[test]
fn publish_staged_blob_is_idempotent_when_blob_exists() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"identical-content").unwrap();
let (staged_a, ingest_a) = store.stage_blob_from_source(&source, false).unwrap();
let hash = crate::file_hash::hash_file(&staged_a).unwrap();
assert!(
store
.publish_staged_blob(&staged_a, ingest_a, &hash, 17)
.unwrap(),
"first publish should win"
);
let (staged_b, _ingest_b) = store.stage_blob_from_source(&source, false).unwrap();
assert_ne!(staged_a, staged_b, "each stage gets its own temp");
assert!(
!store
.publish_staged_blob(&staged_b, ingest_a, &hash, 17)
.unwrap(),
"second publish of the same digest must be a no-op"
);
let staging_leftovers = fs::read_dir(store.staging_dir()).unwrap().flatten().count();
assert_eq!(staging_leftovers, 0, "discarded stage must not linger");
}
#[test]
fn sweep_stale_staging_respects_min_age() {
use std::time::Duration;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let stale = store.staging_dir().join("stage-old-1.tmp");
let fresh = store.staging_dir().join("stage-new-1.tmp");
fs::create_dir_all(store.staging_dir()).unwrap();
fs::write(&stale, b"abandoned").unwrap();
fs::write(&fresh, b"in-flight").unwrap();
let old = filetime::FileTime::from_unix_time(0, 0);
filetime::set_file_mtime(&stale, old).unwrap();
filetime::set_file_atime(&stale, old).unwrap();
let stats = store.sweep_stale_staging(Duration::from_secs(3600));
assert_eq!(stats.removed, 1, "only the aged-out file is swept");
assert_eq!(
stats.scanned, 1,
"the in-flight file is skipped before it is ever counted"
);
assert_eq!(stats.bytes_reclaimed, b"abandoned".len() as u64);
assert!(!stale.exists());
assert!(fresh.exists(), "fresh staging file must survive the sweep");
let stats = store.sweep_stale_staging(Duration::ZERO);
assert_eq!(stats.removed, 1);
assert_eq!(stats.scanned, 1);
assert_eq!(stats.bytes_reclaimed, b"in-flight".len() as u64);
assert!(!fresh.exists());
}
#[cfg(unix)]
#[test]
fn dropping_a_hardlinked_temp_restores_the_source_writable() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"shared-inode-bytes").unwrap();
let tmp = dir.path().join("stage.tmp");
fs::hard_link(&source, &tmp).unwrap();
set_blob_readonly(&tmp);
assert_eq!(
fs::metadata(&source).unwrap().permissions().mode() & 0o200,
0,
"precondition: the shared inode is read-only through both names"
);
Store::drop_tmp_restore_source(&source, &tmp);
assert!(!tmp.exists(), "the staging temp must be gone");
assert_eq!(
fs::metadata(&source).unwrap().permissions().mode() & 0o200,
0o200,
"the build output must be writable again once the temp is gone"
);
}
#[test]
fn publish_attempt_outcome_lets_the_destination_decide() {
let denied = || Err(std::io::Error::from_raw_os_error(5));
assert_eq!(
publish_attempt_outcome(Ok(()), || unreachable!("a clean rename needs no probe")).unwrap(),
PublishRename::Published
);
assert_eq!(
publish_attempt_outcome(denied(), || PublishDest::File).unwrap(),
PublishRename::LostRace
);
for dest in [PublishDest::Vacant, PublishDest::Obstructed] {
let err = publish_attempt_outcome(denied(), || dest).unwrap_err();
assert_eq!(err.raw_os_error(), Some(5), "the rename error is kept");
}
}
#[test]
fn publish_failure_outcome_covers_every_interleaving() {
let settle = |transient, dest| {
publish_failure_outcome(std::io::Error::from_raw_os_error(5), transient, dest)
};
for transient in [true, false] {
assert_eq!(
settle(transient, PublishDest::File).unwrap(),
PublishRename::LostRace,
"a winner that landed after the last attempt still counts"
);
assert!(
settle(transient, PublishDest::Obstructed).is_err(),
"no race leaves a non-file at a blob path"
);
}
assert_eq!(
settle(true, PublishDest::Vacant).unwrap(),
PublishRename::Deferred,
"removed under the publisher: the locked phase publishes"
);
let err = settle(false, PublishDest::Vacant).unwrap_err();
assert!(format!("{err:#}").contains("publishing staged blob"));
assert_eq!(
err.root_cause().to_string(),
std::io::Error::from_raw_os_error(5).to_string()
);
}
#[test]
fn publish_dest_state_tells_a_file_from_a_directory_from_nothing() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("blob");
fs::write(&file, b"x").unwrap();
assert_eq!(publish_dest_state(&file), PublishDest::File);
assert_eq!(publish_dest_state(dir.path()), PublishDest::Obstructed);
assert_eq!(
publish_dest_state(&dir.path().join("absent")),
PublishDest::Vacant
);
}
fn run_publish_rename(
rename_ok: impl Fn(u32) -> bool,
dest: impl Fn(u32) -> PublishDest,
transient: bool,
) -> (Result<PublishRename>, u32) {
let calls = std::cell::Cell::new(0u32);
let outcome = publish_rename(
|| {
let n = calls.get();
calls.set(n + 1);
if rename_ok(n) {
Ok(())
} else {
Err(std::io::Error::from_raw_os_error(5))
}
},
|| dest(calls.get() - 1),
|_| transient,
);
(outcome, calls.get())
}
#[test]
fn publish_rename_publishes_on_a_clean_rename() {
let (outcome, calls) = run_publish_rename(|_| true, |_| PublishDest::Vacant, true);
assert_eq!(outcome.unwrap(), PublishRename::Published);
assert_eq!(calls, 1);
}
#[test]
fn publish_rename_settles_at_once_on_a_present_winner() {
let (outcome, calls) = run_publish_rename(|_| false, |_| PublishDest::File, true);
assert_eq!(outcome.unwrap(), PublishRename::LostRace);
assert_eq!(calls, 1, "a present winner must not burn the retry budget");
}
#[test]
fn publish_rename_retries_into_a_name_a_remover_freed() {
let (outcome, calls) = run_publish_rename(|n| n == 1, |_| PublishDest::Vacant, true);
assert_eq!(outcome.unwrap(), PublishRename::Published);
assert_eq!(calls, 2);
}
#[test]
fn publish_rename_stops_retrying_once_a_winner_appears() {
let dest = |n: u32| {
if n < 2 {
PublishDest::Vacant
} else {
PublishDest::File
}
};
let (outcome, calls) = run_publish_rename(|_| false, dest, true);
assert_eq!(outcome.unwrap(), PublishRename::LostRace);
assert_eq!(calls, 3);
}
#[test]
fn publish_rename_defers_when_the_budget_ends_on_a_vacant_name() {
let (outcome, calls) = run_publish_rename(|_| false, |_| PublishDest::Vacant, true);
assert_eq!(outcome.unwrap(), PublishRename::Deferred);
assert_eq!(calls, crate::atomic::TRANSIENT_ATTEMPTS);
}
#[test]
fn publish_rename_fails_on_a_settled_error_or_an_obstructed_name() {
let (outcome, calls) = run_publish_rename(|_| false, |_| PublishDest::Vacant, false);
let err = outcome.unwrap_err();
assert!(format!("{err:#}").contains("publishing staged blob"));
assert_eq!(err.root_cause().to_string(), {
std::io::Error::from_raw_os_error(5).to_string()
});
assert_eq!(calls, 1, "a settled error is not retried");
let (outcome, calls) = run_publish_rename(|_| false, |_| PublishDest::Obstructed, true);
assert!(outcome.is_err(), "a directory at the blob path is a fault");
assert_eq!(calls, crate::atomic::TRANSIENT_ATTEMPTS);
}
#[test]
fn publish_rename_onto_a_read_only_winner_settles_in_one_attempt() {
let dir = tempfile::tempdir().unwrap();
let blob = dir.path().join("blob");
let staged = dir.path().join("staged");
fs::write(&blob, b"same-bytes").unwrap();
fs::write(&staged, b"same-bytes").unwrap();
set_blob_readonly(&blob);
let calls = std::cell::Cell::new(0u32);
let outcome = publish_rename(
|| {
calls.set(calls.get() + 1);
fs::rename(&staged, &blob)
},
|| publish_dest_state(&blob),
crate::atomic::is_transient_rename_error,
)
.unwrap();
assert_ne!(outcome, PublishRename::Deferred);
assert_eq!(calls.get(), 1);
assert_eq!(fs::read(&blob).unwrap(), b"same-bytes");
unlink_blob(&blob);
unlink_blob(&staged);
}
#[cfg(windows)]
#[test]
fn rename_onto_a_read_only_file_is_access_denied_on_windows() {
let dir = tempfile::tempdir().unwrap();
let blob = dir.path().join("blob");
let staged = dir.path().join("staged");
fs::write(&blob, b"winner").unwrap();
fs::write(&staged, b"loser").unwrap();
set_blob_readonly(&blob);
let err = fs::rename(&staged, &blob).unwrap_err();
assert_eq!(err.raw_os_error(), Some(5));
assert!(crate::atomic::is_transient_rename_error(&err));
assert_eq!(fs::read(&blob).unwrap(), b"winner");
unlink_blob(&blob);
}
#[test]
fn deferred_publish_is_repaired_by_the_locked_phase() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"removed-under-the-publisher").unwrap();
let (staged, _ingest) = store.stage_blob_from_source(&source, false).unwrap();
let hash = crate::file_hash::hash_file(&staged).unwrap();
let blob = store.blob_path(&hash);
let outcome = Store::publish_staged_blob_with(
&staged,
&blob,
|| Err(std::io::Error::from_raw_os_error(5)),
|_| true,
)
.unwrap();
assert_eq!(outcome, PublishRename::Deferred);
assert!(!staged.exists(), "a deferred publish discards its snapshot");
assert!(!blob.exists());
store
.rematerialize_and_verify(&source, &hash, "out.rlib", false)
.unwrap();
assert_eq!(fs::read(&blob).unwrap(), b"removed-under-the-publisher");
}
#[test]
fn failed_publish_discards_its_snapshot_and_keeps_the_error() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"content").unwrap();
let (staged, _ingest) = store.stage_blob_from_source(&source, false).unwrap();
let blob = store.blob_path(&"f".repeat(64));
let err = Store::publish_staged_blob_with(
&staged,
&blob,
|| Err(std::io::Error::other("disk on fire")),
|_| false,
)
.unwrap_err();
assert!(format!("{err:#}").contains("disk on fire"));
assert!(!staged.exists(), "a failed publish discards its snapshot");
}
#[test]
fn successful_publish_keeps_the_staged_bytes_as_the_blob() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"published-bytes").unwrap();
let (staged, _ingest) = store.stage_blob_from_source(&source, false).unwrap();
let blob = store.blob_path(&"a".repeat(64));
let outcome =
Store::publish_staged_blob_with(&staged, &blob, || fs::rename(&staged, &blob), |_| false)
.unwrap();
assert_eq!(outcome, PublishRename::Published);
assert_eq!(fs::read(&blob).unwrap(), b"published-bytes");
}
#[test]
fn staging_sweep_grace_spares_an_in_flight_snapshot() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
fs::create_dir_all(store.staging_dir()).unwrap();
let in_flight = store.staging_dir().join("stage-1-0.tmp");
fs::write(&in_flight, b"mid-put").unwrap();
let stats = store.sweep_stale_staging(STAGING_SWEEP_GRACE);
assert_eq!(stats.removed, 0, "no sweeper may reclaim a live snapshot");
assert!(in_flight.exists());
assert!(
STAGING_SWEEP_GRACE >= Duration::from_secs(3600),
"the grace must stay long enough to outlast a slow put"
);
}
#[test]
fn free_staging_path_skips_occupied_names() {
let dir = tempfile::tempdir().unwrap();
let first = dir.path().join("cand-a");
let second = dir.path().join("cand-b");
fs::write(&first, b"taken").unwrap();
let names: Vec<PathBuf> = vec![first.clone(), second.clone()];
let mut calls = 0usize;
let got = free_staging_path(|_| {
let p = names[calls].clone();
calls += 1;
p
})
.unwrap();
assert_eq!(got, second, "must skip the taken candidate");
assert!(
!got.exists(),
"the chosen path must NOT exist: clonefile(2)/link(2) fail with \
EEXIST on an existing destination, which would demote every put \
to a full byte copy"
);
}
#[test]
fn free_staging_path_propagates_real_errors() {
let dir = tempfile::tempdir().unwrap();
let not_a_dir = dir.path().join("not-a-dir");
fs::write(¬_a_dir, b"").unwrap();
let result = free_staging_path(|n| not_a_dir.join(format!("x-{n}")));
#[cfg(unix)]
{
let err = result.unwrap_err();
assert_ne!(
err.kind(),
std::io::ErrorKind::AlreadyExists,
"a real fault must not be reported as a name collision: {err}"
);
}
#[cfg(windows)]
{
let candidate = result.expect("windows reports the parent as absent");
assert!(candidate.starts_with(¬_a_dir));
}
}
#[test]
fn free_staging_path_gives_up_after_bounded_attempts() {
let dir = tempfile::tempdir().unwrap();
let taken = dir.path().join("always-taken");
fs::write(&taken, b"taken").unwrap();
let mut calls = 0u32;
let err = free_staging_path(|_| {
calls += 1;
taken.clone()
})
.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
assert_eq!(calls, STAGING_NAME_ATTEMPTS, "search must be bounded");
}
#[cfg(unix)]
#[test]
fn staging_ingests_zero_copy_not_a_byte_copy() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"artifact-bytes").unwrap();
let (staged, ingest) = store.stage_blob_from_source(&source, true).unwrap();
assert!(
!matches!(ingest, StoreIngest::Copy(_)),
"staging fell back to a byte copy where a reflink or hardlink \
was available — the ingest destination must not exist yet"
);
assert_eq!(fs::read(&staged).unwrap(), b"artifact-bytes");
Store::drop_tmp_restore_source(&source, &staged);
}
#[cfg(unix)]
#[test]
fn staging_refuses_to_hardlink_a_symlink_source() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let target = dir.path().join("real.rlib");
fs::write(&target, b"target-bytes").unwrap();
let link = dir.path().join("link.rlib");
std::os::unix::fs::symlink(&target, &link).unwrap();
let (staged, _ingest) = store.stage_blob_from_source(&link, true).unwrap();
let meta = fs::symlink_metadata(&staged).unwrap();
assert!(
meta.is_file(),
"staged snapshot must be a regular file, never a symlink"
);
assert_eq!(fs::read(&staged).unwrap(), b"target-bytes");
if !paths_share_inode(&target, &staged) {
let mode = fs::metadata(&target).unwrap().permissions().mode();
assert_eq!(
mode & 0o200,
0o200,
"an isolated snapshot must not flip the symlink target read-only"
);
}
}
#[test]
fn publish_reports_false_when_rename_fails_on_existing_blob() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let hash = "d".repeat(64);
let source = dir.path().join("out.rlib");
fs::write(&source, b"content").unwrap();
let (staged_a, ingest_a) = store.stage_blob_from_source(&source, false).unwrap();
assert!(
store
.publish_staged_blob(&staged_a, ingest_a, &hash, 7)
.unwrap()
);
let bogus_staged = store.staging_dir().join("not-a-file");
fs::create_dir_all(&bogus_staged).unwrap();
let result = store.publish_staged_blob(&bogus_staged, ingest_a, &hash, 7);
assert!(!result.unwrap(), "lost race must report Ok(false)");
}
#[test]
fn publish_errors_when_rename_fails_without_existing_blob() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let hash = "e".repeat(64);
let source = dir.path().join("out.rlib");
fs::write(&source, b"content").unwrap();
let (staged, ingest) = store.stage_blob_from_source(&source, false).unwrap();
let blob = store.blob_path(&hash);
fs::create_dir_all(blob.parent().unwrap()).unwrap();
fs::create_dir_all(&blob).unwrap();
let result = store.publish_staged_blob(&staged, ingest, &hash, 7);
assert!(result.is_err(), "genuine rename errors must propagate");
}
#[test]
fn rematerialize_accepts_untouched_source() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"stable-content").unwrap();
let hash = crate::file_hash::hash_file(&source).unwrap();
store
.rematerialize_and_verify(&source, &hash, "out.rlib", false)
.unwrap();
assert_eq!(fs::read(store.blob_path(&hash)).unwrap(), b"stable-content");
}
#[test]
fn rematerialize_refuses_mutated_source() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"original").unwrap();
let hash = crate::file_hash::hash_file(&source).unwrap();
fs::write(&source, b"mutated-after-snapshot").unwrap();
let err = store
.rematerialize_and_verify(&source, &hash, "out.rlib", false)
.unwrap_err();
assert!(
err.to_string().contains("refusing to commit"),
"expected digest-mismatch refusal, got: {err:#}"
);
}
#[test]
fn a_read_only_blob_flushes_and_a_missing_one_reports_not_found() {
let dir = tempfile::tempdir().unwrap();
let blob = dir.path().join("blob");
fs::write(&blob, b"artifact-bytes").unwrap();
set_blob_readonly(&blob);
assert!(
fs::metadata(&blob).unwrap().permissions().readonly(),
"the fixture must reproduce a published blob"
);
fsync_published_blob(&blob).expect("a published blob flushes");
assert!(
fs::metadata(&blob).unwrap().permissions().readonly(),
"and stays read-only afterwards"
);
assert_eq!(
fsync_published_blob(&dir.path().join("absent"))
.unwrap_err()
.kind(),
std::io::ErrorKind::NotFound
);
}
#[test]
fn blob_ingest_follows_the_last_opened_store_policy() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.deferred_durability = true;
let _deferred = Store::open(&config).unwrap();
assert!(
!durable_writes_now(),
"a deferred store skips the inline fsync"
);
config.deferred_durability = false;
let _strict = Store::open(&config).unwrap();
assert!(
durable_writes_now(),
"a durable store keeps the inline fsync"
);
}
#[test]
fn deferred_durability_verifies_pending_hits_until_the_flush() {
let _env_lock = crate::test_support::process_state_test_lock();
let _verify = EnvVarGuard::remove("KACHE_VERIFY_RESTORES");
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.deferred_durability = true;
let store = Store::open(&config).unwrap();
let puts = std::cell::Cell::new(0u32);
let put = |bytes: &[u8]| {
puts.set(puts.get() + 1);
let output_file = dir.path().join(format!("out-{}.rlib", puts.get()));
fs::write(&output_file, bytes).unwrap();
store
.put(
"pending_key",
"pending_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file.clone(), "libout.rlib".to_string())],
"",
"",
)
.unwrap()
};
let corrupt_same_size = |blob: &Path| {
let mut perms = fs::metadata(blob).unwrap().permissions();
perms.set_readonly(false);
fs::set_permissions(blob, perms).unwrap();
let mut bytes = fs::read(blob).unwrap();
bytes[0] ^= 0xff;
fs::write(blob, bytes).unwrap();
};
put(b"artifact-bytes-one");
assert_eq!(store.pending_durability().unwrap(), 1);
let meta = store
.get("pending_key")
.unwrap()
.expect("intact pending entry hits");
let blob = store.blob_path(&meta.files[0].hash);
corrupt_same_size(&blob);
assert!(
store.get("pending_key").unwrap().is_none(),
"a pending entry whose bytes changed is evicted, not served"
);
assert!(!store.contains("pending_key"));
put(b"artifact-bytes-two");
assert_eq!(store.pending_durability().unwrap(), 1);
assert_eq!(store.flush_durability(10).unwrap(), 1);
assert_eq!(store.pending_durability().unwrap(), 0);
assert_eq!(
store.flush_durability(10).unwrap(),
0,
"nothing left to flush"
);
assert!(!store.flush_entry_durability("pending_key").unwrap());
let meta = store.get("pending_key").unwrap().unwrap();
corrupt_same_size(&store.blob_path(&meta.files[0].hash));
assert!(
store.get("pending_key").unwrap().is_some(),
"a durable entry keeps the size-only check the verification policy asks for"
);
store.remove_entry("pending_key").unwrap();
put(b"artifact-bytes-three");
let meta = store.get("pending_key").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let mut perms = fs::metadata(&blob).unwrap().permissions();
perms.set_readonly(false);
fs::set_permissions(&blob, perms).unwrap();
fs::remove_file(&blob).unwrap();
assert_eq!(store.flush_durability(10).unwrap(), 0);
assert!(
!store.contains("pending_key"),
"evicted rather than marked durable"
);
config.deferred_durability = false;
let strict = Store::open(&config).unwrap();
let strict_file = dir.path().join("out-strict.rlib");
fs::write(&strict_file, b"artifact-bytes-four").unwrap();
strict
.put(
"strict_key",
"strict_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(strict_file.clone(), "libout.rlib".to_string())],
"",
"",
)
.unwrap();
assert_eq!(strict.pending_durability().unwrap(), 0);
assert!(strict.try_durability_flush_lock().unwrap().is_some());
}
#[test]
fn get_counts_hits_and_evicts_damaged_blobs() {
let _env_lock = crate::test_support::process_state_test_lock();
let _verify = EnvVarGuard::remove("KACHE_VERIFY_RESTORES");
for damage in ["missing", "short", "long", "directory"] {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("out.rlib");
fs::write(&output_file, b"artifact-bytes").unwrap();
store
.put(
"probe_key",
"probe_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file, "libout.rlib".to_string())],
"out",
"err",
)
.unwrap();
let hit_count = || {
store
.db
.query_row(
"SELECT hit_count FROM entries WHERE cache_key = 'probe_key'",
[],
|row| row.get::<_, i64>(0),
)
.unwrap()
};
assert_eq!(hit_count(), 0);
store.set_last_accessed_for_test("probe_key", "-1 minutes");
let meta = store.get("probe_key").unwrap().unwrap();
assert_eq!(meta.cache_key, "probe_key");
assert_eq!(meta.stdout, "out");
assert_eq!(meta.stderr, "err");
assert_eq!(meta.files.len(), 1);
assert_eq!(hit_count(), 1, "get must record the hit");
let _ = store.get("probe_key").unwrap().unwrap();
assert_eq!(hit_count(), 1, "a fresh stamp is not rewritten");
let blob = store.blob_path(&meta.files[0].hash);
let mut perms = fs::metadata(&blob).unwrap().permissions();
perms.set_readonly(false);
fs::set_permissions(&blob, perms).unwrap();
fs::remove_file(&blob).unwrap();
match damage {
"missing" => {}
"short" => fs::write(&blob, b"short").unwrap(),
"long" => fs::write(&blob, b"longer than the original artifact").unwrap(),
"directory" => fs::create_dir(&blob).unwrap(),
_ => unreachable!(),
}
assert!(store.get("probe_key").unwrap().is_none(), "{damage}");
assert!(!store.contains("probe_key"), "get must evict: {damage}");
}
}
#[test]
fn probe_connection_refuses_writes() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let _store = Store::open(&config).unwrap();
let ro = open_index_db_readonly(&config.index_db_path()).unwrap();
assert!(
ro.execute("DELETE FROM entries", []).is_err(),
"read-only probe connection must reject writes"
);
}
fn test_config(dir: &Path) -> Config {
Config {
cache_dir: dir.to_path_buf(),
max_size: 1024 * 1024,
gc_evict_shared: false,
upload_spool_max_jobs: 65_536,
deferred_durability: false,
}
}
struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: &str) -> Self {
let previous = std::env::var_os(key);
unsafe { std::env::set_var(key, value) };
Self { key, previous }
}
fn remove(key: &'static str) -> Self {
let previous = std::env::var_os(key);
unsafe { std::env::remove_var(key) };
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.previous {
Some(value) => unsafe { std::env::set_var(self.key, value) },
None => unsafe { std::env::remove_var(self.key) },
}
}
}
#[test]
fn cap_diagnostics_is_lossless_by_default_and_truncates_when_capped() {
let warnings = "warning: unused variable `x`\nwarning: dead code\n";
assert_eq!(cap_diagnostics(warnings, None), warnings);
assert_eq!(cap_diagnostics(warnings, Some(10_000)), warnings);
let capped = cap_diagnostics(warnings, Some(20));
assert!(capped.starts_with("warning: unused vari"));
assert!(capped.contains("diagnostics truncated"));
assert!(capped.len() < warnings.len() + 80);
let unicode = "wörning: ".repeat(20);
let capped = cap_diagnostics(&unicode, Some(5));
assert!(std::str::from_utf8(capped.as_bytes()).is_ok());
}
#[test]
fn file_hash_records_persist_and_reject_changed_fingerprints() {
use crate::file_hash::{FileFingerprint, FileHashLookup};
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let artifact = dir.path().join("artifact.rlib");
fs::write(&artifact, vec![7; 65_536]).unwrap();
let fingerprint = FileFingerprint::from_path(&artifact).unwrap();
assert!(matches!(
store.file_hash_lookup(&artifact),
FileHashLookup::NeedsHash(_)
));
store.file_hash_record(&fingerprint, "recorded");
assert!(
matches!(store.file_hash_lookup(&artifact), FileHashLookup::Hit(hash) if hash == "recorded")
);
store.record_verified_file_hash(&fingerprint, "verified");
assert!(
matches!(store.file_hash_lookup(&artifact), FileHashLookup::Hit(hash) if hash == "verified")
);
let second = dir.path().join("second.rlib");
fs::write(&second, vec![9; 65_536]).unwrap();
let second_fingerprint = FileFingerprint::from_path(&second).unwrap();
store.record_verified_file_hashes(&[
(fingerprint.clone(), "batched"),
(second_fingerprint, "batched-second"),
]);
assert!(
matches!(store.file_hash_lookup(&artifact), FileHashLookup::Hit(hash) if hash == "batched")
);
assert!(
matches!(store.file_hash_lookup(&second), FileHashLookup::Hit(hash) if hash == "batched-second")
);
store.record_verified_file_hashes(&[]);
drop(store);
let store = Store::open(&config).unwrap();
assert!(
matches!(store.file_hash_lookup(&artifact), FileHashLookup::Hit(hash) if hash == "batched")
);
fs::write(&artifact, vec![8; 65_537]).unwrap();
assert!(matches!(
store.file_hash_lookup(&artifact),
FileHashLookup::NeedsHash(_)
));
}
#[test]
fn readonly_blob_detection_requires_a_shared_inode() {
let dir = tempfile::tempdir().unwrap();
let store_dir = dir.path().join("store");
let output = dir.path().join("output.o");
fs::write(&output, b"legacy compiler output").unwrap();
let writable = fs::metadata(&output).unwrap().permissions();
assert_eq!(
Store::matching_readonly_blob_inode(&store_dir, &output).unwrap(),
None
);
let hash = crate::file_hash::hash_file(&output).unwrap();
let blob = blob_path_in_store_dir(&store_dir, &hash);
fs::create_dir_all(blob.parent().unwrap()).unwrap();
fs::hard_link(&output, &blob).unwrap();
let mut readonly = writable.clone();
readonly.set_readonly(true);
fs::set_permissions(&output, readonly).unwrap();
assert_eq!(
Store::matching_readonly_blob_inode(&store_dir, &output).unwrap(),
Some(blob)
);
let independent = dir.path().join("independent.o");
fs::copy(&output, &independent).unwrap();
assert!(fs::metadata(&independent).unwrap().permissions().readonly());
assert_eq!(
Store::matching_readonly_blob_inode(&store_dir, &independent).unwrap(),
None
);
fs::set_permissions(&output, writable.clone()).unwrap();
fs::set_permissions(&independent, writable).unwrap();
}
#[test]
fn put_records_known_hash_only_for_stable_outputs() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let artifact = dir.path().join("artifact.rlib");
std::fs::write(&artifact, vec![b'x'; 64 * 1024]).unwrap();
let expected = crate::file_hash::hash_file(&artifact).unwrap();
store
.put(
"known-hash-stable",
"artifact",
&["rlib".to_string()],
&[],
"host",
"dev",
&[(artifact.clone(), "libartifact.rlib".to_string())],
"",
"",
)
.unwrap();
match store.file_hash_lookup(&artifact) {
crate::file_hash::FileHashLookup::Hit(actual) => assert_eq!(actual, expected),
_ => panic!("publication should seed the persistent file hash"),
}
let dep_info = dir.path().join("artifact.d");
std::fs::write(&dep_info, vec![b'd'; 64 * 1024]).unwrap();
store
.put(
"known-hash-dep-info",
"artifact",
&[],
&[],
"host",
"dev",
&[(dep_info.clone(), "artifact.d".to_string())],
"",
"",
)
.unwrap();
assert!(matches!(
store.file_hash_lookup(&dep_info),
crate::file_hash::FileHashLookup::NeedsHash(_)
));
let independent = dir.path().join("independent.o");
std::fs::write(&independent, vec![b'o'; 64 * 1024]).unwrap();
store
.put_with_compile_time_independent(
"known-hash-independent",
"artifact.c",
&[],
&[],
"host",
"dev",
&[(independent.clone(), "independent.o".to_string())],
"",
"",
1,
)
.unwrap();
assert!(matches!(
store.file_hash_lookup(&independent),
crate::file_hash::FileHashLookup::NeedsHash(_)
));
}
#[test]
fn test_store_put_and_get() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("output.rlib");
std::fs::write(&output_file, b"fake rlib content").unwrap();
store
.put(
"abc123",
"mylib",
&["lib".to_string()],
&["std".to_string()],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file, "libmylib.rlib".to_string())],
"",
"",
)
.unwrap();
assert!(store.contains("abc123"));
let meta = store.get("abc123").unwrap().unwrap();
assert_eq!(meta.crate_name, "mylib");
assert_eq!(meta.files.len(), 1);
assert_eq!(meta.files[0].name, "libmylib.rlib");
}
#[test]
fn sweep_orphan_blobs_removes_unreferenced_files_only() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("output.rlib");
std::fs::write(&output_file, b"real rlib content").unwrap();
store
.put(
"abc123",
"mylib",
&["lib".to_string()],
&["std".to_string()],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file, "libmylib.rlib".to_string())],
"",
"",
)
.unwrap();
let orphan_hash = "f".repeat(64);
let orphan_path = store.blob_path(&orphan_hash);
std::fs::create_dir_all(orphan_path.parent().unwrap()).unwrap();
std::fs::write(&orphan_path, b"orphaned bytes").unwrap();
let tmp_path = orphan_path.with_file_name(format!(".{orphan_hash}.123.0.tmp"));
std::fs::write(&tmp_path, b"in-progress").unwrap();
let stats = store.sweep_orphan_blobs(std::time::Duration::ZERO).unwrap();
assert_eq!(stats.removed, 1, "only the orphan should be removed");
assert_eq!(stats.scanned, 2);
assert_eq!(stats.bytes_reclaimed, b"orphaned bytes".len() as u64);
assert!(!orphan_path.exists(), "orphan blob must be unlinked");
assert!(tmp_path.exists(), "in-progress .tmp must be left alone");
assert!(store.get("abc123").unwrap().is_some());
}
#[test]
fn sweep_orphan_blobs_respects_min_age() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let orphan_hash = "a".repeat(64);
let orphan_path = store.blob_path(&orphan_hash);
std::fs::create_dir_all(orphan_path.parent().unwrap()).unwrap();
std::fs::write(&orphan_path, b"fresh orphan").unwrap();
let stats = store
.sweep_orphan_blobs(std::time::Duration::from_secs(3600))
.unwrap();
assert_eq!(stats.removed, 0);
assert!(orphan_path.exists());
}
#[test]
fn reconcile_blob_index_repairs_refcounts_and_stale_rows_idempotently() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let payload = b"shared authoritative blob";
for key in ["repair_a", "repair_b"] {
let output = dir.path().join(format!("{key}.rlib"));
fs::write(&output, payload).unwrap();
store
.put(
key,
"repairlib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(output, format!("lib{key}.rlib"))],
"",
"",
)
.unwrap();
}
let hash = store.get("repair_a").unwrap().unwrap().files[0]
.hash
.clone();
let stale_hash = "f".repeat(64);
let stale_path = store.blob_path(&stale_hash);
fs::create_dir_all(stale_path.parent().unwrap()).unwrap();
fs::write(&stale_path, b"stale indexed blob").unwrap();
store
.db
.execute(
"UPDATE blobs SET refcount = 41 WHERE hash = ?1",
params![hash],
)
.unwrap();
store
.db
.execute(
"UPDATE entry_blobs SET refs = 7 WHERE cache_key = 'repair_a'",
[],
)
.unwrap();
store
.db
.execute(
"INSERT INTO blobs (hash, size, refcount) VALUES (?1, ?2, 9)",
params![stale_hash, b"stale indexed blob".len() as i64],
)
.unwrap();
assert_eq!(
store.blob_index_drift().unwrap(),
BlobIndexDrift {
entry_mappings: 1,
blobs: 2,
}
);
assert_eq!(
store.reconcile_blob_index().unwrap(),
BlobIndexDrift {
entry_mappings: 1,
blobs: 2,
}
);
assert_eq!(store.blob_index_drift().unwrap(), BlobIndexDrift::default());
assert_eq!(
store.reconcile_blob_index().unwrap(),
BlobIndexDrift::default()
);
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount, 2);
assert_eq!(
store
.db
.query_row(
"SELECT COUNT(*) FROM blobs WHERE hash = ?1",
params![stale_hash],
|row| row.get::<_, i64>(0),
)
.unwrap(),
0
);
let swept = store.sweep_orphan_blobs(Duration::ZERO).unwrap();
assert_eq!(swept.removed, 1);
assert!(!stale_path.exists());
}
#[test]
fn reconcile_blob_index_fails_closed_on_unreadable_metadata() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("output.rlib");
fs::write(&output, b"authoritative bytes").unwrap();
store
.put(
"repair_bad_meta",
"repairlib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(output, "librepair.rlib".to_string())],
"",
"",
)
.unwrap();
let hash = store.get("repair_bad_meta").unwrap().unwrap().files[0]
.hash
.clone();
store
.db
.execute(
"UPDATE blobs SET refcount = 9 WHERE hash = ?1",
params![hash],
)
.unwrap();
fs::write(
store.entry_dir("repair_bad_meta").join("meta.json"),
b"not json",
)
.unwrap();
let error = store.reconcile_blob_index().unwrap_err().to_string();
assert!(error.contains("parsing authoritative meta.json"), "{error}");
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount, 9, "failed repair must leave the index untouched");
}
#[test]
fn unverifiable_entries_are_removed_and_the_index_rebuilds() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
for (key, bytes) in [
("good", &b"good bytes"[..]),
("missing_blob", b"missing blob bytes"),
("resized_blob", b"resized blob bytes"),
("bad_meta", b"bad meta bytes"),
("gone_dir", b"gone dir bytes"),
] {
let output = dir.path().join(format!("{key}.rlib"));
fs::write(&output, bytes).unwrap();
store
.put(
key,
key,
&["lib".to_string()],
&[],
"host",
"dev",
&[(output, format!("lib{key}.rlib"))],
"",
"",
)
.unwrap();
}
let blob_of = |key: &str| store.blob_path(&store.get(key).unwrap().unwrap().files[0].hash);
fs::remove_file(blob_of("missing_blob")).unwrap();
let resized = blob_of("resized_blob");
let mut permissions = fs::metadata(&resized).unwrap().permissions();
#[allow(clippy::permissions_set_readonly_false)]
permissions.set_readonly(false);
fs::set_permissions(&resized, permissions).unwrap();
fs::write(&resized, b"shorter").unwrap();
fs::write(store.entry_dir("bad_meta").join("meta.json"), b"not json").unwrap();
fs::remove_dir_all(store.entry_dir("gone_dir")).unwrap();
assert!(
store.reconcile_blob_index().is_err(),
"the strict rebuild still refuses"
);
let (_, mut dropped) = store.reconcile_blob_index_dropping_unverifiable().unwrap();
dropped.sort();
assert_eq!(
dropped,
["bad_meta", "gone_dir", "missing_blob", "resized_blob"]
);
assert_eq!(
store.entry_count().unwrap(),
1,
"only the good entry's row stays"
);
for key in &dropped {
assert!(!store.entry_dir(key).exists(), "{key}");
assert!(store.get(key).unwrap().is_none(), "{key}");
}
assert!(store.get("good").unwrap().is_some());
assert!(store.blob_refcount_drift().unwrap().is_clean());
assert_eq!(
store
.reconcile_blob_index_dropping_unverifiable()
.unwrap()
.1,
Vec::<String>::new()
);
}
#[test]
fn reconcile_blob_index_rejects_each_invalid_metadata_dimension() {
for invalid_hash in [true, false] {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("output.rlib");
fs::write(&output, b"valid blob bytes").unwrap();
store
.put(
"repair_invalid_metadata",
"repairlib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(output, "librepair.rlib".to_string())],
"",
"",
)
.unwrap();
let meta_path = store.entry_dir("repair_invalid_metadata").join("meta.json");
let mut meta: EntryMeta =
serde_json::from_str(&fs::read_to_string(&meta_path).unwrap()).unwrap();
if invalid_hash {
meta.files[0].hash = "not-a-content-hash".to_string();
} else {
meta.files[0].name = "../unsafe.rlib".to_string();
}
fs::write(&meta_path, serde_json::to_vec(&meta).unwrap()).unwrap();
let error = store.reconcile_blob_index().unwrap_err().to_string();
assert!(error.contains("invalid blob metadata"), "{error}");
}
}
#[test]
fn blob_index_accepts_build_script_out_dir_artifact_names() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let flat = dir.path().join("asm.s");
fs::write(&flat, b"asm bytes").unwrap();
let nested = dir.path().join("asm.o");
fs::write(&nested, b"object bytes").unwrap();
store
.put(
"build_script_entry",
"build_script_run",
&["build-script".to_string()],
&[],
"host",
"dev",
&[
(flat, "out/asm.s".to_string()),
(nested, "out/nested/asm.o".to_string()),
],
"",
"",
)
.unwrap();
assert_eq!(store.blob_index_drift().unwrap().total(), 0);
assert_eq!(store.reconcile_blob_index().unwrap().total(), 0);
}
#[test]
fn rebuild_index_from_store_adopts_build_script_names_and_refuses_escaping_ones() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("asm.s");
fs::write(&output, b"asm bytes").unwrap();
let key = "b".repeat(64);
store
.put(
&key,
"build_script_run",
&["build-script".to_string()],
&[],
"host",
"dev",
&[(output, "out/asm.s".to_string())],
"",
"",
)
.unwrap();
store.db.execute("DELETE FROM entries", []).unwrap();
let stats = store.rebuild_index_from_store().unwrap();
assert_eq!(stats.entries_rebuilt, 1, "{stats:?}");
assert_eq!(stats.entries_skipped, 0, "{stats:?}");
assert!(store.contains(&key));
let meta_path = store.entry_dir(&key).join("meta.json");
let mut meta: EntryMeta =
serde_json::from_str(&fs::read_to_string(&meta_path).unwrap()).unwrap();
meta.files[0].name = "../escape.s".to_string();
fs::write(&meta_path, serde_json::to_vec(&meta).unwrap()).unwrap();
store.db.execute("DELETE FROM entries", []).unwrap();
let stats = store.rebuild_index_from_store().unwrap();
assert_eq!(stats.entries_rebuilt, 0, "{stats:?}");
assert_eq!(stats.entries_skipped, 1, "{stats:?}");
}
#[test]
fn put_rejects_an_artifact_name_that_escapes_the_entry_dir() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("output.rlib");
fs::write(&output, b"valid blob bytes").unwrap();
let error = store
.put(
"escaping_name",
"escapelib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(output, "../escape.rlib".to_string())],
"",
"",
)
.unwrap_err()
.to_string();
assert!(
error.contains("unsafe name"),
"a traversal must be refused before it reaches meta.json: {error}"
);
assert!(!store.contains("escaping_name"));
}
#[test]
fn store_ingest_accounts_new_blob_bytes_by_mechanism() {
let cache_dir = tempfile::tempdir().unwrap();
let config = test_config(cache_dir.path());
let store = Store::open(&config).unwrap();
let payload = b"store-ingest-accounting-unique-artifact-bytes-0xC0FFEE".repeat(64);
let output_file = cache_dir.path().join("output.rlib");
std::fs::write(&output_file, &payload).unwrap();
let before = crate::opcounts::store_reflinked_bytes()
+ crate::opcounts::store_hardlinked_bytes()
+ crate::opcounts::store_copied_bytes();
let put_result = store
.put(
"ingest_key",
"ingestlib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(output_file, "libingest.rlib".to_string())],
"",
"",
)
.unwrap();
assert_eq!(put_result.new_blobs, 1, "expected a genuinely new blob");
let after = crate::opcounts::store_reflinked_bytes()
+ crate::opcounts::store_hardlinked_bytes()
+ crate::opcounts::store_copied_bytes();
assert!(
after >= before + payload.len() as u64,
"store ingest must account the new blob's bytes (delta {} < {})",
after - before,
payload.len()
);
}
#[test]
fn test_store_put_reports_full_dup_for_existing_blob() {
let cache_dir = tempfile::tempdir().unwrap();
let config = test_config(cache_dir.path());
let store = Store::open(&config).unwrap();
let output_file = cache_dir.path().join("output.rlib");
std::fs::write(&output_file, b"fake rlib content").unwrap();
let put_result = store
.put(
"first_key",
"mylib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(output_file.clone(), "libmylib.rlib".to_string())],
"",
"",
)
.unwrap();
assert_eq!(put_result.output_blobs, 1);
assert_eq!(put_result.duplicate_blobs, 0);
assert_eq!(put_result.new_blobs, 1);
assert!(!put_result.is_full_dup());
let meta = store.get("first_key").unwrap().unwrap();
let hash = meta.files[0].hash.clone();
assert!(store.blob_path(&hash).is_file());
let duplicate_output = cache_dir.path().join("duplicate-output.rlib");
std::fs::write(&duplicate_output, b"fake rlib content").unwrap();
let second_put = store
.put(
"second_key",
"mylib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(duplicate_output, "libmylib.rlib".to_string())],
"",
"",
)
.unwrap();
assert_eq!(second_put.output_blobs, 1);
assert_eq!(second_put.duplicate_blobs, 1);
assert_eq!(second_put.new_blobs, 0);
assert!(second_put.is_full_dup());
store.remove_entry("first_key").unwrap();
assert!(store.blob_path(&hash).exists());
store.remove_entry("second_key").unwrap();
assert!(!store.blob_path(&hash).exists());
}
#[test]
fn test_retryable_sqlite_open_error_for_missing_parent() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("missing").join("index.db");
let err = open_index_db(&db_path).unwrap_err();
let sql_err = err.downcast_ref::<SqlError>().unwrap();
assert!(is_retryable_sqlite_open_error(sql_err));
}
#[test]
fn is_corruption_error_flags_a_non_sqlite_file() {
let dir = tempfile::tempdir().unwrap();
let garbage = dir.path().join("garbage.db");
fs::write(&garbage, b"definitely not a sqlite database").unwrap();
let err = try_open_index_db(&garbage).unwrap_err();
assert!(
is_corruption_error(&err),
"a non-sqlite file must classify as corruption: {err}"
);
let missing = dir.path().join("missing").join("index.db");
let err = try_open_index_db(&missing).unwrap_err();
assert!(!is_corruption_error(&err));
}
fn key(seed: u8) -> String {
blake3::hash(&[seed]).to_hex().to_string()
}
fn put_entry(store: &Store, dir: &Path, seed: u8, crate_name: &str, content: &[u8]) -> String {
let k = key(seed);
let src = dir.join(format!("out-{seed}.rlib"));
std::fs::write(&src, content).unwrap();
store
.put(
&k,
crate_name,
&["lib".to_string()],
&["std".to_string()],
"x86_64-unknown-linux-gnu",
"dev",
&[(src, format!("lib{crate_name}.rlib"))],
"",
"",
)
.unwrap();
let _ = std::fs::remove_file(dir.join(format!("out-{seed}.rlib")));
k
}
#[test]
fn rebuild_index_from_store_recovers_entries_after_the_index_is_lost() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let (k1, k2) = {
let store = Store::open(&config).unwrap();
let k1 = put_entry(&store, dir.path(), 1, "alpha", b"alpha rlib content");
let k2 = put_entry(&store, dir.path(), 2, "beta", b"beta rlib content");
(k1, k2)
};
std::fs::remove_file(config.index_db_path()).unwrap();
let store = Store::open(&config).unwrap();
assert_eq!(
store.entry_count().unwrap(),
0,
"a fresh index starts with no rows"
);
let stats = store.rebuild_index_from_store().unwrap();
assert_eq!(
stats.entries_rebuilt, 2,
"both entries are adopted: {stats:?}"
);
assert_eq!(stats.blobs_registered, 2);
for k in [&k1, &k2] {
assert!(store.contains(k), "entry {k} must be usable after rebuild");
let meta = store.get(k).unwrap().unwrap();
assert_eq!(meta.files.len(), 1);
}
assert_eq!(store.entry_count().unwrap(), 2);
}
#[test]
fn rebuild_index_is_idempotent_and_does_not_inflate_refcounts() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let k = put_entry(&store, dir.path(), 3, "gamma", b"gamma rlib content");
let hash: String = store
.db
.query_row("SELECT hash FROM blobs", [], |r| r.get(0))
.unwrap();
let refcount_before: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|r| r.get(0),
)
.unwrap();
for _ in 0..3 {
let stats = store.rebuild_index_from_store().unwrap();
assert_eq!(
stats.entries_rebuilt, 0,
"an already-registered entry is not re-adopted"
);
}
let refcount_after: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|r| r.get(0),
)
.unwrap();
assert_eq!(
refcount_after, refcount_before,
"repeated rebuilds must not inflate refcounts"
);
store.remove_entry(&k).unwrap();
let remaining: i64 = store
.db
.query_row(
"SELECT COUNT(*) FROM blobs WHERE hash = ?1",
params![hash],
|r| r.get(0),
)
.unwrap();
assert_eq!(
remaining, 0,
"blob must be reclaimed on removal, not stranded by an inflated refcount"
);
}
#[test]
fn rebuild_index_skips_entries_whose_blobs_are_missing_or_wrong_size() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let (good, gone, truncated) = {
let store = Store::open(&config).unwrap();
let good = put_entry(&store, dir.path(), 4, "good", b"good content here");
let gone = put_entry(&store, dir.path(), 5, "gone", b"vanishing content");
let truncated = put_entry(&store, dir.path(), 6, "trunc", b"truncated content");
(good, gone, truncated)
};
let meta_of = |k: &str| -> EntryMeta {
let p = config.store_dir().join(k).join("meta.json");
serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
};
let gone_hash = meta_of(&gone).files[0].hash.clone();
let trunc_hash = meta_of(&truncated).files[0].hash.clone();
let blob_of = |h: &str| blob_path_in_store_dir(&config.store_dir(), h);
let gone_blob = blob_of(&gone_hash);
let trunc_blob = blob_of(&trunc_hash);
let make_writable = |p: &Path| {
let mut perms = std::fs::metadata(p).unwrap().permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
std::fs::set_permissions(p, perms).unwrap();
};
make_writable(&gone_blob);
make_writable(&trunc_blob);
std::fs::remove_file(&gone_blob).unwrap();
std::fs::write(&trunc_blob, b"short").unwrap();
std::fs::remove_file(config.index_db_path()).unwrap();
let store = Store::open(&config).unwrap();
let stats = store.rebuild_index_from_store().unwrap();
assert_eq!(stats.entries_rebuilt, 1, "only the intact entry: {stats:?}");
assert_eq!(stats.entries_skipped, 2);
assert!(store.contains(&good));
assert!(
!store.contains(&gone),
"an entry with a missing blob must not be registered"
);
assert!(
!store.contains(&truncated),
"an entry with a wrong-sized blob must not be registered"
);
}
#[test]
fn rebuild_index_validates_artifact_names_and_hashes_independently() {
for (name, invalid_hash, accepted) in [
("foo.rlib", false, true),
("../escape", false, false),
("foo.rlib", true, false),
] {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let key = {
let store = Store::open(&config).unwrap();
put_entry(&store, dir.path(), 30, "foo", b"compiled artifact")
};
let meta_path = config.store_dir().join(&key).join("meta.json");
let mut meta: EntryMeta = serde_json::from_slice(&fs::read(&meta_path).unwrap()).unwrap();
meta.files[0].name = name.into();
if invalid_hash {
let original = blob_path_in_store_dir(&config.store_dir(), &meta.files[0].hash);
meta.files[0].hash = "g".repeat(64);
let malformed = blob_path_in_store_dir(&config.store_dir(), &meta.files[0].hash);
fs::create_dir_all(malformed.parent().unwrap()).unwrap();
fs::copy(original, malformed).unwrap();
}
fs::write(meta_path, serde_json::to_vec(&meta).unwrap()).unwrap();
fs::remove_file(config.index_db_path()).unwrap();
let store = Store::open(&config).unwrap();
let stats = store.rebuild_index_from_store().unwrap();
assert_eq!(
(
stats.entries_rebuilt,
stats.entries_skipped,
stats.blobs_registered
),
if accepted { (1, 0, 1) } else { (0, 1, 0) },
"name={name:?}, invalid_hash={invalid_hash}"
);
assert_eq!(store.contains(&key), accepted);
}
}
#[test]
fn rebuild_index_ignores_the_blobs_dir_and_foreign_names() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
{
let store = Store::open(&config).unwrap();
put_entry(&store, dir.path(), 7, "delta", b"delta rlib content");
}
std::fs::create_dir_all(config.store_dir().join("not-a-cache-key")).unwrap();
std::fs::create_dir_all(config.store_dir().join("0123456789")).unwrap();
std::fs::remove_file(config.index_db_path()).unwrap();
let store = Store::open(&config).unwrap();
let stats = store.rebuild_index_from_store().unwrap();
assert_eq!(
stats.entries_rebuilt, 1,
"only the real entry dir is adopted: {stats:?}"
);
}
#[test]
fn store_open_rebuilds_automatically_after_quarantining_a_corrupt_index() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let k = {
let store = Store::open(&config).unwrap();
put_entry(&store, dir.path(), 8, "epsilon", b"epsilon rlib content")
};
std::fs::write(config.index_db_path(), b"not a sqlite database at all").unwrap();
for ext in ["-wal", "-shm"] {
let p = index_sidecar_path(&config.index_db_path(), ext);
let _ = std::fs::remove_file(p);
}
let store = Store::open(&config).expect("corrupt index must self-heal");
assert!(
store.contains(&k),
"the entry must be recovered by Store::open, not lost to an empty index"
);
assert_eq!(store.entry_count().unwrap(), 1);
}
#[test]
fn open_index_db_self_heals_a_corrupt_index() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("index.db");
fs::write(
&db_path,
b"this is not a sqlite database; it is garbage bytes",
)
.unwrap();
let db = open_index_db(&db_path).expect("a corrupt index must self-heal, not brick");
let count: i64 = db
.query_row("SELECT COUNT(*) FROM entries", [], |r| r.get(0))
.expect("recreated index must be queryable");
assert_eq!(count, 0, "the recreated index starts empty");
assert!(db_path.is_file(), "a fresh index.db is recreated in place");
let quarantined: Vec<_> = fs::read_dir(dir.path())
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().contains(".corrupt-"))
.collect();
assert_eq!(
quarantined.len(),
1,
"the corrupt index is quarantined (kept for forensics), not silently deleted"
);
}
#[test]
fn quarantine_corrupt_index_moves_wal_and_shm_sidecars() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("index.db");
fs::write(&db_path, b"corrupt").unwrap();
fs::write(dir.path().join("index.db-wal"), b"wal").unwrap();
fs::write(dir.path().join("index.db-shm"), b"shm").unwrap();
let quarantined = quarantine_corrupt_index(&db_path).unwrap();
assert!(quarantined.is_file());
assert!(!db_path.exists(), "the corrupt db is moved aside");
assert!(
!dir.path().join("index.db-wal").exists(),
"the -wal sidecar is moved aside"
);
assert!(
!dir.path().join("index.db-shm").exists(),
"the -shm sidecar is moved aside"
);
assert!(index_sidecar_path(&quarantined, "-wal").exists());
assert!(index_sidecar_path(&quarantined, "-shm").exists());
}
#[test]
fn recover_corrupt_index_reuses_a_peer_healed_db_without_requarantine() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("index.db");
let garbage = dir.path().join("garbage.db");
fs::write(&garbage, b"not a sqlite database").unwrap();
let err = try_open_index_db(&garbage).unwrap_err();
drop(try_open_index_db(&db_path).unwrap());
let (db, recovered) = recover_corrupt_index(&db_path, &err).unwrap();
let count: i64 = db
.query_row("SELECT COUNT(*) FROM entries", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 0);
assert!(
!recovered,
"adopting a peer's healed DB must not claim the rebuild: the peer that \
quarantined it owns that, and two processes rebuilding at once would \
double-count blob refcounts"
);
let quarantined = fs::read_dir(dir.path())
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().contains(".corrupt-"))
.count();
assert_eq!(
quarantined, 0,
"a healthy DB on re-check must not be quarantined"
);
}
#[test]
fn test_store_open_creates_cache_root() {
let dir = tempfile::tempdir().unwrap();
let cache_dir = dir.path().join("nested").join("cache");
let config = test_config(&cache_dir);
let _store = Store::open(&config).unwrap();
assert!(cache_dir.is_dir());
assert!(config.store_dir().is_dir());
assert!(config.index_db_path().is_file());
}
#[test]
fn test_store_eviction() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("big.rlib");
std::fs::write(&output_file, vec![0u8; 200]).unwrap();
store
.put(
"key1",
"big_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file.clone(), "libbig.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output_file);
let recent = store.evict().unwrap();
assert_eq!(recent.entries_recent_prefiltered, 1);
assert_eq!(recent.entries_pinned, 1);
assert_eq!(recent.entries_evicted, 0);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = 'key1'",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert!(stats.entries_evicted > 0);
assert!(!store.contains("key1"));
}
fn store_with_one_evictable_entry(dir: &Path, key: &str) -> (Store, Config) {
let mut config = test_config(dir);
config.max_size = 100;
let store = Store::open(&config).unwrap();
let output_file = dir.join("big.rlib");
std::fs::write(&output_file, vec![0u8; 200]).unwrap();
store
.put(
key,
"big_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file.clone(), "libbig.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output_file);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = ?1",
params![key],
)
.unwrap();
(store, config)
}
#[test]
fn evict_counts_an_entry_it_fails_to_remove() {
let dir = tempfile::tempdir().unwrap();
let (store, _config) = store_with_one_evictable_entry(dir.path(), "broken");
std::fs::write(store.entry_dir("broken").join("meta.json"), b"{not json").unwrap();
let stats = store.evict().unwrap();
assert_eq!(stats.entries_evicted, 0);
assert_eq!(
stats.entries_failed, 1,
"the refused removal is counted: {stats:?}"
);
assert_eq!(stats.entries_locked, 0, "bad data is not lock contention");
assert_eq!(store.entry_count().unwrap(), 1);
}
#[test]
fn disk_pressure_stops_before_scanning_and_between_removals_below_the_size_budget() {
let dir = tempfile::tempdir().unwrap();
let (mut store, _) = store_with_one_evictable_entry(dir.path(), "first");
let (other, _) = store_with_one_evictable_entry(dir.path(), "second");
drop(other);
store.config.max_size = u64::MAX;
assert_eq!(store.evict().unwrap().entries_evicted, 0);
assert_eq!(
store
.evict_for_disk_pressure(|| true)
.unwrap()
.entries_evicted,
0
);
assert!(store.contains("first") && store.contains("second"));
let mut checks = 0;
let stats = store
.evict_for_disk_pressure(|| {
checks += 1;
checks >= 3
})
.unwrap();
assert_eq!(stats.entries_evicted, 1);
assert_eq!(checks, 3);
assert_eq!(store.list_entries("last_accessed").unwrap().len(), 1);
assert_eq!(
store
.evict_for_disk_pressure(|| false)
.unwrap()
.entries_evicted,
1
);
}
#[test]
fn evict_counts_lock_contention_apart_from_bad_data() {
let dir = tempfile::tempdir().unwrap();
let (store, config) = store_with_one_evictable_entry(dir.path(), "busy");
store.db.busy_timeout(Duration::from_millis(50)).unwrap();
let blocker = Connection::open(config.index_db_path()).unwrap();
blocker.execute_batch("BEGIN EXCLUSIVE").unwrap();
let stats = store.evict().unwrap();
blocker.execute_batch("ROLLBACK").unwrap();
assert_eq!(stats.entries_evicted, 0);
assert_eq!(stats.entries_failed, 1, "{stats:?}");
assert_eq!(
stats.entries_locked, 1,
"contention is counted as locked: {stats:?}"
);
}
#[test]
fn evict_waits_for_a_competing_writer_and_still_evicts() {
static REMOVAL_WAITED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
fn wait_for_lock(count: i32) -> bool {
REMOVAL_WAITED.store(true, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(1));
count < 5000
}
let dir = tempfile::tempdir().unwrap();
let (store, config) = store_with_one_evictable_entry(dir.path(), "contended");
store.db.busy_handler(Some(wait_for_lock)).unwrap();
let competitor = Connection::open(config.index_db_path()).unwrap();
competitor.execute_batch("BEGIN IMMEDIATE").unwrap();
competitor
.execute("UPDATE entries SET durable = durable", [])
.unwrap();
let committer = std::thread::spawn(move || {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !REMOVAL_WAITED.load(Ordering::SeqCst) && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(1));
}
competitor.execute_batch("COMMIT").unwrap();
});
let stats = store.evict().unwrap();
committer.join().unwrap();
assert!(
REMOVAL_WAITED.load(Ordering::SeqCst),
"the removal never waited for the lock: {stats:?}"
);
assert_eq!(stats.entries_locked, 0, "{stats:?}");
assert_eq!(stats.entries_evicted, 1, "{stats:?}");
assert!(!store.contains("contended"));
}
#[test]
fn is_sqlite_contention_matches_busy_and_locked_only() {
let sqlite = |code: i32| {
anyhow::Error::from(SqlError::SqliteFailure(
rusqlite::ffi::Error::new(code),
None,
))
.context("removing entry")
};
assert!(is_sqlite_contention(&sqlite(rusqlite::ffi::SQLITE_BUSY)));
assert!(is_sqlite_contention(&sqlite(rusqlite::ffi::SQLITE_LOCKED)));
assert!(is_sqlite_busy_snapshot(&sqlite(
rusqlite::ffi::SQLITE_BUSY_SNAPSHOT
)));
assert!(!is_sqlite_busy_snapshot(&sqlite(
rusqlite::ffi::SQLITE_BUSY
)));
assert!(!is_sqlite_busy_snapshot(&sqlite(
rusqlite::ffi::SQLITE_LOCKED
)));
assert!(!is_sqlite_contention(&sqlite(
rusqlite::ffi::SQLITE_CORRUPT
)));
assert!(!is_sqlite_contention(&anyhow::anyhow!(
"meta.json unparseable"
)));
let mut stats = GcStats::default();
record_eviction_failure(&mut stats, &sqlite(rusqlite::ffi::SQLITE_BUSY_SNAPSHOT));
record_eviction_failure(&mut stats, &sqlite(rusqlite::ffi::SQLITE_BUSY));
record_eviction_failure(&mut stats, &anyhow::anyhow!("meta.json unparseable"));
assert_eq!(stats.entries_failed, 3);
assert_eq!(stats.entries_locked, 2);
assert_eq!(stats.entries_busy_snapshot, 1);
}
#[test]
fn evict_leaves_an_entry_whose_blob_is_still_hardlinked_outside() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let mut store = Store::open(&config).unwrap();
let output_file = dir.path().join("big.rlib");
std::fs::write(&output_file, vec![0u8; 200]).unwrap();
store
.put(
"kept",
"big_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file.clone(), "libbig.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output_file);
let meta = store.get("kept").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let retainer = dir.path().join("worktree-copy.rlib");
std::fs::hard_link(&blob, &retainer).unwrap();
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = 'kept'",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(stats.entries_evicted, 0, "clone still holds the blocks");
assert_eq!(
stats.entries_unreclaimable, 1,
"the skip must be counted as unreclaimable, not as a pin: {stats:?}"
);
assert!(store.contains("kept"), "entry remains restorable");
assert!(blob.is_file(), "store name remains");
let pressure = store.evict_for_disk_pressure(|| false).unwrap();
assert_eq!(pressure.entries_evicted, 0);
assert_eq!(pressure.entries_unreclaimable, 1);
assert!(store.contains("kept"));
assert!(retainer.is_file());
store.config.gc_evict_shared = true;
let stats = store.evict().unwrap();
assert_eq!(stats.entries_evicted, 1);
assert_eq!(stats.bytes_freed, 200);
assert_eq!(stats.disk_bytes_reclaimed, 0);
assert!(!store.contains("kept"));
assert!(
retainer.is_file(),
"compatibility mode drops the store name, not the retained blocks"
);
}
#[test]
fn shared_entry_retention_requires_the_last_positive_reference() {
assert!(holds_last_reference(1, 1));
assert!(holds_last_reference(2, 2));
assert!(holds_last_reference(1, 2));
assert!(!holds_last_reference(2, 1));
assert!(!holds_last_reference(0, 1));
assert!(!holds_last_reference(-1, 1));
}
#[cfg(unix)]
#[test]
fn evict_leaves_an_entry_whose_blob_is_still_reflinked_outside() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("big.rlib");
std::fs::write(&output_file, vec![0u8; 4096]).unwrap();
store
.put(
"kept-reflink",
"big_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file.clone(), "libbig.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output_file);
let meta = store.get("kept-reflink").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let retainer = dir.path().join("worktree-reflink.rlib");
if crate::link::try_reflink(&blob, &retainer).is_err() {
return;
}
let sharing = crate::sharing::probe(&blob, 4096);
if !sharing.shared || sharing.private_bytes != 0 {
return;
}
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = 'kept-reflink'",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(stats.entries_evicted, 0);
assert!(stats.entries_unreclaimable > 0);
assert!(store.contains("kept-reflink"));
store.remove_clone_for_test(&retainer);
let stats = store.evict().unwrap();
assert!(stats.entries_evicted > 0);
assert!(!store.contains("kept-reflink"));
}
#[test]
fn durable_upload_intent_pins_payload_across_every_eviction_policy_until_retired() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
let pending_key = "a".repeat(64);
let newer_twin_key = "b".repeat(64);
let pending_output = dir.path().join("pending.rlib");
let newer_output = dir.path().join("newer.rlib");
fs::write(&pending_output, vec![0u8; 200]).unwrap();
fs::write(&newer_output, vec![1u8; 200]).unwrap();
store
.put(
&pending_key,
"pending",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(pending_output.clone(), "libshared.rlib".to_string())],
"",
"",
)
.unwrap();
store.set_last_accessed_for_test(&pending_key, "-48 hours");
let duplicate_group = store
.db
.query_row(
"SELECT content_hash FROM entries WHERE cache_key = ?1",
params![pending_key.as_str()],
|row| row.get::<_, String>(0),
)
.unwrap();
store
.put(
&newer_twin_key,
"newer",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(newer_output.clone(), "libshared.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&pending_output);
store.remove_clone_for_test(&newer_output);
store
.db
.execute(
"UPDATE entries SET content_hash = ?1 WHERE cache_key = ?2",
params![duplicate_group, newer_twin_key.as_str()],
)
.unwrap();
let spool_dir = config.upload_spool_dir();
fs::create_dir_all(&spool_dir).unwrap();
let intent = spool_dir.join(format!("{pending_key}.json"));
fs::write(&intent, b"{malformed").unwrap();
let size = store.evict().unwrap();
assert!(size.entries_pinned >= 1);
assert!(store.contains(&pending_key));
let pressure = store.evict_for_disk_pressure(|| false).unwrap();
assert!(pressure.entries_pinned >= 1);
assert!(store.contains(&pending_key));
let age = store.evict_older_than(24).unwrap();
assert_eq!(age.entries_pinned, 1);
assert!(store.contains(&pending_key));
let duplicate = store.evict_duplicate_entries().unwrap();
assert_eq!(duplicate.entries_pinned, 1);
assert!(store.contains(&pending_key));
fs::remove_file(intent).unwrap();
let retired = store.evict_duplicate_entries().unwrap();
assert_eq!(retired.entries_evicted, 1);
assert!(!store.contains(&pending_key));
assert!(store.contains(&newer_twin_key));
}
#[test]
fn durable_upload_key_enumeration_is_bounded_and_fails_closed_on_read_error() {
let key = "c".repeat(64);
let names = [Ok::<_, std::io::Error>(std::ffi::OsString::from(format!(
"{key}.json"
)))];
let keys = Store::durable_upload_keys_from_names(names, 1).unwrap();
assert_eq!(keys, std::collections::HashSet::from([key]));
let overflow = Store::durable_upload_keys_from_names(
[
Ok::<_, std::io::Error>(std::ffi::OsString::from("junk")),
Ok::<_, std::io::Error>(std::ffi::OsString::from("more-junk")),
],
1,
)
.unwrap_err();
assert!(format!("{overflow:#}").contains("exceeds 1 jobs"));
let unreadable = Store::durable_upload_keys_from_names(
[Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"injected unreadable spool entry",
))],
1,
)
.unwrap_err();
assert!(format!("{unreadable:#}").contains("injected unreadable spool entry"));
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
assert!(
store.durable_upload_keys().unwrap().is_empty(),
"a missing spool directory is the one empty-set case"
);
fs::write(config.upload_spool_dir(), b"not a directory").unwrap();
let blocked = store.durable_upload_keys().unwrap_err();
assert!(
format!("{blocked:#}").contains("reading"),
"a non-directory spool path must fail closed: {blocked:#}"
);
}
#[test]
fn eviction_records_a_tombstone_and_a_later_lookup_marks_demand() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100; let store = Store::open(&config).unwrap();
let out = dir.path().join("big.rlib");
fs::write(&out, vec![0u8; 4096]).unwrap();
store
.put_with_compile_time(
"doomed",
"c",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(out.clone(), "libbig.rlib".to_string())],
"",
"",
2500,
)
.unwrap();
store.remove_clone_for_test(&out);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-2 hours')",
[],
)
.unwrap();
assert!(
store.evict().unwrap().entries_evicted > 0,
"expected eviction"
);
let (key, policy, cost, demanded): (String, String, i64, Option<String>) = store
.db
.query_row(
"SELECT cache_key, policy, compile_time_ms, demanded_at FROM eviction_tombstones",
[],
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
)
.unwrap();
assert_eq!(key, "doomed");
assert_eq!(policy, "size-pressure", "records which policy chose it");
assert_eq!(cost, 2500, "records the rebuild cost that was destroyed");
assert!(demanded.is_none(), "not demanded yet");
assert_eq!(store.tombstone_stats().unwrap(), (1, 0));
assert!(store.get("doomed").unwrap().is_none());
assert_eq!(store.tombstone_stats().unwrap(), (1, 1));
assert!(store.get("never_existed").unwrap().is_none());
assert_eq!(store.tombstone_stats().unwrap(), (1, 1));
}
#[test]
fn tombstone_demand_records_only_the_first_request() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
store
.db
.execute(
"INSERT INTO eviction_tombstones (cache_key, evicted_at, demanded_at)
VALUES ('k', datetime('now','-1 hour'), NULL)",
[],
)
.unwrap();
store.note_tombstone_demand("k");
let first: String = store
.db
.query_row(
"SELECT demanded_at FROM eviction_tombstones WHERE cache_key='k'",
[],
|r| r.get(0),
)
.unwrap();
store.note_tombstone_demand("k");
let second: String = store
.db
.query_row(
"SELECT demanded_at FROM eviction_tombstones WHERE cache_key='k'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(first, second, "first demand must not be overwritten");
}
#[test]
fn tombstones_are_pruned_by_age_and_re_eviction_resets_the_record() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
store
.db
.execute(
"INSERT INTO eviction_tombstones (cache_key, evicted_at) VALUES
('old', datetime('now','-30 days')),
('recent', datetime('now','-1 day'))",
[],
)
.unwrap();
assert_eq!(store.prune_tombstones(14).unwrap(), 1);
assert_eq!(store.tombstone_stats().unwrap().0, 1, "recent one survives");
store
.db
.execute(
"UPDATE eviction_tombstones SET demanded_at = datetime('now') WHERE cache_key='recent'",
[],
)
.unwrap();
let features = crate::eviction::EntryFeatures {
key: "recent".into(),
size: 1,
hit_count: 0,
idle_hours: 5.0,
content_hash: None,
committed: true,
compile_time_ms: 10,
reclaimable_bytes: None,
recently_accessed: false,
recently_imported: false,
};
store.record_tombstone(&features, "size-pressure", Some(("value-density", false)));
assert_eq!(
store.tombstone_stats().unwrap(),
(1, 0),
"re-eviction restarts the observation"
);
}
#[test]
fn put_records_compile_time_and_eviction_can_see_it() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let out = dir.path().join("out.rlib");
fs::write(&out, b"artifact").unwrap();
store
.put_with_compile_time(
"costly",
"c",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(out, "libout.rlib".to_string())],
"",
"",
4321,
)
.unwrap();
let indexed: i64 = store
.db
.query_row(
"SELECT compile_time_ms FROM entries WHERE cache_key = 'costly'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(indexed, 4321, "put must index the rebuild cost");
let features = store.eviction_candidates().unwrap();
let entry = features.iter().find(|e| e.key == "costly").unwrap();
assert_eq!(
entry.compile_time_ms, 4321,
"eviction must see rebuild cost (#594)"
);
}
#[test]
fn backfill_compile_times_recovers_pre_index_entries() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let out = dir.path().join("out.rlib");
fs::write(&out, b"artifact").unwrap();
store
.put_with_compile_time(
"legacy",
"c",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(out, "libout.rlib".to_string())],
"",
"",
7777,
)
.unwrap();
store
.db
.execute("UPDATE entries SET compile_time_ms = 0", [])
.unwrap();
assert_eq!(store.backfill_compile_times().unwrap(), 1);
let restored: i64 = store
.db
.query_row(
"SELECT compile_time_ms FROM entries WHERE cache_key = 'legacy'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(restored, 7777);
assert_eq!(store.backfill_compile_times().unwrap(), 0);
}
#[test]
fn backfill_compile_times_is_bounded_per_sweep_and_converges() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
const LIMIT: i64 = 3;
let total = LIMIT + 2;
for i in 0..total {
let out = dir.path().join(format!("o{i}.rlib"));
fs::write(&out, format!("artifact-{i}")).unwrap();
store
.put_with_compile_time(
&format!("k{i}"),
"c",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(out, format!("libo{i}.rlib"))],
"",
"",
100,
)
.unwrap();
}
store
.db
.execute("UPDATE entries SET compile_time_ms = 0", [])
.unwrap();
let first = store.backfill_compile_times_limited(LIMIT).unwrap();
assert_eq!(
first, LIMIT as usize,
"one sweep must not backfill the whole store"
);
let second = store.backfill_compile_times_limited(LIMIT).unwrap();
assert_eq!(second, 2, "the remainder converges on the next sweep");
assert_eq!(store.backfill_compile_times_limited(LIMIT).unwrap(), 0);
}
#[test]
fn size_pressure_policy_matches_the_sql_ordering_it_replaced() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let seed = [
("huge_stale", 600 * 1024 * 1024_i64, 0_i64, 15.0_f64),
("small_hot", 14 * 1024, 9, 0.1),
("mid", 5 * 1024 * 1024, 2, 48.0),
("zero_size", 0, 0, 3.0),
("just_touched", 1024 * 1024, 1, 0.0),
("ancient_tiny", 512, 0, 5000.0),
("twin_a", 2 * 1024 * 1024, 3, 12.0),
("twin_b", 2 * 1024 * 1024, 3, 12.0),
];
for (key, size, hits, idle) in seed {
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, hit_count, committed, last_accessed)
VALUES (?1, 'c', ?2, ?3, 1, datetime('now', ?4))",
params![key, size, hits, format!("-{} seconds", (idle * 3600.0) as i64)],
)
.unwrap();
}
let sql_order: Vec<String> = {
let mut stmt = store
.db
.prepare(
"SELECT cache_key FROM entries
ORDER BY
CAST((hit_count + 1) AS REAL)
/ (MAX((julianday('now') - julianday(last_accessed)) * 24.0, 0.01)
* MAX(size / 1048576.0, 0.001))
ASC",
)
.unwrap();
stmt.query_map([], |r| r.get(0))
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
let candidates = store.eviction_candidates().unwrap();
let policy_order = crate::eviction::SizePressurePolicy.select(&candidates);
let score_of: std::collections::HashMap<&str, f64> = candidates
.iter()
.map(|e| (e.key.as_str(), crate::eviction::size_pressure_score(e)))
.collect();
let seq =
|order: &[String]| -> Vec<f64> { order.iter().map(|k| score_of[k.as_str()]).collect() };
assert_eq!(
seq(&sql_order),
seq(&policy_order),
"policy ranking diverged from the SQL it replaced\n sql: {sql_order:?}\n policy: {policy_order:?}"
);
assert_eq!(policy_order.len(), seed.len(), "every entry must be ranked");
}
#[test]
fn older_than_matches_former_sql_while_duplicate_fails_closed() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
for (key, idle_h, hash) in [
("stale", 100.0_f64, Some("h1")),
("fresh", 1.0, Some("h1")),
("boundary", 24.0, None),
("lonely", 200.0, Some("h2")),
] {
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed, content_hash, last_accessed)
VALUES (?1, 'c', 100, 1, ?2, datetime('now', ?3))",
params![key, hash, format!("-{} seconds", (idle_h * 3600.0) as i64)],
)
.unwrap();
}
let candidates = store.eviction_candidates().unwrap();
let sql_old: Vec<String> = {
let mut stmt = store
.db
.prepare(
"SELECT cache_key FROM entries WHERE last_accessed < datetime('now', '-24 hours')",
)
.unwrap();
stmt.query_map([], |r| r.get(0))
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
let mut policy_old = crate::eviction::OlderThanPolicy { hours: 24 }.select(&candidates);
policy_old.sort();
let unambiguous = |v: &[String]| -> Vec<String> {
let mut v: Vec<String> = v.iter().filter(|k| *k != "boundary").cloned().collect();
v.sort();
v
};
assert_eq!(
unambiguous(&policy_old),
unambiguous(&sql_old),
"older-than selection diverged away from the cutoff boundary"
);
let sql_dup: Vec<String> = {
let mut stmt = store
.db
.prepare(
"SELECT e.cache_key FROM entries e
JOIN (SELECT content_hash, MAX(last_accessed) AS newest
FROM entries WHERE content_hash IS NOT NULL AND committed = 1
GROUP BY content_hash HAVING COUNT(*) > 1) d
ON e.content_hash = d.content_hash
WHERE e.last_accessed < d.newest AND e.committed = 1",
)
.unwrap();
stmt.query_map([], |r| r.get(0))
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
};
let mut policy_dup = crate::eviction::DuplicatePolicy.select(&candidates);
let mut sql_dup_sorted = sql_dup.clone();
policy_dup.sort();
sql_dup_sorted.sort();
assert_eq!(
sql_dup_sorted,
vec!["stale"],
"former SQL selected the older twin without proving reclaimed bytes"
);
assert!(
policy_dup.is_empty(),
"unmapped legacy victims must fail closed on unknown marginal bytes"
);
}
#[test]
fn evict_skips_recently_accessed_entry() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("big.rlib");
std::fs::write(&output_file, vec![0u8; 200]).unwrap();
store
.put(
"live_key",
"live_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output_file.clone(), "libbig.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output_file);
let stats = store.evict().unwrap();
assert_eq!(
stats.entries_evicted, 0,
"a recently-accessed entry must be pinned against eviction"
);
assert_eq!(
stats.entries_pinned, 1,
"and it must be COUNTED as held back — that count is the whole \
difference between `evicted 0 entries` reading as a broken GC and \
explaining itself (#509)"
);
assert!(store.contains("live_key"));
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = 'live_key'",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert!(stats.entries_evicted > 0);
assert!(!store.contains("live_key"));
}
fn import_test_entry(store: &Store, key: &str, bytes: usize) {
let entry_dir = store.entry_dir(key);
std::fs::create_dir_all(&entry_dir).unwrap();
let content = vec![b'i'; bytes];
std::fs::write(entry_dir.join("lib.rlib"), &content).unwrap();
let meta = EntryMeta {
cache_key: key.to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: format!("{key}_crate"),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: bytes as u64,
hash: blake3::hash(&content).to_hex().to_string(),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: "x86_64-unknown-linux-gnu".to_string(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
std::fs::write(
entry_dir.join("meta.json"),
serde_json::to_vec_pretty(&meta).unwrap(),
)
.unwrap();
store.import_downloaded_entry(key).unwrap();
}
fn put_test_entry(store: &Store, dir: &Path, key: &str, bytes: usize) {
let output = dir.join(format!("{key}.rlib"));
std::fs::write(&output, vec![b'b'; bytes]).unwrap();
store
.put(
key,
&format!("{key}_crate"),
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output.clone(), format!("lib{key}.rlib"))],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output);
}
fn set_idle_past_grace(store: &Store) {
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
}
#[test]
fn an_automatic_sweep_keeps_what_the_remote_just_delivered() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
import_test_entry(&store, "imported_key", 300);
put_test_entry(&store, dir.path(), "built_key", 300);
set_idle_past_grace(&store);
let automatic = store.evict_for(SweepOrigin::Automatic).unwrap();
assert!(store.contains("imported_key"), "the job's import survives");
assert!(!store.contains("built_key"), "the sweep still frees space");
assert_eq!(automatic.entries_import_pinned, 1);
assert_eq!(automatic.entries_pinned, 1, "counted as held back too");
let pressure = store.evict_for_disk_pressure(|| false).unwrap();
assert_eq!(pressure.entries_import_pinned, 1);
assert!(store.contains("imported_key"));
let requested = store.evict().unwrap();
assert!(!store.contains("imported_key"), "`kache gc` can reclaim it");
assert_eq!(requested.entries_import_pinned, 0);
}
#[test]
fn an_import_is_kept_only_within_the_import_pin() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
import_test_entry(&store, "inside_key", 300);
import_test_entry(&store, "outside_key", 300);
set_idle_past_grace(&store);
assert_eq!(IMPORT_PIN.as_secs(), 21_600);
let pin = IMPORT_PIN.as_secs() as i64;
let age = |key: &str, secs: i64| {
store
.db
.execute(
"UPDATE entries SET imported_at = unixepoch() - ?1 WHERE cache_key = ?2",
params![secs, key],
)
.unwrap();
};
age("inside_key", pin - 60);
age("outside_key", pin + 60);
let candidates = store
.eviction_candidates_for(SweepOrigin::Automatic)
.unwrap();
let imported = |key: &str| {
candidates
.iter()
.find(|entry| entry.key == key)
.unwrap()
.recently_imported
};
assert!(imported("inside_key"));
assert!(!imported("outside_key"));
let stats = store.evict_for(SweepOrigin::Automatic).unwrap();
assert!(store.contains("inside_key"));
assert!(!store.contains("outside_key"));
assert_eq!(stats.entries_import_pinned, 1);
}
#[test]
fn only_imports_are_stamped_and_only_automatic_sweeps_keep_them() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
import_test_entry(&store, "imported_key", 10);
put_test_entry(&store, dir.path(), "built_key", 10);
let stamped = |key: &str| -> bool {
store
.db
.query_row(
"SELECT imported_at IS NOT NULL FROM entries WHERE cache_key = ?1",
params![key],
|row| row.get(0),
)
.unwrap()
};
assert!(stamped("imported_key"));
assert!(!stamped("built_key"));
let flags = |origin| -> Vec<(String, bool)> {
let mut flags: Vec<_> = store
.eviction_candidates_for(origin)
.unwrap()
.into_iter()
.map(|entry| (entry.key, entry.recently_imported))
.collect();
flags.sort();
flags
};
assert_eq!(
flags(SweepOrigin::Automatic),
vec![
("built_key".to_string(), false),
("imported_key".to_string(), true)
]
);
assert!(
flags(SweepOrigin::Requested)
.iter()
.all(|(_, imported)| !imported)
);
}
#[test]
fn remove_entry_ignores_recency_guard() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("x.rlib");
std::fs::write(&output_file, b"content").unwrap();
store
.put(
"rk",
"c",
&["lib".to_string()],
&[],
"",
"dev",
&[(output_file, "libx.rlib".to_string())],
"",
"",
)
.unwrap();
assert!(
matches!(
store
.remove_entry_guarded("rk", Some(EVICTION_IDLE_GRACE))
.unwrap(),
GuardedRemoval::Skipped
),
"guarded removal must skip a recently-accessed entry"
);
assert!(store.contains("rk"));
store.remove_entry("rk").unwrap();
assert!(!store.contains("rk"));
}
#[test]
fn test_incremental_dir_registry_deduplicates_and_cleans() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let incremental_dir = dir.path().join("target/debug/incremental");
std::fs::create_dir_all(&incremental_dir).unwrap();
std::fs::write(incremental_dir.join("junk"), b"tmp").unwrap();
store.remember_incremental_dir(&incremental_dir).unwrap();
store.remember_incremental_dir(&incremental_dir).unwrap();
store
.remember_incremental_dir(&dir.path().join("missing/incremental"))
.unwrap();
let count_before: i64 = store
.db
.query_row("SELECT COUNT(*) FROM incremental_dirs", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(count_before, 2);
let cleaned = store.clean_registered_incremental_dirs().unwrap();
assert_eq!(cleaned, 1);
assert!(!incremental_dir.exists());
let count_after: i64 = store
.db
.query_row("SELECT COUNT(*) FROM incremental_dirs", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(count_after, 0);
}
#[test]
fn target_root_registry_is_local_bounded_provenance_with_identity() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
std::fs::create_dir_all(&target).unwrap();
std::fs::write(
target.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
std::fs::write(target.join(".rustc_info.json"), "{}").unwrap();
store.remember_target_root(&target, &workspace).unwrap();
store.remember_target_root(&target, &workspace).unwrap();
let roots = store.tracked_target_roots(0).unwrap();
assert_eq!(roots.len(), 1, "same target is upserted, not duplicated");
assert_eq!(roots[0].path, std::path::absolute(&target).unwrap());
assert_eq!(
roots[0].workspace_root,
std::path::absolute(&workspace).unwrap()
);
assert_eq!(
crate::filesystem::directory_identity(&target),
Some(roots[0].identity)
);
store.remember_target_root(&workspace, &workspace).unwrap();
assert_eq!(
store.tracked_target_roots(0).unwrap().len(),
1,
"a source root must never be registered as a cleanup target"
);
store.forget_target_root(&target).unwrap();
assert!(store.tracked_target_roots(0).unwrap().is_empty());
}
#[test]
fn forgetting_by_identity_keeps_a_row_recorded_for_a_new_directory() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
let tagged = |target: &Path| {
std::fs::create_dir_all(target.join("debug")).unwrap();
std::fs::write(
target.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
};
tagged(&target);
store.remember_target_root(&target, &workspace).unwrap();
let removed = store.tracked_target_roots(0).unwrap().remove(0).identity;
std::fs::rename(&target, workspace.join("aside")).unwrap();
tagged(&target);
store.remember_target_root(&target, &workspace).unwrap();
let current = store.tracked_target_roots(0).unwrap().remove(0).identity;
assert_ne!(current, removed);
let elsewhere = crate::filesystem::PathIdentity {
device: current.device.wrapping_add(1),
inode: current.inode,
};
for stale in [removed, elsewhere] {
store
.forget_target_root_with_identity(&target, stale)
.unwrap();
assert_eq!(
store.tracked_target_roots(0).unwrap().len(),
1,
"{stale:?} forgot the row for {current:?}"
);
}
store
.forget_target_root_with_identity(&target, current)
.unwrap();
assert!(store.tracked_target_roots(0).unwrap().is_empty());
}
#[test]
fn discovered_target_remains_unbuilt_until_a_real_build_uses_it() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).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"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
assert!(
store
.remember_discovered_target_root(&target, &workspace)
.unwrap()
);
assert!(
!store
.remember_discovered_target_root(&target, &workspace)
.unwrap()
);
let before = store.tracked_target_roots(0).unwrap().remove(0);
assert!(before.discovered);
assert_eq!(before.rustc, None);
store
.remember_target_root_built_by(&target, &workspace, Some("rustc 1"))
.unwrap();
let after = store.tracked_target_roots(0).unwrap().remove(0);
assert!(!after.discovered);
assert_eq!(after.rustc.as_deref(), Some("rustc 1"));
assert!(
!store
.remember_discovered_target_root(&target, &workspace)
.unwrap()
);
assert!(!store.tracked_target_roots(0).unwrap()[0].discovered);
assert!(
!store
.remember_discovered_target_root(&workspace, &workspace)
.unwrap()
);
}
#[test]
fn a_target_root_records_the_compiler_that_built_into_it() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
std::fs::create_dir_all(&target).unwrap();
std::fs::write(
target.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
std::fs::create_dir_all(target.join("debug")).unwrap();
let rustc = |store: &Store| store.tracked_target_roots(0).unwrap()[0].rustc.clone();
store
.remember_target_root_built_by(&target, &workspace, Some("rustc 1"))
.unwrap();
assert_eq!(rustc(&store).as_deref(), Some("rustc 1"));
store
.remember_target_root_built_by(&target, &workspace, Some("rustc 2"))
.unwrap();
assert_eq!(
rustc(&store).as_deref(),
Some("rustc 2"),
"another compiler updates a row that is still fresh"
);
store.remember_target_root(&target, &workspace).unwrap();
assert_eq!(
rustc(&store).as_deref(),
Some("rustc 2"),
"registering without a compiler keeps the recorded one"
);
drop(store);
let db = rusqlite::Connection::open(config.index_db_path()).unwrap();
db.execute_batch(
"DROP TABLE target_roots;
CREATE TABLE target_roots (
path TEXT PRIMARY KEY, workspace_root TEXT NOT NULL,
first_seen INTEGER NOT NULL DEFAULT (unixepoch()),
last_seen INTEGER NOT NULL DEFAULT (unixepoch()),
device TEXT NOT NULL, inode TEXT NOT NULL);
PRAGMA user_version = 6;",
)
.unwrap();
drop(db);
let store = Store::open(&config).unwrap();
store
.remember_target_root_built_by(&target, &workspace, Some("rustc 3"))
.unwrap();
assert_eq!(rustc(&store).as_deref(), Some("rustc 3"));
}
#[test]
fn target_root_registry_filters_by_last_seen() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
std::fs::create_dir_all(&target).unwrap();
std::fs::write(
target.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
std::fs::write(target.join(".rustc_info.json"), "{}").unwrap();
store.remember_target_root(&target, &workspace).unwrap();
assert!(store.tracked_target_roots(24).unwrap().is_empty());
store
.db
.execute(
"UPDATE target_roots SET last_seen = unixepoch() - 90000",
[],
)
.unwrap();
assert_eq!(store.tracked_target_roots(24).unwrap().len(), 1);
}
#[test]
fn target_registry_prunes_only_after_a_real_upsert() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let workspace = dir.path().join("workspace");
let target = workspace.join("target");
std::fs::create_dir_all(&target).unwrap();
std::fs::write(
target.join("CACHEDIR.TAG"),
"Signature: 8a477f597d28d172789f06886806bc55",
)
.unwrap();
std::fs::write(target.join(".rustc_info.json"), "{}").unwrap();
let insert_stale = |path: &str| {
store
.db
.execute(
"INSERT INTO target_roots
(path, workspace_root, first_seen, last_seen, device, inode)
VALUES (?1, '/workspace', 0, unixepoch() - 15552001, '1', '1')",
params![path],
)
.unwrap();
};
let contains = |path: &str| -> bool {
store
.db
.query_row(
"SELECT EXISTS(SELECT 1 FROM target_roots WHERE path = ?1)",
params![path],
|row| row.get::<_, i64>(0),
)
.unwrap()
!= 0
};
insert_stale("/stale-before-write");
store.remember_target_root(&target, &workspace).unwrap();
assert!(!contains("/stale-before-write"));
insert_stale("/stale-before-debounced-noop");
store.remember_target_root(&target, &workspace).unwrap();
assert!(contains("/stale-before-debounced-noop"));
}
#[test]
fn clean_registered_incremental_dirs_prunes_a_non_directory_path() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let bogus = dir.path().join("not-a-dir");
std::fs::write(&bogus, b"i am a file").unwrap();
store.remember_incremental_dir(&bogus).unwrap();
let cleaned = store.clean_registered_incremental_dirs().unwrap();
assert_eq!(cleaned, 0, "a non-directory is pruned, not cleaned");
assert!(bogus.exists(), "the non-directory file is not deleted");
let remaining: i64 = store
.db
.query_row("SELECT COUNT(*) FROM incremental_dirs", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(remaining, 0, "the bogus registration was pruned");
}
#[cfg(unix)]
#[test]
fn clean_registered_incremental_dirs_keeps_row_when_remove_fails() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let parent = dir.path().join("readonly-parent");
let incremental_dir = parent.join("incremental");
std::fs::create_dir_all(&incremental_dir).unwrap();
std::fs::write(incremental_dir.join("junk"), b"tmp").unwrap();
store.remember_incremental_dir(&incremental_dir).unwrap();
std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o500)).unwrap();
let cleaned = store.clean_registered_incremental_dirs().unwrap();
std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o700)).unwrap();
assert_eq!(cleaned, 0, "failed removals are not counted as cleaned");
assert!(incremental_dir.exists(), "failed removal leaves the dir");
let remaining: i64 = store
.db
.query_row("SELECT COUNT(*) FROM incremental_dirs", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(remaining, 1, "failed removal keeps the registry row");
}
#[test]
fn test_store_locking() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let lock1 = match store.claim_build("testkey").unwrap() {
BuildClaim::Acquired(lock) => lock,
BuildClaim::Committed(_) | BuildClaim::Contended => {
panic!("first build claim should acquire the key")
}
};
assert!(matches!(
store.claim_build("testkey").unwrap(),
BuildClaim::Contended
));
drop(lock1);
assert!(matches!(
store.claim_build("testkey").unwrap(),
BuildClaim::Acquired(_)
));
}
#[test]
fn claim_build_rechecks_entry_after_acquiring_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let peer = Store::open(&config).unwrap();
let waiter = Store::open(&config).unwrap();
let cache_key = "committed_during_claim_race";
let peer_lock = match peer.claim_build(cache_key).unwrap() {
BuildClaim::Acquired(lock) => lock,
BuildClaim::Committed(_) | BuildClaim::Contended => {
panic!("peer should acquire the initial build claim")
}
};
assert!(matches!(
waiter.claim_build(cache_key).unwrap(),
BuildClaim::Contended
));
let output = dir.path().join("lib.rlib");
fs::write(&output, b"peer output").unwrap();
peer.put(
cache_key,
"peer",
&["rlib".to_string()],
&[],
"host",
"dev",
&[(output, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
drop(peer_lock);
match waiter.claim_build(cache_key).unwrap() {
BuildClaim::Committed(meta) => assert_eq!(meta.cache_key, cache_key),
BuildClaim::Acquired(_) => panic!("committed entry must prevent a duplicate compile"),
BuildClaim::Contended => panic!("peer already released the build lock"),
}
assert!(
waiter.try_lock(cache_key).unwrap().is_some(),
"serving the committed entry must release the claim"
);
}
#[test]
fn claim_build_evicts_empty_committed_entry() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let cache_key = "empty_committed_entry";
let entry_dir = store.entry_dir(cache_key);
fs::create_dir_all(&entry_dir).unwrap();
let meta = EntryMeta {
cache_key: cache_key.to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "empty".to_string(),
crate_types: vec!["rlib".to_string()],
files: vec![],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: "host".to_string(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed) VALUES (?1, ?2, 0, 1)",
params![cache_key, "empty"],
)
.unwrap();
match store.claim_build(cache_key).unwrap() {
BuildClaim::Acquired(_) => {}
BuildClaim::Committed(_) => panic!("empty entry must not be served"),
BuildClaim::Contended => panic!("no peer owns the build lock"),
}
assert!(store.get(cache_key).unwrap().is_none());
assert!(!entry_dir.exists());
assert!(store.try_lock(cache_key).unwrap().is_some());
}
#[test]
fn unlocked_pid_marker_does_not_claim_the_key() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let lock_path = store.entry_dir("pid-marker").with_extension("lock");
fs::write(&lock_path, std::process::id().to_string()).unwrap();
let lock = store.try_lock("pid-marker").unwrap();
assert!(
lock.is_some(),
"an unlocked marker must not cause contention"
);
}
#[test]
fn advisory_lock_owns_key_even_with_unparseable_metadata() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let lock_path = store.entry_dir("foreign-owner").with_extension("lock");
let mut owner = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.unwrap();
use std::io::Write;
write!(owner, "not-a-pid").unwrap();
owner.lock().unwrap();
assert!(
store.try_lock("foreign-owner").unwrap().is_none(),
"OS lock ownership must override PID metadata"
);
owner.unlock().unwrap();
assert!(store.try_lock("foreign-owner").unwrap().is_some());
}
#[test]
fn concurrent_advisory_lock_acquisition_has_one_winner() {
const CONTENDERS: usize = 16;
let dir = tempfile::tempdir().unwrap();
let barrier = std::sync::Arc::new(std::sync::Barrier::new(CONTENDERS));
let mut handles = Vec::new();
for _ in 0..CONTENDERS {
let config = test_config(dir.path());
let barrier = barrier.clone();
handles.push(std::thread::spawn(move || {
let store = Store::open(&config).unwrap();
barrier.wait();
store.try_lock("stale-race").unwrap()
}));
}
let guards: Vec<_> = handles
.into_iter()
.map(|handle| handle.join().unwrap())
.collect();
assert_eq!(
guards.iter().filter(|guard| guard.is_some()).count(),
1,
"the advisory lock must admit exactly one live guard"
);
}
#[test]
fn dropping_key_lock_preserves_stable_lock_file() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let lock_path = store.entry_dir("stable-path").with_extension("lock");
let lock = store.try_lock("stable-path").unwrap();
assert!(lock.is_some());
drop(lock);
assert!(
lock_path.exists(),
"advisory lock paths must not be unlinked after release"
);
assert!(store.try_lock("stable-path").unwrap().is_some());
}
#[test]
fn dropping_store_lock_unlocks_even_with_a_duplicated_handle() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let lock = store
.try_lock("duplicated-handle")
.unwrap()
.expect("owner lock");
let duplicate = lock.file.try_clone().unwrap();
drop(lock);
assert!(
store.try_lock("duplicated-handle").unwrap().is_some(),
"explicit unlock must release duplicate descriptors of the lock"
);
drop(duplicate);
}
#[test]
fn process_exit_releases_advisory_key_lock() {
let dir = tempfile::tempdir().unwrap();
let ready = dir.path().join("lock-ready");
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"store::tests::advisory_key_lock_child_fixture",
"--ignored",
"--nocapture",
])
.env("KACHE_TEST_ADVISORY_LOCK_ROOT", dir.path())
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !ready.exists() && std::time::Instant::now() < deadline {
assert!(
child.try_wait().unwrap().is_none(),
"lock fixture exited before acquiring the key"
);
std::thread::sleep(Duration::from_millis(10));
}
assert!(ready.exists(), "lock fixture did not become ready");
let store = Store::open(test_config(dir.path())).unwrap();
assert!(
store.try_lock("crash-release").unwrap().is_none(),
"child must own the key before it exits"
);
child.kill().unwrap();
child.wait().unwrap();
assert!(
store.try_lock("crash-release").unwrap().is_some(),
"the OS must release the key lock when its process exits"
);
}
#[test]
#[ignore = "subprocess fixture for process_exit_releases_advisory_key_lock"]
fn advisory_key_lock_child_fixture() {
let root =
PathBuf::from(std::env::var_os("KACHE_TEST_ADVISORY_LOCK_ROOT").expect("fixture root"));
let store = Store::open(test_config(&root)).unwrap();
let _lock = store
.try_lock("crash-release")
.unwrap()
.expect("fixture key lock");
fs::write(root.join("lock-ready"), b"ready").unwrap();
std::thread::sleep(Duration::from_secs(30));
}
#[test]
fn test_store_clear() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output_file = dir.path().join("out.rlib");
std::fs::write(&output_file, b"content").unwrap();
store
.put(
"k1",
"c1",
&["lib".to_string()],
&[],
"",
"dev",
&[(output_file.clone(), "lib.rlib".to_string())],
"",
"",
)
.unwrap();
assert!(store.contains("k1"));
store.clear().unwrap();
assert!(!store.contains("k1"));
}
#[test]
fn test_store_entry_dir() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = store.entry_dir("abc123");
assert!(entry_dir.to_string_lossy().contains("store"));
assert!(entry_dir.to_string_lossy().contains("abc123"));
}
#[test]
fn test_store_total_size_empty() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
assert_eq!(store.total_size().unwrap(), 0);
}
#[test]
fn test_store_entry_count_empty() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
assert_eq!(store.entry_count().unwrap(), 0);
}
#[test]
fn test_store_entry_count_after_put() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("a.rlib");
std::fs::write(&output, b"data").unwrap();
store
.put(
"k1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output.clone(), "a.rlib".into())],
"",
"",
)
.unwrap();
rewrite_source(&output, b"data2");
store
.put(
"k2",
"c2",
&["lib".into()],
&[],
"",
"dev",
&[(output, "b.rlib".into())],
"",
"",
)
.unwrap();
assert_eq!(store.entry_count().unwrap(), 2);
}
#[test]
fn test_store_contains_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
assert!(!store.contains("nonexistent_key"));
}
#[test]
fn test_store_get_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
assert!(store.get("nonexistent_key").unwrap().is_none());
}
#[test]
fn test_store_remove_entry() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"content").unwrap();
store
.put(
"rem1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
assert!(store.contains("rem1"));
store.remove_entry("rem1").unwrap();
assert!(!store.contains("rem1"));
assert_eq!(store.entry_count().unwrap(), 0);
}
#[test]
fn remove_entry_refuses_on_corrupt_meta_no_refcount_leak() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"content").unwrap();
store
.put(
"corrupt1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let refcount_sum = |s: &Store| -> i64 {
s.db.query_row("SELECT COALESCE(SUM(refcount), 0) FROM blobs", [], |r| {
r.get(0)
})
.unwrap()
};
let row_present = |s: &Store| -> i64 {
s.db.query_row(
"SELECT COUNT(*) FROM entries WHERE cache_key = 'corrupt1'",
[],
|r| r.get(0),
)
.unwrap()
};
assert_eq!(refcount_sum(&store), 1, "one blob at refcount 1 after put");
assert_eq!(row_present(&store), 1);
let meta_path = store.entry_dir("corrupt1").join("meta.json");
std::fs::write(&meta_path, b"{ not valid json").unwrap();
assert!(
store.remove_entry("corrupt1").is_err(),
"remove_entry must error on unparseable meta.json rather than leak"
);
assert_eq!(
row_present(&store),
1,
"corrupt entry row must survive a refused removal"
);
assert_eq!(
refcount_sum(&store),
1,
"blob refcounts must be unchanged — no orphan"
);
}
#[test]
fn remove_entry_refuses_when_meta_missing_but_row_present() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"x").unwrap();
store
.put(
"m1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
std::fs::remove_file(store.entry_dir("m1").join("meta.json")).unwrap();
let err = store.remove_entry("m1").unwrap_err();
assert!(
format!("{err:#}").contains("meta.json missing but DB row present"),
"wrong refusal shape: {err:#}"
);
let still_there: i64 = store
.db
.query_row(
"SELECT COUNT(*) FROM entries WHERE cache_key = 'm1'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(still_there, 1, "entry row must survive a refused removal");
}
#[test]
fn reput_over_refused_removal_releases_stale_refcounts() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let refcount_sum = |s: &Store| -> i64 {
s.db.query_row("SELECT COALESCE(SUM(refcount), 0) FROM blobs", [], |r| {
r.get(0)
})
.unwrap()
};
let put = |s: &Store, generation: &str, content: &[u8]| {
let output = dir.path().join(format!("lib-{generation}.rlib"));
std::fs::write(&output, content).unwrap();
s.put(
"strand1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
};
put(&store, "one", b"generation one");
assert_eq!(refcount_sum(&store), 1);
std::fs::remove_file(store.entry_dir("strand1").join("meta.json")).unwrap();
assert!(store.remove_entry("strand1").is_err());
put(&store, "two", b"generation two");
assert_eq!(
refcount_sum(&store),
1,
"re-put must release the stranded generation's references"
);
let mapped: i64 = store
.db
.query_row(
"SELECT COUNT(*) FROM entry_blobs WHERE cache_key = 'strand1'",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(mapped, 1, "exactly the new generation's mapping remains");
assert_eq!(
store.blob_index_drift().unwrap().total(),
0,
"index must match the committed meta with no doctor repair"
);
put(&store, "two-again", b"generation two");
assert_eq!(refcount_sum(&store), 1, "shared hash must not double-count");
assert_eq!(store.blob_index_drift().unwrap().total(), 0);
}
fn assert_blob_refs_match_mappings(store: &Store) {
let mut stmt = store
.db
.prepare(
"SELECT b.hash, b.refcount,
COALESCE((SELECT SUM(refs) FROM entry_blobs e WHERE e.hash = b.hash), 0)
FROM blobs b
UNION ALL
SELECT e.hash, 0, e.refs FROM entry_blobs e
WHERE NOT EXISTS (SELECT 1 FROM blobs b WHERE b.hash = e.hash)",
)
.unwrap();
let drifted: Vec<String> = stmt
.query_map([], |r| {
Ok((
r.get::<_, String>(0)?,
r.get::<_, i64>(1)?,
r.get::<_, i64>(2)?,
))
})
.unwrap()
.map(Result::unwrap)
.filter(|(_, refcount, mapped)| refcount != mapped || *mapped == 0)
.map(|(hash, refcount, mapped)| {
let direction = if refcount < mapped {
"TOO LOW"
} else {
"too high"
};
format!(
"{direction}: {} refcount {refcount}, mapped refs {mapped}",
&hash[..8.min(hash.len())]
)
})
.collect();
assert!(drifted.is_empty(), "blob index drift: {drifted:#?}");
}
fn put_outputs(store: &Store, dir: &Path, key: &str, generation: &str, outputs: &[(&str, &[u8])]) {
let files: Vec<(PathBuf, String)> = outputs
.iter()
.map(|(name, content)| {
let source = dir.join(format!("{key}-{generation}-{name}"));
std::fs::write(&source, content).unwrap();
(source, name.to_string())
})
.collect();
store
.put(key, "c1", &["lib".into()], &[], "", "dev", &files, "", "")
.unwrap();
}
fn extract_download(store: &Store, key: &str, outputs: &[(&str, &[u8])]) -> EntryMeta {
let entry_dir = store.entry_dir(key);
std::fs::create_dir_all(&entry_dir).unwrap();
let files = outputs
.iter()
.map(|(name, content)| {
std::fs::write(entry_dir.join(name), content).unwrap();
CachedFile {
name: name.to_string(),
size: content.len() as u64,
hash: blake3::hash(content).to_hex().to_string(),
executable: false,
}
})
.collect();
let meta = EntryMeta {
cache_key: key.to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "c1".to_string(),
crate_types: vec!["lib".to_string()],
files,
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
std::fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
meta
}
fn content_hash(content: &[u8]) -> String {
blake3::hash(content).to_hex().to_string()
}
#[test]
fn reput_with_identical_outputs_keeps_each_refcount_at_one() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let outputs: [(&str, &[u8]); 2] = [("a.rlib", b"blob A"), ("b.rmeta", b"blob B")];
put_outputs(&store, dir.path(), "reput_same", "one", &outputs);
put_outputs(&store, dir.path(), "reput_same", "two", &outputs);
assert_eq!(blob_refcount(&store, &content_hash(b"blob A")), Some(1));
assert_eq!(blob_refcount(&store, &content_hash(b"blob B")), Some(1));
assert_blob_refs_match_mappings(&store);
}
#[test]
fn reput_with_different_outputs_releases_only_the_dropped_blob() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
put_outputs(
&store,
dir.path(),
"reput_diff",
"one",
&[("a.rlib", b"blob A"), ("b.rmeta", b"blob B")],
);
put_outputs(
&store,
dir.path(),
"reput_diff",
"two",
&[("a.rlib", b"blob A"), ("b.rmeta", b"blob C")],
);
assert_eq!(blob_refcount(&store, &content_hash(b"blob A")), Some(1));
assert_eq!(blob_refcount(&store, &content_hash(b"blob B")), None);
assert_eq!(blob_refcount(&store, &content_hash(b"blob C")), Some(1));
assert_blob_refs_match_mappings(&store);
store.remove_entry("reput_diff").unwrap();
assert_eq!(blob_table_count(&store), 0);
assert!(!store.blob_path(&content_hash(b"blob A")).exists());
assert!(!store.blob_path(&content_hash(b"blob C")).exists());
}
#[test]
fn reput_of_one_key_leaves_a_shared_blob_at_two() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let shared: (&str, &[u8]) = ("a.rlib", b"shared blob");
put_outputs(&store, dir.path(), "share_one", "one", &[shared]);
put_outputs(&store, dir.path(), "share_two", "one", &[shared]);
put_outputs(&store, dir.path(), "share_one", "two", &[shared]);
assert_eq!(
blob_refcount(&store, &content_hash(b"shared blob")),
Some(2)
);
assert_blob_refs_match_mappings(&store);
store.remove_entry("share_one").unwrap();
assert_eq!(
blob_refcount(&store, &content_hash(b"shared blob")),
Some(1)
);
store.remove_entry("share_two").unwrap();
assert_eq!(blob_table_count(&store), 0);
}
#[test]
fn reimport_over_a_committed_entry_is_refcount_neutral() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let outputs: [(&str, &[u8]); 2] = [("a.rlib", b"blob A"), ("b.rmeta", b"blob B")];
extract_download(&store, "reimport", &outputs);
store.import_downloaded_entry("reimport").unwrap();
extract_download(&store, "reimport", &outputs);
store.import_downloaded_entry("reimport").unwrap();
assert_eq!(blob_refcount(&store, &content_hash(b"blob A")), Some(1));
assert_eq!(blob_refcount(&store, &content_hash(b"blob B")), Some(1));
assert_blob_refs_match_mappings(&store);
store.remove_entry("reimport").unwrap();
assert_eq!(blob_table_count(&store), 0, "eviction must free both blobs");
}
#[test]
fn reimport_with_different_files_releases_the_replaced_generation() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
extract_download(
&store,
"reimport_diff",
&[("a.rlib", b"blob A"), ("b.rmeta", b"blob B")],
);
store.import_downloaded_entry("reimport_diff").unwrap();
put_outputs(&store, dir.path(), "other", "one", &[("a.rlib", b"blob A")]);
extract_download(
&store,
"reimport_diff",
&[("a.rlib", b"blob A"), ("b.rmeta", b"blob C")],
);
store.import_downloaded_entry("reimport_diff").unwrap();
assert_eq!(blob_refcount(&store, &content_hash(b"blob A")), Some(2));
assert_eq!(blob_refcount(&store, &content_hash(b"blob B")), None);
assert_eq!(blob_refcount(&store, &content_hash(b"blob C")), Some(1));
assert_blob_refs_match_mappings(&store);
}
#[test]
fn startup_migration_leaves_an_unregistered_extraction_alone() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
extract_download(&store, "abandoned", &[("a.rlib", b"blob A")]);
let stats = store.migrate_to_blobs(|_, _| {}).unwrap();
assert_eq!(stats.entries_scanned, 1);
assert_eq!(stats.entries_migrated, 0);
assert_eq!(stats.entries_skipped, 1);
assert_eq!(blob_table_count(&store), 0);
assert!(store.entry_dir("abandoned").join("a.rlib").is_file());
assert_blob_refs_match_mappings(&store);
store.import_downloaded_entry("abandoned").unwrap();
assert_eq!(blob_refcount(&store, &content_hash(b"blob A")), Some(1));
assert_blob_refs_match_mappings(&store);
}
#[test]
fn migration_skips_an_uncommitted_entry() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let meta = extract_download(&store, "uncommitted", &[("a.rlib", b"blob A")]);
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed)
VALUES ('uncommitted', 'c1', 6, 0)",
[],
)
.unwrap();
assert!(!store.migrate_entry_to_blobs(&meta).unwrap());
assert_eq!(blob_table_count(&store), 0);
assert!(store.entry_dir("uncommitted").join("a.rlib").is_file());
}
fn legacy_entry(store: &Store, key: &str, outputs: &[(&str, &[u8])]) -> EntryMeta {
let meta = extract_download(store, key, outputs);
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed)
VALUES (?1, 'c1', 1, 1)",
params![key],
)
.unwrap();
meta
}
#[test]
fn backfill_skips_an_entry_with_unmigrated_artifacts() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let meta = legacy_entry(
&store,
"legacy_unmigrated",
&[("a.rlib", b"blob A"), ("b.rmeta", b"blob B")],
);
fs::remove_file(store.entry_dir("legacy_unmigrated").join("a.rlib")).unwrap();
assert_eq!(store.backfill_entry_blobs().unwrap(), 0);
let mapped: i64 = store
.db
.query_row("SELECT COUNT(*) FROM entry_blobs", [], |r| r.get(0))
.unwrap();
assert_eq!(mapped, 0);
assert!(store.migrate_entry_to_blobs(&meta).unwrap());
assert_eq!(store.backfill_entry_blobs().unwrap(), 1);
assert_eq!(blob_refcount(&store, &content_hash(b"blob B")), Some(1));
}
#[test]
fn migration_counts_a_legacy_hash_another_entry_already_owns() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
put_outputs(&store, dir.path(), "modern", "one", &[("a.rlib", b"twin")]);
let meta = legacy_entry(
&store,
"legacy_shared",
&[
("a.rlib", b"twin"),
("b.rlib", b"twin"),
("c.rmeta", b"solo"),
],
);
assert_eq!(store.backfill_entry_blobs().unwrap(), 0);
assert!(store.migrate_entry_to_blobs(&meta).unwrap());
assert_eq!(store.backfill_entry_blobs().unwrap(), 1);
assert_eq!(blob_refcount(&store, &content_hash(b"twin")), Some(3));
assert_eq!(blob_refcount(&store, &content_hash(b"solo")), Some(1));
assert_blob_refs_match_mappings(&store);
store.remove_entry("legacy_shared").unwrap();
assert!(store.get("modern").unwrap().is_some());
assert_blob_refs_match_mappings(&store);
}
#[test]
fn republish_over_a_mapping_without_a_blob_row_keeps_the_new_reference() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let map = |key: &str, content: &[u8]| {
store
.db
.execute(
"INSERT INTO entry_blobs (cache_key, hash, refs) VALUES (?1, ?2, 1)",
params![key, content_hash(content)],
)
.unwrap();
};
map("via_import", b"import blob");
extract_download(&store, "via_import", &[("a.rlib", b"import blob")]);
store.import_downloaded_entry("via_import").unwrap();
assert_eq!(
blob_refcount(&store, &content_hash(b"import blob")),
Some(1)
);
map("via_put", b"put blob");
put_outputs(
&store,
dir.path(),
"via_put",
"one",
&[("a.rlib", b"put blob")],
);
assert_eq!(blob_refcount(&store, &content_hash(b"put blob")), Some(1));
assert_blob_refs_match_mappings(&store);
}
#[test]
fn claiming_an_unowned_key_releases_its_leftover_mapping() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let leftover = |key: &str| {
store
.db
.execute(
"INSERT INTO blobs (hash, size, refcount) VALUES (?1, 1, 1)",
params![format!("old-{key}")],
)
.unwrap();
store
.db
.execute(
"INSERT INTO entry_blobs (cache_key, hash, refs) VALUES (?1, ?2, 1)",
params![key, format!("old-{key}")],
)
.unwrap();
};
let batch_key = "b".repeat(64);
leftover(&batch_key);
let verified = write_verified_fixture(&store, &batch_key, &batch_key, "lib.rlib", None);
assert_eq!(
store.import_verified_restored_entries(&[verified]).unwrap(),
1
);
assert_blob_refs_match_mappings(&store);
let rebuild_key = "c".repeat(64);
leftover(&rebuild_key);
let mut meta = read_meta(&store, &batch_key);
meta.cache_key = rebuild_key.clone();
let rebuild_dir = store.entry_dir(&rebuild_key);
fs::create_dir_all(&rebuild_dir).unwrap();
fs::write(
rebuild_dir.join("meta.json"),
serde_json::to_string(&meta).unwrap(),
)
.unwrap();
assert_eq!(
store.rebuild_one_entry(&rebuild_key, &rebuild_dir).unwrap(),
Some(1)
);
assert_blob_refs_match_mappings(&store);
assert_eq!(blob_table_count(&store), 1, "both old-* rows are gone");
}
fn owner_and_uncounted_mapping(store: &Store, dir: &Path) -> String {
put_outputs(store, dir, "owner", "one", &[("a.rlib", b"shared blob")]);
let mut meta = read_meta(store, "owner");
meta.cache_key = "uncounted".to_string();
let entry_dir = store.entry_dir("uncounted");
fs::create_dir_all(&entry_dir).unwrap();
fs::write(
entry_dir.join("meta.json"),
serde_json::to_string(&meta).unwrap(),
)
.unwrap();
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed)
VALUES ('uncounted', 'c1', 1, 1)",
[],
)
.unwrap();
record_entry_blobs(&store.db, "uncounted", &meta.files).unwrap();
let hash = content_hash(b"shared blob");
assert_eq!(blob_refcount(store, &hash), Some(1));
hash
}
fn mapped_refs(store: &Store, key: &str, hash: &str) -> Option<i64> {
store
.db
.query_row(
"SELECT refs FROM entry_blobs WHERE cache_key = ?1 AND hash = ?2",
params![key, hash],
|r| r.get(0),
)
.ok()
}
#[test]
fn reput_over_an_uncounted_mapping_keeps_the_other_owners_reference() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let hash = owner_and_uncounted_mapping(&store, dir.path());
put_outputs(
&store,
dir.path(),
"uncounted",
"two",
&[("b.rlib", b"other blob")],
);
assert_eq!(
blob_refcount(&store, &hash),
Some(1),
"the owner's reference"
);
assert_eq!(mapped_refs(&store, "owner", &hash), Some(1));
assert_eq!(mapped_refs(&store, "uncounted", &hash), None);
assert!(store.blob_path(&hash).is_file());
assert_blob_refs_match_mappings(&store);
}
#[test]
fn reimport_over_an_uncounted_mapping_keeps_the_other_owners_reference() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let hash = owner_and_uncounted_mapping(&store, dir.path());
extract_download(&store, "uncounted", &[("b.rlib", b"other blob")]);
store.import_downloaded_entry("uncounted").unwrap();
assert_eq!(
blob_refcount(&store, &hash),
Some(1),
"the owner's reference"
);
assert_eq!(mapped_refs(&store, "owner", &hash), Some(1));
assert_eq!(mapped_refs(&store, "uncounted", &hash), None);
assert_blob_refs_match_mappings(&store);
}
#[test]
fn removing_an_uncounted_mapping_keeps_the_other_owners_blob() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
let hash = owner_and_uncounted_mapping(&store, dir.path());
store.remove_entry("uncounted").unwrap();
assert_eq!(blob_refcount(&store, &hash), Some(1));
assert!(store.blob_path(&hash).is_file());
assert!(store.get("owner").unwrap().is_some());
assert_blob_refs_match_mappings(&store);
}
#[test]
fn floor_raises_only_this_keys_blobs_and_only_up_to_their_mappings() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
store
.db
.execute_batch(
"INSERT INTO blobs (hash, size, refcount) VALUES
('low', 1, 1), ('exact', 1, 5), ('high', 1, 9), ('foreign', 1, 1);
INSERT INTO entry_blobs (cache_key, hash, refs) VALUES
('k', 'low', 2), ('other', 'low', 1),
('k', 'exact', 2), ('other', 'exact', 3),
('k', 'high', 1),
('k', 'norow', 1),
('other', 'foreign', 4);",
)
.unwrap();
assert_eq!(floor_blob_refs_at_mappings(&store.db, "k").unwrap(), 3);
assert_eq!(blob_refcount(&store, "low"), Some(3));
assert_eq!(blob_refcount(&store, "exact"), Some(5));
assert_eq!(blob_refcount(&store, "high"), Some(9), "never lowered");
assert_eq!(blob_refcount(&store, "norow"), None);
assert_eq!(blob_refcount(&store, "foreign"), Some(1), "not this key's");
}
#[test]
fn release_entry_blob_refs_subtracts_this_keys_mapping_only() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
store
.db
.execute_batch(
"INSERT INTO blobs (hash, size, refcount) VALUES
('shared', 1, 5), ('last', 1, 2), ('drifted', 1, 1), ('idle', 1, 0),
('uncounted', 1, 1);
INSERT INTO entry_blobs (cache_key, hash, refs) VALUES
('k', 'shared', 2), ('k', 'last', 2), ('k', 'drifted', 3),
('k', 'norow', 1), ('k', 'uncounted', 1),
('other', 'shared', 3), ('other', 'uncounted', 1);",
)
.unwrap();
release_entry_blob_refs(&store.db, "k").unwrap();
assert_eq!(blob_refcount(&store, "shared"), Some(3));
assert_eq!(blob_refcount(&store, "last"), None, "released to zero");
assert_eq!(blob_refcount(&store, "norow"), None, "nothing to release");
assert_eq!(
blob_refcount(&store, "uncounted"),
Some(1),
"the other owner's reference survives"
);
assert_eq!(
blob_refcount(&store, "drifted"),
None,
"clamped, not negative"
);
assert_eq!(
blob_refcount(&store, "idle"),
Some(0),
"rows this key never mapped are not its to delete"
);
let mapped: Vec<String> = store
.db
.prepare("SELECT cache_key FROM entry_blobs")
.unwrap()
.query_map([], |r| r.get(0))
.unwrap()
.map(Result::unwrap)
.collect();
assert_eq!(mapped, vec!["other".to_string(), "other".to_string()]);
}
#[cfg(unix)]
#[test]
fn remove_entry_refuses_unreadable_meta_as_a_read_error() {
use std::os::unix::fs::PermissionsExt;
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipping: running as root, mode 000 does not deny access");
return;
}
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"x").unwrap();
store
.put(
"locked",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let entry_dir = store.entry_dir("locked");
std::fs::set_permissions(&entry_dir, std::fs::Permissions::from_mode(0o000)).unwrap();
let err = store.remove_entry("locked").unwrap_err();
std::fs::set_permissions(&entry_dir, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(
format!("{err:#}").contains("reading meta.json"),
"an unreadable meta must refuse as a read error, got: {err:#}"
);
assert!(
store.contains("locked"),
"entry row must survive a refused removal"
);
}
#[test]
fn blob_path_is_panic_safe_for_short_hash() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let _ = store.blob_path("a");
let _ = store.blob_path("");
}
#[test]
fn test_store_remove_entry_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
store.remove_entry("nonexistent").unwrap();
}
#[test]
fn test_store_list_entries_empty() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entries = store.list_entries("name").unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_store_list_entries_sort_by() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let out1 = dir.path().join("a.rlib");
std::fs::write(&out1, vec![0u8; 100]).unwrap();
store
.put(
"k1",
"alpha",
&["lib".into()],
&[],
"",
"dev",
&[(out1, "a.rlib".into())],
"",
"",
)
.unwrap();
let out2 = dir.path().join("b.rlib");
std::fs::write(&out2, vec![0u8; 200]).unwrap();
store
.put(
"k2",
"beta",
&["lib".into()],
&[],
"",
"dev",
&[(out2, "b.rlib".into())],
"",
"",
)
.unwrap();
let entries = store.list_entries("name").unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].crate_name, "alpha");
let entries = store.list_entries("size").unwrap();
assert_eq!(entries.len(), 2);
assert!(entries[0].size >= entries[1].size);
let entries = store.list_entries("hits").unwrap();
assert_eq!(entries.len(), 2);
}
#[test]
fn list_entries_errors_on_non_integer_size_row() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
store
.db
.execute(
"INSERT INTO entries \
(cache_key, crate_name, crate_type, profile, size, committed) \
VALUES ('bad_size', 'bad', 'lib', 'dev', x'01', 1)",
[],
)
.unwrap();
let err = store.list_entries("name").unwrap_err();
assert!(
err.to_string().contains("Invalid column type"),
"expected SQLite type error, got: {err}"
);
}
#[test]
fn test_store_evict_older_than() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"content").unwrap();
store
.put(
"k1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-48 hours') WHERE cache_key = 'k1'",
[],
)
.unwrap();
let stats = store.evict_older_than(24).unwrap();
assert_eq!(stats.entries_evicted, 1);
assert!(!store.contains("k1"));
}
#[test]
fn test_store_evict_older_than_keeps_recent() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"content").unwrap();
store
.put(
"k1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let stats = store.evict_older_than(9999).unwrap();
assert_eq!(stats.entries_evicted, 0);
assert!(store.contains("k1"));
}
#[test]
fn evict_stale_key_schemas_keeps_only_the_running_schema() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
for (key, content) in [
("current", b"current artifact".as_slice()),
("old", b"old artifact".as_slice()),
("legacy", b"legacy artifact".as_slice()),
] {
let output = dir.path().join(format!("{key}.rlib"));
std::fs::write(&output, content).unwrap();
store
.put(
key,
key,
&["lib".into()],
&[],
"",
"dev",
&[(output.clone(), format!("{key}.rlib"))],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output);
}
let prior_schema = kache_format::CACHE_KEY_VERSION.saturating_sub(1);
store
.db
.execute(
"UPDATE entries
SET key_schema = ?1, last_accessed = datetime('now', '-1 day')
WHERE cache_key = 'old'",
params![prior_schema],
)
.unwrap();
store
.db
.execute(
"UPDATE entries
SET key_schema = 0, last_accessed = datetime('now', '-1 day')
WHERE cache_key = 'legacy'",
[],
)
.unwrap();
let stats = store
.evict_stale_key_schemas(kache_format::CACHE_KEY_VERSION)
.unwrap();
assert_eq!(stats.entries_evicted, 2);
assert!(stats.bytes_freed > 0);
assert_eq!(stats.blobs_removed, 2);
assert_eq!(stats.entries_pinned, 0);
assert!(store.contains("current"));
assert!(!store.contains("old"));
assert!(!store.contains("legacy"));
assert_eq!(store.entry_count().unwrap(), 1);
let second = store
.evict_stale_key_schemas(kache_format::CACHE_KEY_VERSION)
.unwrap();
assert_eq!(second.entries_evicted, 0);
}
#[test]
fn entry_meta_key_schema_defaults_to_unknown_for_legacy_json() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"artifact").unwrap();
store
.put(
"key",
"crate",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let content = std::fs::read_to_string(store.entry_dir("key").join("meta.json")).unwrap();
let current: EntryMeta = serde_json::from_str(&content).unwrap();
assert_eq!(current.key_schema, kache_format::CACHE_KEY_VERSION);
let mut legacy: serde_json::Value = serde_json::from_str(&content).unwrap();
legacy.as_object_mut().unwrap().remove("key_schema");
let parsed: EntryMeta = serde_json::from_value(legacy).unwrap();
assert_eq!(parsed.key_schema, 0);
}
#[test]
fn test_store_import_downloaded_entry() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = config.store_dir().join("downloaded_key");
std::fs::create_dir_all(&entry_dir).unwrap();
let artifact_content = b"fake artifact";
std::fs::write(entry_dir.join("lib.rlib"), artifact_content).unwrap();
let hash = crate::file_hash::hash_file(&entry_dir.join("lib.rlib")).unwrap();
let prior_schema = kache_format::CACHE_KEY_VERSION.saturating_sub(1);
let meta = EntryMeta {
cache_key: "downloaded_key".to_string(),
key_schema: prior_schema,
crate_name: "downloaded_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: artifact_content.len() as u64,
hash,
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec!["std".to_string()],
target: "x86_64-unknown-linux-gnu".to_string(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
let meta_json = serde_json::to_string_pretty(&meta).unwrap();
std::fs::write(entry_dir.join("meta.json"), meta_json).unwrap();
store.import_downloaded_entry("downloaded_key").unwrap();
assert!(store.contains("downloaded_key"));
assert_eq!(store.entry_count().unwrap(), 1);
let indexed_schema: u32 = store
.db
.query_row(
"SELECT key_schema FROM entries WHERE cache_key = 'downloaded_key'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(indexed_schema, prior_schema);
}
#[test]
fn test_store_import_downloaded_entry_missing_file() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = config.store_dir().join("incomplete_key");
std::fs::create_dir_all(&entry_dir).unwrap();
let meta = EntryMeta {
cache_key: "incomplete_key".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "incomplete_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: 42,
hash: "a".repeat(64),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
let meta_json = serde_json::to_string_pretty(&meta).unwrap();
std::fs::write(entry_dir.join("meta.json"), meta_json).unwrap();
let err = store.import_downloaded_entry("incomplete_key").unwrap_err();
assert!(
err.to_string().contains("missing file"),
"expected 'missing file' error, got: {err}"
);
assert!(!store.contains("incomplete_key"));
}
#[test]
fn failed_restore_cleanup_preserves_a_concurrent_committed_generation() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let publisher_store = Store::open(&config).unwrap();
let cleanup_store = Store::open(&config).unwrap();
let key = blake3::hash(b"restore-cleanup-publication-race")
.to_hex()
.to_string();
let entry_dir = config.store_dir().join(&key);
let meta_json = serde_json::to_string_pretty(&EntryMeta {
cache_key: key.clone(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "published".into(),
crate_types: vec!["lib".into()],
files: Vec::new(),
stdout: String::new(),
stderr: String::new(),
features: Vec::new(),
target: String::new(),
profile: "dev".into(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
})
.unwrap();
let (published_tx, published_rx) = std::sync::mpsc::sync_channel(0);
let (cleanup_attempt_tx, cleanup_attempt_rx) = std::sync::mpsc::sync_channel(0);
let publisher_key = key.clone();
let publisher_entry_dir = entry_dir.clone();
let publisher_meta = meta_json.clone();
let publisher = std::thread::spawn(move || {
let tx = publisher_store.db.unchecked_transaction().unwrap();
tx.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed) \
VALUES (?1, 'published', 0, 1)",
params![publisher_key],
)
.unwrap();
fs::create_dir_all(&publisher_entry_dir).unwrap();
fs::write(publisher_entry_dir.join("meta.json"), publisher_meta).unwrap();
published_tx.send(()).unwrap();
cleanup_attempt_rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("cleanup must attempt the writer lock");
tx.commit().unwrap();
});
published_rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("publisher must reach its pre-commit point");
let cleanup_key = key.clone();
let cleanup = std::thread::spawn(move || {
cleanup_store.discard_uncommitted_restored_entry_inner(
&cleanup_key,
|| {
cleanup_attempt_tx.send(()).unwrap();
},
|| {},
)
});
publisher.join().unwrap();
cleanup
.join()
.unwrap()
.expect("cleanup should observe and preserve the committed winner");
let store = Store::open(&config).unwrap();
assert!(store.contains(&key));
assert_eq!(
fs::read_to_string(entry_dir.join("meta.json")).unwrap(),
meta_json,
"cleanup must not remove or replace the generation that committed first"
);
}
#[test]
fn failed_restore_cleanup_holds_the_writer_lock_through_removal() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let cleanup_store = Store::open(&config).unwrap();
let contender_store = Store::open(&config).unwrap();
let key = blake3::hash(b"restore-cleanup-first").to_hex().to_string();
let entry_dir = cleanup_store.entry_dir(&key);
fs::create_dir_all(&entry_dir).unwrap();
fs::write(entry_dir.join("meta.json"), b"stale restore").unwrap();
let (locked_tx, locked_rx) = std::sync::mpsc::sync_channel(0);
let (release_tx, release_rx) = std::sync::mpsc::sync_channel(0);
let cleanup_key = key.clone();
let cleanup = std::thread::spawn(move || {
cleanup_store.discard_uncommitted_restored_entry_inner(
&cleanup_key,
|| {},
|| {
locked_tx.send(()).unwrap();
release_rx
.recv_timeout(Duration::from_secs(5))
.expect("test must release the cleanup writer lock");
},
)
});
locked_rx
.recv_timeout(Duration::from_secs(5))
.expect("cleanup must acquire its writer lock");
contender_store.db.busy_timeout(Duration::ZERO).unwrap();
let lock_error = match rusqlite::Transaction::new_unchecked(
&contender_store.db,
rusqlite::TransactionBehavior::Immediate,
) {
Ok(transaction) => {
drop(transaction);
panic!("another publisher acquired SQLite's writer lock during cleanup");
}
Err(error) => error,
};
assert!(
matches!(
lock_error,
SqlError::SqliteFailure(code, _)
if matches!(code.code, ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked)
),
"unexpected competing-writer result: {lock_error}"
);
release_tx.send(()).unwrap();
cleanup
.join()
.unwrap()
.expect("cleanup should remove the uncommitted residue");
assert!(!entry_dir.exists());
let output = dir.path().join("published.rlib");
fs::write(&output, b"published after cleanup").unwrap();
contender_store
.put(
&key,
"published",
&["rlib".into()],
&[],
"host",
"dev",
&[(output, "published.rlib".into())],
"",
"",
)
.expect("a publisher must succeed after cleanup releases the lock");
assert!(contender_store.contains(&key));
assert!(entry_dir.join("meta.json").is_file());
}
#[test]
fn failed_restore_cleanup_accepts_an_already_missing_directory() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let key = blake3::hash(b"already-missing-restore")
.to_hex()
.to_string();
assert!(!store.entry_dir(&key).exists());
store
.discard_uncommitted_restored_entry(&key)
.expect("an already-absent restore has nothing left to clean up");
}
#[test]
fn failed_restore_cleanup_reports_non_directory_residue() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let key = blake3::hash(b"non-directory-restore-residue")
.to_hex()
.to_string();
let entry_path = store.entry_dir(&key);
fs::write(&entry_path, b"not a directory").unwrap();
let error = store
.discard_uncommitted_restored_entry(&key)
.expect_err("non-directory residue must not be silently accepted");
assert!(
error
.to_string()
.contains("removing uncommitted restored entry"),
"unexpected cleanup error: {error:#}"
);
assert!(entry_path.is_file());
}
#[test]
fn test_import_downloaded_entry_creates_blobs() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = config.store_dir().join("dl_key");
fs::create_dir_all(&entry_dir).unwrap();
fs::write(entry_dir.join("lib.rlib"), b"artifact data").unwrap();
let hash = crate::file_hash::hash_file(&entry_dir.join("lib.rlib")).unwrap();
let meta = EntryMeta {
cache_key: "dl_key".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "dl_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: 13,
hash: hash.clone(),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store.import_downloaded_entry("dl_key").unwrap();
let blob = store.blob_path(&hash);
assert!(
blob.exists(),
"blob should be created from downloaded artifact"
);
assert!(
!entry_dir.join("lib.rlib").exists(),
"artifact should have been moved to blob store"
);
assert!(
entry_dir.join("meta.json").exists(),
"meta.json should remain"
);
let perms = fs::metadata(&blob).unwrap().permissions();
assert!(perms.readonly(), "imported blob should be read-only");
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![&hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount, 1);
assert!(store.contains("dl_key"));
}
#[test]
fn test_store_get_evicts_entry_with_missing_file() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"content").unwrap();
store
.put(
"damaged_key",
"damaged_crate",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
assert!(store.contains("damaged_key"));
let meta_content =
std::fs::read_to_string(store.entry_dir("damaged_key").join("meta.json")).unwrap();
let meta: EntryMeta = serde_json::from_str(&meta_content).unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let mut perms = std::fs::metadata(&blob).unwrap().permissions();
perms.set_readonly(false);
std::fs::set_permissions(&blob, perms).unwrap();
std::fs::remove_file(&blob).unwrap();
let result = store.get("damaged_key").unwrap();
assert!(
result.is_none(),
"expected None for entry with missing file"
);
assert!(
!store.contains("damaged_key"),
"entry should have been evicted"
);
}
#[test]
fn test_store_get_evicts_entry_with_corrupted_file() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"valid rlib content here").unwrap();
store
.put(
"corrupt_key",
"corrupt_crate",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
assert!(store.contains("corrupt_key"));
let meta_content =
std::fs::read_to_string(store.entry_dir("corrupt_key").join("meta.json")).unwrap();
let meta: EntryMeta = serde_json::from_str(&meta_content).unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let mut perms = std::fs::metadata(&blob).unwrap().permissions();
perms.set_readonly(false);
std::fs::set_permissions(&blob, perms).unwrap();
std::fs::write(&blob, b"short").unwrap();
let result = store.get("corrupt_key").unwrap();
assert!(
result.is_none(),
"expected None for entry with size-corrupted file"
);
assert!(
!store.contains("corrupt_key"),
"entry should have been evicted"
);
}
#[cfg(unix)]
#[test]
fn get_evicts_when_verified_blob_is_unreadable() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
fs::write(&output, b"readable before chmod").unwrap();
store
.put(
"unreadable_key",
"unreadable_crate",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let meta = store.get("unreadable_key").unwrap().unwrap();
let blob = store.blob_path(&meta.files[0].hash);
fs::set_permissions(&blob, fs::Permissions::from_mode(0o000)).unwrap();
let _env_lock = crate::test_support::process_state_test_lock();
let _verify = EnvVarGuard::set("KACHE_VERIFY_RESTORES", "always");
let result = store.get("unreadable_key").unwrap();
assert!(result.is_none(), "unreadable verified blob is evicted");
assert!(!store.contains("unreadable_key"));
}
#[test]
fn test_store_put_rejects_zero_byte_artifact() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("empty.rlib");
std::fs::write(&output, b"").unwrap();
let err = store
.put(
"zero_key",
"zero_crate",
&["lib".into()],
&[],
"",
"dev",
&[(output, "empty.rlib".into())],
"",
"",
)
.unwrap_err();
assert!(
err.to_string().contains("zero-byte"),
"expected 'zero-byte' error, got: {err}"
);
assert!(!store.contains("zero_key"));
}
#[test]
fn test_store_put_accepts_zero_byte_rmeta() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let rmeta = dir.path().join("libit-15ba26cbaff655a7.rmeta");
std::fs::write(&rmeta, b"").unwrap();
let depinfo = dir.path().join("it-15ba26cbaff655a7.d");
std::fs::write(&depinfo, b"it: tests/it.rs\n").unwrap();
store
.put(
"zero_rmeta_key",
"it",
&[],
&[],
"",
"dev",
&[
(rmeta, "libit-15ba26cbaff655a7.rmeta".into()),
(depinfo, "it-15ba26cbaff655a7.d".into()),
],
"",
"",
)
.unwrap();
let meta = store.get("zero_rmeta_key").unwrap().unwrap();
assert_eq!(meta.files.len(), 2, "sibling outputs survive the empty one");
let stored_rmeta = meta
.files
.iter()
.find(|f| f.name.ends_with(".rmeta"))
.expect("rmeta stored");
assert_eq!(stored_rmeta.size, 0);
assert_eq!(
store
.blob_path(&stored_rmeta.hash)
.metadata()
.unwrap()
.len(),
0,
"empty blob materialized in the content store"
);
assert!(meta.emit_kinds.iter().any(|k| k == "metadata"));
}
#[test]
fn test_store_put_rejects_zero_byte_rmeta_from_a_library_unit() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let rmeta = dir.path().join("libfoo-1234.rmeta");
std::fs::write(&rmeta, b"").unwrap();
let err = store
.put(
"truncated_lib_rmeta",
"foo",
&["lib".into()],
&[],
"",
"dev",
&[(rmeta, "libfoo-1234.rmeta".into())],
"",
"",
)
.unwrap_err();
assert!(
err.to_string().contains("zero-byte"),
"expected 'zero-byte' error, got: {err}"
);
assert!(!store.contains("truncated_lib_rmeta"));
}
#[test]
fn zero_byte_is_valid_output_only_for_metadata_without_a_library_unit() {
assert!(zero_byte_is_valid_output::<TestPolicy>(
"libfoo-1234.rmeta",
&[]
));
for ct in ["bin", "cdylib", "staticlib"] {
assert!(
zero_byte_is_valid_output::<TestPolicy>("libfoo-1234.rmeta", &[ct.into()]),
"{ct} emits no metadata, so an empty .rmeta is legitimate"
);
}
for ct in ["lib", "rlib", "dylib", "proc-macro", "some-future-type"] {
assert!(
!zero_byte_is_valid_output::<TestPolicy>("libfoo-1234.rmeta", &[ct.into()]),
"{ct} must keep the truncation guard"
);
}
for name in ["libfoo.rlib", "foo.d", "foo.o", "libfoo.so", "foo"] {
assert!(
!zero_byte_is_valid_output::<TestPolicy>(name, &[]),
"{name} must stay rejected when empty"
);
}
}
#[test]
fn test_store_import_rejects_size_mismatch() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = config.store_dir().join("mismatch_key");
std::fs::create_dir_all(&entry_dir).unwrap();
let meta = EntryMeta {
cache_key: "mismatch_key".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "mismatch_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: 9999, hash: "a".repeat(64),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
let meta_json = serde_json::to_string_pretty(&meta).unwrap();
std::fs::write(entry_dir.join("meta.json"), meta_json).unwrap();
std::fs::write(entry_dir.join("lib.rlib"), b"small content").unwrap();
let err = store.import_downloaded_entry("mismatch_key").unwrap_err();
assert!(
err.to_string().contains("size mismatch"),
"expected 'size mismatch' error, got: {err}"
);
}
#[cfg(test)]
fn import_with_poisoned_meta(
store: &Store,
config: &Config,
key: &str,
content: &[u8],
mutate: impl FnOnce(&mut CachedFile),
) -> anyhow::Result<()> {
let entry_dir = config.store_dir().join(key);
std::fs::create_dir_all(&entry_dir).unwrap();
std::fs::write(entry_dir.join("lib.rlib"), content).unwrap();
let mut file = CachedFile {
name: "lib.rlib".to_string(),
size: content.len() as u64,
hash: crate::file_hash::hash_file(&entry_dir.join("lib.rlib")).unwrap(),
executable: false,
};
mutate(&mut file);
let meta = EntryMeta {
cache_key: key.to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "c".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![file],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
std::fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store.import_downloaded_entry(key)
}
#[test]
fn import_rejects_content_hash_mismatch() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let bogus = blake3::hash(b"DIFFERENT!!!!").to_hex().to_string();
let err = import_with_poisoned_meta(&store, &config, "ch_mismatch", b"real_content!", |f| {
f.hash = bogus;
})
.unwrap_err();
assert!(
err.to_string().contains("content hash mismatch"),
"expected content hash mismatch, got: {err}"
);
assert!(!store.contains("ch_mismatch"));
}
#[test]
fn import_rejects_malformed_hash() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let err = import_with_poisoned_meta(&store, &config, "bad_hash", b"data", |f| {
f.hash = "../../etc/passwd".to_string();
})
.unwrap_err();
assert!(
err.to_string().contains("malformed blob hash"),
"expected malformed blob hash, got: {err}"
);
assert!(!store.contains("bad_hash"));
}
#[test]
fn import_rejects_unsafe_artifact_name() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
for bad in ["/etc/passwd", "../escape.rlib", "sub/dir.rlib"] {
let err = import_with_poisoned_meta(&store, &config, "unsafe_name", b"data", |f| {
f.name = bad.to_string();
})
.unwrap_err();
assert!(
err.to_string().contains("unsafe artifact name"),
"name {bad:?} should be rejected, got: {err}"
);
}
assert!(!store.contains("unsafe_name"));
}
#[test]
fn test_store_keys_for_crates_empty() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let result = store.keys_for_crates(&[]).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_store_keys_for_crates_with_entries() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"content").unwrap();
store
.put(
"k1",
"serde",
&["lib".into()],
&[],
"",
"dev",
&[(output.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
rewrite_source(&output, b"content2");
store
.put(
"k2",
"tokio",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let result = store.keys_for_crates(&["serde".to_string()]).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].crate_name, "serde");
let result = store
.keys_for_crates(&["serde".to_string(), "tokio".to_string()])
.unwrap();
assert_eq!(result.len(), 2);
}
#[test]
fn test_store_keys_for_crates_nonexistent() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let result = store.keys_for_crates(&["nonexistent".to_string()]).unwrap();
assert!(result.is_empty());
}
#[test]
fn keys_for_crates_errors_on_non_text_cache_key_row() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed) \
VALUES (x'80', 'badcrate', 1, 1)",
[],
)
.unwrap();
let err = store
.keys_for_crates(&["badcrate".to_string()])
.unwrap_err();
assert!(
err.to_string().contains("Invalid column type"),
"expected SQLite type error, got: {err}"
);
}
#[test]
fn test_store_put_records_metadata() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
std::fs::write(&output, b"my rlib content").unwrap();
store
.put(
"meta_key",
"mycrate",
&["lib".into(), "rlib".into()],
&["std".into(), "derive".into()],
"x86_64-unknown-linux-gnu",
"release",
&[(output, "lib.rlib".into())],
"stdout text",
"stderr text",
)
.unwrap();
let meta = store.get("meta_key").unwrap().unwrap();
assert_eq!(meta.crate_name, "mycrate");
assert_eq!(meta.crate_types, vec!["lib", "rlib"]);
assert_eq!(meta.features, vec!["std", "derive"]);
assert_eq!(meta.target, "x86_64-unknown-linux-gnu");
assert_eq!(meta.profile, "release");
assert_eq!(meta.stdout, "stdout text");
assert_eq!(meta.stderr, "stderr text");
assert_eq!(meta.files.len(), 1);
assert!(!meta.files[0].hash.is_empty());
}
#[test]
fn wait_for_committed_returns_false_without_an_owner() {
let dir = tempfile::tempdir().unwrap();
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"store::tests::wait_for_committed_missing_child_fixture",
"--ignored",
])
.env("KACHE_TEST_WAIT_ROOT", dir.path())
.spawn()
.unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
loop {
match child.try_wait().unwrap() {
Some(status) => {
assert!(status.success(), "wait fixture failed: {status}");
break;
}
None if std::time::Instant::now() >= deadline => {
child.kill().unwrap();
child.wait().unwrap();
panic!("waiting without an owner must return promptly");
}
None => std::thread::sleep(Duration::from_millis(10)),
}
}
}
#[test]
#[ignore = "subprocess fixture for wait_for_committed_returns_false_without_an_owner"]
fn wait_for_committed_missing_child_fixture() {
let root = PathBuf::from(std::env::var_os("KACHE_TEST_WAIT_ROOT").expect("fixture root"));
let store = Store::open(test_config(&root)).unwrap();
assert!(!store.wait_for_committed("nope").unwrap());
}
#[test]
fn wait_for_committed_returns_true_for_an_existing_entry() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("already-committed.rlib");
fs::write(&output, b"committed output").unwrap();
store
.put(
"already-committed",
"peer",
&["rlib".to_string()],
&[],
"host",
"dev",
&[(output, "already-committed.rlib".to_string())],
"",
"",
)
.unwrap();
assert!(store.wait_for_committed("already-committed").unwrap());
}
#[test]
fn wait_for_committed_observes_advisory_lock_release() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
let cache_key = "peer-commit";
let owner_lock = owner.try_lock(cache_key).unwrap().expect("owner lock");
let root = dir.path().to_path_buf();
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
let store = Store::open(test_config(&root)).unwrap();
ready_tx.send(()).unwrap();
store
.wait_for_committed_with_timeout(cache_key, Duration::from_secs(5))
.unwrap()
});
ready_rx.recv().unwrap();
std::thread::sleep(Duration::from_millis(50));
let output = dir.path().join("peer.rlib");
fs::write(&output, b"peer output").unwrap();
owner
.put(
cache_key,
"peer",
&["rlib".to_string()],
&[],
"host",
"dev",
&[(output, "peer.rlib".to_string())],
"",
"",
)
.unwrap();
drop(owner_lock);
assert!(
waiter.join().unwrap(),
"waiter must observe the committed key"
);
assert!(
owner.entry_dir(cache_key).with_extension("lock").exists(),
"waiting must not depend on deleting the lock file"
);
}
#[test]
fn wait_for_committed_respects_timeout_while_owned() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
let waiter = Store::open(&config).unwrap();
let _lock = owner.try_lock("slow-peer").unwrap().expect("owner lock");
assert!(
!waiter
.wait_for_committed_with_timeout("slow-peer", Duration::from_millis(10))
.unwrap()
);
}
#[test]
#[cfg(target_os = "macos")]
fn test_exclude_from_indexing_creates_sentinel() {
let dir = tempfile::tempdir().unwrap();
if let Some(handle) = exclude_from_indexing(dir.path()) {
let _ = handle.join();
}
let sentinel = dir.path().join(".metadata_never_index");
assert!(sentinel.exists());
assert!(
sentinel.metadata().unwrap().len() == 0,
"sentinel should be empty"
);
if let Some(handle) = exclude_from_indexing(dir.path()) {
let _ = handle.join();
}
assert!(sentinel.exists());
}
#[test]
#[cfg(target_os = "macos")]
fn test_exclude_from_indexing_sets_tmutil_xattr() {
let dir = tempfile::tempdir().unwrap();
if let Some(handle) = exclude_from_indexing(dir.path()) {
let _ = handle.join();
}
let output = std::process::Command::new("tmutil")
.args(["isexcluded", &dir.path().display().to_string()])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("[Excluded]"),
"expected [Excluded] in tmutil output, got: {stdout}"
);
assert!(
exclude_from_indexing(dir.path()).is_none(),
"already-excluded dir must skip the tmutil subprocess"
);
}
#[test]
#[cfg(target_os = "macos")]
fn test_exclude_from_indexing_skips_existing_sentinel() {
let dir = tempfile::tempdir().unwrap();
let sentinel = dir.path().join(".metadata_never_index");
fs::write(&sentinel, b"existing").unwrap();
if let Some(handle) = exclude_from_indexing(dir.path()) {
let _ = handle.join();
}
assert_eq!(fs::read(&sentinel).unwrap(), b"existing");
}
#[test]
fn test_blob_path_sharding() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let hash = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
let path = store.blob_path(hash);
assert!(
path.to_string_lossy()
.replace('\\', "/")
.contains("blobs/ab/")
);
assert!(path.to_string_lossy().ends_with(hash));
}
#[test]
fn test_blobs_table_created() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let count: i64 = store
.db
.query_row("SELECT COUNT(*) FROM blobs", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
}
#[test]
#[cfg(target_os = "macos")]
fn test_exclude_from_indexing_nonexistent_dir_silent() {
let dir = PathBuf::from("/tmp/kache_test_nonexistent_874291");
assert!(!dir.exists());
if let Some(handle) = exclude_from_indexing(&dir) {
let _ = handle.join();
}
}
#[test]
fn test_put_creates_blob() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
fs::write(&output, b"rlib content").unwrap();
store
.put(
"k1",
"mycrate",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let meta_path = store.entry_dir("k1").join("meta.json");
let content = fs::read_to_string(&meta_path).unwrap();
let meta: EntryMeta = serde_json::from_str(&content).unwrap();
let blob = store.blob_path(&meta.files[0].hash);
assert!(
blob.exists(),
"blob file should exist at {}",
blob.display()
);
let entry_dir = store.entry_dir("k1");
let mut files: Vec<_> = fs::read_dir(&entry_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
files.sort();
assert_eq!(
files,
vec!["meta.json"],
"entry dir should only contain meta.json"
);
}
#[test]
fn test_put_deduplicates_identical_content() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
rewrite_source(&output, b"same content");
store
.put(
"k1",
"crate_a",
&["lib".into()],
&[],
"",
"dev",
&[(output.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
rewrite_source(&output, b"same content");
store
.put(
"k2",
"crate_a",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let m1: EntryMeta =
serde_json::from_str(&fs::read_to_string(store.entry_dir("k1").join("meta.json")).unwrap())
.unwrap();
let m2: EntryMeta =
serde_json::from_str(&fs::read_to_string(store.entry_dir("k2").join("meta.json")).unwrap())
.unwrap();
assert_eq!(m1.files[0].hash, m2.files[0].hash);
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![m1.files[0].hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount, 2);
}
#[test]
fn test_get_verifies_blobs_not_entry_files() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
fs::write(&output, b"content").unwrap();
store
.put(
"k1",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
assert!(!store.entry_dir("k1").join("lib.rlib").exists());
let meta = store.get("k1").unwrap();
assert!(meta.is_some());
}
#[test]
fn test_get_evicts_when_blob_missing() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
fs::write(&output, b"content").unwrap();
store
.put(
"k1",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let meta_content = fs::read_to_string(store.entry_dir("k1").join("meta.json")).unwrap();
let meta: EntryMeta = serde_json::from_str(&meta_content).unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let mut perms = fs::metadata(&blob).unwrap().permissions();
perms.set_readonly(false);
fs::set_permissions(&blob, perms).unwrap();
fs::remove_file(&blob).unwrap();
let result = store.get("k1").unwrap();
assert!(result.is_none());
assert!(!store.contains("k1"));
}
#[test]
fn test_put_blob_is_readonly() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
fs::write(&output, b"content").unwrap();
store
.put(
"k1",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let meta: EntryMeta =
serde_json::from_str(&fs::read_to_string(store.entry_dir("k1").join("meta.json")).unwrap())
.unwrap();
let blob = store.blob_path(&meta.files[0].hash);
let perms = fs::metadata(&blob).unwrap().permissions();
assert!(perms.readonly(), "blob should be read-only");
}
#[test]
fn test_remove_entry_decrements_refcount() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
rewrite_source(&output, b"shared content");
store
.put(
"k1",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
rewrite_source(&output, b"shared content");
store
.put(
"k2",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let meta_content = fs::read_to_string(store.entry_dir("k1").join("meta.json")).unwrap();
let meta: EntryMeta = serde_json::from_str(&meta_content).unwrap();
let hash = meta.files[0].hash.clone();
let blob = store.blob_path(&hash);
store.remove_entry("k1").unwrap();
assert!(blob.exists(), "blob should survive when refcount > 0");
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![&hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount, 1);
store.remove_entry("k2").unwrap();
assert!(!blob.exists(), "blob should be deleted when refcount = 0");
let count: i64 = store
.db
.query_row(
"SELECT COUNT(*) FROM blobs WHERE hash = ?1",
params![&hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_concurrent_puts_sharing_blob_are_consistent() {
const N: usize = 8;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
Store::open(&config).unwrap();
let content = b"identical artifact content shared across all entries";
let stores: Vec<_> = (0..N)
.map(|_| {
let store = Store::open(&config).unwrap();
store.db.busy_timeout(Duration::from_secs(30)).unwrap();
store
})
.collect();
let mut handles = Vec::new();
for (i, store) in stores.into_iter().enumerate() {
let src = dir.path().join(format!("art-{i}.rlib"));
std::fs::write(&src, content).unwrap();
handles.push(std::thread::spawn(move || {
store
.put(
&format!("key{i}"),
"shared",
&["lib".into()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(src, "libshared.rlib".into())],
"",
"",
)
.unwrap();
}));
}
for h in handles {
h.join().unwrap();
}
let store = Store::open(&config).unwrap();
let hash = store.get("key0").unwrap().unwrap().files[0].hash.clone();
assert_eq!(store.blob_stats().unwrap().total_blobs, 1);
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![&hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount as usize, N, "refcount must equal the entry count");
assert_blob_refs_match_mappings(&store);
assert!(store.blob_path(&hash).is_file());
for i in 0..N {
assert!(
store.contains(&format!("key{i}")),
"entry key{i} must be committed"
);
}
let shard = store.blob_path(&hash).parent().unwrap().to_path_buf();
let tmp_left = std::fs::read_dir(&shard)
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().ends_with(".tmp"))
.count();
assert_eq!(tmp_left, 0, "no leftover .tmp files");
for i in 0..N - 1 {
store.remove_entry(&format!("key{i}")).unwrap();
}
assert!(
store.blob_path(&hash).is_file(),
"blob persists while still referenced"
);
store.remove_entry(&format!("key{}", N - 1)).unwrap();
assert!(
!store.blob_path(&hash).is_file(),
"blob reclaimed once the last reference is gone"
);
assert_eq!(store.blob_stats().unwrap().total_blobs, 0);
}
#[test]
fn test_concurrent_put_remove_never_dangles() {
const THREADS: usize = 8;
const ROUNDS: usize = 30;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
Store::open(&config).unwrap();
let content = b"hot shared blob churned by concurrent puts and removes";
let stores: Vec<_> = (0..THREADS)
.map(|_| {
let store = Store::open(&config).unwrap();
store.db.busy_timeout(Duration::from_secs(60)).unwrap();
store
})
.collect();
let start = std::sync::Arc::new(std::sync::Barrier::new(THREADS));
let mut handles = Vec::new();
for (t, store) in stores.into_iter().enumerate() {
let dir_path = dir.path().to_path_buf();
let start = std::sync::Arc::clone(&start);
handles.push(std::thread::spawn(move || {
start.wait();
for r in 0..ROUNDS {
let key = format!("t{t}r{r}");
let src = dir_path.join(format!("src-{t}-{r}.rlib"));
std::fs::write(&src, content).unwrap();
store
.put(
&key,
"shared",
&["lib".into()],
&[],
"tgt",
"dev",
&[(src, "lib.rlib".into())],
"",
"",
)
.unwrap();
let meta = store
.get(&key)
.unwrap()
.unwrap_or_else(|| panic!("entry {key} vanished right after put"));
assert!(
store.blob_path(&meta.files[0].hash).is_file(),
"blob missing while {key} still references it"
);
store.remove_entry(&key).unwrap();
}
}));
}
for h in handles {
h.join().unwrap();
}
let store = Store::open(&config).unwrap();
assert_eq!(store.blob_stats().unwrap().total_blobs, 0);
}
#[test]
fn publish_racing_an_unlink_never_errors() {
const PUBLISHERS: usize = 4;
const ROUNDS: usize = 400;
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.deferred_durability = true;
let store = Store::open(&config).unwrap();
let content = b"one blob, published and unlinked in a loop";
let source = dir.path().join("out.rlib");
fs::write(&source, content).unwrap();
let hash = crate::file_hash::hash_file(&source).unwrap();
let blob = store.blob_path(&hash);
let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let remover = {
let blob = blob.clone();
let done = std::sync::Arc::clone(&done);
std::thread::spawn(move || {
while !done.load(Ordering::Relaxed) {
unlink_blob(&blob);
std::thread::yield_now();
}
})
};
let publishers: Vec<_> = (0..PUBLISHERS)
.map(|_| {
let store = Store::open(&config).unwrap();
let source = source.clone();
let hash = hash.clone();
std::thread::spawn(move || {
for _ in 0..ROUNDS {
let (staged, ingest) = store.stage_blob_from_source(&source, false).unwrap();
store
.publish_staged_blob(&staged, ingest, &hash, content.len() as u64)
.unwrap();
}
})
})
.collect();
for p in publishers {
p.join().unwrap();
}
done.store(true, Ordering::Relaxed);
remover.join().unwrap();
store
.rematerialize_and_verify(&source, &hash, "out.rlib", false)
.unwrap();
assert_eq!(fs::read(&blob).unwrap(), content);
}
#[test]
fn losing_remover_leaves_a_publishers_directory_alone() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let src = dir.path().join("x.rlib");
std::fs::write(&src, b"mid-publication content").unwrap();
store
.put(
"pub",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src, "lib.rlib".into())],
"",
"",
)
.unwrap();
store
.db
.execute("DELETE FROM entries WHERE cache_key = 'pub'", [])
.unwrap();
store
.db
.execute("DELETE FROM entry_blobs WHERE cache_key = 'pub'", [])
.unwrap();
store.remove_entry("pub").unwrap();
assert!(
store.entry_dir("pub").join("meta.json").exists(),
"the loser deleted no row and must leave the publisher's meta.json alone"
);
}
#[test]
fn removal_rolls_back_when_the_entry_was_republished_mid_flight() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let src_a = dir.path().join("a.rlib");
std::fs::write(&src_a, b"generation A").unwrap();
store
.put(
"aba",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src_a, "lib.rlib".into())],
"",
"",
)
.unwrap();
let outcome = store
.remove_entry_guarded_with_hook("aba", None, || {
let src_b = dir.path().join("b.rlib");
std::fs::write(&src_b, b"generation B, longer content").unwrap();
store
.put(
"aba",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src_b, "lib.rlib".into())],
"",
"",
)
.unwrap();
})
.unwrap();
assert!(
matches!(outcome, GuardedRemoval::Skipped),
"a removal that lost to a republication must report nothing removed"
);
assert!(
store.contains("aba"),
"generation B's row must survive the rolled-back removal"
);
let meta = store.get("aba").unwrap().expect("B must stay restorable");
let hash = &meta.files[0].hash;
assert!(
store.blob_path(hash).is_file(),
"generation B's blob must remain on disk"
);
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(
refcount, 1,
"B's refcounts must be untouched by the rollback"
);
}
#[test]
fn republication_during_removal_cleanup_is_never_stranded() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let src_a = dir.path().join("a.rlib");
std::fs::write(&src_a, b"generation A").unwrap();
store
.put(
"key",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src_a, "lib.rlib".into())],
"",
"",
)
.unwrap();
let released = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let attempting = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let published = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let wait_for = |flag: &std::sync::atomic::AtomicBool, cap: Duration| {
let deadline = std::time::Instant::now() + cap;
while !flag.load(std::sync::atomic::Ordering::Acquire)
&& std::time::Instant::now() < deadline
{
std::thread::sleep(Duration::from_millis(5));
}
};
let publisher = {
let config = config.clone();
let dir_path = dir.path().to_path_buf();
let released = std::sync::Arc::clone(&released);
let attempting = std::sync::Arc::clone(&attempting);
let published = std::sync::Arc::clone(&published);
std::thread::spawn(move || {
let store = Store::open(&config).unwrap();
store.db.busy_timeout(Duration::from_secs(30)).unwrap();
let src_b = dir_path.join("b.rlib");
std::fs::write(&src_b, b"generation B, republished").unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while !released.load(std::sync::atomic::Ordering::Acquire)
&& std::time::Instant::now() < deadline
{
std::thread::sleep(Duration::from_millis(5));
}
attempting.store(true, std::sync::atomic::Ordering::Release);
store
.put(
"key",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src_b, "lib.rlib".into())],
"",
"",
)
.unwrap();
published.store(true, std::sync::atomic::Ordering::Release);
})
};
let removed = store
.remove_entry_guarded_with_hooks(
"key",
None,
|| {},
|| {
released.store(true, std::sync::atomic::Ordering::Release);
wait_for(&attempting, Duration::from_secs(5));
assert!(
attempting.load(std::sync::atomic::Ordering::Acquire),
"publisher never reached put; the interleaving was not exercised"
);
wait_for(&published, Duration::from_millis(1500));
},
)
.unwrap();
publisher.join().unwrap();
assert!(
matches!(removed, GuardedRemoval::Reclaimed(_)),
"the removal owned generation A's row"
);
assert!(
store.contains("key"),
"generation B's row must be committed"
);
assert!(
store.entry_dir("key").join("meta.json").is_file(),
"generation B's meta.json must survive the racing removal's cleanup"
);
let meta = store.get("key").unwrap().expect("B must be restorable");
let hash = meta.files[0].hash.clone();
assert!(
store.blob_path(&hash).is_file(),
"generation B's blob must be on disk"
);
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount, 1, "B's refcounts must be intact");
}
#[cfg(unix)]
#[test]
fn persistent_directory_cleanup_failure_is_an_error() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let src = dir.path().join("x.rlib");
std::fs::write(&src, b"content").unwrap();
store
.put(
"stuck",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src, "lib.rlib".into())],
"",
"",
)
.unwrap();
let entry_dir = store.entry_dir("stuck");
let inner = entry_dir.join("legacy").join("inner");
std::fs::create_dir_all(&inner).unwrap();
std::fs::write(inner.join("artifact"), b"x").unwrap();
std::fs::set_permissions(&inner, std::fs::Permissions::from_mode(0o555)).unwrap();
let stashed_hash = {
let meta: EntryMeta =
serde_json::from_str(&std::fs::read_to_string(entry_dir.join("meta.json")).unwrap())
.unwrap();
meta.files[0].hash.clone()
};
let err = store.remove_entry("stuck");
std::fs::set_permissions(&inner, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(
err.is_err(),
"a persistent cleanup failure must not be swallowed as a lost race"
);
assert!(
!store.contains("stuck"),
"the logical removal must stay committed across a cleanup failure"
);
assert!(
store.blob_path(&stashed_hash).is_file(),
"blob unlinks run only after directory cleanup succeeds; a failed \
cleanup leaves the file for the orphan sweep"
);
}
#[test]
fn two_removers_on_one_entry_never_double_decrement_shared_blob() {
const ROUNDS: usize = 25;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
Store::open(&config).unwrap();
for round in 0..ROUNDS {
let content = format!("shared blob for round {round}");
let victim = format!("victim-{round}");
let survivor = format!("survivor-{round}");
let store = Store::open(&config).unwrap();
for key in [&victim, &survivor] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, content.as_bytes()).unwrap();
store
.put(
key,
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src, "lib.rlib".into())],
"",
"",
)
.unwrap();
}
let hash = store.get(&survivor).unwrap().unwrap().files[0].hash.clone();
let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
let mut handles = Vec::new();
for _ in 0..2 {
let config = test_config(dir.path());
let victim = victim.clone();
let barrier = barrier.clone();
handles.push(std::thread::spawn(move || {
let store = Store::open(&config).unwrap();
barrier.wait();
store.remove_entry_guarded(&victim, None)
}));
}
let removed: Vec<bool> = handles
.into_iter()
.map(|h| {
matches!(
h.join().unwrap().expect("losing remover must not error"),
GuardedRemoval::Reclaimed(_)
)
})
.collect();
assert_eq!(
removed.iter().filter(|&&won| won).count(),
1,
"exactly one remover releases the entry (round {round}): {removed:?}"
);
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![&hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(
refcount, 1,
"survivor's shared blob refcount (round {round})"
);
assert!(
store.blob_path(&hash).is_file(),
"shared blob unlinked out from under the survivor (round {round})"
);
assert!(
store.get(&survivor).unwrap().is_some(),
"survivor entry must stay restorable (round {round})"
);
store.remove_entry(&survivor).unwrap();
}
}
#[test]
fn evict_does_not_fire_while_physical_size_is_within_budget() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 300;
let store = Store::open(&config).unwrap();
for key in ["dup_a", "dup_b"] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, vec![b'x'; 200]).unwrap();
store
.put(
key,
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&src);
}
assert_eq!(store.total_size().unwrap(), 400, "logical double-counts");
assert_eq!(store.physical_size().unwrap(), 200, "disk holds one copy");
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(
stats.entries_evicted, 0,
"physical 200 <= max 300 (the trigger): nothing to evict"
);
assert!(store.contains("dup_a") && store.contains("dup_b"));
}
#[test]
fn evict_prefers_and_stops_on_actually_freed_bytes() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 500 * 1024; let store = Store::open(&config).unwrap();
for key in ["shared_a", "shared_b", "shared_c"] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, vec![b's'; 300 * 1024]).unwrap();
store
.put(
key,
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&src);
}
let src = dir.path().join("unique.rlib");
std::fs::write(&src, vec![b'u'; 300 * 1024]).unwrap();
store
.put(
"unique",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&src);
assert_eq!(store.physical_size().unwrap(), 600 * 1024);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(
stats.entries_evicted, 1,
"evicting `unique` frees 300 KiB physical → 300 <= 450 KiB, done"
);
assert_eq!(stats.bytes_freed, 300 * 1024);
assert_eq!(stats.disk_bytes_reclaimed, 300 * 1024);
assert!(
!store.contains("unique"),
"the freeing entry is the one evicted"
);
for key in ["shared_a", "shared_b", "shared_c"] {
assert!(store.contains(key), "{key} frees nothing and must survive");
}
}
#[test]
fn evict_noop_within_the_hysteresis_band() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 1000; let store = Store::open(&config).unwrap();
for i in 0..5 {
let src = dir.path().join(format!("u{i}.rlib"));
std::fs::write(&src, format!("{i}{}", "x".repeat(189)).as_bytes()).unwrap();
store
.put(
&format!("u{i}"),
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src, "lib.rlib".into())],
"",
"",
)
.unwrap();
}
assert_eq!(store.physical_size().unwrap(), 950);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(
stats.entries_evicted, 0,
"950 is inside the band (900 < 950 <= 1000): evict() must not fire"
);
assert_eq!(store.physical_size().unwrap(), 950);
}
#[test]
fn evict_fires_at_the_trigger_and_stops_at_the_target() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 1000; let store = Store::open(&config).unwrap();
for i in 0..6 {
let src = dir.path().join(format!("u{i}.rlib"));
std::fs::write(&src, format!("{i}{}", "x".repeat(189)).as_bytes()).unwrap();
store
.put(
&format!("u{i}"),
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&src);
}
assert_eq!(store.physical_size().unwrap(), 1140);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(
stats.entries_evicted, 2,
"1140 > 1000 must trigger, and two 190-byte evictions reach 760 <= 900"
);
assert_eq!(store.physical_size().unwrap(), 760);
}
#[test]
fn size_sweep_records_shadow_verdicts_and_demand_splits() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 450_000; let store = Store::open(&config).unwrap();
for (key, bytes, fill, compile_ms) in [
("huge_expensive", 400_000usize, b'a', 60_000u64),
("small_cheap", 100_000usize, b'b', 1u64),
] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, vec![fill; bytes]).unwrap();
store
.put_with_compile_time(
key,
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
compile_ms,
)
.unwrap();
store.remove_clone_for_test(&src);
}
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(
stats.entries_evicted, 1,
"the live policy evicts the largest entry and reaches its target"
);
assert!(!store.contains("huge_expensive"));
assert!(store.contains("small_cheap"));
let (shadow_policy, shadow_would_evict): (String, i64) = store
.db
.query_row(
"SELECT shadow_policy, shadow_would_evict FROM eviction_tombstones
WHERE cache_key = 'huge_expensive'",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(shadow_policy, "value-density");
assert_eq!(shadow_would_evict, 0, "the shadow would have kept it");
assert!(store.get("huge_expensive").unwrap().is_none());
assert_eq!(
store.shadow_demand_split().unwrap(),
ShadowDemandSplit {
agreed: 0,
agreed_demanded: 0,
shadow_kept: 1,
shadow_kept_demanded: 1,
}
);
store
.db
.execute_batch(
"INSERT INTO eviction_tombstones
(cache_key, policy, compile_time_ms, shadow_policy, shadow_would_evict, demanded_at)
VALUES ('agreed_row', 'size-pressure', 500, 'value-density', 1, datetime('now'));
INSERT INTO eviction_tombstones (cache_key, policy, compile_time_ms)
VALUES ('pre_shadow_row', 'size-pressure', 500);
INSERT INTO eviction_tombstones
(cache_key, policy, compile_time_ms, shadow_policy, shadow_would_evict)
VALUES ('unknown_cost_row', 'size-pressure', 0, 'value-density', 0);",
)
.unwrap();
assert_eq!(
store.shadow_demand_split().unwrap(),
ShadowDemandSplit {
agreed: 1,
agreed_demanded: 1,
shadow_kept: 1,
shadow_kept_demanded: 1,
}
);
}
#[test]
fn size_sweep_records_the_shadows_own_victims_for_both_sided_cost() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 450_000; let store = Store::open(&config).unwrap();
for (key, bytes, fill, compile_ms) in [
("huge_expensive", 400_000usize, b'a', 60_000u64),
("small_cheap", 100_000usize, b'b', 7u64),
] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, vec![fill; bytes]).unwrap();
store
.put_with_compile_time(
key,
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
compile_ms,
)
.unwrap();
store.remove_clone_for_test(&src);
}
let age = |store: &Store| {
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
};
age(&store);
store.evict().unwrap();
assert!(!store.contains("huge_expensive"));
assert!(store.contains("small_cheap"), "the live policy kept it");
let recorded = |store: &Store| -> Vec<(String, i64, i64)> {
let mut stmt = store
.db
.prepare("SELECT cache_key, size, compile_time_ms FROM shadow_victims")
.unwrap();
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.unwrap()
.collect::<rusqlite::Result<_>>()
.unwrap()
};
assert_eq!(
recorded(&store),
vec![("small_cheap".to_string(), 100_000, 7)],
"only the entry the shadow would have taken instead"
);
let quiet = store.shadow_counterfactual().unwrap();
assert_eq!(
quiet.live,
PolicyCost {
evicted: 1,
bytes: 400_000,
demanded_compile_time_ms: 0,
}
);
assert_eq!(
quiet.shadow,
PolicyCost {
evicted: 1,
bytes: 100_000,
demanded_compile_time_ms: 0,
}
);
assert!(store.get("huge_expensive").unwrap().is_none());
assert!(store.get("small_cheap").unwrap().is_some());
let used = store.shadow_counterfactual().unwrap();
assert_eq!(used.live.demanded_compile_time_ms, 60_000);
assert_eq!(used.shadow.demanded_compile_time_ms, 7);
store
.db
.execute(
"UPDATE shadow_victims SET swept_at = datetime('now', '-1 day')",
[],
)
.unwrap();
let first: String = store
.db
.query_row("SELECT swept_at FROM shadow_victims", [], |row| row.get(0))
.unwrap();
let victim = store
.eviction_candidates_for(SweepOrigin::Requested)
.unwrap()
.into_iter()
.find(|entry| entry.key == "small_cheap")
.unwrap();
store.record_shadow_victims("value-density", "2026-01-01 00:00:00", &[&victim]);
let again: String = store
.db
.query_row("SELECT swept_at FROM shadow_victims", [], |row| row.get(0))
.unwrap();
assert_eq!(again, first);
store.prune_tombstones(0).unwrap();
assert!(recorded(&store).is_empty());
}
#[test]
fn shadow_only_victims_skip_what_the_sweep_protects() {
let features = |key: &str| crate::eviction::EntryFeatures {
key: key.to_string(),
size: 1,
hit_count: 0,
idle_hours: 1.0,
content_hash: None,
committed: true,
compile_time_ms: 1,
reclaimable_bytes: Some(1),
recently_accessed: key == "recent",
recently_imported: key == "imported",
};
let all: Vec<_> = ["evicted", "durable", "held", "recent", "imported", "plain"]
.into_iter()
.map(features)
.collect();
let by_key = all.iter().map(|f| (f.key.as_str(), f)).collect();
let shadow = ShadowSelection {
policy: "value-density",
victims: all.iter().map(|f| f.key.clone()).collect(),
};
let evicted = ["evicted"].into_iter().collect();
let durable = ["durable".to_string()].into_iter().collect();
let held = [("held".to_string(), 1)].into_iter().collect();
let kept = shadow_only_victims(&shadow, &evicted, &by_key, &durable, &held);
assert_eq!(
kept.iter().map(|f| f.key.as_str()).collect::<Vec<_>>(),
vec!["plain"]
);
}
#[test]
fn evict_reports_physical_bytes_freed_not_logical() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 200; let store = Store::open(&config).unwrap();
for key in ["a", "b", "c"] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, vec![b'z'; 300]).unwrap();
store
.put(
key,
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&src);
}
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
let stats = store.evict().unwrap();
assert_eq!(
stats.entries_evicted, 3,
"zero-freeing removals must not stop the sweep early"
);
assert_eq!(stats.bytes_freed, 300, "the blob's bytes are freed once");
assert_eq!(stats.disk_bytes_reclaimed, 300);
assert_eq!(stats.blobs_removed, 1);
assert_eq!(store.physical_size().unwrap(), 0);
}
#[test]
fn backfill_entry_blobs_reconstructs_marginal_bytes() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
for (key, content) in [("shared_a", b'x'), ("shared_b", b'x'), ("solo", b'y')] {
let src = dir.path().join(format!("{key}.rlib"));
std::fs::write(&src, vec![content; 100]).unwrap();
store
.put(
key,
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src, "lib.rlib".into())],
"",
"",
)
.unwrap();
}
store.db.execute("DELETE FROM entry_blobs", []).unwrap();
let unknowns = store.eviction_candidates().unwrap();
assert!(
unknowns.iter().all(|f| f.reclaimable_bytes.is_none()),
"un-backfilled entries must report unknown, not zero"
);
assert_eq!(store.backfill_entry_blobs().unwrap(), 3);
assert_eq!(store.backfill_entry_blobs().unwrap(), 0, "converges");
let features = store.eviction_candidates().unwrap();
let by_key: std::collections::HashMap<&str, &crate::eviction::EntryFeatures> =
features.iter().map(|f| (f.key.as_str(), f)).collect();
assert_eq!(by_key["shared_a"].reclaimable_bytes, Some(0));
assert_eq!(by_key["shared_b"].reclaimable_bytes, Some(0));
assert_eq!(by_key["solo"].reclaimable_bytes, Some(100));
}
#[test]
fn test_clear_removes_blobs_too() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let output = dir.path().join("lib.rlib");
fs::write(&output, b"content").unwrap();
store
.put(
"k1",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
store.clear().unwrap();
let blobs_dir = store.blobs_dir();
if blobs_dir.exists() {
let has_files = fs::read_dir(&blobs_dir)
.unwrap()
.flatten()
.any(|e| e.path().is_dir());
assert!(
!has_files,
"blobs dir should have no shard subdirs after clear"
);
}
let count: i64 = store
.db
.query_row("SELECT COUNT(*) FROM blobs", [], |row| row.get(0))
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_get_lazily_migrates_legacy_entry() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = config.store_dir().join("old_key");
fs::create_dir_all(&entry_dir).unwrap();
let content = b"old format artifact";
fs::write(entry_dir.join("lib.rlib"), content).unwrap();
let hash = crate::file_hash::hash_file(&entry_dir.join("lib.rlib")).unwrap();
let meta = EntryMeta {
cache_key: "old_key".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "old_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: content.len() as u64,
hash: hash.clone(),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed) VALUES ('old_key', 'old_crate', ?1, 1)",
params![content.len() as i64],
)
.unwrap();
let result = store.get("old_key").unwrap();
assert!(result.is_some());
let blob = store.blob_path(&hash);
assert!(
blob.exists(),
"get() should have migrated artifact to blob store"
);
assert!(!entry_dir.join("lib.rlib").exists());
}
#[test]
fn get_evicts_when_lazy_legacy_migration_fails() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = config.store_dir().join("old_bad_key");
fs::create_dir_all(&entry_dir).unwrap();
let artifact = entry_dir.join("lib.rlib");
fs::write(&artifact, b"old format artifact").unwrap();
let hash = crate::file_hash::hash_file(&artifact).unwrap();
let meta = EntryMeta {
cache_key: "old_bad_key".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "old_bad_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: fs::metadata(&artifact).unwrap().len(),
hash: hash.clone(),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed) \
VALUES ('old_bad_key', 'old_bad_crate', ?1, 1)",
params![fs::metadata(&artifact).unwrap().len() as i64],
)
.unwrap();
let shard_path = store.blobs_dir().join(&hash[..2]);
fs::create_dir_all(store.blobs_dir()).unwrap();
fs::write(&shard_path, b"not a shard directory").unwrap();
let result = store.get("old_bad_key").unwrap();
assert!(
result.is_none(),
"failed migration falls through to eviction"
);
assert!(!store.contains("old_bad_key"));
assert!(shard_path.is_file(), "unrelated shard conflict remains");
}
#[test]
fn test_migrate_to_blobs_bulk() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let content = b"shared artifact bytes";
let hash = {
let tmp = dir.path().join("tmp");
fs::write(&tmp, content).unwrap();
crate::file_hash::hash_file(&tmp).unwrap()
};
for key in &["old1", "old2"] {
let entry_dir = config.store_dir().join(key);
fs::create_dir_all(&entry_dir).unwrap();
fs::write(entry_dir.join("lib.rlib"), content).unwrap();
let meta = EntryMeta {
cache_key: key.to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "shared_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size: content.len() as u64,
hash: hash.clone(),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store
.db
.execute(
&format!(
"INSERT INTO entries (cache_key, crate_name, size, committed) VALUES ('{key}', 'shared_crate', {}, 1)",
content.len()
),
[],
)
.unwrap();
}
let stats = store.migrate_to_blobs(|_, _| {}).unwrap();
assert_eq!(stats.entries_migrated, 2);
assert_eq!(store.backfill_entry_blobs().unwrap(), 2);
assert_blob_refs_match_mappings(&store);
let refcount: i64 = store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|row| row.get(0),
)
.unwrap();
assert_eq!(refcount, 2);
}
#[test]
fn test_blob_stats() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let stats = store.blob_stats().unwrap();
assert_eq!(stats.total_blobs, 0);
assert_eq!(stats.savings, 0);
let output = dir.path().join("lib.rlib");
rewrite_source(&output, b"shared content!");
store
.put(
"k1",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
rewrite_source(&output, b"shared content!");
store
.put(
"k2",
"c",
&["lib".into()],
&[],
"",
"dev",
&[(output, "lib.rlib".into())],
"",
"",
)
.unwrap();
let stats = store.blob_stats().unwrap();
assert_eq!(stats.total_blobs, 1); assert!(stats.total_logical_size > stats.total_blob_size); assert!(stats.savings > 0);
}
fn write_temp_file(dir: &Path, name: &str, content: &[u8]) -> PathBuf {
let path = dir.join(name);
rewrite_source(&path, content);
path
}
fn rewrite_source(path: &Path, content: &[u8]) {
let _ = fs::remove_file(path);
fs::write(path, content).unwrap();
}
fn read_meta(store: &Store, cache_key: &str) -> EntryMeta {
let meta_path = store.entry_dir(cache_key).join("meta.json");
let content = fs::read_to_string(&meta_path).unwrap();
serde_json::from_str(&content).unwrap()
}
fn blob_refcount(store: &Store, hash: &str) -> Option<i64> {
store
.db
.query_row(
"SELECT refcount FROM blobs WHERE hash = ?1",
params![hash],
|row| row.get(0),
)
.ok()
}
fn blob_table_count(store: &Store) -> i64 {
store
.db
.query_row("SELECT COUNT(*) FROM blobs", [], |row| row.get(0))
.unwrap()
}
#[test]
fn test_full_dedup_lifecycle() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let shared = write_temp_file(dir.path(), "shared.rlib", b"shared artifact data");
let unique1 = write_temp_file(dir.path(), "unique1.rlib", b"unique to entry 1");
let unique2 = write_temp_file(dir.path(), "unique2.rlib", b"unique to entry 2");
store
.put(
"entry1",
"crate_a",
&["lib".into()],
&[],
"",
"dev",
&[
(shared.clone(), "shared.rlib".into()),
(unique1, "unique1.rlib".into()),
],
"",
"",
)
.unwrap();
rewrite_source(&shared, b"shared artifact data");
store
.put(
"entry2",
"crate_b",
&["lib".into()],
&[],
"",
"dev",
&[
(shared, "shared.rlib".into()),
(unique2, "unique2.rlib".into()),
],
"",
"",
)
.unwrap();
let meta1 = read_meta(&store, "entry1");
let meta2 = read_meta(&store, "entry2");
let shared_hash = &meta1
.files
.iter()
.find(|f| f.name == "shared.rlib")
.unwrap()
.hash;
let unique1_hash = &meta1
.files
.iter()
.find(|f| f.name == "unique1.rlib")
.unwrap()
.hash;
let unique2_hash = &meta2
.files
.iter()
.find(|f| f.name == "unique2.rlib")
.unwrap()
.hash;
let shared_hash2 = &meta2
.files
.iter()
.find(|f| f.name == "shared.rlib")
.unwrap()
.hash;
assert_eq!(shared_hash, shared_hash2);
assert_eq!(blob_refcount(&store, shared_hash), Some(2));
assert_blob_refs_match_mappings(&store);
assert_eq!(blob_refcount(&store, unique1_hash), Some(1));
assert_eq!(blob_refcount(&store, unique2_hash), Some(1));
assert!(store.blob_path(shared_hash).exists());
assert!(store.blob_path(unique1_hash).exists());
assert!(store.blob_path(unique2_hash).exists());
store.remove_entry("entry1").unwrap();
assert_eq!(blob_refcount(&store, shared_hash), Some(1));
assert!(store.blob_path(shared_hash).exists());
assert!(!store.blob_path(unique1_hash).exists());
assert_eq!(blob_refcount(&store, unique1_hash), None);
store.remove_entry("entry2").unwrap();
assert!(!store.blob_path(shared_hash).exists());
assert!(!store.blob_path(unique2_hash).exists());
assert_eq!(blob_refcount(&store, shared_hash), None);
assert_eq!(blob_refcount(&store, unique2_hash), None);
assert_eq!(blob_table_count(&store), 0);
}
#[test]
fn gc_lock_is_mutually_exclusive() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let first = store.try_gc_lock().unwrap();
assert!(first.is_some(), "first GC lock acquires");
assert!(
store.try_gc_lock().unwrap().is_none(),
"a second GC lock is refused while the first is held"
);
drop(first);
assert!(
store.try_gc_lock().unwrap().is_some(),
"the GC lock is re-acquirable after release"
);
}
const TWO_HOURS: Duration = Duration::from_secs(7200);
fn aged_key_lock(config: &Config, key: &str, age: Duration) -> PathBuf {
let path = config.store_dir().join(format!("{key}.lock"));
std::fs::create_dir_all(config.store_dir()).unwrap();
std::fs::write(&path, b"1").unwrap();
set_age(&path, age);
path
}
fn set_age(path: &Path, age: Duration) {
let file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
file.set_modified(std::time::SystemTime::now() - age)
.unwrap();
}
fn sweep_key_locks(store: &Store, cap: usize) -> KeyLockSweepStats {
store
.sweep_stale_key_locks(KEY_LOCK_SWEEP_GRACE, cap)
.unwrap()
}
#[test]
fn key_lock_sweep_constants_are_pinned() {
assert_eq!(KEY_LOCK_SWEEP_GRACE, Duration::from_secs(3600));
assert_eq!(KEY_LOCK_SWEEP_GRACE, STAGING_SWEEP_GRACE);
assert_eq!(KEY_LOCK_SWEEP_CAP, 20_000);
assert_eq!(LOCK_OPEN_ATTEMPTS, 4);
assert_eq!(84_496usize.div_ceil(KEY_LOCK_SWEEP_CAP), 5);
}
#[test]
fn key_of_lock_name_accepts_only_a_valid_key_with_the_lock_suffix() {
let k = key(1);
assert_eq!(key_of_lock_name(&format!("{k}.lock")), Some(k.as_str()));
assert_eq!(key_of_lock_name(&k), None);
assert_eq!(key_of_lock_name("gc.lock"), None);
assert_eq!(key_of_lock_name("durability.lock"), None);
assert_eq!(key_of_lock_name(".lock"), None);
assert_eq!(key_of_lock_name(&format!("{k}.lock.tmp")), None);
assert_eq!(key_of_lock_name(&format!("{}.lock", &k[1..])), None);
assert_eq!(
key_of_lock_name(&format!("{}.lock", k.to_uppercase())),
None
);
}
#[test]
fn key_lock_staleness_boundary_is_inclusive_and_future_mtimes_are_young() {
let now = std::time::SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
let grace = Duration::from_secs(3600);
assert!(!key_lock_is_stale(
now - Duration::from_secs(3599),
now,
grace
));
assert!(key_lock_is_stale(now - grace, now, grace));
assert!(key_lock_is_stale(now - TWO_HOURS, now, grace));
assert!(!key_lock_is_stale(now + TWO_HOURS, now, grace));
}
#[test]
fn key_lock_sweep_stats_report_what_remains() {
let stats = KeyLockSweepStats {
seen: 10,
removed: 3,
};
assert_eq!(stats.remaining(), 7);
let none = KeyLockSweepStats {
seen: 0,
removed: 0,
};
assert_eq!(none.remaining(), 0);
}
#[test]
fn key_lock_sweep_removes_a_stale_lock_whose_key_has_no_entry() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let stale = aged_key_lock(&config, &key(1), TWO_HOURS);
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats {
seen: 1,
removed: 1
}
);
assert!(!stale.exists());
let lock = store.try_lock(&key(1)).unwrap();
assert!(lock.is_some());
assert!(stale.exists());
}
#[test]
fn key_lock_sweep_removes_the_lock_of_an_evicted_key() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let k = put_entry(&store, dir.path(), 1, "evicted", b"payload");
drop(store.try_lock(&k).unwrap().expect("claim the key"));
let lock_path = config.store_dir().join(format!("{k}.lock"));
set_age(&lock_path, TWO_HOURS);
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats {
seen: 1,
removed: 0
}
);
assert!(lock_path.exists());
store.remove_entry(&k).unwrap();
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats {
seen: 1,
removed: 1
}
);
assert!(!lock_path.exists());
}
#[test]
fn key_lock_sweep_keeps_a_young_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let young = aged_key_lock(&config, &key(1), Duration::from_secs(3000));
let old = aged_key_lock(&config, &key(2), Duration::from_secs(4200));
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats {
seen: 2,
removed: 1
}
);
assert!(young.exists());
assert!(!old.exists());
}
#[test]
fn key_lock_sweep_never_removes_a_held_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let held = store.try_lock(&key(1)).unwrap().expect("claim the key");
let path = config.store_dir().join(format!("{}.lock", key(1)));
let before = kache_fs::file_identity(&path).unwrap();
set_age(&path, TWO_HOURS);
let stats = std::thread::scope(|scope| {
scope
.spawn(|| {
let sweeper = Store::open(&config).unwrap();
sweep_key_locks(&sweeper, KEY_LOCK_SWEEP_CAP)
})
.join()
.unwrap()
});
assert_eq!(
stats,
KeyLockSweepStats {
seen: 1,
removed: 0
}
);
assert_eq!(kache_fs::file_identity(&path).unwrap(), before);
assert!(
store.try_lock(&key(1)).unwrap().is_none(),
"the holder still excludes every other claimant"
);
drop(held);
set_age(&path, TWO_HOURS);
assert_eq!(sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP).removed, 1);
}
#[test]
fn key_lock_sweep_respects_the_cap_and_converges() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
for seed in 0..7 {
aged_key_lock(&config, &key(seed), TWO_HOURS);
}
assert_eq!(
sweep_key_locks(&store, 3),
KeyLockSweepStats {
seen: 7,
removed: 3
}
);
assert_eq!(
sweep_key_locks(&store, 3),
KeyLockSweepStats {
seen: 4,
removed: 3
}
);
let last = sweep_key_locks(&store, 3);
assert_eq!(
last,
KeyLockSweepStats {
seen: 1,
removed: 1
}
);
assert_eq!(last.remaining(), 0);
assert_eq!(
sweep_key_locks(&store, 0),
KeyLockSweepStats::default(),
"nothing left"
);
}
#[test]
fn key_lock_sweep_with_a_zero_cap_only_counts() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let stale = aged_key_lock(&config, &key(1), TWO_HOURS);
assert_eq!(
sweep_key_locks(&store, 0),
KeyLockSweepStats {
seen: 1,
removed: 0
}
);
assert!(stale.exists());
}
#[test]
fn key_lock_sweep_never_touches_gc_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
drop(store.try_gc_lock().unwrap());
let path = config.store_dir().join("gc.lock");
set_age(&path, TWO_HOURS);
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats::default()
);
assert!(path.exists());
}
#[test]
fn key_lock_sweep_never_touches_durability_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
drop(store.try_durability_flush_lock().unwrap());
let path = config.store_dir().join("durability.lock");
set_age(&path, TWO_HOURS);
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats::default()
);
assert!(path.exists());
}
#[test]
fn key_lock_sweep_never_touches_a_directory_named_like_a_key_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let path = config.store_dir().join(format!("{}.lock", key(1)));
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path.join("inside"), b"kept").unwrap();
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats {
seen: 1,
removed: 0
}
);
assert!(path.join("inside").exists());
}
#[test]
fn key_lock_sweep_never_touches_an_entry_directory_or_other_files() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let k = put_entry(&store, dir.path(), 1, "kept", b"payload");
let short = config.store_dir().join("abc123.lock");
let stray = config.store_dir().join(format!("{}.lock.bak", key(2)));
for path in [&short, &stray] {
std::fs::write(path, b"1").unwrap();
set_age(path, TWO_HOURS);
}
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats::default()
);
assert!(store.entry_dir(&k).join("meta.json").exists());
assert!(short.exists());
assert!(stray.exists());
}
#[cfg(unix)]
#[test]
fn key_lock_sweep_never_follows_a_symlink_named_like_a_key_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let target = dir.path().join("elsewhere");
std::fs::write(&target, b"kept").unwrap();
set_age(&target, TWO_HOURS);
let link = config.store_dir().join(format!("{}.lock", key(1)));
std::os::unix::fs::symlink(&target, &link).unwrap();
assert_eq!(sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP).removed, 0);
let later = std::time::SystemTime::now() + TWO_HOURS;
assert!(!remove_stale_lock_file(
&link,
KEY_LOCK_SWEEP_GRACE,
later,
|| {}
));
assert!(link.symlink_metadata().is_ok());
assert!(target.exists());
}
#[test]
fn key_lock_sweep_on_a_store_with_no_directory_is_empty() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let _ = std::fs::remove_dir_all(config.store_dir());
assert_eq!(
sweep_key_locks(&store, KEY_LOCK_SWEEP_CAP),
KeyLockSweepStats::default()
);
}
#[test]
fn housekeeping_sweeps_key_locks_and_prunes_predictions() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
assert_eq!(store.sweep_housekeeping(), HousekeepingStats::default());
let stale = aged_key_lock(&config, &key(1), TWO_HOURS);
aged_key_lock(&config, &key(2), TWO_HOURS);
let young = aged_key_lock(&config, &key(3), Duration::ZERO);
let predictions = store.file_hash_cache();
for identity in ["unused", "live"] {
predictions
.put_input_prediction(identity, 1, None, "payload")
.unwrap();
}
store
.db
.execute(
"UPDATE input_predictions SET last_used = 1 WHERE identity = 'unused'",
[],
)
.unwrap();
for (path, written) in [
("/old", "2000-01-01 00:00:00"),
("/new", "9999-01-01 00:00:00"),
] {
store
.db
.execute(
"INSERT INTO file_hashes (path, size, mtime_ns, hash, updated_at)
VALUES (?1, 1, 1, 'h', ?2)",
rusqlite::params![path, written],
)
.unwrap();
}
assert_eq!(
store.sweep_housekeeping(),
HousekeepingStats {
key_locks_removed: 2,
key_locks_remaining: 1,
predictions_pruned: 1,
file_hashes_pruned: 1,
}
);
assert!(!stale.exists());
assert!(young.exists());
assert!(predictions.get_input_prediction("live").unwrap().is_some());
assert_eq!(predictions.get_input_prediction("unused").unwrap(), None);
}
#[test]
fn housekeeping_rebuilds_an_older_file_hashes_table_without_rowid() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
store
.db
.execute_batch(
"DROP TABLE file_hashes;
CREATE TABLE file_hashes (
path TEXT PRIMARY KEY, size INTEGER NOT NULL, mtime_ns INTEGER NOT NULL,
ctime_ns INTEGER NOT NULL DEFAULT 0, inode INTEGER NOT NULL DEFAULT 0,
hash TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')));
INSERT INTO file_hashes (path, size, mtime_ns, hash, updated_at)
VALUES ('/old', 1, 1, 'h', '2000-01-01 00:00:00'),
('/new', 1, 1, 'h', '9999-01-01 00:00:00');",
)
.unwrap();
let table_sql = || -> String {
store
.db
.query_row(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'file_hashes'",
[],
|row| row.get(0),
)
.unwrap()
};
assert!(!table_sql().contains("WITHOUT ROWID"));
assert_eq!(store.sweep_housekeeping().file_hashes_pruned, 1);
assert!(table_sql().contains("WITHOUT ROWID"), "{}", table_sql());
let paths: Vec<String> = store
.db
.prepare("SELECT path FROM file_hashes")
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<rusqlite::Result<_>>()
.unwrap();
assert_eq!(paths, ["/new"]);
}
#[test]
fn stale_lock_claimed_between_listing_and_lock_is_kept() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let path = aged_key_lock(&config, &key(1), TWO_HOURS);
let now = std::time::SystemTime::now();
let removed = remove_stale_lock_file(&path, KEY_LOCK_SWEEP_GRACE, now, || {
drop(StoreLock::try_acquire(&path).unwrap().expect("claim"));
});
assert!(!removed);
assert!(path.exists());
set_age(&path, TWO_HOURS);
assert!(remove_stale_lock_file(
&path,
KEY_LOCK_SWEEP_GRACE,
now,
|| {}
));
assert!(!path.exists());
}
#[test]
fn stale_lock_replaced_under_the_sweep_keeps_the_new_file() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let path = aged_key_lock(&config, &key(1), TWO_HOURS);
let now = std::time::SystemTime::now();
let removed = remove_stale_lock_file(&path, KEY_LOCK_SWEEP_GRACE, now, || {
std::fs::remove_file(&path).unwrap();
std::fs::write(&path, b"2").unwrap();
});
assert!(!removed);
assert_eq!(std::fs::read(&path).unwrap(), b"2");
}
#[test]
fn stale_lock_held_at_lock_time_is_kept() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let path = aged_key_lock(&config, &key(1), TWO_HOURS);
let now = std::time::SystemTime::now() + TWO_HOURS + TWO_HOURS;
let mut held = None;
let removed = remove_stale_lock_file(&path, KEY_LOCK_SWEEP_GRACE, now, || {
held = StoreLock::try_acquire(&path).unwrap();
});
assert!(held.is_some());
assert!(!removed);
assert!(path.exists());
}
#[test]
fn lock_is_current_compares_identities_and_trusts_a_handle_without_one() {
let id = |ino| kache_fs::InodeId { dev: 1, ino };
let missing = || std::io::Error::from(std::io::ErrorKind::NotFound);
assert!(lock_is_current(Ok(id(7)), Ok(id(7))));
assert!(!lock_is_current(Ok(id(7)), Ok(id(8))));
assert!(!lock_is_current(Ok(id(7)), Err(missing())));
assert!(lock_is_current(Err(missing()), Ok(id(7))));
assert!(lock_is_current(Err(missing()), Err(missing())));
}
#[test]
fn lock_file_is_at_path_needs_the_same_file_at_the_path() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.lock");
let other = dir.path().join("b.lock");
std::fs::write(&path, b"1").unwrap();
std::fs::write(&other, b"1").unwrap();
let file = std::fs::File::open(&path).unwrap();
assert!(lock_file_is_at_path(&file, &path));
assert!(!lock_file_is_at_path(&file, &other));
assert!(!lock_file_is_at_path(&file, &dir.path().join("missing")));
}
#[test]
fn acquire_reopens_when_the_lock_file_was_replaced_between_open_and_lock() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("key.lock");
let mut opens = 0;
let lock = StoreLock::acquire_current(&path, StoreLock::try_lock_file, || {
opens += 1;
if opens == 1 {
std::fs::remove_file(&path).unwrap();
std::fs::write(&path, b"another claimant").unwrap();
}
})
.unwrap()
.expect("lock acquired");
assert_eq!(opens, 2);
assert_eq!(
kache_fs::handle_identity(&lock.file).unwrap(),
kache_fs::file_identity(&path).unwrap()
);
assert!(StoreLock::try_acquire(&path).unwrap().is_none());
drop(lock);
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
std::process::id().to_string()
);
assert!(StoreLock::try_acquire(&path).unwrap().is_some());
}
#[test]
fn acquire_reopens_when_the_lock_file_was_unlinked_between_open_and_lock() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("key.lock");
let mut opens = 0;
let lock = StoreLock::acquire_current(&path, StoreLock::try_lock_file, || {
opens += 1;
if opens == 1 {
std::fs::remove_file(&path).unwrap();
}
})
.unwrap()
.expect("lock acquired");
assert_eq!(opens, 2);
assert!(path.exists());
assert!(StoreLock::try_acquire(&path).unwrap().is_none());
drop(lock);
}
#[test]
fn acquire_yields_to_the_claimant_holding_the_replacement_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("key.lock");
let mut winner = None;
let mut opens = 0;
let lock = StoreLock::acquire_current(&path, StoreLock::try_lock_file, || {
opens += 1;
if opens == 1 {
std::fs::remove_file(&path).unwrap();
winner = StoreLock::try_acquire(&path).unwrap();
}
})
.unwrap();
assert!(winner.is_some());
assert!(lock.is_none(), "only one claimant may hold the key");
assert_eq!(opens, 2);
}
#[test]
fn acquire_gives_up_after_a_bounded_number_of_replacements() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("key.lock");
let mut opens = 0u32;
let result = StoreLock::acquire_current(&path, StoreLock::try_lock_file, || {
opens += 1;
std::fs::remove_file(&path).unwrap();
});
assert!(result.is_err());
assert_eq!(opens, LOCK_OPEN_ATTEMPTS);
}
#[test]
fn blocking_acquire_checks_the_path_too() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("gc.lock");
let lock = StoreLock::acquire(&path).unwrap();
assert_eq!(
kache_fs::handle_identity(&lock.file).unwrap(),
kache_fs::file_identity(&path).unwrap()
);
assert!(StoreLock::try_acquire(&path).unwrap().is_none());
}
#[test]
fn gc_lock_does_not_expire_live_holder_by_mtime() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let first = store.try_gc_lock().unwrap().expect("first GC lock");
let lock_path = config.store_dir().join("gc.lock");
let old = filetime::FileTime::from_system_time(
std::time::SystemTime::now() - std::time::Duration::from_secs(2 * 3600),
);
filetime::set_file_mtime(&lock_path, old).unwrap();
assert!(
store.try_gc_lock().unwrap().is_none(),
"an old marker file must not let a second GC steal a live lock"
);
drop(first);
assert!(store.try_gc_lock().unwrap().is_some());
}
#[test]
fn verify_restores_evicts_a_corrupted_blob() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let f = write_temp_file(dir.path(), "lib.rlib", b"the real artifact bytes");
store
.put(
"vkey",
"vcrate",
&["lib".into()],
&[],
"aarch64-apple-darwin",
"release",
&[(f, "lib.rlib".into())],
"",
"",
)
.unwrap();
let meta = store.get("vkey").unwrap().expect("entry present after put");
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 _env_lock = crate::test_support::process_state_test_lock();
{
let _verify_off = EnvVarGuard::remove("KACHE_VERIFY_RESTORES");
assert!(
store.get("vkey").unwrap().is_some(),
"without the guard a same-size corrupt blob is not caught"
);
}
let result = {
let _verify_on = EnvVarGuard::set("KACHE_VERIFY_RESTORES", "1");
store.get("vkey").unwrap()
};
assert!(
result.is_none(),
"the guard must evict a blob whose content != its address"
);
}
#[test]
fn verify_restores_mode_parses_tristate() {
assert_eq!(parse_verify_restores(None), VerifyRestores::Off);
assert_eq!(parse_verify_restores(Some("")), VerifyRestores::Off);
assert_eq!(parse_verify_restores(Some("0")), VerifyRestores::Off);
assert_eq!(parse_verify_restores(Some("off")), VerifyRestores::Off);
assert_eq!(
parse_verify_restores(Some("sampled")),
VerifyRestores::Sampled
);
assert_eq!(
parse_verify_restores(Some("SAMPLED")),
VerifyRestores::Sampled
);
assert_eq!(
parse_verify_restores(Some("always")),
VerifyRestores::Always
);
assert_eq!(parse_verify_restores(Some("1")), VerifyRestores::Always);
assert_eq!(parse_verify_restores(Some("true")), VerifyRestores::Always);
}
#[test]
fn verify_restores_sampling_cadence() {
assert!(!should_verify_this_restore(VerifyRestores::Off));
assert!(should_verify_this_restore(VerifyRestores::Always));
let window = VERIFY_SAMPLE_RATE as usize;
let verified = (0..window)
.filter(|_| should_verify_this_restore(VerifyRestores::Sampled))
.count();
assert_eq!(
verified, 1,
"exactly one in {window} consecutive sampled hits must verify"
);
}
#[test]
fn test_put_get_restore_cycle() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let file_a = write_temp_file(dir.path(), "a.rlib", b"rlib artifact content");
let file_b = write_temp_file(dir.path(), "b.dylib", b"dylib artifact content");
let file_c = write_temp_file(dir.path(), "c.rmeta", b"rmeta artifact content");
store
.put(
"multi_key",
"multi_crate",
&["lib".into(), "dylib".into()],
&["serde".into(), "tokio".into()],
"aarch64-apple-darwin",
"release",
&[
(file_a, "a.rlib".into()),
(file_b, "b.dylib".into()),
(file_c, "c.rmeta".into()),
],
"some stdout",
"some stderr",
)
.unwrap();
let meta = store.get("multi_key").unwrap().unwrap();
assert_eq!(meta.crate_name, "multi_crate");
assert_eq!(meta.crate_types, vec!["lib", "dylib"]);
assert_eq!(meta.features, vec!["serde", "tokio"]);
assert_eq!(meta.target, "aarch64-apple-darwin");
assert_eq!(meta.profile, "release");
assert_eq!(meta.stdout, "some stdout");
assert_eq!(meta.stderr, "some stderr");
assert_eq!(meta.files.len(), 3);
for cached_file in &meta.files {
let blob = store.blob_path(&cached_file.hash);
assert!(blob.exists(), "blob for {} should exist", cached_file.name);
let perms = fs::metadata(&blob).unwrap().permissions();
assert!(
perms.readonly(),
"blob for {} should be read-only",
cached_file.name
);
}
let entry_dir = store.entry_dir("multi_key");
let mut files: Vec<String> = fs::read_dir(&entry_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
files.sort();
assert_eq!(files, vec!["meta.json"]);
}
#[test]
fn test_clear_removes_all_blobs_and_tables() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
for i in 0..3 {
let file = write_temp_file(
dir.path(),
&format!("f{i}.rlib"),
format!("content {i}").as_bytes(),
);
store
.put(
&format!("key{i}"),
&format!("crate{i}"),
&["lib".into()],
&[],
"",
"dev",
&[(file, format!("lib{i}.rlib"))],
"",
"",
)
.unwrap();
}
assert_eq!(store.entry_count().unwrap(), 3);
assert!(blob_table_count(&store) >= 3);
store.clear().unwrap();
assert_eq!(store.entry_count().unwrap(), 0);
assert_eq!(blob_table_count(&store), 0);
let blobs_dir = store.blobs_dir();
if blobs_dir.exists() {
let any_content = fs::read_dir(&blobs_dir).unwrap().flatten().any(|_| true);
assert!(!any_content, "blobs dir should be empty after clear");
}
}
#[test]
fn test_migration_of_legacy_entry() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = config.store_dir().join("legacy_key");
fs::create_dir_all(&entry_dir).unwrap();
let content_a = b"legacy artifact A";
let content_b = b"legacy artifact B";
fs::write(entry_dir.join("a.rlib"), content_a).unwrap();
fs::write(entry_dir.join("b.dylib"), content_b).unwrap();
let hash_a = crate::file_hash::hash_file(&entry_dir.join("a.rlib")).unwrap();
let hash_b = crate::file_hash::hash_file(&entry_dir.join("b.dylib")).unwrap();
let meta = EntryMeta {
cache_key: "legacy_key".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "legacy_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![
CachedFile {
name: "a.rlib".to_string(),
size: content_a.len() as u64,
hash: hash_a.clone(),
executable: false,
},
CachedFile {
name: "b.dylib".to_string(),
size: content_b.len() as u64,
hash: hash_b.clone(),
executable: false,
},
],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed) VALUES ('legacy_key', 'legacy_crate', ?1, 1)",
params![(content_a.len() + content_b.len()) as i64],
)
.unwrap();
assert!(store.migrate_entry_to_blobs(&meta).unwrap());
assert!(
!entry_dir.join("a.rlib").exists(),
"a.rlib should be moved to blob store"
);
assert!(
!entry_dir.join("b.dylib").exists(),
"b.dylib should be moved to blob store"
);
assert!(entry_dir.join("meta.json").exists());
let blob_a = store.blob_path(&hash_a);
let blob_b = store.blob_path(&hash_b);
assert!(blob_a.exists(), "blob for a.rlib should exist");
assert!(blob_b.exists(), "blob for b.dylib should exist");
assert!(fs::metadata(&blob_a).unwrap().permissions().readonly());
assert!(fs::metadata(&blob_b).unwrap().permissions().readonly());
assert_eq!(blob_refcount(&store, &hash_a), Some(1));
assert_eq!(blob_refcount(&store, &hash_b), Some(1));
assert_eq!(store.backfill_entry_blobs().unwrap(), 1);
assert_blob_refs_match_mappings(&store);
let files: Vec<String> = fs::read_dir(&entry_dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
assert_eq!(files, vec!["meta.json"]);
}
#[test]
fn migrate_entry_to_blobs_bumps_refcount_when_insert_loses_race() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let entry_dir = store.entry_dir("legacy_race");
fs::create_dir_all(&entry_dir).unwrap();
let artifact = entry_dir.join("lib.rlib");
fs::write(&artifact, b"legacy race artifact").unwrap();
let hash = crate::file_hash::hash_file(&artifact).unwrap();
let size = fs::metadata(&artifact).unwrap().len();
let meta = EntryMeta {
cache_key: "legacy_race".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "legacy_crate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size,
hash: hash.clone(),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: String::new(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
store
.db
.execute(
&format!(
"CREATE TEMP TRIGGER seed_blob_before_insert \
BEFORE INSERT ON blobs \
WHEN NEW.hash = '{hash}' \
BEGIN \
INSERT OR IGNORE INTO blobs (hash, size, refcount) \
VALUES (NEW.hash, NEW.size, 41); \
END"
),
[],
)
.unwrap();
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, size, committed)
VALUES ('legacy_race', 'legacy_crate', ?1, 1)",
params![size as i64],
)
.unwrap();
assert!(store.migrate_entry_to_blobs(&meta).unwrap());
assert_eq!(blob_refcount(&store, &hash), Some(42));
assert!(store.blob_path(&hash).is_file());
assert!(!artifact.exists());
}
#[test]
fn test_eviction_with_shared_blobs() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let shared_content = b"shared between 1 and 2";
let unique3_content = b"unique to entry 3 only";
let f = write_temp_file(dir.path(), "shared.rlib", shared_content);
store
.put(
"e1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[(f, "shared.rlib".into())],
"",
"",
)
.unwrap();
let f = write_temp_file(dir.path(), "shared.rlib", shared_content);
store
.put(
"e2",
"c2",
&["lib".into()],
&[],
"",
"dev",
&[(f, "shared.rlib".into())],
"",
"",
)
.unwrap();
let f = write_temp_file(dir.path(), "unique3.rlib", unique3_content);
store
.put(
"e3",
"c3",
&["lib".into()],
&[],
"",
"dev",
&[(f, "unique3.rlib".into())],
"",
"",
)
.unwrap();
let meta1 = read_meta(&store, "e1");
let meta3 = read_meta(&store, "e3");
let shared_hash = &meta1.files[0].hash;
let unique3_hash = &meta3.files[0].hash;
assert_eq!(blob_refcount(&store, shared_hash), Some(2));
assert_blob_refs_match_mappings(&store);
assert_eq!(blob_refcount(&store, unique3_hash), Some(1));
store.remove_entry("e1").unwrap();
assert_eq!(blob_refcount(&store, shared_hash), Some(1));
assert!(store.blob_path(shared_hash).exists());
assert!(store.blob_path(unique3_hash).exists());
assert_eq!(blob_refcount(&store, unique3_hash), Some(1));
store.remove_entry("e2").unwrap();
assert!(!store.blob_path(shared_hash).exists());
assert_eq!(blob_refcount(&store, shared_hash), None);
assert!(store.blob_path(unique3_hash).exists());
assert_eq!(blob_refcount(&store, unique3_hash), Some(1));
let meta = store.get("e3").unwrap();
assert!(meta.is_some());
}
#[test]
fn test_blob_stats_with_known_overlap() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let shared_content = b"AAAA"; let unique_content = b"BBBBBBBB";
let f_shared = write_temp_file(dir.path(), "shared.rlib", shared_content);
let f_unique = write_temp_file(dir.path(), "unique.rlib", unique_content);
store
.put(
"stats1",
"c1",
&["lib".into()],
&[],
"",
"dev",
&[
(f_shared, "shared.rlib".into()),
(f_unique, "unique.rlib".into()),
],
"",
"",
)
.unwrap();
let f_shared = write_temp_file(dir.path(), "shared.rlib", shared_content);
store
.put(
"stats2",
"c2",
&["lib".into()],
&[],
"",
"dev",
&[(f_shared, "shared.rlib".into())],
"",
"",
)
.unwrap();
let stats = store.blob_stats().unwrap();
assert_eq!(stats.total_blobs, 2, "should have 2 unique blobs");
assert_eq!(
stats.total_blob_size, 12,
"physical size should be 12 bytes"
);
assert_eq!(
stats.total_logical_size, 16,
"logical size should be 16 bytes"
);
assert_eq!(stats.savings, 4, "savings should be 4 bytes");
}
#[test]
fn content_hash_golden_pins_serialization() {
let cf = |name: &str, size: u64, hash: &str, executable: bool| CachedFile {
name: name.to_string(),
size,
hash: hash.to_string(),
executable,
};
let files = vec![
cf("foo", 4096, "cccccccccccccccccccccccccccccccc", true),
cf(
"libfoo.rlib",
1024,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
false,
),
cf(
"libfoo.rmeta",
256,
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
false,
),
];
assert_eq!(
compute_content_hash(&files),
"2dd3b89296eb2d5469d11aa00b312cee8734923698b97890f43ea6a8b9a37585",
);
}
#[test]
fn emit_kinds_derived_from_files() {
let cf = |name: &str| CachedFile {
name: name.to_string(),
size: 1,
hash: "h".to_string(),
executable: false,
};
let kinds = emit_kinds_for_files::<TestPolicy>(&[
cf("libfoo.rlib"),
cf("libfoo.rmeta"),
cf("foo.d"),
cf("foo.dSYM"), ]);
assert_eq!(kinds, vec!["dep-info", "link", "metadata"]);
}
#[test]
fn wasm_link_output_satisfies_the_emit_coverage_gate() {
let files = vec![
CachedFile {
name: "rococo_runtime.wasm".into(),
size: 4,
hash: "h1".into(),
executable: false,
},
CachedFile {
name: "rococo_runtime.d".into(),
size: 4,
hash: "h2".into(),
executable: false,
},
];
let kinds = emit_kinds_for_files::<TestPolicy>(&files);
assert_eq!(
kinds,
vec!["dep-info".to_string(), "link".to_string()],
"a .wasm module is the link product of a wasm32 target"
);
let meta = EntryMeta {
cache_key: "k".into(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "rococo_runtime".into(),
crate_types: vec!["cdylib".into()],
files,
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: "wasm32-unknown-unknown".into(),
profile: "release".into(),
compile_time_ms: 62_000,
emit_kinds: kinds,
};
assert!(
meta.covers_requested_emit(&["link".to_string(), "dep-info".to_string()]),
"the entry must satisfy the --emit it was built for"
);
}
#[test]
fn test_put_stores_content_hash() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(tmp.path());
let store = Store::open(&config).unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
let file1 = dir.join("lib.rlib");
std::fs::write(&file1, b"artifact-content-1234").unwrap();
store
.put(
"key_ch_1",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file1, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
let ch: String = store
.db
.query_row(
"SELECT content_hash FROM entries WHERE cache_key = 'key_ch_1'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(
ch.len(),
64,
"content_hash should be full blake3 hex (64 chars)"
);
}
#[test]
fn test_import_downloaded_entry_stores_content_hash() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(tmp.path());
let store = Store::open(&config).unwrap();
let entry_dir = store.entry_dir("dl_ch_test");
std::fs::create_dir_all(&entry_dir).unwrap();
let artifact = entry_dir.join("lib.rlib");
std::fs::write(&artifact, b"downloaded-artifact-data").unwrap();
let hash = crate::file_hash::hash_file(&artifact).unwrap();
let size = std::fs::metadata(&artifact).unwrap().len();
let meta = EntryMeta {
cache_key: "dl_ch_test".to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "dlcrate".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: "lib.rlib".to_string(),
size,
hash,
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: "x86_64-unknown-linux-gnu".to_string(),
profile: "dev".to_string(),
compile_time_ms: 0,
emit_kinds: Vec::new(),
};
std::fs::write(
entry_dir.join("meta.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
store.import_downloaded_entry("dl_ch_test").unwrap();
let ch: String = store
.db
.query_row(
"SELECT content_hash FROM entries WHERE cache_key = 'dl_ch_test'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(ch.len(), 64);
}
#[test]
fn verified_batch_import_is_atomic_and_registers_every_entry() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(tmp.path());
let store = Store::open(&config).unwrap();
let keys = [
blake3::hash(b"packed-batch-a").to_hex().to_string(),
blake3::hash(b"packed-batch-b").to_hex().to_string(),
];
let mut verified = Vec::new();
for (index, key) in keys.iter().enumerate() {
let entry_dir = store.entry_dir(key);
std::fs::create_dir_all(&entry_dir).unwrap();
let artifact = entry_dir.join(format!("lib{index}.rlib"));
let contents = format!("verified packed artifact {index}");
std::fs::write(&artifact, contents.as_bytes()).unwrap();
let meta = EntryMeta {
cache_key: key.clone(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: format!("crate{index}"),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: format!("lib{index}.rlib"),
size: contents.len() as u64,
hash: blake3::hash(contents.as_bytes()).to_hex().to_string(),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: vec![],
target: "x86_64-unknown-linux-gnu".to_string(),
profile: "dev".to_string(),
compile_time_ms: 1,
emit_kinds: Vec::new(),
};
std::fs::write(
entry_dir.join("meta.json"),
serde_json::to_vec_pretty(&meta).unwrap(),
)
.unwrap();
verified.push(VerifiedRestoredEntry {
cache_key: key.clone(),
meta,
});
}
let original_size = verified[1].meta.files[0].size;
verified[1].meta.files[0].size += 1;
assert!(store.import_verified_restored_entries(&verified).is_err());
let rows: i64 = store
.db
.query_row("SELECT COUNT(*) FROM entries", [], |row| row.get(0))
.unwrap();
assert_eq!(rows, 0, "a failed preflight must register no batch rows");
verified[1].meta.files[0].size = original_size;
store
.db
.execute(
"INSERT INTO entries (cache_key, crate_name, crate_type, profile, num_features, size, content_hash, compile_time_ms, key_schema, committed) VALUES (?1, 'stale', 'lib', 'dev', 0, 0, 'stale', 0, ?2, 0)",
params![keys[0], kache_format::CACHE_KEY_VERSION],
)
.unwrap();
let imported = store.import_verified_restored_entries(&verified).unwrap();
assert_eq!(imported, 2);
let rows: i64 = store
.db
.query_row(
"SELECT COUNT(*) FROM entries WHERE committed = 1",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(rows, 2);
let stamped: i64 = store
.db
.query_row(
"SELECT COUNT(*) FROM entries WHERE imported_at IS NOT NULL",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(stamped, 2, "a prefetched batch is an import too (#1008)");
for key in keys {
assert!(store.get(&key).unwrap().is_some());
}
let refcounts: Vec<i64> = store
.db
.prepare("SELECT refcount FROM blobs ORDER BY hash")
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<rusqlite::Result<_>>()
.unwrap();
assert_eq!(refcounts, vec![1, 1]);
}
fn write_verified_fixture(
store: &Store,
key: &str,
meta_key: &str,
artifact_name: &str,
hash_override: Option<String>,
) -> VerifiedRestoredEntry {
let contents = b"verified fixture artifact";
let entry_dir = store.entry_dir(key);
let artifact = entry_dir.join(artifact_name);
std::fs::create_dir_all(artifact.parent().unwrap()).unwrap();
std::fs::write(&artifact, contents).unwrap();
let meta = EntryMeta {
cache_key: meta_key.to_string(),
key_schema: kache_format::CACHE_KEY_VERSION,
crate_name: "fixture".to_string(),
crate_types: vec!["lib".to_string()],
files: vec![CachedFile {
name: artifact_name.to_string(),
size: contents.len() as u64,
hash: hash_override.unwrap_or_else(|| blake3::hash(contents).to_hex().to_string()),
executable: false,
}],
stdout: String::new(),
stderr: String::new(),
features: Vec::new(),
target: "x86_64-unknown-linux-gnu".to_string(),
profile: "dev".to_string(),
compile_time_ms: 1,
emit_kinds: Vec::new(),
};
std::fs::write(
entry_dir.join("meta.json"),
serde_json::to_vec_pretty(&meta).unwrap(),
)
.unwrap();
VerifiedRestoredEntry {
cache_key: key.to_string(),
meta,
}
}
#[test]
fn verified_batch_import_checks_each_cache_key_binding_independently() {
let tmp = tempfile::tempdir().unwrap();
let store = Store::open(test_config(tmp.path())).unwrap();
let invalid = write_verified_fixture(&store, "invalid", "invalid", "lib.rlib", None);
assert!(store.import_verified_restored_entries(&[invalid]).is_err());
let key = blake3::hash(b"valid-outer-key").to_hex().to_string();
let other = blake3::hash(b"different-meta-key").to_hex().to_string();
let mismatched = write_verified_fixture(&store, &key, &other, "lib.rlib", None);
assert!(
store
.import_verified_restored_entries(&[mismatched])
.is_err()
);
}
#[test]
fn verified_batch_import_checks_each_artifact_field_independently() {
for (label, name, hash_override) in [
("unsafe-name", "nested/lib.rlib", None),
("invalid-hash", "lib.rlib", Some("g".repeat(64))),
] {
let tmp = tempfile::tempdir().unwrap();
let store = Store::open(test_config(tmp.path())).unwrap();
let key = blake3::hash(label.as_bytes()).to_hex().to_string();
let entry = write_verified_fixture(&store, &key, &key, name, hash_override);
assert!(
store.import_verified_restored_entries(&[entry]).is_err(),
"{label} must be rejected independently"
);
}
let tmp = tempfile::tempdir().unwrap();
let store = Store::open(test_config(tmp.path())).unwrap();
let key = blake3::hash(b"duplicate-artifact").to_hex().to_string();
let mut entry = write_verified_fixture(&store, &key, &key, "lib.rlib", None);
entry.meta.files.push(entry.meta.files[0].clone());
std::fs::write(
store.entry_dir(&key).join("meta.json"),
serde_json::to_vec_pretty(&entry.meta).unwrap(),
)
.unwrap();
assert!(store.import_verified_restored_entries(&[entry]).is_err());
}
#[test]
fn verified_batch_import_never_rewrites_an_existing_content_addressed_blob() {
let tmp = tempfile::tempdir().unwrap();
let store = Store::open(test_config(tmp.path())).unwrap();
let key = blake3::hash(b"existing-immutable-blob")
.to_hex()
.to_string();
let entry = write_verified_fixture(&store, &key, &key, "lib.rlib", None);
let blob = store.blob_path(&entry.meta.files[0].hash);
std::fs::create_dir_all(blob.parent().unwrap()).unwrap();
std::fs::write(&blob, b"pre-existing immutable blob").unwrap();
assert_eq!(store.import_verified_restored_entries(&[entry]).unwrap(), 1);
assert_eq!(std::fs::read(blob).unwrap(), b"pre-existing immutable blob");
}
#[test]
fn verified_blob_install_reports_a_vanished_source_before_rename() {
let tmp = tempfile::tempdir().unwrap();
let store = Store::open(test_config(tmp.path())).unwrap();
let file = CachedFile {
name: "lib.rlib".to_string(),
size: 7,
hash: blake3::hash(b"missing verified artifact")
.to_hex()
.to_string(),
executable: false,
};
let error = store
.install_verified_blob(&tmp.path().join("missing-entry"), &file)
.expect_err("a vanished verified source must fail")
.to_string();
assert!(
error.contains("verified restored blob vanished during batch import"),
"unexpected error: {error}"
);
}
#[test]
fn test_list_entries_includes_content_hash() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(tmp.path());
let store = Store::open(&config).unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
let file1 = dir.join("lib.rlib");
std::fs::write(&file1, b"list-test-content").unwrap();
store
.put(
"list_ch_1",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file1, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
let entries = store.list_entries("name").unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].content_hash.is_some());
assert_eq!(entries[0].content_hash.as_ref().unwrap().len(), 64);
}
#[test]
fn evict_duplicate_entries_spares_a_pair_sharing_one_blob() {
let tmp = tempfile::tempdir().unwrap();
let mut config = test_config(tmp.path());
config.max_size = 1;
let store = Store::open(&config).unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
let file1 = dir.join("lib.rlib");
std::fs::write(&file1, b"same-content-bytes").unwrap();
store
.put(
"dup_key_1",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file1.clone(), "lib.rlib".to_string())],
"",
"",
)
.unwrap();
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = 'dup_key_1'",
[],
)
.unwrap();
store
.put(
"dup_key_2",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file1, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
assert_eq!(store.entry_count().unwrap(), 2);
let stats = store.evict_duplicate_entries().unwrap();
assert_eq!(stats.entries_evicted, 0);
assert_eq!(store.entry_count().unwrap(), 2);
assert!(store.contains("dup_key_1") && store.contains("dup_key_2"));
}
#[test]
fn evict_duplicate_entries_skips_the_scan_under_budget() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(tmp.path());
let store = Store::open(&config).unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("lib.rlib");
std::fs::write(&file, b"tiny-shared-content").unwrap();
store
.put(
"under_budget_1",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file.clone(), "lib.rlib".to_string())],
"",
"",
)
.unwrap();
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') \
WHERE cache_key = 'under_budget_1'",
[],
)
.unwrap();
store
.put(
"under_budget_2",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
let stats = store.evict_duplicate_entries().unwrap();
assert!(stats.skipped);
assert_eq!(stats.entries_evicted, 0);
assert_eq!(store.entry_count().unwrap(), 2);
}
#[test]
fn evict_duplicate_entries_fails_closed_for_unmapped_legacy_victim() {
let tmp = tempfile::tempdir().unwrap();
let mut config = test_config(tmp.path());
config.max_size = 1;
let store = Store::open(&config).unwrap();
let file = tmp.path().join("legacy.rlib");
std::fs::write(&file, b"shared-legacy-content").unwrap();
for key in ["legacy_old", "legacy_new"] {
store
.put(
key,
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file.clone(), "lib.rlib".to_string())],
"",
"",
)
.unwrap();
}
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') \
WHERE cache_key = 'legacy_old'",
[],
)
.unwrap();
store
.db
.execute("DELETE FROM entry_blobs WHERE cache_key = 'legacy_old'", [])
.unwrap();
let stats = store.evict_duplicate_entries().unwrap();
assert_eq!(stats.entries_evicted, 0);
assert!(store.contains("legacy_old"));
assert!(store.contains("legacy_new"));
}
#[test]
fn evict_duplicate_entries_stops_at_the_physical_target() {
let tmp = tempfile::tempdir().unwrap();
let mut config = test_config(tmp.path());
config.max_size = 500; let store = Store::open(&config).unwrap();
for group in 0..3 {
let old_key = format!("budget_old_{group}");
let new_key = format!("budget_new_{group}");
let old_file = tmp.path().join(format!("old-{group}.rlib"));
let new_file = tmp.path().join(format!("new-{group}.rlib"));
std::fs::write(&old_file, vec![group as u8 + 1; 100]).unwrap();
std::fs::write(&new_file, vec![group as u8 + 11; 100]).unwrap();
store
.put(
&old_key,
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(old_file.clone(), "lib.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&old_file);
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', ?1) \
WHERE cache_key = ?2",
params![format!("-{} hours", 3 - group), old_key],
)
.unwrap();
let group_hash: String = store
.db
.query_row(
"SELECT content_hash FROM entries WHERE cache_key = ?1",
params![old_key],
|row| row.get(0),
)
.unwrap();
store
.put(
&new_key,
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(new_file.clone(), "lib.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&new_file);
store
.db
.execute(
"UPDATE entries SET content_hash = ?1 WHERE cache_key = ?2",
params![group_hash, new_key],
)
.unwrap();
}
assert_eq!(store.physical_size().unwrap(), 600);
let stats = store.evict_duplicate_entries().unwrap();
assert_eq!(stats.entries_evicted, 2);
assert_eq!(stats.bytes_freed, 200);
assert_eq!(store.physical_size().unwrap(), 400);
assert!(!store.contains("budget_old_0"));
assert!(!store.contains("budget_old_1"));
assert!(
store.contains("budget_old_2"),
"bounded duplicate GC must retain the least-stale eligible victim"
);
}
#[test]
fn evict_duplicate_entries_skips_victim_with_corrupt_meta() {
let tmp = tempfile::tempdir().unwrap();
let mut config = test_config(tmp.path());
config.max_size = 1;
let store = Store::open(&config).unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("lib.rlib");
rewrite_source(&file, b"same-content-for-corrupt-dedup");
store
.put(
"dup_corrupt_old",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file.clone(), "lib.rlib".to_string())],
"",
"",
)
.unwrap();
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') \
WHERE cache_key = 'dup_corrupt_old'",
[],
)
.unwrap();
let old_content_hash: String = store
.db
.query_row(
"SELECT content_hash FROM entries WHERE cache_key = 'dup_corrupt_old'",
[],
|row| row.get(0),
)
.unwrap();
rewrite_source(&file, b"different-content-for-corrupt-dedup");
store
.put(
"dup_corrupt_new",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
store
.db
.execute(
"UPDATE entries SET content_hash = ?1 WHERE cache_key = 'dup_corrupt_new'",
params![old_content_hash],
)
.unwrap();
std::fs::write(
store.entry_dir("dup_corrupt_old").join("meta.json"),
b"{not json",
)
.unwrap();
let stats = store.evict_duplicate_entries().unwrap();
assert_eq!(stats.entries_evicted, 0, "corrupt victim is skipped");
assert!(store.contains("dup_corrupt_old"));
assert!(store.contains("dup_corrupt_new"));
}
#[test]
fn test_backfill_content_hashes() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(tmp.path());
let store = Store::open(&config).unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
let file1 = dir.join("lib.rlib");
std::fs::write(&file1, b"backfill-content").unwrap();
store
.put(
"bf_key_1",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file1, "lib.rlib".to_string())],
"",
"",
)
.unwrap();
store
.db
.execute(
"UPDATE entries SET content_hash = NULL WHERE cache_key = 'bf_key_1'",
[],
)
.unwrap();
let backfilled = store.backfill_content_hashes().unwrap();
assert_eq!(backfilled, 1);
let ch: String = store
.db
.query_row(
"SELECT content_hash FROM entries WHERE cache_key = 'bf_key_1'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(ch.len(), 64);
}
#[test]
fn test_content_hash_column_exists() {
let tmp = tempfile::tempdir().unwrap();
let config = test_config(tmp.path());
let store = Store::open(&config).unwrap();
let result: Result<Option<String>, _> =
store
.db
.query_row("SELECT content_hash FROM entries LIMIT 1", [], |row| {
row.get(0)
});
assert!(result.is_ok() || result.unwrap_err().to_string().contains("no rows"));
}
#[test]
fn test_content_hash_full_dedup_lifecycle() {
let tmp = tempfile::tempdir().unwrap();
let mut config = test_config(tmp.path());
config.max_size = 1;
let store = Store::open(&config).unwrap();
let dir = tmp.path().join("src");
std::fs::create_dir_all(&dir).unwrap();
let file_a = dir.join("a.rlib");
std::fs::write(&file_a, b"shared-content").unwrap();
let file_b = dir.join("b.rlib");
std::fs::write(&file_b, b"different-content").unwrap();
store
.put(
"ch_lc_1",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file_a.clone(), "a.rlib".to_string())],
"",
"",
)
.unwrap();
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = 'ch_lc_1'",
[],
)
.unwrap();
store
.put(
"ch_lc_2",
"mycrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file_a, "a.rlib".to_string())],
"",
"",
)
.unwrap();
store
.put(
"ch_lc_3",
"othercrate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(file_b, "b.rlib".to_string())],
"",
"",
)
.unwrap();
let entries = store.list_entries("name").unwrap();
assert_eq!(entries.len(), 3);
let ch1 = entries
.iter()
.find(|e| e.cache_key == "ch_lc_1")
.unwrap()
.content_hash
.as_ref()
.unwrap();
let ch2 = entries
.iter()
.find(|e| e.cache_key == "ch_lc_2")
.unwrap()
.content_hash
.as_ref()
.unwrap();
let ch3 = entries
.iter()
.find(|e| e.cache_key == "ch_lc_3")
.unwrap()
.content_hash
.as_ref()
.unwrap();
assert_eq!(ch1, ch2, "identical content should have same hash");
assert_ne!(ch1, ch3, "different content should have different hash");
let stats = store.evict_duplicate_entries().unwrap();
assert_eq!(stats.entries_evicted, 0);
assert_eq!(store.entry_count().unwrap(), 3);
assert!(store.contains("ch_lc_1"));
assert!(store.contains("ch_lc_2"));
assert!(store.contains("ch_lc_3"));
}
#[test]
fn store_copy_reason_cross_device_maps_from_exdev() {
assert_eq!(
StoreCopyReason::from_io_kind(std::io::ErrorKind::CrossesDevices),
StoreCopyReason::CrossDevice,
);
}
#[test]
fn store_copy_reason_permission_maps_from_eperm() {
assert_eq!(
StoreCopyReason::from_io_kind(std::io::ErrorKind::PermissionDenied),
StoreCopyReason::Permission,
);
}
#[test]
fn store_copy_reason_other_maps_from_unexpected_errno() {
assert_eq!(
StoreCopyReason::from_io_kind(std::io::ErrorKind::AlreadyExists),
StoreCopyReason::Other,
);
}
#[test]
fn record_store_copy_reason_cross_device_increments() {
let before = crate::opcounts::store_copy_cross_device_bytes();
record_store_copy_reason(StoreCopyReason::CrossDevice, 11);
assert!(crate::opcounts::store_copy_cross_device_bytes() >= before + 11);
}
#[test]
fn record_store_copy_reason_permission_increments() {
let before = crate::opcounts::store_copy_permission_bytes();
record_store_copy_reason(StoreCopyReason::Permission, 13);
assert!(crate::opcounts::store_copy_permission_bytes() >= before + 13);
}
#[test]
fn record_store_copy_reason_ineligible_increments() {
let before = crate::opcounts::store_copy_ineligible_bytes();
record_store_copy_reason(StoreCopyReason::Ineligible, 17);
assert!(crate::opcounts::store_copy_ineligible_bytes() >= before + 17);
}
#[test]
fn record_store_copy_reason_other_increments() {
let before = crate::opcounts::store_copy_other_bytes();
record_store_copy_reason(StoreCopyReason::Other, 19);
assert!(crate::opcounts::store_copy_other_bytes() >= before + 19);
}
#[cfg(unix)]
#[test]
fn rlib_put_hardlinks_on_same_device() {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("libtest.rlib");
fs::write(&source, b"rlib-bytes-for-hardlink").unwrap();
let bytes = fs::metadata(&source).unwrap().len();
let before = crate::opcounts::store_hardlinked_bytes();
let _force = ForceStoreHardlink::enable();
store
.put(
"hardlink_835_key",
"hardlink_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(source.clone(), "libtest.rlib".to_string())],
"out",
"err",
)
.unwrap();
assert!(
crate::opcounts::store_hardlinked_bytes() >= before + bytes,
"same-device .rlib put must record hardlinked bytes"
);
let hash = crate::file_hash::hash_file(&source).unwrap();
let blob = store.blob_path(&hash);
assert_eq!(
fs::metadata(&blob).unwrap().nlink(),
2,
"hardlinked blob must share its inode with the build output"
);
}
#[test]
fn injected_cross_device_ingest_records_cross_device_reason() {
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("libx.rlib");
fs::write(&source, b"cross-device-bytes").unwrap();
let bytes = fs::metadata(&source).unwrap().len();
let before = crate::opcounts::store_copy_cross_device_bytes();
let _force = ForceStoreHardlink::enable();
let _inject = InjectStoreHardlinkError::enable(std::io::ErrorKind::CrossesDevices);
let (staged, ingest) = store.stage_blob_from_source(&source, true).unwrap();
assert!(
matches!(ingest, StoreIngest::Copy(StoreCopyReason::CrossDevice)),
"EXDEV injection must stage as Copy(CrossDevice), got {ingest:?}"
);
let hash = crate::file_hash::hash_file(&staged).unwrap();
store
.publish_staged_blob(&staged, ingest, &hash, bytes)
.unwrap();
assert!(
crate::opcounts::store_copy_cross_device_bytes() >= before + bytes,
"EXDEV injection must record the cross-device reason"
);
}
#[test]
fn injected_permission_ingest_records_permission_reason() {
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("libp.rlib");
fs::write(&source, b"permission-bytes").unwrap();
let bytes = fs::metadata(&source).unwrap().len();
let before = crate::opcounts::store_copy_permission_bytes();
let _force = ForceStoreHardlink::enable();
let _inject = InjectStoreHardlinkError::enable(std::io::ErrorKind::PermissionDenied);
let (staged, ingest) = store.stage_blob_from_source(&source, true).unwrap();
assert!(
matches!(ingest, StoreIngest::Copy(StoreCopyReason::Permission)),
"EPERM injection must stage as Copy(Permission), got {ingest:?}"
);
let hash = crate::file_hash::hash_file(&staged).unwrap();
store
.publish_staged_blob(&staged, ingest, &hash, bytes)
.unwrap();
assert!(
crate::opcounts::store_copy_permission_bytes() >= before + bytes,
"EPERM injection must record the permission reason"
);
}
#[test]
fn injected_other_ingest_records_other_reason() {
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("libo.rlib");
fs::write(&source, b"other-bytes").unwrap();
let bytes = fs::metadata(&source).unwrap().len();
let before = crate::opcounts::store_copy_other_bytes();
let _force = ForceStoreHardlink::enable();
let _inject = InjectStoreHardlinkError::enable(std::io::ErrorKind::AlreadyExists);
let (staged, ingest) = store.stage_blob_from_source(&source, true).unwrap();
assert!(
matches!(ingest, StoreIngest::Copy(StoreCopyReason::Other)),
"other errno injection must stage as Copy(Other), got {ingest:?}"
);
let hash = crate::file_hash::hash_file(&staged).unwrap();
store
.publish_staged_blob(&staged, ingest, &hash, bytes)
.unwrap();
assert!(
crate::opcounts::store_copy_other_bytes() >= before + bytes,
"other errno injection must record the other reason"
);
}
#[test]
fn injected_cross_device_ingest_advises_but_other_errors_do_not() {
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let base = std::env::temp_dir().join(format!(
"kache-store-cross-volume-test-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
crate::link::set_cow_warn_marker(base.join("warn"));
crate::link::set_storage_layout_advice(true);
let marker_written = || std::fs::read_dir(&base).unwrap().next().is_some();
let source = dir.path().join("libv.rlib");
fs::write(&source, b"cross-volume-bytes").unwrap();
crate::link::set_storage_layout_advice(false);
let _force = ForceStoreHardlink::enable();
let _inject = InjectStoreHardlinkError::enable(std::io::ErrorKind::CrossesDevices);
let (_, ingest) = store.stage_blob_from_source(&source, true).unwrap();
assert!(
matches!(ingest, StoreIngest::Copy(StoreCopyReason::CrossDevice)),
"muting advice must not change the copy reason, got {ingest:?}"
);
assert!(
!marker_written(),
"a muted advisory must not write a marker"
);
crate::link::set_storage_layout_advice(true);
let _force = ForceStoreHardlink::enable();
let _inject = InjectStoreHardlinkError::enable(std::io::ErrorKind::AlreadyExists);
let (_, ingest) = store.stage_blob_from_source(&source, true).unwrap();
assert!(
matches!(ingest, StoreIngest::Copy(StoreCopyReason::Other)),
"expected Copy(Other), got {ingest:?}"
);
assert!(
!marker_written(),
"a non-cross-device ingest failure must not advise"
);
let _ = crate::markers::take_emitted();
crate::link::set_layout_advice_to_log(true);
let _inject = InjectStoreHardlinkError::enable(std::io::ErrorKind::CrossesDevices);
let (_, ingest) = store.stage_blob_from_source(&source, true).unwrap();
crate::link::set_layout_advice_to_log(false);
let emitted = crate::markers::take_emitted();
assert_eq!(emitted.len(), 1, "expected one advisory, got {emitted:?}");
assert_eq!(emitted[0].0, crate::markers::WarnSink::Log);
assert!(emitted[0].1.contains("EXDEV"), "{emitted:?}");
assert!(
matches!(ingest, StoreIngest::Copy(StoreCopyReason::CrossDevice)),
"expected Copy(CrossDevice), got {ingest:?}"
);
assert!(
marker_written(),
"a cross-device ingest fallback must advise"
);
crate::link::set_storage_layout_advice(true);
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn ineligible_ingest_records_ineligible_reason() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let source = dir.path().join("out.rlib");
fs::write(&source, b"ineligible-bytes").unwrap();
let bytes = fs::metadata(&source).unwrap().len();
let before = crate::opcounts::store_copy_ineligible_bytes();
let _force = ForceStoreHardlink::enable();
let (staged, ingest) = store.stage_blob_from_source(&source, false).unwrap();
assert!(
matches!(ingest, StoreIngest::Copy(StoreCopyReason::Ineligible)),
"policy refusal must stage as Copy(Ineligible), got {ingest:?}"
);
let hash = crate::file_hash::hash_file(&staged).unwrap();
store
.publish_staged_blob(&staged, ingest, &hash, bytes)
.unwrap();
assert!(
crate::opcounts::store_copy_ineligible_bytes() >= before + bytes,
"policy refusal must record the ineligible reason"
);
}
#[test]
fn eviction_write_pacer_pauses_once_per_full_slice() {
let slice = Duration::from_millis(50);
let pause = Duration::from_millis(150);
let mut pacer = EvictionWritePacer::new(slice, pause);
assert_eq!(pacer.after_write(Duration::from_millis(30)), None);
assert_eq!(
pacer.after_write(Duration::from_millis(20)),
Some(pause),
"a slice exactly used up pauses"
);
assert_eq!(
pacer.after_write(Duration::from_millis(49)),
None,
"the pause starts a fresh slice"
);
assert_eq!(pacer.after_contention(), pause);
assert_eq!(
pacer.after_write(Duration::from_millis(49)),
None,
"contention starts a fresh slice too"
);
assert_eq!(pacer.after_write(Duration::from_millis(1)), Some(pause));
}
fn put_evictable_entries(store: &Store, dir: &Path, n: usize) {
for i in 0..n {
let src = dir.join(format!("evictable-{i}.rlib"));
std::fs::write(&src, format!("evictable payload {i}").repeat(8)).unwrap();
store
.put(
&format!("{i:064x}"),
"c",
&["lib".into()],
&[],
"",
"dev",
&[(src.clone(), "lib.rlib".into())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&src);
}
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour')",
[],
)
.unwrap();
}
#[test]
fn eviction_stands_off_after_losing_the_write_lock() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.deferred_durability = true;
let store = Store::open(&config).unwrap();
put_evictable_entries(&store, dir.path(), 3);
let build = Store::open(&config).unwrap();
build.db.execute_batch("BEGIN IMMEDIATE").unwrap();
let mut gc_config = config.clone();
gc_config.max_size = 1;
let gc = Store::open(&gc_config).unwrap();
gc.db.busy_timeout(Duration::from_millis(50)).unwrap();
let started = std::time::Instant::now();
let stats = gc.evict().unwrap();
let elapsed = started.elapsed();
build.db.execute_batch("ROLLBACK").unwrap();
assert_eq!(stats.entries_locked, 3, "{stats:?}");
assert!(
elapsed >= EVICTION_WRITE_PAUSE * 3,
"one pause per lost write lock, swept in {elapsed:?}"
);
}
#[test]
fn eviction_pauses_between_write_slices() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.deferred_durability = true;
let store = Store::open(&config).unwrap();
put_evictable_entries(&store, dir.path(), 500);
let mut gc_config = config.clone();
gc_config.max_size = 1;
let mut gc = Store::open(&gc_config).unwrap();
let slice = Duration::from_millis(5);
let pause = Duration::from_millis(20);
gc.eviction_pacing = (slice, pause);
let started = std::time::Instant::now();
let stats = gc.evict().unwrap();
let elapsed = started.elapsed();
assert_eq!(stats.entries_evicted, 500, "{stats:?}");
let writing = Duration::from_millis(stats.evict_write_ms);
assert!(
writing >= slice,
"fixture too small to use up a slice: {stats:?}"
);
assert!(
elapsed >= writing + pause,
"{writing:?} of writes must include a pause, swept in {elapsed:?}"
);
}
#[test]
fn eviction_skips_entries_it_found_held_without_taking_the_lock() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.deferred_durability = true;
let store = Store::open(&config).unwrap();
put_evictable_entries(&store, dir.path(), 3);
let hashes: Vec<String> = store
.db
.prepare("SELECT hash FROM blobs")
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<rusqlite::Result<_>>()
.unwrap();
assert_eq!(hashes.len(), 3);
for hash in &hashes {
let retainer = dir.path().join(format!("retained-{hash}"));
std::fs::hard_link(store.blob_path(hash), retainer).unwrap();
}
let mut gc_config = config.clone();
gc_config.max_size = 1;
let mut gc = Store::open(&gc_config).unwrap();
let pause = Duration::from_secs(5);
gc.eviction_pacing = (Duration::ZERO, pause);
let started = std::time::Instant::now();
let stats = gc.evict().unwrap();
let elapsed = started.elapsed();
assert_eq!(stats.entries_unreclaimable, 3, "{stats:?}");
assert_eq!(stats.evict_write_ms, 0, "{stats:?}");
assert!(
elapsed < pause,
"no entry took the lock, swept in {elapsed:?}"
);
}
fn put_idle_entry(store: &Store, dir: &Path, key: &str, size: usize) -> PathBuf {
let output = dir.join(format!("{key}.rlib"));
let mut bytes = vec![0u8; size];
bytes[..key.len()].copy_from_slice(key.as_bytes());
std::fs::write(&output, bytes).unwrap();
store
.put(
key,
key,
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output.clone(), format!("lib{key}.rlib"))],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output);
let meta = store.get(key).unwrap().unwrap();
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime('now', '-1 hour') WHERE cache_key = ?1",
params![key],
)
.unwrap();
store.blob_path(&meta.files[0].hash)
}
#[test]
fn a_size_sweep_leaves_bytes_held_by_target_directories_out_of_its_budget() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let mut store = Store::open(&config).unwrap();
let held_blob = put_idle_entry(&store, dir.path(), "held", 200);
std::fs::hard_link(&held_blob, dir.path().join("target-copy.rlib")).unwrap();
put_idle_entry(&store, dir.path(), "small", 60);
assert_eq!(store.physical_size().unwrap(), 260);
let stats = store.evict().unwrap();
assert_eq!(stats.bytes_held, 200, "{stats:?}");
assert_eq!(stats.entries_unreclaimable, 1, "{stats:?}");
assert_eq!(stats.entries_evicted, 0, "{stats:?}");
assert!(store.contains("held") && store.contains("small"));
put_idle_entry(&store, dir.path(), "large", 90);
let stats = store.evict().unwrap();
assert_eq!(stats.bytes_held, 200, "{stats:?}");
assert_eq!(stats.entries_evicted, 1, "{stats:?}");
assert!(store.contains("held"));
let free_part = store.physical_size().unwrap() - stats.bytes_held;
assert!(free_part <= 90, "stops at the target: {free_part}");
store.config.gc_evict_shared = true;
let stats = store.evict().unwrap();
assert_eq!(stats.bytes_held, 0, "{stats:?}");
assert!(!store.contains("held"));
}
#[test]
fn a_shared_blob_held_outside_is_no_entrys_held_bytes() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
for key in ["first", "second"] {
let output = dir.path().join(format!("{key}.rlib"));
std::fs::write(&output, vec![7u8; 300]).unwrap();
store
.put(
key,
key,
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(output.clone(), "libshared.rlib".to_string())],
"",
"",
)
.unwrap();
store.remove_clone_for_test(&output);
}
let meta = store.get("first").unwrap().unwrap();
std::fs::hard_link(
store.blob_path(&meta.files[0].hash),
dir.path().join("target-copy.rlib"),
)
.unwrap();
let candidates = store
.eviction_candidates_for(SweepOrigin::Requested)
.unwrap();
assert!(store.held_by_live_files(&candidates).unwrap().is_empty());
}
#[test]
fn only_a_size_sweep_counts_held_bytes() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let held_blob = put_idle_entry(&store, dir.path(), "held", 200);
std::fs::hard_link(&held_blob, dir.path().join("target-copy.rlib")).unwrap();
let stats = store.evict_older_than(0).unwrap();
assert_eq!(stats.bytes_held, 0, "{stats:?}");
}
#[test]
fn blob_refcount_drift_reads_the_store_without_the_write_lock() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let payload = b"probe shared blob";
for key in ["probe_a", "probe_b"] {
let output = dir.path().join(format!("{key}.rlib"));
fs::write(&output, payload).unwrap();
store
.put(
key,
"probelib",
&["lib".to_string()],
&[],
"host",
"dev",
&[(output, format!("lib{key}.rlib"))],
"",
"",
)
.unwrap();
}
assert_eq!(
store.blob_refcount_drift().unwrap(),
crate::BlobRefcountDrift::default()
);
store
.db
.execute_batch(
"UPDATE blobs SET refcount = refcount + 1;
INSERT INTO blobs (hash, size, refcount) VALUES ('unowned', 4096, 2);",
)
.unwrap();
let expected = crate::BlobRefcountDrift {
unowned: 1,
unowned_bytes: 4096,
too_high: 1,
too_high_bytes: payload.len() as u64,
..Default::default()
};
assert_eq!(store.blob_refcount_drift().unwrap(), expected);
let writer = Connection::open(config.index_db_path()).unwrap();
writer.execute_batch("BEGIN IMMEDIATE").unwrap();
assert_eq!(store.blob_refcount_drift().unwrap(), expected);
let ro = open_index_db_readonly(&config.index_db_path()).unwrap();
assert_eq!(crate::blob_refcount_drift(&ro).unwrap(), expected);
}
mod size_sweep_properties {
use super::*;
use crate::eviction::{SizePressurePolicy, eviction_target, over_eviction_trigger};
use proptest::prelude::*;
#[derive(Debug, Clone)]
struct EntrySpec {
size: usize,
idle_hours: u32,
hits: i64,
recent: bool,
held: bool,
}
fn entry_spec() -> impl Strategy<Value = EntrySpec> {
(
32usize..=400,
1u32..=4,
0i64..=3,
proptest::bool::weighted(0.25),
proptest::bool::weighted(0.3),
)
.prop_map(|(size, idle_hours, hits, recent, held)| EntrySpec {
size,
idle_hours,
hits,
recent,
held,
})
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 32,
max_shrink_iters: 64,
..ProptestConfig::default()
})]
#[test]
fn size_sweep_evicts_exactly_the_ranked_prefix_it_may(
specs in proptest::collection::vec(entry_spec(), 1..=6),
evict_shared in any::<bool>(),
max_percent in 1u64..=110,
) {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.deferred_durability = true;
let mut store = Store::open(&config).unwrap();
let keys: Vec<String> = (0..specs.len()).map(|i| format!("prop-entry-{i}")).collect();
for (key, spec) in keys.iter().zip(&specs) {
let blob = put_idle_entry(&store, dir.path(), key, spec.size);
if spec.held {
std::fs::hard_link(&blob, dir.path().join(format!("target-{key}"))).unwrap();
}
}
let now: String = store.db.query_row("SELECT datetime('now')", [], |row| row.get(0)).unwrap();
for (key, spec) in keys.iter().zip(&specs) {
let hours = if spec.recent { 0 } else { spec.idle_hours };
store
.db
.execute(
"UPDATE entries SET last_accessed = datetime(?1, ?2), hit_count = ?3 WHERE cache_key = ?4",
params![now, format!("-{hours} hours"), spec.hits, key],
)
.unwrap();
}
let total: u64 = specs.iter().map(|s| s.size as u64).sum();
store.config.max_size = (total * max_percent / 100).max(1);
store.config.gc_evict_shared = evict_shared;
let spec_of = |key: &str| &specs[keys.iter().position(|k| k == key).unwrap()];
let order = SizePressurePolicy.select(&store.eviction_candidates().unwrap());
let physical = store.physical_size().unwrap();
prop_assert_eq!(physical, total);
let target = eviction_target(store.config.max_size);
let mut expected = std::collections::BTreeSet::new();
if over_eviction_trigger(physical, store.config.max_size) {
let held: u64 = specs
.iter()
.filter(|s| s.held && !evict_shared)
.map(|s| s.size as u64)
.sum();
let mut current = physical.saturating_sub(held);
for key in &order {
if current <= target {
break;
}
let spec = spec_of(key);
if spec.recent || (spec.held && !evict_shared) {
continue;
}
expected.insert(key.clone());
current -= spec.size as u64;
}
}
store.evict().unwrap();
let evicted: std::collections::BTreeSet<String> =
keys.iter().filter(|k| !store.contains(k)).cloned().collect();
for key in &evicted {
let spec = spec_of(key);
prop_assert!(!spec.recent, "{key} was used within the grace");
prop_assert!(evict_shared || !spec.held, "{key} is held by a target directory");
}
prop_assert_eq!(evicted, expected);
}
}
}
#[test]
fn lists_indexed_blobs_from_a_size_up() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(test_config(dir.path())).unwrap();
for (hash, size) in [("small", 99_i64), ("edge", 100), ("large", 101)] {
store
.db
.execute(
"INSERT INTO blobs (hash, size, refcount) VALUES (?1, ?2, 1)",
params![hash, size],
)
.unwrap();
}
let mut blobs = store.blobs_at_least(100).unwrap();
blobs.sort();
assert_eq!(
blobs,
vec![("edge".to_string(), 100), ("large".to_string(), 101)]
);
assert!(store.blobs_at_least(u64::MAX).unwrap().is_empty());
}
fn ro_tree_snapshot(dir: &Path) -> Vec<(PathBuf, u64, Option<std::time::SystemTime>, Vec<u8>)> {
let mut out = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for e in fs::read_dir(&d).unwrap() {
let p = e.unwrap().path();
let m = fs::symlink_metadata(&p).unwrap();
if m.is_dir() {
out.push((p.clone(), 0, Some(m.modified().unwrap()), Vec::new()));
stack.push(p);
} else if p.to_string_lossy().ends_with("-shm") {
out.push((p.clone(), m.len(), None, Vec::new()));
} else {
let bytes = fs::read(&p).unwrap();
out.push((p.clone(), m.len(), Some(m.modified().unwrap()), bytes));
}
}
}
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
fn ro_put(store: &Store, src: &Path, key: &str, bytes: &[u8]) {
let out = src.join(format!("{key}.rlib"));
fs::write(&out, bytes).unwrap();
store
.put(
key,
"ro_crate",
&["lib".to_string()],
&[],
"x86_64-unknown-linux-gnu",
"dev",
&[(out, format!("lib{key}.rlib"))],
"",
"",
)
.unwrap();
}
#[test]
fn read_only_store_serves_hits_and_writes_nothing() {
let _env_lock = crate::test_support::process_state_test_lock();
let _verify = EnvVarGuard::set("KACHE_VERIFY_RESTORES", "always");
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
ro_put(&owner, src.path(), "rokey1", b"read-only bytes");
owner.set_last_accessed_for_test("rokey1", "-1 hour");
let before = ro_tree_snapshot(dir.path());
let ro = Store::open_read_only(&config).unwrap();
assert!(ro.is_read_only());
assert!(!owner.is_read_only());
let meta = ro
.get("rokey1")
.unwrap()
.expect("a committed entry is a hit");
assert_eq!(meta.crate_name, "ro_crate");
assert!(ro.get("absent").unwrap().is_none());
let other = src.path().join("other.rlib");
fs::write(&other, b"x").unwrap();
assert!(
ro.try_lock("new-key").is_err(),
"try_lock must be refused on a read-only store"
);
assert!(
ro.put(
"rokey2",
"c",
&["lib".to_string()],
&[],
"t",
"dev",
&[(other.clone(), "libc.rlib".to_string())],
"",
"",
)
.is_err(),
"put must be refused on a read-only store"
);
assert!(
ro.import_downloaded_entry("rokey1").is_err(),
"import_downloaded_entry must be refused on a read-only store"
);
assert!(
ro.import_restored_entry("rokey1").is_err(),
"import_restored_entry must be refused on a read-only store"
);
assert!(
ro.import_verified_restored_entries(&[]).is_err(),
"import_verified_restored_entries must be refused on a read-only store"
);
assert!(
ro.rebuild_index_from_store().is_err(),
"rebuild_index_from_store must be refused on a read-only store"
);
assert!(
ro.record_entry_unit("rokey1", "unit").is_err(),
"record_entry_unit must be refused on a read-only store"
);
assert!(
ro.remove_entry("rokey1").is_err(),
"remove_entry must be refused on a read-only store"
);
assert!(
ro.evict().is_err(),
"evict must be refused on a read-only store"
);
assert!(
ro.evict_for_disk_pressure(|| false).is_err(),
"evict_for_disk_pressure must be refused on a read-only store"
);
assert!(
ro.evict_older_than(0).is_err(),
"evict_older_than must be refused on a read-only store"
);
assert!(
ro.evict_stale_key_schemas(0).is_err(),
"evict_stale_key_schemas must be refused on a read-only store"
);
assert!(
ro.evict_duplicate_entries().is_err(),
"evict_duplicate_entries must be refused on a read-only store"
);
assert!(
ro.clear().is_err(),
"clear must be refused on a read-only store"
);
assert!(
ro.migrate_to_blobs(|_, _| {}).is_err(),
"migrate_to_blobs must be refused on a read-only store"
);
assert!(
ro.claim_build("new-key").is_err(),
"claim_build must be refused on a read-only store"
);
assert!(
ro.try_gc_lock().is_err(),
"try_gc_lock must be refused on a read-only store"
);
assert!(
ro.acquire_gc_lock().is_err(),
"acquire_gc_lock must be refused on a read-only store"
);
assert!(
ro.try_durability_flush_lock().is_err(),
"durability lock must be refused on a read-only store"
);
assert!(
ro.flush_durability(1).is_err(),
"flush_durability must be refused on a read-only store"
);
assert!(
ro.flush_entry_durability("rokey1").is_err(),
"flush_entry_durability must be refused on a read-only store"
);
assert!(
ro.reconcile_blob_index().is_err(),
"reconcile_blob_index must be refused on a read-only store"
);
assert!(
ro.reconcile_blob_index_dropping_unverifiable().is_err(),
"reconcile dropping must be refused on a read-only store"
);
assert!(
ro.sweep_orphan_blobs(Duration::ZERO).is_err(),
"sweep_orphan_blobs must be refused on a read-only store"
);
assert!(
ro.sweep_stale_key_locks(Duration::ZERO, 1).is_err(),
"sweep_stale_key_locks must be refused on a read-only store"
);
assert!(
ro.backfill_content_hashes().is_err(),
"backfill_content_hashes must be refused on a read-only store"
);
assert!(
ro.backfill_compile_times().is_err(),
"backfill_compile_times must be refused on a read-only store"
);
assert!(
ro.backfill_entry_blobs().is_err(),
"backfill_entry_blobs must be refused on a read-only store"
);
assert!(
ro.prune_tombstones(0).is_err(),
"prune_tombstones must be refused on a read-only store"
);
assert!(
ro.remember_incremental_dir(src.path()).is_err(),
"remember_incremental_dir must be refused on a read-only store"
);
assert!(
ro.remember_target_root(src.path(), src.path()).is_err(),
"remember_target_root must be refused on a read-only store"
);
assert!(
ro.remember_discovered_target_root(src.path(), src.path())
.is_err(),
"remember_discovered_target_root must be refused on a read-only store"
);
assert!(
ro.forget_target_root(src.path()).is_err(),
"forget_target_root must be refused on a read-only store"
);
let identity = crate::filesystem::PathIdentity {
device: 1,
inode: 2,
};
let refused = ro
.forget_target_root_with_identity(src.path(), identity)
.unwrap_err();
assert!(
refused
.to_string()
.contains("refusing to forget a target root"),
"forget_target_root_with_identity must be refused on a read-only store: {refused:#}"
);
assert!(
ro.clean_registered_incremental_dirs().is_err(),
"clean_registered_incremental_dirs must be refused on a read-only store"
);
ro.record_known_file_hash(&other, "00");
drop(ro);
assert_eq!(
ro_tree_snapshot(dir.path()),
before,
"a read-only store wrote to the store directory"
);
assert!(owner.contains("rokey1"));
assert!(!owner.contains("rokey2"));
}
#[test]
fn read_only_store_sees_entries_committed_after_it_opened() {
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
ro_put(&owner, src.path(), "early", b"1");
let ro = Store::open_read_only(&config).unwrap();
assert!(ro.get("late").unwrap().is_none());
ro_put(&owner, src.path(), "late", b"2");
assert!(
ro.get("late").unwrap().is_some(),
"a later commit of the owner is visible"
);
}
#[test]
fn read_only_store_refuses_without_a_live_index_and_creates_nothing() {
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let missing = dir.path().join("nope");
assert!(Store::open_read_only(test_config(&missing)).is_err());
assert!(!missing.exists(), "no directory is created");
let config = test_config(dir.path());
{
let owner = Store::open(&config).unwrap();
ro_put(&owner, src.path(), "k", b"bytes");
}
for side in ["index.db-wal", "index.db-shm"] {
let _ = fs::remove_file(dir.path().join(side));
}
let before = ro_tree_snapshot(dir.path());
let err = Store::open_read_only(&config)
.err()
.expect("refused")
.to_string();
assert!(err.contains("WAL mode"), "{err}");
assert_eq!(
ro_tree_snapshot(dir.path()),
before,
"the refusal created nothing"
);
}
#[cfg(unix)]
#[test]
fn read_only_store_treats_a_truncated_blob_as_a_miss_and_evicts_nothing() {
use std::os::unix::fs::PermissionsExt;
let _env_lock = crate::test_support::process_state_test_lock();
let _verify = EnvVarGuard::remove("KACHE_VERIFY_RESTORES");
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
ro_put(&owner, src.path(), "dmg", b"artifact-bytes");
let meta = owner.stored_meta("dmg").unwrap();
let blob = owner.blob_path(&meta.files[0].hash);
fs::set_permissions(&blob, fs::Permissions::from_mode(0o644)).unwrap();
fs::write(&blob, b"short").unwrap();
let ro = Store::open_read_only(&config).unwrap();
assert!(ro.get("dmg").unwrap().is_none(), "a damaged blob is a miss");
assert!(owner.contains("dmg"), "and the entry is not evicted");
assert_eq!(fs::read(&blob).unwrap(), b"short", "nor the blob touched");
}
#[cfg(unix)]
#[test]
fn read_only_store_verifies_content_as_configured() {
use std::os::unix::fs::PermissionsExt;
let _env_lock = crate::test_support::process_state_test_lock();
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
ro_put(&owner, src.path(), "flip", b"artifact-bytes");
let meta = owner.stored_meta("flip").unwrap();
let blob = owner.blob_path(&meta.files[0].hash);
fs::set_permissions(&blob, fs::Permissions::from_mode(0o644)).unwrap();
fs::write(&blob, b"ARTIFACT-BYTES").unwrap();
let ro = Store::open_read_only(&config).unwrap();
{
let _verify = EnvVarGuard::set("KACHE_VERIFY_RESTORES", "always");
assert!(
ro.get("flip").unwrap().is_none(),
"verified: other bytes are a miss"
);
}
let _verify = EnvVarGuard::set("KACHE_VERIFY_RESTORES", "off");
assert!(
ro.get("flip").unwrap().is_some(),
"unverified: the size check passes"
);
}
#[test]
fn read_only_store_treats_a_legacy_entry_as_a_miss() {
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
ro_put(&owner, src.path(), "legacy", b"artifact-bytes");
let meta = owner.stored_meta("legacy").unwrap();
let beside = owner.entry_dir("legacy").join(&meta.files[0].name);
fs::write(&beside, b"artifact-bytes").unwrap();
let ro = Store::open_read_only(&config).unwrap();
assert!(ro.get("legacy").unwrap().is_none());
assert!(beside.exists(), "nothing was migrated");
}
#[test]
fn read_only_maintenance_preserves_files_and_registered_incremental_directories() {
let dir = tempfile::tempdir().unwrap();
let incremental = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
let state = incremental.path().join("state");
fs::write(&state, b"incremental state").unwrap();
owner.remember_incremental_dir(incremental.path()).unwrap();
fs::create_dir_all(owner.staging_dir()).unwrap();
let staging = owner.staging_dir().join("abandoned.tmp");
fs::write(&staging, b"staged bytes").unwrap();
let stale_lock = owner.entry_dir("stale-key").with_extension("lock");
drop(owner.try_lock("stale-key").unwrap().unwrap());
let old = filetime::FileTime::from_unix_time(0, 0);
filetime::set_file_mtime(&staging, old).unwrap();
filetime::set_file_mtime(&stale_lock, old).unwrap();
let mut perms = fs::metadata(&staging).unwrap().permissions();
perms.set_readonly(true);
fs::set_permissions(&staging, perms).unwrap();
let before = ro_tree_snapshot(dir.path());
let ro = Store::open_read_only(&config).unwrap();
assert!(ro.clean_registered_incremental_dirs().is_err());
assert_eq!(fs::read(&state).unwrap(), b"incremental state");
let registry_count: i64 = owner
.db
.query_row("SELECT COUNT(*) FROM incremental_dirs", [], |row| {
row.get(0)
})
.unwrap();
assert_eq!(registry_count, 1);
let sweep = ro.sweep_stale_staging(Duration::ZERO);
assert_eq!(
(sweep.scanned, sweep.removed, sweep.bytes_reclaimed),
(0, 0, 0)
);
assert_eq!(ro.sweep_housekeeping(), HousekeepingStats::default());
assert!(stale_lock.exists());
assert!(fs::metadata(&staging).unwrap().permissions().readonly());
assert_eq!(ro_tree_snapshot(dir.path()), before);
drop(ro);
drop(owner);
fs::remove_file(&staging)
.or_else(|_| {
let mut permissions = fs::metadata(&staging)?.permissions();
#[allow(clippy::permissions_set_readonly_false)]
permissions.set_readonly(false);
fs::set_permissions(&staging, permissions)?;
fs::remove_file(&staging)
})
.unwrap();
}
#[test]
fn read_only_store_treats_malformed_metadata_as_a_miss() {
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
ro_put(&owner, src.path(), "malformed", b"artifact bytes");
let metadata = owner.entry_dir("malformed").join("meta.json");
fs::write(&metadata, b"{broken JSON").unwrap();
let ro = Store::open_read_only(&config).unwrap();
assert!(ro.get("malformed").unwrap().is_none());
assert!(owner.contains("malformed"));
assert_eq!(fs::read(&metadata).unwrap(), b"{broken JSON");
}
#[test]
fn read_only_hash_memo_writers_leave_the_index_and_timeout_untouched() {
let dir = tempfile::tempdir().unwrap();
let src = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
let ro = Store::open_read_only(&config).unwrap();
let artifact = src.path().join("large.rlib");
fs::write(&artifact, vec![42; 65536]).unwrap();
let crate::file_hash::FileHashLookup::NeedsHash(fingerprint) = ro.file_hash_lookup(&artifact)
else {
panic!("a new large file should need hashing");
};
let hash = crate::file_hash::hash_file(&artifact).unwrap();
let before = ro_tree_snapshot(dir.path());
ro.file_hash_record(&fingerprint, &hash);
ro.record_verified_file_hash(&fingerprint, &hash);
ro.record_verified_file_hashes(&[(fingerprint, &hash)]);
ro.record_verified_file_hashes(&[]);
ro.record_known_file_hash(&artifact, &hash);
assert!(matches!(
owner.file_hash_lookup(&artifact),
crate::file_hash::FileHashLookup::NeedsHash(_)
));
let timeout: i64 = ro
.db
.query_row("PRAGMA busy_timeout", [], |row| row.get(0))
.unwrap();
assert_eq!(timeout, 2000);
assert_eq!(ro_tree_snapshot(dir.path()), before);
}
#[test]
fn duplicate_eviction_does_not_probe_unselected_held_entries() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_config(dir.path());
config.max_size = 100;
let store = Store::open(&config).unwrap();
let held = put_idle_entry(&store, dir.path(), "held_duplicate", 200);
put_idle_entry(&store, dir.path(), "new_duplicate", 60);
let unrelated = put_idle_entry(&store, dir.path(), "unrelated", 50);
for (blob, target) in [(held, "held-target.rlib"), (unrelated, "other-target.rlib")] {
fs::hard_link(blob, dir.path().join(target)).unwrap();
}
store.db.execute_batch(
"UPDATE entries SET content_hash = 'duplicate-group' WHERE cache_key IN ('held_duplicate', 'new_duplicate');
UPDATE entries SET last_accessed = datetime('now', '-2 hours') WHERE cache_key = 'held_duplicate';"
).unwrap();
let stats = store.evict_duplicate_entries().unwrap();
assert_eq!(
stats.bytes_held, 0,
"duplicates must not run the whole-store probe: {stats:?}"
);
assert_eq!(
stats.entries_unreclaimable, 1,
"only the selected duplicate is inspected: {stats:?}"
);
assert_eq!(stats.entries_evicted, 0);
assert_eq!(store.entry_count().unwrap(), 3);
let size = store.evict().unwrap();
assert_eq!(size.bytes_held, 250);
assert_eq!(size.entries_unreclaimable, 2);
assert_eq!(size.entries_evicted, 0, "held bytes leave the size budget");
let age = store.evict_older_than(0).unwrap();
assert_eq!(age.bytes_held, 0);
assert_eq!(age.entries_unreclaimable, 2);
assert_eq!(age.entries_evicted, 1);
}
#[test]
fn quarantine_suffix_matches_only_timestamp_pid_and_sqlite_sidecars() {
for suffix in ["0-1", "123-456", "123-456-wal", "123-456-shm"] {
assert!(is_quarantine_suffix(suffix), "{suffix}");
}
for suffix in [
"",
"123",
"-123",
"123-",
"123-x",
"x-123",
"123-456-journal",
"123-456-wal-wal",
"1-2-extra",
] {
assert!(!is_quarantine_suffix(suffix), "{suffix}");
}
}
#[test]
fn full_clear_removes_quarantined_files_and_preserves_live_index_and_other_paths() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let store = Store::open(&config).unwrap();
let db_path = config.index_db_path();
let quarantine = db_path.with_file_name("index.db.corrupt-123-456");
fs::write(&quarantine, b"corrupt").unwrap();
let wal = index_sidecar_path(&quarantine, "-wal");
let shm = index_sidecar_path(&quarantine, "-shm");
fs::write(&wal, b"wal copy").unwrap();
fs::write(&shm, b"shm").unwrap();
let preserved = [
"other.db.corrupt-123-456",
"index.db.corrupt-not-a-quarantine",
"index.db.corrupt-123-456-journal",
"index.db.recovery-lock",
];
for name in preserved {
fs::write(config.cache_dir.join(name), b"keep").unwrap();
}
let directory = config.cache_dir.join("index.db.corrupt-987-654");
fs::create_dir(&directory).unwrap();
#[cfg(unix)]
let symlink = {
let path = config.cache_dir.join("index.db.corrupt-777-888");
std::os::unix::fs::symlink(&db_path, &path).unwrap();
path
};
let files = quarantined_index_files(&db_path).unwrap();
assert_eq!(
files,
vec![
QuarantinedIndexFile {
path: quarantine.clone(),
bytes: 7
},
QuarantinedIndexFile {
path: shm.clone(),
bytes: 3
},
QuarantinedIndexFile {
path: wal.clone(),
bytes: 8
},
]
);
store.evict_older_than(0).unwrap();
assert_eq!(
quarantined_index_files(&db_path).unwrap(),
files,
"ordinary GC retains forensic copies"
);
store.clear().unwrap();
assert!(quarantined_index_files(&db_path).unwrap().is_empty());
for path in [quarantine, wal, shm] {
assert!(!path.exists());
}
assert!(db_path.is_file());
assert_eq!(store.entry_count().unwrap(), 0);
for name in preserved {
assert_eq!(fs::read(config.cache_dir.join(name)).unwrap(), b"keep");
}
assert!(directory.is_dir());
#[cfg(unix)]
assert!(
fs::symlink_metadata(symlink)
.unwrap()
.file_type()
.is_symlink()
);
}
#[test]
fn quarantine_listing_reports_io_errors_and_tolerates_absent_cache() {
let dir = tempfile::tempdir().unwrap();
assert!(
quarantined_index_files(&dir.path().join("missing/index.db"))
.unwrap()
.is_empty()
);
let parent = dir.path().join("not-a-directory");
fs::write(&parent, "file").unwrap();
assert!(quarantined_index_files(&parent.join("index.db")).is_err());
assert!(quarantined_index_files(Path::new("/")).is_err());
}
#[test]
fn quarantine_cleanup_tolerates_peer_removal_and_reports_other_errors() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("index.db");
let first = dir.path().join("index.db.corrupt-123-456");
let second = dir.path().join("index.db.corrupt-123-456-wal");
fs::write(&first, b"old index").unwrap();
fs::write(&second, b"old wal").unwrap();
let listed = quarantined_index_files(&db).unwrap();
assert_eq!(listed.len(), 2);
fs::remove_file(&first).unwrap();
remove_quarantined_index_files(&listed).unwrap();
assert!(
!second.exists(),
"a disappeared copy must not abort the remaining removals"
);
fs::write(&first, b"old index").unwrap();
let listed = quarantined_index_files(&db).unwrap();
fs::remove_file(&first).unwrap();
fs::create_dir(&first).unwrap();
assert!(
remove_quarantined_index_files(&listed).is_err(),
"only NotFound may be ignored"
);
assert!(
first.is_dir(),
"cleanup must not delete a replacement directory"
);
}
#[test]
fn eviction_horizon_public_api_reads_existing_observations_without_writes() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(dir.path());
let owner = Store::open(&config).unwrap();
owner.db.execute("INSERT INTO eviction_tombstones(cache_key,evicted_at,policy,size,hit_count,idle_hours,compile_time_ms,demanded_at,shadow_policy,shadow_would_evict) VALUES ('old',datetime(100,'unixepoch'),'lru',1073741824,0,0,900,datetime(150,'unixepoch'),'value-density',1)",[]).unwrap();
let reader = Store::open_read_only(&config).unwrap();
let changes = reader.db.total_changes();
let report = reader.eviction_horizon_evidence(200, 100).unwrap();
assert_eq!(report.as_of_unix_secs, 200);
assert_eq!(report.horizon_secs, 100);
assert_eq!(report.live_shadow_agreed.mature, 1);
assert_eq!(
report
.live_shadow_agreed
.demanded_gross_compile_cost_ms_lower,
900
);
assert_eq!(reader.db.total_changes(), changes);
assert!(reader.eviction_horizon_evidence(200, 0).is_err());
assert_eq!(
owner
.db
.query_row("SELECT count(*) FROM eviction_tombstones", [], |r| r
.get::<_, i64>(0))
.unwrap(),
1
);
}