#![allow(clippy::unwrap_used)]
mod common;
use std::path::Path;
use common::cli::CliInvoker;
use common::{
TestNode, TestRelation, TestWay, generate_nodes, generate_relations, generate_ways,
read_header, read_normalized, write_multi_block_test_pbf,
};
use pbfhogg::{BlobDecode, BlobReader, Element};
fn run_repack(input: &Path, output: &Path, elements_per_blob: usize) -> common::cli::CliOutput {
CliInvoker::new()
.arg("repack")
.arg(input)
.arg("-o")
.arg(output)
.arg("--elements-per-blob")
.arg(elements_per_blob.to_string())
.assert_success()
}
fn elements_per_blob(path: &Path) -> Vec<(usize, usize, usize)> {
let reader = BlobReader::from_path(path).expect("open pbf");
let mut per_blob = Vec::new();
for blob in reader {
let blob = blob.expect("read blob");
if let BlobDecode::OsmData(block) = blob.decode().expect("decode blob") {
let mut nodes = 0;
let mut ways = 0;
let mut rels = 0;
for element in block.elements() {
match element {
Element::Node(_) | Element::DenseNode(_) => nodes += 1,
Element::Way(_) => ways += 1,
Element::Relation(_) => rels += 1,
_ => {}
}
}
per_blob.push((nodes, ways, rels));
}
}
per_blob
}
fn write_repack_fixture(path: &Path) -> (Vec<TestNode>, Vec<TestWay>, Vec<TestRelation>) {
let mut nodes = generate_nodes(60, 1);
nodes[0].tags = vec![("place", "city"), ("name", "Origo")];
nodes[7].tags = vec![("amenity", "cafe")];
nodes[42].tags = vec![("highway", "bus_stop")];
let ways = generate_ways(12, 1, 3, 1);
let rels = generate_relations(3, 1, 2, 1);
write_multi_block_test_pbf(path, &nodes, &ways, &rels, 20);
(nodes, ways, rels)
}
#[test]
fn repack_round_trip_preserves_elements_on_shrink() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
let (nodes, ways, rels) = write_repack_fixture(&input);
run_repack(&input, &output, 10);
let original = read_normalized(&input);
let repacked = read_normalized(&output);
assert_eq!(repacked.nodes.len(), nodes.len());
assert_eq!(repacked.ways.len(), ways.len());
assert_eq!(repacked.relations.len(), rels.len());
assert_eq!(original.nodes, repacked.nodes);
assert_eq!(original.ways, repacked.ways);
assert_eq!(original.relations, repacked.relations);
}
#[test]
fn repack_respects_element_cap() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
run_repack(&input, &output, 10);
let per_blob = elements_per_blob(&output);
assert!(!per_blob.is_empty(), "output has no OsmData blobs");
for (i, (n, w, r)) in per_blob.iter().enumerate() {
let total = n + w + r;
assert!(total <= 10, "blob {i}: {total} elements exceeds cap 10");
let kinds_present = u8::from(*n > 0) + u8::from(*w > 0) + u8::from(*r > 0);
assert_eq!(kinds_present, 1, "blob {i} is mixed-kind: ({n}, {w}, {r})");
}
}
#[test]
fn repack_blob_count_matches_prediction() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
run_repack(&input, &output, 10);
let per_blob = elements_per_blob(&output);
let node_blobs = per_blob.iter().filter(|(n, _, _)| *n > 0).count();
let way_blobs = per_blob.iter().filter(|(_, w, _)| *w > 0).count();
let rel_blobs = per_blob.iter().filter(|(_, _, r)| *r > 0).count();
assert_eq!(node_blobs, 6, "expected 6 node blobs (60 nodes / cap 10)");
assert_eq!(
way_blobs, 2,
"expected 2 way blobs (12 ways: 10 + 2 partial)"
);
assert_eq!(
rel_blobs, 1,
"expected 1 relation blob (3 relations partial)"
);
}
#[test]
fn repack_propagates_sorted_flag() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
assert!(read_header(&input).is_sorted(), "fixture must be sorted");
run_repack(&input, &output, 10);
assert!(
read_header(&output).is_sorted(),
"output dropped Sort.Type_then_ID"
);
}
#[test]
fn repack_rejects_zero_cap() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
CliInvoker::new()
.arg("repack")
.arg(&input)
.arg("-o")
.arg(&output)
.arg("--elements-per-blob")
.arg("0")
.assert_failure()
.assert_stderr_contains("must be > 0");
}
#[test]
fn repack_grow_collapses_to_one_blob_per_kind() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
let out = run_repack(&input, &output, 8000);
out.assert_stderr_contains("--elements-per-blob 8000 never fired");
let original = read_normalized(&input);
let repacked = read_normalized(&output);
assert_eq!(original.nodes, repacked.nodes);
assert_eq!(original.ways, repacked.ways);
assert_eq!(original.relations, repacked.relations);
let per_blob = elements_per_blob(&output);
let node_blobs = per_blob.iter().filter(|(n, _, _)| *n > 0).count();
let way_blobs = per_blob.iter().filter(|(_, w, _)| *w > 0).count();
let rel_blobs = per_blob.iter().filter(|(_, _, r)| *r > 0).count();
assert_eq!(
node_blobs, 1,
"expected 60 nodes to collapse to 1 blob (cap 8000)"
);
assert_eq!(
way_blobs, 1,
"expected 12 ways to collapse to 1 blob (cap 8000)"
);
assert_eq!(
rel_blobs, 1,
"expected 3 relations to collapse to 1 blob (cap 8000)"
);
}
#[test]
fn repack_round_trip_preserves_elements_on_grow() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
let (nodes, ways, rels) = write_repack_fixture(&input);
run_repack(&input, &output, 30);
let original = read_normalized(&input);
let repacked = read_normalized(&output);
assert_eq!(repacked.nodes.len(), nodes.len());
assert_eq!(repacked.ways.len(), ways.len());
assert_eq!(repacked.relations.len(), rels.len());
assert_eq!(original.nodes, repacked.nodes);
assert_eq!(original.ways, repacked.ways);
assert_eq!(original.relations, repacked.relations);
}
#[test]
fn repack_grow_blob_count_matches_prediction() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
run_repack(&input, &output, 30);
let per_blob = elements_per_blob(&output);
let node_blobs = per_blob.iter().filter(|(n, _, _)| *n > 0).count();
let way_blobs = per_blob.iter().filter(|(_, w, _)| *w > 0).count();
let rel_blobs = per_blob.iter().filter(|(_, _, r)| *r > 0).count();
assert_eq!(node_blobs, 2, "expected 2 node blobs (60 / cap 30)");
assert_eq!(way_blobs, 1, "expected 1 way blob (12 < cap 30)");
assert_eq!(rel_blobs, 1, "expected 1 relation blob (3 < cap 30)");
}
#[test]
fn repack_grow_no_warning_when_cap_fires() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
let out = run_repack(&input, &output, 30);
let stderr = out.stderr_str();
assert!(
!stderr.contains("never fired"),
"no-op-cap warning should not fire when the cap fires mid-stream; stderr was:\n{stderr}"
);
}
#[test]
fn repack_no_warning_when_cap_fires() {
let dir = tempfile::tempdir().expect("tempdir");
let input = dir.path().join("in.osm.pbf");
let output = dir.path().join("out.osm.pbf");
write_repack_fixture(&input);
let out = run_repack(&input, &output, 10);
let stderr = out.stderr_str();
assert!(
!stderr.contains("never fired"),
"no-op-cap warning should not fire on a real shrink; stderr was:\n{stderr}"
);
}