use crate::cli::index::command_handler_index;
use crate::cli::optimize::command_handler_optimize;
use crate::database::admin_levels::{admin_levels as admin_levels_row, batch_upsert};
use crate::database::{open_write, osm_data_path};
use crate::index::admin_levels_hierarchy_tantivy as tantivy;
use crate::presets::{BRAZIL, DEFAULT};
use crate::query;
use geo::{Coord, Geometry, LineString};
use rusqlite::Connection;
use std::path::{Path, PathBuf};
fn unique_tag() -> String {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!("{}_{seq}", std::process::id())
}
struct workspace {
base: PathBuf,
sqlite_path: String,
index_path: String,
}
impl workspace {
fn new(tag: &str) -> Self {
let base = std::env::temp_dir().join(format!("geolite_build_test_{tag}_{}", unique_tag()));
std::fs::create_dir_all(&base).expect("failed to create workspace base dir");
workspace {
sqlite_path: base.join("db.sqlite3").to_string_lossy().into_owned(),
index_path: base.join("index.tantivy").to_string_lossy().into_owned(),
base,
}
}
}
impl Drop for workspace {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.base);
}
}
fn make_street(name: &str, way_id: u64, lon_offset: f64) -> admin_levels_row {
admin_levels_row {
relation_id: None,
way_id: Some(way_id),
admin_level: 12,
wkb: Geometry::LineString(LineString(vec![
Coord { x: -46.3198 + lon_offset, y: -23.9724 },
Coord { x: -46.3197 + lon_offset, y: -23.9724 },
]))
.into(),
name: name.to_string(),
country_iso_code: None,
post_code: None,
}
}
fn synthetic_index(
work: &workspace,
streets: &[&str],
abbreviations: &[(&str, &str)],
) -> (Connection, tantivy::tantivy_index) {
let conn = open_write(":memory:");
let rows: Vec<admin_levels_row> = streets
.iter()
.enumerate()
.map(|(i, name)| make_street(name, (i + 1) as u64, i as f64 * 0.0005))
.collect();
batch_upsert(&conn, &rows);
crate::index::coordinates::run(&conn, |_| {});
crate::index::hierarchy::run(&conn, |_| {});
let index = tantivy::build(
&conn,
Path::new(&work.index_path),
DEFAULT.index_user_friendly_name.boosts,
abbreviations,
);
(conn, index)
}
fn query_text(conn: &Connection, index: &tantivy::tantivy_index, q: &str) -> query::query_output {
query::run(conn, Some(index), q, None, None, None, None, false)
}
fn matched_street_name(m: &query::query_match) -> Option<&str> {
m.admin_levels.iter().max_by_key(|a| a.level).map(|a| a.name.as_str())
}
fn top_street(out: &query::query_output) -> Option<&str> {
out.matches.first().and_then(matched_street_name)
}
#[test]
fn _00_brazil_preset_expands_abbreviations_in_tantivy_index() {
let work = workspace::new("brazil_abbrev");
let (conn, index) = synthetic_index(
&work,
&["Rua Braz Cubas", "Avenida Ana Costa", "Praça dos Andradas"],
BRAZIL.index_user_friendly_name.abbreviations,
);
for (abbreviated, full) in [
("R. Braz Cubas", "Rua Braz Cubas"),
("Av. Ana Costa", "Avenida Ana Costa"),
("Pç. dos Andradas", "Praça dos Andradas"),
] {
assert_eq!(
top_street(&query_text(&conn, &index, abbreviated)),
Some(full),
"abbreviated query '{abbreviated}' should resolve to '{full}'"
);
}
}
#[test]
fn _01_default_preset_does_not_expand_abbreviations() {
let streets = ["Rua São Francisco", "Avenida São Francisco"];
let bw = workspace::new("brazil_disambig");
let (bconn, bindex) = synthetic_index(&bw, &streets, BRAZIL.index_user_friendly_name.abbreviations);
assert_eq!(
top_street(&query_text(&bconn, &bindex, "R. São Francisco")),
Some("Rua São Francisco"),
"brazil preset should disambiguate 'R. São Francisco' to the rua"
);
assert_eq!(
top_street(&query_text(&bconn, &bindex, "Av. São Francisco")),
Some("Avenida São Francisco"),
"brazil preset should disambiguate 'Av. São Francisco' to the avenida"
);
let dw = workspace::new("default_disambig");
let (dconn, dindex) = synthetic_index(&dw, &streets, DEFAULT.index_user_friendly_name.abbreviations);
let default_rua = top_street(&query_text(&dconn, &dindex, "R. São Francisco")).map(str::to_string);
let default_av = top_street(&query_text(&dconn, &dindex, "Av. São Francisco")).map(str::to_string);
assert!(
!(default_rua.as_deref() == Some("Rua São Francisco")
&& default_av.as_deref() == Some("Avenida São Francisco")),
"default preset has no abbreviation table and must not disambiguate both directions \
(rua={default_rua:?}, av={default_av:?})"
);
}
#[test]
fn _02_build_creates_then_deletes_osm_data_sibling() {
let work = workspace::new("osm_data_lifecycle");
{
let conn = open_write(&work.sqlite_path);
batch_upsert(&conn, &[make_street("way_1", 1, 0.0), make_street("way_2", 2, 0.0005)]);
}
let sibling = osm_data_path(&work.sqlite_path);
assert!(
Path::new(&sibling).exists(),
"osm_data sibling must exist after the database is opened for writing"
);
command_handler_index(&work.sqlite_path, &work.index_path, None, &DEFAULT.index_user_friendly_name);
command_handler_optimize(&work.base.to_string_lossy(), &work.sqlite_path, &work.index_path, None);
assert!(
!Path::new(&sibling).exists(),
"osm_data sibling must be deleted after optimize"
);
}