Files
spectral/tests/real_tools.rs
T
huntedbytheirs 1c668c3c1e kernel, patch, doctor, config: fill in the rest of Now and Next
kernel quest reads bugzilla's REST API and picks one bug through a seeded
xorshift, so a pick is reproducible in a test and varied in a person's hands.
kernel test builds under spawn_blocking and boots under qemu with the serial
console streamed line by line, and it reports what qemu did instead of
flattening a failed boot into a success.

patch create writes a whole series when --range is given. git format-patch
does the naming and the numbering because that is already its job, the cover
letter comes from --cover-letter, and a series.toml beside the patches records
the range with both ends as hashes, the revision, the files and the blurb.
patch submit takes a directory or several files and sends them in one
git send-email invocation, and patch update re-rolls a directory through
--reroll-count, deleting the superseded revision only once the new one is
complete.

doctor runs its checks in the order a first run hits them, prints one line and
a fix hint for each, and names the first hard failure in the summary. A missing
qemu or a missing mail route warns and keeps exit 0, because --dry-run is where
the loop actually stops on a machine without an SMTP route.

~/.config/spectral/config.toml holds the tree path, the patch directory,
always-CC addresses and an identity, with the environment winning over the file
and the file over the built-in default. spectral init verifies or clones a tree
and writes it once: a second run with the same tree says so and writes nothing,
and a run that would change an existing answer is refused until --force.

tests/ grew a fixture for each of those. The harness controls PATH and git's
configuration, and its PATH can hold a git that answers send-email either way,
so a host without that support still passes rather than deciding the result.

Two review lanes ran over this work and this commit carries their fixes. A
re-roll regenerated base..HEAD, so a commit that landed after create would have
silently joined the set and been mailed: the range is recorded now and a
re-roll regenerates exactly it. A patch file that was already gone made update
fail forever while the sidecar described a revision that was not on disk: a
missing file is the state the re-roll wanted. An empty blurb aborted after the
files were written: it is checked before anything is generated. A tree that is
not a git repository was reported as a missing revision. init cloned before it
refused. The bugzilla request had no timeout, a signal-ended child printed the
bare word "signal", CommandSpawn dropped the io reason, and the printed
send-email line quoted only arguments with spaces, so --in-reply-to <id@host>
pasted as a redirection.

Checked with cargo fmt --all -- --check, cargo clippy --all-targets --locked --
-D warnings, cargo test --locked (96 passed, 4 ignored) and cargo doc --no-deps.
The four ignored tests want the host's own checkpatch, get_maintainer and
bugzilla, and all four pass when asked for.
2026-09-16 21:52:05 -04:00

170 lines
6.3 KiB
Rust

//! 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}");
}
#[test]
#[ignore = "needs SPECTRAL_LIVE=1 and a network"]
fn live_bugzilla_answers_with_one_issue() {
// The only test that talks to bugzilla.kernel.org. It asserts the shape of
// the answer rather than which bug came back, which is what a live API can
// be held to.
assert_eq!(
std::env::var("SPECTRAL_LIVE").as_deref(),
Ok("1"),
"run this with SPECTRAL_LIVE=1"
);
let fixture = Fixture::bare();
let output = fixture.cli(&["kernel", "quest", "--json"]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
for field in ["\"id\"", "\"title\"", "\"component\"", "\"url\""] {
assert!(stdout.contains(field), "{field} is missing from {stdout}");
}
assert!(
stdout.contains("https://bugzilla.kernel.org/show_bug.cgi?id="),
"{stdout}"
);
}
#[test]
#[ignore = "needs SPECTRAL_LIVE=1 and a network"]
fn live_bugzilla_names_a_filter_that_matches_nothing() {
assert_eq!(
std::env::var("SPECTRAL_LIVE").as_deref(),
Ok("1"),
"run this with SPECTRAL_LIVE=1"
);
let fixture = Fixture::bare();
let output = fixture.cli(&["kernel", "quest", "--filter", "zzq-no-such-word-zzq"]);
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("no open bug matched"), "{stderr}");
}