#![allow(clippy::doc_markdown)]
use crate::components::{ElevatorPhase, RiderPhase};
use crate::dispatch::scan::ScanDispatch;
use crate::door::{DoorCommand, DoorState};
use crate::entity::EntityId;
use crate::error::SimError;
use crate::events::Event;
use crate::sim::Simulation;
use crate::stop::StopId;
use crate::tests::helpers::default_config;
fn make_sim() -> (Simulation, EntityId) {
let config = default_config();
let sim = Simulation::new(&config, ScanDispatch::new()).unwrap();
let elev = sim.world().iter_elevators().next().unwrap().0;
(sim, elev)
}
fn drain_events(sim: &mut Simulation) -> Vec<Event> {
sim.drain_events()
}
fn has_applied(events: &[Event], cmd: DoorCommand) -> bool {
events.iter().any(|e| {
matches!(
e,
Event::DoorCommandApplied { command, .. } if *command == cmd
)
})
}
fn has_queued(events: &[Event], cmd: DoorCommand) -> bool {
events.iter().any(|e| {
matches!(
e,
Event::DoorCommandQueued { command, .. } if *command == cmd
)
})
}
#[test]
fn open_while_stopped_opens_doors() {
let (mut sim, elev) = make_sim();
assert!(matches!(
sim.world().elevator(elev).unwrap().door(),
DoorState::Closed
));
sim.request_door_open(elev).unwrap();
sim.step();
let phase = sim.world().elevator(elev).unwrap().phase();
assert!(
matches!(phase, ElevatorPhase::DoorOpening | ElevatorPhase::Loading),
"expected DoorOpening or Loading, got {phase}"
);
let events = drain_events(&mut sim);
assert!(has_applied(&events, DoorCommand::Open));
}
#[test]
fn close_during_open_forces_close() {
let (mut sim, elev) = make_sim();
sim.request_door_open(elev).unwrap();
for _ in 0..20 {
sim.step();
if sim.world().elevator(elev).unwrap().phase() == ElevatorPhase::Loading {
break;
}
}
assert_eq!(
sim.world().elevator(elev).unwrap().phase(),
ElevatorPhase::Loading
);
let _ = drain_events(&mut sim);
sim.request_door_close(elev).unwrap();
sim.step();
let phase = sim.world().elevator(elev).unwrap().phase();
assert!(
matches!(phase, ElevatorPhase::DoorClosing | ElevatorPhase::Stopped),
"expected DoorClosing or Stopped after forced close, got {phase}"
);
let events = drain_events(&mut sim);
assert!(has_applied(&events, DoorCommand::Close));
}
#[test]
fn open_reverses_closing_door() {
let (mut sim, elev) = make_sim();
sim.request_door_open(elev).unwrap();
for _ in 0..20 {
sim.step();
if sim.world().elevator(elev).unwrap().phase() == ElevatorPhase::Loading {
break;
}
}
sim.request_door_close(elev).unwrap();
sim.step();
let phase = sim.world().elevator(elev).unwrap().phase();
if phase != ElevatorPhase::DoorClosing {
return;
}
let _ = drain_events(&mut sim);
sim.request_door_open(elev).unwrap();
sim.step();
let phase = sim.world().elevator(elev).unwrap().phase();
assert!(
matches!(phase, ElevatorPhase::DoorOpening | ElevatorPhase::Loading),
"expected reversal to DoorOpening, got {phase}"
);
}
#[test]
fn close_waits_for_boarding_rider() {
let (mut sim, elev) = make_sim();
let rider = sim
.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0)
.unwrap();
sim.request_door_open(elev).unwrap();
let mut saw_boarding = false;
for _ in 0..30 {
sim.step();
if matches!(
sim.world().rider(rider).unwrap().phase,
RiderPhase::Boarding(e) if e == elev
) {
saw_boarding = true;
break;
}
}
assert!(saw_boarding, "rider should reach Boarding phase");
let _ = drain_events(&mut sim);
sim.request_door_close(elev).unwrap();
let pending = sim
.world()
.elevator(elev)
.unwrap()
.door_command_queue()
.to_vec();
assert!(
pending.contains(&DoorCommand::Close),
"Close must sit in the queue until the rider has crossed the threshold"
);
sim.step();
assert!(matches!(
sim.world().rider(rider).unwrap().phase,
RiderPhase::Riding(e) if e == elev
));
let events = drain_events(&mut sim);
assert!(has_applied(&events, DoorCommand::Close));
}
#[test]
fn hold_extends_open_timer() {
let (mut sim, elev) = make_sim();
sim.request_door_open(elev).unwrap();
for _ in 0..20 {
sim.step();
if sim.world().elevator(elev).unwrap().phase() == ElevatorPhase::Loading {
break;
}
}
assert_eq!(
sim.world().elevator(elev).unwrap().phase(),
ElevatorPhase::Loading
);
let _ = drain_events(&mut sim);
for _ in 0..5 {
sim.step();
}
sim.hold_door_open(elev, 20).unwrap();
sim.step(); for _ in 0..15 {
sim.step();
}
assert_eq!(
sim.world().elevator(elev).unwrap().phase(),
ElevatorPhase::Loading,
"hold should keep doors open"
);
}
#[test]
fn hold_is_cumulative() {
let (mut sim, elev) = make_sim();
sim.request_door_open(elev).unwrap();
for _ in 0..20 {
sim.step();
if sim.world().elevator(elev).unwrap().phase() == ElevatorPhase::Loading {
break;
}
}
let remaining_before = match sim.world().elevator(elev).unwrap().door() {
DoorState::Open {
ticks_remaining, ..
} => *ticks_remaining,
other => panic!("expected Open, got {other}"),
};
sim.hold_door_open(elev, 10).unwrap();
sim.hold_door_open(elev, 10).unwrap();
sim.step(); let remaining_after = match sim.world().elevator(elev).unwrap().door() {
DoorState::Open {
ticks_remaining, ..
} => *ticks_remaining,
other => panic!("expected Open, got {other}"),
};
assert_eq!(remaining_after, remaining_before + 20 - 1);
}
#[test]
fn cancel_hold_clamps_to_base() {
let (mut sim, elev) = make_sim();
sim.request_door_open(elev).unwrap();
for _ in 0..20 {
sim.step();
if sim.world().elevator(elev).unwrap().phase() == ElevatorPhase::Loading {
break;
}
}
let base = sim.world().elevator(elev).unwrap().door_open_ticks();
sim.hold_door_open(elev, 100).unwrap();
sim.step();
let held = match sim.world().elevator(elev).unwrap().door() {
DoorState::Open {
ticks_remaining, ..
} => *ticks_remaining,
_ => 0,
};
assert!(held > base, "hold should extend beyond base");
sim.cancel_door_hold(elev).unwrap();
sim.step();
let after = match sim.world().elevator(elev).unwrap().door() {
DoorState::Open {
ticks_remaining, ..
} => *ticks_remaining,
_ => 0,
};
assert!(
after <= base,
"cancel_door_hold should clamp remaining to <= base, got {after} > {base}"
);
}
#[test]
fn command_queued_during_motion_fires_on_arrival() {
let (mut sim, elev) = make_sim();
let dest = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, dest).unwrap();
for _ in 0..100 {
sim.step();
if sim.world().elevator(elev).unwrap().phase().is_moving() {
break;
}
}
assert!(sim.world().elevator(elev).unwrap().phase().is_moving());
let _ = drain_events(&mut sim);
sim.request_door_open(elev).unwrap();
let q_events = drain_events(&mut sim);
assert!(has_queued(&q_events, DoorCommand::Open));
assert!(!has_applied(&q_events, DoorCommand::Open));
let mut saw_applied = false;
for _ in 0..500 {
sim.step();
let events = drain_events(&mut sim);
if has_applied(&events, DoorCommand::Open) {
saw_applied = true;
break;
}
}
assert!(
saw_applied,
"queued Open command should apply once the car stops"
);
}
#[test]
fn unknown_elevator_errors() {
let (mut sim, _elev) = make_sim();
let rider = sim
.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0)
.unwrap();
assert!(matches!(
sim.request_door_open(rider),
Err(SimError::InvalidState { .. })
));
assert!(matches!(
sim.request_door_close(rider),
Err(SimError::InvalidState { .. })
));
assert!(matches!(
sim.hold_door_open(rider, 10),
Err(SimError::InvalidState { .. })
));
assert!(matches!(
sim.cancel_door_hold(rider),
Err(SimError::InvalidState { .. })
));
}
#[test]
fn hold_zero_ticks_rejected() {
let (mut sim, elev) = make_sim();
assert!(matches!(
sim.hold_door_open(elev, 0),
Err(SimError::InvalidConfig { .. })
));
}
#[test]
fn queue_is_capped() {
let (mut sim, elev) = make_sim();
let dest = sim.stop_entity(StopId(2)).unwrap();
sim.push_destination(elev, dest).unwrap();
for _ in 0..5 {
sim.step();
}
assert!(sim.world().elevator(elev).unwrap().phase().is_moving());
for i in 0..100 {
if i % 2 == 0 {
sim.request_door_open(elev).unwrap();
} else {
sim.hold_door_open(elev, 5).unwrap();
}
}
let q_len = sim
.world()
.elevator(elev)
.unwrap()
.door_command_queue()
.len();
assert!(
q_len <= crate::components::DOOR_COMMAND_QUEUE_CAP,
"queue length {q_len} exceeds cap {}",
crate::components::DOOR_COMMAND_QUEUE_CAP
);
}