tests/collectors_osm.rs
//! OSM connectors (feature `osm`): the `ways_to_batch` WKB→Arrow contract, plus
//! the OSM-container sniff that turns an osm-katana panic into an honest
//! error. The `RegionSource` fetch seam this file also used to cover is gone —
//! see the note in the middle of the file.
#![cfg(feature = "osm")]
use arrow_array::{Array, BinaryArray, Int64Array, StringArray};
use korp_collectors::osm::{PbfWayRecord, osm_schema, ways_to_batch};
mod common;
use common::emit_for;
/// The bundled tiny extract (generated offline with osm-katana `xml2-pbf` from a
/// 4-node / 2-way synthetic OSM XML → 454-byte zstd PBF). Real OSM bytes.
const FIXTURE_PBF: &[u8] = include_bytes!("fixtures/testland-latest.osm.pbf");
#[test]
fn ways_map_to_wkb_arrow() {
let wkb = vec![0x01u8, 0x02, 0x00, 0x00, 0x00];
let ways = vec![
PbfWayRecord {
id: 42,
geometry: Some(wkb.clone()),
tags: r#"{"highway":"primary"}"#.into(),
},
PbfWayRecord {
id: 99,
geometry: None,
tags: "{}".into(),
},
];
let batch = ways_to_batch(&ways).expect("ways → arrow");
assert_eq!(&batch.schema(), &std::sync::Arc::new(osm_schema()));
assert_eq!(batch.num_rows(), 2);
let id = batch
.column(0)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap();
let geom = batch
.column(1)
.as_any()
.downcast_ref::<BinaryArray>()
.unwrap();
let tags = batch
.column(2)
.as_any()
.downcast_ref::<StringArray>()
.unwrap();
assert_eq!(id.value(0), 42);
assert_eq!(geom.value(0), wkb.as_slice());
assert!(geom.is_null(1), "geometry-less way keeps a null WKB cell");
assert!(tags.value(0).contains("highway"));
emit_for(
"korp-collectors/osm",
"ways_to_batch_wkb",
true,
"2 ways → arrow, WKB preserved, null geom kept",
);
}
// ── the `RegionSource` fetch seam: DELETED, and its three tests with it ──────
//
// `korp_collectors::region` (`RegionSource` / `HttpOsmSource` / `CachedLocalSource`)
// was removed on 2026-08-13 — see the note at the foot of `src/lib.rs`: this crate
// does not acquire regions over HTTP at all; the planet arrives over BitTorrent and
// every region is cut out of it. The three tests that drove that seam, and the
// single-shot HTTP server they shared, went with it.
//
// They did NOT go quietly enough: the `use korp_collectors::region::{…}` line stayed,
// so this whole target has failed to COMPILE ever since — `cargo test -p
// korp-collectors --features osm` stops at `error[E0432]: unresolved import`, and the
// three surviving tests in this file (the WKB→Arrow contract and the two OSM-container
// guards) have not run in that window either. A test file that cannot compile is the
// loudest possible version of a silent green, and it was invisible because nothing in
// the house builds this crate with `--features osm-daemon`.
// ── the OSM container guard (found by the registry RAGNARÖK sweep) ───────────
/// `osm-katana` PANICS when handed a file that is not an OSM container — inside
/// its own blocking worker, so a collector reports an opaque `JoinError` instead
/// of an honest RAGNARÖK reason. znippy-zoomies is hands-off, so the sniff lives
/// at this crate's seam. RED-when-broken: drop the guard and the empty/garbage
/// cases below stop being errors (they become panics, which fails this test too).
#[test]
fn ensure_osm_container_accepts_real_extracts_and_rejects_everything_else() {
use korp_collectors::osm::ensure_osm_container;
let dir = tempfile::tempdir().expect("tempdir");
// The bundled real PBF passes.
let pbf = dir.path().join("ok.osm.pbf");
std::fs::write(&pbf, FIXTURE_PBF).unwrap();
ensure_osm_container(pbf.to_str().unwrap()).expect("a real .osm.pbf is an OSM container");
// An OSM XML document passes.
let xml = dir.path().join("ok.osm");
std::fs::write(&xml, br#"<?xml version="1.0"?><osm version="0.6"></osm>"#).unwrap();
ensure_osm_container(xml.to_str().unwrap()).expect("an OSM XML document is an OSM container");
// A ZERO-BYTE file is rejected by name — the exact input that used to panic.
let empty = dir.path().join("empty.osm.pbf");
std::fs::write(&empty, b"").unwrap();
let err = ensure_osm_container(empty.to_str().unwrap()).expect_err("empty file rejected");
assert!(
err.to_string().contains("empty"),
"the error says what is wrong: {err}"
);
// Arbitrary bytes are rejected.
let junk = dir.path().join("junk.osm.pbf");
std::fs::write(&junk, b"this is a CSV,not,an,osm,extract\n1,2,3,4,5\n").unwrap();
let err = ensure_osm_container(junk.to_str().unwrap()).expect_err("non-OSM bytes rejected");
assert!(err.to_string().contains("not an OSM container"), "{err}");
// A missing path is an error, not a panic.
assert!(ensure_osm_container(dir.path().join("nope.pbf").to_str().unwrap()).is_err());
emit_for(
"korp-collectors/osm",
"osm_container_guard",
true,
"real PBF + OSM XML accepted; empty / junk / missing rejected with a reason, never a panic",
);
}
/// End-to-end: the ways collector fed a NON-OSM file comes back as an explained
/// 0-row collect (RAGNARÖK honest), not a panic and not a silent empty.
#[tokio::test]
async fn osm_ways_collect_on_a_non_osm_file_is_an_explained_empty() {
use korp_collectors::{BatchSink, Bounds, Collector, CollectorCfg, Ragnarok};
let dir = tempfile::tempdir().expect("tempdir");
let junk = dir.path().join("not-osm.pbf");
std::fs::write(&junk, b"").unwrap();
let cfg = CollectorCfg::new(junk.to_string_lossy().to_string(), "osm_ways")
.with_params(serde_json::json!({ "path": junk.to_string_lossy() }));
let sink = BatchSink::memory();
let stats = korp_collectors::osm::OsmGeoCollector
.collect(&cfg, Bounds::default(), &sink)
.await
.expect("a non-OSM file must not panic or error out of collect()");
assert_eq!(stats.rows, 0);
assert_eq!(sink.batches(), 0, "nothing pushed");
assert_eq!(
stats.ragnarok(),
Ragnarok::ExplainedEmpty,
"0 rows must carry a reason: {:?}",
stats.reason
);
emit_for(
"korp-collectors/osm",
"osm_non_container_is_explained_empty",
true,
"empty .pbf → 0 rows with a reason (no panic, no silent green)",
);
}