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:
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user