#[cfg(test)] mod grid_index_tests { use crate::grid_index::GridIndex; #[test] fn query_bounds_fully_below_grid_returns_empty() { let lat = vec![50.0_f32, 50.5, 51.0]; let lon = vec![0.0_f32, 0.5, 1.0]; let grid = GridIndex::build(&lat, &lon, 0.01); let results = grid.query(10.0, -10.0, 20.0, -5.0); assert!( results.is_empty(), "Should return empty for bounds fully below grid" ); } #[test] fn query_bounds_fully_above_grid_returns_empty() { let lat = vec![50.0_f32, 50.5, 51.0]; let lon = vec![0.0_f32, 0.5, 1.0]; let grid = GridIndex::build(&lat, &lon, 0.01); let results = grid.query(80.0, 50.0, 90.0, 60.0); assert!( results.is_empty(), "Should return empty for bounds fully above grid" ); } #[test] fn query_inverted_bounds_returns_empty() { let lat = vec![50.0_f32, 50.5, 51.0]; let lon = vec![0.0_f32, 0.5, 1.0]; let grid = GridIndex::build(&lat, &lon, 0.01); // south > north let results = grid.query(52.0, 0.0, 49.0, 1.0); assert!( results.is_empty(), "Should return empty for inverted bounds" ); } #[test] fn for_each_bounds_fully_outside_yields_nothing() { let lat = vec![50.0_f32, 50.5, 51.0]; let lon = vec![0.0_f32, 0.5, 1.0]; let grid = GridIndex::build(&lat, &lon, 0.01); let mut count = 0; grid.for_each_in_bounds(10.0, -10.0, 20.0, -5.0, |_| count += 1); assert_eq!( count, 0, "for_each should yield nothing for out-of-bounds query" ); } #[test] fn query_with_large_cells_outside_returns_empty() { // Previously, out-of-bounds queries with large cell sizes would // scan cell (0,0) which could contain data. Now returns empty. let lat = vec![50.0_f32]; let lon = vec![0.0_f32]; let grid = GridIndex::build(&lat, &lon, 1.0); let results = grid.query(0.0, -50.0, 10.0, -40.0); assert!( results.is_empty(), "Should return empty even with large cell size" ); } #[test] fn query_within_bounds_returns_correct_results() { let lat = vec![50.0_f32, 50.5, 51.0]; let lon = vec![0.0_f32, 0.5, 1.0]; let grid = GridIndex::build(&lat, &lon, 0.01); let results = grid.query(49.9, -0.1, 51.1, 1.1); assert_eq!(results.len(), 3, "Should return all 3 points within bounds"); } #[test] fn query_partial_bounds_returns_subset() { let lat = vec![50.0_f32, 51.0, 52.0]; let lon = vec![0.0_f32, 0.0, 0.0]; let grid = GridIndex::build(&lat, &lon, 0.01); let results = grid.query(49.9, -0.1, 50.1, 0.1); assert_eq!(results.len(), 1, "Should return only the point at lat=50"); } } #[cfg(test)] mod filter_tests { use crate::data::EnumFeatureData; use crate::filter::{parse_filters, row_passes_filters}; #[test] fn nan_rows_fail_numeric_filter_even_with_infinite_range() { let feature_names = vec!["price".to_string()]; let feature_data = vec![f32::NAN]; let enum_features: Vec = vec![]; let enum_data: Vec = vec![]; let (numeric, enums) = parse_filters(Some("price:-inf:inf"), &feature_names, &enum_features); assert_eq!(numeric.len(), 1, "Should parse -inf:inf as valid filter"); let passes = row_passes_filters(0, &numeric, &enums, &feature_data, 1, &enum_data, 0); assert!(!passes, "NaN should fail filter even with infinite range"); } #[test] fn empty_enum_filter_value_rejects_all() { let enum_features = vec![EnumFeatureData { name: "rating".to_string(), values: vec!["A".to_string(), "B".to_string()], }]; let feature_names: Vec = vec![]; // Row-major enum data: 1 row, 1 enum, value=0 (index into "A") let enum_data: Vec = vec![0]; let (numeric, enums) = parse_filters(Some("rating:"), &feature_names, &enum_features); assert_eq!(enums.len(), 1); assert!(enums[0].allowed.is_empty()); let passes = row_passes_filters(0, &numeric, &enums, &[], 0, &enum_data, 1); assert!(!passes, "Empty allowed set should reject all rows"); } #[test] fn enum_filter_with_nonexistent_values_produces_empty_allowed() { let enum_features = vec![EnumFeatureData { name: "rating".to_string(), values: vec!["A".to_string(), "B".to_string()], }]; let feature_names: Vec = vec![]; let (_, enums) = parse_filters(Some("rating:X|Y|Z"), &feature_names, &enum_features); assert_eq!(enums.len(), 1); assert!(enums[0].allowed.is_empty()); } #[test] fn malformed_numeric_min_is_silently_skipped() { let feature_names = vec!["price".to_string()]; let enum_features: Vec = vec![]; let (numeric, enums) = parse_filters( Some("price:not_a_number:200"), &feature_names, &enum_features, ); assert_eq!(numeric.len(), 0); assert_eq!(enums.len(), 0); } } #[cfg(test)] mod json_tests { #[test] fn json_escaped_postcode_with_quotes_is_valid() { use crate::routes::hexagons::write_json_escaped; let mut buf = String::new(); buf.push_str("{\"postcode\":\""); write_json_escaped(&mut buf, "SW1A \"test"); buf.push_str("\"}"); let result: Result = serde_json::from_str(&buf); assert!( result.is_ok(), "Escaped quote should produce valid JSON: {}", buf ); assert_eq!(result.unwrap()["postcode"].as_str().unwrap(), "SW1A \"test"); } #[test] fn json_escaped_postcode_with_backslash_is_valid() { use crate::routes::hexagons::write_json_escaped; let mut buf = String::new(); buf.push_str("{\"postcode\":\""); write_json_escaped(&mut buf, "SW1A\\2AA"); buf.push_str("\"}"); let result: Result = serde_json::from_str(&buf); assert!( result.is_ok(), "Escaped backslash should produce valid JSON: {}", buf ); assert_eq!(result.unwrap()["postcode"].as_str().unwrap(), "SW1A\\2AA"); } #[test] fn nan_is_not_valid_json() { use std::fmt::Write; // Verify that raw NaN in write! is still invalid JSON (documenting the risk // that the is_finite() guard in write_hexagons_json protects against). let mut buf = String::new(); write!(buf, "{{\"min_price\":{}}}", f64::NAN).unwrap(); let result: Result = serde_json::from_str(&buf); assert!(result.is_err(), "Raw NaN should produce invalid JSON"); } #[test] fn infinity_is_not_valid_json() { use std::fmt::Write; let mut buf = String::new(); write!(buf, "{{\"min_price\":{}}}", f64::INFINITY).unwrap(); let result: Result = serde_json::from_str(&buf); assert!(result.is_err(), "Raw Infinity should produce invalid JSON"); } } #[cfg(test)] mod enum_encoding_tests { #[test] fn u8_cast_wraps_around_beyond_255() { // Documents the underlying u8 wrapping behavior that the truncation // guard in property.rs now prevents. let num_values = 300usize; let indices: Vec = (0..num_values).map(|index| index as u8).collect(); assert_eq!(indices[0], indices[256], "u8 wraps: 0 == 256"); assert_eq!(indices[1], indices[257], "u8 wraps: 1 == 257"); use std::collections::HashMap; let values: Vec = (0..num_values).map(|i| format!("val_{}", i)).collect(); let value_to_idx: HashMap<&str, u8> = values .iter() .enumerate() .map(|(index, value)| (value.as_str(), index as u8)) .collect(); let unique_indices: std::collections::HashSet = value_to_idx.values().cloned().collect(); assert!( unique_indices.len() < num_values, "Without the truncation guard, {} values produce only {} unique u8 indices", num_values, unique_indices.len() ); } }