use std::collections::HashSet;
use crate::components::AccessControl;
use crate::error::RejectionReason;
use crate::events::Event;
use crate::stop::StopId;
use super::helpers;
#[test]
fn rider_rejected_by_elevator_restriction() {
let mut config = helpers::default_config();
config.elevators[0].restricted_stops = vec![StopId(2)];
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
sim.spawn_rider(StopId(0), StopId(2), 70.0)
.expect("spawn should succeed");
let mut all_events = Vec::new();
for _ in 0..500 {
sim.step();
all_events.extend(sim.drain_events());
}
let rejected = all_events.iter().any(|e| {
matches!(
e,
Event::RiderRejected {
reason: RejectionReason::AccessDenied,
..
}
)
});
assert!(rejected, "rider should be rejected with AccessDenied");
assert!(
!helpers::all_riders_arrived(&sim),
"rider should not arrive when the only elevator is restricted"
);
}
#[test]
fn rider_rejected_by_rider_access_control() {
let config = helpers::default_config();
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
let rider = sim
.spawn_rider(StopId(0), StopId(2), 70.0)
.expect("spawn should succeed");
let stop0 = sim.stop_entity(StopId(0)).expect("stop 0 exists");
let stop1 = sim.stop_entity(StopId(1)).expect("stop 1 exists");
sim.set_rider_access(rider.entity(), HashSet::from([stop0, stop1]))
.expect("set_rider_access should succeed");
let mut all_events = Vec::new();
for _ in 0..500 {
sim.step();
all_events.extend(sim.drain_events());
}
let rejected = all_events.iter().any(|e| {
matches!(
e,
Event::RiderRejected {
reason: RejectionReason::AccessDenied,
..
}
)
});
assert!(
rejected,
"rider should be rejected with AccessDenied due to rider access control"
);
}
#[test]
fn rider_boards_without_restrictions() {
let config = helpers::default_config();
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
sim.spawn_rider(StopId(0), StopId(2), 70.0)
.expect("spawn should succeed");
for _ in 0..2000 {
sim.step();
if helpers::all_riders_arrived(&sim) {
break;
}
}
assert!(
helpers::all_riders_arrived(&sim),
"rider should arrive when no restrictions"
);
}
#[test]
fn rider_boards_when_destination_in_allowed_stops() {
let config = helpers::default_config();
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
let rider = sim
.spawn_rider(StopId(0), StopId(2), 70.0)
.expect("spawn should succeed");
let stop0 = sim.stop_entity(StopId(0)).expect("stop 0 exists");
let stop1 = sim.stop_entity(StopId(1)).expect("stop 1 exists");
let stop2 = sim.stop_entity(StopId(2)).expect("stop 2 exists");
sim.set_rider_access(rider.entity(), HashSet::from([stop0, stop1, stop2]))
.expect("set_rider_access should succeed");
for _ in 0..2000 {
sim.step();
if helpers::all_riders_arrived(&sim) {
break;
}
}
assert!(
helpers::all_riders_arrived(&sim),
"rider should arrive when destination is in allowed_stops"
);
}
#[test]
fn restriction_does_not_affect_unrestricted_destinations() {
let mut config = helpers::default_config();
config.elevators[0].restricted_stops = vec![StopId(2)];
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
sim.spawn_rider(StopId(0), StopId(1), 70.0)
.expect("spawn should succeed");
for _ in 0..2000 {
sim.step();
if helpers::all_riders_arrived(&sim) {
break;
}
}
assert!(
helpers::all_riders_arrived(&sim),
"rider to unrestricted stop should arrive"
);
}
#[test]
fn both_restriction_types_work_in_same_sim() {
let mut config = helpers::default_config();
config.elevators[0].restricted_stops = vec![StopId(1)];
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
let rider1 = sim
.spawn_rider(StopId(0), StopId(1), 70.0)
.expect("spawn should succeed");
let mut events_phase1 = Vec::new();
for _ in 0..200 {
sim.step();
events_phase1.extend(sim.drain_events());
}
let rider1_rejected = events_phase1.iter().any(|e| {
matches!(
e,
Event::RiderRejected {
rider,
reason: RejectionReason::AccessDenied,
..
} if *rider == rider1.entity()
)
});
assert!(
rider1_rejected,
"rider1 (elevator-restricted) should be rejected"
);
sim.despawn_rider(rider1).expect("despawn should succeed");
let rider2 = sim
.spawn_rider(StopId(0), StopId(2), 70.0)
.expect("spawn should succeed");
let stop0 = sim.stop_entity(StopId(0)).expect("stop 0 exists");
let stop1 = sim.stop_entity(StopId(1)).expect("stop 1 exists");
sim.set_rider_access(rider2.entity(), HashSet::from([stop0, stop1]))
.expect("set_rider_access should succeed");
let mut events_phase2 = Vec::new();
for _ in 0..200 {
sim.step();
events_phase2.extend(sim.drain_events());
}
let rider2_rejected = events_phase2.iter().any(|e| {
matches!(
e,
Event::RiderRejected {
rider,
reason: RejectionReason::AccessDenied,
..
} if *rider == rider2.entity()
)
});
assert!(
rider2_rejected,
"rider2 (access-control-restricted) should be rejected"
);
}
#[test]
fn rejection_event_has_access_denied_reason() {
let mut config = helpers::default_config();
config.elevators[0].restricted_stops = vec![StopId(2)];
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
let rider = sim
.spawn_rider(StopId(0), StopId(2), 70.0)
.expect("spawn should succeed");
let mut all_events = Vec::new();
for _ in 0..500 {
sim.step();
all_events.extend(sim.drain_events());
}
let rejection = all_events.iter().find(|e| {
matches!(
e,
Event::RiderRejected {
reason: RejectionReason::AccessDenied,
..
}
)
});
assert!(rejection.is_some(), "should have AccessDenied rejection");
if let Some(Event::RiderRejected {
rider: rid,
reason,
context,
..
}) = rejection
{
assert_eq!(*rid, rider.entity());
assert_eq!(*reason, RejectionReason::AccessDenied);
assert!(context.is_none(), "AccessDenied should have no context");
}
}
#[test]
fn elevator_not_dispatched_to_restricted_stop() {
let mut config = helpers::default_config();
config.elevators[0].restricted_stops = vec![StopId(2)];
let mut sim =
crate::sim::Simulation::new(&config, helpers::scan()).expect("config should be valid");
sim.spawn_rider(StopId(0), StopId(2), 70.0)
.expect("spawn should succeed");
let mut all_events = Vec::new();
for _ in 0..500 {
sim.step();
all_events.extend(sim.drain_events());
}
let stop2 = sim.stop_entity(StopId(2)).expect("stop 2 exists");
let dispatched_to_restricted = all_events
.iter()
.any(|e| matches!(e, Event::ElevatorAssigned { stop, .. } if *stop == stop2));
assert!(
!dispatched_to_restricted,
"elevator should never be assigned to a restricted stop"
);
}
#[test]
fn access_control_serde_roundtrip() {
let stop_id = crate::entity::EntityId::default();
let ac = AccessControl::new(HashSet::from([stop_id]));
let serialized = ron::to_string(&ac).expect("serialize should succeed");
let deserialized: AccessControl =
ron::from_str(&serialized).expect("deserialize should succeed");
assert!(deserialized.can_access(stop_id));
}
#[test]
fn config_restricted_stops_serde_roundtrip() {
let mut config = helpers::default_config();
config.elevators[0].restricted_stops = vec![StopId(1), StopId(2)];
let serialized = ron::to_string(&config).expect("serialize should succeed");
let deserialized: crate::config::SimConfig =
ron::from_str(&serialized).expect("deserialize should succeed");
assert_eq!(deserialized.elevators[0].restricted_stops.len(), 2);
assert!(
deserialized.elevators[0]
.restricted_stops
.contains(&StopId(1))
);
assert!(
deserialized.elevators[0]
.restricted_stops
.contains(&StopId(2))
);
}
#[test]
fn access_control_empty_allows_any_stop() {
use crate::entity::EntityId;
let ac = AccessControl::default();
assert!(ac.allowed_stops().is_empty());
assert!(ac.can_access(EntityId::default()));
}
#[test]
fn access_control_non_empty_is_an_allowlist() {
use crate::entity::EntityId;
let allowed = EntityId::default();
let mut set = HashSet::new();
set.insert(allowed);
let ac = AccessControl::new(set);
assert!(ac.can_access(allowed));
}
#[test]
fn run_until_quiet_returns_zero_for_empty_sim() {
use crate::tests::helpers;
let mut sim = crate::sim::Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
let ticks = sim
.run_until_quiet(1_000)
.expect("empty sim is already quiet");
assert_eq!(ticks, 0);
assert_eq!(sim.current_tick(), 0);
}
#[test]
fn run_until_quiet_delivers_rider_within_budget() {
use crate::stop::StopId;
use crate::tests::helpers;
let mut sim = crate::sim::Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
let ticks = sim.run_until_quiet(5_000).expect("rider must deliver");
assert!(ticks > 0, "delivery took zero ticks?");
assert!(ticks <= 5_000);
assert_eq!(sim.metrics().total_delivered(), 1);
}
#[test]
fn run_until_quiet_returns_err_on_budget_exhaustion() {
use crate::stop::StopId;
use crate::tests::helpers;
let mut sim = crate::sim::Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
sim.spawn_rider(StopId(0), StopId(2), 70.0).unwrap();
let err = sim.run_until_quiet(2).unwrap_err();
assert_eq!(err, 2, "error should carry the exhausted budget");
assert_eq!(
sim.current_tick(),
2,
"sim state must reflect the steps taken"
);
}
#[test]
fn set_dispatch_prefers_strategy_builtin_id_over_arg() {
use crate::dispatch::BuiltinStrategy;
use crate::dispatch::look::LookDispatch;
use crate::ids::GroupId;
use crate::sim::Simulation;
use crate::tests::helpers;
let mut sim = Simulation::new(&helpers::default_config(), helpers::scan()).unwrap();
sim.set_dispatch(
GroupId(0),
Box::new(LookDispatch::new()),
BuiltinStrategy::Destination,
);
assert_eq!(
sim.strategy_id(GroupId(0)),
Some(&BuiltinStrategy::Look),
"strategy.builtin_id() must override the caller-supplied id \
when the strategy identifies as a known built-in",
);
assert_eq!(
sim.groups()[0].hall_call_mode(),
crate::dispatch::HallCallMode::Classic,
"HallCallMode must follow the resolved built-in, not the \
stale caller-supplied id",
);
}