386 lines
13 KiB
Rust
386 lines
13 KiB
Rust
extern crate clap;
|
|
|
|
mod common;
|
|
|
|
use crate::common::osrm_world::OSRMWorld;
|
|
use cheap_ruler::CheapRuler;
|
|
use clap::Parser;
|
|
use common::lexicographic_file_walker::LexicographicFileWalker;
|
|
use common::osm::OSMWay;
|
|
use core::panic;
|
|
use cucumber::{self, gherkin::Step, given, when, World};
|
|
use futures::{future, FutureExt};
|
|
use geo_types::{point, Point};
|
|
use std::fmt::Display;
|
|
use std::fs::{create_dir_all, File};
|
|
use std::io::{Read, Write};
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
use std::{env, fs};
|
|
|
|
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) -> geo_types::Point {
|
|
let ruler = CheapRuler::new(DEFAULT_ORIGIN[1], cheap_ruler::DistanceUnit::Meters);
|
|
ruler.offset(
|
|
&point!(DEFAULT_ORIGIN),
|
|
dx * DEFAULT_GRID_SIZE,
|
|
dy * DEFAULT_GRID_SIZE,
|
|
) //TODO: needs to be world's gridSize, not the local one
|
|
}
|
|
|
|
#[given(expr = "the profile \"{word}\"")]
|
|
fn set_profile(world: &mut OSRMWorld, profile: String) {
|
|
println!(
|
|
"using profile: {profile} on scenario: {}",
|
|
world.scenario_id
|
|
);
|
|
world.profile = profile;
|
|
}
|
|
#[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)| {
|
|
row.chars()
|
|
.enumerate()
|
|
.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.));
|
|
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]"),
|
|
}
|
|
});
|
|
});
|
|
} else {
|
|
panic!("node map not found");
|
|
}
|
|
}
|
|
|
|
#[given(regex = "the ways")]
|
|
fn set_ways(world: &mut OSRMWorld, step: &Step) {
|
|
// println!("using profile: {profile}");
|
|
if let Some(table) = step.table.as_ref() {
|
|
if table.rows.is_empty() {
|
|
panic!("empty way table provided")
|
|
}
|
|
// store a reference to the headers for convenient lookup
|
|
let headers = table.rows.first().unwrap();
|
|
|
|
// iterate over the following rows and build ways one by one
|
|
table.rows.iter().skip(1).for_each(|row| {
|
|
let mut way = OSMWay {
|
|
id: world.make_osm_id(),
|
|
..Default::default()
|
|
};
|
|
way.tags.insert("highway".into(), "primary".into()); // default may get overwritten below
|
|
row.iter().enumerate().for_each(|(column_index, token)| {
|
|
let header = headers[column_index].as_str();
|
|
if header == "nodes" {
|
|
assert!(
|
|
token.len() >= 2,
|
|
"ways must be defined by token of at least length two giving"
|
|
);
|
|
way.tags.insert("name".into(), token.clone());
|
|
token.chars().for_each(|name| {
|
|
if !world.known_osm_nodes.contains_key(&name) {
|
|
// TODO: this check is probably not necessary since it is also checked below implicitly
|
|
panic!("referenced unknown node {name} in way {token}");
|
|
}
|
|
if let Some((_, node)) = world.osm_db.find_node(name) {
|
|
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 {
|
|
println!("no table found {step:#?}");
|
|
}
|
|
|
|
// println!("{}", world.osm_db.to_xml())
|
|
}
|
|
|
|
#[when("I request nearest I should get")]
|
|
fn request_nearest(world: &mut OSRMWorld, step: &Step) {
|
|
// if .osm file does not exist
|
|
// write osm file
|
|
// TODO: move to cache_file/path(.) function in OSRMWorld
|
|
let full_path = world.feature_path.clone().unwrap();
|
|
let path = full_path
|
|
.ancestors()
|
|
.find(|p| p.ends_with("features"))
|
|
.expect(".feature files reside in a directory tree with the root name 'features'");
|
|
|
|
let suffix = full_path.strip_prefix(path).unwrap();
|
|
let path = path.parent().unwrap();
|
|
println!("suffix: {suffix:?}");
|
|
let cache_path = path
|
|
.join("test")
|
|
.join("cache")
|
|
.join(suffix)
|
|
.join(&world.feature_digest);
|
|
|
|
println!("{cache_path:?}");
|
|
if !cache_path.exists() {
|
|
create_dir_all(&cache_path).expect("cache path could not be created");
|
|
} else {
|
|
println!("not creating cache dir");
|
|
}
|
|
|
|
let osm_file = cache_path.join(world.scenario_id.clone() + ".osm");
|
|
if !osm_file.exists() {
|
|
println!("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 {
|
|
println!("not writing to OSM file {osm_file:?}");
|
|
}
|
|
|
|
// if extracted file does not exist
|
|
let cache_path = cache_path.join(&world.osrm_digest);
|
|
if cache_path.exists() {
|
|
println!("{cache_path:?} exists");
|
|
} else {
|
|
println!("{cache_path:?} does not exist");
|
|
}
|
|
|
|
let routed_path = path.join("build").join("osrm-routed");
|
|
if !routed_path.exists() {
|
|
panic!("osrm-routed binary not found");
|
|
}
|
|
|
|
// parse query data
|
|
let t = &step.table.as_ref().expect("no query table specified");
|
|
let test_cases: Vec<_> = t
|
|
.rows
|
|
.iter()
|
|
.skip(1)
|
|
.map(|row| {
|
|
assert_eq!(
|
|
row.len(),
|
|
2,
|
|
"test case broken: row needs to have two entries"
|
|
);
|
|
let query = row.get(0).unwrap();
|
|
let expected = row.get(1).unwrap();
|
|
assert_eq!(query.len(), 1);
|
|
assert_eq!(expected.len(), 1);
|
|
(
|
|
query.chars().next().unwrap(),
|
|
expected.chars().next().unwrap(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let data_path = cache_path.join(world.scenario_id.to_owned() + ".osrm");
|
|
println!("{routed_path:?} {}", data_path.to_str().unwrap());
|
|
let handle = Command::new(routed_path)
|
|
.arg(data_path.to_str().unwrap())
|
|
.spawn();
|
|
|
|
if let Err(e) = handle {
|
|
panic!("{e}");
|
|
}
|
|
|
|
// parse and run test cases
|
|
for (query, expected) in test_cases {
|
|
let query_coord = world.get_location(query);
|
|
let expected_coord = world.get_location(expected);
|
|
|
|
println!("{query_coord:?} => {expected_coord:?}");
|
|
// run queries
|
|
|
|
// check results
|
|
}
|
|
|
|
if let Err(e) = handle.expect("osrm-routed died unexpectedly").kill() {
|
|
panic!("shutdown failed: {e}");
|
|
}
|
|
}
|
|
|
|
// TODO: move to different file
|
|
fn get_file_as_byte_vec(path: &PathBuf) -> Vec<u8> {
|
|
println!("opening {path:?}");
|
|
let mut f = File::open(path).expect("no file found");
|
|
let metadata = fs::metadata(path).expect("unable to read metadata");
|
|
let mut buffer = vec![0; metadata.len() as usize];
|
|
match f.read(&mut buffer) {
|
|
Ok(l) => assert_eq!(metadata.len() as usize, l, "data was not completely read"),
|
|
Err(e) => panic!("Error: {e}"),
|
|
}
|
|
|
|
buffer
|
|
}
|
|
|
|
#[derive(clap::ValueEnum, Clone, Default, Debug)]
|
|
enum LoadMethod {
|
|
Mmap,
|
|
#[default]
|
|
Datastore,
|
|
Directly,
|
|
}
|
|
impl Display for LoadMethod {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let result = match self {
|
|
LoadMethod::Mmap => "mmap",
|
|
LoadMethod::Datastore => "datastore",
|
|
LoadMethod::Directly => "directly",
|
|
};
|
|
write!(f, "{result}")
|
|
}
|
|
}
|
|
|
|
#[derive(clap::ValueEnum, Clone, Default, Debug)]
|
|
enum RoutingAlgorithm {
|
|
#[default]
|
|
Ch,
|
|
Mld,
|
|
}
|
|
|
|
impl Display for RoutingAlgorithm {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let result = match self {
|
|
RoutingAlgorithm::Ch => "ch",
|
|
RoutingAlgorithm::Mld => "mld",
|
|
};
|
|
write!(f, "{result}")
|
|
}
|
|
}
|
|
// TODO: move to external file
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, about, long_about = None)]
|
|
struct Args {
|
|
// underlying memory storage
|
|
#[arg(short, default_value_t = LoadMethod::Datastore)]
|
|
memory: LoadMethod,
|
|
|
|
// Number of times to greet
|
|
#[arg(short, default_value_t = RoutingAlgorithm::Ch)]
|
|
p: RoutingAlgorithm,
|
|
}
|
|
|
|
fn main() {
|
|
let args = Args::parse();
|
|
println!("name: {:?}", args);
|
|
|
|
// create OSRM digest before any tests are executed since cucumber-rs doesn't have @beforeAll
|
|
let exe_path = env::current_exe().expect("failed to get current exe path");
|
|
let path = exe_path
|
|
.ancestors()
|
|
.find(|p| p.ends_with("target"))
|
|
.expect("compiled cucumber test executable resides in a directory tree with the root name 'target'")
|
|
.parent().unwrap(); // TODO: Remove after migration to Rust build dir
|
|
let build_path = path.join("build"); // TODO: Remove after migration to Rust build dir
|
|
let mut dependencies = Vec::new();
|
|
|
|
// FIXME: the following iterator gymnastics port the exact behavior of the JavaScript implementation
|
|
let names = [
|
|
"osrm-extract",
|
|
"osrm-contract",
|
|
"osrm-customize",
|
|
"osrm-partition",
|
|
"osrm_extract",
|
|
"osrm_contract",
|
|
"osrm_customize",
|
|
"osrm_partition",
|
|
];
|
|
|
|
let files: Vec<PathBuf> = fs::read_dir(build_path)
|
|
.unwrap()
|
|
.filter_map(|e| e.ok())
|
|
.map(|dir_entry| dir_entry.path())
|
|
.collect();
|
|
|
|
let iter = names.iter().map(|name| {
|
|
files
|
|
.iter()
|
|
.find(|path_buf| {
|
|
path_buf
|
|
.file_stem()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.contains(name)
|
|
})
|
|
.cloned()
|
|
.expect("file exists and is usable")
|
|
});
|
|
|
|
dependencies.extend(iter);
|
|
|
|
let profiles_path = path.join("profiles");
|
|
println!("{profiles_path:?}");
|
|
|
|
dependencies.extend(
|
|
LexicographicFileWalker::new(&profiles_path)
|
|
.filter(|pb| !pb.to_str().unwrap().contains("examples"))
|
|
.filter(|pathbuf| match pathbuf.extension() {
|
|
Some(ext) => ext.to_str().unwrap() == "lua",
|
|
None => false,
|
|
}),
|
|
);
|
|
let mut md5 = chksum_md5::new();
|
|
println!("md5: {}", md5.digest().to_hex_lowercase());
|
|
|
|
for path_buf in dependencies {
|
|
let data = get_file_as_byte_vec(&path_buf);
|
|
if data.is_empty() {
|
|
continue;
|
|
}
|
|
md5.update(data);
|
|
// println!("md5: {}", md5.digest().to_hex_lowercase());
|
|
}
|
|
|
|
futures::executor::block_on(
|
|
OSRMWorld::cucumber()
|
|
.before(move |feature, _rule, scenario, world| {
|
|
// TODO: move to function call below
|
|
// ports the following logic:
|
|
// let name = scenario.getName().toLowerCase().replace(/[/\-'=,():*#]/g, '')
|
|
// .replace(/\s/g, '_').replace(/__/g, '_').replace(/\.\./g, '.')
|
|
// .substring(0, 64);
|
|
let mut s = scenario
|
|
.name
|
|
.to_ascii_lowercase()
|
|
.replace(
|
|
&['/', '\\', '-', '\'', '=', ',', '(', ')', ':', '*', '#'][..],
|
|
"",
|
|
)
|
|
.chars()
|
|
.map(|x| match x {
|
|
' ' => '_',
|
|
_ => x,
|
|
})
|
|
.collect::<String>()
|
|
.replace('\\', "_")
|
|
.replace("__", "_")
|
|
.replace("..", ".");
|
|
s.truncate(64);
|
|
|
|
world.scenario_id = format!("{}_{}", scenario.position.line, s);
|
|
world.set_scenario_specific_paths_and_digests(feature.path.clone());
|
|
world.osrm_digest = md5.digest().to_hex_lowercase();
|
|
|
|
// TODO: clean up cache if needed
|
|
|
|
future::ready(()).boxed()
|
|
})
|
|
.run_and_exit("features/nearest/pick.feature"),
|
|
);
|
|
}
|