From 9457f36f49eea14cf6a7dff8a97f4f3b1b846632 Mon Sep 17 00:00:00 2001 From: Dennis Date: Thu, 27 Jun 2024 10:09:33 +0200 Subject: [PATCH] Implement more features --- Cargo.lock | 137 ++++---- Cargo.toml | 7 +- features/car/bridge.feature | 4 +- tests/common/f64_utils.rs | 13 + tests/common/osm_db.rs | 2 +- tests/common/osrm_world.rs | 41 ++- tests/common/route_response.rs | 32 +- tests/cucumber.rs | 571 +++++++++++++++++++++++++++------ 8 files changed, 597 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2881d08cf..fe64bd21a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,9 +60,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" dependencies = [ "jobserver", "libc", @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -259,9 +259,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -272,21 +272,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "colorchoice" @@ -378,9 +378,9 @@ dependencies = [ [[package]] name = "cucumber" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2940c675f8b0dd864bfedb4283d5fa07b8799eed59a4f7b09fb1257b18c88a1e" +checksum = "6cd12917efc3a8b069a4975ef3cb2f2d835d42d04b3814d90838488f9dd9bf69" dependencies = [ "anyhow", "clap", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "cucumber-codegen" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5c9c7e0af8103f81ab300a21be3df1d57a003a151cf0bf41fdd343f85d14552" +checksum = "9e19cd9e8e7cfd79fbf844eb6a7334117973c01f6bad35571262b00891e60f1c" dependencies = [ "cucumber-expressions", "inflections", @@ -420,7 +420,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.66", + "syn", "synthez", ] @@ -440,13 +440,13 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -597,7 +597,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -673,7 +673,7 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.66", + "syn", "textwrap", "thiserror", "typed-builder", @@ -689,7 +689,7 @@ dependencies = [ "bstr", "log", "regex-automata", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -807,9 +807,9 @@ checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -849,7 +849,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.66", + "syn", ] [[package]] @@ -890,9 +890,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -902,9 +902,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -1058,7 +1058,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1081,9 +1081,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -1105,25 +1105,25 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -1134,9 +1134,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "ring" @@ -1230,7 +1230,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1256,7 +1256,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1333,7 +1333,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1356,20 +1356,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" [[package]] name = "syn" @@ -1388,7 +1377,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d2c2202510a1e186e63e596d9318c91a8cbe85cd1a56a7be0c333e5f59ec8d" dependencies = [ - "syn 2.0.66", + "syn", "synthez-codegen", "synthez-core", ] @@ -1399,7 +1388,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f724aa6d44b7162f3158a57bccd871a77b39a4aef737e01bcdff41f4772c7746" dependencies = [ - "syn 2.0.66", + "syn", "synthez-core", ] @@ -1412,7 +1401,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.66", + "syn", ] [[package]] @@ -1453,7 +1442,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1534,7 +1523,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1589,7 +1578,7 @@ checksum = "29a3151c41d0b13e3d011f98adc24434560ef06673a155a6c7f66b9879eecce2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1627,9 +1616,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "untrusted" @@ -1657,9 +1646,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -1668,9 +1657,9 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" @@ -1702,9 +1691,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -1951,9 +1940,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.11+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index eb9356c51..6eb484c7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" [dependencies] cheap-ruler = "0.4.0" chksum-md5 = "0.0.0" -clap = "4.5.4" +clap = "4.5.7" colored = "2.1.0" -cucumber = { version = "0.21.0", features = ["tracing"] } +cucumber = { version = "0.21.1", features = ["tracing"] } flatbuffers = "24.3.25" futures = "0.3.30" geo-types = "0.7.13" @@ -26,6 +26,9 @@ harness = false [profile.bench] debug = true +[profile.release] +debug = true + [build-dependencies] flatc-rust = "0.2.0" serde = { version = "1.0.203", features = ["serde_derive"] } diff --git a/features/car/bridge.feature b/features/car/bridge.feature index 7c43f24cc..efb18c0d4 100644 --- a/features/car/bridge.feature +++ b/features/car/bridge.feature @@ -60,5 +60,5 @@ Feature: Car - Handle driving When I route I should get | from | to | route | modes | speed | turns | | a | g | abc,cde,efg,efg | driving,driving,driving,driving | 7 km/h | depart,new name right,new name left,arrive | - | c | e | cde,cde | driving,driving | 2 km/h | depart,arrive | - | e | c | cde,cde | driving,driving | 2 km/h | depart,arrive | + | c | e | cde,cde | driving,driving | 2.4 km/h | depart,arrive | + | e | c | cde,cde | driving,driving | 2.4 km/h | depart,arrive | diff --git a/tests/common/f64_utils.rs b/tests/common/f64_utils.rs index aafc74c84..2bc9ddbc6 100644 --- a/tests/common/f64_utils.rs +++ b/tests/common/f64_utils.rs @@ -2,3 +2,16 @@ pub fn approx_equal(a: f32, b: f32, dp: u8) -> bool { let p = 10f32.powi(-(dp as i32)); (a - b).abs() < p } + +pub fn aprox_equal_within_percentage_range(actual: f64, expectation: f64, percentage: u8) -> bool { + assert!(percentage <= 100); + let factor = 0.01 * percentage as f64; + actual >= expectation - (factor * expectation) && actual <= expectation + (factor * expectation) +} + +pub fn approx_equal_within_offset_range(actual: f64, expectation: f64, offset: f64) -> bool { + assert!(offset >= 0., "offset must be positive"); + actual >= expectation - offset && actual <= expectation + offset +} + +// TODO: test coverage diff --git a/tests/common/osm_db.rs b/tests/common/osm_db.rs index c33bcae0c..aa7af53b9 100644 --- a/tests/common/osm_db.rs +++ b/tests/common/osm_db.rs @@ -125,7 +125,7 @@ mod tests { let actual = osm_db.to_xml(); let expected = "\n\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n"; - println!("{actual}"); + // println!("{actual}"); assert_eq!(actual, expected); } } diff --git a/tests/common/osrm_world.rs b/tests/common/osrm_world.rs index d623f771d..0017c3ec6 100644 --- a/tests/common/osrm_world.rs +++ b/tests/common/osrm_world.rs @@ -18,6 +18,7 @@ const DEFAULT_ORIGIN: Location = Location { latitude: 1.0f32, }; const DEFAULT_GRID_SIZE: f32 = 100.; +const WAY_SPACING: f32 = 100.; #[derive(Debug, World)] pub struct OSRMWorld { @@ -35,9 +36,12 @@ pub struct OSRMWorld { pub extraction_parameters: Vec, pub request_with_flatbuffers: bool, + pub bearings: Option, pub grid_size: f32, pub origin: Location, + pub way_spacing: f32, + task: LocalTask, agent: ureq::Agent, } @@ -56,8 +60,10 @@ impl Default for OSRMWorld { osm_db: Default::default(), extraction_parameters: Default::default(), request_with_flatbuffers: Default::default(), + bearings: None, grid_size: DEFAULT_GRID_SIZE, origin: DEFAULT_ORIGIN, + way_spacing: WAY_SPACING, task: LocalTask::default(), agent: ureq::AgentBuilder::new() .timeout_read(Duration::from_secs(5)) @@ -212,7 +218,7 @@ impl OSRMWorld { pub fn nearest( &mut self, query_location: &Location, - request_with_flatbuffers: bool, + // request_with_flatbuffers: bool, ) -> NearestResponse { self.start_routed(); @@ -220,7 +226,7 @@ impl OSRMWorld { "http://localhost:5000/nearest/v1/{}/{:?},{:?}", self.profile, query_location.longitude, query_location.latitude ); - if request_with_flatbuffers { + if self.request_with_flatbuffers { url += ".flatbuffers"; } let call = self.agent.get(&url).call(); @@ -230,42 +236,43 @@ impl OSRMWorld { Err(e) => panic!("http error: {e}"), }; - let response = match request_with_flatbuffers { + let response = match self.request_with_flatbuffers { true => NearestResponse::from_flatbuffer(body), false => NearestResponse::from_json_reader(body), }; response } - pub fn route( - &mut self, - from_location: &Location, - to_location: &Location, - request_with_flatbuffers: bool, - ) -> RouteResponse { + pub fn route(&mut self, waypoints: &[Location]) -> RouteResponse { self.start_routed(); + let waypoint_string = waypoints + .iter() + .map(|location| format!("{:?},{:?}", location.longitude, location.latitude)) + .collect::>() + .join(";"); + let mut url = format!( - "http://localhost:5000/route/v1/{}/{:?},{:?};{:?},{:?}?steps=true&alternatives=false", + "http://localhost:5000/route/v1/{}/{waypoint_string}?steps=true&alternatives=false", self.profile, - from_location.longitude, - from_location.latitude, - to_location.longitude, - to_location.latitude, ); - if request_with_flatbuffers { + if self.request_with_flatbuffers { url += ".flatbuffers"; } + if let Some(bearings) = &self.bearings { + url += "&bearings="; + url += bearings; + } // println!("url: {url}"); let call = self.agent.get(&url).call(); let body = match call { Ok(response) => response.into_reader(), - Err(e) => panic!("http error: {e}"), + Err(_e) => return RouteResponse::default(), }; let text = std::io::read_to_string(body).unwrap(); - let response = match request_with_flatbuffers { + let response = match self.request_with_flatbuffers { true => unimplemented!("RouteResponse::from_flatbuffer(body)"), false => RouteResponse::from_string(&text), }; diff --git a/tests/common/route_response.rs b/tests/common/route_response.rs index ebb8f2a65..920d5be0d 100644 --- a/tests/common/route_response.rs +++ b/tests/common/route_response.rs @@ -1,12 +1,26 @@ use serde::Deserialize; -use super::nearest_response::Waypoint; +use super::{location::Location, nearest_response::Waypoint}; + +#[derive(Deserialize, Default, Debug)] +pub struct Maneuver { + pub bearing_after: f64, + pub bearing_before: f64, + pub location: Location, + pub modifier: Option, // TODO: should be an enum + pub r#type: String, // TODO: should be an enum +} #[derive(Deserialize, Default, Debug)] pub struct Step { + pub geometry: String, + pub mode: String, + pub maneuver: Maneuver, pub name: String, pub pronunciation: Option, pub r#ref: Option, + pub duration: f64, + pub distance: f64, } // #[derive(Deserialize, Debug)] @@ -34,7 +48,7 @@ pub struct Route { pub distance: f64, } -#[derive(Deserialize, Debug)] +#[derive(Debug, Default, Deserialize)] pub struct RouteResponse { pub code: String, pub routes: Vec, @@ -43,13 +57,13 @@ pub struct RouteResponse { } impl RouteResponse { - // pub fn from_json_reader(reader: impl std::io::Read) -> Self { - // let response = match serde_json::from_reader::<_, Self>(reader) { - // Ok(response) => response, - // Err(e) => panic!("parsing error {e}"), - // }; - // response - // } + pub fn from_json_reader(reader: impl std::io::Read) -> Self { + let response = match serde_json::from_reader::<_, Self>(reader) { + Ok(response) => response, + Err(e) => panic!("parsing error {e}"), + }; + response + } pub fn from_string(input: &str) -> Self { // println!("{input}"); diff --git a/tests/cucumber.rs b/tests/cucumber.rs index 8983d7c41..359ae5327 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -3,18 +3,28 @@ mod common; 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, location::Location, osm::OSMWay, osrm_world::OSRMWorld, + cli_arguments::Args, + dot_writer::DotWriter, + f64_utils::{ + approx_equal, approx_equal_within_offset_range, aprox_equal_within_percentage_range, + }, + hash_util::md5_of_osrm_executables, + location::Location, + osm::OSMWay, + osrm_world::OSRMWorld, }; use core::panic; use cucumber::{ gherkin::{Step, Table}, - given, when, World, WriterExt, + given, then, when, World, WriterExt, }; use futures::{future, FutureExt}; use geo_types::Point; use log::debug; -use std::collections::HashMap; +use std::{ + collections::{HashMap, HashSet}, + iter::zip, +}; fn offset_origin_by(dx: f32, dy: f32, origin: Location, grid_size: f32) -> Location { let ruler = CheapRuler::new(origin.latitude, cheap_ruler::DistanceUnit::Meters); @@ -42,10 +52,10 @@ fn set_profile(world: &mut OSRMWorld, profile: String) { fn set_node_locations(world: &mut OSRMWorld, step: &Step) { let table = step.table().expect("cannot get table"); let header = table.rows.first().expect("node locations table empty"); - assert_eq!(header.len(), 3, "header needs to define three columns"); - assert_eq!( - header[0], "node", - "first column needs to be 'node' indicating the one-letter name" + assert!(header.len() >= 3, "header needs to define three columns"); + assert!( + header.contains(&"node".to_string()), + "a column needs to be 'node' indicating the one-letter name" ); // the following lookup allows to define lat lon columns in any order let header_lookup: HashMap<&str, usize> = header @@ -61,7 +71,10 @@ fn set_node_locations(world: &mut OSRMWorld, step: &Step) { }); table.rows.iter().skip(1).for_each(|row| { - assert_eq!(3, row.len()); + assert!( + row.len() >= 3, + "nod locations must at least specify three tables: node, lat, and lon" + ); 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"]]; @@ -70,9 +83,18 @@ fn set_node_locations(world: &mut OSRMWorld, step: &Step) { latitude: lat.parse::().expect("lat {lat} needs to be a f64"), longitude: lon.parse::().expect("lon {lon} needs to be a f64"), }; + let id = match header_lookup.get("id") { + Some(index) => { + let id = row[*index] + .parse::() + .expect("id of a node must be u64 number"); + Some(id) + } + _ => None, + }; match name { '0'...'9' => world.add_location(*name, location), - 'a'...'z' => world.add_osm_node(*name, location, None), + 'a'...'z' => world.add_osm_node(*name, location, id), _ => unreachable!("node name not in [0..9][a..z]"), } }); @@ -100,7 +122,8 @@ fn set_node_map(world: &mut OSRMWorld, step: &Step) { match name { '0'...'9' => world.add_location(name, location), 'a'...'z' => world.add_osm_node(name, location, None), - _ => unreachable!("node name not in [0..9][a..z]: {docstring}"), + _ => {} // TODO: unreachable!("node name not in [0..9][a..z]: {docstring}"), + // tests contain random characters. } }); }); @@ -166,7 +189,7 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) { } } -fn parse_table_from_steps(table: &Option<&Table>) -> Vec> { +fn parse_table_from_steps(table: &Option<&Table>) -> (Vec, Vec>) { // parse query data let table = table.expect("no query table specified"); // the following lookup allows to define lat lon columns in any order @@ -187,19 +210,18 @@ fn parse_table_from_steps(table: &Option<&Table>) -> Vec row_map }) .collect(); - test_cases - // TODO: also return the header + (header.clone(), test_cases) } #[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"; + world.request_with_flatbuffers = state == " with flatbuffers"; world.write_osm_file(); world.extract_osm_file(); // parse query data - let test_cases = parse_table_from_steps(&step.table.as_ref()); + let (_, test_cases) = parse_table_from_steps(&step.table.as_ref()); // run test cases for test_case in &test_cases { @@ -212,7 +234,7 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) { .expect("node name is one char long"), ); - let response = world.nearest(&query_location, request_with_flatbuffers); + let response = world.nearest(&query_location); let expected_location = &world.get_location( test_case @@ -246,117 +268,456 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) { } } -#[when(regex = r"^I route( with flatbuffers|) I should get$")] -fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { - let request_with_flatbuffers = state == " with flatbuffers"; +#[then(expr = "routability should be")] +fn routability(world: &mut OSRMWorld, step: &Step) { world.write_osm_file(); world.extract_osm_file(); // TODO: preprocess - let test_cases = parse_table_from_steps(&step.table.as_ref()); - for test_case in &test_cases { - let from_location = world.get_location( - test_case - .get("from") - .expect("node name is one char long") - .chars() - .next() - .expect("node name is one char long"), + let (header, test_cases) = parse_table_from_steps(&step.table.as_ref()); + // TODO: rename forw/backw to forw/backw_speed + let supported_headers = HashSet::<_>::from([ + "forw", + "backw", + "bothw", + "forw_rate", + "backw_rate", + "bothw_rate", + ]); + if 0 == header + .iter() + .filter(|title| supported_headers.contains(title.as_str())) + .count() + { + panic!( + r#"*** routability table must contain either "forw", "backw", "bothw", "forw_rate" or "backw_mode" column"# ); - let to_location = world.get_location( - test_case - .get("to") - .expect("node name is one char long") - .chars() - .next() - .expect("node name is one char long"), - ); - - let response = world.route(&from_location, &to_location, request_with_flatbuffers); - - if test_case.contains_key("route") { - // NOTE: the following code ports logic from JavaScript that checks only properties of the first route - let route = response - .routes - .first() - .expect("no route returned") - .legs - .first() - .expect("legs required") - .steps - .iter() - .map(|step| step.name.clone()) - .collect::>() - .join(","); - - assert_eq!(*test_case.get("route").expect("msg"), route); - } - - if test_case.contains_key("pronunciations") { - let pronunciations = response - .routes - .first() - .expect("no route returned") - .legs - .first() - .expect("legs required") - .steps - .iter() - .map(|step| match &step.pronunciation { - Some(p) => p.clone(), - None => "".to_string(), - }) - .collect::>() - .join(","); - assert_eq!( - *test_case.get("pronunciations").expect("msg"), - pronunciations - ); - } - - if test_case.contains_key("ref") { - let refs = response - .routes - .first() - .expect("no route returned") - .legs - .first() - .expect("legs required") - .steps - .iter() - .map(|step| match &step.r#ref { - Some(p) => p.clone(), - None => "".to_string(), - }) - .collect::>() - .join(","); - assert_eq!(*test_case.get("ref").expect("msg"), refs); - } - // TODO: more checks need to be implemented - - // TODO: check for unchecked test columns } - // unimplemented!("route"); + test_cases + .iter() + .enumerate() + .for_each(|(index, test_case)| { + let source = offset_origin_by( + 1. + world.way_spacing * index as f32, + 0., + world.origin, + world.grid_size, + ); + let target = offset_origin_by( + 3. + world.way_spacing * index as f32, + 0., + world.origin, + world.grid_size, + ); + test_case + .iter() + .filter(|(title, _)| supported_headers.contains(title.as_str())) + .for_each(|(title, expectation)| { + let forward = title.starts_with("forw"); + // println!("{direction}: >{expectation}<"); + let response = match forward { + true => world.route(&vec![source, target]), + false => world.route(&vec![target, source]), + }; + if expectation.is_empty() { + // if !response.routes.is_empty() { + // println!("> {title} {expectation}"); + // println!("{response:?}"); + // } + + assert!( + response.routes.is_empty() + || response.routes.first().unwrap().distance == 0., + "no route expected when result column {title} is unset" + ); + } else if expectation.contains("km/h") { + assert!( + !response.routes.is_empty(), + "route expected when result column is set" + ); + + let (expected_speed, offset) = + extract_number_and_offset("km/h", expectation); + let route = response.routes.first().unwrap(); + let actual_speed = route.distance / route.duration * 3.6; + assert!( + aprox_equal_within_percentage_range( + actual_speed, + expected_speed, + offset + ), + "{actual_speed} and {expected_speed} differ by more than {offset}" + ); + } else if title.ends_with("_rate") { + assert!(!response.routes.is_empty()); + let expected_rate = expectation + .parse::() + .expect("rate needs to be a number"); + let route = response.routes.first().unwrap(); + let actual_rate = route.distance / route.weight; + assert!( + aprox_equal_within_percentage_range(actual_rate, expected_rate, 1), + "{actual_rate} and {expected_rate} differ by more than 1%" + ); + } else { + unimplemented!("{title} = {expectation}"); + } + }); + }); + // unimplemented!("{test_cases:#?}"); +} + +fn extract_number_and_offset(unit: &str, expectation: &str) -> (f64, u8) { + let tokens: Vec<_> = expectation + .split(unit) + .map(|token| token.trim()) + .filter(|token| !token.is_empty()) + .collect(); + // println!("{tokens:?}"); + let number = tokens[0] + .parse::() + .expect("{expectation} needs to define a speed"); + let offset = match tokens.len() { + 1 => 5u8, // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise + 2 => tokens[1] + .replace("+-", "") + .trim() + .parse() + .expect(&format!("{} needs to specify a number", tokens[1])), + _ => unreachable!("expectations can't be parsed"), + }; + (number, offset) +} + +fn extract_number_vector_and_offset(unit: &str, expectation: &str) -> (Vec, u8) { + let expectation = expectation.replace(",", ""); + let tokens: Vec<_> = expectation + .split(unit) + .map(|token| token.trim()) + .filter(|token| !token.is_empty()) + .collect(); + let numbers = tokens + .iter() + .filter(|token| !token.contains("+-")) + .map(|token| { + token + .parse::() + .expect("input needs to specify a number followed by unit") + }) + .collect(); + + // panic!("{tokens:?}"); + let offset = match tokens.len() { + 1 => 5u8, // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise + _ => tokens + .last() + .expect("offset needs to be specified") + .replace("+-", "") + .trim() + .parse() + .expect(&format!("{} needs to specify a number", tokens[1])), + // _ => unreachable!("expectations can't be parsed"), + }; + (numbers, offset) +} + +enum WaypointsOrLocation { + Waypoints, + Locations, + // Undefined, +} + +pub fn get_location_specification(test_case: &HashMap) -> WaypointsOrLocation { + assert!( + test_case.contains_key("from") + && test_case.contains_key("to") + && !test_case.contains_key("waypoints") + || !test_case.contains_key("from") + && !test_case.contains_key("to") + && test_case.contains_key("waypoints"), + "waypoints need to be specified by either from/to columns or a waypoint column, but not both" + ); + + if test_case.contains_key("from") + && test_case.contains_key("to") + && !test_case.contains_key("waypoints") + { + return WaypointsOrLocation::Locations; + } + + if !test_case.contains_key("from") + && !test_case.contains_key("to") + && test_case.contains_key("waypoints") + { + return WaypointsOrLocation::Waypoints; + } + unreachable!("waypoints need to be specified by either from/to columns or a waypoint column, but not both"); + // WaypointsOrLocation::Undefined +} + +#[when(regex = r"^I route( with flatbuffers|) I should get$")] +fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { + world.request_with_flatbuffers = state == " with flatbuffers"; + world.write_osm_file(); + world.extract_osm_file(); + // TODO: preprocess + + let (_, test_cases) = parse_table_from_steps(&step.table.as_ref()); + for test_case in &test_cases { + let waypoints = match get_location_specification(&test_case) { + WaypointsOrLocation::Waypoints => { + let locations: Vec = test_case + .get("waypoints") + .expect("locations specified as waypoints") + .split(",") + .into_iter() + .map(|name| { + assert!(name.len() == 1, "node names need to be of length one"); + world.get_location(name.chars().next().unwrap()) + }) + .collect(); + locations + } + WaypointsOrLocation::Locations => { + let from_location = world.get_location( + test_case + .get("from") + .expect("test case doesn't have a 'from' column") + .chars() + .next() + .expect("from node name is one char long"), + ); + let to_location = world.get_location( + test_case + .get("to") + .expect("test case doesn't have a 'to' column") + .chars() + .next() + .expect("to node name is one char long"), + ); + vec![from_location, to_location] + } + }; + + if let Some(bearing) = test_case.get("bearings").cloned() { + world.bearings = Some(bearing.replace(" ", ";")); + } + + let response = world.route(&waypoints); + + test_case + .iter() + .map(|(column_title, expectation)| (column_title.as_str(), expectation.as_str())) + .for_each(|(case, expectation)| match case { + "from" | "to" | "bearings"=> {}, // ignore input columns + "route" => { + let route = if expectation.is_empty() { + assert!(response.routes.is_empty()); + String::new() + } else { + response + .routes + .first() + .expect("no route returned") + .legs + .iter() + .map(|leg| { + leg.steps + .iter() + .map(|step| step.name.clone()) + .collect::>() + .join(",") + }).collect::>() + .join(",") + + }; + + assert_eq!(expectation, route); + }, + "pronunciations" => { + let pronunciations = response + .routes + .first() + .expect("no route returned") + .legs + .first() + .expect("legs required") + .steps + .iter() + .map(|step| match &step.pronunciation { + Some(p) => p.clone(), + None => "".to_string(), + }) + .collect::>() + .join(","); + assert_eq!(expectation, pronunciations); + }, + "ref" => { + let refs = response + .routes + .first() + .expect("no route returned") + .legs + .first() + .expect("legs required") + .steps + .iter() + .map(|step| match &step.r#ref { + Some(p) => p.clone(), + None => "".to_string(), + }) + .collect::>() + .join(","); + assert_eq!(expectation, refs); + }, + "speed" => { + let route = response.routes.first().expect("no route returned"); + let actual_speed = route.distance / route.duration * 3.6; + let (expected_speed, offset) = extract_number_and_offset("km/h", expectation); + // println!("{actual_speed} == {expected_speed} +- {offset}"); + assert!( + aprox_equal_within_percentage_range(actual_speed, expected_speed, offset), + "actual time {actual_speed} not equal to expected value {expected_speed}" + ); + }, + "modes" => { + let route = response.routes.first().expect("no route returned"); + let actual_modes = route + .legs + .iter() + .map(|leg| { + leg.steps + .iter() + .map(|step| step.mode.clone()) + .collect::>() + .join(",") + }) + .collect::>() + .join(","); + assert_eq!(actual_modes, expectation); + }, + "turns" => { + let route = response.routes.first().expect("no route returned"); + let actual_turns = route + .legs + .iter() + .map(|leg| { + leg.steps + .iter() + .map(|step| { + let prefix = step.maneuver.r#type.clone(); + let suffix = match &step.maneuver.modifier { + Some(modifier) => " ".to_string() + &modifier, + None => "".into(), + }; + prefix + &suffix + }) + .collect::>() + .join(",") + }) + .collect::>() + .join(","); + assert_eq!(actual_turns, expectation); + }, + "time" => { + let actual_time = response.routes.first().expect("no route returned").duration; + let (expected_time, offset) = extract_number_and_offset("s", expectation); + // println!("{actual_time} == {expected_time} +- {offset}"); + assert!( + approx_equal_within_offset_range(actual_time, expected_time, offset as f64), + "actual time {actual_time} not equal to expected value {expected_time}" + ); + }, + "times" => { + // TODO: go over steps + + let actual_times : Vec= response.routes.first().expect("no route returned").legs.iter().map(|leg| { + leg.steps.iter().map(|step| step.duration).collect::>() + }).flatten().collect(); + let (expected_times, offset) = extract_number_vector_and_offset("s", expectation); + println!("{actual_times:?} == {expected_times:?} +- {offset}"); + assert_eq!(actual_times.len(), expected_times.len(), "times mismatch: {actual_times:?} != {expected_times:?} +- {offset}"); + + zip(actual_times, expected_times).for_each(|(actual_time, expected_time)| { + assert!(approx_equal_within_offset_range(actual_time, expected_time, offset as f64), + "actual time {actual_time} not equal to expected value {expected_time}"); + }); + }, + "distances" => { + + println!("{:?}",response.routes.first().expect("no route returned")); + // TODO: go over steps + let actual_distances : Vec = response.routes.first().expect("no route returned").legs.iter().map(|leg| leg.distance).collect(); + let (expected_distances, offset) = extract_number_vector_and_offset("m", expectation); + println!("{expected_distances:?} == {actual_distances:?}"); + println!("!"); + assert_eq!(expected_distances.len(), actual_distances.len(), "distances mismatch {expected_distances:?} != {actual_distances:?} +- {offset}"); + + zip(actual_distances, expected_distances).for_each(|(actual_distance, expected_distance)| { + assert!(approx_equal_within_offset_range(actual_distance, expected_distance, offset as f64), + "actual distance {actual_distance} not equal to expected value {expected_distance}"); + }); + // // println!("{actual_time} == {expected_time} +- {offset}"); + // assert!( + // approx_equal_within_offset_range(actual_time, expected_time, offset as f64), + // "actual time {actual_time} not equal to expected value {expected_time}" + // ); + }, + "weight" => { + let actual_weight = response.routes.first().expect("no route returned").weight; + let (expected_weight, offset) = extract_number_and_offset("s", expectation); + // println!("{actual_weight} == {expected_weight} +- {offset}"); + assert!( + approx_equal_within_offset_range( + actual_weight, + expected_weight, + offset as f64 + ), + "actual time {actual_weight} not equal to expected value {expected_weight}" + ); + }, + "distance" => { + let actual_distance = response.routes.first().expect("no route returned").distance; + let (expected_distance, offset) = extract_number_and_offset("m", expectation); + assert!( + approx_equal_within_offset_range( + actual_distance, + expected_distance, + offset as f64 + ), + "actual time {actual_distance} not equal to expected value {expected_distance}" + ); + }, + "waypoints" => {}, + // TODO: more checks need to be implemented + _ => { + let msg = format!("case {case} = {expectation} not implemented"); + unimplemented!("{msg}"); + } + }); + } } fn main() { let args = Args::parse(); debug!("arguments: {:?}", args); + let digest = md5_of_osrm_executables().digest().to_hex_lowercase(); + futures::executor::block_on( OSRMWorld::cucumber() .max_concurrent_scenarios(1) .before(move |feature, _rule, scenario, world| { world.scenario_id = common::scenario_id::scenario_id(scenario); world.set_scenario_specific_paths_and_digests(feature.path.clone()); - world.osrm_digest = md5_of_osrm_executables().digest().to_hex_lowercase(); + world.osrm_digest = digest.clone(); // TODO: clean up cache if needed? Or do in scenarios? future::ready(()).boxed() }) - .with_writer(DotWriter::default().normalized()) - .filter_run("features/car/names.feature", |_, _, sc| { + // .with_writer(DotWriter::default().normalized()) + .filter_run("features/testbot/time.feature", |_, _, sc| { !sc.tags.iter().any(|t| t == "todo") }), );