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.
This commit is contained in:
2026-09-16 18:41:51 -04:00
parent 33161f2ae4
commit 97664806b7
11 changed files with 2173 additions and 102 deletions
+124
View File
@@ -0,0 +1,124 @@
//! The real tools, when the machine has them.
//!
//! Everything else in `tests/` drives stubs, because checkpatch's output moves
//! with the kernel version and a test cannot assert on that. These tests are the
//! other half: they run the host's own scripts so a kernel-version change to a
//! summary line, an argument, or an exit code shows up here rather than in
//! someone's patch workflow.
//!
//! They are ignored by default and need both variables:
//!
//! ```console
//! $ SPECTRAL_REAL_CHECKPATCH=1 SPECTRAL_REAL_TREE=/usr/src/linux \
//! cargo test --locked -- --ignored real_
//! ```
mod common;
use common::Fixture;
/// A change checkpatch has opinions about, whatever kernel it comes from.
const BAD: &str =
"static int foo(int a) {\n\tint b = a + 1;\n\tif (a>0)\n\t\tb += 2;\n\treturn b;\n}\n";
#[test]
#[ignore = "needs SPECTRAL_REAL_CHECKPATCH=1 and SPECTRAL_REAL_TREE=/path/to/linux"]
fn real_checkpatch_reads_all_three_kinds_of_target() {
let fixture = real_fixture();
fixture.write("drivers/foo/bar.c", BAD);
// The working tree goes in on stdin, because `--git` with no revision
// dies with "no git commits after extraction".
let working_tree = fixture.cli(&["patch", "check"]);
let stdout = String::from_utf8_lossy(&working_tree.stdout).into_owned();
assert_eq!(working_tree.status.code(), Some(1), "{stdout}");
assert!(stdout.contains("total:"), "{stdout}");
assert!(stdout.contains("ERROR"), "{stdout}");
// A revision goes through --git, and checkpatch names the commit it read.
let by_rev = fixture.cli(&["patch", "check", "--rev", "HEAD"]);
let stdout = String::from_utf8_lossy(&by_rev.stdout).into_owned();
assert!(stdout.contains("fixture: add bar"), "{stdout}");
// A patch file goes through --file.
let patch = fixture.home().join("bad.patch");
std::fs::write(&patch, format!("{}\n", fixture.git(&["diff"]))).expect("write the patch");
let by_file = fixture.cli(&["patch", "check", patch.to_str().expect("the patch path")]);
let stdout = String::from_utf8_lossy(&by_file.stdout).into_owned();
assert_eq!(by_file.status.code(), Some(1), "{stdout}");
assert!(stdout.contains("ERROR"), "{stdout}");
}
/// A fixture whose checkpatch is the host's, copied in.
fn real_fixture() -> Fixture {
assert_eq!(
std::env::var("SPECTRAL_REAL_CHECKPATCH").as_deref(),
Ok("1"),
"run this with SPECTRAL_REAL_CHECKPATCH=1"
);
let tree = std::env::var("SPECTRAL_REAL_TREE")
.expect("set SPECTRAL_REAL_TREE to a kernel tree holding scripts/checkpatch.pl");
let fixture = Fixture::kernel_tree();
let script = std::path::Path::new(&tree).join("scripts/checkpatch.pl");
let body = std::fs::read_to_string(&script)
.unwrap_or_else(|error| panic!("read {}: {error}", script.display()));
fixture.write_script("scripts/checkpatch.pl", &body);
fixture
}
#[test]
#[ignore = "needs SPECTRAL_REAL_GETMAINTAINER=1 and SPECTRAL_REAL_TREE=/path/to/linux"]
fn real_get_maintainer_fills_the_to_and_cc_lines() {
assert_eq!(
std::env::var("SPECTRAL_REAL_GETMAINTAINER").as_deref(),
Ok("1"),
"run this with SPECTRAL_REAL_GETMAINTAINER=1"
);
let tree = std::env::var("SPECTRAL_REAL_TREE")
.expect("set SPECTRAL_REAL_TREE to a kernel tree holding scripts/get_maintainer.pl");
let fixture = Fixture::kernel_tree();
let script = std::path::Path::new(&tree).join("scripts/get_maintainer.pl");
let body = std::fs::read_to_string(&script)
.unwrap_or_else(|error| panic!("read {}: {error}", script.display()));
fixture.write_script("scripts/get_maintainer.pl", &body);
fixture.commit_all("fixture: use the real get_maintainer");
fixture.work_on_a_branch();
fixture.write("drivers/foo/bar.c", common::EDITED);
fixture.commit_all("foo: return a + 1");
let created = fixture.cli(&["patch", "create", "000-foo"]);
assert!(
created.status.success(),
"{}",
String::from_utf8_lossy(&created.stderr)
);
let patch = fixture.home().join(".spectral/patches/000-foo.patch");
let output = fixture.cli(&[
"patch",
"submit",
patch.to_str().expect("the patch path"),
"--dry-run",
]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
// The script's own words decide the split, so the assertion is about the
// shape rather than about which kernel's MAINTAINERS matched.
assert!(
stdout.contains("To: Fixture Maintainer <[email protected]>"),
"{stdout}"
);
assert!(
stdout.lines().any(|line| line.starts_with("Cc: ")),
"a reviewer and a second list belong on the Cc line: {stdout}"
);
assert!(stdout.contains("Dry-OK"), "{stdout}");
}