diff --git a/docs/http.md b/docs/http.md index 9e6649d73..3e6ee4c42 100644 --- a/docs/http.md +++ b/docs/http.md @@ -200,7 +200,7 @@ curl 'http://router.project-osrm.org/nearest/v1/driving/13.388860,52.517037?numb Finds the fastest route between coordinates in the supplied order. ```endpoint -GET /route/v1/{profile}/{coordinates}?alternatives={true|false|number}&steps={true|false}&geometries={polyline|polyline6|geojson}&overview={full|simplified|false}&annotations={true|false} +GET /route/v1/{profile}/{coordinates}?alternatives={true|false|number}&steps={true|false}&geometries={polyline|polyline6|geojson}&overview={full|simplified|false}&annotations={true|false}&continue_straight={default|true|false} ``` In addition to the [general options](#general-options) the following options are supported for this service: diff --git a/features/support/http.js b/features/support/http.js index 55ff660de..71f61761a 100644 --- a/features/support/http.js +++ b/features/support/http.js @@ -29,8 +29,6 @@ module.exports = function () { var params = this.paramsToString(parameters); this.query = baseUri + (params.length ? '/' + params : ''); - console.log(this.query); - request(this.query, (err, res, body) => { if (err && err.code === 'ECONNREFUSED') { return cb(new Error('*** osrm-routed is not running.')); diff --git a/tests/common/local_task.rs b/tests/common/local_task.rs index 5e433978b..e4c0e847f 100644 --- a/tests/common/local_task.rs +++ b/tests/common/local_task.rs @@ -24,7 +24,7 @@ impl LocalTask { // TODO: also check that process is running self.ready } - pub fn arg(&mut self, argument: &str) -> &mut Self { + pub fn arg(mut self, argument: &str) -> Self { self.arguments.push(argument.into()); self } diff --git a/tests/common/osrm_world.rs b/tests/common/osrm_world.rs index 0017c3ec6..ff0c67be5 100644 --- a/tests/common/osrm_world.rs +++ b/tests/common/osrm_world.rs @@ -36,7 +36,7 @@ pub struct OSRMWorld { pub extraction_parameters: Vec, pub request_with_flatbuffers: bool, - pub bearings: Option, + pub query_options: HashMap, pub grid_size: f32, pub origin: Location, @@ -60,7 +60,13 @@ impl Default for OSRMWorld { osm_db: Default::default(), extraction_parameters: Default::default(), request_with_flatbuffers: Default::default(), - bearings: None, + query_options: HashMap::from([ + // default parameters // TODO: check if necessary + ("steps".into(), "true".into()), + ("alternatives".into(), "false".into()), + ("annotations".into(), "true".into()), + ]), + grid_size: DEFAULT_GRID_SIZE, origin: DEFAULT_ORIGIN, way_spacing: WAY_SPACING, @@ -206,9 +212,7 @@ impl OSRMWorld { .artefact_cache_path() .join(self.scenario_id.to_owned() + ".osrm"); - // TODO: this should not require a temporary and behave like the API of std::process - self.task = LocalTask::new(self.routed_path().to_string_lossy().into()); - self.task + self.task = LocalTask::new(self.routed_path().to_string_lossy().into()) .arg(data_path.to_str().expect("data path unwrappable")); self.task .spawn_wait_till_ready("running and waiting for requests"); @@ -229,6 +233,19 @@ impl OSRMWorld { if self.request_with_flatbuffers { url += ".flatbuffers"; } + + if !self.query_options.is_empty() { + let options = self + .query_options + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join("&"); + url += "?"; + url += &options; + } + + // panic!("url: {url}"); let call = self.agent.get(&url).call(); let body = match call { @@ -253,15 +270,21 @@ impl OSRMWorld { .join(";"); let mut url = format!( - "http://localhost:5000/route/v1/{}/{waypoint_string}?steps=true&alternatives=false", + "http://localhost:5000/route/v1/{}/{waypoint_string}", self.profile, ); if self.request_with_flatbuffers { url += ".flatbuffers"; } - if let Some(bearings) = &self.bearings { - url += "&bearings="; - url += bearings; + if !self.query_options.is_empty() { + let options = self + .query_options + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join("&"); + url += "?"; + url += &options; } // println!("url: {url}"); let call = self.agent.get(&url).call(); diff --git a/tests/common/route_response.rs b/tests/common/route_response.rs index 920d5be0d..16f76d06e 100644 --- a/tests/common/route_response.rs +++ b/tests/common/route_response.rs @@ -1,3 +1,5 @@ +use std::default; + use serde::Deserialize; use super::{location::Location, nearest_response::Waypoint}; @@ -11,9 +13,25 @@ pub struct Maneuver { pub r#type: String, // TODO: should be an enum } +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +pub enum Geometry { + A(String), + B { + coordinates: Vec, + r#type: String, + }, +} + +impl Default for Geometry { + fn default() -> Self { + Geometry::A("".to_string()) + } +} + #[derive(Deserialize, Default, Debug)] pub struct Step { - pub geometry: String, + pub geometry: Geometry, pub mode: String, pub maneuver: Maneuver, pub name: String, @@ -38,9 +56,9 @@ pub struct Leg { // pub annotation: Option>, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default)] pub struct Route { - pub geometry: String, + pub geometry: Geometry, pub weight: f64, pub duration: f64, pub legs: Vec, @@ -52,7 +70,7 @@ pub struct Route { pub struct RouteResponse { pub code: String, pub routes: Vec, - pub waypoints: Vec, + pub waypoints: Option>, pub data_version: Option, } @@ -66,7 +84,6 @@ impl RouteResponse { } pub fn from_string(input: &str) -> Self { - // println!("{input}"); let response = match serde_json::from_str(input) { Ok(response) => response, Err(e) => panic!("parsing error {e} => {input}"), @@ -74,3 +91,15 @@ impl RouteResponse { response } } + +// #[cfg(test)] +// mod tests { +// use super::RouteResponse; + +// #[test] +// fn parse_geojson() { +// let input = r#"{"code":"Ok","routes":[{"geometry":{"coordinates":[[1.00009,1],[1.000269,1]],"type":"LineString"},"weight":1.9,"duration":1.9,"legs":[{"annotation":{"speed":[10.5],"weight":[1.9],"nodes":[1,2],"duration":[1.9],"distance":[19.92332315]},"summary":"abc","weight":1.9,"duration":1.9,"steps":[{"geometry":{"coordinates":[[1.00009,1],[1.000269,1]],"type":"LineString"},"maneuver":{"location":[1.00009,1],"bearing_after":90,"bearing_before":0,"modifier":"right","type":"depart"},"mode":"driving","name":"abc","intersections":[{"out":0,"entry":[true],"bearings":[90],"location":[1.00009,1]}],"driving_side":"right","weight":1.9,"duration":1.9,"distance":19.9},{"geometry":{"coordinates":[[1.000269,1],[1.000269,1]],"type":"LineString"},"maneuver":{"location":[1.000269,1],"bearing_after":0,"bearing_before":90,"modifier":"right","type":"arrive"},"mode":"driving","name":"abc","intersections":[{"in":0,"entry":[true],"bearings":[270],"location":[1.000269,1]}],"driving_side":"right","weight":0,"duration":0,"distance":0}],"distance":19.9}],"weight_name":"duration","distance":19.9}],"waypoints":[{"name":"abc","hint":"AAAAgAEAAIAKAAAAHgAAAAAAAAAoAAAA6kYgQWyG70EAAAAA6kYgQgoAAAAeAAAAAAAAACgAAAABAACAmkIPAEBCDwCaQg8Ai0EPAAAArwUAAAAA","distance":20.01400211,"location":[1.00009,1]},{"name":"abc","hint":"AAAAgAEAAIAdAAAACwAAAAAAAAAoAAAAbIbvQepGIEEAAAAA6kYgQh0AAAALAAAAAAAAACgAAAABAACATUMPAEBCDwBNQw8Ai0EPAAAArwUAAAAA","distance":20.01400211,"location":[1.000269,1]}]} "#; +// let result = RouteResponse::from_string(&input); + +// } +// } diff --git a/tests/cucumber.rs b/tests/cucumber.rs index 359ae5327..ba0b53581 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -12,11 +12,14 @@ use common::{ location::Location, osm::OSMWay, osrm_world::OSRMWorld, + route_response, }; use core::panic; use cucumber::{ gherkin::{Step, Table}, - given, then, when, World, WriterExt, + given, then, when, + writer::summarize, + World, WriterExt, }; use futures::{future, FutureExt}; use geo_types::Point; @@ -48,6 +51,12 @@ fn set_profile(world: &mut OSRMWorld, profile: String) { world.profile = profile; } +#[given(expr = "the query options")] +fn set_query_options(world: &mut OSRMWorld, step: &Step) { + let table = parse_option_table(&step.table.as_ref()); + world.query_options.extend(table.into_iter()); +} + #[given(expr = "the node locations")] fn set_node_locations(world: &mut OSRMWorld, step: &Step) { let table = step.table().expect("cannot get table"); @@ -189,6 +198,19 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) { } } +fn parse_option_table(table: &Option<&Table>) -> HashMap { + let table = table.expect("no query table specified"); + let result = table + .rows + .iter() + .map(|row| { + assert_eq!(2, row.len()); + (row[0].clone(), row[1].clone()) + }) + .collect(); + result +} + fn parse_table_from_steps(table: &Option<&Table>) -> (Vec, Vec>) { // parse query data let table = table.expect("no query table specified"); @@ -456,6 +478,15 @@ pub fn get_location_specification(test_case: &HashMap) -> Waypoi // WaypointsOrLocation::Undefined } +#[given(expr = r"skip waypoints")] +fn skip_waypoints(world: &mut OSRMWorld, step: &Step) { + // TODO: adapt test to use query options + // only used in features/testbot/basic.feature + world + .query_options + .insert("skip_waypoints".into(), "true".into()); +} + #[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"; @@ -500,8 +531,11 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { } }; - if let Some(bearing) = test_case.get("bearings").cloned() { - world.bearings = Some(bearing.replace(" ", ";")); + if let Some(bearings) = test_case.get("bearings").cloned() { + // TODO: change test cases to provide proper query options + world + .query_options + .insert("bearings".into(), bearings.replace(" ", ";")); } let response = world.route(&waypoints); @@ -510,7 +544,7 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { .iter() .map(|(column_title, expectation)| (column_title.as_str(), expectation.as_str())) .for_each(|(case, expectation)| match case { - "from" | "to" | "bearings"=> {}, // ignore input columns + "from" | "to" | "bearings" | "waypoints" | "#" => {}, // ignore input and comment columns "route" => { let route = if expectation.is_empty() { assert!(response.routes.is_empty()); @@ -603,13 +637,18 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { .legs .iter() .map(|leg| { - leg.steps + leg.steps .iter() .map(|step| { let prefix = step.maneuver.r#type.clone(); + if prefix == "depart" || prefix == "arrive" { + // TODO: this reimplements the behavior that depart and arrive are not checked for their modifier + // check if tests shall be adapted, since this is reported by the engine + return prefix; + } let suffix = match &step.maneuver.modifier { Some(modifier) => " ".to_string() + &modifier, - None => "".into(), + _ => "".into(), }; prefix + &suffix }) @@ -633,10 +672,10 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { // 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::>() + leg.steps.iter().filter(|step| step.duration > 0.).map(|step| step.duration).collect::>() }).flatten().collect(); let (expected_times, offset) = extract_number_vector_and_offset("s", expectation); - println!("{actual_times:?} == {expected_times:?} +- {offset}"); + // 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)| { @@ -645,29 +684,20 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { }); }, "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 actual_distances = response.routes.first().expect("no route returned").legs.iter().map(|leg| { + leg.steps.iter().filter(|step| step.distance > 0.).map(|step| step.distance).collect::>() + }).flatten().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, @@ -689,7 +719,54 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { "actual time {actual_distance} not equal to expected value {expected_distance}" ); }, - "waypoints" => {}, + "summary" => { + let actual_summary = response.routes.first().expect("no route returned").legs.iter().map(|leg| { + leg.summary.clone() + }).collect::>().join(","); + assert_eq!(actual_summary,expectation, "summary mismatch"); + }, + "data_version" => { + let expected_data_version = match test_case.get("data_version") { + Some(s) if !s.is_empty() => Some(s), + _ => None, + }; + assert_eq!( + expected_data_version, + response.data_version.as_ref(), + "data_version does not match" + ); + }, + "waypoints_count" => { + let expected_waypoint_count = match test_case.get("waypoints_count") { + Some(s) if !s.is_empty() => s.parse::().expect("waypoint_count is a number"), + _ => 0, + }; + let actual_waypoint_count = match &response.waypoints { + Some(w) => w.len(), + None => 0, + }; + assert_eq!( + expected_waypoint_count, + actual_waypoint_count, + "waypoint_count does not match" + ); + }, + "geometry" => { + let expected_geometry = test_case.get("geometry").expect("no geometry found"); + match &response.routes.first().expect("no route").geometry { + route_response::Geometry::A(actual_geometry) => { + assert_eq!( + expected_geometry, + actual_geometry, + "geometry does not match" + ); + }, + route_response::Geometry::B { coordinates: _, r#type: _ } => unimplemented!("geojson comparison"), + } + + + }, + // "classes" => {}, // TODO: more checks need to be implemented _ => { let msg = format!("case {case} = {expectation} not implemented"); @@ -700,8 +777,7 @@ fn request_route(world: &mut OSRMWorld, step: &Step, state: String) { } fn main() { let args = Args::parse(); - debug!("arguments: {:?}", args); - + debug!("{args:?}"); let digest = md5_of_osrm_executables().digest().to_hex_lowercase(); futures::executor::block_on( @@ -717,8 +793,7 @@ fn main() { future::ready(()).boxed() }) // .with_writer(DotWriter::default().normalized()) - .filter_run("features/testbot/time.feature", |_, _, sc| { - !sc.tags.iter().any(|t| t == "todo") - }), + // .filter_run("features/testbot/geometry.feature", |_, _, sc| { + .filter_run("features", |_, _, sc| !sc.tags.iter().any(|t| t == "todo")), ); }