use super::helpers::{default_config, scan};
use crate::builder::SimulationBuilder;
use crate::components::{AccessControl, Preferences, RiderPhase};
use crate::dispatch::BuiltinReposition;
use crate::dispatch::reposition::ReturnToLobby;
use crate::dispatch::{EtdDispatch, LookDispatch, NearestCarDispatch, ScanDispatch};
use crate::entity::EntityId;
use crate::error::SimError;
use crate::events::Event;
use crate::ids::GroupId;
use crate::sim::Simulation;
use crate::stop::{StopConfig, StopId};
use std::collections::HashSet;
#[test]
fn remove_elevator_despawns_from_world() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
assert!(sim.world().elevator(elevator_id).is_some());
sim.remove_elevator(elevator_id).unwrap();
assert!(sim.world().elevator(elevator_id).is_none());
}
#[test]
fn remove_elevator_removes_from_group_cache() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
assert!(sim.groups()[0].elevator_entities().contains(&elevator_id));
sim.remove_elevator(elevator_id).unwrap();
assert!(!sim.groups()[0].elevator_entities().contains(&elevator_id));
}
#[test]
fn remove_elevator_ejects_riders_aboard() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..300 {
sim.step();
let phase = sim.world().rider(rider_id).unwrap().phase;
if matches!(phase, RiderPhase::Riding(_)) {
break;
}
}
let elevator_id = sim.groups()[0].elevator_entities()[0];
assert!(
matches!(
sim.world().rider(rider_id).unwrap().phase,
RiderPhase::Riding(_)
),
"rider should have boarded within 300 ticks"
);
sim.remove_elevator(elevator_id).unwrap();
sim.drain_events();
let phase = sim.world().rider(rider_id).unwrap().phase;
assert!(
matches!(phase, RiderPhase::Waiting),
"rider should be Waiting after elevator removal, got {phase:?}"
);
}
#[test]
fn remove_elevator_ejects_rider_emits_event() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.drain_events();
let rider_id = sim
.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..300 {
sim.step();
if matches!(
sim.world().rider(rider_id).unwrap().phase,
RiderPhase::Riding(_)
) {
break;
}
}
let elevator_id = sim.groups()[0].elevator_entities()[0];
assert!(
matches!(
sim.world().rider(rider_id).unwrap().phase,
RiderPhase::Riding(_)
),
"rider should have boarded within 300 ticks"
);
sim.remove_elevator(elevator_id).unwrap();
let events = sim.drain_events();
let ejected = events
.iter()
.any(|e| matches!(e, Event::RiderEjected { rider, .. } if *rider == rider_id));
assert!(
ejected,
"should emit RiderEjected when removing elevator with rider aboard"
);
}
#[test]
fn remove_nonexistent_elevator_returns_entity_not_found() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let fake_id = EntityId::default();
let result = sim.remove_elevator(fake_id);
assert!(
matches!(result, Err(SimError::EntityNotFound(_))),
"expected EntityNotFound, got {result:?}"
);
}
#[test]
fn remove_stop_despawns_from_world() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let stop_id = sim.stop_entity(StopId(2)).unwrap();
assert!(sim.world().stop(stop_id).is_some());
sim.remove_stop(stop_id).unwrap();
assert!(sim.world().stop(stop_id).is_none());
}
#[test]
fn remove_stop_removes_from_group_stop_cache() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let stop_id = sim.stop_entity(StopId(2)).unwrap();
assert!(sim.groups()[0].stop_entities().contains(&stop_id));
sim.remove_stop(stop_id).unwrap();
assert!(!sim.groups()[0].stop_entities().contains(&stop_id));
}
#[test]
fn remove_stop_removes_from_stop_lookup() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let stop_id = sim.stop_entity(StopId(2)).unwrap();
assert!(stop_id != EntityId::default());
sim.remove_stop(stop_id).unwrap();
let after = sim.stop_entity(StopId(2));
assert!(
after.is_none(),
"stop_entity should return None after removal"
);
}
#[test]
fn remove_stop_with_waiting_rider_invalidates_route() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.drain_events();
let rider_id = sim
.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
let phase = sim.world().rider(rider_id).unwrap().phase;
assert_eq!(phase, RiderPhase::Waiting);
let stop2 = sim.stop_entity(StopId(2)).unwrap();
sim.remove_stop(stop2).unwrap();
let events = sim.drain_events();
let invalidated = events
.iter()
.any(|e| matches!(e, Event::RouteInvalidated { rider, .. } if *rider == rider_id));
assert!(
invalidated,
"should emit RouteInvalidated for rider targeting the removed stop"
);
}
#[test]
fn remove_nonexistent_stop_returns_entity_not_found() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let fake_id = EntityId::default();
let result = sim.remove_stop(fake_id);
assert!(
matches!(result, Err(SimError::EntityNotFound(_))),
"expected EntityNotFound, got {result:?}"
);
}
#[test]
fn drain_events_where_returns_only_matching_events() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..5 {
sim.step();
}
let matched = sim.drain_events_where(|e| matches!(e, Event::ElevatorAssigned { .. }));
for e in &matched {
assert!(
matches!(e, Event::ElevatorAssigned { .. }),
"unexpected event in matched set: {e:?}"
);
}
}
#[test]
fn drain_events_where_retains_non_matching_events() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..5 {
sim.step();
}
let total_before: Vec<Event> = {
sim.drain_events()
};
let total_count = total_before.len();
let config2 = default_config();
let mut sim2 = Simulation::new(&config2, scan()).unwrap();
sim2.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..5 {
sim2.step();
}
let matched = sim2.drain_events_where(|e| matches!(e, Event::ElevatorAssigned { .. }));
let remaining = sim2.drain_events();
assert_eq!(
matched.len() + remaining.len(),
total_count,
"drain_events_where + drain_events should account for all events"
);
for e in &remaining {
assert!(
!matches!(e, Event::ElevatorAssigned { .. }),
"ElevatorAssigned should have been drained already: {e:?}"
);
}
}
#[test]
fn drain_events_where_with_no_match_returns_empty_and_retains_all() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..5 {
sim.step();
}
let all_before = sim.drain_events();
let count_before = all_before.len();
let config2 = default_config();
let mut sim2 = Simulation::new(&config2, scan()).unwrap();
sim2.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..5 {
sim2.step();
}
let matched = sim2.drain_events_where(|_| false);
assert!(
matched.is_empty(),
"should return empty vec when nothing matches"
);
let remaining = sim2.drain_events();
assert_eq!(
remaining.len(),
count_before,
"all events should remain after a no-match drain"
);
}
#[test]
fn riders_on_returns_empty_for_idle_elevator() {
let config = default_config();
let sim = Simulation::new(&config, scan()).unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
assert!(sim.riders_on(elevator_id).is_empty());
}
#[test]
fn riders_on_returns_rider_ids_after_boarding() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
for _ in 0..300 {
sim.step();
if matches!(
sim.world().rider(rider_id).unwrap().phase,
RiderPhase::Riding(_)
) {
break;
}
}
if matches!(
sim.world().rider(rider_id).unwrap().phase,
RiderPhase::Riding(_)
) {
assert!(
sim.riders_on(elevator_id).contains(&rider_id),
"rider should appear in riders_on after boarding"
);
}
}
#[test]
fn riders_on_returns_empty_for_nonexistent_elevator() {
let config = default_config();
let sim = Simulation::new(&config, scan()).unwrap();
let fake_id = EntityId::default();
assert!(sim.riders_on(fake_id).is_empty());
}
#[test]
fn occupancy_returns_zero_for_idle_elevator() {
let config = default_config();
let sim = Simulation::new(&config, scan()).unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
assert_eq!(sim.occupancy(elevator_id), 0);
}
#[test]
fn occupancy_returns_correct_count_after_boarding() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
for _ in 0..300 {
sim.step();
if matches!(
sim.world().rider(rider_id).unwrap().phase,
RiderPhase::Riding(_)
) {
break;
}
}
if matches!(
sim.world().rider(rider_id).unwrap().phase,
RiderPhase::Riding(_)
) {
assert_eq!(
sim.occupancy(elevator_id),
1,
"occupancy should be 1 after one rider boards"
);
}
}
#[test]
fn occupancy_returns_zero_for_nonexistent_elevator() {
let config = default_config();
let sim = Simulation::new(&config, scan()).unwrap();
let fake_id = EntityId::default();
assert_eq!(sim.occupancy(fake_id), 0);
}
#[test]
fn iter_repositioning_elevators_empty_when_no_reposition() {
let config = default_config();
let sim = Simulation::new(&config, scan()).unwrap();
let repositioning: Vec<EntityId> = sim.iter_repositioning_elevators().collect();
assert!(
repositioning.is_empty(),
"no elevators should be repositioning without a reposition strategy"
);
}
#[test]
fn iter_repositioning_elevators_returns_elevator_during_reposition() {
let mut sim = SimulationBuilder::new()
.stops(vec![
StopConfig {
id: StopId(0),
name: "Lobby".into(),
position: 0.0,
},
StopConfig {
id: StopId(1),
name: "Mid".into(),
position: 10.0,
},
StopConfig {
id: StopId(2),
name: "Top".into(),
position: 20.0,
},
])
.elevator(crate::config::ElevatorConfig {
id: 0,
name: "A".into(),
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
starting_stop: StopId(2),
door_open_ticks: 10,
door_transition_ticks: 5,
restricted_stops: Vec::new(),
#[cfg(feature = "energy")]
energy_profile: None,
service_mode: None,
inspection_speed_factor: 0.25,
})
.dispatch(EtdDispatch::new())
.reposition(ReturnToLobby::new(), BuiltinReposition::ReturnToLobby)
.build()
.unwrap();
sim.drain_events();
sim.step();
let repositioning: Vec<EntityId> = sim.iter_repositioning_elevators().collect();
assert!(
!repositioning.is_empty(),
"elevator should be repositioning after first tick"
);
let elevator_ids = sim.world().elevator_ids();
for repo_id in &repositioning {
assert!(
elevator_ids.contains(repo_id),
"iter_repositioning_elevators returned an unknown elevator ID"
);
}
}
#[test]
fn iter_repositioning_elevators_empty_after_reposition_completes() {
let mut sim = SimulationBuilder::new()
.stops(vec![
StopConfig {
id: StopId(0),
name: "Lobby".into(),
position: 0.0,
},
StopConfig {
id: StopId(1),
name: "Mid".into(),
position: 10.0,
},
StopConfig {
id: StopId(2),
name: "Top".into(),
position: 20.0,
},
])
.elevator(crate::config::ElevatorConfig {
id: 0,
name: "A".into(),
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
starting_stop: StopId(2),
door_open_ticks: 10,
door_transition_ticks: 5,
restricted_stops: Vec::new(),
#[cfg(feature = "energy")]
energy_profile: None,
service_mode: None,
inspection_speed_factor: 0.25,
})
.dispatch(EtdDispatch::new())
.reposition(ReturnToLobby::new(), BuiltinReposition::ReturnToLobby)
.build()
.unwrap();
for _ in 0..2000 {
sim.step();
}
let repositioning: Vec<EntityId> = sim.iter_repositioning_elevators().collect();
assert!(
repositioning.is_empty(),
"no elevators should be repositioning after arrival at home stop"
);
}
#[test]
fn rider_builder_basic_spawn() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.spawn()
.unwrap();
let rider = sim.world().rider(rider_id).unwrap();
assert_eq!(rider.phase, RiderPhase::Waiting);
}
#[test]
fn rider_builder_custom_weight() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.weight(90.0)
.spawn()
.unwrap();
let rider = sim.world().rider(rider_id).unwrap();
assert!(
(rider.weight - 90.0).abs() < f64::EPSILON,
"rider weight should be 90.0, got {}",
rider.weight
);
}
#[test]
fn rider_builder_with_explicit_group() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.group(GroupId(0))
.spawn()
.unwrap();
let rider = sim.world().rider(rider_id).unwrap();
assert_eq!(rider.phase, RiderPhase::Waiting);
}
#[test]
fn rider_builder_with_patience() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.patience(100)
.spawn()
.unwrap();
let patience = sim.world().patience(rider_id).unwrap();
assert_eq!(
patience.max_wait_ticks(),
100,
"patience max_wait_ticks should be 100"
);
assert_eq!(patience.waited_ticks(), 0);
}
#[test]
fn rider_builder_with_preferences() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let prefs = Preferences {
skip_full_elevator: true,
max_crowding_factor: 0.5,
};
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.preferences(prefs)
.spawn()
.unwrap();
let stored = sim.world().preferences(rider_id).unwrap();
assert!(
stored.skip_full_elevator(),
"skip_full_elevator should be true"
);
assert!(
(stored.max_crowding_factor() - 0.5).abs() < f64::EPSILON,
"max_crowding_factor should be 0.5"
);
}
#[test]
fn rider_builder_with_access_control() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let stop0 = sim.stop_entity(StopId(0)).unwrap();
let stop2 = sim.stop_entity(StopId(2)).unwrap();
let mut allowed = HashSet::new();
allowed.insert(stop0);
allowed.insert(stop2);
let ac = AccessControl::new(allowed.clone());
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.access_control(ac)
.spawn()
.unwrap();
let stored = sim.world().access_control(rider_id).unwrap();
assert!(
stored.can_access(stop0),
"rider should have access to stop 0"
);
assert!(
stored.can_access(stop2),
"rider should have access to stop 2"
);
}
#[test]
fn rider_builder_invalid_stop_id_returns_stop_not_found() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let result = sim.build_rider_by_stop_id(StopId(0), StopId(99));
assert!(
matches!(result, Err(SimError::StopNotFound(StopId(99)))),
"expected StopNotFound(99)"
);
}
#[test]
fn rider_builder_no_route_when_stops_not_in_same_group() {
let mut sim = SimulationBuilder::new()
.stops(vec![
StopConfig {
id: StopId(0),
name: "A".into(),
position: 0.0,
},
StopConfig {
id: StopId(1),
name: "B".into(),
position: 10.0,
},
])
.elevator(crate::config::ElevatorConfig {
id: 0,
name: "E1".into(),
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
starting_stop: StopId(0),
door_open_ticks: 10,
door_transition_ticks: 5,
restricted_stops: Vec::new(),
#[cfg(feature = "energy")]
energy_profile: None,
service_mode: None,
inspection_speed_factor: 0.25,
})
.build()
.unwrap();
let stop1 = sim.stop_entity(StopId(1)).unwrap();
sim.remove_stop(stop1).unwrap();
let result = sim.build_rider_by_stop_id(StopId(0), StopId(1));
assert!(
matches!(result, Err(SimError::StopNotFound(_))),
"expected StopNotFound after stop removed"
);
}
#[test]
fn rider_builder_spawn_returns_no_route_when_group_not_serving_both_stops() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let result = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.group(GroupId(99))
.spawn();
assert!(
matches!(result, Err(SimError::GroupNotFound(GroupId(99)))),
"expected GroupNotFound(99), got {result:?}"
);
}
#[test]
fn dispatch_scan_resolves() {
let _: ScanDispatch = ScanDispatch::new();
}
#[test]
fn dispatch_look_resolves() {
let _: LookDispatch = LookDispatch::new();
}
#[test]
fn dispatch_etd_resolves() {
let _: EtdDispatch = EtdDispatch::new();
}
#[test]
fn dispatch_nearest_car_resolves() {
let _: NearestCarDispatch = NearestCarDispatch::new();
}
#[test]
fn remove_elevator_then_riders_on_returns_empty() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
sim.remove_elevator(elevator_id).unwrap();
assert!(sim.riders_on(elevator_id).is_empty());
}
#[test]
fn remove_elevator_then_occupancy_returns_zero() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let elevator_id = sim.groups()[0].elevator_entities()[0];
sim.remove_elevator(elevator_id).unwrap();
assert_eq!(sim.occupancy(elevator_id), 0);
}
#[test]
fn drain_events_where_with_all_matching_empties_buffer() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 70.0)
.unwrap();
for _ in 0..5 {
sim.step();
}
let all = sim.drain_events_where(|_| true);
assert!(!all.is_empty(), "should have some events to drain");
let remaining = sim.drain_events();
assert!(
remaining.is_empty(),
"buffer should be empty after all-matching drain"
);
}
#[test]
fn rider_builder_default_weight_is_75() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.spawn()
.unwrap();
let rider = sim.world().rider(rider_id).unwrap();
assert!(
(rider.weight - 75.0).abs() < f64::EPSILON,
"default weight should be 75.0, got {}",
rider.weight
);
}
#[test]
fn rider_builder_no_patience_by_default() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.spawn()
.unwrap();
assert!(
sim.world().patience(rider_id).is_none(),
"rider should have no Patience component by default"
);
}
#[test]
fn rider_builder_no_preferences_by_default() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.spawn()
.unwrap();
assert!(
sim.world().preferences(rider_id).is_none(),
"rider should have no Preferences component by default"
);
}
#[test]
fn rider_builder_no_access_control_by_default() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
let rider_id = sim
.build_rider_by_stop_id(StopId(0), StopId(2))
.unwrap()
.spawn()
.unwrap();
assert!(
sim.world().access_control(rider_id).is_none(),
"rider should have no AccessControl component by default"
);
}
#[test]
fn remove_elevator_emits_elevator_removed_event() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.drain_events();
let elevator_id = sim.groups()[0].elevator_entities()[0];
sim.remove_elevator(elevator_id).unwrap();
let events = sim.drain_events();
let removed = events
.iter()
.any(|e| matches!(e, Event::ElevatorRemoved { elevator, .. } if *elevator == elevator_id));
assert!(removed, "should emit ElevatorRemoved event");
}
#[test]
fn remove_stop_emits_stop_removed_event() {
let config = default_config();
let mut sim = Simulation::new(&config, scan()).unwrap();
sim.drain_events();
let stop_eid = sim.stop_entity(StopId(2)).unwrap();
sim.remove_stop(stop_eid).unwrap();
let events = sim.drain_events();
let removed = events
.iter()
.any(|e| matches!(e, Event::StopRemoved { stop, .. } if *stop == stop_eid));
assert!(removed, "should emit StopRemoved event");
}