From 37f578047244e38d9e56c698532d58e82fae753c Mon Sep 17 00:00:00 2001 From: Dennis Date: Thu, 13 Jun 2024 17:02:28 +0200 Subject: [PATCH] Implement support for car/names.feature --- build.rs | 13 +- features/lib/osrm_loader.js | 2 +- features/support/http.js | 2 + tests/common/cli_arguments.rs | 1 - tests/common/dot_writer.rs | 13 +- tests/common/f64_utils.rs | 2 +- tests/common/file_util.rs | 8 +- tests/common/hash_util.rs | 6 +- tests/common/http_request.rs | 30 +-- .../common/{task_starter.rs => local_task.rs} | 29 ++- tests/common/mod.rs | 19 +- tests/common/nearest_response.rs | 6 +- tests/common/osm_db.rs | 12 +- tests/common/osrm_world.rs | 160 +++++++++++- tests/common/route_response.rs | 62 +++++ tests/cucumber.rs | 229 +++++++++++------- 16 files changed, 442 insertions(+), 152 deletions(-) rename tests/common/{task_starter.rs => local_task.rs} (74%) create mode 100644 tests/common/route_response.rs diff --git a/build.rs b/build.rs index 1499805ed..b0340b000 100644 --- a/build.rs +++ b/build.rs @@ -66,7 +66,7 @@ fn main() { if let Some((platform, compiler)) = match env::consts::OS { "linux" if env::consts::ARCH == "x86_64" => Some((OS::Linux, ".clang++-15")), "macos" if env::consts::ARCH == "x86_64" => Some((OS::MacIntel, "")), - "macos" if env::consts::ARCH == "arm" => Some((OS::Mac, "")), + "macos" if env::consts::ARCH == "aarch64" => Some((OS::Mac, "")), "windows" if env::consts::ARCH == "x86_64" => Some((OS::Windows, "")), _ => None, } { @@ -98,11 +98,16 @@ fn main() { build_println!("unsupported platform: {} {}. 'flatc' binary supporting version {} of the library needs to be in system path", env::consts::OS, env::consts::ARCH, version); } - let flatc = match Path::new(executable_path).exists() { - true => flatc_rust::Flatc::from_path(executable_path), - false => flatc_rust::Flatc::from_env_path(), + let (flatc, location) = match Path::new(executable_path).exists() { + true => (flatc_rust::Flatc::from_path(executable_path), "local"), + false => (flatc_rust::Flatc::from_env_path(), "downloaded"), }; assert!(flatc.check().is_ok()); + let version = &flatc.version().unwrap(); + build_println!( + "using {location} flatc v{} to compile schema files", + version.version() + ); flatc .run(flatc_rust::Args { extra: &["--gen-all"], diff --git a/features/lib/osrm_loader.js b/features/lib/osrm_loader.js index 4e7de8cd1..863a02828 100644 --- a/features/lib/osrm_loader.js +++ b/features/lib/osrm_loader.js @@ -46,7 +46,7 @@ class OSRMBaseLoader{ let retry = (err) => { if (err) { if (retryCount < this.scope.OSRM_CONNECTION_RETRIES) { - const timeoutMs = 10 * Math.pow(this.scope.OSRM_CONNECTION_EXP_BACKOFF_COEF, retryCount); + const timeoutMs = 10 * Math.pow(1.1, retryCount); retryCount++; setTimeout(() => { tryConnect(this.scope.OSRM_IP, this.scope.OSRM_PORT, retry); }, timeoutMs); } else { diff --git a/features/support/http.js b/features/support/http.js index 71f61761a..55ff660de 100644 --- a/features/support/http.js +++ b/features/support/http.js @@ -29,6 +29,8 @@ module.exports = function () { var params = this.paramsToString(parameters); this.query = baseUri + (params.length ? '/' + params : ''); + console.log(this.query); + request(this.query, (err, res, body) => { if (err && err.code === 'ECONNREFUSED') { return cb(new Error('*** osrm-routed is not running.')); diff --git a/tests/common/cli_arguments.rs b/tests/common/cli_arguments.rs index f789da6f8..fcfa55c8d 100644 --- a/tests/common/cli_arguments.rs +++ b/tests/common/cli_arguments.rs @@ -37,7 +37,6 @@ impl Display for RoutingAlgorithm { } } - // TODO: move to external file #[derive(Parser, Debug)] #[command(version, about, long_about = None)] diff --git a/tests/common/dot_writer.rs b/tests/common/dot_writer.rs index 9f924fe96..b1ff8be7f 100644 --- a/tests/common/dot_writer.rs +++ b/tests/common/dot_writer.rs @@ -1,6 +1,9 @@ use colored::Colorize; use cucumber::{cli, event, parser, Event}; -use std::{io::{self, Write}, time::Instant}; +use std::{ + io::{self, Write}, + time::Instant, +}; #[derive(Debug, Default)] pub struct DotWriter { @@ -75,8 +78,8 @@ impl cucumber::Writer for DotWriter { let p = format!("{} passed", self.step_passed).green(); println!("{} steps ({f}, {s}, {p})", self.step_started); - let elapsed = Instant::now() - self.start_time.unwrap(); - let minutes = elapsed.as_secs()/60; + let elapsed = Instant::now() - self.start_time.unwrap(); + let minutes = elapsed.as_secs() / 60; let seconds = (elapsed.as_millis() % 60_000) as f64 / 1000.; println!("{}m{}s", minutes, seconds); } @@ -86,10 +89,10 @@ impl cucumber::Writer for DotWriter { scenarios: _, steps: _, parser_errors: _, - } => {}, + } => {} event::Cucumber::Started => { self.start_time = Some(Instant::now()); - }, + } }, Err(e) => println!("Error: {e}"), } diff --git a/tests/common/f64_utils.rs b/tests/common/f64_utils.rs index 31dbf0959..aafc74c84 100644 --- a/tests/common/f64_utils.rs +++ b/tests/common/f64_utils.rs @@ -1,4 +1,4 @@ pub fn approx_equal(a: f32, b: f32, dp: u8) -> bool { let p = 10f32.powi(-(dp as i32)); (a - b).abs() < p -} \ No newline at end of file +} diff --git a/tests/common/file_util.rs b/tests/common/file_util.rs index d2092fec7..0a3c5a68d 100644 --- a/tests/common/file_util.rs +++ b/tests/common/file_util.rs @@ -1,4 +1,8 @@ -use std::{fs::{self, File}, io::Read, path::PathBuf}; +use std::{ + fs::{self, File}, + io::Read, + path::PathBuf, +}; use log::debug; @@ -13,4 +17,4 @@ pub fn get_file_as_byte_vec(path: &PathBuf) -> Vec { } buffer -} \ No newline at end of file +} diff --git a/tests/common/hash_util.rs b/tests/common/hash_util.rs index 152086dae..ce76b501a 100644 --- a/tests/common/hash_util.rs +++ b/tests/common/hash_util.rs @@ -2,7 +2,9 @@ use std::{env, fs, path::PathBuf}; use log::debug; -use crate::common::{file_util::get_file_as_byte_vec, lexicographic_file_walker::LexicographicFileWalker}; +use crate::common::{ + file_util::get_file_as_byte_vec, lexicographic_file_walker::LexicographicFileWalker, +}; pub fn md5_of_osrm_executables() -> chksum_md5::MD5 { // create OSRM digest before any tests are executed since cucumber-rs doesn't have @beforeAll @@ -75,4 +77,4 @@ pub fn md5_of_osrm_executables() -> chksum_md5::MD5 { // debug!("md5: {}", md5.digest().to_hex_lowercase()); } md5 - } +} diff --git a/tests/common/http_request.rs b/tests/common/http_request.rs index 8e89ccd71..04ab7fddf 100644 --- a/tests/common/http_request.rs +++ b/tests/common/http_request.rs @@ -2,23 +2,23 @@ // functions to make nearest, route, etc calls // fn nearest(arg1, ... argn) -> NearestResponse -use std::{path::Path, time::Duration}; +// use std::{path::Path, time::Duration}; -use ureq::{Agent, AgentBuilder}; +// use ureq::{Agent, AgentBuilder}; -pub struct HttpRequest { - agent: Agent, -} +// pub struct HttpRequest { +// agent: Agent, +// } -impl HttpRequest { - // pub fn fetch_to_file(url: &str, output: &Path) -> Result<()> {} +// impl HttpRequest { +// // pub fn fetch_to_file(url: &str, output: &Path) -> Result<()> {} - pub fn new() -> Self { - let agent = AgentBuilder::new() - .timeout_read(Duration::from_secs(5)) - .timeout_write(Duration::from_secs(5)) - .build(); +// pub fn new() -> Self { +// let agent = AgentBuilder::new() +// .timeout_read(Duration::from_secs(5)) +// .timeout_write(Duration::from_secs(5)) +// .build(); - Self { agent } - } -} +// Self { agent } +// } +// } diff --git a/tests/common/task_starter.rs b/tests/common/local_task.rs similarity index 74% rename from tests/common/task_starter.rs rename to tests/common/local_task.rs index 2d6a45d6a..5e433978b 100644 --- a/tests/common/task_starter.rs +++ b/tests/common/local_task.rs @@ -3,24 +3,25 @@ use std::{ process::{Child, Command, Stdio}, }; -#[derive(Default)] -pub struct TaskStarter { +#[derive(Debug, Default)] +pub struct LocalTask { ready: bool, command: String, arguments: Vec, child: Option, } -impl TaskStarter { - pub fn new(command: &str) -> Self { +impl LocalTask { + pub fn new(command: String) -> Self { Self { ready: false, - command: command.into(), + command: command, arguments: Vec::new(), child: None, } } pub fn is_ready(&self) -> bool { + // TODO: also check that process is running self.ready } pub fn arg(&mut self, argument: &str) -> &mut Self { @@ -39,6 +40,10 @@ impl TaskStarter { Err(e) => panic!("cannot spawn task: {e}"), } + if self.child.is_none() { + return; + } + if let Some(output) = &mut self.child.as_mut().unwrap().stdout { // implement with a timeout let mut reader = BufReader::new(output); @@ -54,10 +59,10 @@ impl TaskStarter { } } -impl Drop for TaskStarter { - fn drop(&mut self) { - if let Err(e) = self.child.as_mut().expect("can't access child").kill() { - panic!("shutdown failed: {e}"); - } - } -} +// impl Drop for LocalTask { +// fn drop(&mut self) { +// if let Err(e) = self.child.as_mut().expect("can't access child").kill() { +// panic!("shutdown failed: {e}"); +// } +// } +// } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 59a15cac6..6c94438c2 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,6 +1,6 @@ #![allow(clippy::derivable_impls, clippy::all)] extern crate flatbuffers; - + pub mod cli_arguments; pub mod dot_writer; pub mod f64_utils; @@ -8,27 +8,28 @@ pub mod file_util; pub mod hash_util; pub mod http_request; pub mod lexicographic_file_walker; +pub mod local_task; pub mod location; pub mod nearest_response; pub mod osm; pub mod osm_db; pub mod osrm_world; +pub mod route_response; pub mod scenario_id; -pub mod task_starter; // flatbuffer #[allow(dead_code, unused_imports)] +#[path = "../../target/flatbuffers/fbresult_generated.rs"] +pub mod fbresult_flatbuffers; +#[allow(dead_code, unused_imports)] #[path = "../../target/flatbuffers/position_generated.rs"] pub mod position_flatbuffers; #[allow(dead_code, unused_imports)] -#[path = "../../target/flatbuffers/waypoint_generated.rs"] -pub mod waypoint_flatbuffers; +#[path = "../../target/flatbuffers/route_generated.rs"] +pub mod route_flatbuffers; #[allow(dead_code, unused_imports)] #[path = "../../target/flatbuffers/table_generated.rs"] pub mod table_flatbuffers; #[allow(dead_code, unused_imports)] -#[path = "../../target/flatbuffers/route_generated.rs"] -pub mod route_flatbuffers; -#[allow(dead_code, unused_imports)] -#[path = "../../target/flatbuffers/fbresult_generated.rs"] -pub mod fbresult_flatbuffers; +#[path = "../../target/flatbuffers/waypoint_generated.rs"] +pub mod waypoint_flatbuffers; diff --git a/tests/common/nearest_response.rs b/tests/common/nearest_response.rs index 39513b120..40f71e1f6 100644 --- a/tests/common/nearest_response.rs +++ b/tests/common/nearest_response.rs @@ -1,11 +1,11 @@ -use crate::common::fbresult_flatbuffers::osrm::engine::api::fbresult::FBResult; use super::location::Location; +use crate::common::fbresult_flatbuffers::osrm::engine::api::fbresult::FBResult; use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct Waypoint { pub hint: String, - pub nodes: Vec, + pub nodes: Option>, pub distance: f32, pub name: String, location: Location, @@ -66,7 +66,7 @@ impl NearestResponse { longitude: location.longitude(), }; let nodes = wp.nodes().expect("waypoint mus have nodes"); - let nodes = vec![nodes.first(), nodes.second()]; + let nodes = Some(vec![nodes.first(), nodes.second()]); let distance = wp.distance(); Waypoint { diff --git a/tests/common/osm_db.rs b/tests/common/osm_db.rs index fc8ee1a71..c33bcae0c 100644 --- a/tests/common/osm_db.rs +++ b/tests/common/osm_db.rs @@ -93,15 +93,19 @@ mod tests { let mut node1 = OSMNode { id: 123, - lat: 50.1234, - lon: 8.9876, + location: Location { + longitude: 8.9876, + latitude: 50.1234, + }, ..Default::default() }; let mut node2 = OSMNode { id: 321, - lat: 50.1234, - lon: 8.9876, + location: Location { + longitude: 8.9876, + latitude: 50.1234, + }, ..Default::default() }; node1.add_tag("name", "a"); diff --git a/tests/common/osrm_world.rs b/tests/common/osrm_world.rs index 9ca31299a..d623f771d 100644 --- a/tests/common/osrm_world.rs +++ b/tests/common/osrm_world.rs @@ -1,15 +1,25 @@ -use crate::Location; +use super::{ + nearest_response::NearestResponse, osm::OSMNode, osm_db::OSMDb, route_response::RouteResponse, +}; +use crate::{common::local_task::LocalTask, Location}; +use core::panic; use cucumber::World; use log::debug; use std::{ collections::HashMap, fs::{create_dir_all, File}, + io::Write, path::PathBuf, + time::Duration, }; -use super::{osm::OSMNode, osm_db::OSMDb}; +const DEFAULT_ORIGIN: Location = Location { + longitude: 1.0f32, + latitude: 1.0f32, +}; +const DEFAULT_GRID_SIZE: f32 = 100.; -#[derive(Debug, Default, World)] +#[derive(Debug, World)] pub struct OSRMWorld { pub feature_path: Option, pub scenario_id: String, @@ -25,6 +35,36 @@ pub struct OSRMWorld { pub extraction_parameters: Vec, pub request_with_flatbuffers: bool, + + pub grid_size: f32, + pub origin: Location, + task: LocalTask, + agent: ureq::Agent, +} + +impl Default for OSRMWorld { + fn default() -> Self { + Self { + feature_path: Default::default(), + scenario_id: Default::default(), + feature_digest: Default::default(), + osrm_digest: Default::default(), + osm_id: Default::default(), + profile: Default::default(), + known_osm_nodes: Default::default(), + known_locations: Default::default(), + osm_db: Default::default(), + extraction_parameters: Default::default(), + request_with_flatbuffers: Default::default(), + grid_size: DEFAULT_GRID_SIZE, + origin: DEFAULT_ORIGIN, + task: LocalTask::default(), + agent: ureq::AgentBuilder::new() + .timeout_read(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) + .build(), + } + } } impl OSRMWorld { @@ -59,7 +99,11 @@ impl OSRMWorld { .ancestors() .find(|p| p.ends_with("features")) .expect(".feature files reside in a directory tree with the root name 'features'"); - let routed_path = path.parent().expect("cannot get parent path").join("build").join("osrm-routed"); + let routed_path = path + .parent() + .expect("cannot get parent path") + .join("build") + .join("osrm-routed"); assert!(routed_path.exists(), "osrm-routed binary not found"); routed_path } @@ -119,4 +163,112 @@ impl OSRMWorld { } self.known_locations.insert(name, location); } + + pub fn write_osm_file(&self) { + let osm_file = self + .feature_cache_path() + .join(self.scenario_id.clone() + ".osm"); + if !osm_file.exists() { + debug!("writing to osm file: {osm_file:?}"); + let mut file = File::create(osm_file).expect("could not create OSM file"); + file.write_all(self.osm_db.to_xml().as_bytes()) + .expect("could not write OSM file"); + } else { + debug!("not writing to OSM file {osm_file:?}"); + } + } + + pub fn extract_osm_file(&self) { + let cache_path = self.artefact_cache_path(); + if cache_path.exists() { + debug!("{cache_path:?} exists"); + } else { + unimplemented!("{cache_path:?} does not exist"); + } + } + + pub fn artefact_cache_path(&self) -> PathBuf { + self.feature_cache_path().join(&self.osrm_digest) + } + + fn start_routed(&mut self) { + if self.task.is_ready() { + // task running already + return; + } + let data_path = self + .artefact_cache_path() + .join(self.scenario_id.to_owned() + ".osrm"); + + // TODO: this should not require a temporary and behave like the API of std::process + self.task = LocalTask::new(self.routed_path().to_string_lossy().into()); + self.task + .arg(data_path.to_str().expect("data path unwrappable")); + self.task + .spawn_wait_till_ready("running and waiting for requests"); + assert!(self.task.is_ready()); + } + + pub fn nearest( + &mut self, + query_location: &Location, + request_with_flatbuffers: bool, + ) -> NearestResponse { + self.start_routed(); + + let mut url = format!( + "http://localhost:5000/nearest/v1/{}/{:?},{:?}", + self.profile, query_location.longitude, query_location.latitude + ); + if request_with_flatbuffers { + url += ".flatbuffers"; + } + let call = self.agent.get(&url).call(); + + let body = match call { + Ok(response) => response.into_reader(), + Err(e) => panic!("http error: {e}"), + }; + + let response = match 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 { + self.start_routed(); + + let mut url = format!( + "http://localhost:5000/route/v1/{}/{:?},{:?};{:?},{:?}?steps=true&alternatives=false", + self.profile, + from_location.longitude, + from_location.latitude, + to_location.longitude, + to_location.latitude, + ); + if request_with_flatbuffers { + url += ".flatbuffers"; + } + // 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}"), + }; + + let text = std::io::read_to_string(body).unwrap(); + let response = match request_with_flatbuffers { + true => unimplemented!("RouteResponse::from_flatbuffer(body)"), + false => RouteResponse::from_string(&text), + }; + response + } } diff --git a/tests/common/route_response.rs b/tests/common/route_response.rs new file mode 100644 index 000000000..ebb8f2a65 --- /dev/null +++ b/tests/common/route_response.rs @@ -0,0 +1,62 @@ +use serde::Deserialize; + +use super::nearest_response::Waypoint; + +#[derive(Deserialize, Default, Debug)] +pub struct Step { + pub name: String, + pub pronunciation: Option, + pub r#ref: Option, +} + +// #[derive(Deserialize, Debug)] +// pub struct Annotation { +// pub nodes: Option>, +// } + +#[derive(Debug, Default, Deserialize)] +pub struct Leg { + pub summary: String, + pub weight: f64, + pub duration: f64, + pub steps: Vec, + pub distance: f64, + // pub annotation: Option>, +} + +#[derive(Deserialize, Debug)] +pub struct Route { + pub geometry: String, + pub weight: f64, + pub duration: f64, + pub legs: Vec, + pub weight_name: String, + pub distance: f64, +} + +#[derive(Deserialize, Debug)] +pub struct RouteResponse { + pub code: String, + pub routes: Vec, + pub waypoints: Vec, + pub data_version: Option, +} + +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_string(input: &str) -> Self { + // println!("{input}"); + let response = match serde_json::from_str(input) { + Ok(response) => response, + Err(e) => panic!("parsing error {e} => {input}"), + }; + response + } +} diff --git a/tests/cucumber.rs b/tests/cucumber.rs index 3618866df..8983d7c41 100644 --- a/tests/cucumber.rs +++ b/tests/cucumber.rs @@ -1,35 +1,31 @@ -// extern crate clap; - 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, nearest_response::NearestResponse, - osm::OSMWay, osrm_world::OSRMWorld, task_starter::TaskStarter, + hash_util::md5_of_osrm_executables, location::Location, osm::OSMWay, osrm_world::OSRMWorld, }; use core::panic; -use cucumber::{gherkin::Step, given, when, World, WriterExt}; +use cucumber::{ + gherkin::{Step, Table}, + given, when, World, WriterExt, +}; use futures::{future, FutureExt}; -use geo_types::point; +use geo_types::Point; use log::debug; -use std::{collections::HashMap, fs::File, io::Write, time::Duration}; -use ureq::Agent; +use std::collections::HashMap; -const DEFAULT_ORIGIN: [f64; 2] = [1., 1.]; // TODO: move to world? -const DEFAULT_GRID_SIZE: f64 = 100.; // TODO: move to world? - -fn offset_origin_by(dx: f64, dy: f64) -> Location { - let ruler = CheapRuler::new(DEFAULT_ORIGIN[1], cheap_ruler::DistanceUnit::Meters); +fn offset_origin_by(dx: f32, dy: f32, origin: Location, grid_size: f32) -> Location { + let ruler = CheapRuler::new(origin.latitude, cheap_ruler::DistanceUnit::Meters); let loc = ruler.offset( - &point!(DEFAULT_ORIGIN), - dx * DEFAULT_GRID_SIZE, - dy * DEFAULT_GRID_SIZE, + &Point::new(origin.longitude, origin.latitude), + dx * grid_size, + dy * grid_size, ); //TODO: needs to be world's gridSize, not the local one Location { - latitude: loc.y() as f32, - longitude: loc.x() as f32, + latitude: loc.y(), + longitude: loc.x(), } } @@ -95,12 +91,16 @@ fn set_node_map(world: &mut OSRMWorld, step: &Step) { .filter(|(_column_index, charater)| *charater != ' ') .for_each(|(column_index, name)| { // This ports the logic from previous implementations. - let location = - offset_origin_by(column_index as f64 * 0.5, -(row_index as f64 - 1.)); + let location = offset_origin_by( + column_index as f32 * 0.5, + -(row_index as f32 - 1.), + 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]"), + _ => unreachable!("node name not in [0..9][a..z]: {docstring}"), } }); }); @@ -114,6 +114,11 @@ 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: f32) { + world.grid_size = meters; +} + #[given(regex = "the ways")] fn set_ways(world: &mut OSRMWorld, step: &Step) { // debug!("using profile: {profile}"); @@ -159,43 +164,13 @@ fn set_ways(world: &mut OSRMWorld, step: &Step) { } else { debug!("no table found {step:#?}"); } - - // debug!("{}", world.osm_db.to_xml()) } -// #[when("I request nearest I should get")] -#[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"; - // if .osm file does not exist - // write osm file - - // TODO: the OSRMWorld instance should have a function to write the .osm file - let osm_file = world - .feature_cache_path() - .join(world.scenario_id.clone() + ".osm"); - if !osm_file.exists() { - debug!("writing to osm file: {osm_file:?}"); - let mut file = File::create(osm_file).expect("could not create OSM file"); - file.write_all(world.osm_db.to_xml().as_bytes()) - .expect("could not write OSM file"); - } else { - debug!("not writing to OSM file {osm_file:?}"); - } - - // if extracted file does not exist - let cache_path = world.feature_cache_path().join(&world.osrm_digest); - if cache_path.exists() { - debug!("{cache_path:?} exists"); - } else { - debug!("{cache_path:?} does not exist"); - } - +fn parse_table_from_steps(table: &Option<&Table>) -> Vec> { // parse query data - let table = &step.table.as_ref().expect("no query table specified"); + 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"); - // TODO: move to common functionality let test_cases: Vec<_> = table .rows .iter() @@ -212,23 +187,22 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) { row_map }) .collect(); + test_cases + // TODO: also return the header +} - let data_path = cache_path.join(world.scenario_id.to_owned() + ".osrm"); +#[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"; - // TODO: this should not require a temporary and behave like the API of std::process - let mut task = TaskStarter::new(world.routed_path().to_str().expect("task can be started")); - task.arg(data_path.to_str().expect("data path unwrappable")); - task.spawn_wait_till_ready("running and waiting for requests"); - assert!(task.is_ready()); + world.write_osm_file(); + world.extract_osm_file(); - // TODO: move to generic http request handling struct - let agent: Agent = ureq::AgentBuilder::new() - .timeout_read(Duration::from_secs(5)) - .timeout_write(Duration::from_secs(5)) - .build(); + // parse query data + let test_cases = parse_table_from_steps(&step.table.as_ref()); - // parse and run test cases - for test_case in test_cases { + // run test cases + for test_case in &test_cases { let query_location = world.get_location( test_case .get("in") @@ -237,7 +211,10 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) { .next() .expect("node name is one char long"), ); - let expected_location = world.get_location( + + let response = world.nearest(&query_location, request_with_flatbuffers); + + let expected_location = &world.get_location( test_case .get("out") .expect("node name is one char long") @@ -246,27 +223,6 @@ fn request_nearest(world: &mut OSRMWorld, step: &Step, state: String) { .expect("node name is one char long"), ); - // debug!("{query_location:?} => {expected_location:?}"); - // run queries - let mut url = format!( - "http://localhost:5000/nearest/v1/{}/{:?},{:?}", - world.profile, query_location.longitude, query_location.latitude - ); - if request_with_flatbuffers { - url += ".flatbuffers"; - } - let call = agent.get(&url).call(); - - let body = match call { - Ok(response) => response.into_reader(), - Err(e) => panic!("http error: {e}"), - }; - - let response = match request_with_flatbuffers { - true => NearestResponse::from_flatbuffer(body), - false => NearestResponse::from_json_reader(body), - }; - if test_case.contains_key("out") { // check that result node is (approximately) equivalent let result_location = response.waypoints[0].location(); @@ -290,6 +246,99 @@ 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"; + 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 to_location = world.get_location( + test_case + .get("to") + .expect("node name is one char long") + .chars() + .next() + .expect("node name is one char long"), + ); + + let response = world.route(&from_location, &to_location, request_with_flatbuffers); + + if test_case.contains_key("route") { + // NOTE: the following code ports logic from JavaScript that checks only properties of the first route + let route = response + .routes + .first() + .expect("no route returned") + .legs + .first() + .expect("legs required") + .steps + .iter() + .map(|step| step.name.clone()) + .collect::>() + .join(","); + + assert_eq!(*test_case.get("route").expect("msg"), route); + } + + if test_case.contains_key("pronunciations") { + let pronunciations = response + .routes + .first() + .expect("no route returned") + .legs + .first() + .expect("legs required") + .steps + .iter() + .map(|step| match &step.pronunciation { + Some(p) => p.clone(), + None => "".to_string(), + }) + .collect::>() + .join(","); + assert_eq!( + *test_case.get("pronunciations").expect("msg"), + pronunciations + ); + } + + if test_case.contains_key("ref") { + let refs = response + .routes + .first() + .expect("no route returned") + .legs + .first() + .expect("legs required") + .steps + .iter() + .map(|step| match &step.r#ref { + Some(p) => p.clone(), + None => "".to_string(), + }) + .collect::>() + .join(","); + assert_eq!(*test_case.get("ref").expect("msg"), refs); + } + // TODO: more checks need to be implemented + + // TODO: check for unchecked test columns + } + + // unimplemented!("route"); +} fn main() { let args = Args::parse(); debug!("arguments: {:?}", args); @@ -307,6 +356,8 @@ fn main() { future::ready(()).boxed() }) .with_writer(DotWriter::default().normalized()) - .run("features/nearest/"), + .filter_run("features/car/names.feature", |_, _, sc| { + !sc.tags.iter().any(|t| t == "todo") + }), ); }