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.
250 lines
8.5 KiB
Rust
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
|
|
}
|
|
}
|