2024-05-30 10:39:51 -04:00
|
|
|
mod common;
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
use cheap_ruler::CheapRuler;
|
2024-05-30 10:19:03 -04:00
|
|
|
use clap::Parser;
|
2024-06-05 04:31:07 -04:00
|
|
|
use common::{
|
2024-06-27 04:09:33 -04:00
|
|
|
cli_arguments::Args,
|
2024-07-09 13:08:04 -04:00
|
|
|
comparison::Offset,
|
|
|
|
f64_utils::{approx_equal, approximate_within_range},
|
2024-06-27 04:09:33 -04:00
|
|
|
hash_util::md5_of_osrm_executables,
|
|
|
|
location::Location,
|
2024-07-09 13:08:04 -04:00
|
|
|
osm::{OSMNode, OSMWay},
|
2024-06-27 04:09:33 -04:00
|
|
|
osrm_world::OSRMWorld,
|
2024-07-04 08:48:58 -04:00
|
|
|
route_response::{self, RouteResponse},
|
2024-06-05 04:31:07 -04:00
|
|
|
};
|
2024-05-30 12:23:03 -04:00
|
|
|
use core::panic;
|
2024-06-13 11:02:28 -04:00
|
|
|
use cucumber::{
|
2024-07-09 13:08:04 -04:00
|
|
|
codegen::ParametersProvider,
|
2024-06-13 11:02:28 -04:00
|
|
|
gherkin::{Step, Table},
|
2024-07-12 13:13:16 -04:00
|
|
|
given, then, when, World,
|
2024-06-13 11:02:28 -04:00
|
|
|
};
|
2024-05-30 08:35:52 -04:00
|
|
|
use futures::{future, FutureExt};
|
2024-06-13 11:02:28 -04:00
|
|
|
use geo_types::Point;
|
2024-06-05 04:31:07 -04:00
|
|
|
use log::debug;
|
2024-06-27 04:09:33 -04:00
|
|
|
use std::{
|
2024-07-09 14:04:25 -04:00
|
|
|
collections::{HashMap, HashSet},
|
|
|
|
iter::zip,
|
2024-06-27 04:09:33 -04:00
|
|
|
};
|
2024-05-30 08:35:52 -04:00
|
|
|
|
2024-07-09 13:08:04 -04:00
|
|
|
fn offset_origin_by(dx: f64, dy: f64, origin: Location, grid_size: f64) -> Location {
|
2024-06-13 11:02:28 -04:00
|
|
|
let ruler = CheapRuler::new(origin.latitude, cheap_ruler::DistanceUnit::Meters);
|
2024-06-08 11:21:54 -04:00
|
|
|
let loc = ruler.offset(
|
2024-06-13 11:02:28 -04:00
|
|
|
&Point::new(origin.longitude, origin.latitude),
|
|
|
|
dx * grid_size,
|
|
|
|
dy * grid_size,
|
2024-07-09 13:08:04 -04:00
|
|
|
);
|
2024-06-08 11:21:54 -04:00
|
|
|
Location {
|
2024-06-13 11:02:28 -04:00
|
|
|
latitude: loc.y(),
|
|
|
|
longitude: loc.x(),
|
2024-06-08 11:21:54 -04:00
|
|
|
}
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[given(expr = "the profile \"{word}\"")]
|
|
|
|
fn set_profile(world: &mut OSRMWorld, profile: String) {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!(
|
2024-05-30 08:35:52 -04:00
|
|
|
"using profile: {profile} on scenario: {}",
|
|
|
|
world.scenario_id
|
|
|
|
);
|
|
|
|
world.profile = profile;
|
|
|
|
}
|
2024-06-05 04:31:07 -04:00
|
|
|
|
2024-06-28 07:04:04 -04:00
|
|
|
#[given(expr = "the query options")]
|
|
|
|
fn set_query_options(world: &mut OSRMWorld, step: &Step) {
|
|
|
|
let table = parse_option_table(&step.table.as_ref());
|
2024-07-09 14:04:25 -04:00
|
|
|
world.query_options.extend(table);
|
2024-06-28 07:04:04 -04:00
|
|
|
}
|
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
#[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");
|
2024-06-27 04:09:33 -04:00
|
|
|
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"
|
2024-06-05 04:31:07 -04:00
|
|
|
);
|
|
|
|
// the following lookup allows to define lat lon columns in any order
|
2024-06-05 12:26:39 -04:00
|
|
|
let header_lookup: HashMap<&str, usize> = header
|
2024-06-05 13:19:36 -04:00
|
|
|
.iter()
|
2024-06-05 12:26:39 -04:00
|
|
|
.enumerate()
|
|
|
|
.map(|(index, name)| (name.as_str(), index))
|
|
|
|
.collect();
|
2024-06-05 04:31:07 -04:00
|
|
|
["lat", "lon"].iter().for_each(|dim| {
|
|
|
|
assert!(
|
2024-06-05 12:26:39 -04:00
|
|
|
header_lookup.contains_key(*dim),
|
2024-06-05 04:31:07 -04:00
|
|
|
"table must define a {dim} column"
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2024-06-08 11:21:54 -04:00
|
|
|
table.rows.iter().skip(1).for_each(|row| {
|
2024-06-27 04:09:33 -04:00
|
|
|
assert!(
|
|
|
|
row.len() >= 3,
|
|
|
|
"nod locations must at least specify three tables: node, lat, and lon"
|
|
|
|
);
|
2024-06-05 04:31:07 -04:00
|
|
|
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
|
2024-06-05 12:26:39 -04:00
|
|
|
let lon = &row[header_lookup["lon"]];
|
|
|
|
let lat = &row[header_lookup["lat"]];
|
2024-06-08 11:21:54 -04:00
|
|
|
let location = Location {
|
2024-07-09 13:08:04 -04:00
|
|
|
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"),
|
2024-06-08 11:21:54 -04:00
|
|
|
};
|
2024-06-27 04:09:33 -04:00
|
|
|
let id = match header_lookup.get("id") {
|
|
|
|
Some(index) => {
|
|
|
|
let id = row[*index]
|
|
|
|
.parse::<u64>()
|
|
|
|
.expect("id of a node must be u64 number");
|
|
|
|
Some(id)
|
|
|
|
}
|
|
|
|
_ => None,
|
|
|
|
};
|
2024-06-05 04:31:07 -04:00
|
|
|
match name {
|
|
|
|
'0'...'9' => world.add_location(*name, location),
|
2024-06-27 04:09:33 -04:00
|
|
|
'a'...'z' => world.add_osm_node(*name, location, id),
|
2024-06-05 04:31:07 -04:00
|
|
|
_ => unreachable!("node name not in [0..9][a..z]"),
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-30 08:35:52 -04:00
|
|
|
#[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)| {
|
2024-07-09 13:08:04 -04:00
|
|
|
let row_index = row_index - 1;
|
2024-05-30 08:35:52 -04:00
|
|
|
row.chars()
|
|
|
|
.enumerate()
|
2024-07-09 13:08:04 -04:00
|
|
|
.filter(|(_column_index, character)| {
|
|
|
|
*character >= '0' && *character <= '9'
|
|
|
|
|| *character >= 'a' && *character <= 'z'
|
|
|
|
})
|
2024-05-30 08:35:52 -04:00
|
|
|
.for_each(|(column_index, name)| {
|
2024-07-09 13:08:04 -04:00
|
|
|
// This ports the logic from previous JS/Ruby implementations.
|
2024-06-13 11:02:28 -04:00
|
|
|
let location = offset_origin_by(
|
2024-07-09 13:08:04 -04:00
|
|
|
column_index as f64 * 0.5,
|
|
|
|
-(row_index as f64),
|
2024-06-13 11:02:28 -04:00
|
|
|
world.origin,
|
|
|
|
world.grid_size,
|
|
|
|
);
|
2024-05-30 08:35:52 -04:00
|
|
|
match name {
|
|
|
|
'0'...'9' => world.add_location(name, location),
|
|
|
|
'a'...'z' => world.add_osm_node(name, location, None),
|
2024-07-09 13:08:04 -04:00
|
|
|
_ => unreachable!("node name not in [0..9][a..z]: {docstring}"),
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
panic!("node map not found");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
#[given(expr = r#"the extract extra arguments {string}"#)]
|
|
|
|
fn extra_parameters(world: &mut OSRMWorld, parameters: String) {
|
|
|
|
world.extraction_parameters.push(parameters);
|
|
|
|
}
|
|
|
|
|
2024-06-13 11:02:28 -04:00
|
|
|
#[given(expr = "a grid size of {float} meters")]
|
2024-07-09 13:08:04 -04:00
|
|
|
fn set_grid_size(world: &mut OSRMWorld, meters: f64) {
|
2024-06-13 11:02:28 -04:00
|
|
|
world.grid_size = meters;
|
|
|
|
}
|
|
|
|
|
2024-05-30 08:35:52 -04:00
|
|
|
#[given(regex = "the ways")]
|
|
|
|
fn set_ways(world: &mut OSRMWorld, step: &Step) {
|
2024-06-05 04:31:07 -04:00
|
|
|
// debug!("using profile: {profile}");
|
2024-05-30 08:35:52 -04:00
|
|
|
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
|
2024-06-08 11:21:54 -04:00
|
|
|
let headers = table.rows.first().expect("table has a first row");
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
// 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| {
|
2024-05-30 12:23:03 -04:00
|
|
|
if !world.known_osm_nodes.contains_key(&name) {
|
|
|
|
// TODO: this check is probably not necessary since it is also checked below implicitly
|
2024-05-30 08:35:52 -04:00
|
|
|
panic!("referenced unknown node {name} in way {token}");
|
|
|
|
}
|
2024-07-09 13:08:04 -04:00
|
|
|
if let Some((_, node)) = world.osm_db.find_node(name.to_string()) {
|
2024-05-30 08:35:52 -04:00
|
|
|
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 {
|
2024-06-05 04:31:07 -04:00
|
|
|
debug!("no table found {step:#?}");
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-28 07:04:04 -04:00
|
|
|
fn parse_option_table(table: &Option<&Table>) -> HashMap<String, String> {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-06-27 04:09:33 -04:00
|
|
|
fn parse_table_from_steps(table: &Option<&Table>) -> (Vec<String>, Vec<HashMap<String, String>>) {
|
2024-05-30 12:23:03 -04:00
|
|
|
// parse query data
|
2024-06-13 11:02:28 -04:00
|
|
|
let table = table.expect("no query table specified");
|
2024-06-05 12:26:39 -04:00
|
|
|
// 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
|
2024-05-30 12:32:06 -04:00
|
|
|
.rows
|
|
|
|
.iter()
|
|
|
|
.skip(1)
|
|
|
|
.map(|row| {
|
2024-06-05 12:26:39 -04:00
|
|
|
let row_map: HashMap<String, String> = row
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(column_index, value)| {
|
|
|
|
let key = header[column_index].clone();
|
|
|
|
(key, value.clone())
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
row_map
|
2024-05-30 12:32:06 -04:00
|
|
|
})
|
|
|
|
.collect();
|
2024-06-27 04:09:33 -04:00
|
|
|
(header.clone(), test_cases)
|
2024-06-13 11:02:28 -04:00
|
|
|
}
|
2024-05-30 12:23:03 -04:00
|
|
|
|
2024-06-13 11:02:28 -04:00
|
|
|
#[when(regex = r"^I request nearest( with flatbuffers|) I should get$")]
|
|
|
|
fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) {
|
2024-06-27 04:09:33 -04:00
|
|
|
world.request_with_flatbuffers = state == " with flatbuffers";
|
2024-06-13 11:02:28 -04:00
|
|
|
world.write_osm_file();
|
|
|
|
world.extract_osm_file();
|
2024-06-02 10:34:05 -04:00
|
|
|
|
2024-06-13 11:02:28 -04:00
|
|
|
// parse query data
|
2024-06-27 04:09:33 -04:00
|
|
|
let (_, test_cases) = parse_table_from_steps(&step.table.as_ref());
|
2024-05-30 12:59:00 -04:00
|
|
|
|
2024-06-13 11:02:28 -04:00
|
|
|
// run test cases
|
|
|
|
for test_case in &test_cases {
|
2024-06-05 12:26:39 -04:00
|
|
|
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"),
|
|
|
|
);
|
2024-06-13 11:02:28 -04:00
|
|
|
|
2024-07-04 08:48:58 -04:00
|
|
|
let nearest_result = world.nearest(&query_location);
|
2024-06-13 11:02:28 -04:00
|
|
|
|
|
|
|
let expected_location = &world.get_location(
|
2024-06-05 12:26:39 -04:00
|
|
|
test_case
|
|
|
|
.get("out")
|
|
|
|
.expect("node name is one char long")
|
|
|
|
.chars()
|
|
|
|
.next()
|
|
|
|
.expect("node name is one char long"),
|
|
|
|
);
|
2024-05-30 12:23:03 -04:00
|
|
|
|
2024-06-05 12:26:39 -04:00
|
|
|
if test_case.contains_key("out") {
|
2024-07-04 08:48:58 -04:00
|
|
|
assert!(nearest_result.is_ok());
|
|
|
|
let (_, response) = nearest_result
|
|
|
|
.as_ref()
|
|
|
|
.expect("did not yield a nearest response");
|
2024-06-05 12:26:39 -04:00
|
|
|
// check that result node is (approximately) equivalent
|
|
|
|
let result_location = response.waypoints[0].location();
|
2024-06-08 11:21:54 -04:00
|
|
|
assert!(approx_equal(
|
|
|
|
result_location.longitude,
|
|
|
|
expected_location.longitude,
|
|
|
|
5
|
|
|
|
));
|
|
|
|
assert!(approx_equal(
|
|
|
|
result_location.latitude,
|
|
|
|
expected_location.latitude,
|
|
|
|
5
|
|
|
|
));
|
2024-06-05 12:26:39 -04:00
|
|
|
}
|
|
|
|
if test_case.contains_key("data_version") {
|
2024-07-04 08:48:58 -04:00
|
|
|
assert!(nearest_result.is_ok());
|
|
|
|
let (_, response) = nearest_result
|
|
|
|
.as_ref()
|
|
|
|
.expect("did not yield a nearest response");
|
2024-06-08 11:21:54 -04:00
|
|
|
assert_eq!(
|
|
|
|
test_case.get("data_version"),
|
|
|
|
response.data_version.as_ref()
|
|
|
|
);
|
2024-06-05 12:26:39 -04:00
|
|
|
}
|
2024-05-30 12:32:06 -04:00
|
|
|
}
|
2024-05-30 08:35:52 -04:00
|
|
|
}
|
|
|
|
|
2024-06-27 04:09:33 -04:00
|
|
|
#[then(expr = "routability should be")]
|
|
|
|
fn routability(world: &mut OSRMWorld, step: &Step) {
|
2024-07-09 13:08:04 -04:00
|
|
|
let tested_headers = HashSet::<_>::from([
|
2024-06-27 04:09:33 -04:00
|
|
|
"forw",
|
|
|
|
"backw",
|
|
|
|
"bothw",
|
|
|
|
"forw_rate",
|
|
|
|
"backw_rate",
|
|
|
|
"bothw_rate",
|
|
|
|
]);
|
2024-07-09 13:08:04 -04:00
|
|
|
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
|
2024-06-27 04:09:33 -04:00
|
|
|
.iter()
|
2024-07-09 13:08:04 -04:00
|
|
|
.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()))
|
2024-06-27 04:09:33 -04:00
|
|
|
.count()
|
|
|
|
{
|
|
|
|
panic!(
|
|
|
|
r#"*** routability table must contain either "forw", "backw", "bothw", "forw_rate" or "backw_mode" column"#
|
2024-06-13 11:02:28 -04:00
|
|
|
);
|
2024-06-27 04:09:33 -04:00
|
|
|
}
|
2024-06-13 11:02:28 -04:00
|
|
|
|
2024-06-27 04:09:33 -04:00
|
|
|
test_cases
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.for_each(|(index, test_case)| {
|
|
|
|
let source = offset_origin_by(
|
2024-07-09 13:08:04 -04:00
|
|
|
1. + world.way_spacing * index as f64,
|
2024-06-27 04:09:33 -04:00
|
|
|
0.,
|
|
|
|
world.origin,
|
|
|
|
world.grid_size,
|
|
|
|
);
|
|
|
|
let target = offset_origin_by(
|
2024-07-09 13:08:04 -04:00
|
|
|
3. + world.way_spacing * index as f64,
|
2024-06-27 04:09:33 -04:00
|
|
|
0.,
|
|
|
|
world.origin,
|
|
|
|
world.grid_size,
|
|
|
|
);
|
2024-07-09 13:08:04 -04:00
|
|
|
|
|
|
|
let expected_summary = format!("w{index}");
|
|
|
|
|
2024-06-27 04:09:33 -04:00
|
|
|
test_case
|
2024-06-13 11:02:28 -04:00
|
|
|
.iter()
|
2024-07-09 13:08:04 -04:00
|
|
|
.filter(|(title, _)| tested_headers.contains(title.as_str()))
|
2024-06-27 04:09:33 -04:00
|
|
|
.for_each(|(title, expectation)| {
|
2024-07-09 14:04:25 -04:00
|
|
|
let route_results = vec![world.route(&[source, target])
|
|
|
|
,world.route(&[target, source])];
|
2024-06-27 04:09:33 -04:00
|
|
|
let forward = title.starts_with("forw");
|
2024-07-04 08:48:58 -04:00
|
|
|
let route_result = match forward {
|
2024-07-09 13:08:04 -04:00
|
|
|
true => &route_results[0],
|
|
|
|
false => &route_results[1],
|
2024-06-27 04:09:33 -04:00
|
|
|
};
|
2024-07-09 13:08:04 -04:00
|
|
|
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"
|
|
|
|
);
|
2024-06-13 11:02:28 -04:00
|
|
|
|
2024-07-09 13:08:04 -04:00
|
|
|
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::<f64>()
|
|
|
|
.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());
|
|
|
|
},
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-06-27 04:09:33 -04:00
|
|
|
}
|
2024-07-09 13:08:04 -04:00
|
|
|
_ => {
|
|
|
|
unreachable!("{title} = {expectation}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2024-06-27 04:09:33 -04:00
|
|
|
});
|
|
|
|
}
|
2024-06-13 11:02:28 -04:00
|
|
|
|
2024-07-09 13:08:04 -04:00
|
|
|
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("+-") {
|
|
|
|
"+-"
|
2024-07-09 14:04:25 -04:00
|
|
|
} else if expectation.contains('~') {
|
2024-07-09 13:08:04 -04:00
|
|
|
"~"
|
|
|
|
} else {
|
|
|
|
"unknown"
|
|
|
|
};
|
2024-06-27 04:09:33 -04:00
|
|
|
let tokens: Vec<_> = expectation
|
2024-07-09 13:08:04 -04:00
|
|
|
.split(delimiter)
|
2024-06-27 04:09:33 -04:00
|
|
|
.map(|token| token.trim())
|
|
|
|
.filter(|token| !token.is_empty())
|
|
|
|
.collect();
|
|
|
|
// println!("{tokens:?}");
|
|
|
|
let number = tokens[0]
|
|
|
|
.parse::<f64>()
|
2024-07-09 14:04:25 -04:00
|
|
|
.unwrap_or_else(|_| panic!("'{}' needs to denote a parseablespeed", tokens[0]));
|
2024-06-27 04:09:33 -04:00
|
|
|
let offset = match tokens.len() {
|
2024-07-04 08:48:58 -04:00
|
|
|
1 => 5., // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise
|
2024-06-27 04:09:33 -04:00
|
|
|
2 => tokens[1]
|
2024-07-09 14:04:25 -04:00
|
|
|
.replace('~', "")
|
2024-07-09 13:08:04 -04:00
|
|
|
.replace("+-", "")
|
2024-07-09 14:04:25 -04:00
|
|
|
.replace('%', "")
|
2024-06-27 04:09:33 -04:00
|
|
|
.trim()
|
|
|
|
.parse()
|
2024-07-09 14:04:25 -04:00
|
|
|
.unwrap_or_else(|_| panic!("{} needs to specify a number", tokens[1])),
|
2024-06-27 04:09:33 -04:00
|
|
|
_ => unreachable!("expectations can't be parsed"),
|
|
|
|
};
|
2024-07-09 14:04:25 -04:00
|
|
|
if expectation.ends_with('%') {
|
2024-07-09 13:08:04 -04:00
|
|
|
return (number, Offset::Percentage(offset));
|
|
|
|
}
|
|
|
|
(number, Offset::Absolute(offset))
|
2024-06-27 04:09:33 -04:00
|
|
|
}
|
2024-06-13 11:02:28 -04:00
|
|
|
|
2024-07-09 13:08:04 -04:00
|
|
|
fn extract_number_vector_and_offset(unit: &str, expectation: &str) -> (Vec<f64>, Offset) {
|
2024-07-09 14:04:25 -04:00
|
|
|
let expectation = expectation.replace(',', "");
|
2024-06-27 04:09:33 -04:00
|
|
|
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::<f64>()
|
|
|
|
.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()
|
2024-07-09 14:04:25 -04:00
|
|
|
.unwrap_or_else(|_| panic!("{} needs to specify a number", tokens[1])),
|
2024-06-27 04:09:33 -04:00
|
|
|
// _ => unreachable!("expectations can't be parsed"),
|
|
|
|
};
|
2024-07-09 14:04:25 -04:00
|
|
|
if expectation.ends_with('%') {
|
2024-07-09 13:08:04 -04:00
|
|
|
return (numbers, Offset::Percentage(offset.into()));
|
|
|
|
}
|
|
|
|
(numbers, Offset::Absolute(offset.into()))
|
2024-06-27 04:09:33 -04:00
|
|
|
}
|
|
|
|
|
2024-07-04 08:48:58 -04:00
|
|
|
pub enum WaypointsOrLocation {
|
2024-06-27 04:09:33 -04:00
|
|
|
Waypoints,
|
|
|
|
Locations,
|
2024-07-04 08:48:58 -04:00
|
|
|
Undefined,
|
2024-06-27 04:09:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_location_specification(test_case: &HashMap<String, String>) -> WaypointsOrLocation {
|
|
|
|
assert!(
|
2024-07-04 08:48:58 -04:00
|
|
|
test_case.contains_key("request") ||
|
2024-06-27 04:09:33 -04:00
|
|
|
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"
|
|
|
|
);
|
|
|
|
|
2024-07-04 08:48:58 -04:00
|
|
|
if test_case.contains_key("request") {
|
|
|
|
return WaypointsOrLocation::Undefined;
|
|
|
|
}
|
|
|
|
|
2024-06-27 04:09:33 -04:00
|
|
|
if test_case.contains_key("from")
|
|
|
|
&& test_case.contains_key("to")
|
|
|
|
&& !test_case.contains_key("waypoints")
|
|
|
|
{
|
|
|
|
return WaypointsOrLocation::Locations;
|
2024-06-13 11:02:28 -04:00
|
|
|
}
|
|
|
|
|
2024-06-27 04:09:33 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-06-28 07:04:04 -04:00
|
|
|
#[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());
|
|
|
|
}
|
|
|
|
|
2024-06-27 04:09:33 -04:00
|
|
|
#[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 {
|
2024-07-09 14:04:25 -04:00
|
|
|
let waypoints = match get_location_specification(test_case) {
|
2024-06-27 04:09:33 -04:00
|
|
|
WaypointsOrLocation::Waypoints => {
|
|
|
|
let locations: Vec<Location> = test_case
|
|
|
|
.get("waypoints")
|
|
|
|
.expect("locations specified as waypoints")
|
2024-07-09 14:04:25 -04:00
|
|
|
.split(',')
|
2024-06-27 04:09:33 -04:00
|
|
|
.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]
|
2024-07-09 13:08:04 -04:00
|
|
|
}
|
2024-07-04 08:48:58 -04:00
|
|
|
WaypointsOrLocation::Undefined => {
|
|
|
|
world.request_string = test_case.get("request").cloned();
|
|
|
|
// println!("setting request to: {:?}", world.request_string);
|
|
|
|
vec![]
|
2024-06-27 04:09:33 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-06-28 07:04:04 -04:00
|
|
|
if let Some(bearings) = test_case.get("bearings").cloned() {
|
|
|
|
// TODO: change test cases to provide proper query options
|
|
|
|
world
|
|
|
|
.query_options
|
2024-07-09 14:04:25 -04:00
|
|
|
.insert("bearings".into(), bearings.replace(' ', ";"));
|
2024-06-27 04:09:33 -04:00
|
|
|
}
|
|
|
|
|
2024-07-04 08:48:58 -04:00
|
|
|
let route_result = world.route(&waypoints);
|
2024-06-27 04:09:33 -04:00
|
|
|
test_case
|
|
|
|
.iter()
|
|
|
|
.map(|(column_title, expectation)| (column_title.as_str(), expectation.as_str()))
|
|
|
|
.for_each(|(case, expectation)| match case {
|
2024-07-04 08:48:58 -04:00
|
|
|
"from" | "to" | "bearings" | "request" | "waypoints" | "#" => {}, // ignore input and comment columns
|
2024-06-27 04:09:33 -04:00
|
|
|
"route" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let response = match route_result.as_ref() {
|
|
|
|
Ok((_, response)) => response,
|
|
|
|
Err(_) => &RouteResponse::default(),
|
|
|
|
};
|
2024-06-27 04:09:33 -04:00
|
|
|
let route = if expectation.is_empty() {
|
2024-07-09 13:08:04 -04:00
|
|
|
assert!(route_result.is_err());
|
2024-06-27 04:09:33 -04:00
|
|
|
assert!(response.routes.is_empty());
|
|
|
|
String::new()
|
|
|
|
} else {
|
|
|
|
response
|
|
|
|
.routes
|
|
|
|
.first()
|
2024-07-09 13:08:04 -04:00
|
|
|
.expect("no route returned when checking 'route' column")
|
2024-06-27 04:09:33 -04:00
|
|
|
.legs
|
|
|
|
.iter()
|
|
|
|
.map(|leg| {
|
|
|
|
leg.steps
|
|
|
|
.iter()
|
|
|
|
.map(|step| step.name.clone())
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join(",")
|
|
|
|
}).collect::<Vec<String>>()
|
|
|
|
.join(",")
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
assert_eq!(expectation, route);
|
|
|
|
},
|
|
|
|
"pronunciations" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-27 04:09:33 -04:00
|
|
|
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::<Vec<String>>()
|
|
|
|
.join(",");
|
|
|
|
assert_eq!(expectation, pronunciations);
|
|
|
|
},
|
|
|
|
"ref" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-27 04:09:33 -04:00
|
|
|
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::<Vec<String>>()
|
|
|
|
.join(",");
|
|
|
|
assert_eq!(expectation, refs);
|
|
|
|
},
|
|
|
|
"speed" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-27 04:09:33 -04:00
|
|
|
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!(
|
2024-07-09 13:08:04 -04:00
|
|
|
approximate_within_range(actual_speed, expected_speed, &offset),
|
2024-06-27 04:09:33 -04:00
|
|
|
"actual time {actual_speed} not equal to expected value {expected_speed}"
|
|
|
|
);
|
|
|
|
},
|
|
|
|
"modes" => {
|
2024-07-09 13:08:04 -04:00
|
|
|
let actual = match &route_result {
|
|
|
|
Ok((_, response)) => {
|
|
|
|
let route = response.routes.first().expect("no route returned");
|
|
|
|
let actual_modes = route
|
|
|
|
.legs
|
2024-06-27 04:09:33 -04:00
|
|
|
.iter()
|
2024-07-09 13:08:04 -04:00
|
|
|
.map(|leg| {
|
|
|
|
leg.steps
|
|
|
|
.iter()
|
|
|
|
.map(|step| step.mode.clone())
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join(",")
|
|
|
|
})
|
2024-06-27 04:09:33 -04:00
|
|
|
.collect::<Vec<String>>()
|
2024-07-09 13:08:04 -04:00
|
|
|
.join(",");
|
|
|
|
actual_modes
|
|
|
|
},
|
|
|
|
Err(_) => String::new(),
|
|
|
|
};
|
|
|
|
|
|
|
|
assert_eq!(actual, expectation);
|
2024-06-27 04:09:33 -04:00
|
|
|
},
|
|
|
|
"turns" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-27 04:09:33 -04:00
|
|
|
let route = response.routes.first().expect("no route returned");
|
|
|
|
let actual_turns = route
|
|
|
|
.legs
|
|
|
|
.iter()
|
|
|
|
.map(|leg| {
|
2024-06-28 07:04:04 -04:00
|
|
|
leg.steps
|
2024-06-27 04:09:33 -04:00
|
|
|
.iter()
|
|
|
|
.map(|step| {
|
2024-07-09 13:08:04 -04:00
|
|
|
// 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(),
|
|
|
|
_ => "",
|
2024-06-27 04:09:33 -04:00
|
|
|
};
|
2024-07-09 13:08:04 -04:00
|
|
|
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),
|
|
|
|
}
|
2024-06-27 04:09:33 -04:00
|
|
|
})
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join(",")
|
|
|
|
})
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join(",");
|
|
|
|
assert_eq!(actual_turns, expectation);
|
|
|
|
},
|
|
|
|
"time" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-27 04:09:33 -04:00
|
|
|
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!(
|
2024-07-09 13:08:04 -04:00
|
|
|
approximate_within_range(actual_time, expected_time, &offset),
|
2024-06-27 04:09:33 -04:00
|
|
|
"actual time {actual_time} not equal to expected value {expected_time}"
|
|
|
|
);
|
|
|
|
},
|
|
|
|
"times" => {
|
|
|
|
// TODO: go over steps
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-07-09 14:04:25 -04:00
|
|
|
let actual_times : Vec<f64>= response.routes.first().expect("no route returned").legs.iter().flat_map(|leg| {
|
2024-06-28 07:04:04 -04:00
|
|
|
leg.steps.iter().filter(|step| step.duration > 0.).map(|step| step.duration).collect::<Vec<f64>>()
|
2024-07-09 14:04:25 -04:00
|
|
|
}).collect();
|
2024-06-27 04:09:33 -04:00
|
|
|
let (expected_times, offset) = extract_number_vector_and_offset("s", expectation);
|
2024-07-09 13:08:04 -04:00
|
|
|
assert_eq!(actual_times.len(), expected_times.len(), "times mismatch: {actual_times:?} != {expected_times:?} +- {offset:?}");
|
2024-06-27 04:09:33 -04:00
|
|
|
|
|
|
|
zip(actual_times, expected_times).for_each(|(actual_time, expected_time)| {
|
2024-07-09 13:08:04 -04:00
|
|
|
assert!(approximate_within_range(actual_time, expected_time, &offset),
|
2024-06-27 04:09:33 -04:00
|
|
|
"actual time {actual_time} not equal to expected value {expected_time}");
|
|
|
|
});
|
|
|
|
},
|
|
|
|
"distances" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-07-09 14:04:25 -04:00
|
|
|
let actual_distances = response.routes.first().expect("no route returned").legs.iter().flat_map(|leg| {
|
2024-06-28 07:04:04 -04:00
|
|
|
leg.steps.iter().filter(|step| step.distance > 0.).map(|step| step.distance).collect::<Vec<f64>>()
|
2024-07-09 14:04:25 -04:00
|
|
|
}).collect::<Vec<f64>>();
|
|
|
|
let (expected_distances, offset) = extract_number_vector_and_offset("m", expectation);
|
2024-07-09 13:08:04 -04:00
|
|
|
assert_eq!(expected_distances.len(), actual_distances.len(), "distances mismatch {expected_distances:?} != {actual_distances:?} +- {offset:?}");
|
2024-06-27 04:09:33 -04:00
|
|
|
|
|
|
|
zip(actual_distances, expected_distances).for_each(|(actual_distance, expected_distance)| {
|
2024-07-09 13:08:04 -04:00
|
|
|
assert!(approximate_within_range(actual_distance, expected_distance, &offset),
|
2024-06-27 04:09:33 -04:00
|
|
|
"actual distance {actual_distance} not equal to expected value {expected_distance}");
|
|
|
|
});
|
|
|
|
},
|
|
|
|
"weight" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-27 04:09:33 -04:00
|
|
|
let actual_weight = response.routes.first().expect("no route returned").weight;
|
|
|
|
let (expected_weight, offset) = extract_number_and_offset("s", expectation);
|
|
|
|
assert!(
|
2024-07-09 13:08:04 -04:00
|
|
|
approximate_within_range(
|
2024-06-27 04:09:33 -04:00
|
|
|
actual_weight,
|
|
|
|
expected_weight,
|
2024-07-09 13:08:04 -04:00
|
|
|
&offset
|
2024-06-27 04:09:33 -04:00
|
|
|
),
|
2024-07-09 13:08:04 -04:00
|
|
|
"actual weight {actual_weight} not equal to expected value {expected_weight}"
|
2024-06-27 04:09:33 -04:00
|
|
|
);
|
|
|
|
},
|
|
|
|
"distance" => {
|
2024-07-09 13:08:04 -04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
};
|
2024-06-27 04:09:33 -04:00
|
|
|
},
|
2024-06-28 07:04:04 -04:00
|
|
|
"summary" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-28 07:04:04 -04:00
|
|
|
let actual_summary = response.routes.first().expect("no route returned").legs.iter().map(|leg| {
|
|
|
|
leg.summary.clone()
|
|
|
|
}).collect::<Vec<String>>().join(",");
|
|
|
|
assert_eq!(actual_summary,expectation, "summary mismatch");
|
|
|
|
},
|
|
|
|
"data_version" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-28 07:04:04 -04:00
|
|
|
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" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-28 07:04:04 -04:00
|
|
|
let expected_waypoint_count = match test_case.get("waypoints_count") {
|
|
|
|
Some(s) if !s.is_empty() => s.parse::<usize>().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" => {
|
2024-07-04 08:48:58 -04:00
|
|
|
let (_, response) = route_result.as_ref().expect("osrm-routed returned an unexpected error");
|
2024-06-28 07:04:04 -04:00
|
|
|
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"),
|
|
|
|
}
|
2024-07-04 08:48:58 -04:00
|
|
|
},
|
|
|
|
"status" => {
|
|
|
|
let actual_status = match route_result {
|
|
|
|
Ok((code, _)) => code,
|
|
|
|
Err((code, _)) => code,
|
|
|
|
};
|
2024-06-28 07:04:04 -04:00
|
|
|
|
2024-07-04 08:48:58 -04:00
|
|
|
let expected_status = test_case.get("status").expect("no status found").parse::<u16>().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:?}");
|
2024-06-28 07:04:04 -04:00
|
|
|
|
|
|
|
},
|
2024-07-09 13:08:04 -04:00
|
|
|
"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::<Vec<String>>().join(",")
|
|
|
|
}).collect::<Vec<_>>().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::<Vec<String>>()
|
|
|
|
.join(" ")
|
|
|
|
})
|
|
|
|
.collect::<Vec<String>>()
|
|
|
|
.join(";");
|
|
|
|
actual_lanes
|
|
|
|
},
|
|
|
|
Err(_) => String::new(),
|
|
|
|
};
|
|
|
|
|
|
|
|
assert_eq!(actual, expectation);
|
2024-07-12 13:13:16 -04:00
|
|
|
},
|
|
|
|
"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::<Vec<_>>().join(",") + "]"
|
|
|
|
}).collect::<Vec<_>>().join(",");
|
|
|
|
|
|
|
|
let expected_classes = test_case.get("classes").expect("test case classes not found");
|
|
|
|
assert_eq!(&actual_classes, expected_classes, "classes don't match");
|
2024-07-09 13:08:04 -04:00
|
|
|
}
|
2024-06-27 04:09:33 -04:00
|
|
|
// TODO: more checks need to be implemented
|
|
|
|
_ => {
|
|
|
|
let msg = format!("case {case} = {expectation} not implemented");
|
|
|
|
unimplemented!("{msg}");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-06-13 11:02:28 -04:00
|
|
|
}
|
2024-05-30 08:35:52 -04:00
|
|
|
fn main() {
|
2024-05-30 10:14:58 -04:00
|
|
|
let args = Args::parse();
|
2024-06-28 07:04:04 -04:00
|
|
|
debug!("{args:?}");
|
2024-06-27 04:09:33 -04:00
|
|
|
let digest = md5_of_osrm_executables().digest().to_hex_lowercase();
|
|
|
|
|
2024-05-30 08:35:52 -04:00
|
|
|
futures::executor::block_on(
|
|
|
|
OSRMWorld::cucumber()
|
2024-05-31 03:43:46 -04:00
|
|
|
.max_concurrent_scenarios(1)
|
2024-05-30 12:23:03 -04:00
|
|
|
.before(move |feature, _rule, scenario, world| {
|
2024-06-02 09:17:12 -04:00
|
|
|
world.scenario_id = common::scenario_id::scenario_id(scenario);
|
2024-05-30 12:23:03 -04:00
|
|
|
world.set_scenario_specific_paths_and_digests(feature.path.clone());
|
2024-07-09 14:04:25 -04:00
|
|
|
world.osrm_digest.clone_from(&digest);
|
2024-05-30 08:35:52 -04:00
|
|
|
|
2024-06-05 04:31:07 -04:00
|
|
|
// TODO: clean up cache if needed? Or do in scenarios?
|
2024-05-30 08:35:52 -04:00
|
|
|
|
|
|
|
future::ready(()).boxed()
|
|
|
|
})
|
2024-06-27 04:09:33 -04:00
|
|
|
// .with_writer(DotWriter::default().normalized())
|
2024-07-12 13:13:16 -04:00
|
|
|
.filter_run("features", |fe, r, sc| {
|
2024-07-09 14:04:25 -04:00
|
|
|
// .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))
|
2024-07-04 08:48:58 -04:00
|
|
|
}),
|
2024-05-30 08:35:52 -04:00
|
|
|
);
|
|
|
|
}
|