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.
217 lines
6.8 KiB
Rust
217 lines
6.8 KiB
Rust
//! `spectral doctor`, driven through the built binary.
|
|
//!
|
|
//! The fixture controls `PATH` and git's configuration, so a missing qemu, a
|
|
//! missing sender identity and a tree that is not a kernel tree are produced on
|
|
//! purpose rather than inherited from whichever machine this runs on. That
|
|
//! extends to git's own send-email support, which the harness cannot control
|
|
//! through git's configuration: a `git` wrapper answers for it in both
|
|
//! directions, so a host without it still passes.
|
|
//!
|
|
//! Every test name here carries the verb, so the plan's own command
|
|
//! `cargo test --locked doctor` selects exactly this file's tests.
|
|
|
|
mod common;
|
|
|
|
use common::Fixture;
|
|
|
|
fn stdout_of(output: &std::process::Output) -> String {
|
|
String::from_utf8_lossy(&output.stdout).into_owned()
|
|
}
|
|
|
|
fn stderr_of(output: &std::process::Output) -> String {
|
|
String::from_utf8_lossy(&output.stderr).into_owned()
|
|
}
|
|
|
|
/// Run `doctor` with a `git` that has send-email support, whatever the host has.
|
|
fn doctor_with_send_email(fixture: &Fixture) -> std::process::Output {
|
|
let bin = fixture.path_with_send_email(0);
|
|
|
|
fixture.cli_env(
|
|
&["doctor"],
|
|
&[("PATH", bin.to_str().expect("the git bin directory"))],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_passes_on_a_complete_fixture_tree() {
|
|
let fixture = Fixture::kernel_tree();
|
|
|
|
let output = doctor_with_send_email(&fixture);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"{}{}",
|
|
stdout_of(&output),
|
|
stderr_of(&output)
|
|
);
|
|
let stdout = stdout_of(&output);
|
|
assert!(!stdout.contains("fail "), "{stdout}");
|
|
assert!(stdout.contains("ok kernel tree"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("from $SPECTRAL_KERNEL"),
|
|
"the report has to say which knob set the path: {stdout}"
|
|
);
|
|
assert!(stdout.contains("ok git send-email"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("nothing failed") || stdout.contains("everything checked out"),
|
|
"{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_names_the_first_hard_failure_when_the_tree_is_missing() {
|
|
let fixture = Fixture::bare();
|
|
|
|
let output = fixture.cli_env(&["doctor"], &[("SPECTRAL_KERNEL", "/nonexistent/linux")]);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("fail kernel tree"), "{stdout}");
|
|
assert!(stdout.contains("first failure: kernel tree"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("skip checkpatch.pl"),
|
|
"the checks that need a tree are not answers when there is none: {stdout}"
|
|
);
|
|
assert!(stdout.contains("-> "), "the fix hint is printed: {stdout}");
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_names_a_directory_that_is_not_a_kernel_tree() {
|
|
let fixture = Fixture::bare();
|
|
let tree = fixture.join("not-a-tree");
|
|
std::fs::create_dir_all(&tree).expect("create the directory");
|
|
|
|
let output = fixture.cli_env(
|
|
&["doctor"],
|
|
&[("SPECTRAL_KERNEL", tree.to_str().expect("the tree path"))],
|
|
);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("fail kernel tree shape"), "{stdout}");
|
|
assert!(stdout.contains("not a kernel source tree"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("first failure: kernel tree shape"),
|
|
"{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_warns_about_qemu_off_the_path_rather_than_failing() {
|
|
let fixture = Fixture::kernel_tree();
|
|
// A `PATH` holding the git wrapper and nothing else: qemu is gone, the
|
|
// rest is not.
|
|
let bin = fixture.path_with_send_email(0);
|
|
|
|
let output = fixture.cli_env(
|
|
&["doctor"],
|
|
&[("PATH", bin.to_str().expect("the git bin directory"))],
|
|
);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"{}{}",
|
|
stdout_of(&output),
|
|
stderr_of(&output)
|
|
);
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("warn qemu"), "{stdout}");
|
|
assert!(
|
|
!stdout.contains("fail "),
|
|
"a missing qemu is not something the loop cannot work around: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_fails_on_no_sender_identity_and_says_how_to_fix_it() {
|
|
let fixture = Fixture::kernel_tree();
|
|
// Both places git would find one: the tree's own config and the fixture's
|
|
// global one.
|
|
fixture.git(&["config", "--unset", "user.email"]);
|
|
std::fs::write(
|
|
fixture.home().join("gitconfig"),
|
|
"[core]\n\trepositoryformatversion = 0\n",
|
|
)
|
|
.expect("write a global config with no identity");
|
|
|
|
let output = doctor_with_send_email(&fixture);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("fail sender identity"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("user.email"),
|
|
"the failure says how to fix it: {stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains("first failure: sender identity"),
|
|
"{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_warns_when_git_has_no_send_email_support() {
|
|
// The direction the host cannot be trusted for: this machine has it, and a
|
|
// machine without it must fail the same way rather than pass by accident.
|
|
let fixture = Fixture::kernel_tree();
|
|
let bin = fixture.path_with_send_email(1);
|
|
|
|
let output = fixture.cli_env(
|
|
&["doctor"],
|
|
&[("PATH", bin.to_str().expect("the git bin directory"))],
|
|
);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("fail git send-email"), "{stdout}");
|
|
assert!(stdout.contains("first failure: git send-email"), "{stdout}");
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_warns_about_no_mail_route_because_dry_runs_are_what_matter() {
|
|
// The fixture's git config has no sendemail.* at all, which is the state
|
|
// this machine's own config was in when --dry-run was verified.
|
|
let fixture = Fixture::kernel_tree();
|
|
|
|
let output = doctor_with_send_email(&fixture);
|
|
|
|
assert!(output.status.success());
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("warn mail route"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("--dry-run works"),
|
|
"the warning says what still works: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn doctor_reports_where_the_paths_came_from() {
|
|
let fixture = Fixture::kernel_tree();
|
|
let patches = fixture.home().join("patches");
|
|
|
|
let output = fixture.cli_env(
|
|
&["doctor"],
|
|
&[
|
|
(
|
|
"PATH",
|
|
fixture.path_with_send_email(0).to_str().expect("the bin"),
|
|
),
|
|
(
|
|
"SPECTRAL_PATCH_DIR",
|
|
patches.to_str().expect("the patch dir"),
|
|
),
|
|
],
|
|
);
|
|
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("from $SPECTRAL_PATCH_DIR"), "{stdout}");
|
|
assert!(
|
|
stdout.contains(patches.to_str().expect("the patch dir")),
|
|
"{stdout}"
|
|
);
|
|
assert!(
|
|
patches.is_dir(),
|
|
"doctor is the one check with a side effect: it makes the directory it was pointed at"
|
|
);
|
|
}
|