1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//! Regression tests: elevators must never go idle with riders aboard.
//!
//! Root cause: `pending_stops_minus_covered` filters out a stop when
//! another car in a door-cycle or `MovingToStop` phase targets it —
//! but "covered" only checks *waiting* demand. If the stop's sole
//! demand comes from aboard riders (`riding_to_stop`) needing to exit
//! there, the filter erroneously removes it from the candidate set.
//! With no pending stops, `fallback()` returns `Idle`, stranding
//! passengers.
use crate::components::{ElevatorPhase, RiderPhase};
use crate::dispatch::etd::EtdDispatch;
use crate::sim::Simulation;
use crate::stop::StopId;
use super::helpers::{default_config, multi_floor_config};
/// Assert no elevator in the sim is idle with riders aboard.
fn assert_no_idle_with_riders(sim: &Simulation, tick: u64) {
for (eid, _pos, car) in sim.world().iter_elevators() {
assert!(
car.phase() != ElevatorPhase::Idle || car.riders().is_empty(),
"BUG: car {eid:?} went Idle with {} riders aboard at tick {tick}",
car.riders().len()
);
}
}
/// Two cars, both with riders heading to the same destination. When one
/// car is en route (`MovingToStop`), the other car (just finished
/// loading, now `Stopped`) must NOT go idle — its aboard riders still
/// need delivery.
#[test]
fn stopped_car_with_riders_not_idled_when_destination_covered() {
let cfg = multi_floor_config(3, 2);
let mut sim = Simulation::new(&cfg, EtdDispatch::new()).unwrap();
// Spawn two riders at stop 0 heading to stop 2.
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
let max_setup = 500;
for tick in 0..max_setup {
sim.step();
sim.drain_events();
// Check if both riders have been delivered (fast path — no bug).
let all_arrived = sim
.world()
.iter_riders()
.all(|(_, r)| r.phase == RiderPhase::Arrived);
if all_arrived {
return;
}
assert_no_idle_with_riders(&sim, tick);
}
panic!("riders not delivered within {max_setup} ticks");
}
/// Single car: after picking up a rider and closing doors, the car
/// must not go idle — it should proceed to the rider's destination.
/// This tests the eligibility path independent of `is_covered`.
#[test]
fn single_car_with_rider_not_idled_after_doors_close() {
let cfg = default_config();
let mut sim = Simulation::new(&cfg, EtdDispatch::new()).unwrap();
// Spawn a rider at stop 0 heading to stop 2.
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
for tick in 0..2000 {
sim.step();
sim.drain_events();
assert_no_idle_with_riders(&sim, tick);
let all_arrived = sim
.world()
.iter_riders()
.all(|(_, r)| r.phase == RiderPhase::Arrived);
if all_arrived {
return;
}
}
panic!("rider never arrived within 2000 ticks");
}
/// Convention-center-like scenario: burst of riders from one floor
/// to another, multiple cars. No car should ever idle with riders.
#[test]
fn burst_scenario_no_idle_with_riders() {
let cfg = multi_floor_config(5, 4);
let mut sim = Simulation::new(&cfg, EtdDispatch::new()).unwrap();
// Burst: 20 riders from stop 4 to stop 0.
for _ in 0..20 {
sim.spawn_rider(StopId(4), StopId(0), 75.0).unwrap();
}
for tick in 0..10_000 {
sim.step();
sim.drain_events();
assert_no_idle_with_riders(&sim, tick);
let all_arrived = sim
.world()
.iter_riders()
.all(|(_, r)| matches!(r.phase, RiderPhase::Arrived | RiderPhase::Abandoned));
if all_arrived {
return;
}
}
panic!("not all riders delivered within 10000 ticks");
}