use mitts::adapter::{RustAdapter, RustBuildOptions};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
const STORES: [&str; 6] = ["cas", "ac", "locks", "jobserver", "sources", "index"];
fn copy_dir(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if entry.file_name() == "target" {
continue;
}
let dest = dst.join(entry.file_name());
if path.is_dir() {
copy_dir(&path, &dest);
} else {
fs::copy(&path, &dest).unwrap();
}
}
}
fn project_needing_resolution(root: &Path) -> PathBuf {
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("fixtures")
.join("hello-rust");
let project = root.join("project");
copy_dir(&src, &project);
let _ = fs::remove_file(project.join("Cargo.lock"));
project.join("Cargo.toml")
}
struct Sandbox {
home: TempDir,
}
impl Sandbox {
fn new() -> Self {
Self {
home: TempDir::new().unwrap(),
}
}
fn default_store(&self) -> PathBuf {
self.home.path().join(".cache").join("mitts")
}
fn xdg_store(&self) -> PathBuf {
self.home.path().join("xdg").join("mitts")
}
fn mitts(&self) -> Command {
let real_home = std::env::var_os("HOME").map(PathBuf::from);
let cargo_home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| real_home.as_ref().map(|h| h.join(".cargo")))
.expect("HOME or CARGO_HOME");
let rustup_home = std::env::var_os("RUSTUP_HOME")
.map(PathBuf::from)
.or_else(|| real_home.as_ref().map(|h| h.join(".rustup")))
.expect("HOME or RUSTUP_HOME");
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mitts"));
cmd.env("HOME", self.home.path())
.env("XDG_CACHE_HOME", self.home.path().join("xdg"))
.env("CARGO_HOME", cargo_home)
.env("RUSTUP_HOME", rustup_home)
.env_remove("MITTS_CACHE_DIR");
cmd
}
}
fn stores_under(root: &Path) -> Vec<&'static str> {
STORES
.iter()
.copied()
.filter(|name| root.join(name).is_dir())
.collect()
}
fn assert_build_ok(output: std::process::Output) {
assert!(
output.status.success(),
"mitts build failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn assert_every_store_under(root: &Path) {
let present = stores_under(root);
for name in ["cas", "ac", "sources", "index"] {
assert!(
present.contains(&name),
"{name} missing under {}: found {present:?}",
root.display()
);
}
}
#[test]
fn cache_dir_flag_relocates_every_store() {
let sandbox = Sandbox::new();
let work = TempDir::new().unwrap();
let manifest = project_needing_resolution(work.path());
let cache = work.path().join("cache");
let output = sandbox
.mitts()
.arg("build")
.arg("--manifest-path")
.arg(&manifest)
.arg("--cache-dir")
.arg(&cache)
.output()
.unwrap();
assert_build_ok(output);
assert_every_store_under(&cache);
assert!(
!sandbox.default_store().exists(),
"build leaked into ~/.cache/mitts"
);
assert!(
!sandbox.xdg_store().exists(),
"build leaked into $XDG_CACHE_HOME/mitts"
);
}
#[test]
fn mitts_cache_dir_env_relocates_every_store() {
let sandbox = Sandbox::new();
let work = TempDir::new().unwrap();
let manifest = project_needing_resolution(work.path());
let cache = work.path().join("env_cache");
let output = sandbox
.mitts()
.env("MITTS_CACHE_DIR", &cache)
.arg("build")
.arg("--manifest-path")
.arg(&manifest)
.output()
.unwrap();
assert_build_ok(output);
assert_every_store_under(&cache);
assert!(
!sandbox.default_store().exists(),
"build leaked into ~/.cache/mitts"
);
assert!(
!sandbox.xdg_store().exists(),
"build leaked into $XDG_CACHE_HOME/mitts"
);
}
#[test]
fn xdg_cache_home_is_honoured_by_every_store() {
let sandbox = Sandbox::new();
let work = TempDir::new().unwrap();
let manifest = project_needing_resolution(work.path());
let output = sandbox
.mitts()
.arg("build")
.arg("--manifest-path")
.arg(&manifest)
.output()
.unwrap();
assert_build_ok(output);
assert_every_store_under(&sandbox.xdg_store());
assert!(
!sandbox.default_store().exists(),
"a store ignored XDG_CACHE_HOME and fell back to ~/.cache/mitts"
);
}
#[test]
fn explain_with_cache_dir_keeps_index_and_sources_under_it() {
let sandbox = Sandbox::new();
let work = TempDir::new().unwrap();
let manifest = project_needing_resolution(work.path());
let cache = work.path().join("explain_cache");
let output = sandbox
.mitts()
.arg("explain")
.arg("--manifest-path")
.arg(&manifest)
.arg("--cache-dir")
.arg(&cache)
.output()
.unwrap();
assert!(
output.status.success(),
"mitts explain failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let present = stores_under(&cache);
assert!(
present.contains(&"index"),
"index cache not under --cache-dir: {present:?}"
);
assert!(
present.contains(&"sources"),
"sources not under --cache-dir: {present:?}"
);
assert!(!sandbox.default_store().exists());
assert!(!sandbox.xdg_store().exists());
}
fn dependency_free_project(root: &Path) -> PathBuf {
let project = root.join("plain");
fs::create_dir_all(project.join("src")).unwrap();
fs::write(
project.join("Cargo.toml"),
"[package]\nname = \"plain\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[workspace]\n",
)
.unwrap();
fs::write(project.join("src/lib.rs"), "pub fn answer() -> u8 { 42 }\n").unwrap();
project.join("Cargo.toml")
}
#[test]
fn fast_lock_stays_active_with_explicit_cache_dir_only_when_asked() {
let work = TempDir::new().unwrap();
let manifest = dependency_free_project(work.path());
let cache = work.path().join("cache");
let asked = RustBuildOptions {
manifest_path: Some(manifest.clone()),
cache_dir: Some(cache.clone()),
fast_lock: Some(true),
..Default::default()
};
let (_, first) = RustAdapter::build(&asked).unwrap();
assert!(first.success, "{}", first.stderr);
let (_, second) = RustAdapter::build(&asked).unwrap();
assert!(second.success, "{}", second.stderr);
assert_eq!(
second.action_id, "fast-lock-hit",
"fast lock must answer the no-op build when fast_lock is Some(true)"
);
let not_asked = RustBuildOptions {
manifest_path: Some(manifest),
target_dir: Some(work.path().join("other_target")),
cache_dir: Some(cache),
..Default::default()
};
let (_, first) = RustAdapter::build(¬_asked).unwrap();
assert!(first.success, "{}", first.stderr);
let (_, second) = RustAdapter::build(¬_asked).unwrap();
assert!(second.success, "{}", second.stderr);
assert_ne!(
second.action_id, "fast-lock-hit",
"an explicit cache_dir alone keeps the fast lock off"
);
}