use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU64, Ordering};
use crate::components::TextLabel;
use crate::ecs::{
Access, Clock, EventStore, PipelineContext, StepResult, System, SystemEntry, SystemTable, World,
};
#[derive(Debug)]
struct Marker {
steps: u32,
stop_after: Option<u32>,
}
impl System for Marker {
fn init(&mut self, ctx: &mut PipelineContext) {
mark(ctx, "init");
}
fn step(&mut self, _ctx: &mut PipelineContext) -> StepResult {
self.steps += 1;
match self.stop_after {
Some(n) if self.steps >= n => StepResult::Done,
_ => StepResult::Continue,
}
}
}
fn mark(ctx: &mut PipelineContext, what: &str) {
ctx.push(TextLabel {
content: what.to_string(),
..Default::default()
});
}
fn gate(world: &World) -> Option<Box<dyn System>> {
world.query::<TextLabel>().next()?;
Some(Box::new(Marker {
steps: 0,
stop_after: None,
}))
}
fn finishing_gate(world: &World) -> Option<Box<dyn System>> {
world.query::<TextLabel>().next()?;
Some(Box::new(Marker {
steps: 0,
stop_after: Some(1),
}))
}
fn before_init(ctx: &mut PipelineContext) {
mark(ctx, "before_init");
}
fn prepare_events(store: &mut EventStore, _access: Access) {
store.get_mut_or_create::<u8>();
}
const fn entry(name: &'static str, gate: fn(&World) -> Option<Box<dyn System>>) -> SystemEntry {
SystemEntry {
name,
present_when: "the world holds a TextLabel",
gate,
after: &[],
before: &[],
}
}
const TABLE: SystemTable = SystemTable {
entries: &[entry("Marker", gate)],
complete_world: None,
before_init: Some(before_init),
prepare_events: Some(prepare_events),
};
const FINISHING: SystemTable = SystemTable {
entries: &[entry("Marker", finishing_gate)],
complete_world: None,
before_init: None,
prepare_events: None,
};
fn seeded() -> World {
let mut world = World::new();
world.add_component(TextLabel {
content: "seed".to_string(),
..Default::default()
});
world
}
fn marks(world: &World) -> Vec<&str> {
world
.query::<TextLabel>()
.map(|l| l.content.as_str())
.collect()
}
#[test]
fn start_runs_the_load_pass_before_system_init() {
let mut world = seeded();
world.start(&TABLE).expect("the world starts");
assert_eq!(marks(&world), ["seed", "before_init", "init"]);
assert_eq!(world.system_count(), 1);
assert_eq!(world.systems()[0].name(), "Marker");
}
#[test]
fn the_manifest_matches_what_start_builds() {
let mut world = seeded();
let manifest = world.system_manifest(&TABLE);
world.start(&TABLE).expect("the world starts");
let built: Vec<&str> = world.systems().iter().map(|s| s.name()).collect();
assert_eq!(manifest, built);
}
#[test]
fn an_ungated_world_builds_nothing_and_is_done() {
let mut world = World::new();
world.start(&TABLE).expect("the world starts");
assert_eq!(world.system_count(), 0);
assert_eq!(world.step(), StepResult::Done);
}
#[test]
fn starting_twice_builds_the_systems_once() {
let mut world = seeded();
world.start(&TABLE).expect("the world starts");
world.start(&TABLE).expect("the world starts again");
assert_eq!(world.system_count(), 1);
}
#[test]
fn a_finished_system_leaves_the_set() {
let mut world = seeded();
world.start(&FINISHING).expect("the world starts");
assert_eq!(world.system_count(), 1);
assert_eq!(world.step(), StepResult::Done);
assert_eq!(world.system_count(), 0);
}
#[test]
fn an_empty_table_starts_a_world_that_does_nothing() {
let mut world = seeded();
world.start(&SystemTable::EMPTY).expect("the world starts");
assert_eq!(marks(&world), ["seed"], "no load pass ran");
assert_eq!(world.step(), StepResult::Done);
}
#[test]
fn start_pre_creates_the_declared_event_queues() {
let mut world = seeded();
assert!(world.events::<u8>().is_none());
world.start(&TABLE).expect("the world starts");
assert!(world.events::<u8>().is_some());
}
#[test]
fn event_queues_rotate_every_step() {
let mut world = seeded();
world.start(&TABLE).expect("the world starts");
for _ in 0..5 {
world.events_mut::<u16>().send(1);
world.events_mut::<u32>().send(2);
world.step();
}
for len in [
world.events::<u16>().expect("queue exists").len(),
world.events::<u32>().expect("queue exists").len(),
] {
assert!(len <= 2, "queue holds at most two frames of events: {len}");
}
}
#[test]
fn the_clock_resource_times_each_system() {
static NOW: AtomicU64 = AtomicU64::new(0);
fn ticking() -> u64 {
NOW.fetch_add(10, Ordering::Relaxed)
}
let mut world = seeded();
world.insert_resource(Clock(ticking));
world.start(&TABLE).expect("the world starts");
world.step();
world.step();
assert_eq!(world.profile().system_timings(), [("Marker", 10)]);
}
#[test]
fn an_absent_clock_records_zero_micros() {
let mut world = seeded();
world.start(&TABLE).expect("the world starts");
world.step();
world.step();
assert_eq!(world.profile().system_timings(), [("Marker", 0)]);
}
#[test]
fn the_profile_is_exposed_before_any_step() {
let world = World::new();
assert!(world.profile().system_timings().is_empty());
}
#[test]
fn the_debug_impl_reports_counts() {
let mut world = seeded();
world.start(&TABLE).expect("the world starts");
let text: String = alloc::format!("{world:?}");
assert!(text.contains("components: 3"), "{text}");
assert!(text.contains("systems: 1"), "{text}");
}