server/cli/tests/systemd_flavour.rs
//! **Holger's `systemd` flavour links only the systemd lifecycle** (lane B4, A31 §7.5).
//!
//! `holger install …` and `holger service …` register Holger with systemd. They
//! came from the whole `skidbladnir` crate (77k lines: k8s, vmsuite, korpprov,
//! machine, msi, promote, pack …), none of which a container or an appliance
//! uses. Now they come from `skidbladnir-systemd` alone, behind the `systemd`
//! feature, which the plain `cargo install holger` (the exe flavour) keeps on and
//! the container and appliance builds leave off.
const MANIFEST: &str = include_str!("../Cargo.toml");
fn section<'a>(text: &'a str, name: &str) -> Vec<&'a str> {
let mut out = Vec::new();
let mut inside = false;
for l in text.lines() {
let t = l.trim();
if t.starts_with('[') {
inside = t == name;
continue;
}
if inside && !t.starts_with('#') && !t.is_empty() {
out.push(t);
}
}
out
}
#[test]
fn the_cli_names_the_lifecycle_crate_behind_a_default_systemd_feature() {
let deps = section(MANIFEST, "[dependencies]");
assert!(
!deps.iter().any(|l| l.starts_with("skidbladnir =")),
"the whole skidbladnir crate is still a dependency"
);
let edge = deps
.iter()
.find(|l| l.starts_with("skidbladnir-systemd ="))
.expect("skidbladnir-systemd is a dependency");
assert!(edge.contains("optional = true"), "{edge}");
let features = section(MANIFEST, "[features]");
let systemd = features
.iter()
.find(|l| l.starts_with("systemd ="))
.expect("a `systemd` feature");
assert!(systemd.contains("dep:skidbladnir-systemd"), "{systemd}");
let default = features
.iter()
.find(|l| l.starts_with("default ="))
.expect("default");
assert!(
default.contains("\"systemd\""),
"`cargo install holger` keeps `holger install`: {default}"
);
}
#[test]
fn the_install_and_service_verbs_are_there_exactly_with_the_feature() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_holger"))
.arg("--help")
.output()
.expect("holger --help");
let help = String::from_utf8_lossy(&out.stdout);
for verb in ["install", "service"] {
let listed = help
.lines()
.any(|l| l.trim_start().starts_with(&format!("{verb} ")) || l.trim() == verb);
assert_eq!(
listed,
cfg!(feature = "systemd"),
"`{verb}` in --help with systemd={}:\n{help}",
cfg!(feature = "systemd")
);
}
}
/// The crates of `holger` built with `features` (and no default), by name.
fn tree(features: &str) -> Vec<String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let out = std::process::Command::new(cargo)
.args([
"tree",
"--locked",
"--offline",
"-p",
"holger",
"-e",
"normal",
"--prefix",
"none",
"--no-default-features",
"--features",
features,
])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output()
.expect("cargo tree");
assert!(
out.status.success(),
"cargo tree: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut names: Vec<String> = String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|l| l.split_whitespace().next().map(str::to_string))
.collect();
names.sort();
names.dedup();
names
}
/// **The container image carries no Skidbladnir** (lane OX-HOLFLAV, A31 §7.3): the
/// Kubernetes flavour is the default without `systemd` (`cargo xtask k8s image`,
/// `K8S_FEATURES`), and its tree holds no crate of the Skidbladnir repository
/// (`skidbladnir`, `skidbladnir-systemd`, the `nordisk-*` estate crates,
/// `plugin-door-wire`) and no D-Bus. The exe flavour (the default) has exactly the
/// lifecycle crate and zbus more. `holger-handler-skidbladnir` is not Skidbladnir: it is
/// holger's own reader of that bundle FORMAT (a `ron_handler`, beside the `fmt-*`
/// formats), with znippy's path classifier.
#[test]
fn the_container_flavour_links_no_skidbladnir() {
let features = section(MANIFEST, "[features]");
let default = features
.iter()
.find(|l| l.starts_with("default ="))
.expect("default");
let all: Vec<&str> = default
.trim_start_matches("default = [")
.trim_end_matches(']')
.split(',')
.map(|f| f.trim().trim_matches('"'))
.collect();
let container: Vec<&str> = all.iter().copied().filter(|f| *f != "systemd").collect();
// `nordisk-heimdall-*` is Heimdall's job API (lane HOL-SANDBOX-TRAIT), not a Skidbladnir crate.
let forbidden = |n: &str| {
n == "skidbladnir"
|| n.starts_with("skidbladnir-")
|| (n.starts_with("nordisk-") && !n.starts_with("nordisk-heimdall-"))
|| n == "plugin-door-wire"
|| n.starts_with("zbus")
};
let k8s = tree(&container.join(","));
let found: Vec<&String> = k8s.iter().filter(|n| forbidden(n)).collect();
assert!(
found.is_empty(),
"the container flavour ({}) links {found:?}",
container.join(",")
);
let exe = tree(&all.join(","));
let extra: Vec<&String> = exe.iter().filter(|n| forbidden(n)).collect();
assert!(
extra.iter().any(|n| *n == "skidbladnir-systemd") && extra.iter().any(|n| *n == "zbus"),
"the exe flavour has the lifecycle: {extra:?}"
);
assert!(
extra
.iter()
.all(|n| *n == "skidbladnir-systemd" || n.starts_with("zbus")),
"the exe flavour links more of Skidbladnir than the lifecycle: {extra:?}"
);
}