mod common; use cheap_ruler::CheapRuler; use clap::Parser; use common::{ cli_arguments::Args, comparison::Offset, f64_utils::{approx_equal, approximate_within_range}, hash_util::md5_of_osrm_executables, location::Location, osm::{OSMNode, OSMWay}, osrm_world::OSRMWorld, route_response::{self, RouteResponse}, }; use core::panic; use cucumber::{ codegen::ParametersProvider, gherkin::{Step, Table}, given, then, when, World, }; use futures::{future, FutureExt}; use geo_types::Point; use log::debug; use std::{ collections::{HashMap, HashSet}, iter::zip, }; fn offset_origin_by(dx: f64, dy: f64, origin: Location, grid_size: f64) -> Location { let ruler = CheapRuler::new(origin.latitude, cheap_ruler::DistanceUnit::Meters); let loc = ruler.offset( &Point::new(origin.longitude, origin.latitude), dx * grid_size, dy * grid_size, ); Location { latitude: loc.y(), longitude: loc.x(), } } #[given(expr = "the profile \"{word}\"")] fn set_profile(world: &mut OSRMWorld, profile: String) { debug!( "using profile: {profile} on scenario: {}", world.scenario_id ); 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); } #[given(expr = "the node locations")] 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!(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 .iter() .enumerate() .map(|(index, name)| (name.as_str(), index)) .collect(); ["lat", "lon"].iter().for_each(|dim| { assert!( header_lookup.contains_key(*dim), "table must define a {dim} column" ); }); table.rows.iter().skip(1).for_each(|row| { 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"]]; let lat = &row[header_lookup["lat"]]; let location = Location { latitude: lat .parse() .expect("lat {lat} needs to be a floating point number"), longitude: lon .parse() .expect("lon {lon} needs to be a floating point number"), }; 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, id), _ => unreachable!("node name not in [0..9][a..z]"), } }); } #[given(expr = "the node map")] fn set_node_map(world: &mut OSRMWorld, step: &Step) { if let Some(docstring) = step.docstring() { // TODO: refactor into a function docstring .split('\n') .enumerate() .for_each(|(row_index, row)| { let row_index = row_index - 1; row.chars() .enumerate() .filter(|(_column_index, character)| { *character >= '0' && *character <= '9' || *character >= 'a' && *character <= 'z' }) .for_each(|(column_index, name)| { // This ports the logic from previous JS/Ruby implementations. let location = offset_origin_by( column_index as f64 * 0.5, -(row_index as f64), world.origin, world.grid_size, ); 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}"), } }); }); } else { panic!("node map not found"); } } #[given(expr = r#"the extract extra arguments {string}"#)] fn extra_parameters(world: &mut OSRMWorld, parameters: String) { world.extraction_parameters.push(parameters); } #[given(expr = "a grid size of {float} meters")] fn set_grid_size(world: &mut OSRMWorld, meters: f64) { world.grid_size = meters; } #[given(regex = "the ways")] fn set_ways(world: &mut OSRMWorld, step: &Step) { // debug!("using profile: {profile}"); if let Some(table) = step.table.as_ref() { if table.rows.is_empty() { panic!("empty way table provided") } // store a reference to the headers for convenient lookup 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| { let mut way = OSMWay { id: world.make_osm_id(), ..Default::default() }; way.tags.insert("highway".into(), "primary".into()); // default may get overwritten below row.iter().enumerate().for_each(|(column_index, token)| { let header = headers[column_index].as_str(); if header == "nodes" { assert!( token.len() >= 2, "ways must be defined by token of at least length two giving" ); way.tags.insert("name".into(), token.clone()); token.chars().for_each(|name| { if !world.known_osm_nodes.contains_key(&name) { // TODO: this check is probably not necessary since it is also checked below implicitly panic!("referenced unknown node {name} in way {token}"); } if let Some((_, node)) = world.osm_db.find_node(name.to_string()) { way.add_node(node.clone()); } else { panic!("node is known, but not found in osm_db"); } }) } else if !token.is_empty() { way.tags.insert(header.into(), token.clone()); } }); world.osm_db.add_way(way); }); } else { debug!("no table found {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"); // the following lookup allows to define lat lon columns in any order let header = table.rows.first().expect("node locations table empty"); let test_cases: Vec<_> = table .rows .iter() .skip(1) .map(|row| { let row_map: HashMap = row .iter() .enumerate() .map(|(column_index, value)| { let key = header[column_index].clone(); (key, value.clone()) }) .collect(); row_map }) .collect(); (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) { 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()); // run test cases for test_case in &test_cases { let query_location = world.get_location( test_case .get("in") .expect("node name is one char long") .chars() .next() .expect("node name is one char long"), ); let nearest_result = world.nearest(&query_location); let expected_location = &world.get_location( test_case .get("out") .expect("node name is one char long") .chars() .next() .expect("node name is one char long"), ); if test_case.contains_key("out") { assert!(nearest_result.is_ok()); let (_, response) = nearest_result .as_ref() .expect("did not yield a nearest response"); // check that result node is (approximately) equivalent let result_location = response.waypoints[0].location(); 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!(nearest_result.is_ok()); let (_, response) = nearest_result .as_ref() .expect("did not yield a nearest response"); assert_eq!( test_case.get("data_version"), response.data_version.as_ref() ); } } } #[then(expr = "routability should be")] fn routability(world: &mut OSRMWorld, step: &Step) { let tested_headers = HashSet::<_>::from([ "forw", "backw", "bothw", "forw_rate", "backw_rate", "bothw_rate", ]); let (headers, test_cases) = parse_table_from_steps(&step.table.as_ref()); // add all non-empty, non-test headers as key=value pairs to OSM data test_cases .iter() .enumerate() .for_each(|(index, test_case)| { let mut way = OSMWay { id: world.make_osm_id(), ..Default::default() }; way.add_tag("highway", "primary"); way.add_tag("name", &format!("w{index}")); let source = offset_origin_by( world.way_spacing * index as f64, 0., world.origin, world.grid_size, ); let mut source_node = OSMNode { id: world.make_osm_id(), location: source, ..Default::default() }; source_node.add_tag("name", &format!("a{index}")); way.add_node(source_node); let target = offset_origin_by( 4. + world.way_spacing * index as f64, 0., world.origin, world.grid_size, ); let mut target_node = OSMNode { id: world.make_osm_id(), location: target, ..Default::default() }; target_node.add_tag("name", &format!("e{index}")); way.add_node(target_node); test_case .iter() .filter(|(key, _value)| !tested_headers.iter().any(|header| header == key)) .for_each(|(key, value)| { if key != "#" { // ignore comments way.add_tag(key, value); } }); world.add_osm_way(way); }); world.write_osm_file(); world.extract_osm_file(); // TODO: preprocess // TODO: rename forw/backw to forw/backw_speed (comment from JS implementation) if 0 == headers .iter() .filter(|title| tested_headers.contains(title.as_str())) .count() { panic!( r#"*** routability table must contain either "forw", "backw", "bothw", "forw_rate" or "backw_mode" column"# ); } test_cases .iter() .enumerate() .for_each(|(index, test_case)| { let source = offset_origin_by( 1. + world.way_spacing * index as f64, 0., world.origin, world.grid_size, ); let target = offset_origin_by( 3. + world.way_spacing * index as f64, 0., world.origin, world.grid_size, ); let expected_summary = format!("w{index}"); test_case .iter() .filter(|(title, _)| tested_headers.contains(title.as_str())) .for_each(|(title, expectation)| { let route_results = vec![world.route(&[source, target]) ,world.route(&[target, source])]; let forward = title.starts_with("forw"); let route_result = match forward { true => &route_results[0], false => &route_results[1], }; match title.as_str() { "forw" | "backw" => { match expectation.as_str() { "" => { assert!( route_result.is_err() || extract_summary_from_route_response(&route_result.as_ref().unwrap().1) != expected_summary, // || response.routes.first().unwrap().distance == 0., "no route expected when result column {title} is unset" ); } "x" => { let (_, response) = route_result .as_ref() .expect("osrm-routed returned an unexpected error"); assert!( !response.routes.is_empty() && response.routes.first().unwrap().distance >= 0., "no route expected when result column {title} is set to {expectation}" ); } _ if expectation.contains("km/h") => { assert!(route_result.is_ok()); let (_, response) = route_result.as_ref().unwrap(); 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!( approximate_within_range( actual_speed, expected_speed, &offset ), "{actual_speed} and {expected_speed} differ by more than {offset:?}" ); } _ => { let (_, response) = route_result .as_ref() .expect("osrm-routed returned an unexpected error"); let mode = extract_mode_string_from_route_response(response); assert_eq!(&mode, expectation, "failed: {test_case:?}"); } } } "forw_rate" | "backw_rate" => { assert!(route_result.is_ok()); let (_, response) = route_result.as_ref().unwrap(); 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!( approximate_within_range(actual_rate, expected_rate, &Offset::Percentage(1.)), "{actual_rate} and {expected_rate} differ by more than 1%" ); } "bothw" => { match expectation.as_str() { "x" => { for result in &route_results { let (_, response) = result .as_ref() .expect("osrm-routed returned an unexpected error"); assert!( !response.routes.is_empty() && response.routes.first().unwrap().distance >= 0., "no forward route when result column {title}={expectation}" ); } } _ if expectation.contains("km/h") => { let (expected_speed, offset) = extract_number_and_offset("km/h", expectation); for result in &route_results { assert!(result.is_ok()); let (_, response) = result.as_ref().unwrap(); assert!( !response.routes.is_empty(), "route expected when result column is set" ); let route = response.routes.first().unwrap(); let actual_speed = route.distance / route.duration * 3.6; assert!( approximate_within_range( actual_speed, expected_speed, &offset ), "{actual_speed} and {expected_speed} differ by more than {offset:?}" ); } } _ => { // match expectation against mode for result in &route_results { match result { Ok((_,response)) => { let mode = extract_mode_string_from_route_response(response); let summary = extract_summary_from_route_response(response); if summary != expected_summary { assert!(expectation.is_empty()); } else { assert_eq!(&mode, expectation, "failed: {source:?} -> {target:?}"); } }, Err(_) => { assert!(expectation.is_empty()); }, } } } } } _ => { unreachable!("{title} = {expectation}"); } } }); }); } fn extract_summary_from_route_response(response: &RouteResponse) -> String { response .routes .first() .unwrap() .legs .first() .unwrap() .summary .clone() } fn extract_mode_string_from_route_response(response: &RouteResponse) -> String { // From JS test suite: // use the mode of the first step of the route // for routability table test, we can assume the mode is the same throughout the route, // since the route is just a single way assert!( !response.routes.is_empty(), "route expected when extracting mode" ); response .routes .first() .unwrap() .legs .first() .unwrap() .steps .first() .unwrap() .mode .clone() } fn extract_number_and_offset(unit: &str, expectation: &str) -> (f64, Offset) { let expectation = expectation.replace(unit, ""); let delimiter = if expectation.contains("+-") { "+-" } else if expectation.contains('~') { "~" } else { "unknown" }; let tokens: Vec<_> = expectation .split(delimiter) .map(|token| token.trim()) .filter(|token| !token.is_empty()) .collect(); // println!("{tokens:?}"); let number = tokens[0] .parse::() .unwrap_or_else(|_| panic!("'{}' needs to denote a parseablespeed", tokens[0])); let offset = match tokens.len() { 1 => 5., // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise 2 => tokens[1] .replace('~', "") .replace("+-", "") .replace('%', "") .trim() .parse() .unwrap_or_else(|_| panic!("{} needs to specify a number", tokens[1])), _ => unreachable!("expectations can't be parsed"), }; if expectation.ends_with('%') { return (number, Offset::Percentage(offset)); } (number, Offset::Absolute(offset)) } fn extract_number_vector_and_offset(unit: &str, expectation: &str) -> (Vec, Offset) { 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() .unwrap_or_else(|_| panic!("{} needs to specify a number", tokens[1])), // _ => unreachable!("expectations can't be parsed"), }; if expectation.ends_with('%') { return (numbers, Offset::Percentage(offset.into())); } (numbers, Offset::Absolute(offset.into())) } pub enum WaypointsOrLocation { Waypoints, Locations, Undefined, } pub fn get_location_specification(test_case: &HashMap) -> WaypointsOrLocation { assert!( test_case.contains_key("request") || 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("request") { return WaypointsOrLocation::Undefined; } 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 } #[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"; 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(',') .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] } WaypointsOrLocation::Undefined => { world.request_string = test_case.get("request").cloned(); // println!("setting request to: {:?}", world.request_string); vec![] } }; 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 route_result = 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" | "request" | "waypoints" | "#" => {}, // ignore input and comment columns "route" => { let response = match route_result.as_ref() { Ok((_, response)) => response, Err(_) => &RouteResponse::default(), }; let route = if expectation.is_empty() { assert!(route_result.is_err()); assert!(response.routes.is_empty()); String::new() } else { response .routes .first() .expect("no route returned when checking 'route' column") .legs .iter() .map(|leg| { leg.steps .iter() .map(|step| step.name.clone()) .collect::>() .join(",") }).collect::>() .join(",") }; assert_eq!(expectation, route); }, "pronunciations" => { let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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 (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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 (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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!( approximate_within_range(actual_speed, expected_speed, &offset), "actual time {actual_speed} not equal to expected value {expected_speed}" ); }, "modes" => { let actual = match &route_result { Ok((_, response)) => { 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(","); actual_modes }, Err(_) => String::new(), }; assert_eq!(actual, expectation); }, "turns" => { let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); let route = response.routes.first().expect("no route returned"); let actual_turns = route .legs .iter() .map(|leg| { leg.steps .iter() .map(|step| { // NOTE: this is port of JS logic as is. Arguably, this should be replace by a simple join over all type/modifier pairs let r#type = step.maneuver.r#type.clone(); let modifier = match &step.maneuver.modifier { Some(modifier) => modifier.as_str(), _ => "", }; match r#type.as_str() { "depart" | "arrive" => { r#type } "roundabout" => { let exit = match step.maneuver.exit { Some(x) => x, None => unreachable!("roundabout maneuver must come with an exit number"), }; format!("roundabout-exit-{}", exit) }, "rotary" => { let exit = match step.maneuver.exit { Some(x) => x, None => unreachable!("roundabout maneuver must come with an exit number"), }; if let Some(rotary_name) = &step.rotary_name { format!("{rotary_name}-exit-{exit}") } else { format!("rotary-exit-{exit}") } }, "roundabout turn" => { let exit = match step.maneuver.exit { Some(x) => x, None => unreachable!("roundabout maneuver must come with an exit number"), }; format!("{} {} exit-{}", r#type, modifier, exit) } _ => format!("{} {}", r#type, &modifier), } }) .collect::>() .join(",") }) .collect::>() .join(","); assert_eq!(actual_turns, expectation); }, "time" => { let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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!( approximate_within_range(actual_time, expected_time, &offset), "actual time {actual_time} not equal to expected value {expected_time}" ); }, "times" => { // TODO: go over steps let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); let actual_times : Vec= response.routes.first().expect("no route returned").legs.iter().flat_map(|leg| { leg.steps.iter().filter(|step| step.duration > 0.).map(|step| step.duration).collect::>() }).collect(); let (expected_times, offset) = extract_number_vector_and_offset("s", expectation); 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!(approximate_within_range(actual_time, expected_time, &offset), "actual time {actual_time} not equal to expected value {expected_time}"); }); }, "distances" => { let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); let actual_distances = response.routes.first().expect("no route returned").legs.iter().flat_map(|leg| { leg.steps.iter().filter(|step| step.distance > 0.).map(|step| step.distance).collect::>() }).collect::>(); let (expected_distances, offset) = extract_number_vector_and_offset("m", expectation); 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!(approximate_within_range(actual_distance, expected_distance, &offset), "actual distance {actual_distance} not equal to expected value {expected_distance}"); }); }, "weight" => { let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); let actual_weight = response.routes.first().expect("no route returned").weight; let (expected_weight, offset) = extract_number_and_offset("s", expectation); assert!( approximate_within_range( actual_weight, expected_weight, &offset ), "actual weight {actual_weight} not equal to expected value {expected_weight}" ); }, "distance" => { match &route_result { Ok((_, response)) => { let actual_distance = response.routes.first().expect("no route returned").distance; let (expected_distance, offset) = extract_number_and_offset("m", expectation); assert!( approximate_within_range( actual_distance, expected_distance, &offset ), "actual distance {actual_distance} not equal to expected value {expected_distance} +- {offset:?}" ); }, Err(_) => { assert_eq!("", expectation); } }; }, "summary" => { let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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 (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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 (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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 (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error"); 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"), } }, "status" => { let actual_status = match route_result { Ok((code, _)) => code, Err((code, _)) => code, }; let expected_status = test_case.get("status").expect("no status found").parse::().expect("status code must be an u16 number"); assert_eq!( expected_status, actual_status, "status does not match for test case {test_case:?}" ); } "message" => { let actual_message = match route_result.as_ref() { Ok(_) => "".to_string(), Err((_status, e)) => e.message.clone(), }; let expected_message = test_case.get("message").expect("no message found"); assert_eq!(&actual_message, expected_message, "message does not match {test_case:?}"); }, "bearing" => { let reverse_bearing = | bearing: u64 | -> u64 { assert!(bearing <= 360); if bearing >= 180 { return bearing - 180; } bearing + 180 }; let actual_bearing = match &route_result { Ok((_,r)) => { r.routes.first().expect("no routes found in 'bearing' check").legs.iter().map(|leg| { leg.steps.iter().map(|step| { let intersection = step.intersections.first().expect("could not find intersection when checking bearing"); let prefix = if let Some(r#in) = intersection.r#in { reverse_bearing(intersection.bearings[r#in as usize]) } else { 0 }; let suffix = if let Some(out) = intersection.out { intersection.bearings[out as usize] } else { 0 }; format!("{}->{}", prefix, suffix) }).collect::>().join(",") }).collect::>().join(",") }, Err(_) => String::new(), }; let expected_bearing = test_case.get("bearing").expect("test case doesn't have bearing"); assert_eq!(&actual_bearing, expected_bearing, "bearings don't match"); }, "lanes" => { let actual = match &route_result { Ok((_, response)) => { let route = response.routes.first().expect("no route returned"); /* if(i.lanes) { return i.lanes.map( l => { let indications = l.indications.join(';'); return indications + ':' + (l.valid ? 'true' : 'false'); }).join(' '); } else { return ''; } }).join(';'); */ let actual_lanes = route .legs .iter() .map(|leg| { leg.steps .iter() .map(|step| {//step.intersections.iter().map(|i|{ "-".to_string() }) .collect::>() .join(" ") }) .collect::>() .join(";"); actual_lanes }, Err(_) => String::new(), }; assert_eq!(actual, expectation); }, "classes" => { // '[' + s.intersections.map(i => '(' + (i.classes ? i.classes.join(',') : '') + ')').join(',') + ']'); let (_, response) = route_result.as_ref().expect("no route response in 'classes'"); let first_route = &response.routes[0]; let first_leg = first_route.legs.first().expect("no first leg on route"); let actual_classes = first_leg.steps.iter().map(|step| { "[".to_string() + &step.intersections.iter().map(|intersection| { let tmp = match &intersection.classes { Some(classes) => { classes.join(",") }, None => "".to_string(), }; "(".to_string() + &tmp + ")" }).collect::>().join(",") + "]" }).collect::>().join(","); let expected_classes = test_case.get("classes").expect("test case classes not found"); assert_eq!(&actual_classes, expected_classes, "classes don't match"); } // TODO: more checks need to be implemented _ => { let msg = format!("case {case} = {expectation} not implemented"); unimplemented!("{msg}"); } }); } } fn main() { let args = Args::parse(); debug!("{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.clone_from(&digest); // TODO: clean up cache if needed? Or do in scenarios? future::ready(()).boxed() }) // .with_writer(DotWriter::default().normalized()) .filter_run("features", |fe, r, sc| { // .filter_run("features/bicycle/classes.feature", |fe, r, sc| { let tag = "todo".to_string(); !sc.tags.contains(&tag) && !fe.tags.contains(&tag) && !r.is_some_and(|rule| rule.tags.contains(&tag)) }), ); }