Skip to main content
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

# 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

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

Build docs developers (and LLMs) love