Files
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

136 lines
4.4 KiB
Rust

//! `kernel test`, driven through the built binary against a fixture tree.
//!
//! Nothing here boots a kernel: qemu is taken off `PATH` deliberately, so the
//! assertions are about the paths around the boot rather than about a machine
//! that can run one. The real boot is the plan's P6 manual step.
mod common;
use common::Fixture;
/// A fixture tree with something at the path qemu would be handed.
fn fixture_with_image() -> Fixture {
let fixture = Fixture::kernel_tree();
fixture.write("arch/x86/boot/bzImage", "not really a kernel\n");
fixture.commit_all("fixture: add an image");
fixture
}
#[test]
fn a_missing_qemu_is_named_rather_than_panicked_on() {
let fixture = fixture_with_image();
// A `PATH` with nothing on it: the binary itself is found by its full
// path, and qemu cannot be.
let bare = fixture.restricted_path(&[]);
let output = fixture.cli_env(
&["kernel", "test", "--no-build"],
&[("PATH", bare.to_str().expect("the restricted path"))],
);
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("could not run `qemu-system-x86_64`"),
"{stderr}"
);
assert!(
!String::from_utf8_lossy(&output.stdout).contains("exited cleanly"),
"a boot that never started was reported as a success"
);
}
#[test]
fn a_tree_with_no_image_is_named_rather_than_a_build_being_attempted() {
let fixture = Fixture::kernel_tree();
let output = fixture.cli(&["kernel", "test", "--no-build"]);
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("no bootable image at"), "{stderr}");
assert!(stderr.contains("arch/x86/boot/bzImage"), "{stderr}");
}
#[test]
fn build_runs_make_in_the_tree_and_finds_the_image_it_wrote() {
// A Makefile that writes the image where a real build leaves it, and
// records that it ran. `sh` is the only tool it needs, so the restricted
// `PATH` can hold make and sh and still leave qemu out.
let fixture = Fixture::kernel_tree();
fixture.write("arch/x86/boot/.keep", "");
fixture.write(
"Makefile",
"all:\n\tprintf '' > arch/x86/boot/bzImage\n\tprintf 'built\\n' > .make-call\n",
);
fixture.commit_all("fixture: add a Makefile");
let bin = fixture.restricted_path(&["make", "sh"]);
let output = fixture.cli_env(
&["kernel", "test"],
&[("PATH", bin.to_str().expect("the restricted path"))],
);
assert_eq!(
fixture.read(".make-call"),
"built\n",
"make did not run in the tree"
);
// The image check passed, which is why the failure is qemu's rather than
// the image's.
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("could not run `qemu-system-x86_64`"),
"the build did not get as far as qemu: {stderr}"
);
}
#[test]
fn qemu_runs_with_the_arguments_the_plan_describes() {
// What qemu was handed, captured without needing a real qemu: the stub is
// reached through `PATH`, so the binary under test cannot tell.
let fixture = fixture_with_image();
fixture.write_script(
"bin/qemu-system-x86_64",
"#!/bin/sh\necho \"argv: $*\"\nexit 1\n",
);
let bin = fixture.join("bin");
let output = fixture.cli_env(
&[
"kernel",
"test",
"--no-build",
"--qemu-arg",
"-m",
"--qemu-arg",
"1G",
],
&[("PATH", bin.to_str().expect("the stub bin directory"))],
);
assert_eq!(
output.status.code(),
Some(1),
"a failing qemu must not look clean: {}",
String::from_utf8_lossy(&output.stdout)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("-kernel ") && stdout.contains("arch/x86/boot/bzImage"),
"{stdout}"
);
assert!(
stdout.contains("-append console=ttyS0"),
"the console is what makes a boot readable: {stdout}"
);
assert!(stdout.contains("-nographic"), "{stdout}");
assert!(stdout.contains("-m 1G"), "the pass-through args: {stdout}");
assert!(
stdout.contains("qemu exited with status 1"),
"the exit status was flattened: {stdout}"
);
}