Nethercore Testing
Sync Testing
Runs two identical instances, compares checksums each frame.
nether run --sync-test
nether run --sync-test --frames 3000 # Specific duration
Pass criteria: Identical checksums for 1000+ frames.
Replay Testing
Record and replay for regression testing:
nether run --record replay.bin # Record
nether run --replay replay.bin # Playback
Workflow:
- Record on known-good build
- Replay on new build
- Compare outcomes
Determinism Rules
| Do | Don't |
|---|---|
| random() FFI | rand::thread_rng() |
| BTreeMap, BTreeSet | HashMap, HashSet |
| Frame counter | Instant::now() |
| Fixed-point math | Floating-point accumulation |
Test Organization
| Type | Tool | Purpose |
|------|------|---------|
| Unit | cargo test | Pure logic |
| Sync | nether run --sync-test | Runtime determinism |
| Replay | --record/--replay | Cross-build validation |
Common Desync Causes
- Non-deterministic RNG - Using rand crate instead of FFI
- HashMap iteration - Order varies between runs
- System time - Reading wall clock
- Uninitialized memory - Undefined values
- State in render() - Skipped during rollback
Debugging Desyncs
- Run sync test to confirm failure
- Add
log()calls around suspicious code - Check for forbidden patterns
- Verify all state is in static variables
Debug Actions (Efficient Testing)
Instead of recording long input sequences, use debug actions to skip directly to test scenarios:
# Skip to level 3 boss
[[frames]]
f = 0
action = "Load Level"
action_params = { level = 3 }
[[frames]]
f = 1
snap = true
assert = "$boss_health > 0"
Games register actions in init():
debug_action_begin(b"Load Level".as_ptr(), 10, b"debug_load_level".as_ptr(), 16);
debug_action_param_i32(b"level".as_ptr(), 5, 1);
debug_action_end();
When to use:
- Testing specific levels without playing through earlier ones
- Setting up edge-case scenarios (low health, specific enemy spawns)
- Regression tests that need consistent starting state