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

nordisk/korp-collectors

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