Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

installtest/tests/strict_flags.rs

//! **An argument this binary does not recognise must STOP the run, by name.**
//!
//! This crate is a *gate*: `cargo xtask reimage` runs `gunnar-installtest
//! installer` over a freshly composed medium and re-images a live appliance only
//! if it comes back green. A gate that accepts an argument it does not
//! understand, and then runs a different test in silence, is worse than no gate
//! at all — the operator reads a green they did not ask for.
//!
//! That is not hypothetical. MEASURED 2026-09-17, twice, at the cost of two full
//! re-image cycles:
//!
//!   * `cargo xtask reimage --machine twin` gates a TWIN medium, which serves no
//!     git at all — the guest says so itself, `this box is a TWIN: the git
//!     listener is bound to loopback only asked=0.0.0.0:2222` — so the door's
//!     `B2 serve` leg waited 180 s for `:2222` and could never pass, however
//!     good the medium was. The right door is `--grpc-only`, which proves the
//!     medium by `Bootstrap.Status` instead, and which this binary has supported
//!     since 2026-09-15.
//!   * The ops xtask did not pass the flag while its own PLAN printed it, and
//!     the `gunnar-installtest` on `PATH` was older than the flag. **Both of
//!     those should have been one command to find.** They were not, because
//!     `main`'s parse was `args.iter().any(|a| a == "--grpc-only")` and anything
//!     it did not recognise simply fell through: a stale binary and a correct
//!     one behave *identically* on a flag neither ran, and `--help | grep
//!     grpc-only` printed nothing because `--help` was not a verb either — it
//!     started building a server instead.
//!
//! So the rule these tests pin is: **unknown argument → refusal naming it, and
//! the list of what this verb does accept; `--help` → the usage, and nothing
//! else happens.** A typo, a renamed flag and a binary older than its caller
//! then all fail the same loud way.
//!
//! # Why the reds here are cheap, and why they name a `--binary`
//!
//! The arm verbs resolve a [`Subject`] — they BUILD a gunnar — before they do
//! anything else, so a regression that drops the strict parse must not leave
//! these tests compiling a server for ten minutes to reach their assertion.
//! `--binary /bin/true` is a file that exists (so nothing is built) and is
//! plainly not a gunnar (so `Subject::prepare` refuses it in milliseconds). The
//! assertion is on the refusal *text*, so that fallback can never be mistaken
//! for the refusal under test.
//!
//! [`Subject`]: gunnar_installtest::subject::Subject

use std::process::{Command, Output};

/// The binary under test, as cargo built it — never a path guessed from
/// `target/`. Same rule as `tests/exit_code_is_the_verdict.rs`.
const EXE: &str = env!("CARGO_BIN_EXE_gunnar-installtest");

fn run(args: &[&str]) -> Output {
    Command::new(EXE)
        .args(args)
        .output()
        .expect("spawn gunnar-installtest")
}

fn code(out: &Output) -> i32 {
    // `None` means killed by a signal, which is neither pass nor fail.
    out.status.code().unwrap_or(-1)
}

fn err(out: &Output) -> String {
    String::from_utf8_lossy(&out.stderr).into_owned()
}

fn out(o: &Output) -> String {
    String::from_utf8_lossy(&o.stdout).into_owned()
}

/// **The one that cost two re-image cycles.** A near-miss spelling of the flag
/// that decides WHICH TEST RUNS must be refused, and the refusal must print the
/// flag as it was typed — otherwise the operator's next move is to believe the
/// run they got.
///
/// RED-when-broken: delete the `check_args` call in `main` and this fires, because
/// the door then boots a medium instead of answering.
#[test]
fn an_unknown_flag_is_refused_by_name() {
    let bad = run(&["installer", "--iso", "/nonexistent.iso", "--grpc_only"]);
    let e = err(&bad);
    assert_eq!(
        code(&bad),
        1,
        "an unknown flag must exit 1, not run a different test.\nstderr:\n{e}"
    );
    assert!(
        e.contains("--grpc_only"),
        "the refusal must NAME the argument it did not understand, exactly as it \
         was typed:\n{e}"
    );
    assert!(
        e.contains("installer"),
        "and it must say which verb refused it:\n{e}"
    );
    assert!(
        e.contains("--grpc-only") && e.contains("--firmware"),
        "and it must list what the verb DOES accept — that list is how a typo, a \
         renamed flag and a stale binary are told apart:\n{e}"
    );
}

/// The pair for the red above: the flag as it is actually spelled must get
/// *through* the parse and reach the door.
///
/// Without this, the refusal above is satisfied by a binary that refuses
/// everything — the identity-value trap. The medium named does not exist, so the
/// door refuses it on the very next line, and that refusal is the proof the flag
/// was read: only `--grpc-only` produces this message.
#[test]
fn a_known_flag_is_accepted_and_reaches_the_door() {
    let ok = run(&["installer", "--iso", "/nonexistent.iso", "--grpc-only"]);
    let e = err(&ok);
    assert!(
        e.contains("--grpc-only needs a medium"),
        "`--grpc-only` must be ACCEPTED by the parse and answered by the door — \
         this is the message only the door produces:\n{e}"
    );
    assert!(
        !e.contains("does not accept"),
        "…and it must not be swept up by the strict parse:\n{e}"
    );
}

/// `--help` prints the usage, exits zero, and runs NOTHING.
///
/// This is the test that would have ended 2026-09-17 in one command: `--help`
/// used to fall through to the arm dispatch, build a server, and only then
/// report `unknown mode --help` on stderr — so `gunnar-installtest --help | grep
/// grpc-only` found nothing and looked like proof the flag did not exist.
#[test]
fn help_prints_the_usage_on_stdout_and_runs_nothing() {
    for spelling in ["--help", "-h", "help"] {
        let h = run(&[spelling]);
        let (o, e) = (out(&h), err(&h));
        assert_eq!(
            code(&h),
            0,
            "`{spelling}` is a question, not a failure.\nstderr:\n{e}"
        );
        assert!(
            o.contains("usage: gunnar-installtest"),
            "`{spelling}` must print the usage on STDOUT, where `| grep` reads \
             it.\nstdout:\n{o}"
        );
        assert!(
            o.contains("--grpc-only"),
            "and the usage must carry every flag the code accepts, `--grpc-only` \
             above all: its absence from this text is what made a stale binary \
             indistinguishable from a current one.\nstdout:\n{o}"
        );
        assert!(
            !o.contains("base image") && !e.contains("base image"),
            "…and nothing may RUN: no subject, no arm, no boot.\nstdout:\n{o}\nstderr:\n{e}"
        );
    }
    // The verb's own help, for the verb that has the most flags.
    let h = run(&["installer", "--help"]);
    assert_eq!(code(&h), 0, "`installer --help` is a question too");
    assert!(
        out(&h).contains("--grpc-only"),
        "and it prints the same usage:\n{}",
        out(&h)
    );
}

/// The flags are per VERB, not one global bag. `--grpc-only` means something to
/// `installer` and nothing to `bare`, and a run that silently ignores it is the
/// same defect in a smaller place.
#[test]
fn a_flag_that_belongs_to_another_verb_is_refused_here() {
    let bad = run(&["bare", "--binary", "/bin/true", "--grpc-only"]);
    let e = err(&bad);
    assert_eq!(code(&bad), 1, "…must exit 1.\nstderr:\n{e}");
    assert!(
        e.contains("`bare` does not accept") && e.contains("--grpc-only"),
        "the refusal must name the verb AND the flag:\n{e}"
    );
    assert!(
        e.contains("--fault"),
        "and list what `bare` does accept:\n{e}"
    );
}

/// A flag's VALUE is not a flag. `--firmware seabios` must leave `seabios`
/// alone, or the strict parse becomes a new way to refuse correct runs.
#[test]
fn a_value_is_not_mistaken_for_an_argument() {
    let bad = run(&[
        "installer",
        "--firmware",
        "seabios",
        "--iso",
        "/nonexistent.iso",
        "--nonsense",
    ]);
    let e = err(&bad);
    assert_eq!(code(&bad), 1, "…must exit 1.\nstderr:\n{e}");
    assert!(
        e.contains("--nonsense"),
        "the refusal must name the argument that is wrong:\n{e}"
    );
    assert!(
        !e.contains("`seabios`"),
        "…and not the VALUE of a flag that is right:\n{e}"
    );
}

/// A flag that takes a value and was given none is the same silent drop wearing
/// a different hat: `--iso` at the end of the line used to fall back to the
/// default medium and gate something nobody asked about.
#[test]
fn a_flag_with_no_value_is_refused_rather_than_defaulted() {
    let bad = run(&["installer", "--iso"]);
    let e = err(&bad);
    assert_eq!(code(&bad), 1, "…must exit 1.\nstderr:\n{e}");
    assert!(
        e.contains("--iso") && e.contains("wants a value"),
        "the refusal must say which flag was left dangling:\n{e}"
    );
}

/// A word that is not a flag at all. `red bare` *looks* like it restricts the
/// run to one arm; it does not, `--arm bare` does — and until this refusal it
/// ran the whole matrix instead, which is hours.
#[test]
fn a_stray_word_is_refused() {
    let bad = run(&["red", "--binary", "/bin/true", "bare"]);
    let e = err(&bad);
    assert_eq!(code(&bad), 1, "…must exit 1.\nstderr:\n{e}");
    assert!(
        e.contains("bare"),
        "the refusal must name the stray word:\n{e}"
    );
    assert!(
        e.contains("--arm"),
        "and list the flags that verb takes, which is where `--arm` is:\n{e}"
    );
}

/// The verb itself keeps its own refusal, and it now happens BEFORE a server is
/// built: an unknown mode used to compile a gunnar and identify it against HEAD
/// before reporting that the verb does not exist.
#[test]
fn an_unknown_verb_is_still_refused_and_no_longer_builds_first() {
    let bad = run(&["definitely-not-a-mode"]);
    let e = err(&bad);
    assert_eq!(code(&bad), 1, "…must exit 1.\nstderr:\n{e}");
    assert!(e.contains("unknown mode"), "…and say so:\n{e}");
    assert!(
        !e.contains("base image"),
        "…without building and identifying a subject first:\n{e}"
    );
}