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

nordisk/gunnar-ui

xtask/tests/readme_version.rs

//! The front page may not name a version this tree cannot produce.
//!
//! ⚠ A KNOWN TWIN — `monetize/xtask/tests/readme_version.rs`, and gunnar's.
//! `nornir mimir analyze duplicates` measured 38 diff lines across 166 on
//! 2026-09-10, and every code difference is the crate name. It stays for the
//! reason `../src/release_version.rs` records at length: an xtask here has ZERO
//! dependencies by a repository rule, so it cannot reach the shared leaf that
//! would hold it (`edda/crates/nornir-xtask-support`).
//!
//! # What it asserts
//!
//! Every `gunnar-ui <version>` literal in the root `README.md` equals
//! `[workspace.package] version`.
//!
//! # Why it also counts what it found
//!
//! A scanner that matches nothing passes. Deleting the sentence — or renaming
//! the binary in it — would turn this guard off silently, which is the exact
//! disease it was written for. So it asserts a **non-zero census** and names
//! the count.
//!
//! This test is one of the three steps `cargo xtask publish` runs against the
//! PUBLIC worktree, so the sentence has to survive into the published tree too.

use std::path::{Path, PathBuf};

use xtask::release_version::{disagreements, scan, triple, workspace_version};

fn workspace_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xtask/ has a parent")
        .to_path_buf()
}

fn read(root: &Path, rel: &str) -> String {
    std::fs::read_to_string(root.join(rel)).unwrap_or_else(|e| panic!("read {rel}: {e}"))
}

#[test]
fn the_readme_names_the_workspace_version() {
    let root = workspace_root();
    let version = workspace_version(&root);
    let text = read(&root, "README.md");

    let census = scan(&text).found;
    assert!(
        !census.is_empty(),
        "README.md contains no `gunnar-ui <version>` literal at all, so this guard checked \
         nothing. That is how it would be turned off by accident: the sentence naming the \
         binary was removed or reworded. Restore a line that names the version, or delete \
         this guard on purpose."
    );

    let wrong = disagreements(&text, &version);
    assert!(
        wrong.is_empty(),
        "[workspace.package] version is `{version}`, but README.md names {wrong:?}. The \
         front page of the release advertises a build this tree cannot produce."
    );
}

/// The scanner must be able to say NO, and must be seen finding the thing it is
/// looking for.
///
/// Driven against synthetic inputs, because the live tree is (by the test
/// above) the matching case — and a scanner that answered "no disagreements" to
/// every input would satisfy it and be worth nothing.
#[test]
fn the_scanner_finds_a_wrong_version_and_is_not_satisfied_by_an_empty_file() {
    // ── red ─────────────────────────────────────────────────────────────────
    let stale = "This page documents\n**gunnar-ui 0.0.1** — the version it prints.\n";
    let wrong = disagreements(stale, "0.1.0");
    assert_eq!(wrong.len(), 1, "the stale line must be caught: {wrong:?}");
    assert_eq!(wrong[0].version, "0.0.1");
    assert_eq!(wrong[0].line, 2, "the line number must name where to look");

    // ── green, and for the right reason: it FOUND one and it agreed ──────────
    let fixed = "This page documents\n**gunnar-ui 0.1.0** — the version it prints.\n";
    assert!(disagreements(fixed, "0.1.0").is_empty());
    assert_eq!(
        scan(fixed).found.len(),
        1,
        "the fixed line must still be SEEN, or the green above is vacuous"
    );

    // ── the census, seen empty ──────────────────────────────────────────────
    let deleted = "One binary is the BFF.\n";
    assert!(disagreements(deleted, "0.1.0").is_empty());
    assert!(
        scan(deleted).found.is_empty(),
        "and the census must report zero, so the test above can refuse it"
    );

    // ── every spelling of the claim ─────────────────────────────────────────
    for token in [
        "gunnar-ui 0.0.1",
        "gunnar-ui-server 0.0.1",
        "gunnar-ui-web 0.0.1",
    ] {
        assert_eq!(
            disagreements(token, "0.1.0").len(),
            1,
            "`{token}` names a stale version and was not caught"
        );
    }

    // ── the package names are not read as the bare workspace name ───────────
    // `gunnar-ui ` requires the space, so `gunnar-ui-server 0.0.1` is ONE
    // literal and not two.
    assert_eq!(scan("gunnar-ui-server 0.0.1").found.len(), 1);

    // ── two-component versions are the same release ─────────────────────────
    assert!(disagreements("gunnar-ui 0.1", "0.1.0").is_empty());
    assert_eq!(triple("0.1"), triple("0.1.0"));

    // ── things that are NOT a claim about gunnar-ui's version ───────────────
    // The README quotes other projects. Flagging them would get the guard
    // switched off within a day.
    for quiet in [
        "measured against leptos 0.8 and axum 0.8",
        "git clone ssh://git.gunnar.rs:2222/gunnar/gunnar-ui",
        "cargo build --release -p gunnar-ui-server",
        "the gunnar-ui 1 release",
    ] {
        assert!(
            scan(quiet).found.is_empty(),
            "`{quiet}` is not a version claim about this workspace, but was scanned as \
             one: {:?}",
            scan(quiet).found
        );
    }

    // ── trailing punctuation is not a fourth component ──────────────────────
    let sentence = "built by gunnar-ui 0.1.0.";
    assert_eq!(scan(sentence).found[0].version, "0.1.0");
    assert!(disagreements(sentence, "0.1.0").is_empty());

    // ── more than one literal on one line, both judged ──────────────────────
    let two = "gunnar-ui 0.0.1 is served by gunnar-ui-server 0.1.0";
    assert_eq!(scan(two).found.len(), 2);
    assert_eq!(disagreements(two, "0.1.0").len(), 1);
}

/// `workspace_version` must read the real field, not the first `version =` it
/// trips over.
///
/// The root manifest carries `version = "…"` lines under
/// `[workspace.dependencies]` too, so a reader that took the first match would
/// answer whatever those happened to say.
#[test]
fn the_workspace_version_comes_from_the_workspace_package_table() {
    let root = workspace_root();
    let version = workspace_version(&root);
    assert!(
        triple(&version).is_some(),
        "[workspace.package] version `{version}` is not a numeric release name"
    );

    let text = read(&root, "Cargo.toml");
    let after_table: String = text
        .split("[workspace.package]")
        .nth(1)
        .expect("the root manifest has a [workspace.package] table")
        .lines()
        .take_while(|l| !l.trim_start().starts_with('['))
        .collect::<Vec<_>>()
        .join("\n");
    assert!(
        after_table.contains(&format!("version = \"{version}\"")),
        "`{version}` was not read out of the [workspace.package] table itself"
    );
}