The Crimsonland project uses pytest for testing, with 200+ tests covering gameplay, perks, physics, replay, and parity verification.
Running Tests
Basic Test Runs
# Run all tests
uv run pytest
# Run specific test file
uv run pytest tests/test_gameplay.py
# Run specific test
uv run pytest tests/test_gameplay.py::test_weapon_damage
# Run tests matching pattern
uv run pytest -k "weapon"
Using Just Commands
# Run all tests (basic)
just test
# Run with coverage reports
just test-cov
# Run full verification suite (includes tests)
just check
Test Options
Coverage
Verbose Output
Parallel Execution
Stop on Failure
# Terminal coverage report
uv run pytest --cov=crimson --cov-report=term-missing
# HTML + XML reports
uv run pytest --cov-report=html --cov-report=xml
Coverage configuration is in pyproject.toml:
Branch coverage enabled
Source: crimson package
Omits: tests/*
Shows missing lines, skips covered files
# Verbose test output
uv run pytest -v
# Show print statements
uv run pytest -s
# Show local variables on failure
uv run pytest -l
# Install pytest-xdist
uv add --dev pytest-xdist
# Run tests in parallel
uv run pytest -n auto
# Stop after first failure
uv run pytest -x
# Stop after N failures
uv run pytest --maxfail=3
Test Organization
The test suite is organized by subsystem:
tests/
├── test_gameplay.py # Core gameplay mechanics
├── test_perks.py # Perk implementations and effects
├── test_physics.py # Movement, collision, projectiles
├── test_replay.py # Replay recording and verification
├── test_parity.py # Native parity verification
├── test_weapons.py # Weapon behavior and damage
├── test_creatures.py # Enemy AI and behavior
├── test_formats.py # Asset format parsing (PAQ, JAZ)
└── conftest.py # Shared fixtures
Writing Tests
Test Structure
import pytest
from crimson.gameplay import WeaponSystem
def test_weapon_damage_calculation ():
"""Test that weapon damage is calculated correctly."""
weapon = WeaponSystem.create_weapon( "PLASMA_RIFLE" )
damage = weapon.calculate_damage( distance = 100.0 )
assert damage == pytest.approx( 75.0 , rel = 1e-6 )
Using Fixtures
@pytest.fixture
def game_state ():
"""Create a fresh game state for testing."""
from crimson.game import GameState
state = GameState.new_survival_game( seed = 12345 )
return state
def test_spawn_creature ( game_state ):
"""Test creature spawning logic."""
creature = game_state.spawn_creature( "ZOMBIE" )
assert creature.health > 0
assert creature.position is not None
Snapshot Testing with Syrupy
The project uses syrupy for snapshot testing:
def test_weapon_table_structure ( snapshot ):
"""Verify weapon table structure doesn't change unexpectedly."""
from crimson.weapons import WEAPON_TABLE
assert WEAPON_TABLE == snapshot
Update snapshots when intentional changes occur:
uv run pytest --snapshot-update
Mocking with pytest-mock
def test_audio_playback ( mocker ):
"""Test audio playback without actual sound."""
mock_play = mocker.patch( "grim.audio.play_sound" )
from crimson.game import Game
game = Game()
game.play_weapon_sound( "SHOTGUN" )
mock_play.assert_called_once_with( "shotgun_fire.wav" )
Parity Testing
Parity tests verify that the rewrite matches the original binary’s behavior. These are critical for gameplay accuracy.
Deterministic Tests
Tests that verify exact numeric behavior:
def test_rng_sequence_determinism ():
"""Verify RNG produces identical sequences with same seed."""
from crimson.rng import RNG
rng1 = RNG( seed = 42 )
rng2 = RNG( seed = 42 )
seq1 = [rng1.next_u32() for _ in range ( 100 )]
seq2 = [rng2.next_u32() for _ in range ( 100 )]
assert seq1 == seq2
Float Comparison
For float parity tests, use tight tolerances:
import pytest
def test_player_movement_parity ():
"""Test that player movement matches native float32 behavior."""
from crimson.player import Player
player = Player( position = ( 100.0 , 100.0 ))
player.move( dx = 1.5 , dy = 2.3 , dt = 0.016666668 ) # 60 FPS
# Use relative tolerance appropriate for f32 precision
assert player.position[ 0 ] == pytest.approx( 101.5 , rel = 1e-6 )
assert player.position[ 1 ] == pytest.approx( 102.3 , rel = 1e-6 )
See Float Parity Policy for detailed guidance.
Replay Verification Tests
Tests that verify replay files produce expected outcomes:
def test_replay_determinism ():
"""Test that replay produces identical results on multiple runs."""
from crimson.replay import ReplayPlayer
replay = ReplayPlayer.load( "fixtures/survival_seed_42.replay" )
result1 = replay.run_headless()
result2 = replay.run_headless()
assert result1.score == result2.score
assert result1.final_wave == result2.final_wave
assert result1.rng_checksum == result2.rng_checksum
Test Categories
Unit Tests
Test individual functions and classes in isolation:
uv run pytest tests/test_weapons.py
Integration Tests
Test subsystems working together:
uv run pytest tests/test_gameplay.py
Parity Tests
Verify behavior matches original binary:
uv run pytest tests/test_parity.py -v
Regression Tests
Lock in discovered behaviors to prevent regressions:
uv run pytest -m regression
Continuous Integration
The pre-push hook runs the full test suite:
# Automatic via hook
git push
# Manual run
prek run --stage pre-push
# CI-equivalent local run
just check && uv build
Debugging Failed Tests
Drop into Debugger
# Drop into pdb on failure
uv run pytest --pdb
# Drop into pdb immediately
uv run pytest --trace
Isolate Failures
# Run only failed tests from last run
uv run pytest --lf
# Run failed tests first, then others
uv run pytest --ff
Verbose Logging
# Show captured log output
uv run pytest --log-cli-level=DEBUG
Best Practices
Each test should verify one specific behavior. If a test has multiple unrelated assertions, split it.
Test names should clearly describe what they verify:
Good: test_weapon_damage_decreases_with_distance
Bad: test_weapon_1
Avoid tests that depend on timing, randomness (without seeds), or external state.
Keep tests fast. Use mocks for I/O operations. Heavy integration tests should be marked.
When adding parity tests, document the source: def test_zombie_speed_parity ():
"""Verify zombie speed matches native decompile.
Evidence: analysis/ghidra/raw/crimsonland.exe_decompiled.c:12345
Native value: 2.5f
"""
from crimson.creatures import ZOMBIE
assert ZOMBIE .speed == pytest.approx( 2.5 , rel = 1e-6 )
Next Steps
Verification Process Learn about the full verification workflow
Parity Workflow Understand parity-first development
Code Style Follow project coding standards
Float Parity Master float32 precision requirements