use std::path::Path;
use std::process::Command;
fn write_project(directory: &Path) {
write_named_project(directory, "fixture");
}
fn write_named_project(directory: &Path, name: &str) {
std::fs::create_dir_all(directory.join("src")).unwrap();
std::fs::write(
directory.join("Cargo.toml"),
format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
)
.unwrap();
std::fs::write(
directory.join("src/lib.rs"),
"pub fn double(value: u32) -> u32 {\n value * 2\n}\n",
)
.unwrap();
let status = Command::new(cargo())
.current_dir(directory)
.args(["generate-lockfile", "--offline"])
.status()
.expect("cargo should run");
assert!(status.success(), "the fixture should resolve offline");
}
fn write_dependent_project(directory: &Path) {
std::fs::create_dir_all(directory.join("base/src")).unwrap();
std::fs::create_dir_all(directory.join("above/src")).unwrap();
std::fs::write(
directory.join("Cargo.toml"),
"[workspace]\nmembers = [\"base\", \"above\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::write(
directory.join("base/Cargo.toml"),
"[package]\nname = \"base\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
std::fs::write(
directory.join("base/src/lib.rs"),
"pub fn value() -> u32 { 0 }\n",
)
.unwrap();
std::fs::write(
directory.join("above/Cargo.toml"),
"[package]\nname = \"above\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\nbase = { path = \"../base\" }\n",
)
.unwrap();
std::fs::write(
directory.join("above/src/lib.rs"),
"pub fn doubled() -> u32 { base::value() * 2 }\n",
)
.unwrap();
let status = Command::new(cargo())
.current_dir(directory)
.args(["generate-lockfile", "--offline"])
.status()
.expect("cargo should run");
assert!(status.success(), "the fixture should resolve offline");
}
fn write_partially_failing_project(directory: &Path) {
std::fs::create_dir_all(directory.join("good/src")).unwrap();
std::fs::create_dir_all(directory.join("bad/src")).unwrap();
std::fs::write(
directory.join("Cargo.toml"),
"[workspace]\nmembers = [\"good\", \"bad\"]\nresolver = \"2\"\n",
)
.unwrap();
std::fs::write(
directory.join("good/Cargo.toml"),
"[package]\nname = \"good\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
std::fs::write(directory.join("good/src/lib.rs"), "pub fn good() {}\n").unwrap();
std::fs::write(
directory.join("bad/Cargo.toml"),
"[package]\nname = \"bad\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ngood = { path = \"../good\" }\n",
)
.unwrap();
std::fs::write(
directory.join("bad/src/lib.rs"),
"use good as _;\ncompile_error!(\"expected failure\");\n",
)
.unwrap();
let status = Command::new(cargo())
.current_dir(directory)
.args(["generate-lockfile", "--offline"])
.status()
.expect("cargo should run");
assert!(status.success(), "the fixture should resolve offline");
}
fn cargo() -> std::ffi::OsString {
std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())
}
#[cfg(unix)]
#[test]
fn transparent_rustc_replaces_the_shim_process() {
let directory = tempfile::tempdir().unwrap();
let shim = mbx::session::install_shim(
Path::new(env!("CARGO_BIN_EXE_mbx")),
directory.path(),
mbx::session::ShimLink::Tracking,
)
.unwrap();
let pid_file = directory.path().join("compiler.pid");
let mut child = {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let attempt = Command::new(&shim)
.arg("/bin/sh")
.args(["-c", "printf '%s' \"$$\" > \"$1\"", "sh"])
.arg(&pid_file)
.spawn();
match attempt {
Err(error)
if error.kind() == std::io::ErrorKind::ExecutableFileBusy
&& std::time::Instant::now() < deadline =>
{
std::thread::sleep(std::time::Duration::from_millis(20));
}
other => break other.unwrap(),
}
}
};
let shim_pid = child.id();
let status = child.wait().unwrap();
assert!(status.success());
assert_eq!(
std::fs::read_to_string(pid_file).unwrap(),
shim_pid.to_string()
);
}
fn build(project: &Path, store: &Path, report: &Path) -> serde_json::Value {
build_with(project, store, report, &[]).0
}
fn build_with(
project: &Path,
store: &Path,
report: &Path,
settings: &[(&str, &str)],
) -> (serde_json::Value, String) {
let mut command = Command::new(env!("CARGO_BIN_EXE_mbx"));
command
.current_dir(project)
.args(["build", "--offline"])
.env("MBX_CACHE_DIR", store)
.env("MBX_STATS_REPORT", report)
.env_remove("CARGO_TARGET_DIR")
.env_remove("MBX_INCREMENTAL")
.env_remove("CARGO_INCREMENTAL")
.env_remove("CI")
.env_remove("MBX_SHARE_OUT_DIR")
.env_remove("MBX_LEARNED_INCREMENTAL")
.env_remove("MBX_CC")
.env_remove("CC")
.env_remove("CXX")
.env_remove("HOST_CC")
.env_remove("HOST_CXX")
.env_remove("TARGET_CC")
.env_remove("TARGET_CXX")
.env_remove("MBX_REAL_CC")
.env_remove("MBX_REAL_CXX");
for (name, value) in settings {
command.env(name, value);
}
let output = command.output().expect("mbx should run");
assert!(
output.status.success(),
"build failed ({}): {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
let stats = std::fs::read(report).expect("a statistics report should be written");
(
serde_json::from_slice(&stats).expect("the report should be JSON"),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn mbx(store: &Path, arguments: &[&str]) -> String {
let output = Command::new(env!("CARGO_BIN_EXE_mbx"))
.args(arguments)
.env("MBX_CACHE_DIR", store)
.output()
.expect("mbx should run");
assert!(
output.status.success(),
"{arguments:?} failed ({}): {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn store_bytes(store: &Path) -> u64 {
tree_bytes(&store.join("actions/cas")) + tree_bytes(&store.join("actions/action-results"))
}
fn tree_bytes(directory: &Path) -> u64 {
let mut total = 0;
let mut pending = vec![directory.to_path_buf()];
while let Some(next) = pending.pop() {
let Ok(listing) = std::fs::read_dir(&next) else {
continue;
};
for entry in listing.flatten() {
let metadata = entry.metadata().expect("the entry should be readable");
if metadata.is_dir() {
pending.push(entry.path());
} else if metadata.is_file() {
total += metadata.len();
}
}
}
total
}
fn count(stats: &serde_json::Value, field: &str) -> u64 {
stats[field]
.as_u64()
.unwrap_or_else(|| panic!("{field} should be a number"))
}
fn wipe_target(project: &Path) {
let target = project.join("target");
let outputs = std::fs::read_link(&target).unwrap_or(target);
std::fs::remove_dir_all(outputs).unwrap();
}
fn corrupt_action_results(store: &Path) -> usize {
let mut corrupted = 0;
let mut pending = vec![store.join("actions/action-results")];
while let Some(directory) = pending.pop() {
let Ok(entries) = std::fs::read_dir(directory) else {
continue;
};
for entry in entries.flatten() {
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
pending.push(entry.path());
} else if std::fs::write(entry.path(), b"not json").is_ok() {
corrupted += 1;
}
}
}
corrupted
}
#[test]
fn incremental_is_opt_in_and_reaches_cargo() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build(
project.path(),
store.path(),
&reports.path().join("default.json"),
);
assert_eq!(
incremental_sessions(project.path()),
0,
"the default build should still force CARGO_INCREMENTAL=0"
);
let (stats, _) = build_with(
project.path(),
store.path(),
&reports.path().join("incremental.json"),
&[("MBX_INCREMENTAL", "1")],
);
assert!(
incremental_sessions(project.path()) > 0,
"cargo should have compiled the member incrementally: {stats}"
);
}
#[test]
fn workspace_policy_reaches_cargo() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
std::fs::write(project.path().join(".mbx.toml"), "incremental = true\n").unwrap();
let stats = build(
project.path(),
store.path(),
&reports.path().join("workspace-policy.json"),
);
assert!(
incremental_sessions(project.path()) > 0,
"the checked-in workspace policy should reach cargo: {stats}"
);
}
fn incremental_sessions(project: &Path) -> usize {
match std::fs::read_dir(project.join("target/debug/incremental")) {
Ok(entries) => entries.count(),
Err(_) => 0,
}
}
fn edit_project(project: &Path, revision: u32) {
std::fs::write(
project.join("src/lib.rs"),
format!(
"pub fn double(value: u32) -> u32 {{\n value * 2 + {revision} - {revision}\n}}\n"
),
)
.unwrap();
}
fn learned_sessions(project: &Path) -> usize {
let target = project.join("target");
let outputs = std::fs::read_link(&target).unwrap_or(target);
match std::fs::read_dir(outputs.join("mbx-incremental")) {
Ok(entries) => entries
.flatten()
.filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_dir()))
.count(),
Err(_) => 0,
}
}
fn compiled_incrementally(stats: &serde_json::Value) -> u64 {
stats["compiler"]["incremental"]["invocations"]
.as_u64()
.unwrap_or(0)
}
#[test]
fn a_churning_crate_earns_its_own_incremental_state() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
let mut stats = build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
for revision in 1..=4 {
edit_project(project.path(), revision);
stats = build(
project.path(),
store.path(),
&reports.path().join(format!("edit-{revision}.json")),
);
if revision < 3 {
assert_eq!(
compiled_incrementally(&stats),
0,
"one or two edits is not a pattern yet: {stats}"
);
}
}
assert!(
compiled_incrementally(&stats) > 0,
"the edited crate should have compiled incrementally by now: {stats}"
);
assert!(
learned_sessions(project.path()) > 0,
"it should have left incremental state behind: {stats}"
);
assert_eq!(
stats["stored_bytes"].as_u64(),
Some(0),
"an incremental artifact must never be published: {stats}"
);
}
#[test]
fn a_settled_crate_publishes_again() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
for revision in 1..=4 {
edit_project(project.path(), revision);
build(
project.path(),
store.path(),
&reports.path().join(format!("edit-{revision}.json")),
);
}
wipe_target(project.path());
let settled = build(
project.path(),
store.path(),
&reports.path().join("settled.json"),
);
assert_eq!(
compiled_incrementally(&settled),
0,
"unchanged content should compile normally: {settled}"
);
assert!(
settled["stored_bytes"].as_u64().unwrap_or(0) > 0,
"and it should be published: {settled}"
);
wipe_target(project.path());
let warm = build(
project.path(),
store.path(),
&reports.path().join("warm.json"),
);
assert!(
warm["hits"].as_u64().unwrap_or(0) > 0,
"so a later build can restore it: {warm}"
);
}
#[test]
fn a_failed_build_does_not_cost_the_streak() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
for revision in 1..=3 {
edit_project(project.path(), revision);
build(
project.path(),
store.path(),
&reports.path().join(format!("edit-{revision}.json")),
);
}
std::fs::write(project.path().join("src/lib.rs"), "fn broken( {\n").unwrap();
for attempt in 0..2 {
let failed = Command::new(env!("CARGO_BIN_EXE_mbx"))
.current_dir(project.path())
.args(["build", "--offline"])
.env("MBX_CACHE_DIR", store.path())
.env_remove("CARGO_TARGET_DIR")
.env_remove("MBX_INCREMENTAL")
.env_remove("CARGO_INCREMENTAL")
.env_remove("CI")
.output()
.expect("mbx should run");
assert!(!failed.status.success(), "attempt {attempt} should fail");
}
edit_project(project.path(), 4);
let stats = build(
project.path(),
store.path(),
&reports.path().join("recovered.json"),
);
assert!(
compiled_incrementally(&stats) > 0,
"the failures should not have reset what the edits established: {stats}"
);
}
#[test]
fn churn_earns_nothing_in_ci() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
let mut stats = build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
for revision in 1..=4 {
edit_project(project.path(), revision);
stats = build_with(
project.path(),
store.path(),
&reports.path().join(format!("edit-{revision}.json")),
&[("CI", "true")],
)
.0;
}
assert_eq!(
compiled_incrementally(&stats),
0,
"CI should have compiled every edit normally: {stats}"
);
}
#[test]
fn editing_one_crate_leaves_its_dependents_publishing() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_dependent_project(project.path());
let mut stats = build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
for revision in 1..=5 {
std::fs::write(
project.path().join("base/src/lib.rs"),
format!("pub fn value() -> u32 {{ {revision} }}\n"),
)
.unwrap();
stats = build(
project.path(),
store.path(),
&reports.path().join(format!("edit-{revision}.json")),
);
}
assert_eq!(
compiled_incrementally(&stats),
1,
"only the edited crate should be compiling incrementally: {stats}"
);
assert!(
stats["stored_bytes"].as_u64().unwrap_or(0) > 0,
"the crate above it never changed, so it should still publish: {stats}"
);
}
#[test]
fn learned_incremental_can_be_turned_off() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
let mut stats = build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
for revision in 1..=4 {
edit_project(project.path(), revision);
stats = build_with(
project.path(),
store.path(),
&reports.path().join(format!("edit-{revision}.json")),
&[("MBX_LEARNED_INCREMENTAL", "0")],
)
.0;
}
assert_eq!(
compiled_incrementally(&stats),
0,
"the setting should have kept every edit normal: {stats}"
);
}
#[test]
fn restores_a_wiped_target_directory_from_the_store() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
let cold = build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
assert!(
cold["compiler"]["unconsulted"]["duration_ns"]
.as_u64()
.is_some_and(|duration| duration > 0),
"the cold build should report time spent compiling: {cold}"
);
assert!(
cold["slow_compilations"]
.as_array()
.is_some_and(|crates| !crates.is_empty()),
"the cold build should identify its slowest uncached crates: {cold}"
);
assert_eq!(count(&cold, "hits"), 0, "a cold build cannot hit");
assert!(
count(&cold, "stored_bytes") > 0,
"a cold build should publish its outputs: {cold}"
);
assert_eq!(
count(&cold, "misses"),
0,
"a cold build looks nothing up, so it cannot miss: {cold}"
);
assert!(
count(&cold, "unconsulted") > 0,
"a cold build should report the compilations it had no key for: {cold}"
);
wipe_target(project.path());
let warm = build(
project.path(),
store.path(),
&reports.path().join("warm.json"),
);
assert!(
count(&warm, "hits") > 0,
"the rebuilt target directory should be restored: {warm}"
);
assert!(
project.path().join("target/debug/libfixture.rlib").exists(),
"the restored artifact should be materialized: {warm}"
);
assert_eq!(
count(&warm, "compiler_invocations_avoided"),
count(&warm, "hits")
);
assert!(
count(&warm, "estimated_compiler_duration_avoided_ns") > 0,
"a warm build should report the compiler time recorded by its cold build: {warm}"
);
}
#[test]
fn failed_prediction_lookups_are_timed_as_misses() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
assert!(corrupt_action_results(store.path()) > 0);
wipe_target(project.path());
let rebuilt = build(
project.path(),
store.path(),
&reports.path().join("rebuilt.json"),
);
assert!(count(&rebuilt, "lookups") > 0, "{rebuilt}");
assert!(
rebuilt["compiler"]["miss"]["invocations"]
.as_u64()
.is_some_and(|invocations| invocations > 0),
"{rebuilt}"
);
assert_eq!(count(&rebuilt, "unconsulted"), 0, "{rebuilt}");
}
#[test]
fn a_second_checkout_starts_warm() {
let store = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(first.path());
write_project(second.path());
build(
first.path(),
store.path(),
&reports.path().join("first.json"),
);
let warm = build(
second.path(),
store.path(),
&reports.path().join("second.json"),
);
assert!(
count(&warm, "hits") > 0,
"a checkout at another path should reuse the first build: {warm}"
);
}
#[derive(Clone, Copy)]
enum Generated {
Cfg,
Include,
Text,
}
fn write_generated_project(directory: &Path, generated: Generated) {
std::fs::create_dir_all(directory.join("src")).unwrap();
std::fs::write(
directory.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n [lints.rust]\nunexpected_cfgs = { level = \"allow\" }\n",
)
.unwrap();
std::fs::write(
directory.join("build.rs"),
"use std::{env, fs, path::PathBuf};\n fn main() {\n \u{20} let out = PathBuf::from(env::var(\"OUT_DIR\").unwrap());\n \u{20} fs::write(out.join(\"generated.rs\"), \"pub const VALUE: u32 = 7;\\n\").unwrap();\n \u{20} println!(\"cargo:rustc-cfg=generated\");\n }\n",
)
.unwrap();
let lib = match generated {
Generated::Cfg => "#[cfg(generated)]\npub fn value() -> u32 { 7 }\n".to_string(),
Generated::Include => {
"include!(concat!(env!(\"OUT_DIR\"), \"/generated.rs\"));\npub fn value() -> u32 { VALUE }\n"
.to_string()
}
Generated::Text => {
"include!(concat!(env!(\"OUT_DIR\"), \"/generated.rs\"));\n\
pub const WHERE: &str = env!(\"OUT_DIR\");\n\
pub fn value() -> u32 { VALUE }\n"
.to_string()
}
};
std::fs::write(directory.join("src/lib.rs"), lib).unwrap();
let status = Command::new(cargo())
.current_dir(directory)
.args(["generate-lockfile", "--offline"])
.status()
.expect("cargo should run");
assert!(status.success());
}
#[test]
fn out_dir_decides_whether_two_checkouts_share() {
for (generated, expect_hits) in [(Generated::Cfg, true), (Generated::Include, false)] {
assert_eq!(
two_checkouts_share(generated, &[]),
expect_hits,
"the default should not share a compilation that reads OUT_DIR"
);
}
}
#[test]
fn sharing_out_dir_crosses_checkouts_only_where_the_artifact_allows_it() {
let sharing = [("MBX_SHARE_OUT_DIR", "1")];
for (generated, expect_hits) in [(Generated::Include, true), (Generated::Text, false)] {
assert_eq!(
two_checkouts_share(generated, &sharing),
expect_hits,
"sharing changed the wrong shape"
);
}
}
fn two_checkouts_share(generated: Generated, settings: &[(&str, &str)]) -> bool {
let store = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_generated_project(first.path(), generated);
write_generated_project(second.path(), generated);
build_with(
first.path(),
store.path(),
&reports.path().join("first.json"),
settings,
);
let (stats, _) = build_with(
second.path(),
store.path(),
&reports.path().join("second.json"),
settings,
);
count(&stats, "hits") > 0
}
#[test]
fn an_empty_store_reports_nothing() {
let store = tempfile::tempdir().unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_mbx"))
.args(["cache", "stats"])
.env("MBX_CACHE_DIR", store.path())
.output()
.expect("mbx should run");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("objects: 0"), "unexpected output: {stdout}");
}
#[test]
fn forwards_non_build_cargo_subcommands() {
let root = tempfile::tempdir().unwrap();
let store = tempfile::tempdir().unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_mbx"))
.current_dir(root.path())
.args(["new", "--vcs", "none", "new-project"])
.env("MBX_CACHE_DIR", store.path())
.env("MBX_GC_AUTO", "0")
.env_remove("CARGO_TARGET_DIR")
.output()
.expect("mbx new should run");
assert!(
output.status.success(),
"mbx new failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(root.path().join("new-project/Cargo.toml").is_file());
let initialized = root.path().join("initialized-project");
std::fs::create_dir(&initialized).unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_mbx"))
.current_dir(&initialized)
.args(["init", "--vcs", "none"])
.env("MBX_CACHE_DIR", store.path())
.env("MBX_GC_AUTO", "0")
.env_remove("CARGO_TARGET_DIR")
.output()
.expect("mbx init should run");
assert!(
output.status.success(),
"mbx init failed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(initialized.join("Cargo.toml").is_file());
}
#[test]
fn a_build_records_the_checkout_it_ran_in() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
let records = store.path().join("actions/checkouts/v1");
assert!(
tree_bytes(&records) > 0,
"the build should record its checkout under {}",
records.display()
);
}
#[test]
fn a_failed_build_records_the_compilations_it_completed() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
write_partially_failing_project(project.path());
let output = Command::new(env!("CARGO_BIN_EXE_mbx"))
.current_dir(project.path())
.args(["build", "--workspace", "--offline"])
.env("MBX_CACHE_DIR", store.path())
.env("MBX_GC_AUTO", "0")
.env_remove("CARGO_TARGET_DIR")
.env_remove("CARGO_INCREMENTAL")
.output()
.expect("mbx should run");
assert!(!output.status.success(), "the bad member should fail");
assert!(
tree_bytes(&store.path().join("actions/task-manifests/v1")) > 0,
"the successful dependency should still be recorded as reachable"
);
}
#[test]
fn a_build_sweeps_the_store_to_its_budget() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
let (_, stderr) = build_with(
project.path(),
store.path(),
&reports.path().join("cold.json"),
&[("MBX_GC_MAX_SIZE", "1"), ("MBX_GC_INTERVAL", "0")],
);
assert!(
stderr.contains("mbx[gc]: evicted"),
"the sweep should say what it evicted: {stderr}"
);
let stats = mbx(store.path(), &["cache", "stats"]);
assert!(
stats.contains("objects: 0"),
"the store should be swept empty: {stats}"
);
}
#[test]
fn automatic_sweeps_can_be_turned_off() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
let (_, stderr) = build_with(
project.path(),
store.path(),
&reports.path().join("cold.json"),
&[
("MBX_GC_MAX_SIZE", "1"),
("MBX_GC_INTERVAL", "0"),
("MBX_GC_AUTO", "0"),
],
);
assert!(
!stderr.contains("mbx[gc]:"),
"no sweep should run: {stderr}"
);
let stats = mbx(store.path(), &["cache", "stats"]);
assert!(
!stats.contains("objects: 0"),
"the store should be left alone: {stats}"
);
}
#[test]
fn deleting_a_checkout_releases_what_only_it_used() {
let store = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
let surviving = tempfile::tempdir().unwrap();
let deleted = tempfile::tempdir().unwrap();
write_named_project(surviving.path(), "surviving");
write_named_project(deleted.path(), "deleted");
build(
surviving.path(),
store.path(),
&reports.path().join("surviving.json"),
);
let budget = store_bytes(store.path()) + 4096;
build(
deleted.path(),
store.path(),
&reports.path().join("deleted.json"),
);
std::fs::remove_dir_all(deleted.path()).unwrap();
mbx(store.path(), &["gc", "--max-size", &budget.to_string()]);
wipe_target(surviving.path());
let warm = build(
surviving.path(),
store.path(),
&reports.path().join("warm.json"),
);
assert!(
count(&warm, "hits") > 0,
"the surviving checkout should still be warm: {warm}"
);
}
mod session_events {
use super::*;
use std::path::PathBuf;
fn sessions_dir(store: &Path) -> PathBuf {
store.join("actions/sessions/v1")
}
fn stream(store: &Path) -> Vec<serde_json::Value> {
let directory = sessions_dir(store);
let mut streams: Vec<PathBuf> = std::fs::read_dir(&directory)
.expect("a session directory should exist")
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.extension()
.is_some_and(|extension| extension == "jsonl")
})
.collect();
assert_eq!(
streams.len(),
1,
"one build should record exactly one stream, found {streams:?}"
);
let contents = std::fs::read_to_string(streams.pop().unwrap()).unwrap();
contents
.lines()
.map(|line| serde_json::from_str(line).expect("every line should be JSON"))
.collect()
}
fn outcomes(events: &[serde_json::Value], kind: &str) -> usize {
events
.iter()
.filter(|event| event["type"] == "action" && event["outcome"]["kind"] == kind)
.count()
}
#[test]
fn a_build_records_its_compilations_between_a_start_and_its_totals() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
let cold = build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
let events = stream(store.path());
assert_eq!(events.first().unwrap()["type"], "session_started");
assert_eq!(
events.first().unwrap()["command"],
serde_json::json!(["build", "--offline"])
);
let last = events.last().unwrap();
assert_eq!(last["type"], "session_finished");
assert_eq!(last["stats"]["unconsulted"], cold["unconsulted"]);
assert_eq!(last["stats"]["hits"], cold["hits"]);
assert_eq!(
outcomes(&events, "unconsulted") as u64,
count(&cold, "unconsulted"),
"every unconsulted compilation should have a row: {events:?}"
);
}
#[test]
fn a_warm_build_records_a_row_per_hit_naming_its_crate() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
wipe_target(project.path());
let warm = build(
project.path(),
store.path(),
&reports.path().join("warm.json"),
);
let directory = sessions_dir(store.path());
let mut streams: Vec<PathBuf> = std::fs::read_dir(&directory)
.unwrap()
.flatten()
.map(|entry| entry.path())
.filter(|path| {
path.extension()
.is_some_and(|extension| extension == "jsonl")
})
.collect();
streams.sort();
let contents = std::fs::read_to_string(streams.last().unwrap()).unwrap();
let events: Vec<serde_json::Value> = contents
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect();
let hits = count(&warm, "hits");
assert!(hits > 0, "the warm build should hit: {warm}");
assert_eq!(
outcomes(&events, "hit") as u64,
hits,
"every hit should have a row: {events:?}"
);
assert!(
events.iter().any(|event| {
event["outcome"]["kind"] == "hit" && event["crate_name"] == "fixture"
}),
"a hit row should name the crate it restored: {events:?}"
);
}
#[test]
fn recording_can_be_turned_off() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build_with(
project.path(),
store.path(),
&reports.path().join("cold.json"),
&[("MBX_EVENTS", "0")],
);
assert!(
!sessions_dir(store.path()).exists(),
"a build that records nothing should leave no session directory"
);
}
}
#[cfg(unix)]
mod target_views {
use super::*;
fn managed(project: &Path) -> std::path::PathBuf {
std::fs::read_link(project.join("target")).expect("target should be a link")
}
#[test]
fn a_managed_target_directory_keeps_the_workspace_clean() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build(
project.path(),
store.path(),
&reports.path().join("cold.json"),
);
let directory = managed(project.path());
assert!(
directory.starts_with(store.path().join("targets")),
"outputs should land under the managed root, not the workspace: {}",
directory.display()
);
assert!(
project.path().join("target/debug/libfixture.rlib").exists(),
"the workspace should still reach its own build outputs"
);
}
#[test]
fn a_managed_target_directory_still_hits_the_cache() {
let store = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(first.path());
write_project(second.path());
build(
first.path(),
store.path(),
&reports.path().join("first.json"),
);
let (warm, _) = build_with(
second.path(),
store.path(),
&reports.path().join("second.json"),
&[("MBX_TARGET_VIEWS", "1")],
);
assert!(
count(&warm, "hits") > 0,
"a relocated target directory should still reuse the first build: {warm}"
);
}
#[test]
fn deleting_a_checkout_frees_its_managed_target_directory() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build_with(
project.path(),
store.path(),
&reports.path().join("cold.json"),
&[("MBX_TARGET_VIEWS", "1")],
);
let directory = managed(project.path());
assert!(tree_bytes(&directory) > 0);
std::fs::remove_dir_all(project.path()).unwrap();
let output = mbx(store.path(), &["gc", "--max-size", "20GiB"]);
assert!(
!directory.exists(),
"the target directory of a checkout that is gone should be freed"
);
assert!(
output.contains("freed 1 target directories"),
"gc should say what it freed: {output}"
);
}
#[test]
fn a_store_sweep_failure_still_frees_managed_target_directories() {
let store = tempfile::tempdir().unwrap();
let gone = tempfile::tempdir().unwrap();
let current = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_named_project(gone.path(), "gone");
write_named_project(current.path(), "current");
build_with(
gone.path(),
store.path(),
&reports.path().join("gone.json"),
&[("MBX_TARGET_VIEWS", "1")],
);
let directory = managed(gone.path());
std::fs::remove_dir_all(gone.path()).unwrap();
let cas = store.path().join("actions/cas/v1");
std::fs::remove_dir_all(&cas).unwrap();
std::fs::write(&cas, b"not a directory").unwrap();
let (_, stderr) = build_with(
current.path(),
store.path(),
&reports.path().join("current.json"),
&[("MBX_TARGET_VIEWS", "1"), ("MBX_GC_INTERVAL", "0")],
);
assert!(stderr.contains("the store was not swept"), "{stderr}");
assert!(
!directory.exists(),
"target collection should still run when store collection fails"
);
}
#[test]
fn explicit_gc_still_frees_targets_when_store_collection_fails() {
let store = tempfile::tempdir().unwrap();
let gone = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(gone.path());
build_with(
gone.path(),
store.path(),
&reports.path().join("gone.json"),
&[("MBX_TARGET_VIEWS", "1")],
);
let directory = managed(gone.path());
std::fs::remove_dir_all(gone.path()).unwrap();
let cas = store.path().join("actions/cas/v1");
std::fs::remove_dir_all(&cas).unwrap();
std::fs::write(&cas, b"not a directory").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_mbx"))
.args(["gc", "--max-size", "20GiB"])
.env("MBX_CACHE_DIR", store.path())
.output()
.expect("mbx should run");
assert!(
!output.status.success(),
"the broken store should be reported"
);
assert!(
String::from_utf8_lossy(&output.stdout).contains("freed 1 target directories"),
"successful target collection should still be reported"
);
assert!(
!directory.exists(),
"explicit gc should collect targets independently of the store"
);
}
#[test]
fn a_noninteractive_build_never_removes_a_real_target_directory() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
write_project(project.path());
build_with(
project.path(),
store.path(),
&reports.path().join("cold.json"),
&[("MBX_TARGET_VIEWS", "0")],
);
assert!(project.path().join("target").is_dir());
build(
project.path(),
store.path(),
&reports.path().join("warm.json"),
);
assert!(
std::fs::read_link(project.path().join("target")).is_err(),
"existing build outputs are not ours to move"
);
assert!(project.path().join("target/debug/libfixture.rlib").exists());
}
}
fn build_into_target(
project: &Path,
store: &Path,
target: &Path,
settings: &[(&str, &str)],
) -> String {
let mut command = Command::new(env!("CARGO_BIN_EXE_mbx"));
command
.current_dir(project)
.args(["build", "--offline"])
.env("MBX_CACHE_DIR", store)
.env("CARGO_TARGET_DIR", target)
.env_remove("MBX_INCREMENTAL")
.env_remove("CARGO_INCREMENTAL")
.env_remove("MBX_LEARNED_INCREMENTAL")
.env_remove("CI")
.env_remove("MBX_SHARE_OUT_DIR");
for (name, value) in settings {
command.env(name, value);
}
let output = command.output().expect("mbx should run");
assert!(
output.status.success(),
"build failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn dep_info_contents(target: &Path) -> String {
let deps = target.join("debug/deps");
let entry = std::fs::read_dir(&deps)
.expect("deps directory should exist")
.filter_map(Result::ok)
.find(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
name.starts_with("fixture-") && name.ends_with(".d")
})
.expect("the library's dep-info should exist");
std::fs::read_to_string(entry.path()).expect("dep-info should be readable")
}
#[test]
fn a_restored_dep_info_names_the_target_directory_it_was_restored_into() {
let store = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
write_project(project.path());
build_into_target(project.path(), store.path(), first.path(), &[]);
build_into_target(project.path(), store.path(), second.path(), &[]);
let restored = dep_info_contents(second.path());
let foreign = first.path().to_string_lossy().into_owned();
assert!(
!restored.contains(&foreign),
"restored dep-info still names the publishing target directory:\n{restored}"
);
assert!(
restored.contains(&*second.path().to_string_lossy()),
"restored dep-info should name this target directory:\n{restored}"
);
}
#[cfg(unix)]
#[test]
fn verification_is_clean_across_target_directories() {
let store = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
write_project(project.path());
build_into_target(project.path(), store.path(), first.path(), &[]);
let report = reports.path().join("verify.json");
let stderr = build_into_target(
project.path(),
store.path(),
second.path(),
&[
("MBX_VERIFY", "1"),
("MBX_STATS_REPORT", report.to_str().unwrap()),
],
);
let reported = stderr
.lines()
.filter(|line| line.contains("diverged"))
.collect::<Vec<_>>()
.join("\n");
let stats: serde_json::Value =
serde_json::from_slice(&std::fs::read(&report).expect("a report should be written"))
.expect("the report should be JSON");
assert!(
count(&stats, "verifications") > 0,
"the run should have verified something: {stats}"
);
assert_eq!(
count(&stats, "divergences"),
0,
"a restore into another target directory diverged: {reported}"
);
}
#[cfg(unix)]
fn write_c_project(directory: &Path) {
std::fs::create_dir_all(directory.join("src")).unwrap();
std::fs::create_dir_all(directory.join("include")).unwrap();
std::fs::write(
directory.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
std::fs::write(
directory.join("include/hello.h"),
"int hello_value(void);\n",
)
.unwrap();
std::fs::write(
directory.join("src/hello.c"),
"#include \"hello.h\"\nint hello_value(void) { return 7; }\n",
)
.unwrap();
std::fs::write(
directory.join("build.rs"),
r#"use std::{env, path::PathBuf, process::Command};
fn main() {
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
// The `cc` crate reads HOST_CC before CC when host and target agree, and
// that is the variable mbx sets; mirroring its precedence is what makes
// this fixture exercise the same path a real build script takes.
let compiler = env::var("HOST_CC")
.or_else(|_| env::var("CC"))
.unwrap_or_else(|_| "cc".into());
let status = Command::new(&compiler)
.arg("-O2")
.arg("-Iinclude")
.arg("-c")
.arg("-o")
.arg(out.join("hello.o"))
.arg("src/hello.c")
.status()
.expect("the C compiler should run");
assert!(status.success(), "the fixture's C should compile");
println!("cargo:rerun-if-changed=src/hello.c");
}
"#,
)
.unwrap();
std::fs::write(
directory.join("src/lib.rs"),
"pub fn value() -> u32 { 7 }\n",
)
.unwrap();
let status = Command::new(cargo())
.current_dir(directory)
.args(["generate-lockfile", "--offline"])
.status()
.expect("cargo should run");
assert!(status.success(), "the fixture should resolve offline");
}
#[cfg(unix)]
fn has_c_compiler() -> bool {
let Ok(directory) = tempfile::tempdir() else {
return false;
};
let source = directory.path().join("probe.c");
if std::fs::write(&source, "int probe(void) { return 0; }\n").is_err() {
return false;
}
Command::new("cc")
.arg("-c")
.arg("-o")
.arg(directory.path().join("probe.o"))
.arg(&source)
.output()
.is_ok_and(|output| output.status.success())
}
#[cfg(unix)]
fn warm_checkout_hits(settings: &[(&str, &str)]) -> u64 {
let store = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
write_c_project(first.path());
write_c_project(second.path());
build_with(
first.path(),
store.path(),
&reports.path().join("first.json"),
settings,
);
let (warm, _) = build_with(
second.path(),
store.path(),
&reports.path().join("second.json"),
settings,
);
count(&warm, "hits")
}
#[cfg(unix)]
#[test]
fn a_build_script_c_compilation_crosses_checkouts() {
if !has_c_compiler() {
return;
}
let cached = warm_checkout_hits(&[]);
let uncached = warm_checkout_hits(&[("MBX_CC", "0")]);
assert_eq!(
cached,
uncached + 1,
"the C compilation should be exactly one more restored action"
);
}
#[cfg(unix)]
fn write_generated_header_project(directory: &Path) {
std::fs::create_dir_all(directory.join("src")).unwrap();
std::fs::write(
directory.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
for name in ["a", "b", "c"] {
std::fs::write(
directory.join(format!("src/{name}.c")),
format!("#include \"config.h\"\nint {name}(void) {{ return CONFIG_V; }}\n"),
)
.unwrap();
}
std::fs::write(
directory.join("build.rs"),
r##"use std::{env, fs, path::PathBuf, process::Command};
fn main() {
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
fs::write(out.join("config.h"), "#define CONFIG_V 7
").unwrap();
let compiler = env::var("HOST_CC")
.or_else(|_| env::var("CC"))
.unwrap_or_else(|_| "cc".into());
for name in ["a", "b", "c"] {
let status = Command::new(&compiler)
.arg(format!("-I{}", out.display()))
.arg("-c")
.arg("-o")
.arg(out.join(format!("{name}.o")))
.arg(format!("src/{name}.c"))
.status()
.expect("the C compiler should run");
assert!(status.success());
}
}
"##,
)
.unwrap();
std::fs::write(
directory.join("src/lib.rs"),
"pub fn value() -> u32 { 7 }\n",
)
.unwrap();
let status = Command::new(cargo())
.current_dir(directory)
.args(["generate-lockfile", "--offline"])
.status()
.expect("cargo should run");
assert!(status.success(), "the fixture should resolve offline");
}
#[cfg(unix)]
#[test]
fn objects_landing_beside_a_generated_header_still_cross_checkouts() {
if !has_c_compiler() {
return;
}
let store = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
write_generated_header_project(first.path());
write_generated_header_project(second.path());
build_with(
first.path(),
store.path(),
&reports.path().join("cold.json"),
&[],
);
let (warm, _) = build_with(
second.path(),
store.path(),
&reports.path().join("warm.json"),
&[],
);
assert_eq!(
count(&warm, "hits"),
4,
"all three C compilations and the Rust one should be restored: {warm}"
);
}
#[cfg(unix)]
#[test]
fn a_restored_c_diagnostic_names_the_checkout_it_is_replayed_in() {
if !has_c_compiler() {
return;
}
let store = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
write_warning_project(first.path());
write_warning_project(second.path());
build_with(
first.path(),
store.path(),
&reports.path().join("cold.json"),
&[],
);
let (warm, stderr) = build_with(
second.path(),
store.path(),
&reports.path().join("warm.json"),
&[],
);
assert!(
count(&warm, "hits") > 0,
"the second checkout should have restored the compilation: {warm}"
);
let diagnostic = stderr
.lines()
.find(|line| line.contains("CCWARN>>>"))
.unwrap_or_default();
assert!(
!diagnostic.contains(&first.path().display().to_string()),
"a replayed warning must not name the checkout that published it: {diagnostic}"
);
}
#[cfg(unix)]
fn write_warning_project(directory: &Path) {
std::fs::create_dir_all(directory.join("src")).unwrap();
std::fs::write(
directory.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
std::fs::write(
directory.join("build.rs"),
r##"use std::{env, fs, path::PathBuf, process::Command};
fn main() {
let out = PathBuf::from(env::var("OUT_DIR").unwrap());
let source = out.join("gen.c");
fs::write(&source, "int value(void) { int unused = 1; return 7; }
").unwrap();
let compiler = env::var("HOST_CC")
.or_else(|_| env::var("CC"))
.unwrap_or_else(|_| "cc".into());
let output = Command::new(&compiler)
.arg("-Wall")
.arg("-c")
.arg("-o")
.arg(out.join("gen.o"))
.arg(&source)
.output()
.expect("the C compiler should run");
for line in String::from_utf8_lossy(&output.stderr).lines() {
println!("cargo:warning=CCWARN>>>{line}");
}
assert!(output.status.success());
}
"##,
)
.unwrap();
std::fs::write(
directory.join("src/lib.rs"),
"pub fn value() -> u32 { 7 }\n",
)
.unwrap();
let status = Command::new(cargo())
.current_dir(directory)
.args(["generate-lockfile", "--offline"])
.status()
.expect("cargo should run");
assert!(status.success(), "the fixture should resolve offline");
}
#[cfg(unix)]
#[test]
fn a_stale_prediction_recovers_instead_of_stranding_the_compilation() {
if !has_c_compiler() {
return;
}
let store = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
let project = tempfile::tempdir().unwrap();
write_c_project(project.path());
build_with(
project.path(),
store.path(),
&reports.path().join("cold.json"),
&[],
);
std::fs::remove_file(project.path().join("include/hello.h")).unwrap();
std::fs::write(
project.path().join("src/hello.c"),
"int hello_value(void) { return 7; }\n",
)
.unwrap();
let (recovered, _) = build_with(
project.path(),
store.path(),
&reports.path().join("recover.json"),
&[],
);
assert_eq!(
recovered["bypasses"].get("cc-input-read"),
None,
"a stale prediction is not a bypass: {recovered}"
);
let second = tempfile::tempdir().unwrap();
write_c_project(second.path());
std::fs::remove_file(second.path().join("include/hello.h")).unwrap();
std::fs::write(
second.path().join("src/hello.c"),
"int hello_value(void) { return 7; }\n",
)
.unwrap();
let (warm, _) = build_with(
second.path(),
store.path(),
&reports.path().join("warm.json"),
&[],
);
assert_eq!(
count(&warm, "hits"),
2,
"both the Rust and the C compilation should be restored: {warm}"
);
}
#[cfg(unix)]
#[test]
fn an_existing_cc_setting_is_left_alone() {
if !has_c_compiler() {
return;
}
let chosen = which::which("cc").expect("cc should resolve");
let preset = warm_checkout_hits(&[("HOST_CC", chosen.to_str().unwrap())]);
let uncached = warm_checkout_hits(&[("MBX_CC", "0")]);
assert_eq!(
preset, uncached,
"mbx should not have intercepted a compiler the build chose"
);
}
#[cfg(unix)]
#[test]
fn a_restored_object_is_byte_identical_to_a_compiled_one() {
if !has_c_compiler() {
return;
}
let store = tempfile::tempdir().unwrap();
let reports = tempfile::tempdir().unwrap();
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
write_c_project(first.path());
write_c_project(second.path());
build_with(
first.path(),
store.path(),
&reports.path().join("first.json"),
&[],
);
build_with(
second.path(),
store.path(),
&reports.path().join("second.json"),
&[],
);
let compiled = find_object(first.path()).expect("the first checkout should have an object");
let restored = find_object(second.path()).expect("the second checkout should have an object");
assert_eq!(
std::fs::read(&compiled).unwrap(),
std::fs::read(&restored).unwrap(),
"a restored object must match the one that was compiled"
);
}
#[cfg(unix)]
fn find_object(project: &Path) -> Option<std::path::PathBuf> {
let target = project.join("target");
let root = std::fs::read_link(&target).unwrap_or(target);
let mut pending = vec![root];
while let Some(directory) = pending.pop() {
for entry in std::fs::read_dir(&directory).ok()? {
let entry = entry.ok()?;
let path = entry.path();
if path.is_dir() {
pending.push(path);
} else if path.file_name().is_some_and(|name| name == "hello.o") {
return Some(path);
}
}
}
None
}