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:
+2
-13
@@ -29,6 +29,8 @@ fn every_verb_has_help_of_its_own() {
|
||||
&["patch", "create"],
|
||||
&["patch", "submit"],
|
||||
&["patch", "update"],
|
||||
&["doctor"],
|
||||
&["init"],
|
||||
] {
|
||||
let mut args = verb.to_vec();
|
||||
args.push("--help");
|
||||
@@ -67,16 +69,3 @@ fn a_leaf_missing_its_argument_exits_two() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("MESSAGE"), "{stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_verb_that_is_still_a_stub_panics_where_it_says_it_does() {
|
||||
// `kernel quest` is P5 and `kernel test` is P6. Until they land, this is
|
||||
// the proof that the dispatch is wired end to end: exit 101 with the
|
||||
// panic's own location in it.
|
||||
let fixture = Fixture::kernel_tree();
|
||||
let output = fixture.cli(&["kernel", "quest"]);
|
||||
|
||||
assert_eq!(output.status.code(), Some(101));
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("kernel/quest.rs"), "{stderr}");
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
//! 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}");
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
//! `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"
|
||||
);
|
||||
}
|
||||
+44
-6
@@ -152,6 +152,24 @@ fn a_bare_diff_is_refused_by_send_email() {
|
||||
assert!(stderr.contains("No subject line"), "{stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_commit_with_nothing_staged_says_what_git_said() {
|
||||
// git explains "nothing staged" on stdout, so an error path that only
|
||||
// reads stderr prints a failure with nothing after it.
|
||||
let fixture = Fixture::kernel_tree();
|
||||
fixture.write("drivers/foo/bar.c", common::EDITED);
|
||||
|
||||
let output = fixture.cli(&["patch", "commit", "foo: nothing staged"]);
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("failed (exit 1)"), "{stderr}");
|
||||
assert!(
|
||||
stderr.contains("no changes added to commit") || stderr.contains("not staged"),
|
||||
"git's own explanation was dropped: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_names_a_base_that_does_not_resolve() {
|
||||
let fixture = Fixture::kernel_tree();
|
||||
@@ -180,8 +198,8 @@ fn create_refuses_a_range_that_holds_more_than_one_commit() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("holds 2 commits"), "{stderr}");
|
||||
assert!(
|
||||
stderr.contains("series support is not built yet"),
|
||||
"{stderr}"
|
||||
stderr.contains("--range"),
|
||||
"the refusal has to say where a set belongs: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -229,7 +247,7 @@ fn check_pipes_the_working_tree_diff_into_checkpatch_and_fails_on_errors() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warnings_alone_do_not_fail_the_command() {
|
||||
fn checkpatch_warnings_alone_do_not_fail_the_command() {
|
||||
let fixture = Fixture::kernel_tree();
|
||||
fixture.write("drivers/foo/bar.c", common::EDITED);
|
||||
|
||||
@@ -246,7 +264,7 @@ fn warnings_alone_do_not_fail_the_command() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_run_with_no_total_line_counts_as_clean() {
|
||||
fn checkpatch_counts_a_run_with_no_total_line_as_clean() {
|
||||
// What a clean run looks like under checkpatch's own --terse, where the
|
||||
// summary line is suppressed entirely.
|
||||
let fixture = Fixture::kernel_tree();
|
||||
@@ -524,12 +542,32 @@ fn submit_threads_a_reroll_off_the_patch_it_replaces() {
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("--in-reply-to <[email protected]>"),
|
||||
"{stdout}"
|
||||
stdout.contains("--in-reply-to '<[email protected]>'"),
|
||||
"an unquoted <...> is a redirection when pasted: {stdout}"
|
||||
);
|
||||
assert!(stdout.contains("In-Reply-To: <[email protected]>"), "{stdout}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tree_that_is_not_a_repository_says_so_rather_than_blaming_a_revision() {
|
||||
// `git rev-parse --verify --quiet master` fails both for a revision that is
|
||||
// not there and for a directory that is not a repository. They want
|
||||
// different fixes, so they get different messages.
|
||||
let fixture = Fixture::bare();
|
||||
fixture.write_script("scripts/checkpatch.pl", "#!/bin/sh\n");
|
||||
fixture.write_script("scripts/get_maintainer.pl", "#!/bin/sh\n");
|
||||
|
||||
let output = fixture.cli(&["patch", "create", "000-foo"]);
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("not a git repository"), "{stderr}");
|
||||
assert!(
|
||||
!stderr.contains("no revision"),
|
||||
"a missing repository is not a missing revision: {stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_refuses_a_template_cover_letter_instead_of_swallowing_it() {
|
||||
let fixture = Fixture::kernel_tree();
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
//! `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}"
|
||||
);
|
||||
}
|
||||
@@ -122,3 +122,48 @@ fn real_get_maintainer_fills_the_to_and_cc_lines() {
|
||||
);
|
||||
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}");
|
||||
}
|
||||
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
//! Patch series, driven through the built binary against a fixture tree.
|
||||
//!
|
||||
//! Every test name here carries the verb, so the plan's own command
|
||||
//! `cargo test --locked series` selects exactly this file's tests.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use common::Fixture;
|
||||
|
||||
/// The blurb the tests write into a cover letter.
|
||||
const BLURB: &str = "Port the re-arm to v2\n\nThis replaces the v1 series after review.";
|
||||
|
||||
/// A tree with two commits on a branch of its own: the smallest thing that is
|
||||
/// really a set rather than a patch.
|
||||
fn fixture_with_two_commits() -> Fixture {
|
||||
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");
|
||||
fixture.write(
|
||||
"drivers/foo/bar.c",
|
||||
"static int foo(int a)\n{\n\treturn a + 2;\n}\n",
|
||||
);
|
||||
fixture.commit_all("foo: return a + 2");
|
||||
|
||||
fixture
|
||||
}
|
||||
|
||||
/// The directory a series was written into, as `create` printed it.
|
||||
fn create_series(fixture: &Fixture, extra: &[&str]) -> PathBuf {
|
||||
let mut args = vec!["patch", "create", "xhci-series", "--range", "master..HEAD"];
|
||||
args.extend_from_slice(extra);
|
||||
let output = fixture.cli(&args);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
PathBuf::from(String::from_utf8_lossy(&output.stdout).trim())
|
||||
}
|
||||
|
||||
/// What is in a directory, sorted, so two listings can be compared.
|
||||
fn listing(dir: &Path) -> Vec<String> {
|
||||
let mut names: Vec<String> = std::fs::read_dir(dir)
|
||||
.expect("read the series directory")
|
||||
.map(|entry| {
|
||||
entry
|
||||
.expect("an entry")
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
})
|
||||
.collect();
|
||||
names.sort();
|
||||
|
||||
names
|
||||
}
|
||||
|
||||
/// The patches in a directory, in name order.
|
||||
fn patches(dir: &Path) -> Vec<String> {
|
||||
listing(dir)
|
||||
.into_iter()
|
||||
.filter(|name| name.ends_with(".patch"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read(dir: &Path, name: &str) -> String {
|
||||
std::fs::read_to_string(dir.join(name)).expect("read a file in the series directory")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_series_gets_the_names_and_the_cover_letter_format_patch_gives_it() {
|
||||
let fixture = fixture_with_two_commits();
|
||||
|
||||
let dir = create_series(&fixture, &["--cover-letter", BLURB]);
|
||||
|
||||
assert_eq!(
|
||||
dir,
|
||||
fixture.home().join(".spectral/patches/xhci-series"),
|
||||
"the series belongs in a directory of its own"
|
||||
);
|
||||
let names = listing(&dir);
|
||||
assert_eq!(names.len(), 4, "{names:?}");
|
||||
assert_eq!(names[0], "0000-cover-letter.patch");
|
||||
assert!(names[1].starts_with("0001-"), "{names:?}");
|
||||
assert!(names[2].starts_with("0002-"), "{names:?}");
|
||||
assert_eq!(names[3], "series.toml", "the sidecar says what this is");
|
||||
|
||||
// The sidecar records the whole range as hashes, the revision, and the
|
||||
// files in the order they are sent. The range matters as much as the base:
|
||||
// a re-roll rebuilt as `base..HEAD` would pick up later commits.
|
||||
let manifest = read(&dir, "series.toml");
|
||||
let master = fixture.git(&["rev-parse", "master"]);
|
||||
let head = fixture.git(&["rev-parse", "HEAD"]);
|
||||
assert!(
|
||||
manifest.contains(&format!("base = \"{master}\"")),
|
||||
"the base is a hash: {manifest}"
|
||||
);
|
||||
assert!(
|
||||
manifest.contains(&format!("head = \"{head}\"")),
|
||||
"the head is a hash too: {manifest}"
|
||||
);
|
||||
assert!(manifest.contains("version = 1"), "{manifest}");
|
||||
assert!(manifest.contains("0000-cover-letter.patch"), "{manifest}");
|
||||
|
||||
// The blurb landed, and the template that send-email refuses is gone.
|
||||
let cover = read(&dir, "0000-cover-letter.patch");
|
||||
assert!(
|
||||
cover.contains("Subject: [PATCH 0/2] Port the re-arm to v2"),
|
||||
"{cover}"
|
||||
);
|
||||
assert!(cover.contains("This replaces the v1 series"), "{cover}");
|
||||
assert!(!cover.contains("*** BLURB HERE ***"), "{cover}");
|
||||
assert!(!cover.contains("*** SUBJECT HERE ***"), "{cover}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_series_goes_out_in_one_send_email_invocation() {
|
||||
let fixture = fixture_with_two_commits();
|
||||
create_series(&fixture, &["--cover-letter", BLURB]);
|
||||
|
||||
let output = fixture.cli(&["patch", "submit", "xhci-series", "--dry-run"]);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let command = stdout
|
||||
.lines()
|
||||
.find(|line| line.contains("send-email --dry-run"))
|
||||
.unwrap_or_else(|| panic!("no command line in {stdout}"));
|
||||
|
||||
for name in ["0000-cover-letter.patch", "0001-", "0002-"] {
|
||||
assert!(command.contains(name), "{name} is not in {command}");
|
||||
}
|
||||
let cover_at = command.find("0000-cover-letter").expect("the cover letter");
|
||||
let first_at = command.find("0001-").expect("the first patch");
|
||||
assert!(
|
||||
cover_at < first_at,
|
||||
"the cover letter is sent first: {command}"
|
||||
);
|
||||
assert!(stdout.contains("Dry-OK"), "{stdout}");
|
||||
|
||||
// One lookup over the whole set: a series has one recipient list.
|
||||
let call = fixture.read(".getmaintainer-call");
|
||||
assert_eq!(
|
||||
call.matches("argv:").count(),
|
||||
1,
|
||||
"the lookup ran more than once: {call}"
|
||||
);
|
||||
assert!(call.contains("0000-cover-letter.patch"), "{call}");
|
||||
assert!(call.contains("0001-"), "{call}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_series_without_a_blurb_is_refused_by_send_email() {
|
||||
// No --cover-letter, so format-patch's template subject is still in the
|
||||
// file and send-email refuses the set. The refusal has to reach the user.
|
||||
let fixture = fixture_with_two_commits();
|
||||
create_series(&fixture, &[]);
|
||||
|
||||
let output = fixture.cli(&["patch", "submit", "xhci-series", "--dry-run"]);
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("Refusing to send"), "{stderr}");
|
||||
assert!(stderr.contains("exit 25"), "{stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn series_reroll_moves_every_file_at_once_and_stays_idempotent() {
|
||||
let fixture = fixture_with_two_commits();
|
||||
let dir = create_series(&fixture, &["--cover-letter", BLURB]);
|
||||
let before = listing(&dir);
|
||||
|
||||
// No -v: the next revision is inferred from the sidecar.
|
||||
let output = fixture.cli(&["patch", "update", "xhci-series"]);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout).trim(),
|
||||
dir.display().to_string()
|
||||
);
|
||||
|
||||
let after = listing(&dir);
|
||||
assert_eq!(
|
||||
after.len(),
|
||||
before.len(),
|
||||
"the old revision was left beside the new one: {after:?}"
|
||||
);
|
||||
assert!(
|
||||
patches(&dir).iter().all(|name| name.starts_with("v2-")),
|
||||
"every patch moves together: {after:?}"
|
||||
);
|
||||
assert!(
|
||||
after
|
||||
.iter()
|
||||
.any(|name| name == "v2-0000-cover-letter.patch")
|
||||
);
|
||||
assert!(read(&dir, "series.toml").contains("version = 2"));
|
||||
assert!(
|
||||
read(&dir, "v2-0000-cover-letter.patch").contains("This replaces the v1 series"),
|
||||
"a re-roll threw the blurb away"
|
||||
);
|
||||
|
||||
// The same revision again is a no-op, which is what keeps a double run
|
||||
// from producing a v3.
|
||||
let again = fixture.cli(&["patch", "update", "xhci-series", "-v", "2"]);
|
||||
assert!(again.status.success());
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&again.stdout).trim(),
|
||||
dir.display().to_string()
|
||||
);
|
||||
let unchanged = listing(&dir);
|
||||
assert_eq!(unchanged, after, "a second run at v2 changed the directory");
|
||||
assert!(
|
||||
!unchanged.iter().any(|name| name.starts_with("v3-")),
|
||||
"{unchanged:?}"
|
||||
);
|
||||
|
||||
// And the re-rolled set still sends, with the cover letter first.
|
||||
let sent = fixture.cli(&["patch", "submit", "xhci-series", "--dry-run"]);
|
||||
assert!(
|
||||
sent.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&sent.stderr)
|
||||
);
|
||||
assert!(String::from_utf8_lossy(&sent.stdout).contains("Dry-OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rerolled_series_holds_exactly_one_revision() {
|
||||
let fixture = fixture_with_two_commits();
|
||||
let dir = create_series(&fixture, &["--cover-letter", BLURB]);
|
||||
|
||||
// Straight to v3, so the files that must go are the unversioned ones.
|
||||
assert!(
|
||||
fixture
|
||||
.cli(&["patch", "update", "xhci-series", "-v", "3"])
|
||||
.status
|
||||
.success()
|
||||
);
|
||||
assert!(
|
||||
fixture
|
||||
.cli(&["patch", "update", "xhci-series", "-v", "4"])
|
||||
.status
|
||||
.success()
|
||||
);
|
||||
|
||||
let names = patches(&dir);
|
||||
assert_eq!(names.len(), 3, "{names:?}");
|
||||
assert!(
|
||||
names.iter().all(|name| name.starts_with("v4-")),
|
||||
"{names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn series_reroll_does_not_pick_up_a_commit_that_landed_after_create() {
|
||||
// The range a series was created for is the range it re-rolls. Rebuilding
|
||||
// it as `base..HEAD` would quietly mail commits nobody asked to send.
|
||||
let fixture = fixture_with_two_commits();
|
||||
let dir = create_series(&fixture, &["--cover-letter", BLURB]);
|
||||
assert_eq!(patches(&dir).len(), 3);
|
||||
|
||||
fixture.write(
|
||||
"drivers/foo/bar.c",
|
||||
"static int foo(int a)\n{\n\treturn a + 3;\n}\n",
|
||||
);
|
||||
fixture.commit_all("foo: return a + 3");
|
||||
|
||||
let output = fixture.cli(&["patch", "update", "xhci-series"]);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let names = patches(&dir);
|
||||
assert_eq!(
|
||||
names.len(),
|
||||
3,
|
||||
"a commit that landed after create joined the set: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
!names.iter().any(|name| name.contains("return-a-3")),
|
||||
"{names:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
read(&dir, "series.toml").matches("version = 2").count(),
|
||||
1,
|
||||
"{}",
|
||||
read(&dir, "series.toml")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn series_reroll_tolerates_a_patch_that_is_already_gone() {
|
||||
// A missing file is the state the re-roll wanted, not a failure it should
|
||||
// report forever while leaving the sidecar describing a revision that is
|
||||
// no longer on disk.
|
||||
let fixture = fixture_with_two_commits();
|
||||
let dir = create_series(&fixture, &["--cover-letter", BLURB]);
|
||||
let doomed = patches(&dir)
|
||||
.into_iter()
|
||||
.find(|name| name.starts_with("0002-"))
|
||||
.expect("the second patch");
|
||||
std::fs::remove_file(dir.join(&doomed)).expect("remove a patch by hand");
|
||||
|
||||
let output = fixture.cli(&["patch", "update", "xhci-series"]);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"a stale name stopped the re-roll: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let names = patches(&dir);
|
||||
assert_eq!(names.len(), 3, "{names:?}");
|
||||
assert!(
|
||||
names.iter().all(|name| name.starts_with("v2-")),
|
||||
"{names:?}"
|
||||
);
|
||||
assert!(read(&dir, "series.toml").contains("version = 2"));
|
||||
|
||||
// And it is ready to send, which is the thing the old failure made
|
||||
// impossible: the sidecar and the directory agree again.
|
||||
let sent = fixture.cli(&["patch", "submit", "xhci-series", "--dry-run"]);
|
||||
assert!(
|
||||
sent.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&sent.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn series_create_refuses_an_empty_blurb_before_it_writes_anything() {
|
||||
let fixture = fixture_with_two_commits();
|
||||
|
||||
let output = fixture.cli(&[
|
||||
"patch",
|
||||
"create",
|
||||
"xhci-series",
|
||||
"--range",
|
||||
"master..HEAD",
|
||||
"--cover-letter",
|
||||
" ",
|
||||
]);
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains("blurb is empty"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let dir = fixture.home().join(".spectral/patches/xhci-series");
|
||||
assert!(
|
||||
!dir.exists(),
|
||||
"files with no sidecar beside them were left behind: {:?}",
|
||||
listing(&dir)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_directory_that_is_not_a_series_still_sends_its_patches() {
|
||||
// No sidecar: the directory is read as a pile of patches, in name order,
|
||||
// which is the order format-patch numbered them in.
|
||||
let fixture = fixture_with_two_commits();
|
||||
let dir = fixture.home().join("hand-made");
|
||||
std::fs::create_dir_all(&dir).expect("create the directory");
|
||||
fixture.git(&[
|
||||
"format-patch",
|
||||
"-o",
|
||||
dir.to_str().expect("the directory path"),
|
||||
"master..HEAD",
|
||||
]);
|
||||
|
||||
let output = fixture.cli(&[
|
||||
"patch",
|
||||
"submit",
|
||||
dir.to_str().expect("the directory path"),
|
||||
"--dry-run",
|
||||
]);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("0001-") && stdout.contains("0002-"),
|
||||
"{stdout}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_series_wants_a_range_and_a_range_that_is_not_one_is_named() {
|
||||
let fixture = fixture_with_two_commits();
|
||||
|
||||
let not_a_range = fixture.cli(&["patch", "create", "x", "--range", "master"]);
|
||||
assert_eq!(not_a_range.status.code(), Some(1));
|
||||
let stderr = String::from_utf8_lossy(¬_a_range.stderr);
|
||||
assert!(stderr.contains("is not a range"), "{stderr}");
|
||||
|
||||
let orphan_blurb = fixture.cli(&["patch", "create", "x", "--cover-letter", "hi"]);
|
||||
assert_eq!(orphan_blurb.status.code(), Some(1));
|
||||
assert!(
|
||||
String::from_utf8_lossy(&orphan_blurb.stderr).contains("belongs to a series"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&orphan_blurb.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_needs_a_name_when_no_series_range_is_given() {
|
||||
let fixture = fixture_with_two_commits();
|
||||
|
||||
let output = fixture.cli(&["patch", "create"]);
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains("wants a NAME"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_series_with_no_name_lands_in_the_patch_directory_itself() {
|
||||
// NAME names the series directory, so leaving it off means the patch
|
||||
// directory is the series directory.
|
||||
let fixture = fixture_with_two_commits();
|
||||
|
||||
let output = fixture.cli(&[
|
||||
"patch",
|
||||
"create",
|
||||
"--range",
|
||||
"master..HEAD",
|
||||
"--cover-letter",
|
||||
BLURB,
|
||||
]);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let patches_dir = fixture.home().join(".spectral/patches");
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout).trim(),
|
||||
patches_dir.display().to_string()
|
||||
);
|
||||
assert!(patches_dir.join("series.toml").is_file());
|
||||
assert!(patches_dir.join("0000-cover-letter.patch").is_file());
|
||||
}
|
||||
Reference in New Issue
Block a user