use core::error::Error as StdError;
use core::num::NonZeroUsize;
use core::sync::atomic::{AtomicBool, Ordering};
use std::ffi::OsString;
use std::fs::{self, File, TryLockError};
use std::io::{Read as _, Write as _};
use std::process::{Command, Output};
use std::sync::OnceLock;
use std::{env, io, thread};
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
use cargo_gamma_process::{MemoryRequest, output as contained_output};
use walkdir::WalkDir;
use super::cargo_options::CargoOptions;
use super::config::Config;
use super::copy::{CopyOptions, visible_vcs_metadata};
use super::events::Events;
#[cfg(test)]
use super::faults::{self, Fault};
use super::loader::{Launch, toolchain_libraries};
use super::manifest::{CAP_LINTS, Manifest, RUNTIME_CRATE, RUNTIME_PACKAGE, anchor_cargo_config, cap_lints};
use super::nextest::Harness;
use super::sync::sync_or_copy;
use super::test_binary::{TEST_THREADS_VAR, TestBinary, harness_threads};
use crate::Result;
use crate::discover::TargetFile;
use crate::error::error;
pub(crate) type CacheLocks = (File, Option<File>);
const RUNTIME_SOURCES: [(&str, &str); 3] = [
("lib.rs", include_str!("../../../cargo-gamma-rt/src/lib.rs")),
("either.rs", include_str!("../../../cargo-gamma-rt/src/either.rs")),
("runtime.rs", include_str!("../../../cargo-gamma-rt/src/runtime.rs")),
];
const WORKSPACE_MANIFEST: &str = include_str!("../../../../Cargo.toml");
const CACHE_OWNER: &str = ".cargo-gamma-owner";
#[derive(Debug)]
pub struct Workspace {
pub(super) root: Utf8PathBuf,
pub(super) target: Utf8PathBuf,
pub(super) libraries: Vec<Utf8PathBuf>,
pub(super) cargo: CargoOptions,
runtime: Utf8PathBuf,
pub(super) leak: bool,
settled: AtomicBool,
nextest: Option<Harness>,
launch: OnceLock<Launch>,
harness_threads: OnceLock<Option<String>>,
_workspace_lock: File,
_cache_lock: Option<File>,
torn_down: bool,
}
impl Drop for Workspace {
fn drop(&mut self) {
if self.torn_down {
return;
}
let _torn_down = self.teardown();
}
}
impl Workspace {
pub fn teardown(&mut self) -> Result<()> {
if self.torn_down {
return Ok(());
}
self.torn_down = true;
if self.leak {
return Ok(());
}
let tree = if self.settled.load(Ordering::Relaxed) {
Ok(())
} else {
remove_tree(&self.root)
};
let build = if self.settled.load(Ordering::Relaxed) {
Ok(())
} else {
remove_tree(&self.target)
};
tree.and(build)
}
pub(super) fn inspect_hint(&self) -> String {
if self.leak {
format!("The tree is at `{}` if you want to look.", self.root)
} else {
"Re-run with `--leak-dirs` to keep the instrumented tree and look at it.".to_owned()
}
}
#[cfg(test)]
pub(super) fn prepare(source: &Utf8Path, config: &Config, events: &mut impl Events) -> Result<Self> {
Self::prepare_with_locks(source, config, events, None)
}
pub(super) fn prepare_with_locks(
source: &Utf8Path,
config: &Config,
events: &mut impl Events,
locks: Option<CacheLocks>,
) -> Result<Self> {
config.cargo.validate()?;
let source = &absolute(source);
let base = gamma_base(source, config.cache_dir.as_deref());
ensure_copy_terminates(source, &base)?;
let root = base.join("workspace");
if config.cache_dir.is_some() {
ensure_vcs_visibility(source, &root)?;
}
let target = base.join("target");
let runtime = base.join("rt");
events.begin("Copying", "Copied", "the workspace");
fs::create_dir_all(base.as_std_path())
.map_err(|cause| error!("could not create the scratch directory at `{base}`").caused_by(cause))?;
let (workspace_lock, cache_lock) = match locks {
Some(locks) => locks,
None => claim_cache(source, config.cache_dir.as_deref())?,
};
#[cfg(feature = "internals")]
crate::testing::pause_during_workspace_preparation(source);
events.testing_log(&base)?;
let _outcome = sync_or_copy(
source,
&root,
&base,
CopyOptions {
copy_ignored: config.copy_ignored,
},
)?;
if config.cache_dir.is_none() {
expose_vcs_metadata(source, &root)?;
}
vendor_runtime(&runtime)?;
anchor_manifests(source, &root, &runtime)?;
let libraries = toolchain_libraries(&root, &target);
let workspace = Self {
root,
target,
libraries,
cargo: config.cargo.clone(),
runtime,
leak: config.leak_dirs,
settled: AtomicBool::new(false),
nextest: None,
launch: OnceLock::new(),
harness_threads: OnceLock::new(),
_workspace_lock: workspace_lock,
_cache_lock: cache_lock,
torn_down: false,
};
events.end("");
Ok(workspace)
}
pub(super) fn link_runtime(&self, package: &str, files: &[TargetFile]) -> Result<()> {
let runtime = &self.runtime;
let Some(path) = self.manifest_of(package, files) else {
return Ok(());
};
let mut manifest = Manifest::read(&path)?;
manifest.link_runtime(runtime)?;
manifest.save()
}
fn manifest_of(&self, package: &str, files: &[TargetFile]) -> Option<Utf8PathBuf> {
let file = files.iter().find(|file| file.package == package)?;
let mut directory = self.root.join(&file.path);
let real_root = physical(&self.root);
while directory.pop() {
let candidate = directory.join("Cargo.toml");
if candidate.as_std_path().is_file() && physical(&candidate).starts_with(&real_root) {
return Some(candidate);
}
if directory == self.root {
break;
}
}
None
}
pub(super) fn overwrite(root: &Utf8Path, path: &Utf8Path, contents: &str) -> Result<bool> {
let metadata = fs::symlink_metadata(path.as_std_path())
.map_err(|cause| error!("could not write `{path}`, which the copy did not create").caused_by(cause))?;
if !metadata.is_file() {
return Err(error!(
"refusing to write `{path}`, which is a link or a device rather than the copied source file"
));
}
let (real_path, real_root) = (physical(path), physical(root));
if !real_path.starts_with(&real_root) {
return Err(error!(
"refusing to write `{path}`, which is `{real_path}` — outside the scratch tree at `{real_root}`, \
so the write would land in the real source tree"
));
}
if let Ok(existing) = fs::read(path.as_std_path())
&& existing == contents.as_bytes()
{
return Ok(false);
}
fs::write(path.as_std_path(), contents).map_err(|cause| error!("could not write `{path}`").caused_by(cause))?;
Ok(true)
}
#[cfg(any(test, feature = "internals"))]
pub(crate) fn adopt(root: Utf8PathBuf, target: Utf8PathBuf) -> Self {
Self {
runtime: root.join("gamma-rt"),
root,
target,
libraries: Vec::new(),
cargo: CargoOptions::default(),
nextest: None,
settled: AtomicBool::new(true),
leak: true,
launch: OnceLock::new(),
harness_threads: OnceLock::new(),
_workspace_lock: tempfile::tempfile().expect("a temporary file should be creatable"),
_cache_lock: None,
torn_down: false,
}
}
#[cfg(any(test, feature = "internals"))]
pub(crate) fn set_test_args(&mut self, args: Vec<String>) {
self.cargo.test_args = args;
}
pub(super) fn cargo(&self) -> Command {
let mut command = Command::new(cargo_binary());
let _ = command.current_dir(self.root.as_std_path());
let _ = command.env("CARGO_TARGET_DIR", self.target.as_std_path());
let _ = command.env("CARGO_TERM_COLOR", if self.cargo.color { "always" } else { "never" });
cap_ambient_rustflags(&mut command);
let _ = command.env_remove(gamma_rt::ACTIVE_VAR);
command
}
pub(super) fn settle(&self) {
self.settled.store(true, Ordering::Relaxed);
}
pub(super) fn root(&self) -> &Utf8Path {
&self.root
}
pub(super) fn test_arguments(&self) -> &[String] {
&self.cargo.test_args
}
pub(super) const fn runner(&self) -> Option<&Harness> {
self.nextest.as_ref()
}
pub(super) fn launch(&self) -> &Launch {
self.launch.get_or_init(|| Launch::derive(&self.libraries))
}
pub(super) fn calibrate_harness(&self, jobs: usize) {
let cores = thread::available_parallelism().map_or(1, NonZeroUsize::get);
let inherited = env::var(TEST_THREADS_VAR).ok();
let _settled = self
.harness_threads
.set(harness_threads(jobs, cores, inherited.as_deref()).map(|threads| threads.to_string()));
}
pub(super) fn harness_threads(&self) -> Option<&str> {
self.harness_threads.get().and_then(Option::as_deref)
}
pub(super) fn arm_nextest(&mut self, binaries: &[TestBinary]) -> Result<()> {
self.nextest = Some(Harness::prepare(self, binaries)?);
Ok(())
}
#[cfg(test)]
#[cfg_attr(loom, expect(dead_code, reason = "the loom build excludes the tests that call this"))]
pub(super) fn set_runner(&mut self, harness: Harness) {
self.nextest = Some(harness);
}
pub(super) fn write_scratch(&self, name: &str, contents: &str) -> Result<Utf8PathBuf> {
let path = self.target.join(name);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent.as_std_path()).map_err(|cause| error!("could not create `{parent}`").caused_by(cause))?;
}
fs::write(path.as_std_path(), contents).map_err(|cause| error!("could not write `{path}`").caused_by(cause))?;
Ok(path)
}
pub(super) fn capture_nextest_list(&self, binaries: &[TestBinary]) -> Result<String> {
let command = self.nextest_list_command(binaries);
self.capture(command, "cargo nextest list")
}
fn nextest_list_command(&self, binaries: &[TestBinary]) -> Command {
let mut command = self.cargo();
let mut args = vec![
"nextest".to_owned(),
"list".to_owned(),
"--list-type".to_owned(),
"binaries-only".to_owned(),
"--message-format".to_owned(),
"json".to_owned(),
];
let mut packages: Vec<&str> = binaries
.iter()
.map(|binary| {
if binary.package_id.is_empty() {
binary.package.as_str()
} else {
binary.package_id.as_str()
}
})
.collect();
packages.sort_unstable();
packages.dedup();
for package in packages {
args.push("--package".to_owned());
args.push(package.to_owned());
}
self.cargo.extend_nextest_args(&mut args);
let _ = command.args(args);
command
}
pub(super) fn capture_cargo_metadata(&self) -> Result<String> {
let mut command = self.cargo();
let _ = command.args(["metadata", "--format-version", "1"]);
self.capture(command, "cargo metadata")
}
fn capture(&self, command: Command, what: &str) -> Result<String> {
interpret(
contained_output(command, MemoryRequest::default()).map(Captured::from),
what,
&self.root,
)
}
pub(super) fn base(&self) -> &Utf8Path {
self.root.parent().unwrap_or(&self.root)
}
}
#[derive(Debug)]
struct Captured {
succeeded: bool,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
impl From<Output> for Captured {
fn from(output: Output) -> Self {
Self {
succeeded: output.status.success(),
stdout: output.stdout,
stderr: output.stderr,
}
}
}
fn interpret<E>(captured: core::result::Result<Captured, E>, what: &str, root: &Utf8Path) -> Result<String>
where
E: StdError + Send + Sync + 'static,
{
let captured = captured.map_err(|cause| error!("could not run `{what}` in {root}").caused_by(cause))?;
if !captured.succeeded {
let stderr = String::from_utf8_lossy(&captured.stderr);
return Err(error!("`{what}` failed in {root}:\n{}", stderr.trim()));
}
String::from_utf8(captured.stdout).map_err(|cause| error!("`{what}` did not print valid UTF-8").caused_by(cause))
}
fn remove_tree(path: &Utf8Path) -> Result<()> {
match fs::remove_dir_all(path.as_std_path()) {
Err(cause) if cause.kind() != io::ErrorKind::NotFound => {
Err(error!("could not remove the scratch directory at `{path}`").caused_by(cause))
}
_removed => Ok(()),
}
}
#[must_use]
pub fn footprint(base: &Utf8Path) -> u64 {
WalkDir::new(base.as_std_path())
.into_iter()
.filter_map(Result::ok)
.filter_map(|entry| entry.metadata().ok())
.filter(fs::Metadata::is_file)
.map(|metadata| metadata.len())
.sum()
}
#[must_use]
pub fn gamma_base(root: &Utf8Path, cache: Option<&Utf8Path>) -> Utf8PathBuf {
let base = cache.map_or_else(
|| default_cache_home(root).join(workspace_identity(&absolute(root))),
Utf8Path::to_owned,
);
absolute(&base)
}
fn default_cache_home(root: &Utf8Path) -> Utf8PathBuf {
env::var_os("XDG_CACHE_HOME")
.or_else(|| env::var_os("LOCALAPPDATA"))
.and_then(|path| Utf8PathBuf::from_path_buf(path.into()).ok())
.or_else(|| {
env::var_os("HOME")
.and_then(|path| Utf8PathBuf::from_path_buf(path.into()).ok())
.map(|home| home.join(".cache"))
})
.unwrap_or_else(|| absolute(root).parent().unwrap_or(root).join(".cargo-gamma-cache"))
.join("cargo-gamma")
}
fn workspace_identity(root: &Utf8Path) -> String {
let digest = blake3::hash(root.as_str().as_bytes());
let mut identity = [0_u8; 8];
identity.copy_from_slice(digest.as_bytes().get(..8).expect("a BLAKE3 digest is 32 bytes long"));
format!("{:016x}", u64::from_be_bytes(identity))
}
pub fn clean_cache(root: &Utf8Path) -> Result<bool> {
let base = gamma_base(root, None);
let mut removed = false;
if base.exists() {
reject_linked_cache(&base)?;
let _lock = claim(&base)?;
validate_cache_owner(root, &base, CacheKind::Default)?;
let entries =
fs::read_dir(base.as_std_path()).map_err(|cause| error!("could not read cargo-gamma's cache at `{base}`").caused_by(cause))?;
for entry in entries {
let entry = entry.map_err(|cause| error!("could not read an entry in cargo-gamma's cache at `{base}`").caused_by(cause))?;
if entry.file_name() == "lock" || entry.file_name() == CACHE_OWNER {
continue;
}
remove_cached(&entry)?;
removed = true;
}
}
Ok(removed)
}
fn remove_cached(entry: &fs::DirEntry) -> Result<()> {
let path = entry.path();
let file_type = entry
.file_type()
.map_err(|cause| error!("could not inspect cached data at `{}`", path.display()).caused_by(cause))?;
let result = if file_type.is_dir() && !file_type.is_symlink() {
fs::remove_dir_all(&path)
} else {
fs::remove_file(&path)
};
result.map_err(|cause| error!("could not remove cached data at `{}`", path.display()).caused_by(cause))
}
pub(super) fn absolute(path: &Utf8Path) -> Utf8PathBuf {
let rooted = if path.is_absolute() {
path.to_owned()
} else {
env::current_dir()
.ok()
.and_then(|cwd| Utf8PathBuf::from_path_buf(cwd).ok())
.map_or_else(|| path.to_owned(), |cwd| cwd.join(path))
};
let mut normalised = Utf8PathBuf::new();
for component in rooted.components() {
match component {
Utf8Component::CurDir => {}
Utf8Component::ParentDir => match normalised.components().next_back() {
Some(Utf8Component::Normal(_)) => {
let _popped = normalised.pop();
}
_ => normalised.push(component),
},
other => normalised.push(other),
}
}
normalised
}
fn physical(path: &Utf8Path) -> Utf8PathBuf {
let mut existing = path.to_owned();
let mut tail: Vec<String> = Vec::new();
loop {
if let Ok(resolved) = fs::canonicalize(existing.as_std_path()) {
let Ok(mut resolved) = Utf8PathBuf::from_path_buf(resolved) else {
return path.to_owned();
};
for name in tail.iter().rev() {
resolved.push(name);
}
return resolved;
}
let Some(name) = existing.file_name().map(str::to_owned) else {
return path.to_owned();
};
tail.push(name);
if !existing.pop() {
return path.to_owned();
}
}
}
fn ensure_copy_terminates(source: &Utf8Path, base: &Utf8Path) -> Result<()> {
let (real_source, real_base) = (physical(source), physical(base));
if !real_base.starts_with(&real_source) || prunes_in_practice(source, base, &real_source, &real_base) {
return Ok(());
}
if prunes(&real_source, &real_base) {
return Err(error!(
"the scratch directory `{base}` is `{real_base}`, inside the workspace at `{source}`, but is \
not named as a path inside it — so the copy cannot skip it and would copy the copy.\n\
Point --cache-dir at a directory that is really outside the workspace."
)
.usage());
}
Err(error!(
"the scratch directory `{base}` is the workspace at `{source}` itself, so copying the \
workspace would copy the copy.\n\
Point --cache-dir at a directory outside the workspace."
)
.usage())
}
fn ensure_vcs_visibility(source: &Utf8Path, scratch: &Utf8Path) -> Result<()> {
let source_metadata = visible_vcs_metadata(source);
if source_metadata.is_empty() {
return Ok(());
}
let scratch_metadata = visible_vcs_metadata(scratch);
let hidden: Vec<&Utf8PathBuf> = source_metadata.iter().filter(|marker| !scratch_metadata.contains(marker)).collect();
if hidden.is_empty() {
return Ok(());
}
Err(error!(
"`--cache-dir` would relocate the cached workspace to `{scratch}`, where build scripts cannot see VCS metadata available from `{source}`: {}. \
Use a cache directory beneath the same repository, or remove the build-time VCS dependency.",
hidden.iter().map(|marker| marker.as_str()).collect::<Vec<_>>().join(", ")
)
.usage())
}
fn expose_vcs_metadata(source: &Utf8Path, scratch: &Utf8Path) -> Result<()> {
let markers = visible_vcs_metadata(source);
for name in super::copy::VCS_DIRS {
let Some(marker) = markers
.iter()
.filter(|marker| marker.file_name() == Some(name))
.max_by_key(|marker| marker.components().count())
else {
continue;
};
let destination = scratch.join(name);
if name == ".git" {
let git_dir = if marker.as_std_path().is_dir() {
marker.clone()
} else {
let text = fs::read_to_string(marker.as_std_path())
.map_err(|cause| error!("could not read Git metadata pointer `{marker}`").caused_by(cause))?;
let relative = text
.trim()
.strip_prefix("gitdir:")
.map(str::trim)
.ok_or_else(|| error!("Git metadata pointer `{marker}` has no `gitdir:` target"))?;
absolute(&marker.parent().unwrap_or_else(|| Utf8Path::new("")).join(relative))
};
fs::write(destination.as_std_path(), format!("gitdir: {git_dir}\n"))
.map_err(|cause| error!("could not expose Git metadata at `{destination}`").caused_by(cause))?;
continue;
}
#[cfg(unix)]
std::os::unix::fs::symlink(marker.as_std_path(), destination.as_std_path())
.map_err(|cause| error!("could not expose VCS metadata at `{destination}`").caused_by(cause))?;
#[cfg(windows)]
{
let linked = if marker.as_std_path().is_dir() {
std::os::windows::fs::symlink_dir(marker.as_std_path(), destination.as_std_path())
} else {
std::os::windows::fs::symlink_file(marker.as_std_path(), destination.as_std_path())
};
linked.map_err(|cause| error!("could not expose VCS metadata at `{destination}`").caused_by(cause))?;
}
}
Ok(())
}
fn prunes_in_practice(source: &Utf8Path, base: &Utf8Path, real_source: &Utf8Path, real_base: &Utf8Path) -> bool {
let Ok(relative) = base.strip_prefix(source) else {
return false;
};
!relative.as_str().is_empty() && real_base == real_source.join(relative)
}
fn prunes(source: &Utf8Path, base: &Utf8Path) -> bool {
base.starts_with(source) && base != source
}
#[must_use]
pub fn scratch_tree(root: &Utf8Path, scratch: Option<&Utf8Path>) -> Utf8PathBuf {
gamma_base(root, scratch).join("workspace")
}
fn claim_redirected_cache(source: &Utf8Path, base: &Utf8Path) -> Result<File> {
if base.exists() {
reject_linked_cache(base)?;
match fs::symlink_metadata(base.join(CACHE_OWNER).as_std_path()) {
Ok(_metadata) => {}
Err(cause) if cause.kind() == io::ErrorKind::NotFound => {
if has_any_entries(base)? {
return Err(unowned_cache(base));
}
}
Err(cause) => {
return Err(error!(
"could not inspect the cargo-gamma cache owner marker at `{}`",
base.join(CACHE_OWNER)
)
.caused_by(cause));
}
}
}
create_private_dir_all(base)
.map_err(|cause| error!("could not create the redirected cargo-gamma cache at `{base}`").caused_by(cause))?;
reject_linked_cache(base)?;
reject_foreign_writers(base)?;
let lock = claim(base)?;
validate_cache_owner(source, base, CacheKind::Redirected)?;
Ok(lock)
}
#[cfg(unix)]
fn create_private_dir_all(path: &Utf8Path) -> io::Result<()> {
use std::os::unix::fs::DirBuilderExt as _;
let mut builder = fs::DirBuilder::new();
builder.recursive(true).mode(0o700);
builder.create(path.as_std_path())
}
#[cfg(not(unix))]
fn create_private_dir_all(path: &Utf8Path) -> io::Result<()> {
fs::create_dir_all(path.as_std_path())
}
fn reject_linked_cache(base: &Utf8Path) -> Result<()> {
let metadata = fs::symlink_metadata(base.as_std_path())
.map_err(|cause| error!("could not inspect the redirected cargo-gamma cache at `{base}`").caused_by(cause))?;
if metadata.file_type().is_symlink() {
return Err(error!(
"the redirected cache at `{base}` is a link.\n\
Pass the directory itself to --cache-dir: a link can be repointed after it has been checked, \
at a directory whose contents this tool would then build and run."
)
.usage());
}
if !metadata.is_dir() {
return Err(error!(
"the redirected cache at `{base}` is not a directory.\n\
Choose an empty directory for --cache-dir."
)
.usage());
}
Ok(())
}
#[cfg(unix)]
fn reject_foreign_writers(base: &Utf8Path) -> Result<()> {
use std::os::unix::fs::MetadataExt as _;
const SHARED_WRITE: u32 = 0o022;
const STICKY: u32 = 0o1000;
const ROOT: u32 = 0;
let user = cargo_gamma_unsafe::identity::effective_user();
let physical = physical(base);
for (index, directory) in physical.ancestors().enumerate() {
if directory.as_str().is_empty() {
break;
}
let metadata = fs::symlink_metadata(directory.as_std_path())
.map_err(|cause| error!("could not inspect `{directory}` on the way to the redirected cache").caused_by(cause))?;
if metadata.uid() != user && metadata.uid() != ROOT {
return Err(error!(
"`{directory}`, on the way to the redirected cache at `{base}`, belongs to another user.\n\
Choose a directory only you can write to for --cache-dir: this run builds test executables \
there and then runs them as you."
)
.usage());
}
let mode = metadata.mode();
if mode & SHARED_WRITE != 0 && (index == 0 || mode & STICKY == 0) {
return Err(error!(
"`{directory}`, on the way to the redirected cache at `{base}`, is writable by other users.\n\
Choose a directory only you can write to for --cache-dir: this run builds test executables \
there and then runs them as you."
)
.usage());
}
}
Ok(())
}
#[cfg(not(unix))]
#[expect(clippy::unnecessary_wraps, reason = "matches the Unix signature this stands in for")]
fn reject_foreign_writers(_base: &Utf8Path) -> Result<()> {
Ok(())
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CacheKind {
Default,
Redirected,
}
impl CacheKind {
const fn collision_advice(self) -> &'static str {
match self {
Self::Default => "Pass --cache-dir to give one of the two workspaces a cache of its own.",
Self::Redirected => "Choose a different directory for --cache-dir.",
}
}
}
fn validate_cache_owner(source: &Utf8Path, base: &Utf8Path, kind: CacheKind) -> Result<()> {
let owner = base.join(CACHE_OWNER);
match open_cache_owner(&owner) {
Ok(mut marker) => {
let metadata = marker
.metadata()
.map_err(|cause| error!("could not inspect the cargo-gamma cache owner marker at `{owner}`").caused_by(cause))?;
if !metadata.is_file() {
return Err(error!(
"the cargo-gamma cache owner marker at `{owner}` is not a regular file.\n\
{}",
kind.collision_advice()
)
.usage());
}
let mut recorded = String::new();
marker
.read_to_string(&mut recorded)
.map_err(|cause| error!("could not read the cargo-gamma cache owner marker at `{owner}`").caused_by(cause))?;
let source = physical(source);
if recorded != source.as_str() {
return Err(error!(
"the cargo-gamma cache at `{base}` belongs to the workspace at `{}`, not `{source}`.\n\
{}",
crate::report::encode_controls(&recorded),
kind.collision_advice()
)
.usage());
}
}
Err(cause) if cause.kind() == io::ErrorKind::NotFound => {
if has_unowned_entries(base)? {
return Err(unowned_cache(base));
}
let mut marker = File::options()
.create_new(true)
.write(true)
.open(owner.as_std_path())
.map_err(|cause| error!("could not write the cargo-gamma cache owner marker at `{owner}`").caused_by(cause))?;
marker
.write_all(physical(source).as_str().as_bytes())
.map_err(|cause| error!("could not write the cargo-gamma cache owner marker at `{owner}`").caused_by(cause))?;
}
Err(cause) => {
return Err(error!("could not inspect the cargo-gamma cache owner marker at `{owner}`").caused_by(cause));
}
}
Ok(())
}
#[cfg(unix)]
fn open_cache_owner(owner: &Utf8Path) -> io::Result<File> {
use std::os::unix::fs::OpenOptionsExt;
File::options()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
.open(owner.as_std_path())
}
#[cfg(not(unix))]
fn open_cache_owner(owner: &Utf8Path) -> io::Result<File> {
let metadata = fs::symlink_metadata(owner.as_std_path())?;
if !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cache owner marker is not a regular file",
));
}
File::open(owner.as_std_path())
}
fn has_any_entries(base: &Utf8Path) -> Result<bool> {
let mut entries =
fs::read_dir(base.as_std_path()).map_err(|cause| error!("could not inspect the redirected cache at `{base}`").caused_by(cause))?;
entries
.next()
.transpose()
.map(|entry| entry.is_some())
.map_err(|cause| error!("could not inspect an entry in the redirected cache at `{base}`").caused_by(cause))
}
fn has_unowned_entries(base: &Utf8Path) -> Result<bool> {
let entries = fs::read_dir(base.as_std_path()).map_err(|cause| error!("could not inspect the cache at `{base}`").caused_by(cause))?;
for entry in entries {
let entry = entry.map_err(|cause| error!("could not inspect an entry in the cache at `{base}`").caused_by(cause))?;
if entry.file_name() != "lock" {
return Ok(true);
}
}
Ok(false)
}
fn unowned_cache(base: &Utf8Path) -> crate::error::Error {
error!(
"the cache at `{base}` is not empty and is not marked as cargo-gamma state.\n\
Existing contents will not be adopted or removed; pass --cache-dir to use an empty directory instead."
)
.usage()
}
fn claim(base: &Utf8Path) -> Result<File> {
let path = base.join("lock");
let file = File::options()
.create(true)
.truncate(false)
.write(true)
.open(path.as_std_path())
.map_err(|cause| error!("could not open the scratch lock at `{path}`").caused_by(cause))?;
match try_lock(&file) {
Ok(()) => {}
Err(TryLockError::WouldBlock) => {
return Err(error!(
"another `cargo gamma` run is already using `{base}`.\n\
Wait for it to finish before running another command that uses this workspace or cache."
)
.usage());
}
Err(TryLockError::Error(cause)) => {
return Err(error!(
"the cargo-gamma workspace lock at `{path}` could not be taken.\n\
The workspace cache must be on a filesystem that supports advisory file locking."
)
.caused_by(cause));
}
}
Ok(file)
}
pub(crate) fn claim_workspace(root: &Utf8Path) -> Result<File> {
let base = gamma_base(root, None);
fs::create_dir_all(base.as_std_path())
.map_err(|cause| error!("could not create cargo-gamma's workspace cache at `{base}`").caused_by(cause))?;
let lock = claim(&base)?;
validate_cache_owner(root, &base, CacheKind::Default)?;
Ok(lock)
}
pub(crate) fn claim_cache(root: &Utf8Path, cache: Option<&Utf8Path>) -> Result<CacheLocks> {
let workspace = claim_workspace(root)?;
let base = gamma_base(root, cache);
let default = gamma_base(root, None);
let redirected = if cache.is_some() && physical(&base) != physical(&default) {
Some(claim_redirected_cache(root, &base)?)
} else {
None
};
Ok((workspace, redirected))
}
fn try_lock(file: &File) -> core::result::Result<(), TryLockError> {
#[cfg(test)]
if faults::fired(Fault::Lock) {
return Err(TryLockError::Error(io::Error::from(io::ErrorKind::Unsupported)));
}
#[cfg(test)]
for _ in 0..250 {
match file.try_lock() {
Err(TryLockError::WouldBlock) => std::thread::sleep(std::time::Duration::from_millis(1)),
result => return result,
}
}
file.try_lock()
}
fn anchor_manifests(source: &Utf8Path, root: &Utf8Path, runtime: &Utf8Path) -> Result<()> {
for entry in WalkDir::new(root.as_std_path()).into_iter().filter_map(core::result::Result::ok) {
if entry.file_name() != "Cargo.toml" {
continue;
}
let Some(path) = Utf8Path::from_path(entry.path()) else {
continue;
};
let Some(original) = path.parent().and_then(|directory| directory.strip_prefix(root).ok()) else {
continue;
};
let _destination = crate::paths::require_within(path, root, "a scratch manifest")?;
let mut manifest = Manifest::read(path)?;
manifest.anchor_paths(&source.join(original), original);
manifest.redirect_runtime(runtime)?;
manifest.save()?;
}
anchor_cargo_config(root, source)?;
cap_lints(root)
}
fn cap_ambient_rustflags(command: &mut Command) {
if let Some(inherited) = env::var_os("CARGO_ENCODED_RUSTFLAGS") {
return extend(command, "CARGO_ENCODED_RUSTFLAGS", inherited, "\u{1f}");
}
if let Some(inherited) = env::var_os("RUSTFLAGS") {
return extend(command, "RUSTFLAGS", inherited, " ");
}
for (name, inherited) in env::vars_os() {
let Some(name) = name.to_str() else {
continue;
};
if name.starts_with("CARGO_TARGET_") && name.ends_with("_RUSTFLAGS") {
extend(command, name, inherited, " ");
}
}
if let Some(inherited) = env::var_os("CARGO_BUILD_RUSTFLAGS") {
extend(command, "CARGO_BUILD_RUSTFLAGS", inherited, " ");
}
}
fn extend(command: &mut Command, name: &str, inherited: OsString, separator: &str) {
let mut merged = inherited;
merged.push(separator);
merged.push(CAP_LINTS);
let _ = command.env(name, merged);
}
fn cargo_binary() -> String {
env::var("CARGO").unwrap_or_else(|_missing| "cargo".to_owned())
}
fn vendor_runtime(at: &Utf8Path) -> Result<()> {
let source = at.join("src");
fs::create_dir_all(source.as_std_path()).map_err(|cause| error!("could not create `{source}`").caused_by(cause))?;
let workspace: toml::Value = toml::from_str(WORKSPACE_MANIFEST)
.map_err(|cause| error!("could not read the embedded workspace package contract").caused_by(cause))?;
let package = workspace
.get("workspace")
.and_then(|workspace| workspace.get("package"))
.and_then(toml::Value::as_table)
.ok_or_else(|| error!("the embedded workspace manifest has no `[workspace.package]` table"))?;
let inherited = |name| {
package
.get(name)
.and_then(toml::Value::as_str)
.ok_or_else(|| error!("the embedded workspace package contract has no string `{name}`"))
};
let edition = inherited("edition")?;
let rust_version = inherited("rust-version")?;
let manifest = format!(
"[package]\nname = \"{RUNTIME_PACKAGE}\"\nversion = \"0.0.0\"\nedition = \"{edition}\"\nrust-version = \"{rust_version}\"\npublish = false\n\n\
[features]\nloom = []\n\n[lints.rust]\nunexpected_cfgs = {{ level = \"warn\", check-cfg = ['cfg(coverage_nightly)', 'cfg(loom)'] }}\n\n\
[lib]\nname = \"{RUNTIME_CRATE}\"\npath = \"src/lib.rs\"\n\n[workspace]\n"
);
fs::write(at.join("Cargo.toml").as_std_path(), manifest)
.map_err(|cause| error!("could not write the runtime manifest in `{at}`").caused_by(cause))?;
for (name, contents) in RUNTIME_SOURCES {
let path = source.join(name);
fs::write(path.as_std_path(), contents).map_err(|cause| error!("could not write the runtime source `{path}`").caused_by(cause))?;
}
Ok(())
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use std::collections::BTreeMap;
use std::ffi::OsStr;
use super::*;
fn private_system_tempdir(prefix: &str) -> tempfile::TempDir {
let mut builder = tempfile::Builder::new();
builder.prefix(prefix);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
builder.permissions(fs::Permissions::from_mode(0o700));
}
builder
.tempdir()
.expect("the redirected-cache fixture should be creatable in the system temporary directory")
}
#[test]
fn a_successful_capture_returns_what_the_command_printed() {
let captured: io::Result<Captured> = Ok(Captured {
succeeded: true,
stdout: b"{\"kind\":\"test\"}".to_vec(),
stderr: b"warning: ignored".to_vec(),
});
let text = interpret(captured, "cargo nextest list", Utf8Path::new("/w")).expect("a successful capture");
assert_eq!(text, "{\"kind\":\"test\"}");
}
#[test]
fn a_capture_that_could_not_be_spawned_is_reported() {
let captured = Err(io::Error::new(io::ErrorKind::NotFound, "no such file"));
let failure = interpret(captured, "cargo nextest list", Utf8Path::new("/w")).expect_err("a spawn failure must be reported");
assert!(
failure.to_string().contains("could not run `cargo nextest list` in /w"),
"{failure}"
);
assert!(failure.to_string().contains("no such file"), "the cause is kept: {failure}");
}
#[test]
fn a_capture_that_exited_non_zero_is_reported_with_its_diagnostic() {
let captured: io::Result<Captured> = Ok(Captured {
succeeded: false,
stdout: b"partial output".to_vec(),
stderr: b" error: no such subcommand: `nextest`\n".to_vec(),
});
let failure = interpret(captured, "cargo nextest list", Utf8Path::new("/w")).expect_err("a non-zero exit must be reported");
let text = failure.to_string();
assert!(text.contains("`cargo nextest list` failed in /w"), "{text}");
assert!(text.contains("error: no such subcommand: `nextest`"), "{text}");
assert!(!text.contains("partial output"), "the failing output is not the answer: {text}");
}
#[test]
fn a_capture_that_is_not_utf8_is_reported() {
let captured: io::Result<Captured> = Ok(Captured {
succeeded: true,
stdout: vec![0x7b, 0xff, 0xfe, 0x7d],
stderr: Vec::new(),
});
let failure = interpret(captured, "cargo metadata", Utf8Path::new("/w")).expect_err("invalid UTF-8 must be reported");
assert!(
failure.to_string().contains("`cargo metadata` did not print valid UTF-8"),
"{failure}"
);
}
#[test]
fn preparing_a_fresh_workspace_produces_a_usable_scratch_tree() {
let directory = crate::testing::workdir("prepare-happy-");
let source = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the scratch path is UTF-8");
fs::write(
source.join("Cargo.toml").as_std_path(),
"[package]\nname = \"trivial\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[workspace]\n",
)
.expect("a manifest");
fs::create_dir_all(source.join("src").as_std_path()).expect("src");
fs::write(source.join("src/lib.rs").as_std_path(), "pub const A: i32 = 1;\n").expect("lib");
let config = Config::default();
let mut events = crate::testing::Recorder::default();
let work = Workspace::prepare(&source, &config, &mut events).expect("a fresh tree must prepare cleanly");
assert!(work.root.join("Cargo.toml").as_std_path().is_file(), "the manifest was not copied");
assert!(
work.root.join("src/lib.rs").as_std_path().is_file(),
"the source file was not copied"
);
assert!(
work.runtime.join("Cargo.toml").as_std_path().is_file(),
"the runtime was not vendored"
);
}
#[test]
fn an_unowned_redirected_cache_is_refused_without_touching_its_contents() {
let directory = private_system_tempdir("unowned-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
let marker = base.join("build/user-data");
fs::create_dir_all(marker.parent().expect("the marker has a parent")).expect("the user's directory");
fs::write(marker.as_std_path(), "keep").expect("the user's file");
let failure = claim_redirected_cache(&source, &base).expect_err("an unowned non-empty directory must be refused");
assert!(failure.is_usage());
assert!(failure.to_string().contains("not marked as cargo-gamma state"), "{failure}");
assert_eq!(fs::read_to_string(marker.as_std_path()).expect("the user's file remains"), "keep");
assert!(
!base.join("lock").exists(),
"refusal must happen before cargo-gamma writes into the directory"
);
assert!(!base.join(CACHE_OWNER).exists());
}
#[test]
fn a_directory_containing_only_somebody_elses_lock_file_is_not_adopted() {
let directory = private_system_tempdir("foreign-lock-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
create_private_dir_all(&base).expect("the user's directory");
fs::write(base.join("lock"), "not ours").expect("the user's lock file");
let failure = claim_redirected_cache(&source, &base).expect_err("an existing lock file is user data");
assert!(failure.is_usage());
assert_eq!(fs::read_to_string(base.join("lock")).expect("the user's lock remains"), "not ours");
assert!(!base.join(CACHE_OWNER).exists());
}
#[test]
fn an_empty_redirected_cache_is_claimed_for_its_workspace_and_can_be_reused() {
let directory = private_system_tempdir("owned-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
create_private_dir_all(&base).expect("the empty cache");
let first = claim_redirected_cache(&source, &base).expect("an empty cache can be claimed");
assert_eq!(
fs::read_to_string(base.join(CACHE_OWNER)).expect("the owner marker"),
physical(&source).as_str()
);
drop(first);
let _second = claim_redirected_cache(&source, &base).expect("the owning workspace can reuse its cache");
}
#[test]
fn a_redirected_cache_owned_by_another_workspace_is_refused() {
let directory = private_system_tempdir("foreign-cache-");
let first = Utf8PathBuf::from_path_buf(directory.path().join("first")).expect("the source path is UTF-8");
let second = Utf8PathBuf::from_path_buf(directory.path().join("second")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
let held = claim_redirected_cache(&first, &base).expect("the first workspace claims the cache");
drop(held);
let failure = claim_redirected_cache(&second, &base).expect_err("a second workspace must not adopt the cache");
assert!(failure.is_usage());
assert!(failure.to_string().contains("belongs to the workspace"), "{failure}");
assert!(failure.to_string().contains(physical(&first).as_str()), "{failure}");
}
#[cfg(unix)]
#[test]
fn a_new_redirected_cache_is_private_under_a_permissive_umask() {
use std::os::unix::fs::PermissionsExt as _;
const CHILD: &str = "CARGO_GAMMA_PERMISSIVE_UMASK_CHILD";
const TEST: &str = "exec::workspace::tests::a_new_redirected_cache_is_private_under_a_permissive_umask";
if env::var_os(CHILD).is_none() {
let executable = env::current_exe().expect("the current test executable");
let output = Command::new("sh")
.args(["-c", "umask 000; exec \"$1\" --exact \"$2\" --nocapture", "sh"])
.arg(executable)
.arg(TEST)
.env(CHILD, "1")
.output()
.expect("the child test process starts");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "child stdout:\n{stdout}\nchild stderr:\n{stderr}");
return;
}
let directory = private_system_tempdir("private-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
let _held = claim_redirected_cache(&source, &base).expect("a new cache is private under any umask");
let mode = fs::metadata(base.as_std_path()).expect("the cache metadata").permissions().mode();
assert_eq!(mode & 0o077, 0, "the cache mode {mode:o} grants group or other access");
}
#[test]
fn a_linked_redirected_cache_is_refused() {
let directory = private_system_tempdir("linked-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let real = Utf8PathBuf::from_path_buf(directory.path().join("real")).expect("the real path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
fs::create_dir_all(&real).expect("the directory the link points at");
#[cfg(unix)]
std::os::unix::fs::symlink(real.as_std_path(), base.as_std_path()).expect("the link");
#[cfg(windows)]
if std::os::windows::fs::symlink_dir(real.as_std_path(), base.as_std_path()).is_err() {
return;
}
let failure = claim_redirected_cache(&source, &base).expect_err("a linked cache must be refused");
assert!(failure.is_usage());
assert!(failure.to_string().contains("is a link"), "{failure}");
assert!(!real.join(CACHE_OWNER).exists(), "nothing may be written through the link");
}
#[test]
fn a_redirected_cache_that_is_not_a_directory_is_refused() {
let directory = private_system_tempdir("file-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
fs::write(base.as_std_path(), "not a directory").expect("the user's file");
let failure = claim_redirected_cache(&source, &base).expect_err("a file is not a cache directory");
assert!(failure.is_usage());
assert_eq!(
fs::read_to_string(base.as_std_path()).expect("the user's file remains"),
"not a directory"
);
}
#[cfg(unix)]
#[test]
fn a_redirected_cache_under_a_world_writable_directory_is_refused() {
use std::os::unix::fs::PermissionsExt as _;
let directory = private_system_tempdir("shared-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let shared = Utf8PathBuf::from_path_buf(directory.path().join("shared")).expect("the shared path is UTF-8");
let base = shared.join("cache");
create_private_dir_all(&base).expect("the cache directory");
fs::set_permissions(shared.as_std_path(), fs::Permissions::from_mode(0o777)).expect("make the parent world-writable");
let failure = claim_redirected_cache(&source, &base).expect_err("a world-writable ancestor must be refused");
assert!(failure.is_usage());
assert!(failure.to_string().contains("is writable by other users"), "{failure}");
assert!(!base.join(CACHE_OWNER).exists());
assert!(!base.join("lock").exists(), "refusal must happen before anything is written");
fs::set_permissions(shared.as_std_path(), fs::Permissions::from_mode(0o755)).expect("restore the parent");
}
#[cfg(unix)]
#[test]
fn a_redirected_cache_under_a_sticky_shared_directory_is_allowed() {
use std::os::unix::fs::PermissionsExt as _;
let directory = private_system_tempdir("sticky-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let shared = Utf8PathBuf::from_path_buf(directory.path().join("shared")).expect("the shared path is UTF-8");
let base = shared.join("cache");
create_private_dir_all(&base).expect("the cache directory");
fs::set_permissions(shared.as_std_path(), fs::Permissions::from_mode(0o1777)).expect("make the parent sticky and shared");
let claimed = claim_redirected_cache(&source, &base);
fs::set_permissions(shared.as_std_path(), fs::Permissions::from_mode(0o755)).expect("restore the parent");
let _lock = claimed.expect("a sticky shared ancestor is not a foreign writer");
}
#[cfg(unix)]
#[test]
fn a_sticky_world_writable_cache_itself_is_refused() {
use std::os::unix::fs::PermissionsExt as _;
let directory = private_system_tempdir("sticky-base-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
fs::create_dir_all(&base).expect("the cache directory");
fs::set_permissions(base.as_std_path(), fs::Permissions::from_mode(0o1777)).expect("make the cache sticky and shared");
let failure = claim_redirected_cache(&source, &base).expect_err("a shared cache must be refused even when sticky");
assert!(failure.is_usage());
assert!(failure.to_string().contains("is writable by other users"), "{failure}");
assert!(!base.join(CACHE_OWNER).exists());
assert!(!base.join("lock").exists());
fs::set_permissions(base.as_std_path(), fs::Permissions::from_mode(0o755)).expect("restore the cache");
}
#[test]
fn a_hostile_owner_marker_cannot_address_the_terminal_it_is_reported_on() {
let directory = private_system_tempdir("hostile-marker-cache-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
create_private_dir_all(&base).expect("the cache directory");
fs::write(base.join(CACHE_OWNER).as_std_path(), "/w\r\u{1b}[2Kforged").expect("the planted marker");
let failure = claim_redirected_cache(&source, &base).expect_err("a foreign marker must be refused");
let text = failure.to_string();
assert!(!text.contains('\u{1b}'), "{text:?}");
assert!(!text.contains('\r'), "{text:?}");
assert!(text.contains("\\r\\e[2Kforged"), "{text:?}");
}
#[test]
fn two_workspaces_cannot_use_one_redirected_cache_concurrently() {
let directory = private_system_tempdir("contended-cache-");
let first = Utf8PathBuf::from_path_buf(directory.path().join("first")).expect("the source path is UTF-8");
let second = Utf8PathBuf::from_path_buf(directory.path().join("second")).expect("the source path is UTF-8");
let base = Utf8PathBuf::from_path_buf(directory.path().join("cache")).expect("the cache path is UTF-8");
let _held = claim_redirected_cache(&first, &base).expect("the first workspace claims the cache");
let failure = claim_redirected_cache(&second, &base).expect_err("the live cache lock must serialize workspaces");
assert!(failure.is_usage());
assert!(failure.to_string().contains("already using"), "{failure}");
}
#[test]
fn explicitly_naming_the_default_cache_does_not_take_its_lock_twice() {
let directory = crate::testing::workdir("default-cache-alias-");
let source = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the source path is UTF-8");
let base = gamma_base(&source, None);
let (_workspace, redirected) = claim_cache(&source, Some(&base)).expect("the default cache has one lock domain");
assert!(redirected.is_none());
}
#[test]
fn cleaning_removes_cache_data_but_not_published_reports() {
let directory = crate::testing::workdir("clean-cache-");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the workspace path is UTF-8");
let base = gamma_base(&root, None);
let report = root.join("target/cargo-gamma/gamma-report.json");
fs::create_dir_all(&base).expect("cache");
validate_cache_owner(&root, &base, CacheKind::Default).expect("claim cache");
fs::create_dir(base.join("workspace")).expect("cached workspace");
fs::create_dir_all(base.join("target")).expect("cached target");
fs::write(base.join("last-gamma-run.json"), "{}").expect("run record");
fs::create_dir_all(report.parent().expect("report directory")).expect("report directory");
fs::write(&report, "{}").expect("published report");
assert!(clean_cache(&root).expect("clean cache"));
assert!(!base.join("workspace").exists());
assert!(!base.join("target").exists());
assert!(!base.join("last-gamma-run.json").exists());
assert!(base.join("lock").exists(), "the concurrency lock remains");
assert!(report.exists(), "published output is not cache data");
assert!(!clean_cache(&root).expect("cleaning an empty cache"));
}
#[test]
fn cleaning_refuses_an_unmarked_populated_cache() {
let directory = crate::testing::workdir("clean-unmarked-cache-");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the workspace path is UTF-8");
let base = gamma_base(&root, None);
fs::create_dir_all(base.join("workspace")).expect("unowned contents");
let failure = clean_cache(&root).expect_err("unowned contents must not be removed");
assert!(failure.is_usage(), "{failure}");
assert!(base.join("workspace").exists(), "unowned contents must survive");
assert!(!base.join(CACHE_OWNER).exists(), "the cache must not be adopted");
}
#[test]
fn cleaning_refuses_a_cache_owned_by_an_active_run() {
let directory = crate::testing::workdir("clean-active-cache-");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the workspace path is UTF-8");
let base = gamma_base(&root, None);
fs::create_dir_all(&base).expect("cache");
let _held = claim(&base).expect("active run lock");
let failure = clean_cache(&root).expect_err("an active cache must not be cleaned");
assert!(failure.to_string().contains("already using"), "{failure}");
}
#[test]
fn the_vendored_runtime_is_the_real_one() {
let runtime = RUNTIME_SOURCES
.iter()
.find_map(|(name, source)| (*name == "runtime.rs").then_some(*source))
.expect("runtime.rs is one of the embedded runtime sources");
assert!(runtime.contains("pub fn a(id: u32) -> bool"));
assert!(runtime.contains("GAMMA_ACTIVE"));
}
#[test]
fn vendoring_writes_a_buildable_crate() {
let temporary = tempfile::tempdir().unwrap();
let at = Utf8PathBuf::from_path_buf(temporary.path().join("rt")).unwrap();
vendor_runtime(&at).unwrap();
let manifest = fs::read_to_string(at.join("Cargo.toml").as_std_path()).unwrap();
assert!(manifest.contains("name = \"cargo-gamma-rt\""));
assert!(manifest.contains("[lib]\nname = \"gamma_rt\""));
assert!(manifest.contains("edition = \"2024\""));
assert!(manifest.contains("rust-version = \"1.95\""));
assert!(manifest.contains("check-cfg = ['cfg(coverage_nightly)', 'cfg(loom)']"));
assert!(manifest.contains("[workspace]"));
for (name, _contents) in RUNTIME_SOURCES {
assert!(at.join("src").join(name).as_std_path().is_file(), "{name} was not vendored");
}
let checked = Command::new(cargo_binary())
.args(["check", "--offline", "--manifest-path"])
.arg(at.join("Cargo.toml"))
.env("CARGO_TARGET_DIR", at.join("target"))
.output()
.expect("cargo checks the vendored runtime");
assert!(checked.status.success(), "{}", String::from_utf8_lossy(&checked.stderr));
}
#[test]
fn vendoring_into_a_location_whose_src_directory_cannot_be_created_reports_the_failure() {
let temporary = tempfile::tempdir().unwrap();
let at = Utf8PathBuf::from_path_buf(temporary.path().join("rt")).unwrap();
fs::create_dir_all(at.as_std_path()).unwrap();
fs::write(at.join("src").as_std_path(), "not a directory").unwrap();
let cause = vendor_runtime(&at).unwrap_err();
assert!(cause.to_string().contains("could not create"), "{cause}");
}
#[test]
fn vendoring_a_manifest_blocked_by_a_directory_reports_the_write_failure() {
let temporary = tempfile::tempdir().unwrap();
let at = Utf8PathBuf::from_path_buf(temporary.path().join("rt")).unwrap();
fs::create_dir_all(at.join("Cargo.toml").as_std_path()).unwrap();
let cause = vendor_runtime(&at).unwrap_err();
assert!(cause.to_string().contains("could not write the runtime manifest"), "{cause}");
}
#[test]
fn vendoring_a_source_file_blocked_by_a_directory_reports_the_write_failure() {
let temporary = tempfile::tempdir().unwrap();
let at = Utf8PathBuf::from_path_buf(temporary.path().join("rt")).unwrap();
fs::create_dir_all(at.join("src").join("lib.rs").as_std_path()).unwrap();
let cause = vendor_runtime(&at).unwrap_err();
assert!(cause.to_string().contains("could not write the runtime source"), "{cause}");
}
#[test]
fn a_run_that_never_settled_takes_its_build_output_with_it() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
fs::write(target.join("artifact").as_std_path(), "x").unwrap();
drop(unsettled(&base, &root, &target));
assert!(!root.as_std_path().exists());
assert!(!target.as_std_path().exists());
}
#[test]
fn a_run_that_settled_keeps_its_tree_and_build_output_for_the_next_one() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
let work = unsettled(&base, &root, &target);
work.settle();
drop(work);
assert!(root.as_std_path().exists(), "the tree is kept for delta sync on the next run");
assert!(target.as_std_path().exists());
}
#[test]
fn a_leaked_tree_keeps_everything() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
let mut work = unsettled(&base, &root, &target);
work.leak = true;
drop(work);
assert!(root.as_std_path().exists());
assert!(target.as_std_path().exists());
}
#[test]
fn an_explicit_teardown_removes_the_tree_and_leaves_the_destructor_nothing_to_do() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
let mut work = unsettled(&base, &root, &target);
work.teardown().expect("a tree that exists must tear down cleanly");
assert!(!root.as_std_path().exists());
assert!(!target.as_std_path().exists());
assert!(work.torn_down, "the destructor would walk the tree a second time");
fs::create_dir_all(root.as_std_path()).unwrap();
drop(work);
assert!(root.as_std_path().exists(), "the destructor repeated a teardown already done");
}
#[test]
fn a_second_teardown_is_not_a_failure() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
let mut work = unsettled(&base, &root, &target);
work.teardown().expect("the first teardown");
work.teardown().expect("the second teardown must agree with the first");
}
#[test]
fn an_explicit_teardown_keeps_the_tree_and_build_output_of_a_settled_run() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
let mut work = unsettled(&base, &root, &target);
work.settle();
work.teardown().expect("a settled tree must tear down cleanly");
assert!(root.as_std_path().exists(), "the tree is kept for delta sync");
assert!(target.as_std_path().exists());
}
#[test]
fn an_explicit_teardown_keeps_a_leaked_tree() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
let mut work = unsettled(&base, &root, &target);
work.leak = true;
work.teardown().expect("a leaked tree tears down by leaving everything alone");
assert!(root.as_std_path().exists());
assert!(target.as_std_path().exists());
}
#[test]
fn a_tree_that_cannot_be_removed_is_reported() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::write(root.as_std_path(), "not a directory").unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
let mut work = unsettled(&base, &root, &target);
let failure = work.teardown().expect_err("a tree that is a file cannot be removed");
assert!(failure.to_string().contains("could not remove the scratch directory"), "{failure}");
assert!(!target.as_std_path().exists(), "the failure stopped the rest of the teardown");
}
#[test]
fn a_teardown_of_a_tree_that_was_never_created_succeeds() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
let mut work = unsettled(&base, &root, &target);
work.teardown().expect("nothing to remove is the outcome asked for");
}
#[test]
fn the_footprint_counts_everything_the_run_leaves_behind() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_owned()).unwrap();
let root = base.join("workspace");
let target = base.join("target");
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(target.as_std_path()).unwrap();
fs::write(root.join("a.rs").as_std_path(), "0123456789").unwrap();
fs::write(target.join("a.o").as_std_path(), "01234").unwrap();
let mut work = unsettled(&base, &root, &target);
assert_eq!(footprint(work.base()), 15);
work.leak = true;
}
fn unsettled(base: &Utf8Path, root: &Utf8Path, target: &Utf8Path) -> Workspace {
Workspace {
root: root.to_owned(),
runtime: base.join("rt"),
target: target.to_owned(),
libraries: Vec::new(),
cargo: CargoOptions::default(),
nextest: None,
settled: AtomicBool::new(false),
leak: false,
launch: OnceLock::new(),
harness_threads: OnceLock::new(),
_workspace_lock: File::create(base.join("lock").as_std_path()).unwrap(),
_cache_lock: None,
torn_down: false,
}
}
#[test]
fn the_harness_width_is_settled_on_the_workspace_rather_than_on_this_process() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let child = run_child("calibrate", &[(TEST_THREADS_VAR, None), (CHILD_JOBS_VAR, Some("1"))]);
assert_eq!(child["before"], PAYLOAD_MISSING, "nothing is settled before calibration");
assert_eq!(
child["ambient"], PAYLOAD_MISSING,
"calibrating must not write the variable into the process environment"
);
assert_eq!(child["threads"], child["cores"], "one worker gets the whole machine");
}
#[test]
fn a_caller_who_chose_a_harness_width_gets_no_setting_from_the_run() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let child = run_child("calibrate", &[(TEST_THREADS_VAR, Some("3")), (CHILD_JOBS_VAR, Some("4"))]);
assert_eq!(child["threads"], PAYLOAD_MISSING, "the caller's choice stands");
}
#[test]
fn the_build_cannot_see_an_active_mutant() {
let work = unsettled_default();
let command = work.cargo();
let scrubbed = command
.get_envs()
.any(|(key, value)| key == gamma_rt::ACTIVE_VAR && value.is_none());
assert!(scrubbed, "the build environment must not carry {}", gamma_rt::ACTIVE_VAR);
}
const CHILD_SCENARIO_VAR: &str = "GAMMA_ENV_CHILD_SCENARIO";
const CHILD_JOBS_VAR: &str = "GAMMA_ENV_CHILD_JOBS";
const PAYLOAD_OPEN: &str = "<<<GAMMA-ENV-PAYLOAD";
const PAYLOAD_CLOSE: &str = "GAMMA-ENV-PAYLOAD>>>";
const PAYLOAD_MISSING: &str = "<missing>";
const PAYLOAD_REMOVED: &str = "<removed>";
const TARGET_RUSTFLAGS_VAR: &str = "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS";
#[test]
fn env_child_helper() {
let Ok(scenario) = env::var(CHILD_SCENARIO_VAR) else {
return;
};
let mut payload: Vec<(&str, String)> = Vec::new();
match scenario.as_str() {
"cargo_binary" => payload.push(("cargo", cargo_binary())),
"cargo_flags" => {
let command = unsettled_default().cargo();
payload.push(("encoded", command_env(&command, "CARGO_ENCODED_RUSTFLAGS")));
payload.push(("rustflags", command_env(&command, "RUSTFLAGS")));
payload.push(("build", command_env(&command, "CARGO_BUILD_RUSTFLAGS")));
payload.push(("target", command_env(&command, TARGET_RUSTFLAGS_VAR)));
}
"calibrate" => {
let jobs = env::var(CHILD_JOBS_VAR).ok().and_then(|value| value.parse().ok()).unwrap_or(1);
let work = unsettled_default();
payload.push(("before", option_payload(work.harness_threads())));
work.calibrate_harness(jobs);
payload.push(("threads", option_payload(work.harness_threads())));
payload.push(("cores", thread::available_parallelism().map_or(1, NonZeroUsize::get).to_string()));
payload.push((
"ambient",
env::var(TEST_THREADS_VAR).unwrap_or_else(|_absent| PAYLOAD_MISSING.to_owned()),
));
}
other => panic!("unknown child scenario `{other}`"),
}
let mut printed = format!("{PAYLOAD_OPEN}\n");
for (key, value) in &payload {
printed.push_str(key);
printed.push('=');
printed.push_str(value);
printed.push('\n');
}
printed.push_str(PAYLOAD_CLOSE);
println!("{printed}");
}
fn run_child(scenario: &str, vars: &[(&str, Option<&str>)]) -> BTreeMap<String, String> {
let executable = env::current_exe().expect("the test binary knows its own path");
let mut command = Command::new(executable);
let module = module_path!();
let relative = module.split_once("::").map_or(module, |(_crate_name, rest)| rest);
let target = format!("{relative}::env_child_helper");
let _ = command.args([target.as_str(), "--exact", "--nocapture"]);
let _ = command.env(CHILD_SCENARIO_VAR, scenario);
for (key, value) in vars {
match value {
Some(value) => {
let _ = command.env(key, value);
}
None => {
let _ = command.env_remove(key);
}
}
}
let output = command.output().expect("the child test binary runs");
let stdout = String::from_utf8(output.stdout).expect("the child prints UTF-8");
if let Some(payload) = parse_payload(&stdout) {
return payload;
}
panic!(
"the `env_child_helper` child produced no payload (status {status}); stdout:\n{stdout}\nstderr:\n{stderr}",
status = output.status,
stderr = String::from_utf8_lossy(&output.stderr),
);
}
fn parse_payload(stdout: &str) -> Option<BTreeMap<String, String>> {
let mut lines = stdout.lines().skip_while(|line| *line != PAYLOAD_OPEN);
let _ = lines.next()?;
let mut payload = BTreeMap::new();
for line in lines {
if line == PAYLOAD_CLOSE {
return Some(payload);
}
if let Some((key, value)) = line.split_once('=') {
let _ = payload.insert(key.to_owned(), value.to_owned());
}
}
None
}
fn command_env(command: &Command, key: &str) -> String {
command.get_envs().find(|(name, _value)| *name == OsStr::new(key)).map_or_else(
|| PAYLOAD_MISSING.to_owned(),
|(_name, value)| value.map_or_else(|| PAYLOAD_REMOVED.to_owned(), |value| value.to_string_lossy().into_owned()),
)
}
fn option_payload(value: Option<&str>) -> String {
value.map_or_else(|| PAYLOAD_MISSING.to_owned(), ToOwned::to_owned)
}
#[test]
fn the_child_helper_isolates_the_environment_from_the_parent() {
const INJECTED: &str = "/gamma/isolated/child/only/cargo";
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let before = env::var_os("CARGO");
let child = run_child("cargo_binary", &[("CARGO", Some(INJECTED))]);
assert_eq!(child["cargo"], INJECTED, "the child read the value it was launched with");
assert_eq!(env::var_os("CARGO"), before, "the child's environment did not leak into the parent");
}
#[test]
fn cargo_binary_prefers_the_invoking_cargo_but_falls_back_when_it_is_unset() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let honoured = run_child("cargo_binary", &[("CARGO", Some("/opt/pinned/toolchain/bin/cargo"))]);
assert_eq!(
honoured["cargo"], "/opt/pinned/toolchain/bin/cargo",
"a set CARGO is honoured verbatim"
);
let fallback = run_child("cargo_binary", &[("CARGO", None)]);
assert_eq!(fallback["cargo"], "cargo", "an unset CARGO falls back to the bare name");
}
fn unsettled_at(root: Utf8PathBuf, runtime: Utf8PathBuf) -> Workspace {
Workspace {
root,
runtime,
target: Utf8PathBuf::from("/scratch/build"),
libraries: Vec::new(),
cargo: CargoOptions::default(),
nextest: None,
settled: AtomicBool::new(true),
leak: true,
launch: OnceLock::new(),
harness_threads: OnceLock::new(),
_workspace_lock: tempfile::tempfile().unwrap(),
_cache_lock: None,
torn_down: false,
}
}
fn unsettled_default() -> Workspace {
Workspace {
root: Utf8PathBuf::from("/tmp/gamma-root"),
runtime: Utf8PathBuf::from("/tmp/gamma-rt"),
target: Utf8PathBuf::from("/tmp/gamma-target"),
libraries: Vec::new(),
cargo: CargoOptions::default(),
nextest: None,
settled: AtomicBool::new(true),
leak: true,
launch: OnceLock::new(),
harness_threads: OnceLock::new(),
_workspace_lock: tempfile::tempfile().unwrap(),
_cache_lock: None,
torn_down: false,
}
}
#[test]
fn an_ambient_encoded_rustflags_is_extended_rather_than_replaced() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let child = run_child(
"cargo_flags",
&[("CARGO_ENCODED_RUSTFLAGS", Some("--cfg\u{1f}loom")), ("RUSTFLAGS", None)],
);
let value = &child["encoded"];
assert!(value.contains("--cfg\u{1f}loom"), "{value}");
assert!(value.contains(CAP_LINTS), "{value}");
}
#[test]
fn an_ambient_rustflags_is_extended_rather_than_replaced() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let child = run_child(
"cargo_flags",
&[("CARGO_ENCODED_RUSTFLAGS", None), ("RUSTFLAGS", Some("--cfg loom"))],
);
let value = &child["rustflags"];
assert!(value.contains("--cfg loom"), "{value}");
assert!(value.contains(CAP_LINTS), "{value}");
}
#[test]
fn an_ambient_build_rustflags_carries_the_cap() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let child = run_child(
"cargo_flags",
&[
("CARGO_ENCODED_RUSTFLAGS", None),
("RUSTFLAGS", None),
("CARGO_BUILD_RUSTFLAGS", Some("-D warnings")),
],
);
let value = &child["build"];
assert!(value.contains("-D warnings"), "{value}");
assert!(value.contains(CAP_LINTS), "{value}");
}
#[test]
fn an_ambient_target_rustflags_carries_the_cap() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let child = run_child(
"cargo_flags",
&[
("CARGO_ENCODED_RUSTFLAGS", None),
("RUSTFLAGS", None),
(TARGET_RUSTFLAGS_VAR, Some("-D warnings")),
],
);
let value = &child["target"];
assert!(value.contains("-D warnings"), "{value}");
assert!(value.contains(CAP_LINTS), "{value}");
}
#[test]
fn a_global_rustflags_leaves_the_lower_spellings_alone() {
if env::var_os(crate::exec::UNDER_GAMMA_VAR).is_some() {
return;
}
let child = run_child(
"cargo_flags",
&[
("CARGO_ENCODED_RUSTFLAGS", None),
("RUSTFLAGS", Some("--cfg loom")),
("CARGO_BUILD_RUSTFLAGS", Some("-D warnings")),
],
);
assert!(child["rustflags"].contains(CAP_LINTS), "{}", child["rustflags"]);
assert_eq!(
child["build"], PAYLOAD_MISSING,
"cargo will not read this one, so it must be left as it was"
);
}
#[test]
fn nextest_inventories_the_profile_gamma_built() {
let mut work = unsettled_default();
work.cargo.features = vec!["--all-features".to_owned()];
work.cargo.profile = Some("mutants".to_owned());
let binaries = [TestBinary {
package: "subject".to_owned(),
package_id: "path+file:///tmp/subject#subject@0.1.0".to_owned(),
..crate::testing::test_binary("/tmp/subject")
}];
let command = work.nextest_list_command(&binaries);
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(
args,
vec![
"nextest",
"list",
"--list-type",
"binaries-only",
"--message-format",
"json",
"--package",
"path+file:///tmp/subject#subject@0.1.0",
"--all-features",
"--cargo-profile",
"mutants",
]
);
}
#[test]
fn the_default_cache_directory_name_is_pinned_to_this_crate() {
assert_eq!(workspace_identity(Utf8Path::new("/workspace")), "155d8208f4c61a79");
assert_eq!(workspace_identity(Utf8Path::new("/workspace/one")), "85dacdefd093e7c1");
assert_ne!(
workspace_identity(Utf8Path::new("/workspace/one")),
workspace_identity(Utf8Path::new("/workspace/two"))
);
let expected = workspace_identity(&absolute(Utf8Path::new("/workspace")));
assert!(
gamma_base(Utf8Path::new("/workspace"), None).as_str().ends_with(&expected),
"{}",
gamma_base(Utf8Path::new("/workspace"), None)
);
}
#[test]
fn a_default_cache_directory_claimed_by_another_workspace_is_refused() {
let directory = crate::testing::workdir("default-owner");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the scratch path is UTF-8");
let base = root.join("cache");
let mine = root.join("mine");
let theirs = root.join("theirs");
fs::create_dir_all(base.as_std_path()).expect("an empty cache");
fs::create_dir_all(mine.as_std_path()).expect("one workspace root");
fs::create_dir_all(theirs.as_std_path()).expect("another workspace root");
validate_cache_owner(&mine, &base, CacheKind::Default).expect("an unclaimed default cache is adopted");
assert_eq!(
fs::read_to_string(base.join(CACHE_OWNER).as_std_path()).expect("the owner marker"),
physical(&mine).as_str()
);
validate_cache_owner(&mine, &base, CacheKind::Default).expect("the owning workspace reuses its cache");
let failure = validate_cache_owner(&theirs, &base, CacheKind::Default).expect_err("a colliding workspace must be refused");
assert!(failure.to_string().contains(physical(&mine).as_str()), "{failure}");
assert!(failure.to_string().contains("--cache-dir"), "{failure}");
assert!(failure.is_usage(), "{failure}");
}
#[test]
fn an_unmarked_populated_cache_is_refused_for_default_and_redirected_paths() {
let directory = crate::testing::workdir("unmarked-cache");
let base = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the scratch path is UTF-8");
let source = base.join("source");
fs::create_dir_all(source.as_std_path()).expect("a workspace root");
fs::create_dir_all(base.join("target").as_std_path()).expect("cached build output");
for kind in [CacheKind::Redirected, CacheKind::Default] {
let failure = validate_cache_owner(&source, &base, kind).expect_err("existing contents are unowned");
assert!(failure.is_usage(), "{failure}");
assert!(!base.join(CACHE_OWNER).exists(), "nothing may be claimed on the refused path");
}
}
#[test]
fn scratch_tree_is_derived_from_the_same_base_as_prepare() {
assert_eq!(
scratch_tree(Utf8Path::new("/workspace"), None),
gamma_base(Utf8Path::new("/workspace"), None).join("workspace")
);
assert_eq!(
scratch_tree(Utf8Path::new("/workspace"), Some(Utf8Path::new("/scratch"))),
gamma_base(Utf8Path::new("/workspace"), Some(Utf8Path::new("/scratch"))).join("workspace")
);
}
#[test]
fn the_default_scratch_tree_cannot_rediscover_workspace_cargo_configuration() {
let root = Utf8Path::new("/workspace");
let base = gamma_base(root, None);
assert!(!base.starts_with(root), "{base}");
assert!(
!base.ancestors().any(|ancestor| ancestor == root.join(".cargo")),
"the real workspace configuration remains in Cargo's scratch ancestor chain: {base}"
);
}
#[test]
fn array_rustflags_from_workspace_config_reach_scratch_cargo_once() {
let directory = crate::testing::workdir("scratch-config-once");
let source = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("utf8");
let capture = source.join("captured-flags");
fs::create_dir_all(source.join(".cargo").as_std_path()).expect(".cargo");
fs::create_dir_all(source.join("src").as_std_path()).expect("src");
fs::write(
source.join("Cargo.toml").as_std_path(),
"[package]\nname = \"scratch-config-once\"\nversion = \"0.0.0\"\nedition = \"2024\"\nbuild = \"build.rs\"\n\n[workspace]\n",
)
.expect("manifest");
fs::write(source.join("src/lib.rs").as_std_path(), "").expect("lib");
fs::write(
source.join("build.rs").as_std_path(),
"fn main() { std::fs::write(std::env::var(\"GAMMA_CAPTURE\").unwrap(), std::env::var(\"CARGO_ENCODED_RUSTFLAGS\").unwrap()).unwrap(); }\n",
)
.expect("build script");
fs::write(
source.join(".cargo/config.toml").as_std_path(),
"[build]\nrustflags = [\"--cfg\", \"gamma_once\"]\n",
)
.expect("config");
let mut events = crate::testing::Recorder::default();
let work = Workspace::prepare(&source, &Config::default(), &mut events).expect("prepare");
let mut command = work.cargo();
let status = command
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.env_remove("RUSTFLAGS")
.env_remove("CARGO_BUILD_RUSTFLAGS")
.env("GAMMA_CAPTURE", capture.as_std_path())
.arg("check")
.status()
.expect("cargo check");
assert!(status.success(), "{status}");
let flags = fs::read_to_string(capture.as_std_path()).expect("captured flags");
assert_eq!(flags.matches("gamma_once").count(), 1, "{flags}");
}
#[test]
fn a_relative_cache_directory_is_resolved_against_the_current_directory() {
let cwd = Utf8PathBuf::from_path_buf(env::current_dir().unwrap()).unwrap();
let base = gamma_base(Utf8Path::new("/workspace"), Some(Utf8Path::new("scratch/here")));
assert_eq!(base, cwd.join("scratch/here"));
assert!(base.is_absolute(), "{base}");
assert!(scratch_tree(Utf8Path::new("/workspace"), Some(Utf8Path::new("scratch/here"))).is_absolute());
assert_eq!(gamma_base(Utf8Path::new("."), None), gamma_base(&cwd, None));
}
#[test]
fn an_absolute_cache_directory_outside_the_workspace_is_left_alone() {
let base = gamma_base(Utf8Path::new("/workspace"), Some(Utf8Path::new("/elsewhere/scratch")));
assert_eq!(base, absolute(Utf8Path::new("/elsewhere/scratch")));
ensure_copy_terminates(Utf8Path::new("/workspace"), &base).expect("a scratch directory outside the workspace is fine");
let inside = gamma_base(Utf8Path::new("/workspace"), None);
ensure_copy_terminates(Utf8Path::new("/workspace"), &inside).expect("the default base is outside the copy");
assert!(!inside.starts_with("/workspace"), "{inside}");
}
#[test]
fn a_scratch_directory_the_copy_cannot_prune_is_refused() {
let source = absolute(Utf8Path::new("/workspace/gamma"));
let base = gamma_base(&source, Some(&source));
assert_eq!(base, source);
assert!(!prunes(&source, &base));
let failure = ensure_copy_terminates(&source, &base).expect_err("an unprunable base must be refused");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains(source.as_str()), "{failure}");
assert!(failure.to_string().contains("--cache-dir"), "{failure}");
}
#[test]
#[cfg(unix)]
fn a_scratch_directory_linked_back_into_the_workspace_is_refused() {
let directory = crate::testing::workdir("scratch-linked");
let root = Utf8Path::from_path(directory.path()).expect("the scratch path is UTF-8");
let (source, link) = (root.join("workspace"), root.join("link"));
fs::create_dir_all(source.join("inside").as_std_path()).expect("the workspace is creatable");
std::os::unix::fs::symlink(source.join("inside").as_std_path(), link.as_std_path()).expect("the link is creatable");
let base = gamma_base(&source, Some(&link));
assert!(!base.starts_with(&source), "{base}");
let failure = ensure_copy_terminates(&source, &base).expect_err("a base linked into the workspace must be refused");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains("--cache-dir"), "{failure}");
}
#[cfg(unix)]
#[test]
fn a_scratch_directory_reached_through_an_in_workspace_link_is_refused() {
let directory = crate::testing::workdir("scratch-through-link");
let root = Utf8Path::from_path(directory.path()).expect("the scratch path is UTF-8");
fs::create_dir_all(root.join("workspace").join("inside").as_std_path()).expect("the workspace is creatable");
let source = physical(&root.join("workspace"));
std::os::unix::fs::symlink(source.join("inside").as_std_path(), source.join("link").as_std_path()).expect("the link is creatable");
let base = gamma_base(&source, Some(&source.join("link")));
assert!(prunes(&source, &base), "source={source} base={base}");
let failure = ensure_copy_terminates(&source, &base).expect_err("a base the exclusion cannot match must be refused");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains("--cache-dir"), "{failure}");
}
#[test]
#[cfg(unix)]
fn a_scratch_directory_linked_to_somewhere_outside_is_still_allowed() {
let directory = crate::testing::workdir("scratch-linked-out");
let root = Utf8Path::from_path(directory.path()).expect("the scratch path is UTF-8");
let (source, elsewhere, link) = (root.join("workspace"), root.join("elsewhere"), root.join("link"));
fs::create_dir_all(source.as_std_path()).expect("the workspace is creatable");
fs::create_dir_all(elsewhere.as_std_path()).expect("the target is creatable");
std::os::unix::fs::symlink(elsewhere.as_std_path(), link.as_std_path()).expect("the link is creatable");
let base = gamma_base(&source, Some(&link));
ensure_copy_terminates(&source, &base).expect("a base that really is outside the workspace is fine");
}
#[test]
fn preparing_with_a_scratch_directory_the_copy_cannot_prune_fails_before_copying() {
let directory = crate::testing::workdir("scratch-inside-");
let outer = Utf8PathBuf::from_path_buf(directory.path().to_path_buf()).expect("the scratch path is UTF-8");
let source = outer.join("gamma");
fs::create_dir_all(source.as_std_path()).expect("the workspace");
fs::write(source.join("Cargo.toml").as_std_path(), "[workspace]\nmembers = []\n").expect("a manifest");
let config = Config {
cache_dir: Some(source.clone()),
..Config::default()
};
let mut events = crate::testing::Recorder::default();
let failure = Workspace::prepare(&source, &config, &mut events).expect_err("the run must be refused");
assert!(failure.to_string().contains(absolute(&source).as_str()), "{failure}");
assert!(
!source.join("workspace").as_std_path().exists(),
"the refusal came after the copy had already started"
);
}
#[test]
fn relocating_a_workspace_that_exposes_vcs_metadata_is_refused_before_copying() {
let directory = crate::testing::workdir("scratch-vcs-relocation-");
let source = Utf8PathBuf::from_path_buf(directory.path().join("source")).expect("UTF-8 path");
let scratch = Utf8PathBuf::from_path_buf(directory.path().join("external")).expect("UTF-8 path");
let marker = source.join(".git/HEAD");
fs::create_dir_all(source.join(".git").as_std_path()).expect("VCS metadata");
fs::write(marker.as_std_path(), "ref: refs/heads/main\n").expect("VCS metadata");
fs::write(source.join("Cargo.toml").as_std_path(), "[workspace]\nmembers = []\n").expect("manifest");
let config = Config {
cache_dir: Some(scratch),
..Config::default()
};
let failure = Workspace::prepare(&source, &config, &mut crate::testing::Recorder::default())
.expect_err("a relocated tree must not lose VCS metadata");
assert!(failure.is_usage(), "{failure}");
assert!(failure.to_string().contains("--cache-dir"), "{failure}");
assert_eq!(
fs::read_to_string(marker.as_std_path()).expect("VCS metadata"),
"ref: refs/heads/main\n"
);
}
#[test]
fn a_scratch_path_that_names_nothing_is_reduced_to_one_spelling() {
assert_eq!(absolute(Utf8Path::new("/a/./b/../c")), absolute(Utf8Path::new("/a/c")));
assert!(
absolute(Utf8Path::new("/../a"))
.components()
.any(|component| component == Utf8Component::ParentDir)
);
}
#[test]
fn linking_a_package_with_no_manifest_is_a_noop() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
fs::create_dir_all(root.join("crate").join("src").as_std_path()).unwrap();
let work = unsettled_at(root, Utf8PathBuf::from("/scratch/rt"));
let files = vec![TargetFile {
package: "pkg".to_owned(),
path: Utf8PathBuf::from("crate/src/lib.rs"),
absolute: Utf8PathBuf::from("/source/crate/src/lib.rs"),
}];
work.link_runtime("pkg", &files).unwrap();
assert!(!work.root.join("Cargo.toml").as_std_path().exists());
}
#[test]
fn manifest_lookup_stops_at_the_workspace_root() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
fs::create_dir_all(root.join("crate").join("src").as_std_path()).unwrap();
let work = unsettled_at(root, Utf8PathBuf::from("/scratch/rt"));
let files = vec![TargetFile {
package: "pkg".to_owned(),
path: Utf8PathBuf::from("crate/src/lib.rs"),
absolute: Utf8PathBuf::from("/source/crate/src/lib.rs"),
}];
assert_eq!(work.manifest_of("pkg", &files), None);
}
#[test]
fn a_package_absent_from_the_scanned_files_has_no_manifest() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
fs::create_dir_all(root.join("crate").join("src").as_std_path()).unwrap();
let work = unsettled_at(root, Utf8PathBuf::from("/scratch/rt"));
let files = vec![TargetFile {
package: "pkg".to_owned(),
path: Utf8PathBuf::from("crate/src/lib.rs"),
absolute: Utf8PathBuf::from("/source/crate/src/lib.rs"),
}];
assert_eq!(work.manifest_of("someone-else", &files), None);
work.link_runtime("someone-else", &files)
.expect("an unknown package is a noop, not an error");
}
#[test]
fn linking_a_package_whose_manifest_exists_adds_the_runtime_dependency() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
let runtime = Utf8PathBuf::from_path_buf(temporary.path().join("rt")).unwrap();
fs::create_dir_all(root.join("crate").join("src").as_std_path()).unwrap();
fs::write(
root.join("crate/Cargo.toml").as_std_path(),
"[package]\nname = \"pkg\"\nversion = \"0.0.0\"\nedition = \"2024\"\n",
)
.unwrap();
let work = unsettled_at(root, runtime);
let files = vec![TargetFile {
package: "pkg".to_owned(),
path: Utf8PathBuf::from("crate/src/lib.rs"),
absolute: Utf8PathBuf::from("/source/crate/src/lib.rs"),
}];
work.link_runtime("pkg", &files).expect("a real manifest must be linkable");
let manifest = fs::read_to_string(work.root.join("crate/Cargo.toml").as_std_path()).unwrap();
assert!(manifest.contains(RUNTIME_CRATE), "{manifest}");
}
#[test]
fn overwriting_a_directory_is_refused() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("not-a-file")).unwrap();
let root = path.parent().expect("the temporary directory is the root").to_owned();
fs::create_dir_all(path.as_std_path()).unwrap();
let cause = Workspace::overwrite(&root, &path, "new").unwrap_err();
assert!(cause.to_string().contains("refusing to write"), "{cause}");
}
#[test]
fn overwriting_a_path_the_copy_never_created_is_reported_rather_than_silently_writing_a_new_file() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("never-copied.rs")).unwrap();
let root = path.parent().expect("the temporary directory is the root").to_owned();
let cause = Workspace::overwrite(&root, &path, "new").unwrap_err();
assert!(cause.to_string().contains("which the copy did not create"), "{cause}");
}
#[test]
fn overwriting_a_copied_file_writes_only_when_the_content_actually_differs() {
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("lib.rs")).unwrap();
let root = path.parent().expect("the temporary directory is the root").to_owned();
fs::write(path.as_std_path(), "old").unwrap();
let changed = Workspace::overwrite(&root, &path, "new").expect("a real file must be writable");
assert!(changed, "different content should be reported as written");
assert_eq!(fs::read_to_string(path.as_std_path()).unwrap(), "new");
let unchanged = Workspace::overwrite(&root, &path, "new").expect("identical content must still succeed");
assert!(!unchanged, "identical content should be reported as a no-op");
}
#[cfg(unix)]
#[test]
fn overwriting_a_read_only_file_reports_the_write_failure() {
use std::os::unix::fs::PermissionsExt;
let temporary = tempfile::tempdir().unwrap();
let path = Utf8PathBuf::from_path_buf(temporary.path().join("locked.rs")).unwrap();
let root = path.parent().expect("the temporary directory is the root").to_owned();
fs::write(path.as_std_path(), "old").unwrap();
fs::set_permissions(path.as_std_path(), fs::Permissions::from_mode(0o400)).unwrap();
let cause = Workspace::overwrite(&root, &path, "new").unwrap_err();
fs::set_permissions(path.as_std_path(), fs::Permissions::from_mode(0o600)).unwrap();
assert!(cause.to_string().contains("could not write"), "{cause}");
}
#[cfg(unix)]
#[test]
fn overwriting_through_a_symlinked_directory_prefix_is_refused() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
let outside = Utf8PathBuf::from_path_buf(temporary.path().join("real")).unwrap();
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(outside.as_std_path()).unwrap();
fs::write(outside.join("lib.rs").as_std_path(), "user source").unwrap();
std::os::unix::fs::symlink(outside.as_std_path(), root.join("link").as_std_path()).unwrap();
let through_the_link = root.join("link").join("lib.rs");
assert!(fs::symlink_metadata(through_the_link.as_std_path()).unwrap().is_file());
let cause = Workspace::overwrite(&root, &through_the_link, "instrumented")
.expect_err("a write resolving outside the scratch tree must be refused");
assert!(cause.to_string().contains("outside the scratch tree"), "{cause}");
assert_eq!(fs::read_to_string(outside.join("lib.rs").as_std_path()).unwrap(), "user source");
}
#[cfg(unix)]
#[test]
fn a_manifest_reached_through_a_symlinked_directory_prefix_is_not_returned() {
let temporary = tempfile::tempdir().unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
let outside = Utf8PathBuf::from_path_buf(temporary.path().join("real")).unwrap();
fs::create_dir_all(root.as_std_path()).unwrap();
fs::create_dir_all(outside.join("src").as_std_path()).unwrap();
fs::write(outside.join("Cargo.toml").as_std_path(), "[package]\nname = \"pkg\"\n").unwrap();
fs::write(outside.join("src").join("lib.rs").as_std_path(), "").unwrap();
std::os::unix::fs::symlink(outside.as_std_path(), root.join("link").as_std_path()).unwrap();
let work = Workspace::adopt(root.clone(), root.join("target"));
let files = vec![TargetFile {
path: Utf8PathBuf::from("link/src/lib.rs"),
absolute: root.join("link/src/lib.rs"),
package: "pkg".to_owned(),
}];
assert_eq!(work.manifest_of("pkg", &files), None);
}
#[test]
fn a_second_claim_on_the_same_scratch_directory_is_a_usage_error() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().join("gamma")).unwrap();
fs::create_dir_all(base.as_std_path()).unwrap();
let _held = claim(&base).unwrap();
let cause = claim(&base).unwrap_err();
assert!(cause.is_usage());
assert!(cause.to_string().contains("already using"), "{cause}");
}
#[test]
fn a_filesystem_that_cannot_lock_is_not_reported_as_another_run() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().to_path_buf()).unwrap();
let _armed = faults::arm(Fault::Lock);
let cause = claim(&base).unwrap_err();
let message = cause.to_string();
assert!(message.contains("could not be taken"), "{message}");
assert!(!message.contains("already using"), "{message}");
assert!(!cause.is_usage(), "a filesystem the tool cannot lock is not the user's mistake");
}
#[test]
fn claiming_a_scratch_directory_that_does_not_exist_reports_the_lock_failure() {
let temporary = tempfile::tempdir().unwrap();
let base = Utf8PathBuf::from_path_buf(temporary.path().join("never-created")).unwrap();
let cause = claim(&base).unwrap_err();
assert!(cause.to_string().contains("could not open the scratch lock"), "{cause}");
}
#[cfg(unix)]
#[test]
fn non_utf8_manifests_are_skipped_while_anchoring() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt as _;
let temporary = tempfile::tempdir().unwrap();
let source = Utf8PathBuf::from_path_buf(temporary.path().join("source")).unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
let name = OsString::from_vec(b"bad-\xff".to_vec());
let bad = root.as_std_path().join(name);
fs::create_dir_all(&bad).unwrap();
fs::write(bad.join("Cargo.toml"), "[package]\nname = \"bad\"\nversion = \"0.0.0\"\n").unwrap();
anchor_manifests(&source, &root, &root.join("rt")).unwrap();
assert!(bad.join("Cargo.toml").exists());
}
#[test]
fn files_that_are_not_manifests_are_left_alone_while_anchoring() {
let temporary = tempfile::tempdir().unwrap();
let source = Utf8PathBuf::from_path_buf(temporary.path().join("source")).unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
fs::create_dir_all(root.as_std_path()).unwrap();
fs::write(root.join("Cargo.toml").as_std_path(), "[workspace]\nmembers = []\n").unwrap();
fs::write(root.join("Cargo.lock").as_std_path(), "# not a manifest\n").unwrap();
fs::write(root.join("lib.rs").as_std_path(), "pub fn f() {}\n").unwrap();
anchor_manifests(&source, &root, &root.join("rt")).unwrap();
assert_eq!(
fs::read_to_string(root.join("Cargo.lock").as_std_path()).unwrap(),
"# not a manifest\n"
);
assert_eq!(fs::read_to_string(root.join("lib.rs").as_std_path()).unwrap(), "pub fn f() {}\n");
}
#[cfg(unix)]
#[test]
fn an_external_manifest_link_is_refused_before_its_target_is_rewritten() {
let temporary = tempfile::tempdir().unwrap();
let source = Utf8PathBuf::from_path_buf(temporary.path().join("source")).unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
let outside = Utf8PathBuf::from_path_buf(temporary.path().join("outside.toml")).unwrap();
let original = "[package]\nname = \"outside\"\nversion = \"0.0.0\"\n\n[dependencies]\nshared = { path = \"../shared\" }\n";
fs::create_dir_all(root.as_std_path()).unwrap();
fs::write(outside.as_std_path(), original).unwrap();
std::os::unix::fs::symlink(outside.as_std_path(), root.join("Cargo.toml").as_std_path()).unwrap();
let failure = anchor_manifests(&source, &root, &root.join("rt")).expect_err("an external manifest must not be rewritten");
assert!(failure.to_string().contains("outside"), "{failure}");
assert_eq!(fs::read_to_string(outside.as_std_path()).unwrap(), original);
}
#[cfg(unix)]
#[test]
fn an_external_cargo_configuration_link_is_refused_before_its_target_is_rewritten() {
let temporary = tempfile::tempdir().unwrap();
let source = Utf8PathBuf::from_path_buf(temporary.path().join("source")).unwrap();
let root = Utf8PathBuf::from_path_buf(temporary.path().join("tree")).unwrap();
let outside = Utf8PathBuf::from_path_buf(temporary.path().join("outside-config.toml")).unwrap();
let original = "paths = [\"../shared\"]\n";
fs::create_dir_all(root.join(".cargo").as_std_path()).unwrap();
fs::write(outside.as_std_path(), original).unwrap();
std::os::unix::fs::symlink(outside.as_std_path(), root.join(".cargo/config.toml").as_std_path()).unwrap();
let failure = anchor_manifests(&source, &root, &root.join("rt")).expect_err("an external config must not be rewritten");
assert!(failure.to_string().contains("outside"), "{failure}");
assert_eq!(fs::read_to_string(outside.as_std_path()).unwrap(), original);
}
}