//! Shared scaffolding for tests. Compiled only under `cfg(test)`. //! //! Everything here exists to keep tests hermetic — see [`crate::docs::testing`]. Two //! things live here: a throwaway git repo with the process parked inside it, //! which several modules need because the code under test shells out to git //! in the working directory rather than against an explicit path, and a //! throwaway SSH keypair, which `ssh.rs` and `key.rs` need because the //! thing they get right or wrong is how real key material compares. use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::{Mutex, MutexGuard}; use tempfile::TempDir; /// The working directory is process-wide and cargo runs tests on threads that /// share it, so anything that moves the process has to take this first. /// [`TempRepo`] is the only thing in the tree that does. fn cwd_lock() -> MutexGuard<'static, ()> { static LOCK: Mutex<()> = Mutex::new(()); // A panicking test poisons the lock; the data behind it is `()`, so there // is nothing to have corrupted and the next test may proceed. LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) } /// A throwaway git repo, with the process inside it for as long as the value /// lives. Everything it does is confined to a temp directory: no command run /// through it writes outside `path`, so a test that sets git config can only /// reach this repo's `.git/config` and never the user's. pub struct TempRepo { dir: TempDir, previous: PathBuf, _guard: MutexGuard<'static, ()>, } impl TempRepo { pub fn new(label: &str) -> Self { let guard = cwd_lock(); let previous = std::env::current_dir().expect("a working directory"); // `tempfile` picks the name, so two repos cannot collide however the // suite is run — the old scheme was unique per pid and per call, // which assumed one process at a time. let dir = tempfile::Builder::new() .prefix(&format!("atgc-test-{label}-")) .tempdir() .expect("temp dir"); // -b main so the branch does not depend on whatever // init.defaultBranch the machine running the tests has set. run_git(dir.path(), &["init", "-q", "-b", "main"]); std::env::set_current_dir(dir.path()).expect("enter the temp repo"); TempRepo { dir, previous, _guard: guard, } } fn path(&self) -> &Path { self.dir.path() } pub fn commit(&self, name: &str, body: &str, subject: &str) { std::fs::write(self.path().join(name), body).expect("write a file"); run_git(self.path(), &["add", name]); run_git(self.path(), &["commit", "-q", "-m", subject]); } pub fn git(&self, args: &[&str]) -> String { run_git(self.path(), args) } /// This repo's own config file, to prove a write landed here and not in /// the user's `~/.gitconfig`. pub fn local_config(&self) -> String { std::fs::read_to_string(self.path().join(".git/config")).expect("a local config") } } impl Drop for TempRepo { fn drop(&mut self) { // Leave the directory before it is deleted, and restore whatever the // harness was in — a later test may be relative to it. `dir` drops // straight after this and takes the tree with it. let _ = std::env::set_current_dir(&self.previous); } } /// A throwaway SSH keypair in a directory of its own, generated by /// `ssh-keygen` and deleted on drop. /// /// Real key material rather than a pasted-in constant, because what the key /// tests are about is the agreement between three things this project does /// not control: what `ssh-keygen` writes into a `.pub` file, what a /// `sh.tangled.publicKey` record holds, and what `ssh-keygen -lf` prints as /// the fingerprint. A hardcoded key would pin our own reading of all three /// and prove none of them. /// /// It needs no cwd lock: nothing here moves the process, and every path is /// passed in explicitly. `ssh-keygen` is a hard requirement of the suite, the /// way `git` already is — a machine that can push to Tangled has it. pub struct TempKeys { dir: TempDir, } impl TempKeys { pub fn new(label: &str) -> Self { let dir = tempfile::Builder::new() .prefix(&format!("atgc-test-keys-{label}-")) // 0o700, because these stand in for ~/.config/atgc, which // production creates owner-only. `tempfile` defaults a // directory to 0o777 & ~umask, which would quietly make // every mode assertion below weaker than the real thing. .permissions( ::from_mode(0o700), ) .tempdir() .expect("temp dir"); let keys = TempKeys { dir }; // -N "" is an empty passphrase and -q keeps the randomart off the // test output. The comment is what a real key carries and what // `normalize` has to strip. keys.keygen(&[ "-t", "ed25519", "-N", "", "-q", "-C", &format!("atgc-test-{label}"), "-f", &keys.private().to_string_lossy(), ]); keys } /// The directory holding the pair, which stands in for `~/.ssh`. pub fn dir(&self) -> &Path { self.dir.path() } pub fn private(&self) -> PathBuf { self.dir.path().join("id_ed25519") } pub fn public(&self) -> PathBuf { self.dir.path().join("id_ed25519.pub") } /// The `.pub` file's contents: `ssh-ed25519 atgc-test-