123 lines
3.9 KiB
Rust
123 lines
3.9 KiB
Rust
//! Headless runtime-journey gates (TRAFFIC-14).
|
|
//!
|
|
//! The Makepad GUI harness cannot boot in CI (see tests/ui.rs ignore
|
|
//! reasons), so these journeys drive the PRODUCTION path one layer down:
|
|
//! scenario selection → phase gating → tick → terminal result → restart /
|
|
//! next / previous → persistence-shape result — through `TrafficWorld`,
|
|
//! the same simulation/rules/scoring the GUI drives. `tests/ui_basic.rs`
|
|
//! remains the headless smoke test and is labelled as such; this file is
|
|
//! the lifecycle/assessment-honesty coverage, not GUI coverage.
|
|
|
|
use nigig_traffic::traffic::scenario::all;
|
|
use nigig_traffic::traffic::{allowed_transition, Phase, TrafficWorld, Transition};
|
|
|
|
#[test]
|
|
fn journey_select_drive_complete_restart() {
|
|
let mut w = TrafficWorld::new();
|
|
w.load_scenario(0);
|
|
assert_eq!(w.run.phase, Phase::Intro);
|
|
// Intro ignores throttle: no head start.
|
|
let full = makepad_game_blocks::DriveInput {
|
|
throttle: 1.0,
|
|
..makepad_game_blocks::DriveInput::default()
|
|
};
|
|
let start = w.sim.world.entity(0u64).map(|e| (e.pos.x, e.pos.z));
|
|
for _ in 0..60 {
|
|
w.tick(&full);
|
|
}
|
|
assert_eq!(w.run.phase, Phase::Driving);
|
|
let now = w.sim.world.entity(0u64).map(|e| (e.pos.x, e.pos.z));
|
|
assert_eq!(start, now, "intro must not displace the car");
|
|
// Restart returns to Intro with cleared score.
|
|
w.load_scenario(0);
|
|
assert_eq!(w.run.phase, Phase::Intro);
|
|
assert_eq!(w.run.score.mistakes, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn journey_next_prev_cycle_all_scenarios() {
|
|
let mut w = TrafficWorld::new();
|
|
w.load_scenario(0);
|
|
let n = all().len();
|
|
let first = w.scenario().expect("scenario").id;
|
|
for _ in 0..n {
|
|
w.next();
|
|
}
|
|
assert_eq!(w.scenario().expect("scenario").id, first);
|
|
w.next();
|
|
let second = w.scenario().expect("scenario").id;
|
|
w.prev();
|
|
assert_eq!(w.scenario().expect("scenario").id, first);
|
|
assert_ne!(first, second);
|
|
}
|
|
|
|
#[test]
|
|
fn journey_pause_resume_freezes_and_continues() {
|
|
let mut w = TrafficWorld::new();
|
|
w.load_scenario(0);
|
|
for _ in 0..61 {
|
|
w.tick(&makepad_game_blocks::DriveInput::default());
|
|
}
|
|
assert_eq!(w.run.phase, Phase::Driving);
|
|
assert!(w.pause());
|
|
let tick = w.run.tick;
|
|
for _ in 0..120 {
|
|
w.tick(&makepad_game_blocks::DriveInput {
|
|
throttle: 1.0,
|
|
..makepad_game_blocks::DriveInput::default()
|
|
});
|
|
}
|
|
assert_eq!(w.run.tick, tick);
|
|
assert!(w.resume());
|
|
w.tick(&makepad_game_blocks::DriveInput::default());
|
|
assert_eq!(w.run.tick, tick + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn journey_terminal_result_is_immutable() {
|
|
let mut w = TrafficWorld::new();
|
|
w.load_scenario(0);
|
|
for _ in 0..61 {
|
|
w.tick(&makepad_game_blocks::DriveInput::default());
|
|
}
|
|
// Force terminal through the public transition table only.
|
|
assert!(allowed_transition(w.run.phase, Transition::Complete));
|
|
// Direct terminal simulation: complete via score path then hammer ticks.
|
|
w.run.score.completed = true;
|
|
w.run.phase = Phase::Complete;
|
|
let snap = (
|
|
w.run.phase,
|
|
w.run.score.total_points,
|
|
w.run.score.mistakes,
|
|
w.run.tick,
|
|
);
|
|
for _ in 0..1000 {
|
|
w.tick(&makepad_game_blocks::DriveInput {
|
|
throttle: 1.0,
|
|
steer: 1.0,
|
|
..makepad_game_blocks::DriveInput::default()
|
|
});
|
|
}
|
|
assert_eq!(
|
|
(
|
|
w.run.phase,
|
|
w.run.score.total_points,
|
|
w.run.score.mistakes,
|
|
w.run.tick
|
|
),
|
|
snap
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn journey_result_binds_id_and_content_version() {
|
|
// Results bind scenario ID + content version, never list index: moving
|
|
// through the catalog keeps identity stable.
|
|
let w = TrafficWorld::new();
|
|
let ids: Vec<&str> = all().iter().map(|s| s.id).collect();
|
|
for (i, s) in all().iter().enumerate() {
|
|
assert_eq!(ids[i], s.id);
|
|
assert_eq!(s.content_version(), 0);
|
|
}
|
|
assert!(w.scenario().is_none());
|
|
}
|