Files
spectral/tests/common/mod.rs
T
huntedbytheirs 97664806b7 patch: fill in the loop verbs, and test them
Every patch verb outside the series work is real. check reads the working
tree through checkpatch's stdin, which is the only way it can read one,
since --git with no revision dies; format puts --fix-inplace's output back
into the tree after setting the uncommitted change aside, and restores it
if the fix does not apply; commit and create sit on git; create writes a
mail-formatted patch, because git send-email refuses a bare diff with "No
subject line"; submit splits get_maintainer.pl's roles into To and Cc,
prints the command line it is about to run, and only sends when --dry-run
is off; update re-rolls a patch by renaming it and does nothing at the
revision it is already at.

git.rs grows the plumbing those verbs stand on: diff_unstaged,
format_patch, apply, commit, rev_parse. apply feeds the patch in on stdin,
so a fixed patch stays in memory until it is known to apply.

tests/ holds a fixture kernel tree, so the suite needs no kernel tree, no
network, and no configured git send-email. The two tests that do want the
host's own checkpatch and get_maintainer are ignored unless asked for, and
one of them earned its place immediately: checkpatch's --file means "this
argument is source code", so checking a patch with it reported a dirty
patch as clean, 8004 lines and no errors. Patchfile mode is checkpatch's
default and the path now stands alone.

patch check also takes --rev, which is how a committed range gets checked.

Checked with cargo fmt --check, cargo clippy --all-targets -- -D warnings,
cargo test, and cargo doc --no-deps.
2026-09-16 18:41:51 -04:00

250 lines
8.5 KiB
Rust

//! 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 <[email protected]> (maintainer:FIXTURE DRIVER)"
echo "[email protected] (open list:FIXTURE DRIVER)"
echo "Fixture Reviewer <[email protected]> (reviewer:FIXTURE REVIEWERS)"
echo "[email protected] (open list:FIXTURE REVIEWERS)"
echo "\"GitAuthor: Fixture\" <[email protected]> (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 = [email protected]\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", "[email protected]"]);
fixture.write(
"MAINTAINERS",
"FIXTURE DRIVER\nM:\tFixture Maintainer <[email protected]>\nL:\t[email protected]\nS:\tMaintained\nF:\tdrivers/foo/*\n\nFIXTURE REVIEWERS\nR:\tFixture Reviewer <[email protected]>\nL:\t[email protected]\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");
}
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 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
}
}