Implement more features
This commit is contained in:
@@ -2,3 +2,16 @@ pub fn approx_equal(a: f32, b: f32, dp: u8) -> bool {
|
||||
let p = 10f32.powi(-(dp as i32));
|
||||
(a - b).abs() < p
|
||||
}
|
||||
|
||||
pub fn aprox_equal_within_percentage_range(actual: f64, expectation: f64, percentage: u8) -> bool {
|
||||
assert!(percentage <= 100);
|
||||
let factor = 0.01 * percentage as f64;
|
||||
actual >= expectation - (factor * expectation) && actual <= expectation + (factor * expectation)
|
||||
}
|
||||
|
||||
pub fn approx_equal_within_offset_range(actual: f64, expectation: f64, offset: f64) -> bool {
|
||||
assert!(offset >= 0., "offset must be positive");
|
||||
actual >= expectation - offset && actual <= expectation + offset
|
||||
}
|
||||
|
||||
// TODO: test coverage
|
||||
|
||||
@@ -125,7 +125,7 @@ mod tests {
|
||||
let actual = osm_db.to_xml();
|
||||
let expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<osm generator=\"osrm-test\" version=\"0.6\">\n\t<node id=\"123\" version=\"1.0\" user=\"osrm\" timestamp=\"2000-01-01T00:00:00Z\" lon=\"8.9876\" lat=\"50.1234\">\n\t\t<tag name=\"a\" />\n\t</node>\n\t<node id=\"321\" version=\"1.0\" user=\"osrm\" timestamp=\"2000-01-01T00:00:00Z\" lon=\"8.9876\" lat=\"50.1234\">\n\t\t<tag name=\"b\" />\n\t</node>\n\t<way id=\"890\" version=\"1\" uid=\"1\" user=\"osrm\" timestamp=\"2000-01-01T00:00:00Z\">\n\t\t<nd ref=\"123\" />\n\t\t<nd ref=\"321\" />\n\t</way>\n</osm>\n";
|
||||
|
||||
println!("{actual}");
|
||||
// println!("{actual}");
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-17
@@ -18,6 +18,7 @@ const DEFAULT_ORIGIN: Location = Location {
|
||||
latitude: 1.0f32,
|
||||
};
|
||||
const DEFAULT_GRID_SIZE: f32 = 100.;
|
||||
const WAY_SPACING: f32 = 100.;
|
||||
|
||||
#[derive(Debug, World)]
|
||||
pub struct OSRMWorld {
|
||||
@@ -35,9 +36,12 @@ pub struct OSRMWorld {
|
||||
pub extraction_parameters: Vec<String>,
|
||||
|
||||
pub request_with_flatbuffers: bool,
|
||||
pub bearings: Option<String>,
|
||||
|
||||
pub grid_size: f32,
|
||||
pub origin: Location,
|
||||
pub way_spacing: f32,
|
||||
|
||||
task: LocalTask,
|
||||
agent: ureq::Agent,
|
||||
}
|
||||
@@ -56,8 +60,10 @@ impl Default for OSRMWorld {
|
||||
osm_db: Default::default(),
|
||||
extraction_parameters: Default::default(),
|
||||
request_with_flatbuffers: Default::default(),
|
||||
bearings: None,
|
||||
grid_size: DEFAULT_GRID_SIZE,
|
||||
origin: DEFAULT_ORIGIN,
|
||||
way_spacing: WAY_SPACING,
|
||||
task: LocalTask::default(),
|
||||
agent: ureq::AgentBuilder::new()
|
||||
.timeout_read(Duration::from_secs(5))
|
||||
@@ -212,7 +218,7 @@ impl OSRMWorld {
|
||||
pub fn nearest(
|
||||
&mut self,
|
||||
query_location: &Location,
|
||||
request_with_flatbuffers: bool,
|
||||
// request_with_flatbuffers: bool,
|
||||
) -> NearestResponse {
|
||||
self.start_routed();
|
||||
|
||||
@@ -220,7 +226,7 @@ impl OSRMWorld {
|
||||
"http://localhost:5000/nearest/v1/{}/{:?},{:?}",
|
||||
self.profile, query_location.longitude, query_location.latitude
|
||||
);
|
||||
if request_with_flatbuffers {
|
||||
if self.request_with_flatbuffers {
|
||||
url += ".flatbuffers";
|
||||
}
|
||||
let call = self.agent.get(&url).call();
|
||||
@@ -230,42 +236,43 @@ impl OSRMWorld {
|
||||
Err(e) => panic!("http error: {e}"),
|
||||
};
|
||||
|
||||
let response = match request_with_flatbuffers {
|
||||
let response = match self.request_with_flatbuffers {
|
||||
true => NearestResponse::from_flatbuffer(body),
|
||||
false => NearestResponse::from_json_reader(body),
|
||||
};
|
||||
response
|
||||
}
|
||||
|
||||
pub fn route(
|
||||
&mut self,
|
||||
from_location: &Location,
|
||||
to_location: &Location,
|
||||
request_with_flatbuffers: bool,
|
||||
) -> RouteResponse {
|
||||
pub fn route(&mut self, waypoints: &[Location]) -> RouteResponse {
|
||||
self.start_routed();
|
||||
|
||||
let waypoint_string = waypoints
|
||||
.iter()
|
||||
.map(|location| format!("{:?},{:?}", location.longitude, location.latitude))
|
||||
.collect::<Vec<String>>()
|
||||
.join(";");
|
||||
|
||||
let mut url = format!(
|
||||
"http://localhost:5000/route/v1/{}/{:?},{:?};{:?},{:?}?steps=true&alternatives=false",
|
||||
"http://localhost:5000/route/v1/{}/{waypoint_string}?steps=true&alternatives=false",
|
||||
self.profile,
|
||||
from_location.longitude,
|
||||
from_location.latitude,
|
||||
to_location.longitude,
|
||||
to_location.latitude,
|
||||
);
|
||||
if request_with_flatbuffers {
|
||||
if self.request_with_flatbuffers {
|
||||
url += ".flatbuffers";
|
||||
}
|
||||
if let Some(bearings) = &self.bearings {
|
||||
url += "&bearings=";
|
||||
url += bearings;
|
||||
}
|
||||
// println!("url: {url}");
|
||||
let call = self.agent.get(&url).call();
|
||||
|
||||
let body = match call {
|
||||
Ok(response) => response.into_reader(),
|
||||
Err(e) => panic!("http error: {e}"),
|
||||
Err(_e) => return RouteResponse::default(),
|
||||
};
|
||||
|
||||
let text = std::io::read_to_string(body).unwrap();
|
||||
let response = match request_with_flatbuffers {
|
||||
let response = match self.request_with_flatbuffers {
|
||||
true => unimplemented!("RouteResponse::from_flatbuffer(body)"),
|
||||
false => RouteResponse::from_string(&text),
|
||||
};
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::nearest_response::Waypoint;
|
||||
use super::{location::Location, nearest_response::Waypoint};
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
pub struct Maneuver {
|
||||
pub bearing_after: f64,
|
||||
pub bearing_before: f64,
|
||||
pub location: Location,
|
||||
pub modifier: Option<String>, // TODO: should be an enum
|
||||
pub r#type: String, // TODO: should be an enum
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default, Debug)]
|
||||
pub struct Step {
|
||||
pub geometry: String,
|
||||
pub mode: String,
|
||||
pub maneuver: Maneuver,
|
||||
pub name: String,
|
||||
pub pronunciation: Option<String>,
|
||||
pub r#ref: Option<String>,
|
||||
pub duration: f64,
|
||||
pub distance: f64,
|
||||
}
|
||||
|
||||
// #[derive(Deserialize, Debug)]
|
||||
@@ -34,7 +48,7 @@ pub struct Route {
|
||||
pub distance: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct RouteResponse {
|
||||
pub code: String,
|
||||
pub routes: Vec<Route>,
|
||||
@@ -43,13 +57,13 @@ pub struct RouteResponse {
|
||||
}
|
||||
|
||||
impl RouteResponse {
|
||||
// pub fn from_json_reader(reader: impl std::io::Read) -> Self {
|
||||
// let response = match serde_json::from_reader::<_, Self>(reader) {
|
||||
// Ok(response) => response,
|
||||
// Err(e) => panic!("parsing error {e}"),
|
||||
// };
|
||||
// response
|
||||
// }
|
||||
pub fn from_json_reader(reader: impl std::io::Read) -> Self {
|
||||
let response = match serde_json::from_reader::<_, Self>(reader) {
|
||||
Ok(response) => response,
|
||||
Err(e) => panic!("parsing error {e}"),
|
||||
};
|
||||
response
|
||||
}
|
||||
|
||||
pub fn from_string(input: &str) -> Self {
|
||||
// println!("{input}");
|
||||
|
||||
+466
-105
@@ -3,18 +3,28 @@ mod common;
|
||||
use cheap_ruler::CheapRuler;
|
||||
use clap::Parser;
|
||||
use common::{
|
||||
cli_arguments::Args, dot_writer::DotWriter, f64_utils::approx_equal,
|
||||
hash_util::md5_of_osrm_executables, location::Location, osm::OSMWay, osrm_world::OSRMWorld,
|
||||
cli_arguments::Args,
|
||||
dot_writer::DotWriter,
|
||||
f64_utils::{
|
||||
approx_equal, approx_equal_within_offset_range, aprox_equal_within_percentage_range,
|
||||
},
|
||||
hash_util::md5_of_osrm_executables,
|
||||
location::Location,
|
||||
osm::OSMWay,
|
||||
osrm_world::OSRMWorld,
|
||||
};
|
||||
use core::panic;
|
||||
use cucumber::{
|
||||
gherkin::{Step, Table},
|
||||
given, when, World, WriterExt,
|
||||
given, then, when, World, WriterExt,
|
||||
};
|
||||
use futures::{future, FutureExt};
|
||||
use geo_types::Point;
|
||||
use log::debug;
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
iter::zip,
|
||||
};
|
||||
|
||||
fn offset_origin_by(dx: f32, dy: f32, origin: Location, grid_size: f32) -> Location {
|
||||
let ruler = CheapRuler::new(origin.latitude, cheap_ruler::DistanceUnit::Meters);
|
||||
@@ -42,10 +52,10 @@ fn set_profile(world: &mut OSRMWorld, profile: String) {
|
||||
fn set_node_locations(world: &mut OSRMWorld, step: &Step) {
|
||||
let table = step.table().expect("cannot get table");
|
||||
let header = table.rows.first().expect("node locations table empty");
|
||||
assert_eq!(header.len(), 3, "header needs to define three columns");
|
||||
assert_eq!(
|
||||
header[0], "node",
|
||||
"first column needs to be 'node' indicating the one-letter name"
|
||||
assert!(header.len() >= 3, "header needs to define three columns");
|
||||
assert!(
|
||||
header.contains(&"node".to_string()),
|
||||
"a column needs to be 'node' indicating the one-letter name"
|
||||
);
|
||||
// the following lookup allows to define lat lon columns in any order
|
||||
let header_lookup: HashMap<&str, usize> = header
|
||||
@@ -61,7 +71,10 @@ fn set_node_locations(world: &mut OSRMWorld, step: &Step) {
|
||||
});
|
||||
|
||||
table.rows.iter().skip(1).for_each(|row| {
|
||||
assert_eq!(3, row.len());
|
||||
assert!(
|
||||
row.len() >= 3,
|
||||
"nod locations must at least specify three tables: node, lat, and lon"
|
||||
);
|
||||
assert_eq!(row[0].len(), 1, "node name not in [0..9][a..z]");
|
||||
let name = &row[0].chars().next().expect("node name cannot be empty"); // the error is unreachable
|
||||
let lon = &row[header_lookup["lon"]];
|
||||
@@ -70,9 +83,18 @@ fn set_node_locations(world: &mut OSRMWorld, step: &Step) {
|
||||
latitude: lat.parse::<f32>().expect("lat {lat} needs to be a f64"),
|
||||
longitude: lon.parse::<f32>().expect("lon {lon} needs to be a f64"),
|
||||
};
|
||||
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,
|
||||
};
|
||||
match name {
|
||||
'0'...'9' => world.add_location(*name, location),
|
||||
'a'...'z' => world.add_osm_node(*name, location, None),
|
||||
'a'...'z' => world.add_osm_node(*name, location, id),
|
||||
_ => unreachable!("node name not in [0..9][a..z]"),
|
||||
}
|
||||
});
|
||||
@@ -100,7 +122,8 @@ fn set_node_map(world: &mut OSRMWorld, step: &Step) {
|
||||
match name {
|
||||
'0'...'9' => world.add_location(name, location),
|
||||
'a'...'z' => world.add_osm_node(name, location, None),
|
||||
_ => unreachable!("node name not in [0..9][a..z]: {docstring}"),
|
||||
_ => {} // TODO: unreachable!("node name not in [0..9][a..z]: {docstring}"),
|
||||
// tests contain random characters.
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -166,7 +189,7 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_table_from_steps(table: &Option<&Table>) -> Vec<HashMap<String, String>> {
|
||||
fn parse_table_from_steps(table: &Option<&Table>) -> (Vec<String>, Vec<HashMap<String, String>>) {
|
||||
// parse query data
|
||||
let table = table.expect("no query table specified");
|
||||
// the following lookup allows to define lat lon columns in any order
|
||||
@@ -187,19 +210,18 @@ fn parse_table_from_steps(table: &Option<&Table>) -> Vec<HashMap<String, String>
|
||||
row_map
|
||||
})
|
||||
.collect();
|
||||
test_cases
|
||||
// TODO: also return the header
|
||||
(header.clone(), test_cases)
|
||||
}
|
||||
|
||||
#[when(regex = r"^I request nearest( with flatbuffers|) I should get$")]
|
||||
fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) {
|
||||
let request_with_flatbuffers = state == " with flatbuffers";
|
||||
world.request_with_flatbuffers = state == " with flatbuffers";
|
||||
|
||||
world.write_osm_file();
|
||||
world.extract_osm_file();
|
||||
|
||||
// parse query data
|
||||
let test_cases = parse_table_from_steps(&step.table.as_ref());
|
||||
let (_, test_cases) = parse_table_from_steps(&step.table.as_ref());
|
||||
|
||||
// run test cases
|
||||
for test_case in &test_cases {
|
||||
@@ -212,7 +234,7 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) {
|
||||
.expect("node name is one char long"),
|
||||
);
|
||||
|
||||
let response = world.nearest(&query_location, request_with_flatbuffers);
|
||||
let response = world.nearest(&query_location);
|
||||
|
||||
let expected_location = &world.get_location(
|
||||
test_case
|
||||
@@ -246,117 +268,456 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) {
|
||||
}
|
||||
}
|
||||
|
||||
#[when(regex = r"^I route( with flatbuffers|) I should get$")]
|
||||
fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
|
||||
let request_with_flatbuffers = state == " with flatbuffers";
|
||||
#[then(expr = "routability should be")]
|
||||
fn routability(world: &mut OSRMWorld, step: &Step) {
|
||||
world.write_osm_file();
|
||||
world.extract_osm_file();
|
||||
// TODO: preprocess
|
||||
|
||||
let test_cases = parse_table_from_steps(&step.table.as_ref());
|
||||
for test_case in &test_cases {
|
||||
let from_location = world.get_location(
|
||||
test_case
|
||||
.get("from")
|
||||
.expect("node name is one char long")
|
||||
.chars()
|
||||
.next()
|
||||
.expect("node name is one char long"),
|
||||
let (header, test_cases) = parse_table_from_steps(&step.table.as_ref());
|
||||
// TODO: rename forw/backw to forw/backw_speed
|
||||
let supported_headers = HashSet::<_>::from([
|
||||
"forw",
|
||||
"backw",
|
||||
"bothw",
|
||||
"forw_rate",
|
||||
"backw_rate",
|
||||
"bothw_rate",
|
||||
]);
|
||||
if 0 == header
|
||||
.iter()
|
||||
.filter(|title| supported_headers.contains(title.as_str()))
|
||||
.count()
|
||||
{
|
||||
panic!(
|
||||
r#"*** routability table must contain either "forw", "backw", "bothw", "forw_rate" or "backw_mode" column"#
|
||||
);
|
||||
let to_location = world.get_location(
|
||||
test_case
|
||||
.get("to")
|
||||
.expect("node name is one char long")
|
||||
.chars()
|
||||
.next()
|
||||
.expect("node name is one char long"),
|
||||
);
|
||||
|
||||
let response = world.route(&from_location, &to_location, request_with_flatbuffers);
|
||||
|
||||
if test_case.contains_key("route") {
|
||||
// NOTE: the following code ports logic from JavaScript that checks only properties of the first route
|
||||
let route = response
|
||||
.routes
|
||||
.first()
|
||||
.expect("no route returned")
|
||||
.legs
|
||||
.first()
|
||||
.expect("legs required")
|
||||
.steps
|
||||
.iter()
|
||||
.map(|step| step.name.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
assert_eq!(*test_case.get("route").expect("msg"), route);
|
||||
}
|
||||
|
||||
if test_case.contains_key("pronunciations") {
|
||||
let pronunciations = response
|
||||
.routes
|
||||
.first()
|
||||
.expect("no route returned")
|
||||
.legs
|
||||
.first()
|
||||
.expect("legs required")
|
||||
.steps
|
||||
.iter()
|
||||
.map(|step| match &step.pronunciation {
|
||||
Some(p) => p.clone(),
|
||||
None => "".to_string(),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
assert_eq!(
|
||||
*test_case.get("pronunciations").expect("msg"),
|
||||
pronunciations
|
||||
);
|
||||
}
|
||||
|
||||
if test_case.contains_key("ref") {
|
||||
let refs = response
|
||||
.routes
|
||||
.first()
|
||||
.expect("no route returned")
|
||||
.legs
|
||||
.first()
|
||||
.expect("legs required")
|
||||
.steps
|
||||
.iter()
|
||||
.map(|step| match &step.r#ref {
|
||||
Some(p) => p.clone(),
|
||||
None => "".to_string(),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
assert_eq!(*test_case.get("ref").expect("msg"), refs);
|
||||
}
|
||||
// TODO: more checks need to be implemented
|
||||
|
||||
// TODO: check for unchecked test columns
|
||||
}
|
||||
|
||||
// unimplemented!("route");
|
||||
test_cases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.for_each(|(index, test_case)| {
|
||||
let source = offset_origin_by(
|
||||
1. + world.way_spacing * index as f32,
|
||||
0.,
|
||||
world.origin,
|
||||
world.grid_size,
|
||||
);
|
||||
let target = offset_origin_by(
|
||||
3. + world.way_spacing * index as f32,
|
||||
0.,
|
||||
world.origin,
|
||||
world.grid_size,
|
||||
);
|
||||
test_case
|
||||
.iter()
|
||||
.filter(|(title, _)| supported_headers.contains(title.as_str()))
|
||||
.for_each(|(title, expectation)| {
|
||||
let forward = title.starts_with("forw");
|
||||
// println!("{direction}: >{expectation}<");
|
||||
let response = match forward {
|
||||
true => world.route(&vec![source, target]),
|
||||
false => world.route(&vec![target, source]),
|
||||
};
|
||||
if expectation.is_empty() {
|
||||
// if !response.routes.is_empty() {
|
||||
// println!("> {title} {expectation}");
|
||||
// println!("{response:?}");
|
||||
// }
|
||||
|
||||
assert!(
|
||||
response.routes.is_empty()
|
||||
|| response.routes.first().unwrap().distance == 0.,
|
||||
"no route expected when result column {title} is unset"
|
||||
);
|
||||
} else if expectation.contains("km/h") {
|
||||
assert!(
|
||||
!response.routes.is_empty(),
|
||||
"route expected when result column is set"
|
||||
);
|
||||
|
||||
let (expected_speed, offset) =
|
||||
extract_number_and_offset("km/h", expectation);
|
||||
let route = response.routes.first().unwrap();
|
||||
let actual_speed = route.distance / route.duration * 3.6;
|
||||
assert!(
|
||||
aprox_equal_within_percentage_range(
|
||||
actual_speed,
|
||||
expected_speed,
|
||||
offset
|
||||
),
|
||||
"{actual_speed} and {expected_speed} differ by more than {offset}"
|
||||
);
|
||||
} else if title.ends_with("_rate") {
|
||||
assert!(!response.routes.is_empty());
|
||||
let expected_rate = expectation
|
||||
.parse::<f64>()
|
||||
.expect("rate needs to be a number");
|
||||
let route = response.routes.first().unwrap();
|
||||
let actual_rate = route.distance / route.weight;
|
||||
assert!(
|
||||
aprox_equal_within_percentage_range(actual_rate, expected_rate, 1),
|
||||
"{actual_rate} and {expected_rate} differ by more than 1%"
|
||||
);
|
||||
} else {
|
||||
unimplemented!("{title} = {expectation}");
|
||||
}
|
||||
});
|
||||
});
|
||||
// unimplemented!("{test_cases:#?}");
|
||||
}
|
||||
|
||||
fn extract_number_and_offset(unit: &str, expectation: &str) -> (f64, u8) {
|
||||
let tokens: Vec<_> = expectation
|
||||
.split(unit)
|
||||
.map(|token| token.trim())
|
||||
.filter(|token| !token.is_empty())
|
||||
.collect();
|
||||
// println!("{tokens:?}");
|
||||
let number = tokens[0]
|
||||
.parse::<f64>()
|
||||
.expect("{expectation} needs to define a speed");
|
||||
let offset = match tokens.len() {
|
||||
1 => 5u8, // TODO: the JS fuzzy matcher has a default margin of 5% for absolute comparsions. This is imprecise
|
||||
2 => tokens[1]
|
||||
.replace("+-", "")
|
||||
.trim()
|
||||
.parse()
|
||||
.expect(&format!("{} needs to specify a number", tokens[1])),
|
||||
_ => unreachable!("expectations can't be parsed"),
|
||||
};
|
||||
(number, offset)
|
||||
}
|
||||
|
||||
fn extract_number_vector_and_offset(unit: &str, expectation: &str) -> (Vec<f64>, u8) {
|
||||
let expectation = expectation.replace(",", "");
|
||||
let tokens: Vec<_> = expectation
|
||||
.split(unit)
|
||||
.map(|token| token.trim())
|
||||
.filter(|token| !token.is_empty())
|
||||
.collect();
|
||||
let numbers = tokens
|
||||
.iter()
|
||||
.filter(|token| !token.contains("+-"))
|
||||
.map(|token| {
|
||||
token
|
||||
.parse::<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()
|
||||
.expect(&format!("{} needs to specify a number", tokens[1])),
|
||||
// _ => unreachable!("expectations can't be parsed"),
|
||||
};
|
||||
(numbers, offset)
|
||||
}
|
||||
|
||||
enum WaypointsOrLocation {
|
||||
Waypoints,
|
||||
Locations,
|
||||
// Undefined,
|
||||
}
|
||||
|
||||
pub fn get_location_specification(test_case: &HashMap<String, String>) -> WaypointsOrLocation {
|
||||
assert!(
|
||||
test_case.contains_key("from")
|
||||
&& test_case.contains_key("to")
|
||||
&& !test_case.contains_key("waypoints")
|
||||
|| !test_case.contains_key("from")
|
||||
&& !test_case.contains_key("to")
|
||||
&& test_case.contains_key("waypoints"),
|
||||
"waypoints need to be specified by either from/to columns or a waypoint column, but not both"
|
||||
);
|
||||
|
||||
if test_case.contains_key("from")
|
||||
&& test_case.contains_key("to")
|
||||
&& !test_case.contains_key("waypoints")
|
||||
{
|
||||
return WaypointsOrLocation::Locations;
|
||||
}
|
||||
|
||||
if !test_case.contains_key("from")
|
||||
&& !test_case.contains_key("to")
|
||||
&& test_case.contains_key("waypoints")
|
||||
{
|
||||
return WaypointsOrLocation::Waypoints;
|
||||
}
|
||||
unreachable!("waypoints need to be specified by either from/to columns or a waypoint column, but not both");
|
||||
// WaypointsOrLocation::Undefined
|
||||
}
|
||||
|
||||
#[when(regex = r"^I route( with flatbuffers|) I should get$")]
|
||||
fn request_route(world: &mut OSRMWorld, step: &Step, state: String) {
|
||||
world.request_with_flatbuffers = state == " with flatbuffers";
|
||||
world.write_osm_file();
|
||||
world.extract_osm_file();
|
||||
// TODO: preprocess
|
||||
|
||||
let (_, test_cases) = parse_table_from_steps(&step.table.as_ref());
|
||||
for test_case in &test_cases {
|
||||
let waypoints = match get_location_specification(&test_case) {
|
||||
WaypointsOrLocation::Waypoints => {
|
||||
let locations: Vec<Location> = test_case
|
||||
.get("waypoints")
|
||||
.expect("locations specified as waypoints")
|
||||
.split(",")
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
assert!(name.len() == 1, "node names need to be of length one");
|
||||
world.get_location(name.chars().next().unwrap())
|
||||
})
|
||||
.collect();
|
||||
locations
|
||||
}
|
||||
WaypointsOrLocation::Locations => {
|
||||
let from_location = world.get_location(
|
||||
test_case
|
||||
.get("from")
|
||||
.expect("test case doesn't have a 'from' column")
|
||||
.chars()
|
||||
.next()
|
||||
.expect("from node name is one char long"),
|
||||
);
|
||||
let to_location = world.get_location(
|
||||
test_case
|
||||
.get("to")
|
||||
.expect("test case doesn't have a 'to' column")
|
||||
.chars()
|
||||
.next()
|
||||
.expect("to node name is one char long"),
|
||||
);
|
||||
vec![from_location, to_location]
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(bearing) = test_case.get("bearings").cloned() {
|
||||
world.bearings = Some(bearing.replace(" ", ";"));
|
||||
}
|
||||
|
||||
let response = world.route(&waypoints);
|
||||
|
||||
test_case
|
||||
.iter()
|
||||
.map(|(column_title, expectation)| (column_title.as_str(), expectation.as_str()))
|
||||
.for_each(|(case, expectation)| match case {
|
||||
"from" | "to" | "bearings"=> {}, // ignore input columns
|
||||
"route" => {
|
||||
let route = if expectation.is_empty() {
|
||||
assert!(response.routes.is_empty());
|
||||
String::new()
|
||||
} else {
|
||||
response
|
||||
.routes
|
||||
.first()
|
||||
.expect("no route returned")
|
||||
.legs
|
||||
.iter()
|
||||
.map(|leg| {
|
||||
leg.steps
|
||||
.iter()
|
||||
.map(|step| step.name.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
}).collect::<Vec<String>>()
|
||||
.join(",")
|
||||
|
||||
};
|
||||
|
||||
assert_eq!(expectation, route);
|
||||
},
|
||||
"pronunciations" => {
|
||||
let pronunciations = response
|
||||
.routes
|
||||
.first()
|
||||
.expect("no route returned")
|
||||
.legs
|
||||
.first()
|
||||
.expect("legs required")
|
||||
.steps
|
||||
.iter()
|
||||
.map(|step| match &step.pronunciation {
|
||||
Some(p) => p.clone(),
|
||||
None => "".to_string(),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
assert_eq!(expectation, pronunciations);
|
||||
},
|
||||
"ref" => {
|
||||
let refs = response
|
||||
.routes
|
||||
.first()
|
||||
.expect("no route returned")
|
||||
.legs
|
||||
.first()
|
||||
.expect("legs required")
|
||||
.steps
|
||||
.iter()
|
||||
.map(|step| match &step.r#ref {
|
||||
Some(p) => p.clone(),
|
||||
None => "".to_string(),
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
assert_eq!(expectation, refs);
|
||||
},
|
||||
"speed" => {
|
||||
let route = response.routes.first().expect("no route returned");
|
||||
let actual_speed = route.distance / route.duration * 3.6;
|
||||
let (expected_speed, offset) = extract_number_and_offset("km/h", expectation);
|
||||
// println!("{actual_speed} == {expected_speed} +- {offset}");
|
||||
assert!(
|
||||
aprox_equal_within_percentage_range(actual_speed, expected_speed, offset),
|
||||
"actual time {actual_speed} not equal to expected value {expected_speed}"
|
||||
);
|
||||
},
|
||||
"modes" => {
|
||||
let route = response.routes.first().expect("no route returned");
|
||||
let actual_modes = route
|
||||
.legs
|
||||
.iter()
|
||||
.map(|leg| {
|
||||
leg.steps
|
||||
.iter()
|
||||
.map(|step| step.mode.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
assert_eq!(actual_modes, expectation);
|
||||
},
|
||||
"turns" => {
|
||||
let route = response.routes.first().expect("no route returned");
|
||||
let actual_turns = route
|
||||
.legs
|
||||
.iter()
|
||||
.map(|leg| {
|
||||
leg.steps
|
||||
.iter()
|
||||
.map(|step| {
|
||||
let prefix = step.maneuver.r#type.clone();
|
||||
let suffix = match &step.maneuver.modifier {
|
||||
Some(modifier) => " ".to_string() + &modifier,
|
||||
None => "".into(),
|
||||
};
|
||||
prefix + &suffix
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
assert_eq!(actual_turns, expectation);
|
||||
},
|
||||
"time" => {
|
||||
let actual_time = response.routes.first().expect("no route returned").duration;
|
||||
let (expected_time, offset) = extract_number_and_offset("s", expectation);
|
||||
// println!("{actual_time} == {expected_time} +- {offset}");
|
||||
assert!(
|
||||
approx_equal_within_offset_range(actual_time, expected_time, offset as f64),
|
||||
"actual time {actual_time} not equal to expected value {expected_time}"
|
||||
);
|
||||
},
|
||||
"times" => {
|
||||
// TODO: go over steps
|
||||
|
||||
let actual_times : Vec<f64>= response.routes.first().expect("no route returned").legs.iter().map(|leg| {
|
||||
leg.steps.iter().map(|step| step.duration).collect::<Vec<f64>>()
|
||||
}).flatten().collect();
|
||||
let (expected_times, offset) = extract_number_vector_and_offset("s", expectation);
|
||||
println!("{actual_times:?} == {expected_times:?} +- {offset}");
|
||||
assert_eq!(actual_times.len(), expected_times.len(), "times mismatch: {actual_times:?} != {expected_times:?} +- {offset}");
|
||||
|
||||
zip(actual_times, expected_times).for_each(|(actual_time, expected_time)| {
|
||||
assert!(approx_equal_within_offset_range(actual_time, expected_time, offset as f64),
|
||||
"actual time {actual_time} not equal to expected value {expected_time}");
|
||||
});
|
||||
},
|
||||
"distances" => {
|
||||
|
||||
println!("{:?}",response.routes.first().expect("no route returned"));
|
||||
// TODO: go over steps
|
||||
let actual_distances : Vec<f64> = response.routes.first().expect("no route returned").legs.iter().map(|leg| leg.distance).collect();
|
||||
let (expected_distances, offset) = extract_number_vector_and_offset("m", expectation);
|
||||
println!("{expected_distances:?} == {actual_distances:?}");
|
||||
println!("!");
|
||||
assert_eq!(expected_distances.len(), actual_distances.len(), "distances mismatch {expected_distances:?} != {actual_distances:?} +- {offset}");
|
||||
|
||||
zip(actual_distances, expected_distances).for_each(|(actual_distance, expected_distance)| {
|
||||
assert!(approx_equal_within_offset_range(actual_distance, expected_distance, offset as f64),
|
||||
"actual distance {actual_distance} not equal to expected value {expected_distance}");
|
||||
});
|
||||
// // println!("{actual_time} == {expected_time} +- {offset}");
|
||||
// assert!(
|
||||
// approx_equal_within_offset_range(actual_time, expected_time, offset as f64),
|
||||
// "actual time {actual_time} not equal to expected value {expected_time}"
|
||||
// );
|
||||
},
|
||||
"weight" => {
|
||||
let actual_weight = response.routes.first().expect("no route returned").weight;
|
||||
let (expected_weight, offset) = extract_number_and_offset("s", expectation);
|
||||
// println!("{actual_weight} == {expected_weight} +- {offset}");
|
||||
assert!(
|
||||
approx_equal_within_offset_range(
|
||||
actual_weight,
|
||||
expected_weight,
|
||||
offset as f64
|
||||
),
|
||||
"actual time {actual_weight} not equal to expected value {expected_weight}"
|
||||
);
|
||||
},
|
||||
"distance" => {
|
||||
let actual_distance = response.routes.first().expect("no route returned").distance;
|
||||
let (expected_distance, offset) = extract_number_and_offset("m", expectation);
|
||||
assert!(
|
||||
approx_equal_within_offset_range(
|
||||
actual_distance,
|
||||
expected_distance,
|
||||
offset as f64
|
||||
),
|
||||
"actual time {actual_distance} not equal to expected value {expected_distance}"
|
||||
);
|
||||
},
|
||||
"waypoints" => {},
|
||||
// TODO: more checks need to be implemented
|
||||
_ => {
|
||||
let msg = format!("case {case} = {expectation} not implemented");
|
||||
unimplemented!("{msg}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
debug!("arguments: {:?}", args);
|
||||
|
||||
let digest = md5_of_osrm_executables().digest().to_hex_lowercase();
|
||||
|
||||
futures::executor::block_on(
|
||||
OSRMWorld::cucumber()
|
||||
.max_concurrent_scenarios(1)
|
||||
.before(move |feature, _rule, scenario, world| {
|
||||
world.scenario_id = common::scenario_id::scenario_id(scenario);
|
||||
world.set_scenario_specific_paths_and_digests(feature.path.clone());
|
||||
world.osrm_digest = md5_of_osrm_executables().digest().to_hex_lowercase();
|
||||
world.osrm_digest = digest.clone();
|
||||
|
||||
// TODO: clean up cache if needed? Or do in scenarios?
|
||||
|
||||
future::ready(()).boxed()
|
||||
})
|
||||
.with_writer(DotWriter::default().normalized())
|
||||
.filter_run("features/car/names.feature", |_, _, sc| {
|
||||
// .with_writer(DotWriter::default().normalized())
|
||||
.filter_run("features/testbot/time.feature", |_, _, sc| {
|
||||
!sc.tags.iter().any(|t| t == "todo")
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user