Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,143 @@ target_include_directories(gxtest_prime_sieve PRIVATE

gtest_discover_tests(gxtest_prime_sieve)

# -----------------------------------------------------------------------------
# Symbol Example Test (demonstrates symbol-based testing)
# -----------------------------------------------------------------------------

# Option to build ROMs from source (requires m68k-elf toolchain)
option(GXTEST_BUILD_ROMS "Build test ROMs from source (requires m68k-elf-gcc)" OFF)

# Find Python for symbol extraction
find_package(Python3 COMPONENTS Interpreter)

if(GXTEST_BUILD_ROMS)
# Find m68k cross-compiler
find_program(M68K_GCC m68k-elf-gcc)
find_program(M68K_AS m68k-elf-as)
find_program(M68K_LD m68k-elf-ld)
find_program(M68K_NM m68k-elf-nm)
find_program(M68K_OBJCOPY m68k-elf-objcopy)

if(NOT Python3_FOUND)
message(WARNING "Python3 not found, ROM builds disabled (needed for symbol extraction)")
elseif(M68K_GCC AND M68K_AS AND M68K_LD AND M68K_NM AND M68K_OBJCOPY)
message(STATUS "m68k-elf toolchain and Python3 found, enabling ROM builds")

set(SYMBOL_EXAMPLE_DIR ${CMAKE_SOURCE_DIR}/roms/symbol_example)
set(SYMBOL_EXAMPLE_ROM ${SYMBOL_EXAMPLE_DIR}/symbol_example.bin)
set(SYMBOL_EXAMPLE_ELF ${SYMBOL_EXAMPLE_DIR}/symbol_example.elf)
set(SYMBOL_EXAMPLE_SYMBOLS_TXT ${SYMBOL_EXAMPLE_DIR}/symbol_example_symbols.txt)
set(GENERATED_INCLUDE_DIR ${CMAKE_BINARY_DIR}/generated)

# Create generated include directory
file(MAKE_DIRECTORY ${GENERATED_INCLUDE_DIR})

# Build the ROM using the Makefile
add_custom_command(
OUTPUT ${SYMBOL_EXAMPLE_ROM} ${SYMBOL_EXAMPLE_ELF}
COMMAND make -C ${SYMBOL_EXAMPLE_DIR} ${SYMBOL_EXAMPLE_ROM}
DEPENDS
${SYMBOL_EXAMPLE_DIR}/main.c
${SYMBOL_EXAMPLE_DIR}/crt0.s
${SYMBOL_EXAMPLE_DIR}/genesis.ld
${SYMBOL_EXAMPLE_DIR}/Makefile
COMMENT "Building symbol_example ROM..."
)

# Extract symbols from ELF
add_custom_command(
OUTPUT ${SYMBOL_EXAMPLE_SYMBOLS_TXT}
COMMAND ${M68K_NM} -n ${SYMBOL_EXAMPLE_ELF} > ${SYMBOL_EXAMPLE_SYMBOLS_TXT}
DEPENDS ${SYMBOL_EXAMPLE_ELF}
COMMENT "Extracting symbols from symbol_example.elf..."
)

# Generate C++ symbol header
add_custom_command(
OUTPUT ${GENERATED_INCLUDE_DIR}/symbol_example_symbols.h
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/tools/elf2sym.py
${SYMBOL_EXAMPLE_SYMBOLS_TXT} > ${GENERATED_INCLUDE_DIR}/symbol_example_symbols.h
DEPENDS
${SYMBOL_EXAMPLE_SYMBOLS_TXT}
${CMAKE_SOURCE_DIR}/tools/elf2sym.py
COMMENT "Generating symbol_example_symbols.h..."
)

# Generate C++ ROM header with embedded binary
add_custom_command(
OUTPUT ${GENERATED_INCLUDE_DIR}/symbol_example_rom.h
COMMAND make -C ${SYMBOL_EXAMPLE_DIR} symbol_example_rom.h
COMMAND ${CMAKE_COMMAND} -E copy
${SYMBOL_EXAMPLE_DIR}/symbol_example_rom.h
${GENERATED_INCLUDE_DIR}/symbol_example_rom.h
DEPENDS ${SYMBOL_EXAMPLE_ROM}
COMMENT "Generating symbol_example_rom.h..."
)

# Custom target for ROM generation
add_custom_target(symbol_example_rom_gen
DEPENDS
${GENERATED_INCLUDE_DIR}/symbol_example_symbols.h
${GENERATED_INCLUDE_DIR}/symbol_example_rom.h
)

# Symbol-based test executable
add_executable(gxtest_symbol_example
tests/symbol_example_test.cpp
)

add_dependencies(gxtest_symbol_example symbol_example_rom_gen)

target_link_libraries(gxtest_symbol_example
gxtest
genplusgx_core
GTest::gtest_main
)

target_include_directories(gxtest_symbol_example PRIVATE
${CMAKE_SOURCE_DIR}/include
${GENERATED_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/vendor/genplusgx
)

gtest_discover_tests(gxtest_symbol_example)

message(STATUS "Symbol example test enabled")
else()
message(WARNING "m68k-elf toolchain not found, ROM builds disabled")
endif()
else()
# When GXTEST_BUILD_ROMS is OFF, check for pre-built headers
set(PREBUILT_SYMBOLS ${CMAKE_SOURCE_DIR}/tests/symbol_example_symbols.h)
set(PREBUILT_ROM ${CMAKE_SOURCE_DIR}/tests/symbol_example_rom.h)

if(EXISTS ${PREBUILT_SYMBOLS} AND EXISTS ${PREBUILT_ROM})
message(STATUS "Using pre-built symbol_example headers")

add_executable(gxtest_symbol_example
tests/symbol_example_test.cpp
)

target_link_libraries(gxtest_symbol_example
gxtest
genplusgx_core
GTest::gtest_main
)

target_include_directories(gxtest_symbol_example PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/vendor/genplusgx
)

gtest_discover_tests(gxtest_symbol_example)
else()
message(STATUS "Symbol example test disabled (no pre-built headers and GXTEST_BUILD_ROMS=OFF)")
message(STATUS "To enable: cmake -DGXTEST_BUILD_ROMS=ON or run 'make install' in roms/symbol_example/")
endif()
endif()

# -----------------------------------------------------------------------------
# Installation
# -----------------------------------------------------------------------------
Expand Down
249 changes: 249 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# gxtest

A headless Genesis/Mega Drive test harness built on [Genesis Plus GX](https://github.com/ekeeke/Genesis-Plus-GX). gxtest enables fast, automated testing of Genesis ROMs using GoogleTest, with direct memory access for assertions and symbol-based debugging.

## Features

- **Headless Execution**: Run Genesis ROMs at maximum speed without graphics or audio output
- **GoogleTest Integration**: Write tests using familiar GoogleTest patterns and assertions
- **Direct Memory Access**: Read/write emulated RAM for state verification and injection
- **Symbol-Based Testing**: Assert on variable names (`Sym::player_score`) instead of magic addresses (`0xFF0008`)
- **CPU State Inspection**: Access M68K registers (D0-D7, A0-A7, PC, SR)
- **Input Simulation**: Programmatically control gamepad inputs
- **State Save/Load**: Capture and restore emulator state
- **Conditional Execution**: Run until memory conditions are met

## Quick Start

### Building

```bash
mkdir build && cd build
cmake ..
make -j4
```

To enable building test ROMs from source (requires `m68k-elf-gcc`):

```bash
cmake -DGXTEST_BUILD_ROMS=ON ..
```

### Running Tests

```bash
ctest --output-on-failure
```

## Writing Tests

### Basic Test Fixture

```cpp
#include <gxtest.h>

class MyRomTest : public GX::Test {
protected:
void SetUp() override {
ASSERT_TRUE(emu.LoadRom("path/to/rom.bin"));
}
};

TEST_F(MyRomTest, GameInitializes) {
emu.RunFrames(60); // Run 1 second at 60fps

// Assert on memory values
EXPECT_EQ(ReadWord(0xFF0100), 0x0001);
}
```

### Loading ROMs

```cpp
// From file
emu.LoadRom("roms/game.bin");

// From embedded byte array
emu.LoadRom(ROM_DATA, ROM_SIZE);
```

### Memory Access

```cpp
// Read memory (handles Genesis byte-swapping automatically)
uint8_t byte = ReadByte(0xFF0000);
uint16_t word = ReadWord(0xFF0000);
uint32_t dword = ReadLong(0xFF0000);

// Write memory
WriteByte(0xFF0000, 0x42);
WriteWord(0xFF0000, 0x1234);
WriteLong(0xFF0000, 0xDEADBEEF);

// Direct RAM access (faster for bulk operations)
uint8_t* work_ram = emu.GetWorkRam(); // 64KB at 0xFF0000
uint8_t* z80_ram = emu.GetZ80Ram(); // 8KB at 0xA00000
```

### Conditional Execution

```cpp
// Run until memory equals a value (or timeout)
int frames = emu.RunUntilMemoryEquals(0xFF0502, 0xAD, 1000);
ASSERT_GE(frames, 0) << "Condition not met within timeout";

// Run until custom condition
emu.RunUntil([this]() {
return ReadWord(0xFF0100) >= 100;
}, 500);
```

### Input Simulation

```cpp
GX::Input input;
input.up = true;
input.a = true;
emu.SetInput(0, input); // Player 1

emu.RunFrames(1);

// Or use convenience method
emu.PressButton(0, GX::Button::Start);
```

## Symbol-Based Testing

For readable tests that use variable names instead of addresses, gxtest supports extracting symbols from ELF files.

### Pipeline Overview

1. **Compile ROM** with debug symbols (`m68k-elf-gcc` produces `.elf`)
2. **Extract symbols** using `tools/elf2sym.py`
3. **Include generated header** in your tests
4. **Assert using symbol names**

### Example

ROM source (`main.c`):
```c
volatile uint16_t player_score = 0;
volatile uint8_t player_lives = 3;

void main(void) {
while (!game_over) {
player_score += 10;
// ...
}
}
```

Extract symbols:
```bash
m68k-elf-nm -n rom.elf | python3 tools/elf2sym.py > symbols.h
```

Generated header (`symbols.h`):
```cpp
namespace Sym {
constexpr uint32_t player_score = 0xFF0008;
constexpr uint32_t player_lives = 0xFF0018;
}
```

Test using symbols:
```cpp
#include "symbols.h"

TEST_F(GameTest, ScoreIncrements) {
emu.RunFrames(10);

// Readable: assert on player_score, not 0xFF0008
uint16_t score = ReadWord(Sym::player_score);
EXPECT_GT(score, 0);
EXPECT_EQ(score % 10, 0);
}

TEST_F(GameTest, InjectState) {
// Set score directly for testing edge cases
WriteWord(Sym::player_score, 9990);
WriteByte(Sym::player_lives, 1);

emu.RunFrames(1);

// Verify game-over triggers at 10000 points
EXPECT_EQ(ReadByte(Sym::game_over), 1);
}
```

### Benefits

- **Compile-time safety**: Renamed variables cause build failures, not runtime crashes
- **IDE autocomplete**: Symbol names appear in code completion
- **Self-documenting**: `Sym::player_score` is clearer than `0xFF0008`

## API Reference

### GX::Emulator

| Method | Description |
|--------|-------------|
| `LoadRom(path)` | Load ROM from file |
| `LoadRom(data, size)` | Load ROM from memory |
| `Reset()` | Soft reset |
| `HardReset()` | Hard reset (power cycle) |
| `RunFrames(n)` | Run n emulator frames |
| `RunUntilMemoryEquals(addr, val, max)` | Run until memory matches |
| `RunUntil(condition, max)` | Run until lambda returns true |
| `ReadByte/Word/Long(addr)` | Read memory |
| `WriteByte/Word/Long(addr, val)` | Write memory |
| `GetWorkRam()` | Direct pointer to 68K work RAM |
| `GetZ80Ram()` | Direct pointer to Z80 RAM |
| `GetDataRegister(n)` | Read D0-D7 |
| `GetAddressRegister(n)` | Read A0-A7 |
| `GetPC()` | Read program counter |
| `GetSR()` | Read status register |
| `SetInput(player, state)` | Set controller state |
| `SaveState()` | Capture emulator state |
| `LoadState(state)` | Restore emulator state |

### GX::Test

Base class for test fixtures. Provides:
- `emu` - Emulator instance
- `ReadByte/Word/Long()` - Convenience wrappers
- `WriteByte/Word/Long()` - Convenience wrappers

## Project Structure

```
gxtest/
├── include/
│ └── gxtest.h # Public API
├── src/
│ ├── gxtest.cpp # Implementation
│ ├── osd.h # Platform abstraction
│ └── stubs.c # Sega CD stubs
├── tests/
│ ├── example_test.cpp # Basic test patterns
│ ├── prime_sieve_test.cpp
│ └── symbol_example_test.cpp
├── tools/
│ └── elf2sym.py # Symbol extraction
├── roms/
│ ├── prime_sieve/ # Verification ROM
│ └── symbol_example/ # Symbol testing demo
└── vendor/
└── genplusgx/ # Genesis Plus GX core
```

## Requirements

- CMake 3.14+
- C++17 compiler
- Python 3 (for symbol extraction)
- m68k-elf-gcc (optional, for building test ROMs)

## License

gxtest framework is provided under the MIT license. Genesis Plus GX is licensed under its own terms (see `vendor/genplusgx/`).
Loading