use super::dispatch_tests::{
add_demand, decide_all, decide_one, spawn_elevator, test_group, test_world,
};
use super::helpers::default_config;
use crate::components::{ElevatorPhase, Route, Weight};
use crate::dispatch::etd::EtdDispatch;
use crate::dispatch::nearest_car::NearestCarDispatch;
use crate::dispatch::{DispatchDecision, DispatchManifest, RiderInfo};
use crate::sim::Simulation;
#[test]
fn etd_upward_bypass_skips_pickup_above_threshold() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
{
let car = world.elevator_mut(elev).unwrap();
car.current_load = Weight::from(car.weight_capacity.value() * 0.9);
car.set_bypass_load_up_pct(Some(0.80));
car.phase = ElevatorPhase::MovingToStop(stops[3]);
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[0], stops[3], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
manifest
.riding_to_stop
.entry(stops[3])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[3]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
let decision = decide_one(&mut etd, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[3]),
"a car above the up-bypass threshold must skip pickups and head to the aboard \
rider's destination"
);
}
#[test]
fn nearest_car_downward_bypass_skips_pickup_above_threshold() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 12.0);
{
let car = world.elevator_mut(elev).unwrap();
car.current_load = Weight::from(car.weight_capacity.value() * 0.6);
car.set_bypass_load_down_pct(Some(0.50));
car.phase = ElevatorPhase::MovingToStop(stops[0]);
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[3], stops[0], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
manifest
.riding_to_stop
.entry(stops[0])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[0]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut nc = NearestCarDispatch::new();
let decision = decide_one(&mut nc, elev, 12.0, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[0]),
"a downward-moving car above its down-bypass threshold must not detour for a pickup"
);
}
#[test]
fn bypass_below_threshold_still_picks_up() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
{
let car = world.elevator_mut(elev).unwrap();
car.current_load = Weight::from(car.weight_capacity.value() * 0.5);
car.set_bypass_load_up_pct(Some(0.80));
car.phase = ElevatorPhase::MovingToStop(stops[3]);
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[0], stops[3], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[1], 70.0);
manifest
.riding_to_stop
.entry(stops[3])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[3]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
let decisions = decide_all(&mut etd, &[(elev, 0.0)], &group, &manifest, &mut world);
assert_eq!(
decisions[0].1,
DispatchDecision::GoToStop(stops[1]),
"below-threshold car must still honor on-the-way pickups"
);
}
#[test]
fn bypass_is_per_car_not_group_wide() {
let (mut world, stops) = test_world();
let elev_full = spawn_elevator(&mut world, 0.0);
let elev_empty = spawn_elevator(&mut world, 4.0);
{
let car = world.elevator_mut(elev_full).unwrap();
car.current_load = Weight::from(car.weight_capacity.value() * 0.9);
car.set_bypass_load_up_pct(Some(0.80));
car.phase = ElevatorPhase::MovingToStop(stops[3]);
}
{
let car = world.elevator_mut(elev_empty).unwrap();
car.set_bypass_load_up_pct(Some(0.80));
car.phase = ElevatorPhase::MovingToStop(stops[3]);
}
let aboard = world.spawn();
world.elevator_mut(elev_full).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[0], stops[3], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev_full, elev_empty]);
let mut manifest = DispatchManifest::default();
add_demand(&mut manifest, &mut world, stops[2], 70.0);
manifest
.riding_to_stop
.entry(stops[3])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[3]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
let decisions = decide_all(
&mut etd,
&[(elev_full, 0.0), (elev_empty, 4.0)],
&group,
&manifest,
&mut world,
);
let empty_dec = decisions.iter().find(|(e, _)| *e == elev_empty).unwrap();
assert_eq!(
empty_dec.1,
DispatchDecision::GoToStop(stops[2]),
"the below-threshold car must take the pickup even when a peer bypasses"
);
}
#[test]
fn bypass_never_blocks_aboard_exit() {
let (mut world, stops) = test_world();
let elev = spawn_elevator(&mut world, 0.0);
{
let car = world.elevator_mut(elev).unwrap();
car.current_load = car.weight_capacity;
car.set_bypass_load_up_pct(Some(0.10));
car.set_bypass_load_down_pct(Some(0.10));
car.phase = ElevatorPhase::MovingToStop(stops[2]);
}
let aboard = world.spawn();
world.elevator_mut(elev).unwrap().riders.push(aboard);
world.set_route(
aboard,
Route::direct(stops[0], stops[2], crate::ids::GroupId(0)),
);
let group = test_group(&stops, vec![elev]);
let mut manifest = DispatchManifest::default();
manifest
.riding_to_stop
.entry(stops[2])
.or_default()
.push(RiderInfo {
id: aboard,
destination: Some(stops[2]),
weight: Weight::from(70.0),
wait_ticks: 0,
});
let mut etd = EtdDispatch::new();
let decision = decide_one(&mut etd, elev, 0.0, &group, &manifest, &mut world);
assert_eq!(
decision,
DispatchDecision::GoToStop(stops[2]),
"bypass must never prevent the car from reaching an aboard rider's destination"
);
}
#[test]
fn bypass_pct_out_of_range_rejected_at_construction() {
for bad in [f64::NAN, -0.1, 0.0, 1.01, f64::INFINITY] {
for (field_name, up, down) in [
("bypass_load_up_pct", Some(bad), None),
("bypass_load_down_pct", None, Some(bad)),
] {
let mut config = default_config();
config.elevators[0].bypass_load_up_pct = up;
config.elevators[0].bypass_load_down_pct = down;
let err = Simulation::new(&config, crate::dispatch::scan::ScanDispatch::new())
.expect_err(&format!("Simulation::new must reject {field_name} = {bad}"));
assert!(
matches!(&err, crate::error::SimError::InvalidConfig { field, .. } if field.contains(field_name)),
"expected InvalidConfig on {field_name} for {bad}, got {err:?}"
);
}
}
}
#[test]
#[should_panic(expected = "wait_squared_weight must be finite and non-negative")]
fn etd_wait_squared_weight_rejects_nan() {
let _ = EtdDispatch::new().with_wait_squared_weight(f64::NAN);
}
#[test]
#[should_panic(expected = "wait_squared_weight must be finite and non-negative")]
fn etd_wait_squared_weight_rejects_negative() {
let _ = EtdDispatch::new().with_wait_squared_weight(-1.0);
}
#[test]
#[should_panic(expected = "PredictiveParking::with_window_ticks requires a positive window")]
fn predictive_parking_rejects_zero_window() {
let _ = crate::dispatch::reposition::PredictiveParking::with_window_ticks(0);
}