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."
);
}