use super::*;
use crate::format_time;
use crate::helpers::*;
use vrp_core::models::examples::create_example_problem;
parameterized_test! {check_vehicles, (known_ids, tours, expected_result), {
check_vehicles_impl(known_ids, tours, expected_result);
}}
check_vehicles! {
case_01: (vec!["vehicle_1"], vec![("vehicle_1", 0)], Ok(())),
case_02: (vec!["vehicle_1"], vec![("vehicle_2", 0)], Err(())),
case_03: (vec!["vehicle_1"], vec![("vehicle_1", 0), ("vehicle_1", 1)], Ok(())),
case_04: (vec!["vehicle_1"], vec![("vehicle_1", 0), ("vehicle_1", 0)], Err(())),
}
fn check_vehicles_impl(known_ids: Vec<&str>, tours: Vec<(&str, usize)>, expected_result: Result<(), ()>) {
let problem = Problem {
fleet: Fleet {
vehicles: vec![VehicleType {
vehicle_ids: known_ids.into_iter().map(|id| id.to_string()).collect(),
..create_default_vehicle_type()
}],
..create_default_fleet()
},
..create_empty_problem()
};
let solution = Solution {
statistic: Statistic::default(),
tours: tours
.into_iter()
.map(|(id, shift_index)| Tour {
vehicle_id: id.to_string(),
type_id: "my_vehicle".to_string(),
shift_index,
stops: vec![],
statistic: Statistic::default(),
})
.collect(),
..create_empty_solution()
};
let ctx = CheckerContext::new(create_example_problem(), problem, None, solution).unwrap();
let result = check_vehicles(&ctx);
assert_eq!(result.map_err(|_| ()), expected_result);
}
parameterized_test! {check_jobs, (jobs, tours, unassigned, expected_result), {
check_jobs_impl(jobs, tours, unassigned, expected_result);
}}
check_jobs! {
case_01: (
vec![("job1", vec!["pickup", "delivery"])],
vec![("my_vehicle_1", 0, vec![("job1", "pickup"), ("job1", "delivery")])],
vec![],
Ok(())
),
case_02: (
vec![("job1", vec!["pickup", "delivery"])],
vec![
("my_vehicle_1", 0, vec![("job1", "pickup")]),
("my_vehicle_2", 0, vec![("job1", "delivery")])
],
vec![],
Err("job served in multiple tours: 'job1'".to_string())
),
case_03: (
vec![("job1", vec!["pickup", "delivery"])],
vec![("my_vehicle_1", 0, vec![("job1", "pickup")])],
vec![],
Err("not all tasks served for 'job1', expected: 2, assigned: 1".to_string())
),
case_04: (
vec![("job1", vec!["pickup", "delivery"])],
vec![("my_vehicle_1", 0, vec![("job1", "delivery"), ("job1", "pickup")])],
vec![],
Err("found pickup after delivery for 'job1'".to_string())
),
case_05: (
vec![("job1", vec!["pickup", "delivery"])],
vec![],
vec!["job1"],
Ok(())
),
case_06: (
vec![("job1", vec!["pickup", "delivery"])],
vec![],
vec!["job1", "job1"],
Err("duplicated job ids in the list of unassigned jobs".to_string())
),
case_07: (
vec![("job1", vec!["pickup", "delivery"])],
vec![],
vec!["job2"],
Err("unknown job id in the list of unassigned jobs: 'job2'".to_string())
),
case_08: (
vec![("job1", vec!["pickup", "delivery"])],
vec![],
vec!["job1", "vehicle_break"],
Ok(())
),
case_09: (
vec![("job1", vec!["pickup", "delivery"])],
vec![("my_vehicle_1", 0, vec![("job1", "pickup"), ("job1", "delivery")])],
vec!["job1"],
Err("job present as assigned and unassigned: 'job1'".to_string())
),
case_10: (
vec![("job1", vec!["pickup"])],
vec![("my_vehicle_1", 0, vec![("job1", "pickup")])],
vec![],
Ok(())
),
}
#[allow(clippy::type_complexity)]
fn check_jobs_impl(
jobs: Vec<(&str, Vec<&str>)>,
tours: Vec<(&str, usize, Vec<(&str, &str)>)>,
unassigned: Vec<&str>,
expected_result: Result<(), String>,
) {
let create_tasks = |tgt: &str, tasks: &Vec<&str>| {
(1..)
.zip(tasks.iter())
.filter(|(_, t)| **t == tgt)
.map(|(idx, _)| JobTask {
places: vec![JobPlace {
location: Location::Coordinate { lat: 0.0, lng: 0.0 },
duration: 0.0,
times: None,
tag: Some(format!("{}{}", tgt, idx)),
}],
demand: if tgt != "service" { Some(vec![1]) } else { None },
order: None,
})
.collect()
};
let create_stop = |stop: (&str, &str)| create_stop_with_activity(stop.0, stop.1, (0., 0.), 0, ("", ""), 0);
let problem = Problem {
plan: Plan {
jobs: jobs
.into_iter()
.map(|(id, tasks)| Job {
pickups: Some(create_tasks("pickup", &tasks)),
deliveries: Some(create_tasks("delivery", &tasks)),
replacements: Some(create_tasks("replacement", &tasks)),
services: Some(create_tasks("service", &tasks)),
..create_job(id)
})
.collect(),
..create_empty_plan()
},
fleet: create_default_fleet(),
..create_empty_problem()
};
let solution = Solution {
statistic: Statistic::default(),
tours: tours
.into_iter()
.map(|(id, shift_index, stops)| Tour {
vehicle_id: id.to_string(),
type_id: "my_vehicle".to_string(),
shift_index,
stops: stops.into_iter().map(create_stop).collect(),
statistic: Statistic::default(),
})
.collect(),
unassigned: Some(
unassigned.into_iter().map(|job| UnassignedJob { job_id: job.to_string(), reasons: vec![] }).collect(),
),
..create_empty_solution()
};
let ctx = CheckerContext::new(create_example_problem(), problem, None, solution).unwrap();
let result = check_jobs_presence(&ctx);
assert_eq!(result, expected_result);
}
#[test]
fn can_detect_time_window_violation() {
let problem = Problem {
plan: Plan {
jobs: vec![create_delivery_job_with_times("job1", (1., 0.), vec![(1, 2)], 1.)],
..create_empty_plan()
},
fleet: create_default_fleet(),
..create_empty_problem()
};
let solution = Solution {
statistic: Statistic {
cost: 15.,
distance: 2,
duration: 3,
times: Timing { driving: 2, serving: 1, ..Timing::default() },
},
tours: vec![Tour {
vehicle_id: "my_vehicle_1".to_string(),
type_id: "my_vehicle".to_string(),
shift_index: 0,
stops: vec![
create_stop_with_activity(
"departure",
"departure",
(0., 0.),
1,
("1970-01-01T00:00:02Z", "1970-01-01T00:00:02Z"),
0,
),
create_stop_with_activity(
"job1",
"delivery",
(1., 0.),
0,
("1970-01-01T00:00:03Z", "1970-01-01T00:00:04Z"),
1,
),
create_stop_with_activity(
"arrival",
"arrival",
(0., 0.),
0,
("1970-01-01T00:00:05Z", "1970-01-01T00:00:05Z"),
2,
),
],
statistic: Statistic {
cost: 15.,
distance: 2,
duration: 3,
times: Timing { driving: 2, serving: 1, ..Timing::default() },
},
}],
..create_empty_solution()
};
let core_problem = Arc::new(problem.clone().read_pragmatic().unwrap());
let ctx = CheckerContext::new(core_problem, problem, None, solution).unwrap();
let result = check_assignment(&ctx);
assert_eq!(result, Err(vec!["cannot match activities to jobs: job1:<no tag>".to_owned()]));
}
#[test]
fn can_detect_job_duration_violation() {
let problem = Problem {
plan: Plan {
jobs: vec![create_delivery_job_with_times("job1", (1., 0.), vec![(5, 10)], 1.)],
..create_empty_plan()
},
fleet: create_default_fleet(),
..create_empty_problem()
};
let solution = Solution {
statistic: Statistic {
cost: 18.,
distance: 2,
duration: 6,
times: Timing { driving: 2, serving: 2, waiting: 2, ..Timing::default() },
},
tours: vec![Tour {
vehicle_id: "my_vehicle_1".to_string(),
type_id: "my_vehicle".to_string(),
shift_index: 0,
stops: vec![
create_stop_with_activity(
"departure",
"departure",
(0., 0.),
1,
("1970-01-01T00:00:02Z", "1970-01-01T00:00:02Z"),
0,
),
create_stop_with_activity(
"job1",
"delivery",
(1., 0.),
0,
("1970-01-01T00:00:05Z", "1970-01-01T00:00:07Z"),
1,
),
create_stop_with_activity(
"arrival",
"arrival",
(0., 0.),
0,
("1970-01-01T00:00:08Z", "1970-01-01T00:00:08Z"),
2,
),
],
statistic: Statistic {
cost: 18.,
distance: 2,
duration: 6,
times: Timing { driving: 2, serving: 2, waiting: 2, ..Timing::default() },
},
}],
..create_empty_solution()
};
let core_problem = Arc::new(problem.clone().read_pragmatic().unwrap());
let ctx = CheckerContext::new(core_problem, problem, None, solution).unwrap();
let result = check_assignment(&ctx);
assert_eq!(result, Err(vec!["cannot match activities to jobs: job1:<no tag>".to_owned()]));
}
#[test]
fn can_detect_dispatch_violations() {
let problem = Problem {
plan: Plan { jobs: vec![create_delivery_job("job1", (2., 0.))], ..create_empty_plan() },
fleet: Fleet {
vehicles: vec![VehicleType {
shifts: vec![VehicleShift {
dispatch: Some(vec![VehicleDispatch {
location: (1., 0.).to_loc(),
limits: vec![VehicleDispatchLimit { max: 1, start: format_time(1.), end: format_time(2.) }],
tag: None,
}]),
..create_default_vehicle_shift()
}],
..create_default_vehicle_type()
}],
..create_default_fleet()
},
..create_empty_problem()
};
let solution = Solution {
tours: vec![Tour {
vehicle_id: "my_vehicle_1".to_string(),
type_id: "my_vehicle".to_string(),
stops: vec![
create_stop_with_activity(
"departure",
"departure",
(0., 0.),
1,
("1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"),
0,
),
create_stop_with_activity(
"job1",
"delivery",
(2., 0.),
0,
("1970-01-01T00:00:02Z", "1970-01-01T00:00:03Z"),
2,
),
create_stop_with_activity(
"arrival",
"arrival",
(0., 0.),
0,
("1970-01-01T00:00:05Z", "1970-01-01T00:00:05Z"),
4,
),
],
..create_empty_tour()
}],
..create_empty_solution()
};
let core_problem = Arc::new(problem.clone().read_pragmatic().unwrap());
let ctx = CheckerContext::new(core_problem, problem, None, solution).unwrap();
let result = check_dispatch(&ctx);
assert_eq!(result, Err("tour should have dispatch, but none is found: 'my_vehicle_1'".to_owned()));
}
#[test]
fn can_detect_group_violations() {
let problem = Problem {
plan: Plan {
jobs: vec![
create_delivery_job_with_group("job1", (1., 0.), "group1"),
create_delivery_job_with_group("job2", (1., 0.), "group1"),
],
..create_empty_plan()
},
fleet: Fleet {
vehicles: vec![VehicleType {
vehicle_ids: vec!["v1".to_string(), "v2".to_string()],
..create_default_vehicle_type()
}],
..create_default_fleet()
},
..create_empty_problem()
};
let create_tour = |vehicle_id: &str, job_id: &str| Tour {
vehicle_id: vehicle_id.to_string(),
type_id: "my_vehicle".to_string(),
stops: vec![
create_stop_with_activity(
"departure",
"departure",
(0., 0.),
1,
("1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"),
0,
),
create_stop_with_activity(
job_id,
"delivery",
(1., 0.),
0,
("1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"),
1,
),
create_stop_with_activity(
"arrival",
"arrival",
(0., 0.),
0,
("1970-01-01T00:00:03Z", "1970-01-01T00:00:03Z"),
2,
),
],
..create_empty_tour()
};
let solution =
Solution { tours: vec![create_tour("v1", "job1"), create_tour("v2", "job2")], ..create_empty_solution() };
let core_problem = Arc::new(problem.clone().read_pragmatic().unwrap());
let ctx = CheckerContext::new(core_problem, problem, None, solution).unwrap();
let result = check_groups(&ctx);
assert_eq!(result, Err("job groups are not respected: 'group1'".to_owned()));
}