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

nordisk/gunnar

xtask/tests/cargo_install_gunnar.rs

//! **`cargo install gunnar` installs a startable Gunnar server** (owner order 2026-10-22).
//!
//! Three properties, each red when broken:
//!
//! 1. `crates/gunnar` is a workspace member named `gunnar` that may be published and
//!    ships a `gunnar` binary, so `cargo install gunnar` has something to install.
//! 2. **The default build compiles no monetize.** `cargo tree -p gunnar -e normal` with
//!    the default features names no crate whose name contains `monetize`: no
//!    monetize-impl, no open monetize crate, no metered or shop feature. Metered is
//!    an opt-in feature and stays one.
//! 3. The crate's README documents the first start: `cargo install gunnar`, the
//!    command, and the URL that answers health.

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

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

fn cargo() -> String {
    std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string())
}

#[test]
fn the_gunnar_crate_is_a_publishable_server_binary() {
    let manifest = root().join("crates/gunnar/Cargo.toml");
    let text = std::fs::read_to_string(&manifest).unwrap_or_else(|e| {
        panic!(
            "{} is missing ({e}): `cargo install gunnar` installs nothing",
            manifest.display()
        )
    });
    let lines: Vec<&str> = text
        .lines()
        .map(str::trim)
        .filter(|l| !l.starts_with('#'))
        .collect();
    assert!(
        lines.contains(&"name = \"gunnar\""),
        "crates/gunnar is not the package `gunnar`"
    );
    assert!(
        lines.contains(&"publish = true"),
        "crates/gunnar must say `publish = true` by name"
    );
    assert!(
        text.contains("[[bin]]") && text.contains("path = \"src/main.rs\""),
        "crates/gunnar ships no binary"
    );
}

/// Pure: the crate names in `cargo tree --prefix none` output that contain `monetize`.
fn monetize_in_tree(tree: &str) -> Vec<String> {
    let mut out: Vec<String> = tree
        .lines()
        .filter_map(|l| l.split_whitespace().next())
        .filter(|name| name.contains("monetize"))
        .map(str::to_string)
        .collect();
    out.sort();
    out.dedup();
    out
}

#[test]
fn monetize_in_tree_sees_every_spelling() {
    let tree = "gunnar v1.5.0\nmonetize-embed v0.1.3\nmonetize-impl v0.1.0 (/x)\ngunnar-monetize-guest v1.5.0\nserde v1.0.0\n";
    assert_eq!(
        monetize_in_tree(tree),
        vec!["gunnar-monetize-guest", "monetize-embed", "monetize-impl"]
    );
    assert!(monetize_in_tree("gunnar v1.5.0\nserde v1.0.0\n").is_empty());
}

#[test]
fn the_default_gunnar_build_compiles_no_monetize() {
    let out = Command::new(cargo())
        .args(["tree", "-p", "gunnar", "-e", "normal", "--prefix", "none"])
        .current_dir(root())
        .output()
        .expect("run `cargo tree`");
    assert!(
        out.status.success(),
        "`cargo tree -p gunnar -e normal` failed: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let tree = String::from_utf8_lossy(&out.stdout);
    assert!(
        tree.lines().any(|l| l.starts_with("gunnar-server ")),
        "the default gunnar build has no server in it:\n{tree}"
    );
    let found = monetize_in_tree(&tree);
    assert!(
        found.is_empty(),
        "the DEFAULT `gunnar` build compiles monetize crates: {found:?}"
    );
}

#[test]
fn the_readme_documents_the_first_start() {
    let readme = std::fs::read_to_string(root().join("crates/gunnar/README.md"))
        .expect("crates/gunnar/README.md");
    for needle in [
        "cargo install gunnar",
        "\ngunnar\n",
        "http://127.0.0.1:8080/healthz",
        "--features",
    ] {
        assert!(
            readme.contains(needle),
            "crates/gunnar/README.md does not document `{}`",
            needle.trim()
        );
    }
}