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

nordisk/facett

facett-map/tests/building_height_corpus.rs

//! ★ **The agreement check between facett's height parser and osm-katana's.**
//!
//! `facett_map::height` and `osm_katana::shared::height` are two texts of ONE decision —
//! what metres a building's OSM tagging means. facett must not depend on osm-katana (it
//! is a UI component library; that edge would drag arrow/parquet into every consumer of
//! a map widget), so the duplication is real and LAW 5 is **not** closed by construction.
//! What is done instead is to make it detectable:
//!
//! `tests/data/building_height_corpus.jsonl` carries 95 tag objects whose expected metres
//! were **produced by running `shared::height::of_tags`** (znippy-zoomies `04248be`), not
//! transcribed. This file replays them through facett's parser and fails **naming the tag
//! value** that disagrees. Its header names the regeneration procedure and the
//! osm-katana-side arm that has to be kept in step.
//!
//! Run: `cargo test -p facett-map --test building_height_corpus`

use facett_map::parse_building_height_m;

/// The corpus, as checked in beside this file.
const CORPUS: &str = include_str!("data/building_height_corpus.jsonl");

/// **FNV-1a 64 over the corpus's DATA lines** (`#` comments and blanks excluded, so each
/// repo may word its own header). Not a cryptographic digest and not pretending to be —
/// it is a DRIFT DETECTOR. Regenerate the corpus on one side without the other and this
/// goes red with the new value in the message, instead of two repos quietly meaning two
/// different things.
const CORPUS_DIGEST: u64 = 0x7016_9911_80fb_f507;

/// One corpus row.
struct Row {
    tags: Vec<(String, String)>,
    /// `shared::height`'s answer in metres, or `None` for "no usable height".
    height_m: Option<f32>,
    /// `shared::height`'s `HeightSource` name. facett does not model it (its one
    /// consumer, the extruder default, treats Absent and Rejected identically) — it is
    /// carried so the corpus records the whole reference decision, and so this file can
    /// prove the corpus exercises every branch.
    source: String,
}

fn rows() -> Vec<Row> {
    CORPUS
        .lines()
        .map(str::trim_end)
        .filter(|l| !l.trim().is_empty() && !l.starts_with('#'))
        .map(|line| {
            let v: serde_json::Value =
                serde_json::from_str(line).unwrap_or_else(|e| panic!("corpus line is not JSON: {line}\n{e}"));
            let tags = v["tags"]
                .as_object()
                .unwrap_or_else(|| panic!("corpus line has no `tags` object: {line}"))
                .iter()
                .map(|(k, val)| {
                    (k.clone(), val.as_str().expect("tag values are strings").to_string())
                })
                .collect();
            let height_m = match &v["height_m"] {
                serde_json::Value::Null => None,
                n => Some(n.as_f64().unwrap_or_else(|| panic!("bad height_m: {line}")) as f32),
            };
            Row { tags, height_m, source: v["source"].as_str().unwrap_or("?").to_string() }
        })
        .collect()
}

/// ★ **RED-when-broken: facett's parser reproduces osm-katana's answer on every row.**
///
/// Change ONE arm of `facett_map::height` — the foot constant, the comma rule, the
/// ceiling, `METRES_PER_LEVEL`, the `roof:levels` addition — and this names the tag value
/// that disagreed and both numbers.
#[test]
fn facett_agrees_with_osm_katana_shared_height() {
    let rows = rows();
    let mut disagreements: Vec<String> = Vec::new();
    let (mut accepted, mut refused) = (0usize, 0usize);

    for r in &rows {
        let got = parse_building_height_m(&r.tags);
        match r.height_m {
            Some(_) => accepted += 1,
            None => refused += 1,
        }
        let agrees = match (got, r.height_m) {
            (None, None) => true,
            // The corpus carries `shared::height`'s f32 through JSON as an f64, so the
            // tolerance is a round-trip tolerance, not a semantic one.
            (Some(a), Some(b)) => (a - b).abs() <= 1e-3 * b.abs().max(1.0),
            _ => false,
        };
        if !agrees {
            let shown: Vec<String> =
                r.tags.iter().map(|(k, v)| format!("{k}={v}")).collect();
            disagreements.push(format!(
                "  [{}] facett said {:?} m, osm-katana said {:?} m (source {})",
                shown.join(" "),
                got,
                r.height_m,
                r.source
            ));
        }
    }

    assert!(
        disagreements.is_empty(),
        "facett_map::height and osm_katana::shared::height disagree on {} of {} corpus \
         rows — they are two texts of ONE decision and one of them has drifted:\n{}",
        disagreements.len(),
        rows.len(),
        disagreements.join("\n")
    );

    // The all-`None` guard. A facett parser that refused everything would agree with
    // this corpus on 47 of its 95 rows; the accepted half is what stops it passing.
    assert!(
        accepted >= 40 && refused >= 40,
        "the corpus is {accepted} accepted / {refused} refused — a corpus that lost \
         either half cannot hold a parser to anything"
    );
    // and the corpus must exercise every branch the reference has, or a whole class of
    // disagreement is untested.
    for want in ["absent", "tagged_metres", "tagged_feet", "derived_from_levels", "rejected"] {
        assert!(
            rows.iter().any(|r| r.source == want),
            "no corpus row has source `{want}` — a branch nothing exercises is a branch \
             nothing checks"
        );
    }
}

/// ★ **RED-when-broken: the corpus itself has not been edited on one side only.**
///
/// The corpus is the shared text. If it is regenerated in facett and not in
/// znippy-zoomies (or hand-patched to make a failing row pass), the two repos silently
/// mean two different things again — which is the exact failure this whole arrangement
/// exists to prevent. Edit any data line and this names the new digest to paste in.
#[test]
fn the_corpus_has_not_drifted_on_one_side() {
    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
    let mut lines = 0usize;
    for line in CORPUS.lines().map(str::trim_end) {
        if line.trim().is_empty() || line.starts_with('#') {
            continue;
        }
        lines += 1;
        for b in line.as_bytes().iter().copied().chain(std::iter::once(b'\n')) {
            h ^= u64::from(b);
            h = h.wrapping_mul(0x0000_0100_0000_01b3);
        }
    }
    assert_eq!(
        lines, 95,
        "the corpus has {lines} data rows, not the 95 it was generated with"
    );
    assert_eq!(
        h, CORPUS_DIGEST,
        "building_height_corpus.jsonl changed. If that was deliberate, regenerate it \
         from osm_katana::shared::height, put the SAME data lines in znippy-zoomies, and \
         set CORPUS_DIGEST = {h:#018x}. If it was not deliberate, the corpus was edited \
         to make a failing row pass and the two parsers now disagree in production."
    );
}