use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const ROOTS: &[(&str, &str)] = &[
("HOME", "home"),
("XDG_DATA_HOME", "data"),
("XDG_RUNTIME_DIR", "runtime"),
("XDG_CONFIG_HOME", "config"),
("XDG_STATE_HOME", "state"),
("XDG_CACHE_HOME", "cache"),
("TMPDIR", "tmp"),
];
const PASSTHROUGH: &[&str] = &[
"PATH",
"LANG",
"LANGUAGE",
"LC_ALL",
"LC_CTYPE",
"LC_COLLATE",
"LC_MESSAGES",
"LC_NUMERIC",
"LC_TIME",
"LC_MONETARY",
"TZ",
"USER",
"LOGNAME",
"RUST_BACKTRACE",
#[cfg(windows)]
"SystemRoot",
];
pub fn fenced_subc_env_dir(scratch: &Path, var: &str) -> PathBuf {
ROOTS
.iter()
.find(|(name, _)| *name == var)
.map(|(_, dir)| scratch.join(dir))
.unwrap_or_else(|| panic!("{var} is not a fenced daemon env root"))
}
pub fn fenced_subc_daemon_env(scratch: &Path) -> Vec<(OsString, OsString)> {
fenced_env_with_roots(scratch, &[])
}
pub(crate) fn fenced_env_with_roots(
scratch: &Path,
extra_roots: &[(&str, &str)],
) -> Vec<(OsString, OsString)> {
let mut names = std::collections::HashSet::new();
for (name, directory) in extra_roots {
assert!(
!name.is_empty()
&& name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
&& !name.as_bytes()[0].is_ascii_digit(),
"extra root must have a valid environment variable name"
);
assert!(
!ROOTS
.iter()
.any(|(builtin, _)| name.eq_ignore_ascii_case(builtin)),
"extra roots cannot replace a built-in directory variable"
);
assert!(
names.insert(name.to_ascii_uppercase()),
"duplicate extra root variable"
);
plain_name(directory);
}
ROOTS
.iter()
.chain(extra_roots)
.map(|(name, dir)| {
let path = scratch.join(dir);
std::fs::create_dir_all(&path).unwrap_or_else(|e| panic!("create fenced {name}: {e}"));
let metadata = std::fs::symlink_metadata(&path).expect("inspect test directory root");
assert!(
metadata.is_dir() && !metadata.is_symlink(),
"test directory root must be a real directory"
);
(OsString::from(name), path.into_os_string())
})
.collect()
}
pub fn fenced_command(program: impl AsRef<OsStr>, scratch: &Path) -> Command {
fenced_command_with_roots(program, scratch, &[])
}
pub fn fenced_command_with_roots(
program: impl AsRef<OsStr>,
scratch: &Path,
extra_roots: &[(&str, &str)],
) -> Command {
let program = crate::stage_test_binary(Path::new(program.as_ref()));
let mut command = crate::dev_command(program);
command.env_clear();
for name in PASSTHROUGH {
if let Some(value) = std::env::var_os(name) {
command.env(name, value);
}
}
command.envs(fenced_env_with_roots(scratch, extra_roots));
command
}
pub fn warm_exec_fenced(binary: &Path) {
let scratch = crate::ScratchDir::new("warm-exec");
if let Err(error) = crate::checked_output(
fenced_command(binary, scratch.path())
.arg("--version")
.stderr(Stdio::null()),
) {
eprintln!("warm-exec: {error}");
}
}
pub fn sign_test_binary(path: &Path, identity: Option<&OsStr>) {
#[cfg(target_os = "macos")]
{
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = LOCK.lock().unwrap_or_else(|p| p.into_inner());
let env = std::env::var_os("CORTEXKIT_TEST_SIGNING_IDENTITY");
let Some(identity) = identity.or(env.as_deref()) else {
return;
};
if Command::new("codesign")
.arg("-v")
.arg(path)
.output()
.is_ok_and(|o| o.status.success())
{
return;
}
if let Err(e) = crate::checked_output(
Command::new("codesign")
.args(["-f", "-s"])
.arg(identity)
.arg(path),
) {
eprintln!("sign-test-binary: {e}; running {} anyway", path.display());
}
}
#[cfg(not(target_os = "macos"))]
let _ = (path, identity);
}
pub fn sibling_cargo_build(repo: &Path, target_dir: &Path, bin: &str) -> Command {
let mut command = Command::new(env!("CARGO"));
command
.current_dir(repo)
.args(["build", "--locked", "--bin", bin])
.env("CARGO_TARGET_DIR", target_dir);
command
}
fn workspace_root() -> PathBuf {
if let Some(root) = std::env::var_os("CORTEXKIT_TEST_WORKSPACE_ROOT") {
return PathBuf::from(root);
}
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.expect("locate test workspace");
assert!(
output.status.success(),
"set CORTEXKIT_TEST_WORKSPACE_ROOT outside a git workspace"
);
PathBuf::from(String::from_utf8(output.stdout).unwrap().trim())
}
pub(crate) fn plain_name(name: &str) {
assert!(
!name.is_empty()
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'),
"expected a plain directory name (ASCII letters, digits, '-' or '_')"
);
}
pub fn sibling_target_dir(name: &str) -> PathBuf {
plain_name(name);
let dir = workspace_root().join("target/sibling-target").join(name);
std::fs::create_dir_all(&dir).unwrap();
dir
}
pub fn sibling_checkout(name: &str) -> Result<PathBuf, String> {
plain_name(name);
let workspace = workspace_root();
let direct = workspace.join("..").join(name);
let primary = || {
let marker = std::fs::read_to_string(workspace.join(".git")).ok()?;
let git_dir = workspace.join(marker.trim().strip_prefix("gitdir: ")?);
let primary = git_dir
.ancestors()
.find(|p| p.file_name().is_some_and(|n| n == ".git"))?
.parent()?;
Some(primary.parent()?.join(name))
};
[Some(direct.clone()), primary()]
.into_iter()
.flatten()
.find(|p| p.exists())
.and_then(|p| p.canonicalize().ok())
.ok_or_else(|| {
format!(
"sibling {name} checkout missing (looked at {})",
direct.display()
)
})
}
#[derive(Debug, Clone)]
pub struct SubcDaemonBinary {
path: PathBuf,
}
impl SubcDaemonBinary {
pub fn build(dependency_checkouts: &[&str]) -> Self {
Self::try_build(dependency_checkouts).unwrap_or_else(|reason| panic!("{reason}"))
}
pub fn try_build(dependency_checkouts: &[&str]) -> Result<Self, String> {
let repo = sibling_checkout("subconscious")?;
Ok(Self {
path: crate::build_sibling_binary(
&repo,
"subconscious",
"ck-subc",
dependency_checkouts,
),
})
}
pub fn command(&self, scratch: &Path) -> crate::TestDaemonCommand {
crate::TestDaemonCommand::new(&self.path, scratch)
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn caller_selected_root_lands_under_scratch() {
let scratch = crate::ScratchDir::new("extra-command-root");
let output = crate::checked_output(&mut fenced_command_with_roots(
"/usr/bin/env",
&scratch,
&[("EXAMPLE_STORAGE_ROOT", "storage")],
))
.unwrap();
let text = String::from_utf8(output.stdout).unwrap();
assert!(text
.lines()
.any(|line| line
== format!("EXAMPLE_STORAGE_ROOT={}", scratch.join("storage").display())));
assert!(scratch.join("storage").is_dir());
}
#[test]
fn extra_roots_cannot_replace_builtin_variables_or_escape_scratch() {
let scratch = crate::ScratchDir::new("extra-root-validation");
for roots in [
vec![("XDG_DATA_HOME", "other")],
vec![("xdg_data_home", "other")],
vec![("EXTRA_ROOT", "..")],
vec![("EXTRA_ROOT", "/outside")],
vec![("EXTRA_ROOT", "../outside")],
vec![("BAD=NAME", "data")],
vec![("EXTRA_ROOT", "first"), ("EXTRA_ROOT", "second")],
] {
assert!(
std::panic::catch_unwind(|| fenced_command_with_roots("env", &scratch, &roots))
.is_err(),
"accepted invalid roots: {roots:?}"
);
}
let outside = crate::ScratchDir::new("extra-root-outside");
std::os::unix::fs::symlink(outside.path(), scratch.join("linked")).unwrap();
assert!(std::panic::catch_unwind(|| fenced_command_with_roots(
"env",
&scratch,
&[("EXTRA_ROOT", "linked")]
))
.is_err());
}
#[test]
fn fenced_command_drops_ambient_variables_and_keeps_the_allowlist() {
let scratch = crate::ScratchDir::new("fence-env");
std::env::set_var("CORTEXKIT_TEST_LEAK_CANARY", "secret");
let output =
crate::checked_output(fenced_command("/usr/bin/env", &scratch).env("EXPLICIT", "kept"))
.unwrap();
let text = String::from_utf8(output.stdout).unwrap();
assert!(!text.contains("CORTEXKIT_TEST_LEAK_CANARY="));
assert!(text.lines().any(|s| s == "EXPLICIT=kept"));
assert!(text
.lines()
.any(|s| s == format!("PATH={}", std::env::var("PATH").unwrap())));
for (name, dir) in ROOTS {
assert!(text
.lines()
.any(|s| s == format!("{name}={}", scratch.join(dir).display())));
}
for line in text.lines() {
let name = line.split_once('=').unwrap().0;
assert!(
ROOTS.iter().any(|(root, _)| *root == name)
|| PASSTHROUGH.contains(&name)
|| name == "EXPLICIT",
"unexpected variable {name}"
);
}
std::env::remove_var("CORTEXKIT_TEST_LEAK_CANARY");
}
#[test]
fn sibling_cargo_build_fails_on_a_stale_lock_and_leaves_it_untouched() {
let scratch = crate::ScratchDir::new("locked-build");
std::fs::write(scratch.join("Cargo.toml"), "[package]\nname='lockprobe'\nversion='0.2.0'\nedition='2021'\n[[bin]]\nname='lockprobe'\npath='main.rs'\n[workspace]\n").unwrap();
std::fs::write(scratch.join("main.rs"), "fn main() {}\n").unwrap();
let lock = |version: &str| {
format!("version = 3\n\n[[package]]\nname = \"lockprobe\"\nversion = \"{version}\"\n")
};
std::fs::write(scratch.join("Cargo.lock"), lock("0.1.0")).unwrap();
let build = || {
sibling_cargo_build(&scratch, &scratch.join("target"), "lockprobe")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.unwrap()
};
assert!(!build().success());
assert_eq!(
std::fs::read_to_string(scratch.join("Cargo.lock")).unwrap(),
lock("0.1.0")
);
std::fs::write(scratch.join("Cargo.lock"), lock("0.2.0")).unwrap();
assert!(build().success());
}
}