xtask/tests/build_flow_svg.rs
//! **The generated build-flow diagram must not lie.**
//!
//! `cargo xtask build-flow-svg` exists to draw ONE distinction: which steps of a
//! `Build{variant}` the DECLARATION orders (variant → features → settings /
//! sub-settings → typ) and which are fixed in Rust. A diagram that renders both
//! halves identically while its legend claims they differ is worse than no
//! diagram — it is a silent green with a picture attached.
//!
//! So these tests are about the RENDERING, not about the data:
//!
//! * [`the_two_verdicts_render_differently`] — swap a step's `Origin` and the
//! emitted bytes must change. A generator that painted every box cyan passes
//! every "does it contain a rect" test ever written and fails this one.
//! * [`every_hard_coded_step_names_where_it_was_read`] — a coral box with no
//! `file:line` is a drawing again. The citation is the claim.
//! * [`the_legend_counts_the_boxes_it_draws`] — the two numbers in the legend
//! are counted from `STEPS`, so a step added without a verdict cannot slip
//! past the summary.
//! * [`the_svg_is_well_formed_and_has_no_stray_braces`] — `write!` over a raw
//! string is easy to get wrong (a `"#` inside a colour literal closes an
//! `r#"…"#`), and a half-written SVG still "renders" in a browser as nothing.
//!
//! Run: `cargo test -p xtask --test build_flow_svg`
use xtask::build_flow_svg::{self, Origin, STEPS};
#[test]
fn the_two_verdicts_render_differently() {
let declared = STEPS
.iter()
.find(|s| matches!(s.origin, Origin::Declared))
.expect("a declared step");
let fixed = STEPS
.iter()
.find(|s| matches!(s.origin, Origin::HardCoded { .. }))
.expect("a hard-coded step");
let svg = build_flow_svg::svg();
// The distinction must be carried by TWO independent channels — colour and
// dash — so it survives a greyscale print and a colour-blind reader. A
// generator that only changed the hue would pass a colour-only assertion.
assert!(
svg.contains("#6fd3f5"),
"the declared stroke is absent entirely"
);
assert!(
svg.contains("#f0a9a0"),
"the hard-coded stroke is absent entirely"
);
assert!(
svg.contains("stroke-dasharray=\"6 4\""),
"hard-coded boxes are not dashed — the distinction is colour-only"
);
// And the two example boxes must not be painted the same. Find each box's
// rect by its following label and compare the stroke colour that precedes it.
let stroke_near = |name: &str| -> String {
let at = svg
.find(&format!(">{name}<"))
.unwrap_or_else(|| panic!("no box named {name}"));
let head = &svg[..at];
let rect = head.rfind("<rect").expect("a rect before the label");
head[rect..].to_string()
};
let a = stroke_near(declared.name);
let b = stroke_near(fixed.name);
assert!(
a.contains("#6fd3f5"),
"{} is not drawn declared: {a}",
declared.name
);
assert!(
b.contains("#f0a9a0"),
"{} is not drawn hard-coded: {b}",
fixed.name
);
assert_ne!(a, b, "the two verdicts render byte-identically");
}
#[test]
fn every_hard_coded_step_names_where_it_was_read() {
let svg = build_flow_svg::svg();
for s in STEPS {
match &s.origin {
Origin::Declared => {}
Origin::HardCoded { at } | Origin::TypedNeverRun { at } => {
let cite = format!("{}:{}", at.file, at.line);
assert!(
svg.contains(&cite),
"'{}' is drawn as a code decision but its citation {cite} is not on the \
diagram — an uncitable claim is a drawing, not a generated fact",
s.name
);
assert!(at.line > 0, "'{}' cites line 0", s.name);
assert!(
at.file.ends_with(".rs"),
"'{}' cites {} — a claim about code cites a source file",
s.name,
at.file
);
}
}
}
}
#[test]
fn the_legend_counts_the_boxes_it_draws() {
let svg = build_flow_svg::svg();
let declared = STEPS
.iter()
.filter(|s| matches!(s.origin, Origin::Declared))
.count();
let fixed = STEPS
.iter()
.filter(|s| matches!(s.origin, Origin::HardCoded { .. }))
.count();
assert!(
declared > 0 && fixed > 0,
"a diagram with only one verdict proves nothing"
);
assert!(
svg.contains(&format!("{declared} steps the recipe orders")),
"the legend does not count the declared boxes ({declared})"
);
assert!(
svg.contains(&format!("{fixed} steps fixed in Rust")),
"the legend does not count the hard-coded boxes ({fixed})"
);
// Every step reaches the canvas: a box the renderer skipped would still be
// counted by the legend, which is the exact inversion of an honest summary.
for s in STEPS {
assert!(
svg.contains(&format!(">{}<", s.name)),
"'{}' was counted but not drawn",
s.name
);
}
}
#[test]
fn the_svg_is_well_formed_and_has_no_stray_braces() {
let svg = build_flow_svg::svg();
assert!(svg.starts_with("<svg "), "no svg root");
assert!(svg.trim_end().ends_with("</svg>"), "unclosed svg root");
// An unsubstituted `write!` placeholder is the failure mode of a raw-string
// template, and it renders as literal text rather than as an error.
assert!(
!svg.contains("{W}"),
"an unsubstituted placeholder survived"
);
assert!(
!svg.contains("{height}"),
"an unsubstituted placeholder survived"
);
// Balanced tags, cheaply: every `<rect` has a self-close, every `<text` a
// `</text>`.
assert_eq!(
svg.matches("<text").count(),
svg.matches("</text>").count(),
"unbalanced <text> elements"
);
// Raw `&` would make it invalid XML; the renderer escapes to entities.
for (i, l) in svg.lines().enumerate() {
for (j, _) in l.match_indices('&') {
let tail = &l[j..];
assert!(
tail.starts_with("&")
|| tail.starts_with("<")
|| tail.starts_with(">")
|| tail.starts_with("&#"),
"line {}: a bare '&' makes this invalid XML: {l}",
i + 1
);
}
}
}