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

nordisk/dwarves

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("&amp;")
                    || tail.starts_with("&lt;")
                    || tail.starts_with("&gt;")
                    || tail.starts_with("&#"),
                "line {}: a bare '&' makes this invalid XML: {l}",
                i + 1
            );
        }
    }
}