xtask/tests/default_tree.rs
//! **The plain default is small and holds one version of each crate** (owner order
//! 2026-10-22).
//!
//! `cargo build --release` at the root (and `cargo install gunnar`) builds the
//! `gunnar` package with its default features: a server that starts and answers
//! SSH and HTTPS (TLS on the HTTP port, `--http-tls-self` or `--http-tls-cert`).
//! Measured on 3aa3acd it resolved 399 crates, 47 of them names in two versions
//! (mostly the RustCrypto 0.10 line beside russh's current one). This holds the
//! dedup: every name still in two versions is listed with the crate that pins
//! the other one, and company SSO (OIDC + SAML) and mimalloc stay opt-in.
use std::collections::{BTreeMap, BTreeSet};
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()
}
/// The names allowed in two versions in the default, each with the pin outside
/// our control that keeps the second one.
const KNOWN_PAIRS: &[(&str, &str)] = &[
("base64", "tonic 0.14 and jsonwebtoken pin 0.22; ureq 3 and arrow-cast pin 0.23"),
("generic-array", "russh 0.63 links 1.x, which re-exports 0.14 itself"),
("getrandom", "ring 0.17 is on 0.2, ahash (arrow) on 0.3, the RustCrypto line on 0.4"),
("num-bigint", "der-parser 10 (x509-parser 0.18) is on 0.4; russh and arrow on 0.5"),
("russh", "the twin's push (replication, in `cargo install gunnar`'s default since GUN-PRODUCTS) dials through nornir-git 0.2.2 on russh 0.63.3 beside gunnar-ssh's 0.64.1; one again once nornir-git 0.2.3 is published (OSSF-NEXT)"),
("redb", "znippy-plugin-git links 2 and 4; gunnar-core's 2 -> 4 is its own round (migration + drill)"),
("syn", "proc macros still on 2 beside serde/clap/tokio on 3"),
("untrusted", "aws-lc-rs is on 0.7, ring and rustls-webpki on 0.9"),
];
/// Never in the plain default.
/// The public site (with IndexNow and its crawler lists), the MCP connector and the forge
/// import are never in it either (owner rule 7, 2026-10-26): they are the console's, behind
/// default-off features (`crates/gunnar-recipe/tests/airgap_default_builds.rs` holds the front).
/// Nor is an outbound HTTP client (A32-18, owner rule 7): `ureq` is the mirror pull's https half
/// (`mirror-http`) and the company-SSO egress (`sso-egress`), both default-off and named by the
/// appliance rows that ship them; and no SMTP client (`lettre`, the resend mail, rides
/// `fleet-pull`).
const OPT_IN: &[&str] = &[
"nornir-auth-oidc",
"nornir-auth-saml",
"jsonwebtoken",
"mimalloc",
"monetize-embed",
"gunnar-public-site",
"gunnar-mcp",
"gunnar-forge",
"ureq",
"lettre",
];
/// Pure: (name -> versions) out of `cargo tree --prefix none --format {p}` lines.
fn versions(tree: &str) -> BTreeMap<String, BTreeSet<String>> {
let mut out: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for line in tree.lines() {
let mut parts = line.split_whitespace();
if let (Some(name), Some(version)) = (parts.next(), parts.next()) {
out.entry(name.to_owned())
.or_default()
.insert(version.to_owned());
}
}
out
}
#[test]
fn versions_reads_names_in_two_versions() {
let v = versions("a v1.0.0\nb v0.1.0 (proc-macro)\na v2.0.0 (*)\nc v0.1.0 (/x/c)\n");
assert_eq!(v["a"].len(), 2);
assert_eq!(v["b"].len(), 1);
assert_eq!(v.len(), 3);
}
#[test]
fn the_plain_default_is_deduplicated_and_has_no_opt_in_crate() {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
let out = Command::new(&cargo)
.current_dir(root())
.args([
"tree", "-p", "gunnar", "-e", "normal", "--prefix", "none", "--format", "{p}",
])
.output()
.expect("running cargo tree");
assert!(
out.status.success(),
"cargo tree -p gunnar: {}",
String::from_utf8_lossy(&out.stderr)
);
let v = versions(&String::from_utf8_lossy(&out.stdout));
assert!(v.len() > 200, "the tree read only {} crates", v.len());
let known: BTreeSet<&str> = KNOWN_PAIRS.iter().map(|(n, _)| *n).collect();
let doubled: Vec<String> = v
.iter()
.filter(|(n, vs)| vs.len() > 1 && !known.contains(n.as_str()))
.map(|(n, vs)| format!("{n} {vs:?}"))
.collect();
assert!(
doubled.is_empty(),
"names in two versions in the plain default, beyond the known pins:\n {}",
doubled.join("\n ")
);
let opt_in: Vec<&str> = OPT_IN
.iter()
.copied()
.filter(|n| v.contains_key(*n))
.collect();
assert!(
opt_in.is_empty(),
"opt-in crates in the plain default: {opt_in:?}"
);
}
/// The crates company SSO's protocols link (`gunnar-grpc/sso`, edda nornir-identity-sso's
/// `oidc` + `saml`).
const SSO_PROTOCOLS: &[&str] = &["nornir-auth-oidc", "nornir-auth-saml", "jsonwebtoken"];
fn tree_of(args: &[&str]) -> BTreeMap<String, BTreeSet<String>> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
let out = Command::new(&cargo)
.current_dir(root())
.args([
"tree", "-e", "normal", "--prefix", "none", "--format", "{p}",
])
.args(args)
.output()
.expect("running cargo tree");
assert!(
out.status.success(),
"cargo tree {args:?}: {}",
String::from_utf8_lossy(&out.stderr)
);
versions(&String::from_utf8_lossy(&out.stdout))
}
/// ★ **Company SSO is its own default-off feature on the server too** (A34-15): gunnar-server's
/// default (and its `server` marker) links no SSO protocol; `sso` turns them on, and
/// `sso-egress`, which every appliance row names, brings `sso` with it.
#[test]
fn the_servers_default_has_no_company_sso_and_sso_turns_it_on() {
let plain = tree_of(&["-p", "gunnar-server"]);
let linked: Vec<&str> = SSO_PROTOCOLS
.iter()
.copied()
.filter(|n| plain.contains_key(*n))
.collect();
assert!(
linked.is_empty(),
"gunnar-server's default links company SSO: {linked:?}"
);
let marker = tree_of(&[
"-p",
"gunnar-server",
"--no-default-features",
"--features",
"server,engine-znippy",
]);
let linked: Vec<&str> = SSO_PROTOCOLS
.iter()
.copied()
.filter(|n| marker.contains_key(*n))
.collect();
assert!(
linked.is_empty(),
"gunnar-server's `server` marker turns company SSO on: {linked:?}"
);
for features in ["sso", "sso-egress"] {
let on = tree_of(&["-p", "gunnar-server", "--features", features]);
let missing: Vec<&str> = SSO_PROTOCOLS
.iter()
.copied()
.filter(|n| !on.contains_key(*n))
.collect();
assert!(
missing.is_empty(),
"gunnar-server --features {features} lacks {missing:?}"
);
}
}