diff --git a/Cargo.lock b/Cargo.lock index ad1b4fd55..e3b74239c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.5.0" @@ -368,6 +374,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "errno" version = "0.3.9" @@ -378,6 +393,25 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "flatbuffers" +version = "24.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8add37afff2d4ffa83bc748a70b4b1370984f6980768554182424ef71447c35f" +dependencies = [ + "bitflags 1.3.2", + "rustc_version", +] + +[[package]] +name = "flatc-rust" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e61227926ef5b237af48bee74394cc4a5a221ebd10c5147a98e612f207851d" +dependencies = [ + "log", +] + [[package]] name = "flate2" version = "1.0.30" @@ -544,7 +578,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags", + "bitflags 2.5.0", "ignore", "walkdir", ] @@ -768,6 +802,8 @@ dependencies = [ "clap", "colored", "cucumber", + "flatbuffers", + "flatc-rust", "futures", "geo-types", "help", @@ -917,13 +953,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -988,6 +1033,12 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "serde" version = "1.0.203" @@ -1322,6 +1373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ "base64", + "encoding_rs", "flate2", "log", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 413bee52d..4496aa7c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,14 @@ chksum-md5 = "0.0.0" clap = "4.5.4" colored = "2.1.0" cucumber = { version = "0.21.0", features = ["tracing"] } +flatbuffers = "24.3.25" futures = "0.3.30" geo-types = "0.7.13" help = "0.0.0" log = "0.4.21" serde = { version = "1.0.203", features = ["serde_derive"] } serde_json = "1.0.117" -ureq = "2.9.7" +ureq = { version = "2.9.7", features = ["charset"] } xml-builder = "0.5.2" [[test]] @@ -24,3 +25,6 @@ harness = false [profile.bench] debug = true + +[build-dependencies] +flatc-rust = "0.2.0" diff --git a/build.rs b/build.rs new file mode 100644 index 000000000..48fcdd0ca --- /dev/null +++ b/build.rs @@ -0,0 +1,20 @@ +extern crate flatc_rust; // or just `use flatc_rust;` with Rust 2018 edition. + +use std::path::Path; + +fn main() { + println!("cargo:rerun-if-changed=include/engine/api/flatbuffers/"); + flatc_rust::run(flatc_rust::Args { + extra: &["--gen-all"], + inputs: &[ + Path::new("include/engine/api/flatbuffers/position.fbs"), + Path::new("include/engine/api/flatbuffers/waypoint.fbs"), + Path::new("include/engine/api/flatbuffers/route.fbs"), + Path::new("include/engine/api/flatbuffers/table.fbs"), + Path::new("include/engine/api/flatbuffers/fbresult.fbs"), + ], + out_dir: Path::new("target/flatbuffers/"), + ..Default::default() + }) + .expect("flatc"); +} diff --git a/tests/common/f64_utils.rs b/tests/common/f64_utils.rs index 838bc2732..31dbf0959 100644 --- a/tests/common/f64_utils.rs +++ b/tests/common/f64_utils.rs @@ -1,4 +1,4 @@ -pub fn approx_equal(a: f64, b: f64, dp: u8) -> bool { - let p = 10f64.powi(-(dp as i32)); +pub fn approx_equal(a: f32, b: f32, dp: u8) -> bool { + let p = 10f32.powi(-(dp as i32)); (a - b).abs() < p } \ No newline at end of file diff --git a/tests/common/location.rs b/tests/common/location.rs new file mode 100644 index 000000000..52e0cf127 --- /dev/null +++ b/tests/common/location.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +pub struct Location { + // Note: The order is important since we derive Deserialize + pub longitude: f32, + pub latitude: f32, +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1d1a29356..f3741f8d3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,12 +1,33 @@ +#![allow(clippy::derivable_impls, clippy::all)] +extern crate flatbuffers; + pub mod cli_arguments; pub mod dot_writer; pub mod f64_utils; pub mod file_util; pub mod hash_util; pub mod lexicographic_file_walker; +pub mod location; pub mod nearest_response; pub mod osm; pub mod osm_db; pub mod osrm_world; pub mod scenario_id; -pub mod task_starter; \ No newline at end of file +pub mod task_starter; + +// flatbuffer +#[allow(dead_code, unused_imports)] +#[path = "../../target/flatbuffers/position_generated.rs"] +pub mod position_flatbuffers; +#[allow(dead_code, unused_imports)] +#[path = "../../target/flatbuffers/waypoint_generated.rs"] +pub mod waypoint_flatbuffers; +#[allow(dead_code, unused_imports)] +#[path = "../../target/flatbuffers/table_generated.rs"] +pub mod table_flatbuffers; +#[allow(dead_code, unused_imports)] +#[path = "../../target/flatbuffers/route_generated.rs"] +pub mod route_flatbuffers; +#[allow(dead_code, unused_imports)] +#[path = "../../target/flatbuffers/fbresult_generated.rs"] +pub mod fbresult_flatbuffers; diff --git a/tests/common/nearest_response.rs b/tests/common/nearest_response.rs index 204bc05d5..8aa626b61 100644 --- a/tests/common/nearest_response.rs +++ b/tests/common/nearest_response.rs @@ -1,18 +1,19 @@ -use geo_types::{point, Point}; +use crate::common::fbresult_flatbuffers::osrm::engine::api::fbresult::FBResult; +use super::location::Location; use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct Waypoint { pub hint: String, pub nodes: Vec, - pub distance: f64, + pub distance: f32, pub name: String, - location: [f64; 2], + location: Location, } impl Waypoint { - pub fn location(&self) -> Point { - point!(self.location) + pub fn location(&self) -> &Location { + &self.location } } @@ -22,3 +23,65 @@ pub struct NearestResponse { pub waypoints: Vec, pub data_version: Option, } + +impl NearestResponse { + pub fn from_json_reader(reader: impl std::io::Read) -> Self { + let response = match serde_json::from_reader::<_, NearestResponse>(reader) { + Ok(response) => response, + Err(e) => panic!("parsing error {e}"), + }; + response + } + + pub fn from_flatbuffer(mut reader: impl std::io::Read) -> Self { + let mut buffer = Vec::new(); + if let Err(e) = reader.read_to_end(&mut buffer) { + panic!("cannot read from strem: {e}"); + }; + let decoded: Result = + flatbuffers::root::(&buffer); + let decoded: FBResult = match decoded { + Ok(d) => d, + Err(e) => panic!("Error during parsing: {e} {:?}", buffer), + }; + let code = match decoded.code() { + Some(e) => e.message().expect("code exists but is not unwrappable"), + None => "", + }; + let data_version = match decoded.data_version() { + Some(s) => s, + None => "", + }; + + let waypoints = decoded + .waypoints() + .expect("waypoints should be at least an empty list") + .iter() + .map(|wp| { + let hint = wp.hint().expect("hint is missing").to_string(); + let location = wp.location().expect("waypoint must have a location"); + let location = Location { + latitude: location.latitude(), + longitude: location.longitude(), + }; + let nodes = wp.nodes().expect("waypoint mus have nodes"); + let nodes = vec![nodes.first(), nodes.second()]; + let distance = wp.distance(); + + Waypoint { + hint, + nodes, + distance, + name: "".into(), + location, + } + }) + .collect(); + + Self { + code: code.into(), + waypoints, + data_version: Some(data_version.into()), + } + } +} diff --git a/tests/common/osm.rs b/tests/common/osm.rs index f24deffde..d9835f4bc 100644 --- a/tests/common/osm.rs +++ b/tests/common/osm.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use xml_builder::XMLElement; +use super::location::Location; + static OSM_USER: &str = "osrm"; static OSM_TIMESTAMP: &str = "2000-01-01T00:00:00Z"; static OSM_UID: &str = "1"; @@ -9,8 +11,7 @@ static OSM_UID: &str = "1"; #[derive(Clone, Debug, Default)] pub struct OSMNode { pub id: u64, - pub lat: f64, - pub lon: f64, + pub location: Location, pub tags: HashMap, } @@ -34,8 +35,8 @@ impl OSMNode { node.add_attribute("uid", OSM_UID); node.add_attribute("user", OSM_USER); node.add_attribute("timestamp", OSM_TIMESTAMP); - node.add_attribute("lon", &format!("{:?}", self.lon)); - node.add_attribute("lat", &format!("{:?}", self.lat)); + node.add_attribute("lon", &format!("{:?}", self.location.longitude)); + node.add_attribute("lat", &format!("{:?}", self.location.latitude)); if !self.tags.is_empty() { for (key, value) in &self.tags { @@ -81,8 +82,8 @@ impl OSMWay { let mut nd = XMLElement::new("nd"); nd.add_attribute("ref", &node.id.to_string()); if self.add_locations { - nd.add_attribute("lon", &format!("{:?}", node.lon)); - nd.add_attribute("lat", &format!("{:?}", node.lat)); + nd.add_attribute("lon", &format!("{:?}", node.location.longitude)); + nd.add_attribute("lat", &format!("{:?}", node.location.latitude)); } way.add_child(nd).unwrap(); } diff --git a/tests/common/osrm_world.rs b/tests/common/osrm_world.rs index 50581ba95..9ca31299a 100644 --- a/tests/common/osrm_world.rs +++ b/tests/common/osrm_world.rs @@ -1,4 +1,4 @@ -use crate::Point; +use crate::Location; use cucumber::World; use log::debug; use std::{ @@ -18,11 +18,13 @@ pub struct OSRMWorld { pub osm_id: u64, pub profile: String, - pub known_osm_nodes: HashMap, - pub known_locations: HashMap, + pub known_osm_nodes: HashMap, + pub known_locations: HashMap, pub osm_db: OSMDb, pub extraction_parameters: Vec, + + pub request_with_flatbuffers: bool, } impl OSRMWorld { @@ -78,7 +80,7 @@ impl OSRMWorld { self.osm_id } - pub fn add_osm_node(&mut self, name: char, location: Point, id: Option) { + pub fn add_osm_node(&mut self, name: char, location: Location, id: Option) { if self.known_osm_nodes.contains_key(&name) { panic!("duplicate node: {name}"); } @@ -88,8 +90,7 @@ impl OSRMWorld { }; let node = OSMNode { id, - lat: location.y(), - lon: location.x(), + location, tags: HashMap::from([("name".to_string(), name.to_string())]), }; @@ -97,7 +98,7 @@ impl OSRMWorld { self.osm_db.add_node(node); } - pub fn get_location(&self, name: char) -> Point { + pub fn get_location(&self, name: char) -> Location { *match name { // TODO: move lookup to world '0'..='9' => self @@ -112,7 +113,7 @@ impl OSRMWorld { } } - pub fn add_location(&mut self, name: char, location: Point) { + pub fn add_location(&mut self, name: char, location: Location) { if self.known_locations.contains_key(&name) { panic!("duplicate location: {name}") } diff --git a/tests/cucumber.rs b/tests/cucumber.rs index cb20bfc24..3618866df 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -1,4 +1,4 @@ -extern crate clap; +// extern crate clap; mod common; @@ -6,13 +6,13 @@ use cheap_ruler::CheapRuler; use clap::Parser; use common::{ cli_arguments::Args, dot_writer::DotWriter, f64_utils::approx_equal, - hash_util::md5_of_osrm_executables, nearest_response::NearestResponse, osm::OSMWay, - osrm_world::OSRMWorld, task_starter::TaskStarter, + hash_util::md5_of_osrm_executables, location::Location, nearest_response::NearestResponse, + osm::OSMWay, osrm_world::OSRMWorld, task_starter::TaskStarter, }; use core::panic; use cucumber::{gherkin::Step, given, when, World, WriterExt}; use futures::{future, FutureExt}; -use geo_types::{point, Point}; +use geo_types::point; use log::debug; use std::{collections::HashMap, fs::File, io::Write, time::Duration}; use ureq::Agent; @@ -20,13 +20,17 @@ use ureq::Agent; const DEFAULT_ORIGIN: [f64; 2] = [1., 1.]; // TODO: move to world? const DEFAULT_GRID_SIZE: f64 = 100.; // TODO: move to world? -fn offset_origin_by(dx: f64, dy: f64) -> geo_types::Point { +fn offset_origin_by(dx: f64, dy: f64) -> Location { let ruler = CheapRuler::new(DEFAULT_ORIGIN[1], cheap_ruler::DistanceUnit::Meters); - ruler.offset( + let loc = ruler.offset( &point!(DEFAULT_ORIGIN), dx * DEFAULT_GRID_SIZE, dy * DEFAULT_GRID_SIZE, - ) //TODO: needs to be world's gridSize, not the local one + ); //TODO: needs to be world's gridSize, not the local one + Location { + latitude: loc.y() as f32, + longitude: loc.x() as f32, + } } #[given(expr = "the profile \"{word}\"")] @@ -60,13 +64,16 @@ fn set_node_locations(world: &mut OSRMWorld, step: &Step) { ); }); - table.rows.iter().skip(1).for_each(|row|{ + table.rows.iter().skip(1).for_each(|row| { assert_eq!(3, row.len()); assert_eq!(row[0].len(), 1, "node name not in [0..9][a..z]"); let name = &row[0].chars().next().expect("node name cannot be empty"); // the error is unreachable let lon = &row[header_lookup["lon"]]; let lat = &row[header_lookup["lat"]]; - let location = point!(x: lon.parse::().expect("lon {lon} needs to be a f64"), y: lat.parse::().expect("lat {lat} needs to be a f64")); + let location = Location { + latitude: lat.parse::().expect("lat {lat} needs to be a f64"), + longitude: lon.parse::().expect("lon {lon} needs to be a f64"), + }; match name { '0'...'9' => world.add_location(*name, location), 'a'...'z' => world.add_osm_node(*name, location, None), @@ -115,7 +122,7 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) { panic!("empty way table provided") } // store a reference to the headers for convenient lookup - let headers = table.rows.first().unwrap(); + let headers = table.rows.first().expect("table has a first row"); // iterate over the following rows and build ways one by one table.rows.iter().skip(1).for_each(|row| { @@ -156,8 +163,10 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) { // debug!("{}", world.osm_db.to_xml()) } -#[when("I request nearest I should get")] -fn request_nearest(world: &mut OSRMWorld, step: &Step) { +// #[when("I request nearest I should get")] +#[when(regex = r"^I request nearest( with flatbuffers|) I should get$")] +fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) { + let request_with_flatbuffers = state == " with flatbuffers"; // if .osm file does not exist // write osm file @@ -207,8 +216,8 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step) { let data_path = cache_path.join(world.scenario_id.to_owned() + ".osrm"); // TODO: this should not require a temporary and behave like the API of std::process - let mut task = TaskStarter::new(world.routed_path().to_str().unwrap()); - task.arg(data_path.to_str().unwrap()); + let mut task = TaskStarter::new(world.routed_path().to_str().expect("task can be started")); + task.arg(data_path.to_str().expect("data path unwrappable")); task.spawn_wait_till_ready("running and waiting for requests"); assert!(task.is_ready()); @@ -239,33 +248,44 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step) { // debug!("{query_location:?} => {expected_location:?}"); // run queries - let url = format!( - "http://localhost:5000/nearest/v1/{}/{},{}", - world.profile, - query_location.x(), - query_location.y() + let mut url = format!( + "http://localhost:5000/nearest/v1/{}/{:?},{:?}", + world.profile, query_location.longitude, query_location.latitude ); + if request_with_flatbuffers { + url += ".flatbuffers"; + } let call = agent.get(&url).call(); let body = match call { - Ok(response) => response.into_string().expect("response not parseable"), + Ok(response) => response.into_reader(), Err(e) => panic!("http error: {e}"), }; - // debug!("body: {body}"); - let response: NearestResponse = match serde_json::from_str(&body) { - Ok(response) => response, - Err(e) => panic!("parsing error {e}"), + let response = match request_with_flatbuffers { + true => NearestResponse::from_flatbuffer(body), + false => NearestResponse::from_json_reader(body), }; if test_case.contains_key("out") { // check that result node is (approximately) equivalent let result_location = response.waypoints[0].location(); - assert!(approx_equal(result_location.x(), expected_location.x(), 5)); - assert!(approx_equal(result_location.y(), expected_location.y(), 5)); + assert!(approx_equal( + result_location.longitude, + expected_location.longitude, + 5 + )); + assert!(approx_equal( + result_location.latitude, + expected_location.latitude, + 5 + )); } if test_case.contains_key("data_version") { - assert_eq!(test_case.get("data_version"), response.data_version.as_ref()); + assert_eq!( + test_case.get("data_version"), + response.data_version.as_ref() + ); } } }