use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
mod common;
use common::{SCAN_MACRO, build_kache, isolated_config_path, kache_binary, settle_writes};
fn rustc_path() -> String {
std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string())
}
fn toml_path(path: &Path) -> String {
toml::Value::String(path.to_string_lossy().into_owned()).to_string()
}
fn write_test_config(cache_dir: &Path, predictions: bool) -> PathBuf {
write_test_config_with(cache_dir, cache_dir, None, predictions)
}
fn write_test_config_with(
cache_dir: &Path,
runtime_dir: &Path,
shard: Option<(&Path, &Path)>,
predictions: bool,
) -> PathBuf {
let config_path = isolated_config_path(cache_dir);
let mut config = format!(
"[cache]\nlocal_only = true\nignore_env = true\ninput_predictions = {predictions}\nlocal_store = {}\nruntime_dir = {}\n",
toml_path(cache_dir),
toml_path(runtime_dir)
);
if let Some((root, store)) = shard {
config.push_str(&format!(
"[cache.volumes]\n{} = {}\n",
toml_path(root),
toml_path(store)
));
}
std::fs::write(&config_path, config).unwrap();
config_path
}
fn run_kache_rustc_predict(
cache_dir: &Path,
out_dir: &Path,
src: &Path,
predictions: bool,
) -> std::process::Output {
run_kache_rustc_verify(cache_dir, out_dir, src, predictions, None)
}
fn run_kache_rustc_verify(
cache_dir: &Path,
out_dir: &Path,
src: &Path,
predictions: bool,
verify: Option<&str>,
) -> std::process::Output {
run_kache_rustc_with(
cache_dir,
out_dir,
src,
predictions,
verify,
Invocation::Plain,
)
}
#[derive(Clone, Copy, PartialEq)]
enum Invocation {
Plain,
CargoEmits,
Cargo,
}
fn run_kache_rustc_as_cargo(cache_dir: &Path, out_dir: &Path, src: &Path, predictions: bool) {
run_kache_rustc_with(
cache_dir,
out_dir,
src,
predictions,
None,
Invocation::Cargo,
);
}
fn run_kache_rustc_with(
cache_dir: &Path,
out_dir: &Path,
src: &Path,
predictions: bool,
verify: Option<&str>,
invocation: Invocation,
) -> std::process::Output {
run_kache_compiler_with(
&rustc_path(),
cache_dir,
out_dir,
src,
predictions,
verify,
invocation,
)
}
fn run_kache_compiler_with(
compiler: &str,
cache_dir: &Path,
out_dir: &Path,
src: &Path,
predictions: bool,
verify: Option<&str>,
invocation: Invocation,
) -> std::process::Output {
let mut args: Vec<String> = vec![
compiler.to_string(),
"--crate-name".into(),
"kt".into(),
"--crate-type".into(),
"lib".into(),
"--edition".into(),
"2021".into(),
"--out-dir".into(),
out_dir.display().to_string(),
src.display().to_string(),
];
if invocation == Invocation::Plain {
args.push("--emit=link".into());
} else {
args.push("--emit=dep-info,metadata,link".into());
}
if invocation == Invocation::Cargo {
args.extend(
[
"--error-format=json",
"--json=diagnostic-rendered-ansi,artifacts",
]
.map(String::from),
);
}
let config_path = write_test_config(cache_dir, predictions);
settle_writes(&[src.parent().expect("a source in a directory")]);
let output = std::process::Command::new(kache_binary())
.args(&args)
.env("KACHE_CACHE_DIR", cache_dir)
.env("KACHE_CONFIG", config_path)
.env("KACHE_INT_SET", "x")
.env_remove("KACHE_INT_UNSET")
.env_remove("CARGO_MANIFEST_DIR")
.env_remove("KACHE_DISABLED")
.env_remove("KACHE_NAMESPACE")
.env_remove("KACHE_BASE_DIR")
.env_remove("KACHE_SOCKET_PATH")
.env_remove("KACHE_ACTIVE")
.env_remove("KACHE_FAMILY_PROBE_ACTIVE")
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER")
.envs(verify.map(|mode| ("KACHE_VERIFY_INPUT_PREDICTIONS", mode)))
.output()
.expect("failed to run kache rustc");
assert!(
output.status.success(),
"kache rustc failed.\nargs: {args:?}\nstderr: {}",
String::from_utf8_lossy(&output.stderr),
);
output
}
struct LastEvent {
result: String,
cache_key: String,
dep_info_runs: u64,
compiler_runs: u64,
prediction_mismatches: u64,
}
fn summary_local_hits(cache_dir: &Path) -> u64 {
let output = std::process::Command::new(kache_binary())
.args(["report", "--format", "json", "--since", "1h"])
.env("KACHE_CACHE_DIR", cache_dir)
.env("KACHE_CONFIG", isolated_config_path(cache_dir))
.env_remove("KACHE_DISABLED")
.env_remove("KACHE_NAMESPACE")
.env_remove("KACHE_BASE_DIR")
.env_remove("KACHE_SOCKET_PATH")
.env_remove("KACHE_ACTIVE")
.env_remove("KACHE_FAMILY_PROBE_ACTIVE")
.output()
.expect("failed to run kache report");
assert!(output.status.success(), "kache report failed");
let report: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("report should be valid json");
report["summary"]["local_hits"].as_u64().unwrap_or(u64::MAX)
}
fn last_event(cache_dir: &Path) -> LastEvent {
last_event_with(cache_dir, &[])
}
fn last_event_with(cache_dir: &Path, env: &[(&str, std::ffi::OsString)]) -> LastEvent {
let output = std::process::Command::new(kache_binary())
.args(["report", "--format", "json", "--since", "1h"])
.env("KACHE_CACHE_DIR", cache_dir)
.env("KACHE_CONFIG", isolated_config_path(cache_dir))
.env_remove("KACHE_DISABLED")
.env_remove("KACHE_NAMESPACE")
.env_remove("KACHE_BASE_DIR")
.env_remove("KACHE_SOCKET_PATH")
.env_remove("KACHE_ACTIVE")
.env_remove("KACHE_FAMILY_PROBE_ACTIVE")
.envs(env.iter().map(|(name, value)| (name, value)))
.output()
.expect("failed to run kache report");
assert!(output.status.success(), "kache report failed");
let report: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("report should be valid json");
let event = report["all_events"]
.as_array()
.expect("report should include all_events")
.iter()
.rev()
.find(|e| e["crate_name"].as_str() == Some("kt"))
.expect("report should include a kt event");
LastEvent {
result: event["result"].as_str().unwrap_or("").to_string(),
cache_key: event["cache_key"].as_str().unwrap_or("").to_string(),
dep_info_runs: event["dep_info_runs"].as_u64().unwrap_or(u64::MAX),
compiler_runs: event["compiler_runs"].as_u64().unwrap_or(u64::MAX),
prediction_mismatches: event["prediction_mismatches"].as_u64().unwrap_or(u64::MAX),
}
}
fn fixture() -> (TempDir, TempDir, TempDir, PathBuf) {
let work = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
let src = work.path().join("lib.rs");
std::fs::write(
&src,
b"mod a;\npub fn f() -> u32 {\n let _ = include_str!(\"data.txt\");\n let _ = env!(\"KACHE_INT_SET\");\n let _ = option_env!(\"KACHE_INT_UNSET\");\n a::g() + 1\n}\n",
)
.unwrap();
std::fs::write(work.path().join("a.rs"), b"pub fn g() -> u32 { 41 }\n").unwrap();
std::fs::write(work.path().join("data.txt"), b"data\n").unwrap();
(work, cache, out, src)
}
#[test]
fn predictions_cold_warm_off_and_stale_closure() {
build_kache();
let (work, cache, out, src) = fixture();
let cache_dir = cache.path();
let out_dir = out.path();
run_kache_rustc_predict(cache_dir, out_dir, &src, true);
let cold = last_event(cache_dir);
assert_eq!(cold.result, "miss");
assert_eq!(cold.dep_info_runs, 1);
run_kache_rustc_predict(cache_dir, out_dir, &src, true);
let warm = last_event(cache_dir);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0);
assert_eq!(warm.cache_key, cold.cache_key);
run_kache_rustc_predict(cache_dir, out_dir, &src, false);
let off = last_event(cache_dir);
assert_eq!(off.result, "local_hit");
assert_eq!(off.dep_info_runs, 1);
assert_eq!(off.cache_key, cold.cache_key);
std::fs::write(
&src,
b"mod a;\nmod b;\npub fn f() -> u32 {\n let _ = include_str!(\"data.txt\");\n let _ = env!(\"KACHE_INT_SET\");\n let _ = option_env!(\"KACHE_INT_UNSET\");\n a::g() + b::h()\n}\n",
)
.unwrap();
std::fs::write(work.path().join("b.rs"), b"pub fn h() -> u32 { 1 }\n").unwrap();
run_kache_rustc_predict(cache_dir, out_dir, &src, true);
let grown = last_event(cache_dir);
assert_eq!(grown.result, "miss");
assert_eq!(grown.dep_info_runs, 1);
assert_ne!(grown.cache_key, cold.cache_key);
run_kache_rustc_predict(cache_dir, out_dir, &src, false);
let regrown = last_event(cache_dir);
assert_eq!(regrown.result, "local_hit");
assert_eq!(regrown.cache_key, grown.cache_key);
}
#[test]
fn a_stale_record_is_re_derived_from_the_running_compile() {
build_kache();
let (work, cache, out, src) = fixture();
let (cache_dir, out_dir) = (cache.path(), out.path());
run_kache_rustc_as_cargo(cache_dir, out_dir, &src, true);
let cold = last_event(cache_dir);
assert_eq!(cold.result, "miss");
std::fs::write(
&src,
b"mod a;\nmod b;\npub fn f() -> u32 {\n let _ = include_str!(\"data.txt\");\n let _ = env!(\"KACHE_INT_SET\");\n let _ = option_env!(\"KACHE_INT_UNSET\");\n a::g() + b::h()\n}\n",
)
.unwrap();
std::fs::write(work.path().join("b.rs"), b"pub fn h() -> u32 { 1 }\n").unwrap();
run_kache_rustc_as_cargo(cache_dir, out_dir, &src, true);
let grown = last_event(cache_dir);
assert_eq!(grown.result, "miss");
assert_eq!(grown.dep_info_runs, 0, "the compile re-derived the closure");
assert_eq!(grown.compiler_runs, 1);
assert_ne!(grown.cache_key, cold.cache_key);
let fresh = TempDir::new().unwrap();
run_kache_rustc_with(
cache_dir,
fresh.path(),
&src,
false,
None,
Invocation::CargoEmits,
);
let checked = last_event(cache_dir);
assert_eq!(checked.result, "local_hit");
assert_eq!(checked.dep_info_runs, 1);
assert_eq!(checked.cache_key, grown.cache_key);
}
fn last_raw_event(cache_dir: &Path) -> serde_json::Value {
std::fs::read_to_string(cache_dir.join("events.jsonl"))
.expect("events.jsonl")
.lines()
.rev()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.find(|event| event["crate_name"] == "kt")
.expect("a kt event")
}
fn entries_for(cache_dir: &Path, event: &serde_json::Value) -> i64 {
let key = event["cache_key"].as_str().expect("a keyed event");
rusqlite::Connection::open(cache_dir.join("index.db"))
.unwrap()
.query_row(
"SELECT COUNT(*) FROM entries WHERE cache_key = ?1",
[key],
|row| row.get(0),
)
.unwrap()
}
#[cfg(unix)]
#[test]
fn a_source_saved_while_rustc_ran_is_not_restored_for_its_new_content() {
use std::os::unix::fs::PermissionsExt;
build_kache();
let (work, cache, out, src) = fixture();
let (cache_dir, out_dir) = (cache.path(), out.path());
let module = work.path().join("a.rs");
let sysroot = std::process::Command::new(rustc_path())
.args(["--print", "sysroot"])
.output()
.expect("rustc --print sysroot");
let real = PathBuf::from(String::from_utf8(sysroot.stdout).unwrap().trim())
.join("bin")
.join("rustc");
let armed = work.path().join("armed");
let bin = work.path().join("bin");
std::fs::create_dir_all(&bin).unwrap();
let stand_in = bin.join("rustc");
std::fs::write(
&stand_in,
format!(
"#!/bin/sh\n\"{real}\" \"$@\"\nstatus=$?\n\
case \" $* \" in *\" --crate-name kt \"*)\n\
if [ -e \"{armed}\" ]; then rm -f \"{armed}\"; \
printf 'pub fn g() -> u32 {{ 42 }}\\n' > \"{module}\"; fi ;;\nesac\n\
exit $status\n",
real = real.display(),
armed = armed.display(),
module = module.display(),
),
)
.unwrap();
std::fs::set_permissions(&stand_in, std::fs::Permissions::from_mode(0o755)).unwrap();
let compiler = stand_in.to_str().unwrap();
std::fs::write(&armed, b"").unwrap();
run_kache_compiler_with(
compiler,
cache_dir,
out_dir,
&src,
true,
None,
Invocation::Cargo,
);
assert!(!armed.exists(), "the stand-in rewrote a.rs after compiling");
assert_eq!(
std::fs::read_to_string(&module).unwrap(),
"pub fn g() -> u32 { 42 }\n"
);
let raced = last_raw_event(cache_dir);
assert_eq!(raced["result"], "skipped", "{raced}");
assert_eq!(raced["skip_reason"], "inputs-changed", "{raced}");
assert_eq!(entries_for(cache_dir, &raced), 0);
let fresh = TempDir::new().unwrap();
run_kache_compiler_with(
compiler,
cache_dir,
fresh.path(),
&src,
true,
None,
Invocation::Cargo,
);
let next = last_raw_event(cache_dir);
assert_eq!(next["result"], "miss", "{next}");
assert_eq!(next["cache_key"], raced["cache_key"], "{next}");
}
#[test]
fn a_too_new_input_leaves_a_compile_first_result_unstored() {
build_kache();
let (work, cache, out, src) = fixture();
let (cache_dir, out_dir) = (cache.path(), out.path());
let module = work.path().join("a.rs");
let stamp = |at: SystemTime| {
std::fs::File::options()
.write(true)
.open(&module)
.unwrap()
.set_modified(at)
.unwrap();
};
let hour = Duration::from_secs(3600);
stamp(SystemTime::now() + hour);
run_kache_rustc_as_cargo(cache_dir, out_dir, &src, true);
let cold = last_raw_event(cache_dir);
assert_eq!(cold["result"], "skipped", "{cold}");
assert_eq!(cold["skip_reason"], "inputs-changed", "{cold}");
assert_eq!(cold["dep_info_runs"], 0, "compiled before keying: {cold}");
assert!(
out_dir.join("libkt.rlib").is_file(),
"the compile succeeded"
);
assert_eq!(entries_for(cache_dir, &cold), 0);
stamp(SystemTime::now() - hour);
run_kache_rustc_as_cargo(cache_dir, out_dir, &src, true);
let stored = last_raw_event(cache_dir);
assert_eq!(stored["result"], "miss", "{stored}");
assert_eq!(entries_for(cache_dir, &stored), 1);
std::fs::write(&module, b"pub fn g() -> u32 { 40 }\n").unwrap();
stamp(SystemTime::now() + hour);
run_kache_rustc_as_cargo(cache_dir, out_dir, &src, true);
let rederived = last_raw_event(cache_dir);
assert_eq!(rederived["result"], "skipped", "{rederived}");
assert_eq!(rederived["skip_reason"], "inputs-changed", "{rederived}");
assert_eq!(rederived["dep_info_runs"], 0, "{rederived}");
assert_ne!(rederived["cache_key"], stored["cache_key"]);
assert_eq!(entries_for(cache_dir, &rederived), 0);
}
#[test]
fn verified_predictions_agree_with_the_pre_pass_and_count_it() {
build_kache();
let (_work, cache, out, src) = fixture();
let cache_dir = cache.path();
let out_dir = out.path();
run_kache_rustc_verify(cache_dir, out_dir, &src, true, Some("always"));
let cold = last_event(cache_dir);
assert_eq!(cold.result, "miss");
assert_eq!(cold.prediction_mismatches, 0);
run_kache_rustc_verify(cache_dir, out_dir, &src, true, Some("always"));
let verified = last_event(cache_dir);
assert_eq!(verified.result, "local_hit");
assert_eq!(verified.cache_key, cold.cache_key);
assert_eq!(
verified.prediction_mismatches, 0,
"the prediction must reproduce exactly what the pre-pass discovered"
);
assert_eq!(
verified.dep_info_runs, 1,
"verification pays for the pre-pass it was checking against"
);
run_kache_rustc_predict(cache_dir, out_dir, &src, true);
let unverified = last_event(cache_dir);
assert_eq!(unverified.result, "local_hit");
assert_eq!(unverified.cache_key, cold.cache_key);
assert_eq!(unverified.dep_info_runs, 0);
assert_eq!(unverified.prediction_mismatches, 0);
}
#[test]
fn predictions_sibling_shadow_fails_closed() {
build_kache();
let (work, cache, out, src) = fixture();
let cache_dir = cache.path();
let out_dir = out.path();
run_kache_rustc_predict(cache_dir, out_dir, &src, true);
assert_eq!(last_event(cache_dir).result, "miss");
run_kache_rustc_predict(cache_dir, out_dir, &src, true);
assert_eq!(last_event(cache_dir).result, "local_hit");
std::fs::create_dir_all(work.path().join("a")).unwrap();
std::fs::write(
work.path().join("a").join("mod.rs"),
b"pub fn h() -> u32 { 1 }\n",
)
.unwrap();
let args: Vec<String> = vec![
rustc_path(),
"--crate-name".into(),
"kt".into(),
"--crate-type".into(),
"lib".into(),
"--edition".into(),
"2021".into(),
"--emit=link".into(),
"--out-dir".into(),
out_dir.display().to_string(),
src.display().to_string(),
];
let config_path = write_test_config(cache_dir, true);
let output = std::process::Command::new(kache_binary())
.args(&args)
.env("KACHE_CACHE_DIR", cache_dir)
.env("KACHE_CONFIG", config_path)
.env("KACHE_INT_SET", "x")
.env_remove("KACHE_INT_UNSET")
.env_remove("CARGO_MANIFEST_DIR")
.env_remove("KACHE_DISABLED")
.env_remove("KACHE_NAMESPACE")
.env_remove("KACHE_BASE_DIR")
.env_remove("KACHE_SOCKET_PATH")
.env_remove("KACHE_ACTIVE")
.env_remove("KACHE_FAMILY_PROBE_ACTIVE")
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER")
.output()
.expect("failed to run kache rustc");
assert!(
!output.status.success(),
"the real E0761 must surface, not a stale hit"
);
assert_eq!(
summary_local_hits(cache_dir),
1,
"exactly the one warm hit from before may exist"
);
}
const REGISTRY_PACKAGE: &str = "home/registry/src/index-test/kt-1.0.0";
const BUILD_UNIT: &str = "kt-0123456789abcdef";
const GENERATED: &str = "pub fn generated() -> u32 { 7 }\n";
fn out_dir_in(target: &Path) -> PathBuf {
target.join("debug/build").join(BUILD_UNIT).join("out")
}
struct OutDirUnit {
root: TempDir,
cache: PathBuf,
package: PathBuf,
proc_macro: Option<PathBuf>,
facade: Vec<PathBuf>,
}
impl OutDirUnit {
fn new(manifest_dir: &str, with_macro: bool) -> Self {
let root = TempDir::new().unwrap();
let package = root.path().join(manifest_dir);
std::fs::create_dir_all(package.join("src")).unwrap();
std::fs::write(package.join("Cargo.toml"), "[package]\nname = \"kt\"\n").unwrap();
let mut lib = String::new();
if with_macro {
lib.push_str("pm::answer!();\n");
}
lib.push_str("include!(concat!(env!(\"OUT_DIR\"), \"/gen.rs\"));\n");
std::fs::write(package.join("src/lib.rs"), lib).unwrap();
let proc_macro = with_macro.then(|| build_proc_macro(root.path()));
let cache = root.path().join("cache");
std::fs::create_dir_all(&cache).unwrap();
Self {
root,
cache,
package,
proc_macro,
facade: Vec::new(),
}
}
fn with_facade(mut self) -> Self {
self.facade = build_facade(self.root.path());
self
}
fn with_file(self, path: &str, content: &str) -> Self {
let path = self.package.join(path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
self
}
fn target(&self, checkout: &str) -> PathBuf {
let target = self.root.path().join(checkout).join("target");
std::fs::create_dir_all(out_dir_in(&target)).unwrap();
std::fs::create_dir_all(target.join("debug/deps")).unwrap();
std::fs::write(out_dir_in(&target).join("gen.rs"), GENERATED).unwrap();
if let Some(proc_macro) = &self.proc_macro {
std::fs::copy(proc_macro, self.macro_in(&target)).unwrap();
}
copy_into_deps(&self.facade, &target);
target
}
fn macro_in(&self, target: &Path) -> PathBuf {
let name = self.proc_macro.as_ref().unwrap().file_name().unwrap();
target.join("debug/deps").join(name)
}
fn build(&self, target: &Path, predictions: bool, verify: Option<&str>) -> LastEvent {
let deps = target.join("debug/deps");
let mut args: Vec<String> = vec![
rustc_path(),
"--crate-name".into(),
"kt".into(),
"--edition=2021".into(),
self.package.join("src/lib.rs").display().to_string(),
"--crate-type".into(),
"lib".into(),
"--emit=dep-info,metadata,link".into(),
"-C".into(),
"metadata=fedcba9876543210".into(),
"-C".into(),
"extra-filename=-fedcba9876543210".into(),
"--out-dir".into(),
deps.display().to_string(),
"-L".into(),
format!("dependency={}", deps.display()),
];
if self.proc_macro.is_some() {
args.push("--extern".into());
args.push(format!("pm={}", self.macro_in(target).display()));
}
if !self.facade.is_empty() {
args.push("--extern".into());
args.push(format!("facade={}", deps.join("libfacade.rlib").display()));
}
let config_path = write_test_config(&self.cache, predictions);
let out_dir = out_dir_in(target);
let envs = [
("CARGO_MANIFEST_DIR", self.package.as_os_str()),
("OUT_DIR", out_dir.as_os_str()),
];
kache_rustc_in(
&self.package,
&self.cache,
&config_path,
&args,
&envs,
verify,
)
}
}
fn kache_rustc_in(
package: &Path,
cache: &Path,
config_path: &Path,
args: &[String],
envs: &[(&str, &std::ffi::OsStr)],
verify: Option<&str>,
) -> LastEvent {
let read: Vec<&Path> = std::iter::once(package)
.chain(envs.iter().map(|(_, dir)| Path::new(dir)))
.collect();
settle_writes(&read);
let mut command = std::process::Command::new(kache_binary());
command
.args(args)
.current_dir(package)
.env("KACHE_CACHE_DIR", cache)
.env("KACHE_CONFIG", config_path)
.envs(envs.iter().copied())
.env_remove("KACHE_DISABLED")
.env_remove("KACHE_NAMESPACE")
.env_remove("KACHE_BASE_DIR")
.env_remove("KACHE_SOCKET_PATH")
.env_remove("KACHE_ACTIVE")
.env_remove("KACHE_FAMILY_PROBE_ACTIVE")
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER");
match verify {
Some(mode) => command.env("KACHE_VERIFY_INPUT_PREDICTIONS", mode),
None => command.env_remove("KACHE_VERIFY_INPUT_PREDICTIONS"),
};
let output = command.output().expect("failed to run kache rustc");
assert!(
output.status.success(),
"kache rustc failed.\nargs: {args:?}\nstderr: {}",
String::from_utf8_lossy(&output.stderr),
);
last_event(cache)
}
fn build_proc_macro(root: &Path) -> PathBuf {
let src = root.join("pm.rs");
std::fs::write(
&src,
"extern crate proc_macro;\n\
#[proc_macro]\n\
pub fn answer(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {\n\
\x20 \"pub fn answer() -> u32 { 42 }\".parse().unwrap()\n\
}\n",
)
.unwrap();
let out = root.join("pm-out");
std::fs::create_dir_all(&out).unwrap();
let status = std::process::Command::new(rustc_path())
.args(["--crate-name", "pm", "--crate-type", "proc-macro"])
.args(["--edition", "2021", "--out-dir"])
.arg(&out)
.arg(&src)
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER")
.status()
.expect("run rustc to build the test proc macro");
assert!(status.success(), "building the test proc macro failed");
let extension = if cfg!(windows) {
"dll"
} else if cfg!(target_os = "macos") {
"dylib"
} else {
"so"
};
std::fs::read_dir(&out)
.unwrap()
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.find(|path| path.extension().is_some_and(|e| e == extension))
.unwrap_or_else(|| panic!("no proc-macro {extension} in {}", out.display()))
}
const SCAN_LIB: &str = "facade::scan!();\npub fn count() -> usize { ASSETS.len() }\n";
fn build_facade(root: &Path) -> Vec<PathBuf> {
build_facade_with(root, SCAN_MACRO)
}
fn build_facade_with(root: &Path, scan: &str) -> Vec<PathBuf> {
let out = root.join("facade-out");
std::fs::create_dir_all(&out).unwrap();
let rustc = |args: &[&str], source: &Path| {
let status = std::process::Command::new(rustc_path())
.args(args)
.args(["--edition", "2021", "--out-dir"])
.arg(&out)
.arg(source)
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER")
.status()
.expect("run rustc to build the scan fixture");
assert!(status.success(), "building {} failed", source.display());
};
let scan_source = root.join("scan.rs");
std::fs::write(&scan_source, scan).unwrap();
rustc(
&["--crate-name", "scan", "--crate-type", "proc-macro"],
&scan_source,
);
let extension = std::env::consts::DLL_EXTENSION;
let scan = std::fs::read_dir(&out)
.unwrap()
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.find(|path| path.extension().is_some_and(|e| e == extension))
.unwrap_or_else(|| panic!("no proc-macro {extension} in {}", out.display()));
let facade_source = root.join("facade.rs");
std::fs::write(&facade_source, "pub use scan::scan;\n").unwrap();
rustc(
&[
"--crate-name",
"facade",
"--crate-type",
"rlib",
"--extern",
&format!("scan={}", scan.display()),
],
&facade_source,
);
vec![scan, out.join("libfacade.rlib")]
}
fn copy_into_deps(artifacts: &[PathBuf], target: &Path) {
for artifact in artifacts {
let deps = target.join("debug/deps");
std::fs::create_dir_all(&deps).unwrap();
std::fs::copy(artifact, deps.join(artifact.file_name().unwrap())).unwrap();
}
}
fn predicts_in_another_target(unit: OutDirUnit) {
build_kache();
let a = unit.target("a");
let b = unit.target("b");
let cold = unit.build(&a, true, None);
assert_eq!(cold.result, "miss");
let warm = unit.build(&b, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "B uses the record A made");
assert_eq!(warm.compiler_runs, 0);
let off = unit.build(&b, false, None);
assert_eq!(off.result, "local_hit");
assert_eq!(off.dep_info_runs, 1);
assert_eq!(warm.cache_key, off.cache_key);
let verified = unit.build(&b, true, Some("always"));
assert_eq!(verified.result, "local_hit");
assert_eq!(verified.dep_info_runs, 1);
assert_eq!(verified.prediction_mismatches, 0);
assert_eq!(verified.cache_key, off.cache_key);
}
#[test]
fn a_registry_unit_predicts_in_another_target() {
predicts_in_another_target(OutDirUnit::new(REGISTRY_PACKAGE, true));
}
#[test]
fn a_registry_unit_without_a_macro_predicts_in_another_target() {
predicts_in_another_target(OutDirUnit::new(REGISTRY_PACKAGE, false));
}
const README_DOC: &str = "#![doc = include_str!(\"../README.md\")]\n";
#[test]
fn a_registry_unit_reading_its_readme_predicts_in_another_target() {
for lib in [
format!("{README_DOC}pub fn f() {{}}\n"),
format!("{README_DOC}include!(concat!(env!(\"OUT_DIR\"), \"/gen.rs\"));\n"),
] {
let unit = OutDirUnit::new(REGISTRY_PACKAGE, false)
.with_file("README.md", "Docs.\n")
.with_file("src/lib.rs", &lib);
predicts_in_another_target(unit);
}
}
fn build_a_then_b(unit: &OutDirUnit, prepare: impl FnOnce(&Path, &Path)) -> LastEvent {
build_kache();
let a = unit.target("a");
let b = unit.target("b");
prepare(&a, &b);
assert_eq!(unit.build(&a, true, None).result, "miss");
unit.build(&b, true, None)
}
#[test]
fn a_relocated_record_needs_the_same_out_dir() {
let unit = OutDirUnit::new(REGISTRY_PACKAGE, false);
let warm = build_a_then_b(&unit, |_, b| {
std::fs::write(out_dir_in(b).join("extra.rs"), "// not included\n").unwrap();
});
assert_eq!(warm.dep_info_runs, 1);
assert_eq!(
warm.result, "local_hit",
"the closure itself did not change"
);
}
#[test]
fn a_generated_file_naming_the_target_keeps_the_record_local() {
let unit = OutDirUnit::new(REGISTRY_PACKAGE, false);
let warm = build_a_then_b(&unit, |a, b| {
let generated = format!("// generated in {}\n{GENERATED}", a.display());
for target in [a, b] {
std::fs::write(out_dir_in(target).join("gen.rs"), &generated).unwrap();
}
});
assert_eq!(warm.dep_info_runs, 1);
assert_eq!(warm.result, "local_hit");
}
#[test]
fn a_registry_unit_reading_past_its_package_keeps_the_record_local() {
let other = "pub const OTHER: &str = include_str!(\"../../other-1.0.0/lib.rs\");\n";
for lib in [
other.to_string(),
format!("{other}include!(concat!(env!(\"OUT_DIR\"), \"/gen.rs\"));\n"),
] {
let unit = OutDirUnit::new(REGISTRY_PACKAGE, false)
.with_file("../other-1.0.0/lib.rs", "other\n")
.with_file("src/lib.rs", &lib);
let warm = build_a_then_b(&unit, |_, _| {});
assert_eq!(warm.dep_info_runs, 1, "{lib}");
assert_eq!(warm.result, "local_hit");
}
}
#[test]
fn a_standalone_package_predicts_with_external_targets() {
predicts_in_another_target(OutDirUnit::new("ws/kt", false));
}
struct WorkspaceUnit {
root: TempDir,
cache: PathBuf,
lib: String,
files: Vec<(String, String)>,
proc_macro: Option<PathBuf>,
facade: Vec<PathBuf>,
external_targets: bool,
env: Vec<(&'static str, std::ffi::OsString)>,
runtime: Option<PathBuf>,
shard: Option<PathBuf>,
}
impl WorkspaceUnit {
fn new(with_macro: bool) -> Self {
let root = TempDir::new().unwrap();
let cache = root.path().join("cache");
std::fs::create_dir_all(&cache).unwrap();
let mut lib = String::new();
if with_macro {
lib.push_str("pm::answer!();\n");
}
lib.push_str("pub const SHARED: &str = include_str!(\"../../assets/shared.txt\");\n");
lib.push_str("include!(concat!(env!(\"OUT_DIR\"), \"/gen.rs\"));\n");
let proc_macro = with_macro.then(|| build_proc_macro(root.path()));
Self {
root,
cache,
lib,
files: vec![("assets/shared.txt".into(), "shared\n".into())],
proc_macro,
facade: Vec::new(),
external_targets: false,
env: Vec::new(),
runtime: None,
shard: None,
}
}
fn with_facade(self) -> Self {
self.with_facade_macro(SCAN_MACRO)
}
fn with_facade_macro(mut self, scan: &str) -> Self {
self.facade = build_facade_with(self.root.path(), scan);
self.lib = SCAN_LIB.to_string();
self
}
fn with_file(mut self, path: &str, content: &str) -> Self {
self.files.push((path.to_string(), content.to_string()));
self
}
fn with_external_targets(mut self) -> Self {
self.external_targets = true;
self
}
fn target(&self, checkout: &Path) -> PathBuf {
if self.external_targets {
self.root
.path()
.join("outputs")
.join(checkout.file_name().unwrap())
.join("target")
} else {
checkout.join("target")
}
}
fn with_lib(mut self, lib: &str) -> Self {
self.lib = lib.to_string();
self
}
fn checkout(&self, name: &str) -> PathBuf {
let checkout = self.root.path().join(name);
let write = |path: &str, content: &str| {
let path = checkout.join(path);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
};
write("Cargo.toml", "[workspace]\nmembers = [\"kt\"]\n");
write("kt/Cargo.toml", "[package]\nname = \"kt\"\n");
write("kt/src/lib.rs", &self.lib);
for (path, content) in &self.files {
write(path, content);
}
self.prepare_target(&self.target(&checkout));
checkout
}
fn prepare_target(&self, target: &Path) {
std::fs::create_dir_all(target.join("debug/deps")).unwrap();
std::fs::create_dir_all(out_dir_in(target)).unwrap();
std::fs::write(out_dir_in(target).join("gen.rs"), GENERATED).unwrap();
if let Some(proc_macro) = &self.proc_macro {
std::fs::copy(proc_macro, self.macro_in(target)).unwrap();
}
copy_into_deps(&self.facade, target);
}
fn macro_in(&self, target: &Path) -> PathBuf {
let name = self.proc_macro.as_ref().unwrap().file_name().unwrap();
target.join("debug/deps").join(name)
}
fn build(&self, checkout: &Path, predictions: bool, verify: Option<&str>) -> LastEvent {
self.build_with(checkout, predictions, verify, &[])
}
fn build_as_cargo(&self, checkout: &Path) -> LastEvent {
self.build_with(
checkout,
true,
None,
&[
"--error-format=json",
"--json=diagnostic-rendered-ansi,artifacts",
],
)
}
fn build_with(
&self,
checkout: &Path,
predictions: bool,
verify: Option<&str>,
extra: &[&str],
) -> LastEvent {
self.build_in(checkout, &self.target(checkout), predictions, verify, extra)
}
fn build_in(
&self,
checkout: &Path,
target: &Path,
predictions: bool,
verify: Option<&str>,
extra: &[&str],
) -> LastEvent {
let deps = target.join("debug/deps");
let mut args: Vec<String> = vec![
rustc_path(),
"--crate-name".into(),
"kt".into(),
"--edition=2021".into(),
"kt/src/lib.rs".into(),
"--crate-type".into(),
"lib".into(),
"--emit=dep-info,metadata,link".into(),
"-C".into(),
"metadata=fedcba9876543210".into(),
"-C".into(),
"extra-filename=-fedcba9876543210".into(),
"--out-dir".into(),
deps.display().to_string(),
"-L".into(),
format!("dependency={}", deps.display()),
];
if self.proc_macro.is_some() {
args.push("--extern".into());
args.push(format!("pm={}", self.macro_in(target).display()));
}
if !self.facade.is_empty() {
args.push("--extern".into());
args.push(format!("facade={}", deps.join("libfacade.rlib").display()));
}
args.extend(extra.iter().map(|arg| arg.to_string()));
let config_path = write_test_config_with(
&self.cache,
self.runtime.as_deref().unwrap_or(&self.cache),
self.shard.as_deref().map(|shard| (target, shard)),
predictions,
);
settle_writes(&[checkout, target]);
let mut command = std::process::Command::new(kache_binary());
command
.args(&args)
.current_dir(checkout)
.env("KACHE_CACHE_DIR", &self.cache)
.env("KACHE_CONFIG", config_path)
.env("CARGO_MANIFEST_DIR", checkout.join("kt"))
.env("OUT_DIR", out_dir_in(target))
.env_remove("KACHE_DISABLED")
.env_remove("KACHE_NAMESPACE")
.env_remove("KACHE_BASE_DIR")
.env_remove("KACHE_SOCKET_PATH")
.env_remove("KACHE_ACTIVE")
.env_remove("KACHE_FAMILY_PROBE_ACTIVE")
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER");
match verify {
Some(mode) => command.env("KACHE_VERIFY_INPUT_PREDICTIONS", mode),
None => command.env_remove("KACHE_VERIFY_INPUT_PREDICTIONS"),
};
command.envs(self.env.iter().map(|(name, value)| (name, value)));
let output = command.output().expect("failed to run kache rustc");
assert!(
output.status.success(),
"kache rustc failed.\nargs: {args:?}\nstderr: {}",
String::from_utf8_lossy(&output.stderr),
);
last_event_with(&self.cache, &self.env)
}
fn build_a_then_b(&self, prepare: impl FnOnce(&Path)) -> LastEvent {
build_kache();
let a = self.checkout("a");
let b = self.checkout("b");
prepare(&b);
assert_eq!(self.build(&a, true, None).result, "miss");
self.build(&b, true, None)
}
}
fn workspace_unit_predicts_in_another_checkout(unit: WorkspaceUnit) {
let warm = unit.build_a_then_b(|_| {});
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "B uses the record A made");
assert_eq!(warm.compiler_runs, 0);
let b = unit.root.path().join("b");
let off = unit.build(&b, false, None);
assert_eq!(off.result, "local_hit");
assert_eq!(off.dep_info_runs, 1);
assert_eq!(warm.cache_key, off.cache_key);
let verified = unit.build(&b, true, Some("always"));
assert_eq!(verified.dep_info_runs, 1);
assert_eq!(verified.prediction_mismatches, 0);
assert_eq!(verified.cache_key, off.cache_key);
}
#[test]
fn workspace_predictions_relocate_between_external_targets() {
for with_macro in [false, true] {
workspace_unit_predicts_in_another_checkout(
WorkspaceUnit::new(with_macro).with_external_targets(),
);
}
}
#[test]
fn external_target_predictions_reject_changed_generated_inputs() {
let unit = WorkspaceUnit::new(true).with_external_targets();
let warm = unit.build_a_then_b(|checkout| {
std::fs::write(
out_dir_in(&unit.target(checkout)).join("gen.rs"),
"pub const GENERATED: u32 = 123;\n",
)
.unwrap();
});
assert_ne!(warm.result, "local_hit");
assert_eq!(warm.compiler_runs, 1);
let checkout = unit.root.path().join("b");
let off = unit.build(&checkout, false, None);
assert_eq!(off.result, "local_hit");
assert_eq!(off.cache_key, warm.cache_key);
}
#[test]
fn a_rejected_record_keys_from_the_running_compile() {
let unit = WorkspaceUnit::new(true);
build_kache();
let a = unit.checkout("a");
let b = unit.checkout("b");
std::fs::write(b.join("NOTES.md"), "not in checkout a\n").unwrap();
assert_eq!(unit.build_as_cargo(&a).result, "miss");
let hit = unit.build_as_cargo(&b);
assert_eq!(hit.result, "local_hit");
assert_eq!(hit.dep_info_runs, 1);
assert_eq!(hit.compiler_runs, 0);
std::fs::write(
b.join("kt/src/lib.rs"),
format!("{}pub const EDIT: u8 = 1;\n", unit.lib),
)
.unwrap();
let miss = unit.build_as_cargo(&b);
assert_eq!(miss.result, "miss");
assert_eq!(miss.dep_info_runs, 0);
assert_eq!(miss.compiler_runs, 1);
std::fs::remove_dir_all(b.join("target/debug/deps")).unwrap();
std::fs::create_dir_all(b.join("target/debug/deps")).unwrap();
if let Some(proc_macro) = &unit.proc_macro {
std::fs::copy(proc_macro, unit.macro_in(&b.join("target"))).unwrap();
}
let checked = unit.build(&b, false, None);
assert_eq!(checked.result, "local_hit");
assert_eq!(checked.dep_info_runs, 1);
assert_eq!(checked.cache_key, miss.cache_key);
}
#[test]
fn a_workspace_unit_predicts_in_another_checkout() {
workspace_unit_predicts_in_another_checkout(WorkspaceUnit::new(false));
}
#[test]
fn a_workspace_unit_with_a_proc_macro_predicts_in_another_checkout() {
workspace_unit_predicts_in_another_checkout(WorkspaceUnit::new(true));
}
#[test]
fn a_workspace_unit_with_a_proc_macro_predicts_in_its_own_checkout() {
build_kache();
let unit = WorkspaceUnit::new(true);
let a = unit.checkout("a");
assert_eq!(unit.build(&a, true, None).result, "miss");
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0);
}
#[test]
fn a_cache_inside_the_workspace_keeps_the_record_guarded() {
build_kache();
let mut unit = WorkspaceUnit::new(false);
let a = unit.checkout("a");
unit.cache = a.join(".cache/kache");
std::fs::create_dir_all(&unit.cache).unwrap();
assert_eq!(unit.build(&a, true, None).result, "miss");
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "the record kept its guard");
}
#[test]
fn kaches_own_directories_apart_inside_the_workspace_keep_the_record_guarded() {
build_kache();
let mut unit = WorkspaceUnit::new(false);
let a = unit.checkout("a");
let own = a.join(".cache");
unit.cache = own.join("kache");
let runtime = own.join("runtime");
let shard = own.join("shard");
for dir in [&unit.cache, &runtime, &shard] {
std::fs::create_dir_all(dir).unwrap();
}
unit.runtime = Some(runtime.clone());
unit.shard = Some(shard.clone());
assert_eq!(unit.build(&a, true, None).result, "miss");
assert!(
shard.join("index.db").is_file(),
"the shard holds the store"
);
for dir in [&unit.cache, &runtime, &shard] {
std::fs::write(dir.join("written-by-another-build"), "x").unwrap();
}
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "the record kept its guard");
}
#[test]
fn a_cache_inside_the_workspace_keeps_the_record_guarded_under_a_trust_domain() {
build_kache();
let mut unit = WorkspaceUnit::new(false);
let a = unit.checkout("a");
unit.cache = a.join(".cache/kache");
std::fs::create_dir_all(&unit.cache).unwrap();
unit.env.push(("KACHE_TRUST_DOMAIN", "ci".into()));
assert_eq!(unit.build(&a, true, None).result, "miss");
let probes = unit.cache.join("probes");
std::fs::create_dir_all(&probes).unwrap();
std::fs::write(probes.join("another-unit"), "memo").unwrap();
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "the record kept its guard");
}
#[test]
fn a_cargo_home_inside_the_workspace_keeps_the_record_guarded() {
build_kache();
let mut unit = WorkspaceUnit::new(false);
let a = unit.checkout("a");
let home = a.join(".cargo");
std::fs::create_dir_all(&home).unwrap();
std::fs::write(home.join(".global-cache"), "first run").unwrap();
std::fs::write(home.join("config.toml"), "[build]\n").unwrap();
unit.env.push(("CARGO_HOME", home.clone().into()));
assert_eq!(unit.build(&a, true, None).result, "miss");
std::fs::write(home.join(".global-cache"), "second run").unwrap();
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "the record kept its guard");
std::fs::write(home.join("config.toml"), "[build]\njobs = 2\n").unwrap();
let configured = unit.build(&a, true, None);
assert_eq!(configured.result, "local_hit");
assert_eq!(
configured.dep_info_runs, 1,
"the home's configuration is part of the guard"
);
}
#[test]
fn a_new_file_anywhere_in_the_workspace_refuses_the_record() {
for with_macro in [false, true] {
let unit = WorkspaceUnit::new(with_macro);
let warm = unit.build_a_then_b(|b| {
std::fs::create_dir_all(b.join("docs")).unwrap();
std::fs::write(b.join("docs/notes.md"), "not read\n").unwrap();
});
assert_eq!(warm.dep_info_runs, 1, "with_macro={with_macro}");
assert_eq!(
warm.result, "local_hit",
"the closure itself did not change"
);
}
}
#[test]
fn a_workspace_unit_reading_outside_the_workspace_keeps_the_record_local() {
let outside = TempDir::new().unwrap();
let file = outside.path().join("outside.txt");
std::fs::write(&file, "outside\n").unwrap();
let lib = format!(
"pub const OUTSIDE: &str = include_str!({:?});\n",
file.display().to_string()
);
let unit = WorkspaceUnit::new(false).with_lib(&lib);
let warm = unit.build_a_then_b(|_| {});
assert_eq!(warm.dep_info_runs, 1);
assert_eq!(warm.result, "local_hit");
}
#[test]
fn a_generated_file_naming_the_checkout_keeps_the_record_local() {
let unit = WorkspaceUnit::new(false);
let a = unit.root.path().join("a");
let generated = format!("// generated in {}\n{GENERATED}", a.display());
let warm = unit.build_a_then_b(|b| {
let a_out = out_dir_in(&a.join("target"));
std::fs::write(a_out.join("gen.rs"), &generated).unwrap();
std::fs::write(out_dir_in(&b.join("target")).join("gen.rs"), &generated).unwrap();
});
assert_eq!(warm.dep_info_runs, 1);
assert_eq!(warm.result, "local_hit");
}
#[test]
fn a_proc_macro_member_reading_outside_the_workspace_keeps_the_pre_pass() {
build_kache();
let outside = TempDir::new().unwrap();
let file = outside.path().join("outside.txt");
std::fs::write(&file, "outside\n").unwrap();
let lib = format!(
"pm::answer!();\npub const OUTSIDE: &str = include_str!({:?});\n",
file.display().to_string()
);
let unit = WorkspaceUnit::new(true).with_lib(&lib);
let a = unit.checkout("a");
assert_eq!(unit.build(&a, true, None).result, "miss");
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 1);
}
#[test]
fn a_registry_unit_with_a_macro_predicts_in_its_own_target() {
build_kache();
let unit = OutDirUnit::new(REGISTRY_PACKAGE, true);
let a = unit.target("a");
assert_eq!(unit.build(&a, true, None).result, "miss");
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0);
}
#[test]
fn a_file_added_where_a_re_exported_macro_lists_misses() {
build_kache();
let unit = WorkspaceUnit::new(false)
.with_facade()
.with_file("kt/assets/a.txt", "a\n");
let a = unit.checkout("a");
let cold = unit.build(&a, true, None);
assert_eq!(cold.result, "miss");
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "an unchanged tree keeps its record");
std::fs::write(a.join("kt/assets/b.txt"), "b\n").unwrap();
let added = unit.build(&a, true, None);
assert_eq!(added.result, "miss");
assert_ne!(added.cache_key, cold.cache_key);
assert_eq!(added.dep_info_runs, 1);
let off = unit.build(&a, false, None);
assert_eq!(off.result, "local_hit");
assert_eq!(off.cache_key, added.cache_key);
std::fs::write(a.join("kt/assets/c.txt"), "c\n").unwrap();
let compiled = unit.build_as_cargo(&a);
assert_eq!(compiled.result, "miss");
assert_ne!(compiled.cache_key, added.cache_key);
assert_eq!(compiled.dep_info_runs, 0);
assert_eq!(compiled.compiler_runs, 1);
}
const REMOVING_SCAN_MACRO: &str = r#"extern crate proc_macro;
#[proc_macro]
pub fn scan(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_dir = std::path::Path::new(&manifest_dir);
let assets = manifest_dir.join("assets");
let marker = manifest_dir.join("../../remove-a");
if marker.exists() {
let _ = std::fs::remove_file(assets.join("a.txt"));
let _ = std::fs::remove_file(&marker);
}
let mut files: Vec<std::path::PathBuf> = std::fs::read_dir(assets)
.map(|entries| entries.flatten().map(|entry| entry.path()).collect())
.unwrap_or_default();
files.sort();
let mut code = String::from("pub const ASSETS: &[&str] = &[");
for file in files {
code.push_str(&format!("include_str!({:?}),", file.display().to_string()));
}
code.push_str("];");
code.parse().unwrap()
}
"#;
#[test]
fn a_file_removed_while_the_unit_builds_leaves_no_guarded_record() {
build_kache();
let unit = WorkspaceUnit::new(false)
.with_facade_macro(REMOVING_SCAN_MACRO)
.with_file("kt/assets/a.txt", "a\n");
let a = unit.checkout("a");
assert_eq!(unit.build(&a, true, None).result, "miss");
std::fs::write(a.join("kt/assets/b.txt"), "b\n").unwrap();
std::fs::write(unit.root.path().join("remove-a"), "").unwrap();
let removed = unit.build(&a, true, None);
assert_eq!(removed.result, "miss");
assert!(!a.join("kt/assets/a.txt").exists(), "the macro removed it");
std::fs::write(a.join("kt/assets/a.txt"), "a\n").unwrap();
let restored = unit.build(&a, true, None);
assert_eq!(restored.result, "miss");
assert_ne!(restored.cache_key, removed.cache_key);
let off = unit.build(&a, false, None);
assert_eq!(off.result, "local_hit");
assert_eq!(off.cache_key, restored.cache_key);
}
#[cfg(unix)]
#[test]
fn an_unreadable_directory_in_the_workspace_keeps_predictions() {
use std::os::unix::fs::PermissionsExt;
struct Readable(PathBuf);
impl Drop for Readable {
fn drop(&mut self) {
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o755));
}
}
build_kache();
let unit = WorkspaceUnit::new(false)
.with_facade()
.with_file("kt/assets/a.txt", "a\n")
.with_file("pgdata/PG_VERSION", "16\n");
let a = unit.checkout("a");
let pgdata = Readable(a.join("pgdata"));
std::fs::set_permissions(&pgdata.0, std::fs::Permissions::from_mode(0o000)).unwrap();
if std::fs::read_dir(&pgdata.0).is_ok() {
eprintln!("skipped: this user reads past permissions");
return;
}
assert_eq!(unit.build(&a, true, None).result, "miss");
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "the record applies");
}
fn rebuild_facade(root: &Path, scan: &Path, target: &Path) {
let out = root.join("facade-out-2");
std::fs::create_dir_all(&out).unwrap();
let source = root.join("facade-2.rs");
std::fs::write(
&source,
"pub use scan::scan;\npub fn extra() -> u32 { 42 }\n",
)
.unwrap();
let status = std::process::Command::new(rustc_path())
.args([
"--crate-name",
"facade",
"--crate-type",
"rlib",
"--edition",
"2021",
])
.arg("--extern")
.arg(format!("scan={}", scan.display()))
.arg("--out-dir")
.arg(&out)
.arg(&source)
.env_remove("RUSTC_WRAPPER")
.env_remove("CARGO_BUILD_RUSTC_WRAPPER")
.status()
.expect("run rustc to rebuild facade");
assert!(status.success(), "rebuilding facade failed");
std::fs::copy(
out.join("libfacade.rlib"),
target.join("debug/deps/libfacade.rlib"),
)
.unwrap();
}
#[test]
fn a_re_derived_record_keeps_the_tree_guard() {
build_kache();
let unit = WorkspaceUnit::new(false)
.with_facade()
.with_file("kt/assets/a.txt", "a\n");
let a = unit.checkout("a");
assert_eq!(unit.build(&a, true, None).result, "miss");
assert_eq!(unit.build(&a, true, None).dep_info_runs, 0);
rebuild_facade(unit.root.path(), &unit.facade[0], &unit.target(&a));
let changed = unit.build(&a, true, None);
assert_eq!(changed.result, "miss");
assert_eq!(changed.dep_info_runs, 1, "the predicted key is re-derived");
let next = unit.build(&a, true, None);
assert_eq!(next.result, "local_hit");
assert_eq!(
next.dep_info_runs, 0,
"the re-derived record keeps the guard"
);
}
#[test]
fn a_re_exported_macro_finds_the_first_file_of_an_empty_or_new_directory() {
build_kache();
for empty_dir in [true, false] {
let unit = WorkspaceUnit::new(false).with_facade();
let a = unit.checkout("a");
if empty_dir {
std::fs::create_dir_all(a.join("kt/assets")).unwrap();
}
let cold = unit.build(&a, true, None);
assert_eq!(cold.result, "miss");
std::fs::create_dir_all(a.join("kt/assets")).unwrap();
std::fs::write(a.join("kt/assets/first.txt"), "first\n").unwrap();
let first = unit.build(&a, true, None);
assert_eq!(first.result, "miss", "empty_dir={empty_dir}");
assert_ne!(first.cache_key, cold.cache_key, "empty_dir={empty_dir}");
}
}
#[test]
fn a_shared_row_is_refused_once_a_listed_file_is_added() {
build_kache();
let unit = WorkspaceUnit::new(false)
.with_facade()
.with_file("kt/assets/a.txt", "a\n")
.with_external_targets();
let a = unit.checkout("a");
let first = unit.root.path().join("outputs/first/target");
let second = unit.root.path().join("outputs/second/target");
unit.prepare_target(&first);
unit.prepare_target(&second);
let cold = unit.build_in(&a, &first, true, None, &[]);
assert_eq!(cold.result, "miss");
let shared = unit.build_in(&a, &second, true, None, &[]);
assert_eq!(shared.result, "local_hit");
assert_eq!(
shared.dep_info_runs, 0,
"the second target uses the shared row"
);
std::fs::write(a.join("kt/assets/b.txt"), "b\n").unwrap();
let added = unit.build_in(&a, &second, true, None, &[]);
assert_eq!(added.result, "miss");
assert_ne!(added.cache_key, cold.cache_key);
}
#[test]
fn a_registry_unit_that_links_an_rlib_takes_no_tree_guard() {
build_kache();
let unit = OutDirUnit::new(REGISTRY_PACKAGE, false)
.with_facade()
.with_file("assets/a.txt", "a\n")
.with_file("src/lib.rs", SCAN_LIB);
let a = unit.target("a");
let cold = unit.build(&a, true, None);
assert_eq!(cold.result, "miss");
std::fs::write(unit.package.join("assets/b.txt"), "b\n").unwrap();
let warm = unit.build(&a, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0);
assert_eq!(warm.cache_key, cold.cache_key);
}
#[cfg(unix)]
struct EmptyOutDirMacro {
root: TempDir,
cache: PathBuf,
package: PathBuf,
alias: bool,
proc_macro: Option<PathBuf>,
}
#[cfg(unix)]
impl EmptyOutDirMacro {
fn new(alias: bool, with_macro: bool) -> Self {
let root = TempDir::new().unwrap();
let package = root.path().join(REGISTRY_PACKAGE);
std::fs::create_dir_all(package.join("src")).unwrap();
std::fs::write(package.join("Cargo.toml"), "[package]\nname = \"kt\"\n").unwrap();
let mut lib = String::from(
"#[proc_macro]\n\
pub fn out_dir(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {\n\
\x20 format!(\"{:?}\", env!(\"OUT_DIR\")).parse().unwrap()\n\
}\n",
);
if with_macro {
lib.push_str(
"mod expanded {\n\
\x20 pm::answer!();\n\
}\n\
#[proc_macro]\n\
pub fn answer(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {\n\
\x20 expanded::answer().to_string().parse().unwrap()\n\
}\n",
);
}
std::fs::write(package.join("src/lib.rs"), lib).unwrap();
let proc_macro = with_macro.then(|| build_proc_macro(root.path()));
let cache = root.path().join("cache");
std::fs::create_dir_all(&cache).unwrap();
Self {
root,
cache,
package,
alias,
proc_macro,
}
}
fn target(&self, checkout: &str) -> PathBuf {
let target = self.root.path().join(checkout).join("target");
let out_dir = out_dir_in(&target);
std::fs::create_dir_all(&out_dir).unwrap();
std::fs::write(
out_dir.parent().unwrap().join("output"),
"cargo:rerun-if-changed=build.rs\n",
)
.unwrap();
std::fs::create_dir_all(target.join("debug/deps")).unwrap();
if let Some(proc_macro) = &self.proc_macro {
std::fs::copy(proc_macro, self.macro_in(&target)).unwrap();
}
target
}
fn macro_in(&self, target: &Path) -> PathBuf {
let name = self.proc_macro.as_ref().unwrap().file_name().unwrap();
target.join("debug/deps").join(name)
}
fn shared_out_dir(&self) -> PathBuf {
std::fs::canonicalize(&self.cache)
.unwrap()
.join("out-dirs/v1/d")
.join(BUILD_UNIT)
.join("out")
}
fn build(&self, target: &Path, predictions: bool, verify: Option<&str>) -> LastEvent {
let deps = target.join("debug/deps");
let mut args: Vec<String> = vec![
rustc_path(),
"--crate-name".into(),
"kt".into(),
"--edition=2021".into(),
self.package.join("src/lib.rs").display().to_string(),
"--crate-type".into(),
"proc-macro".into(),
"--emit=dep-info,link".into(),
"-C".into(),
"prefer-dynamic".into(),
"-C".into(),
"metadata=fedcba9876543210".into(),
"-C".into(),
"extra-filename=-fedcba9876543210".into(),
"--out-dir".into(),
deps.display().to_string(),
"-L".into(),
format!("dependency={}", deps.display()),
"--extern".into(),
"proc_macro".into(),
];
if self.proc_macro.is_some() {
args.push("--extern".into());
args.push(format!("pm={}", self.macro_in(target).display()));
}
let config_path = write_test_config(&self.cache, predictions);
let mut config = std::fs::read_to_string(&config_path).unwrap();
config.push_str(&format!("out_dir_alias = {}\n", self.alias));
std::fs::write(&config_path, config).unwrap();
let out_dir = out_dir_in(target);
let envs = [
("CARGO_MANIFEST_DIR", self.package.as_os_str()),
("CARGO_PKG_NAME", std::ffi::OsStr::new("kt")),
("OUT_DIR", out_dir.as_os_str()),
];
kache_rustc_in(
&self.package,
&self.cache,
&config_path,
&args,
&envs,
verify,
)
}
}
#[cfg(unix)]
fn predicts_on_the_shared_out_dir(with_macro: bool) {
if unsafe { libc::geteuid() } == 0 {
eprintln!("skipping the shared OUT_DIR test as root");
return;
}
build_kache();
let own = EmptyOutDirMacro::new(false, with_macro);
let (a, b) = (own.target("a"), own.target("b"));
assert_eq!(own.build(&a, true, None).result, "miss");
assert_eq!(own.build(&b, true, None).result, "miss");
assert!(!own.shared_out_dir().exists());
let unit = EmptyOutDirMacro::new(true, with_macro);
let (a, b) = (unit.target("a"), unit.target("b"));
let cold = unit.build(&a, true, None);
assert_eq!(cold.result, "miss");
assert!(unit.shared_out_dir().is_dir(), "the macro was not aliased");
let warm = unit.build(&b, true, None);
assert_eq!(warm.result, "local_hit");
assert_eq!(warm.dep_info_runs, 0, "B uses the record A made");
assert_eq!(warm.compiler_runs, 0);
assert_eq!(warm.cache_key, cold.cache_key);
let off = unit.build(&b, false, None);
assert_eq!(off.result, "local_hit");
assert_eq!(off.dep_info_runs, 1);
assert_eq!(off.cache_key, warm.cache_key);
let verified = unit.build(&b, true, Some("always"));
assert_eq!(verified.result, "local_hit");
assert_eq!(verified.dep_info_runs, 1);
assert_eq!(verified.prediction_mismatches, 0);
assert_eq!(verified.cache_key, warm.cache_key);
}
#[cfg(unix)]
#[test]
fn a_macro_on_the_shared_out_dir_predicts_in_another_target() {
predicts_on_the_shared_out_dir(false);
}
#[cfg(unix)]
#[test]
fn a_macro_expanding_a_macro_on_the_shared_out_dir_predicts_in_another_target() {
predicts_on_the_shared_out_dir(true);
}