nigig-org/crates/apps/map/tests/tile_decode_integration.rs
andodeki 08d3e9a7fb
Some checks failed
nigig-map / test (push) Has been cancelled
fix(map): increase MAX_ELEMENTS_PER_TILE to 250k and add CI workflow
- Increased MAX_ELEMENTS_PER_TILE from 100,000 to 250,000 to handle
  Kenya MBTiles that contain 101k-140k elements per tile
- Updated security limit test to use valid JSON with 260k elements
- Added .forgejo/workflows/nigig-map.yml CI workflow to catch
  map-related regressions in tile parsing, style compilation,
  and tessellation

Fixes runtime errors:
- 'failed to triangulate local mbtile: too many elements (N > 100000)'
- Tiles with 101k-140k elements now process successfully
2026-07-28 20:41:02 +00:00

367 lines
11 KiB
Rust

//! Integration tests for the tile decoding pipeline
//!
//! These tests verify the end-to-end functionality of tile decoding,
//! from raw MVT/JSON data to renderable tile buffers.
use nigig_map::tile_decode::{
build_tile_buffers_from_body, mbtiles_tile_to_overpass_response,
};
use nigig_map::geometry::TileKey;
use nigig_map::style::CompiledMapTheme;
/// Test decoding a simple Overpass JSON response with nodes and ways
#[test]
fn test_decode_simple_overpass_response() {
let json = r#"{
"version": 0.6,
"elements": [
{
"type": "node",
"id": 1,
"lat": 52.3676,
"lon": 4.9041,
"tags": {
"name": "Amsterdam Central"
}
},
{
"type": "way",
"id": 2,
"nodes": [1, 3, 4, 1],
"tags": {
"building": "yes",
"name": "Test Building"
}
},
{
"type": "node",
"id": 3,
"lat": 52.3680,
"lon": 4.9045
},
{
"type": "node",
"id": 4,
"lat": 52.3678,
"lon": 4.9048
}
]
}"#;
let tile_key = TileKey { z: 14, x: 8536, y: 5534 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Failed to decode: {:?}", result.err());
let buffers = result.unwrap();
assert!(buffers.feature_count > 0, "Should have decoded at least one feature");
assert!(!buffers.fill_vertices.is_empty() || !buffers.stroke_vertices.is_empty(),
"Should have generated geometry");
}
/// Test decoding with security limits (should reject oversized input)
#[test]
fn test_security_limit_json_size() {
// Create a JSON that exceeds the element count limit (250,000).
// Build valid JSON with comma-separated elements.
let mut elements = String::new();
for i in 0..260_000 {
if i > 0 { elements.push(','); }
elements.push_str(&format!(
r#"{{"type":"node","id":{},"lat":0.0,"lon":0.0}}"#,
i
));
}
let large_json = format!(r#"{{"elements":[{}]}}"#, elements);
let tile_key = TileKey { z: 14, x: 0, y: 0 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, &large_json, &theme);
assert!(result.is_err(), "Should reject oversized JSON");
assert!(result.unwrap_err().contains("too large") ||
result.unwrap_err().contains("too many"),
"Error should mention size limit");
}
/// Test decoding with invalid JSON
#[test]
fn test_decode_invalid_json() {
let invalid_json = r#"{"version": 0.6, "elements": [invalid}"#;
let tile_key = TileKey { z: 14, x: 0, y: 0 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, invalid_json, &theme);
assert!(result.is_err(), "Should reject invalid JSON");
}
/// Test decoding empty elements array
#[test]
fn test_decode_empty_elements() {
let json = r#"{"version": 0.6, "elements": []}"#;
let tile_key = TileKey { z: 14, x: 0, y: 0 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Should handle empty elements");
let buffers = result.unwrap();
assert_eq!(buffers.feature_count, 0, "Should have no features");
}
/// Test decoding various OSM feature types
#[test]
fn test_decode_various_feature_types() {
let json = r#"{
"version": 0.6,
"elements": [
{
"type": "node",
"id": 1,
"lat": 52.3676,
"lon": 4.9041,
"tags": {"amenity": "restaurant", "name": "Test Restaurant"}
},
{
"type": "way",
"id": 2,
"nodes": [10, 11, 12, 10],
"tags": {"highway": "residential", "name": "Test Street"}
},
{
"type": "way",
"id": 3,
"nodes": [20, 21, 22, 23, 20],
"tags": {"natural": "water", "name": "Test Lake"}
},
{
"type": "node", "id": 10, "lat": 52.367, "lon": 4.904
},
{
"type": "node", "id": 11, "lat": 52.368, "lon": 4.905
},
{
"type": "node", "id": 12, "lat": 52.367, "lon": 4.905
},
{
"type": "node", "id": 20, "lat": 52.369, "lon": 4.906
},
{
"type": "node", "id": 21, "lat": 52.370, "lon": 4.907
},
{
"type": "node", "id": 22, "lat": 52.369, "lon": 4.908
},
{
"type": "node", "id": 23, "lat": 52.368, "lon": 4.907
}
]
}"#;
let tile_key = TileKey { z: 14, x: 8536, y: 5534 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Failed to decode: {:?}", result.err());
let buffers = result.unwrap();
assert!(buffers.feature_count >= 3, "Should decode restaurant, street, and lake");
// Should have POIs (restaurant)
assert!(!buffers.pois.is_empty(), "Should have POIs");
// Should have labels
assert!(!buffers.labels.is_empty(), "Should have labels");
}
/// Test decoding with different zoom levels
#[test]
fn test_decode_different_zoom_levels() {
let json = r#"{
"version": 0.6,
"elements": [
{
"type": "way",
"id": 1,
"nodes": [10, 11, 12, 10],
"tags": {"building": "yes"}
},
{
"type": "node", "id": 10, "lat": 52.367, "lon": 4.904
},
{
"type": "node", "id": 11, "lat": 52.368, "lon": 4.905
},
{
"type": "node", "id": 12, "lat": 52.367, "lon": 4.905
}
]
}"#;
let theme = CompiledMapTheme::default();
// Test at different zoom levels
for zoom in [10, 12, 14, 16] {
let tile_key = TileKey { z: zoom, x: 0, y: 0 };
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Failed at zoom {}: {:?}", zoom, result.err());
}
}
/// Test that malformed way references are handled gracefully
#[test]
fn test_decode_malformed_way_references() {
let json = r#"{
"version": 0.6,
"elements": [
{
"type": "way",
"id": 1,
"nodes": [999, 1000, 1001],
"tags": {"highway": "residential"}
}
]
}"#;
let tile_key = TileKey { z: 14, x: 0, y: 0 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Should handle missing nodes gracefully");
let buffers = result.unwrap();
// Way should be skipped due to missing nodes
assert_eq!(buffers.feature_count, 0, "Should skip malformed way");
}
/// Test decoding with Unicode characters in tags
#[test]
fn test_decode_unicode_tags() {
let json = r#"{
"version": 0.6,
"elements": [
{
"type": "node",
"id": 1,
"lat": 52.3676,
"lon": 4.9041,
"tags": {
"name": "阿姆斯特丹",
"name:ja": "アムステルダム",
"name:ar": "أمستردام"
}
}
]
}"#;
let tile_key = TileKey { z: 14, x: 0, y: 0 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Should handle Unicode tags");
}
/// Test MBTiles to Overpass conversion
#[test]
fn test_mbtiles_to_overpass_conversion() {
// This test would require actual MVT binary data
// For now, we'll skip it or use a mock
// In a real implementation, we'd load a test MVT file
// Placeholder test
let tile_key = TileKey { z: 14, x: 0, y: 0 };
let mock_mvt_data = vec![]; // Would be actual MVT binary
if !mock_mvt_data.is_empty() {
let result = mbtiles_tile_to_overpass_response(tile_key, &mock_mvt_data);
assert!(result.is_ok(), "Should convert MVT to Overpass format");
}
}
/// Test that geometry is correctly tessellated
#[test]
fn test_geometry_tessellation() {
let json = r#"{
"version": 0.6,
"elements": [
{
"type": "way",
"id": 1,
"nodes": [10, 11, 12, 13, 10],
"tags": {"building": "yes"}
},
{
"type": "node", "id": 10, "lat": 52.367, "lon": 4.904
},
{
"type": "node", "id": 11, "lat": 52.368, "lon": 4.904
},
{
"type": "node", "id": 12, "lat": 52.368, "lon": 4.905
},
{
"type": "node", "id": 13, "lat": 52.367, "lon": 4.905
}
]
}"#;
let tile_key = TileKey { z: 14, x: 8536, y: 5534 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Failed to decode: {:?}", result.err());
let buffers = result.unwrap();
// Building should generate fill geometry
assert!(!buffers.fill_vertices.is_empty(), "Should have fill vertices");
assert!(!buffers.fill_indices.is_empty(), "Should have fill indices");
// Vertices should be in groups of 7 (x, y, color, nx, ny, shape_id, zbias)
assert_eq!(buffers.fill_vertices.len() % 7, 0,
"Fill vertices should be properly formatted");
}
/// Test stroke geometry generation
#[test]
fn test_stroke_geometry_generation() {
let json = r#"{
"version": 0.6,
"elements": [
{
"type": "way",
"id": 1,
"nodes": [10, 11, 12],
"tags": {"highway": "primary"}
},
{
"type": "node", "id": 10, "lat": 52.367, "lon": 4.904
},
{
"type": "node", "id": 11, "lat": 52.368, "lon": 4.905
},
{
"type": "node", "id": 12, "lat": 52.369, "lon": 4.906
}
]
}"#;
let tile_key = TileKey { z: 14, x: 8536, y: 5534 };
let theme = CompiledMapTheme::default();
let result = build_tile_buffers_from_body(tile_key, json, &theme);
assert!(result.is_ok(), "Failed to decode: {:?}", result.err());
let buffers = result.unwrap();
// Highway should generate stroke geometry
assert!(!buffers.stroke_vertices.is_empty(), "Should have stroke vertices");
assert!(!buffers.stroke_indices.is_empty(), "Should have stroke indices");
// Vertices should be in groups of 7
assert_eq!(buffers.stroke_vertices.len() % 7, 0,
"Stroke vertices should be properly formatted");
}