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.
374 lines
11 KiB
Rust
374 lines
11 KiB
Rust
//! The config file and `spectral init`, driven through the built binary.
|
|
//!
|
|
//! Precedence is asserted at the command line rather than in a unit test,
|
|
//! because `std::env::set_var` is `unsafe` under edition 2024 and this crate
|
|
//! allows no `unsafe`: the process under test is the one that sets the
|
|
//! variables.
|
|
|
|
mod common;
|
|
|
|
use std::path::Path;
|
|
|
|
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()
|
|
}
|
|
|
|
/// A second kernel tree, so a test can tell two answers apart.
|
|
fn other_tree(fixture: &Fixture) -> std::path::PathBuf {
|
|
let tree = fixture.join("other-tree");
|
|
std::fs::create_dir_all(tree.join("scripts")).expect("create the other tree");
|
|
std::fs::write(tree.join("scripts/checkpatch.pl"), "#!/bin/sh\n").expect("write checkpatch");
|
|
std::fs::write(tree.join("scripts/get_maintainer.pl"), "#!/bin/sh\n")
|
|
.expect("write the script");
|
|
std::fs::write(tree.join("MAINTAINERS"), "OTHER DRIVER\n").expect("write MAINTAINERS");
|
|
|
|
tree
|
|
}
|
|
|
|
#[test]
|
|
fn a_config_file_supplies_the_tree_when_the_environment_does_not() {
|
|
let fixture = Fixture::kernel_tree();
|
|
fixture.write_config(&format!(
|
|
"kernel_tree = \"{}\"\npatch_dir = \"{}\"\n",
|
|
fixture.tree().display(),
|
|
fixture.home().join("patches").display()
|
|
));
|
|
|
|
let output = fixture.cli_without_the_tree_variable(&["doctor"], &[]);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"{}{}",
|
|
stdout_of(&output),
|
|
stderr_of(&output)
|
|
);
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("ok kernel tree"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("from the config file"),
|
|
"the report says the file answered: {stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains("ok config file"),
|
|
"the file is a check of its own: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_environment_beats_the_config_file() {
|
|
let fixture = Fixture::kernel_tree();
|
|
fixture.write_config(&format!(
|
|
"kernel_tree = \"{}\"\n",
|
|
other_tree(&fixture).display()
|
|
));
|
|
|
|
let output = fixture.cli_without_the_tree_variable(
|
|
&["doctor"],
|
|
&[(
|
|
"SPECTRAL_KERNEL",
|
|
fixture.tree().to_str().expect("the tree"),
|
|
)],
|
|
);
|
|
|
|
assert!(output.status.success(), "{}", stdout_of(&output));
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("from $SPECTRAL_KERNEL"), "{stdout}");
|
|
assert!(
|
|
stdout.contains(fixture.tree().to_str().expect("the tree")),
|
|
"{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_file_beats_the_built_in_default() {
|
|
let fixture = Fixture::kernel_tree();
|
|
let other = other_tree(&fixture);
|
|
fixture.write_config(&format!("kernel_tree = \"{}\"\n", other.display()));
|
|
|
|
let output = fixture.cli_without_the_tree_variable(&["doctor"], &[]);
|
|
|
|
assert!(output.status.success(), "{}", stdout_of(&output));
|
|
let stdout = stdout_of(&output);
|
|
assert!(
|
|
stdout.contains(other.to_str().expect("the other tree")),
|
|
"the file's tree is the one doctor looked at: {stdout}"
|
|
);
|
|
assert!(
|
|
!stdout.contains(".spectral/linux"),
|
|
"the default was not used: {stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn with_no_file_and_no_variable_the_default_is_used() {
|
|
let fixture = Fixture::kernel_tree();
|
|
|
|
let output = fixture.cli_without_the_tree_variable(&["doctor"], &[]);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
let stdout = stdout_of(&output);
|
|
assert!(
|
|
stdout.contains(".spectral/linux"),
|
|
"the default is $HOME/.spectral/linux: {stdout}"
|
|
);
|
|
assert!(stdout.contains("the built-in default"), "{stdout}");
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_config_is_a_named_error_rather_than_a_panic() {
|
|
let fixture = Fixture::kernel_tree();
|
|
let path = fixture.write_config("kernel_tree = 42\n");
|
|
|
|
let output = fixture.cli_without_the_tree_variable(&["doctor"], &[]);
|
|
|
|
assert_eq!(
|
|
output.status.code(),
|
|
Some(1),
|
|
"a bad config is not a panic: {}{}",
|
|
stdout_of(&output),
|
|
stderr_of(&output)
|
|
);
|
|
let stderr = stderr_of(&output);
|
|
assert!(stderr.contains("is not a readable config file"), "{stderr}");
|
|
assert!(stderr.contains(&path.display().to_string()), "{stderr}");
|
|
}
|
|
|
|
#[test]
|
|
fn an_unknown_key_is_refused_rather_than_ignored() {
|
|
let fixture = Fixture::kernel_tree();
|
|
fixture.write_config(&format!(
|
|
"kernel_tree = \"{}\"\ntree = \"a typo\"\n",
|
|
fixture.tree().display()
|
|
));
|
|
|
|
let output = fixture.cli_without_the_tree_variable(&["doctor"], &[]);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(
|
|
stderr_of(&output).contains("is not a readable config file"),
|
|
"{}",
|
|
stderr_of(&output)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_relative_path_in_the_config_is_refused() {
|
|
let fixture = Fixture::kernel_tree();
|
|
fixture.write_config("kernel_tree = \"linux\"\n");
|
|
|
|
let output = fixture.cli_without_the_tree_variable(&["doctor"], &[]);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
let stderr = stderr_of(&output);
|
|
assert!(stderr.contains("not an absolute path"), "{stderr}");
|
|
}
|
|
|
|
#[test]
|
|
fn init_writes_the_config_and_a_second_run_changes_nothing() {
|
|
let fixture = Fixture::kernel_tree();
|
|
|
|
let first = fixture.cli_without_the_tree_variable(
|
|
&["init", "--tree", fixture.tree().to_str().expect("the tree")],
|
|
&[],
|
|
);
|
|
assert!(
|
|
first.status.success(),
|
|
"{}{}",
|
|
stdout_of(&first),
|
|
stderr_of(&first)
|
|
);
|
|
assert!(stdout_of(&first).contains("wrote"), "{}", stdout_of(&first));
|
|
let config = fixture.home().join("config/spectral/config.toml");
|
|
let written = std::fs::read(&config).expect("the config file");
|
|
assert!(
|
|
String::from_utf8_lossy(&written).contains(fixture.tree().to_str().expect("the tree")),
|
|
"{}",
|
|
String::from_utf8_lossy(&written)
|
|
);
|
|
|
|
let second = fixture.cli_without_the_tree_variable(
|
|
&["init", "--tree", fixture.tree().to_str().expect("the tree")],
|
|
&[],
|
|
);
|
|
assert!(second.status.success(), "{}", stderr_of(&second));
|
|
assert!(
|
|
stdout_of(&second).contains("already says this"),
|
|
"{}",
|
|
stdout_of(&second)
|
|
);
|
|
assert_eq!(
|
|
std::fs::read(&config).expect("the config file"),
|
|
written,
|
|
"the second run rewrote the file"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn init_refuses_to_clobber_a_config_that_says_something_else() {
|
|
let fixture = Fixture::kernel_tree();
|
|
let other = other_tree(&fixture);
|
|
fixture.write_config(&format!("kernel_tree = \"{}\"\n", other.display()));
|
|
|
|
let refused = fixture.cli_without_the_tree_variable(
|
|
&["init", "--tree", fixture.tree().to_str().expect("the tree")],
|
|
&[],
|
|
);
|
|
assert_eq!(refused.status.code(), Some(1));
|
|
let stderr = stderr_of(&refused);
|
|
assert!(stderr.contains("already points somewhere else"), "{stderr}");
|
|
assert!(stderr.contains("--force"), "{stderr}");
|
|
|
|
let forced = fixture.cli_without_the_tree_variable(
|
|
&[
|
|
"init",
|
|
"--tree",
|
|
fixture.tree().to_str().expect("the tree"),
|
|
"--force",
|
|
],
|
|
&[],
|
|
);
|
|
assert!(
|
|
forced.status.success(),
|
|
"{}{}",
|
|
stdout_of(&forced),
|
|
stderr_of(&forced)
|
|
);
|
|
let config = std::fs::read_to_string(fixture.home().join("config/spectral/config.toml"))
|
|
.expect("the config file");
|
|
assert!(
|
|
config.contains(fixture.tree().to_str().expect("the tree")),
|
|
"{config}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn init_verifies_the_tree_it_is_pointed_at() {
|
|
let fixture = Fixture::kernel_tree();
|
|
let not_a_tree = fixture.join("not-a-tree");
|
|
std::fs::create_dir_all(¬_a_tree).expect("create the directory");
|
|
|
|
let output = fixture.cli_without_the_tree_variable(
|
|
&[
|
|
"init",
|
|
"--tree",
|
|
not_a_tree.to_str().expect("the directory"),
|
|
],
|
|
&[],
|
|
);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(
|
|
stderr_of(&output).contains("not a kernel source tree"),
|
|
"{}",
|
|
stderr_of(&output)
|
|
);
|
|
assert!(
|
|
!Path::new(&fixture.home().join("config/spectral/config.toml")).exists(),
|
|
"a tree that did not verify must not be written into the config"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn init_refuses_before_it_clones_anything() {
|
|
// The refusal is about the config file, so it has to happen before a
|
|
// multi-minute kernel clone rather than after one has already landed.
|
|
let fixture = Fixture::kernel_tree();
|
|
let other = other_tree(&fixture);
|
|
fixture.write_config(&format!("kernel_tree = \"{}\"\n", other.display()));
|
|
let into = fixture.home().join("should-not-exist");
|
|
|
|
let output = fixture.cli_without_the_tree_variable(
|
|
&[
|
|
"init",
|
|
"--clone",
|
|
fixture.tree().to_str().expect("the tree"),
|
|
"--tree",
|
|
into.to_str().expect("the destination"),
|
|
],
|
|
&[],
|
|
);
|
|
|
|
assert_eq!(output.status.code(), Some(1));
|
|
assert!(
|
|
stderr_of(&output).contains("already points somewhere else"),
|
|
"{}",
|
|
stderr_of(&output)
|
|
);
|
|
assert!(
|
|
!into.exists(),
|
|
"the clone ran before the refusal was printed"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn init_clones_a_tree_it_was_given_a_url_for() {
|
|
// A local path is a url git can clone, so this exercises the clone branch
|
|
// without a network.
|
|
let fixture = Fixture::kernel_tree();
|
|
let into = fixture.home().join("cloned");
|
|
|
|
let output = fixture.cli_without_the_tree_variable(
|
|
&[
|
|
"init",
|
|
"--clone",
|
|
fixture.tree().to_str().expect("the tree"),
|
|
"--tree",
|
|
into.to_str().expect("the destination"),
|
|
],
|
|
&[],
|
|
);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"{}{}",
|
|
stdout_of(&output),
|
|
stderr_of(&output)
|
|
);
|
|
assert!(into.join("scripts/checkpatch.pl").is_file());
|
|
assert!(into.join("MAINTAINERS").is_file());
|
|
let config = std::fs::read_to_string(fixture.home().join("config/spectral/config.toml"))
|
|
.expect("the config file");
|
|
assert!(config.contains("cloned"), "{config}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_config_file_carries_always_cc_and_an_identity_into_submit() {
|
|
let fixture = Fixture::kernel_tree();
|
|
fixture.work_on_a_branch();
|
|
fixture.write("drivers/foo/bar.c", common::EDITED);
|
|
fixture.commit_all("foo: return a + 1");
|
|
assert!(
|
|
fixture
|
|
.cli(&["patch", "create", "000-foo"])
|
|
.status
|
|
.success()
|
|
);
|
|
fixture.write_config(&format!(
|
|
"kernel_tree = \"{}\"\nalways_cc = [\"[email protected]\"]\n\n[send_email]\nfrom = \"Someone <[email protected]>\"\n",
|
|
fixture.tree().display()
|
|
));
|
|
|
|
let output = fixture
|
|
.cli_without_the_tree_variable(&["patch", "submit", "000-foo.patch", "--dry-run"], &[]);
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"{}{}",
|
|
stdout_of(&output),
|
|
stderr_of(&output)
|
|
);
|
|
let stdout = stdout_of(&output);
|
|
assert!(stdout.contains("Cc: [email protected]"), "{stdout}");
|
|
assert!(
|
|
stdout.contains("--from 'Someone <[email protected]>'"),
|
|
"{stdout}"
|
|
);
|
|
assert!(stdout.contains("Dry-OK"), "{stdout}");
|
|
}
|