Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Solutions: 43 — Tests are systems

Exercise 1 — A test as a system

Add no_creature_moves_too_far to your simulator’s DAG behind a --test flag:

#![allow(unused)]
fn main() {
if cfg.test_mode {
    let suspicious = no_creature_moves_too_far(&world.pos_before, &world.pos, MAX_STEP);
    assert!(suspicious.is_empty(), "{:?}", suspicious);
}
}

In live mode, the system is absent. In test mode, it runs every tick. Same code path; different schedule.

Exercise 2 — Property test

#![allow(unused)]
fn main() {
let mut world = init_world(0xCAFE);
let initial = world.creatures.len();
for _ in 0..1000 {
    tick(&mut world, 0.033);
    assert!(world.creatures.len() <= 2 * initial);
}
}

Run twice. Both runs report identical assertion outcomes (because of §16). If the property fails, both runs fail at the same tick.

Exercise 3 — Replay test

#![allow(unused)]
fn main() {
let recording = run_and_record(&mut world1, 100);
let mut world2 = init_world(seed);
for inputs in &recording {
    world2.in_queue.extend(inputs.iter().cloned());
    tick(&mut world2, /* recorded current_time */);
}
assert_eq!(hash_world(&world1), hash_world(&world2));
}

Replay and live run produce bit-identical states. The test is assert_eq!; the test fixture is the recorded queue.

Exercise 4 — TDD a new system

Test first:

#![allow(unused)]
fn main() {
fn test_growth_slows_at_high_energy() {
    let mut world = init_one_creature_with_energy(100.0);
    let initial = world.creatures[0].size;
    for _ in 0..10 { tick(&mut world, 0.033); }
    let final_size = world.creatures[0].size;
    assert!(
        final_size - initial < HIGH_ENERGY_GROWTH_RATE * 10.0,
        "growth too fast at high energy"
    );
}
}

The test states what the system should do. Then write the system. Then watch the test pass. The order matters: writing the test first forces you to specify the behaviour before implementing it.

Exercise 5 — InspectionSystem connection

Both:

  • Read all relevant tables (& borrows everywhere)
  • Have empty (or report-only) write-sets
  • Run last in the DAG (after all mutations have settled)
  • Produce reports for consumption outside the simulator

The only difference: an InspectionSystem reports state to a debug consumer (pptop, an IDE, a log). A test reports assertion results to a test runner. Same shape; different consumer.

Exercise 6 — Test runner = simulator scheduler

The simulator’s main:

fn main() {
    let mut world = init_world(seed);
    let scheduler = build_schedule(&[
        food_spawn,
        motion,
        next_event,
        apply_eat, apply_reproduce, apply_starve,
        cleanup,
        // inspect: present in --debug only
    ]);
    loop { scheduler.tick(&mut world); }
}

The test runner:

#![allow(unused)]
fn main() {
fn test_main() {
    let mut world = init_world(seed);
    let scheduler = build_schedule(&[
        food_spawn,
        motion,
        next_event,
        apply_eat, apply_reproduce, apply_starve,
        cleanup,
        check_no_creature_moves_too_far, // assertion system
        check_population_bounded,        // assertion system
        inspect,                         // and inspect
    ]);
    for _ in 0..1000 { scheduler.tick(&mut world); }
}
}

The two binaries differ in which systems they include. The scheduler, the world, and every system itself is the same code. Most of the binary is shared.