//! Fixture helpers shared by the CLI tests. //! //! A test never touches the machine it runs on. `HOME`, `SPECTRAL_KERNEL`, and //! git's global configuration all point into a temp directory that deletes //! itself when the test ends, so `~/.spectral` is never written and the //! developer's git config never leaks in. //! //! The kernel tree, when a test wants one, is a real git repository holding the //! two scripts spectral shells out to, a MAINTAINERS entry, and one commit. #![allow(dead_code)] // each test binary uses a different part of this set use std::os::unix::fs::PermissionsExt as _; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::sync::atomic::{AtomicU32, Ordering}; /// The checkpatch stand-in. /// /// The real one is a kernel script whose output moves with the kernel version, /// which is not something a test can assert on. This records how it was called, /// then says what the test asked for through the environment: /// `FIXTURE_SUMMARY` is printed as checkpatch's summary line, `FIXTURE_EXIT` is /// the exit status, and `FIXTURE_RECORD` names the file the call is noted in. const CHECKPATCH: &str = r#"#!/bin/sh : "${FIXTURE_RECORD:=.checkpatch-call}" { echo "argv: $*" echo "cwd: $(pwd)" cat } >> "$FIXTURE_RECORD" [ -n "$FIXTURE_SUMMARY" ] && printf '%s\n' "$FIXTURE_SUMMARY" exit "${FIXTURE_EXIT:-0}" "#; /// The get_maintainer stand-in, for the same reason. /// /// One line per role the real script emits, including a git signer whose /// address is already on the `To:` line, so the dedup is exercised. const GET_MAINTAINER: &str = r#"#!/bin/sh echo "argv: $*" >> .getmaintainer-call echo "Fixture Maintainer (maintainer:FIXTURE DRIVER)" echo "fixture-list@example.com (open list:FIXTURE DRIVER)" echo "Fixture Reviewer (reviewer:FIXTURE REVIEWERS)" echo "another-list@example.com (open list:FIXTURE REVIEWERS)" echo "\"GitAuthor: Fixture\" (authored,added_lines)" "#; /// The file `return a;` is committed as, and the edit most tests start from. pub const COMMITTED: &str = "static int foo(int a)\n{\n\treturn a;\n}\n"; pub const EDITED: &str = "static int foo(int a)\n{\n\treturn a + 1;\n}\n"; /// A temp directory that removes itself when the test ends. #[derive(Debug)] pub struct TempDir { path: PathBuf, } impl TempDir { fn new(label: &str) -> Self { static NEXT: AtomicU32 = AtomicU32::new(0); let path = std::env::temp_dir().join(format!( "spectral-test-{}-{label}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); std::fs::create_dir_all(&path).expect("create the temp directory"); Self { path } } pub fn path(&self) -> &Path { &self.path } pub fn join(&self, rel: &str) -> PathBuf { self.path.join(rel) } } impl Drop for TempDir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.path); } } /// A throwaway home directory, and a kernel tree when the test wants one. pub struct Fixture { home: TempDir, root: TempDir, } impl Fixture { /// A home with nothing in it but a git identity. pub fn bare() -> Self { let fixture = Self { home: TempDir::new("home"), root: TempDir::new("root"), }; std::fs::write( fixture.home.join("gitconfig"), "[user]\n\tname = Fixture\n\temail = fixture@example.com\n", ) .expect("write the fixture git config"); fixture } /// A git repository shaped like a kernel tree. pub fn kernel_tree() -> Self { let fixture = Self::bare(); // Pinned: `create` defaults to `--base master`, the kernel's branch, // and the fixture should not inherit whatever a machine's git defaults // to for a new repository. fixture.git(&["init", "--quiet", "--initial-branch=master"]); fixture.git(&["config", "user.name", "Fixture"]); fixture.git(&["config", "user.email", "fixture@example.com"]); fixture.write( "MAINTAINERS", "FIXTURE DRIVER\nM:\tFixture Maintainer \nL:\tfixture-list@example.com\nS:\tMaintained\nF:\tdrivers/foo/*\n\nFIXTURE REVIEWERS\nR:\tFixture Reviewer \nL:\tanother-list@example.com\nS:\tOdd Fixes\nF:\tdrivers/foo/*\n", ); fixture.write("drivers/foo/bar.c", COMMITTED); fixture.write_script("scripts/checkpatch.pl", CHECKPATCH); fixture.write_script("scripts/get_maintainer.pl", GET_MAINTAINER); fixture.commit_all("fixture: add bar"); fixture } /// The directory the tests treat as `$HOME`. pub fn home(&self) -> &Path { self.home.path() } /// The kernel tree, which is also the working directory the binary runs in. pub fn tree(&self) -> &Path { self.root.path() } pub fn join(&self, rel: &str) -> PathBuf { self.root.join(rel) } /// Write a file into the tree, making its directory first. pub fn write(&self, rel: &str, contents: &str) { let path = self.join(rel); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).expect("create the file's directory"); } std::fs::write(&path, contents).expect("write the file"); } /// Write an executable script into the tree. pub fn write_script(&self, rel: &str, body: &str) { self.write(rel, body); let path = self.join(rel); let mut permissions = std::fs::metadata(&path) .expect("stat the script") .permissions(); permissions.set_mode(0o755); std::fs::set_permissions(&path, permissions).expect("make the script executable"); } /// Write an executable script into a directory of its own. fn write_script_in(&self, dir: &Path, name: &str, body: &str) { let path = dir.join(name); std::fs::write(&path, body).expect("write the script"); let mut permissions = std::fs::metadata(&path) .expect("stat the script") .permissions(); permissions.set_mode(0o755); std::fs::set_permissions(&path, permissions).expect("make the script executable"); } pub fn read(&self, rel: &str) -> String { std::fs::read_to_string(self.join(rel)).expect("read the file") } /// Run git in the tree and return its trimmed stdout. pub fn git(&self, args: &[&str]) -> String { let output = self.git_command(args).output().expect("run git"); assert!( output.status.success(), "git {} failed: {}", args.join(" "), String::from_utf8_lossy(&output.stderr) ); String::from_utf8_lossy(&output.stdout) .trim_end() .to_owned() } /// Run git expecting a failure, and hand back its stderr. /// /// Used to pin the things spectral works around, such as send-email /// refusing a bare diff. pub fn git_must_fail(&self, args: &[&str]) -> String { let output = self.git_command(args).output().expect("run git"); assert!( !output.status.success(), "git {} was supposed to fail", args.join(" ") ); String::from_utf8_lossy(&output.stderr) .trim_end() .to_owned() } pub fn commit_all(&self, message: &str) { self.git(&["add", "-A"]); self.git(&["commit", "--quiet", "-m", message]); } /// Do the work on a branch of its own, the way the loop expects: `create` /// diffs the branch against `master`, so committing straight to `master` /// leaves it nothing to send. pub fn work_on_a_branch(&self) { self.git(&["checkout", "--quiet", "-b", "work"]); } /// Run the built binary with the fixture's environment. pub fn cli(&self, args: &[&str]) -> Output { self.cli_env(args, &[]) } /// Run the built binary without `$SPECTRAL_KERNEL`, so the config file and /// the built-in default are the only things left to resolve a tree from. /// /// `XDG_CONFIG_HOME` points into the fixture, which is where the test's /// config file lives. pub fn cli_without_the_tree_variable(&self, args: &[&str], env: &[(&str, &str)]) -> Output { let mut command = Command::new(env!("CARGO_BIN_EXE_spectral")); command .args(args) .current_dir(self.root.path()) .env_clear() .env("PATH", std::env::var("PATH").unwrap_or_default()) .env("HOME", self.home.path()) .env("XDG_CONFIG_HOME", self.home.join("config")) .env("GIT_CONFIG_GLOBAL", self.home.join("gitconfig")) .env("GIT_CONFIG_SYSTEM", "/dev/null"); for (key, value) in env { command.env(key, value); } command.output().expect("run spectral") } /// Write a config file into the fixture's own config directory. pub fn write_config(&self, contents: &str) -> PathBuf { let path = self.home.join("config/spectral/config.toml"); std::fs::create_dir_all(path.parent().expect("the config directory")) .expect("create the config directory"); std::fs::write(&path, contents).expect("write the config file"); path } /// Run the built binary with extra environment on top of the fixture's. pub fn cli_env(&self, args: &[&str], env: &[(&str, &str)]) -> Output { let mut command = Command::new(env!("CARGO_BIN_EXE_spectral")); command .args(args) .current_dir(self.root.path()) .env_clear() .env("PATH", std::env::var("PATH").unwrap_or_default()) .env("HOME", self.home.path()) .env("SPECTRAL_KERNEL", self.root.path()) .env("GIT_CONFIG_GLOBAL", self.home.join("gitconfig")) .env("GIT_CONFIG_SYSTEM", "/dev/null"); for (key, value) in env { command.env(key, value); } command.output().expect("run spectral") } fn git_command(&self, args: &[&str]) -> Command { let mut command = Command::new("git"); command .arg("-C") .arg(self.root.path()) .args(args) .env("HOME", self.home.path()) .env("GIT_CONFIG_GLOBAL", self.home.join("gitconfig")) .env("GIT_CONFIG_SYSTEM", "/dev/null"); command } /// A `PATH` holding symlinks to exactly these programs, found on the /// machine's own path. /// /// This is how a test takes a tool away: uninstalling qemu to prove the /// missing-qemu path would be absurd, and pointing `PATH` somewhere narrow /// asserts the same thing without touching the host. pub fn restricted_path(&self, programs: &[&str]) -> PathBuf { let bin = self.home.join("bin"); std::fs::create_dir_all(&bin).expect("create the restricted bin directory"); for program in programs { let found = self.program(program); std::os::unix::fs::symlink(found, bin.join(program)).expect("link the program"); } bin } /// A `PATH` whose `git` is the machine's, except that `send-email` answers /// with `answer` instead of running. /// /// The harness controls `PATH` and git's configuration but not git's own /// exec-path, so whether the host has git's send-email support installed is /// otherwise inherited from the machine. This makes the answer the test's, /// in both directions. pub fn path_with_send_email(&self, answer: u32) -> PathBuf { let bin = self.home.join("gitbin"); std::fs::create_dir_all(&bin).expect("create the git bin directory"); let real = self.program("git"); let wrapper = format!( "#!/bin/sh\nif [ \"$1\" = send-email ]; then exit {answer}; fi\nexec {} \"$@\"\n", real.display() ); self.write_script_in(&bin, "git", &wrapper); bin } /// The machine's own copy of a program, found on its `PATH`. fn program(&self, name: &str) -> PathBuf { std::env::var("PATH") .unwrap_or_default() .split(':') .map(|dir| Path::new(dir).join(name)) .find(|candidate| candidate.is_file()) .unwrap_or_else(|| panic!("{name} is not on this machine's PATH")) } }