#![cfg(unix)]
use mitts::adapter::rust::toolchain_probes::{
file_identity, is_rustup_proxy, load_or_probe, rustup_which_rustc, set_cache_root,
};
use std::cell::Cell;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
fn real_rustc() -> PathBuf {
if let Ok(out) = Command::new("rustup").args(["which", "rustc"]).output() {
if out.status.success() {
let p = PathBuf::from(String::from_utf8_lossy(&out.stdout).trim());
if p.is_file() {
return p;
}
}
}
std::env::split_paths(&std::env::var_os("PATH").unwrap())
.map(|d| d.join("rustc"))
.find(|p| p.is_file())
.expect("a rustc on PATH")
}
fn write_script(path: &Path, body: &str) {
fs::write(path, body).unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
}
fn rustc_wrapper(dir: &Path, log: &Path) -> PathBuf {
let wrapper = dir.join("rustc");
write_script(
&wrapper,
&format!(
"#!/bin/sh\necho \"$*\" >> '{}'\nexec '{}' \"$@\"\n",
log.display(),
real_rustc().display()
),
);
wrapper
}
fn write_project(dir: &Path) -> PathBuf {
fs::create_dir_all(dir.join("app/src")).unwrap();
fs::create_dir_all(dir.join("helper/src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
"[workspace]\nmembers = [\"app\", \"helper\"]\nresolver = \"2\"\n",
)
.unwrap();
fs::write(
dir.join("app/Cargo.toml"),
"[package]\nname = \"probe_app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[target.'cfg(unix)'.dependencies]\nhelper = { path = \"../helper\" }\n",
)
.unwrap();
fs::write(
dir.join("app/src/main.rs"),
"fn main() { helper::hello(); }\n",
)
.unwrap();
fs::write(
dir.join("helper/Cargo.toml"),
"[package]\nname = \"helper\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
fs::write(dir.join("helper/src/lib.rs"), "pub fn hello() {}\n").unwrap();
dir.join("Cargo.toml")
}
fn mitts_build(manifest: &Path, cache: &Path, env: &[(&str, &str)], drop_rustc: bool) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mitts"));
cmd.arg("build")
.arg("--manifest-path")
.arg(manifest)
.arg("--cache-dir")
.arg(cache)
.current_dir(manifest.parent().unwrap())
.env("CI", "true")
.env_remove("MITTS_CACHE_DIR");
if drop_rustc {
cmd.env_remove("RUSTC");
}
for (k, v) in env {
cmd.env(k, v);
}
let out = cmd.output().expect("run mitts");
assert!(
out.status.success(),
"mitts build failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
}
fn count_lines(log: &Path, pred: impl Fn(&str) -> bool) -> usize {
fs::read_to_string(log)
.unwrap_or_default()
.lines()
.filter(|l| pred(l))
.count()
}
fn bump_mtime(path: &Path) {
let later = SystemTime::now() + Duration::from_secs(10);
fs::File::options()
.write(true)
.open(path)
.unwrap()
.set_modified(later)
.unwrap();
}
#[test]
fn probe_cache_rustc_version_and_cfg_run_once_per_compiler_binary() {
let temp = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let log = temp.path().join("rustc.log");
let wrapper = rustc_wrapper(temp.path(), &log);
let manifest = write_project(&temp.path().join("app"));
let env = [("RUSTC", wrapper.to_str().unwrap())];
let vv = |l: &str| l == "-vV";
let cfg = |l: &str| l.starts_with("--print cfg");
mitts_build(&manifest, cache.path(), &env, false);
assert!(count_lines(&log, vv) >= 1, "first build must probe -vV");
assert!(
count_lines(&log, cfg) >= 1,
"first build must probe --print cfg"
);
let entries: Vec<_> = fs::read_dir(cache.path().join("toolchain"))
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
assert!(
entries.iter().any(|n| n.starts_with("rustc-vV-"))
&& entries.iter().any(|n| n.starts_with("rustc-cfg-")),
"probe entries missing: {entries:?}"
);
let _ = fs::remove_file(&log); mitts_build(&manifest, cache.path(), &env, false);
assert_eq!(count_lines(&log, vv), 0, "second build spawned rustc -vV");
assert_eq!(
count_lines(&log, cfg),
0,
"second build spawned rustc --print cfg"
);
bump_mtime(&wrapper);
let _ = fs::remove_file(&log); mitts_build(&manifest, cache.path(), &env, false);
assert!(
count_lines(&log, vv) >= 1,
"a changed rustc must be probed again"
);
assert!(count_lines(&log, cfg) >= 1);
}
#[test]
fn probe_cache_rustup_which_runs_once_per_rustup_and_selection() {
let temp = TempDir::new().unwrap();
let cache = TempDir::new().unwrap();
let log = temp.path().join("rustup.log");
let bin = temp.path().join("bin");
fs::create_dir_all(&bin).unwrap();
write_script(
&bin.join("rustup"),
&format!(
"#!/bin/sh\necho \"$*\" >> '{}'\nif [ \"$1\" = which ] && [ \"$2\" = rustc ]; then echo '{}'; exit 0; fi\nexit 1\n",
log.display(),
real_rustc().display()
),
);
let path = format!(
"{}:{}",
bin.display(),
std::env::var("PATH").unwrap_or_default()
);
let manifest = write_project(&temp.path().join("app"));
let env = [("PATH", path.as_str())];
let which = |l: &str| l == "which rustc";
mitts_build(&manifest, cache.path(), &env, true);
assert_eq!(
count_lines(&log, which),
1,
"first build resolves rustc once"
);
let _ = fs::remove_file(&log); mitts_build(&manifest, cache.path(), &env, true);
assert_eq!(
count_lines(&log, which),
0,
"second build spawned rustup which"
);
fs::write(
temp.path().join("app/rust-toolchain.toml"),
"[toolchain]\nchannel = \"stable\"\n",
)
.unwrap();
let _ = fs::remove_file(&log); mitts_build(&manifest, cache.path(), &env, true);
assert_eq!(
count_lines(&log, which),
1,
"a rust-toolchain file must re-resolve"
);
}
#[test]
fn probe_cache_primitives_follow_key_identity_and_tolerate_damage() {
let cache = TempDir::new().unwrap();
set_cache_root(cache.path());
let calls = Cell::new(0u32);
let probe = || -> Result<String, ()> {
calls.set(calls.get() + 1);
Ok(format!("answer{}", calls.get()))
};
assert_eq!(load_or_probe("t", "k1", true, probe).unwrap(), "answer1");
assert_eq!(load_or_probe("t", "k1", true, probe).unwrap(), "answer1");
assert_eq!(calls.get(), 1, "a cached key must not probe");
assert_eq!(load_or_probe("t", "k2", true, probe).unwrap(), "answer2");
assert_eq!(load_or_probe("t", "k1", false, probe).unwrap(), "answer3");
assert_eq!(calls.get(), 3, "an uncacheable probe always runs");
assert!(load_or_probe("t", "k3", true, || Err::<String, ()>(())).is_err());
assert_eq!(
load_or_probe("t", "k3", true, probe).unwrap(),
"answer4",
"a failure is not stored"
);
let dir = cache.path().join("toolchain");
for entry in fs::read_dir(&dir).unwrap().flatten() {
fs::write(entry.path(), b"{ not json").unwrap();
}
assert_eq!(load_or_probe("t", "k1", true, probe).unwrap(), "answer5");
assert_eq!(load_or_probe("t", "k1", true, probe).unwrap(), "answer5");
let f = cache.path().join("rustc");
fs::write(&f, b"x").unwrap();
let before = file_identity(&f);
bump_mtime(&f);
assert_ne!(before, file_identity(&f));
assert!(!is_rustup_proxy(&f));
fs::hard_link(&f, cache.path().join("rustup")).unwrap();
assert!(is_rustup_proxy(&f));
let bin = cache.path().join("bin");
fs::create_dir_all(&bin).unwrap();
let fake_rustc = bin.join("fake-rustc");
fs::write(&fake_rustc, b"old").unwrap();
let log = cache.path().join("which.log");
write_script(
&bin.join("rustup"),
&format!(
"#!/bin/sh\necho \"$*\" >> '{}'\necho '{}'\n",
log.display(),
fake_rustc.display()
),
);
assert_eq!(rustup_which_rustc(&bin.join("rustup")).unwrap(), fake_rustc);
assert_eq!(rustup_which_rustc(&bin.join("rustup")).unwrap(), fake_rustc);
assert_eq!(count_lines(&log, |l| l == "which rustc"), 1);
bump_mtime(&fake_rustc);
assert_eq!(rustup_which_rustc(&bin.join("rustup")).unwrap(), fake_rustc);
assert_eq!(
count_lines(&log, |l| l == "which rustc"),
2,
"a replaced toolchain re-resolves"
);
assert_eq!(rustup_which_rustc(&bin.join("rustup")).unwrap(), fake_rustc);
assert_eq!(
count_lines(&log, |l| l == "which rustc"),
2,
"and the fresh answer is cached"
);
}