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.
This commit is contained in:
2026-09-16 21:52:05 -04:00
parent b74dafb475
commit 1c668c3c1e
19 changed files with 3432 additions and 155 deletions
+93
View File
@@ -161,6 +161,17 @@ impl Fixture {
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")
}
@@ -214,6 +225,40 @@ impl Fixture {
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"));
@@ -246,4 +291,52 @@ impl Fixture {
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"))
}
}