use crate::components::{Elevator, ElevatorPhase, Preferences, Rider, RiderPhase, Stop};
use crate::dispatch::etd::EtdDispatch;
use crate::dispatch::scan::ScanDispatch;
use crate::dispatch::{
self, DispatchDecision, DispatchManifest, ElevatorGroup, LineInfo, RiderInfo,
};
use crate::door::DoorState;
use crate::entity::EntityId;
use crate::events::Event;
use crate::ids::GroupId;
use crate::metrics::Metrics;
use crate::sim::Simulation;
use crate::stop::StopId;
use crate::world::World;
use std::collections::HashSet;
use super::helpers;
fn three_stop_sim() -> Simulation {
let mut sim = Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
sim.drain_events();
sim
}
fn run_until_all_delivered(sim: &mut Simulation, count: u64, max_ticks: u64) -> u64 {
for tick in 0..max_ticks {
sim.step();
if sim.metrics().total_delivered() >= count {
return tick;
}
}
panic!("not all delivered within {max_ticks} ticks");
}
#[test]
fn metrics_records_spawn_board_delivery_in_sequence() {
let mut sim = three_stop_sim();
sim.spawn_rider(StopId(0), StopId(2), 72.0).unwrap();
run_until_all_delivered(&mut sim, 1, 2000);
let m = sim.metrics();
assert_eq!(m.total_delivered(), 1);
assert!(m.avg_wait_time() > 0.0, "avg_wait_time should be > 0");
assert!(m.avg_ride_time() > 0.0, "avg_ride_time should be > 0");
}
#[test]
fn metrics_accumulates_distance_proportional_to_run_length() {
let mut sim_short = three_stop_sim();
sim_short.spawn_rider(StopId(0), StopId(1), 75.0).unwrap();
run_until_all_delivered(&mut sim_short, 1, 2000);
let short_dist = sim_short.metrics().total_distance();
let mut sim_long = three_stop_sim();
sim_long.spawn_rider(StopId(0), StopId(2), 75.0).unwrap();
run_until_all_delivered(&mut sim_long, 1, 2000);
let long_dist = sim_long.metrics().total_distance();
assert!(
long_dist > short_dist,
"long trip should accumulate more distance: short={short_dist}, long={long_dist}"
);
assert!(long_dist > 5.0, "long-trip distance should be > 5 units");
}
#[test]
fn metrics_records_abandonment() {
let mut sim = three_stop_sim();
let rid = sim
.build_rider(StopId(0), StopId(2))
.unwrap()
.patience(1)
.spawn()
.unwrap();
let _ = rid;
for _ in 0..200 {
sim.step();
if sim.metrics().total_abandoned() > 0 {
break;
}
}
assert!(
sim.metrics().total_abandoned() > 0,
"rider with patience=1 must eventually abandon"
);
}
#[test]
fn metrics_utilization_is_zero_when_no_elevators_moving() {
let mut sim = three_stop_sim();
sim.step(); let utils = &sim.metrics().utilization_by_group;
assert!(
utils.values().all(|&u| (u - 0.0).abs() < 1e-9),
"utilization should be 0.0 for all groups with no active riders"
);
}
#[test]
fn metrics_distance_stays_zero_on_idle_ticks() {
let mut sim = three_stop_sim();
for _ in 0..10 {
sim.step();
}
assert_eq!(sim.metrics().total_distance(), 0.0);
}
#[test]
fn dispatch_manifest_wait_ticks_grows_with_time() {
let (mut world, stops) = dispatch_world();
let _elev = spawn_elev(&mut world, 0.0, 1);
let rid = world.spawn();
world.set_rider(
rid,
Rider {
weight: 70.0,
phase: RiderPhase::Waiting,
current_stop: Some(stops[1]),
spawn_tick: 0,
board_tick: None,
},
);
let manifest_0 = build_test_manifest(&world, stops[1], rid, 0);
let at_stop = manifest_0.waiting_at_stop.get(&stops[1]).unwrap();
assert_eq!(at_stop[0].wait_ticks, 0);
let manifest_100 = build_test_manifest(&world, stops[1], rid, 100);
let at_stop = manifest_100.waiting_at_stop.get(&stops[1]).unwrap();
assert_eq!(at_stop[0].wait_ticks, 100);
}
fn dispatch_world() -> (World, Vec<EntityId>) {
let mut world = World::new();
let stops: Vec<_> = [("G", 0.0), ("M", 4.0), ("R", 8.0)]
.iter()
.map(|(n, p)| {
let eid = world.spawn();
world.set_stop(
eid,
Stop {
name: (*n).into(),
position: *p,
},
);
eid
})
.collect();
(world, stops)
}
fn spawn_elev(world: &mut World, pos: f64, n: usize) -> Vec<EntityId> {
(0..n)
.map(|_| {
let eid = world.spawn();
world.set_position(eid, crate::components::Position { value: pos });
world.set_velocity(eid, crate::components::Velocity { value: 0.0 });
world.set_elevator(
eid,
Elevator {
phase: ElevatorPhase::Idle,
door: DoorState::Closed,
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
current_load: 0.0,
riders: vec![],
target_stop: None,
door_transition_ticks: 5,
door_open_ticks: 10,
line: EntityId::default(),
repositioning: false,
restricted_stops: HashSet::new(),
inspection_speed_factor: 0.25,
going_up: true,
going_down: true,
move_count: 0,
door_command_queue: Vec::new(),
manual_target_velocity: None,
},
);
eid
})
.collect()
}
fn build_test_manifest(
_world: &World,
at_stop: EntityId,
rider: EntityId,
tick: u64,
) -> DispatchManifest {
let mut m = DispatchManifest::default();
m.waiting_at_stop
.entry(at_stop)
.or_default()
.push(RiderInfo {
id: rider,
destination: None,
weight: 70.0,
wait_ticks: tick,
});
m
}
#[test]
fn etd_prefers_closer_elevator_to_call() {
let config = helpers::default_config();
let mut sim = Simulation::new(&config, EtdDispatch::new()).unwrap();
sim.drain_events();
let line = sim.lines_in_group(GroupId(0))[0];
let params = crate::sim::ElevatorParams::default();
let _elev_b = sim.add_elevator(¶ms, line, 4.0).unwrap();
let _elev_a = sim.groups()[0].elevator_entities()[0];
sim.spawn_rider(StopId(2), StopId(0), 70.0).unwrap();
sim.step();
let events = sim.drain_events();
let assigned = events.iter().any(|e| {
matches!(e, Event::ElevatorAssigned { stop, .. }
if sim.stop_entity(StopId(2)).is_some_and(|s| s == *stop))
});
assert!(assigned, "ETD should assign an elevator to stop 2");
}
#[test]
fn etd_infinity_cost_path_exists() {
let config = helpers::default_config();
let mut sim = Simulation::new(&config, EtdDispatch::new()).unwrap();
sim.drain_events();
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.step();
let any_assigned = sim
.drain_events()
.iter()
.any(|e| matches!(e, Event::ElevatorAssigned { .. }));
assert!(any_assigned);
}
#[test]
fn loading_accepts_rider_exactly_at_capacity() {
let config = helpers::default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
sim.drain_events();
sim.spawn_rider(StopId(0), StopId(2), 800.0).unwrap();
for _ in 0..2000 {
sim.step();
if sim.metrics().total_delivered() > 0 {
break;
}
}
assert_eq!(
sim.metrics().total_delivered(),
1,
"rider at exact capacity should board and be delivered"
);
}
#[test]
fn loading_preference_boundary_allows_exact_match() {
let config = helpers::default_config();
let mut sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
sim.drain_events();
let rider = sim
.build_rider(StopId(0), StopId(2))
.unwrap()
.preferences(Preferences {
skip_full_elevator: true,
max_crowding_factor: 0.0,
balk_threshold_ticks: None,
abandon_on_full: false,
})
.spawn()
.unwrap();
let _ = rider;
for _ in 0..1500 {
sim.step();
if sim.metrics().total_delivered() > 0 {
break;
}
}
assert_eq!(sim.metrics().total_delivered(), 1);
}
#[test]
fn scan_at_stop_position_does_not_target_self() {
let (mut world, stops) = dispatch_world();
let elevs = spawn_elev(&mut world, 0.0, 1); let elev = elevs[0];
let group = ElevatorGroup::new(
GroupId(0),
"default".into(),
vec![LineInfo::new(
EntityId::default(),
vec![elev],
stops.clone(),
)],
);
let mut manifest = DispatchManifest::default();
for stop in [stops[0], stops[2]] {
manifest
.waiting_at_stop
.entry(stop)
.or_default()
.push(RiderInfo {
id: world.spawn(),
destination: None,
weight: 70.0,
wait_ticks: 0,
});
}
let mut scan = ScanDispatch::new();
let result = dispatch::assign(&mut scan, &[(elev, 0.0)], &group, &manifest, &world);
let decision = result.decisions[0].1.clone();
assert_eq!(decision, DispatchDecision::GoToStop(stops[2]));
}
#[test]
fn metrics_avg_wait_time_matches_recorded_sum() {
let mut m = Metrics::default();
m.record_spawn();
m.record_spawn();
m.record_board(10);
m.record_board(30);
assert!((m.avg_wait_time() - 20.0).abs() < 1e-9);
assert_eq!(m.max_wait_time(), 30);
}
#[test]
fn metrics_max_wait_time_keeps_the_peak() {
let mut m = Metrics::default();
m.record_spawn();
m.record_board(50);
m.record_board(10); assert_eq!(m.max_wait_time(), 50);
}
#[test]
fn metrics_record_delivery_increments_delivered() {
let mut m = Metrics::default();
m.record_spawn();
m.record_board(10);
m.record_delivery(100, 110);
assert_eq!(m.total_delivered(), 1);
assert!((m.avg_ride_time() - 100.0).abs() < 1e-9);
}
#[test]
fn metrics_record_abandonment_increments_counter() {
let mut m = Metrics::default();
m.record_spawn();
m.record_abandonment();
assert_eq!(m.total_abandoned(), 1);
assert_eq!(m.total_delivered(), 0);
}
#[test]
fn metrics_record_distance_accumulates() {
let mut m = Metrics::default();
m.record_distance(10.0);
m.record_distance(5.0);
assert!((m.total_distance() - 15.0).abs() < 1e-9);
}