diff --git a/.editorconfig b/.editorconfig index a922223..85b6851 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,6 @@ insert_final_newline = true [Makefile] indent_style = tab indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true + +[*.py] +generated_code = true diff --git a/.gitignore b/.gitignore index 58e4f32..e6f1988 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .vscode/settings.json +.venv/ gbdk/ Source/obj Source/out mgb.gb +bgb diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..871a86a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "tests/gbdk_unit_test"] + path = tests/gbdk_unit_test + url = git@github.com:tstirrat/gbdk_unit_test.git + branch = tstirrat-parse-bess-core diff --git a/.vscode/tasks.json b/.vscode/tasks.json index de8d61f..69a39aa 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -66,6 +66,16 @@ }, "dependsOn": "make-debug", "problemMatcher": [] + }, + { + "label": "make-test-roms", + "type": "shell", + "command": "make", + "args": [], + "options": { + "cwd": "tests/" + }, + "problemMatcher": [] } ] } diff --git a/tests/.github/workflows/unit_test.yml b/tests/.github/workflows/unit_test.yml new file mode 100644 index 0000000..57bad70 --- /dev/null +++ b/tests/.github/workflows/unit_test.yml @@ -0,0 +1,29 @@ +name: Run Unit Test via Pytest + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with Ruff + run: | + pip install ruff + ruff --format=github --target-version=py310 . + continue-on-error: true + - name: Test with pytest + run: | + pytest -v -s diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..f50aa38 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,4 @@ +/build +/unused +__pycache__ +.pytest_cache diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..3e8134d --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,25 @@ +MYDIR = . +BLDDIR = $(MYDIR)/build +FW = $(MYDIR)/framework +CC = ../gbdk/bin/lcc -tempdir=$(BLDDIR) -Wl-j -Wl-m -Wl-w -Wl-yt2 -Wl-yo4 -Wl-ya4 + +TESTS = $(wildcard *.c) +OBJS = $(TESTS:%.c=$(BLDDIR)/%.o) +TESTROMS = $(TESTS:%.c=$(BLDDIR)/%.gb) + + +all: clean mkdirs build-all + +.PHONY: clean +clean: + rm -rf $(BLDDIR) + +.PHONY: mkdirs +mkdirs: + mkdir -p $(BLDDIR) + +$(BLDDIR)/%.gb: $(MYDIR)/%.c mkdirs + $(CC) -o $@ $< + +.PHONY: build-all +build-all: $(TESTROMS) diff --git a/tests/bgb_get_snapshot.py b/tests/bgb_get_snapshot.py new file mode 100644 index 0000000..48ce8af --- /dev/null +++ b/tests/bgb_get_snapshot.py @@ -0,0 +1,141 @@ +import subprocess +from array import array +import os +from pathlib import Path +from PIL import Image, ImageChops + +from gbdk_unit_test.framework.BGB_toolkit import load_noi, read_bgb_snspshot + +base_dir = os.path.dirname(os.path.realpath(__file__)) +base_path = Path(base_dir) + + +def load_rom_snapshot(rom_relative): + + make_and_run(rom_relative) + + snapshot_file = base_path.joinpath(rom_relative).with_suffix('.sna') + + if not snapshot_file.is_file(): + raise Exception("Cannot load snapshot: " + snapshot_file) + + noi_file = base_path.joinpath(rom_relative).with_suffix('.noi') + if not snapshot_file.is_file(): + raise Exception("Cannot load symbols: " + noi_file) + + screenshot = base_path.joinpath(rom_relative).with_suffix('.bmp') + + snapshot = read_bgb_snspshot(snapshot_file) + symbols = load_noi(noi_file) + symbols = {value: key for key, value in symbols.items()} + + snapshot['symbols'] = symbols + snapshot['screenshot'] = screenshot + + return snapshot + + +def make_and_run(rom_relative): + make_rom(rom_relative) + + rom_path_full = base_path.joinpath(rom_relative) + screenshot_path = rom_path_full.with_suffix('.bmp') + + subprocess.run([ + "/usr/bin/env", + "wine", + "../bgb/bgb64.exe", + "-set \"DebugSrcBrk=1\"", + "-hf", + "-stateonexit", + "-screenonexit", + screenshot_path, + + rom_relative, + ], + cwd=base_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + if not rom_path_full.with_suffix(".sna").is_file(): + raise Exception("Tried to run rom, failed " + rom_relative) + + +def make_rom(rom_relative): + result = subprocess.call( + [ + "/usr/bin/env", + "make", + rom_relative + ], + cwd=base_dir, + stdout=subprocess.DEVNULL + ) + + if result != 0: + raise Exception("Tried to make rom, failed " + rom_relative) + + +# The following code is repurposed from unit_checker.py by untoxa (MIT License) + +# WRAM = 49152 +mem_map = { + 'VRAM': 0x8000, + 'WRAM': 0xC000, + 'OAM': 0xFE00, + 'IO_REG': 0xFF00, + 'HRAM': 0xFF80, +} + + +def symbol_addr(snapshot, symbol, base): + if type(base) is str: + base = mem_map[base.upper()] + return snapshot['symbols'].get(symbol) - base + + +def get(snapshot, section, address, len=0): + if isinstance(address, str): + address = symbol_addr(snapshot, address, section) + + if len > 1: + return snapshot[section][address:address + len] + else: + return snapshot[section][address] + + +def ASCIIZ(snapshot, section, address): + ofs = address + data = snapshot[section] + fin = ofs + while data[fin] != 0: + fin += 1 + return str(data[ofs:fin], 'ascii') if fin - ofs > 0 else '' + + +def CHECKSCREEN(snapshot, file_name): + image_one = Image.open(base_path.joinpath(file_name)).convert('RGB') + image_two = Image.open(snapshot['screenshot']).convert('RGB') + + diff = ImageChops.difference(image_one, image_two) + + return (diff.getbbox() is None) + + +def find(input: dict | array, val: str | int, parent_key: str | None): + if isinstance(input, array): + for i, v in enumerate(input): + if v == val: + print("found", [parent_key, i]) + return [parent_key, i] + elif isinstance(v, array): + find(v, val, i) + return + + for k, v in input.items(): + if v == val: + print("found", [parent_key, k]) + return [parent_key, k] + elif isinstance(v, array) or isinstance(v, dict): + find(v, val, k) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..505b6c0 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest + +from bgb_get_snapshot import load_rom_snapshot + + +@pytest.fixture +def snapshot(request): + return load_rom_snapshot(request.param) diff --git a/tests/gbdk_unit_test b/tests/gbdk_unit_test new file mode 160000 index 0000000..4f4bb23 --- /dev/null +++ b/tests/gbdk_unit_test @@ -0,0 +1 @@ +Subproject commit 4f4bb23f8f1cb4f1700da710ff86183f6ad01745 diff --git a/tests/pu1_plays_note.c b/tests/pu1_plays_note.c new file mode 100644 index 0000000..d6490dc --- /dev/null +++ b/tests/pu1_plays_note.c @@ -0,0 +1,35 @@ +#include +#include + +#include "../Source/io/midi.h" + +#include "../Source/mGB.h" +#include "../Source/synth/common.c" +#include "../Source/synth/pulse.c" +#include "../Source/synth/wav.c" + +bool systemIdle = true; + +uint8_t statusByte; +uint8_t addressByte; +uint8_t valueByte; +uint8_t capturedAddress; + +uint8_t result[2] = {0U, 0U}; + +void main(void) { + rAUDENA = AUDENA_ON; + rAUDVOL = AUDVOL_VOL_LEFT(7U) | AUDVOL_VOL_RIGHT(7U); + + setOutputPan(PU1, 64U); + + addressByte = 64U; // MIDI note + valueByte = 127U; // MIDI velocity + + playNotePu1(); + updatePu1(); + + delay(500); + + EMU_BREAKPOINT; +} diff --git a/tests/pu1_test.py b/tests/pu1_test.py new file mode 100644 index 0000000..9f0e998 --- /dev/null +++ b/tests/pu1_test.py @@ -0,0 +1,18 @@ +import pytest + +from bgb_get_snapshot import get + + +def describe_pu1(): + + @pytest.mark.parametrize('snapshot', ['build/pu1_plays_note.gb'], indirect=True) + def it_plays_a_note(snapshot): + rAUD1LOW = get(snapshot, 'IO_REG', '_NR13_REG') + rAUD1HIGH = get(snapshot, 'IO_REG', '_NR14_REG') + + # noteIndex = 64U - 36U == 28U + # f = freq[noteIndex] == 1650U == 0x0672 + + assert rAUD1LOW == 0x72 + retrig = 0x80 + assert rAUD1HIGH == 0x06 | retrig diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..a80fbc8 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +pillow ~= 10.4.0 +pytest ~= 8.3.0 +pytest-describe ~= 2.2.0 +# pytest-sugar ~= 1.0.0 diff --git a/tests/screens_test.py b/tests/screens_test.py new file mode 100644 index 0000000..281963c --- /dev/null +++ b/tests/screens_test.py @@ -0,0 +1,10 @@ +import pytest + +from bgb_get_snapshot import CHECKSCREEN + + +def describe_screens(): + + @pytest.mark.parametrize('snapshot', ['build/splash_screen.gb'], indirect=True) + def it_matches_snapshot(snapshot): + assert CHECKSCREEN(snapshot, "splash_screen.png") diff --git a/tests/splash_screen.c b/tests/splash_screen.c new file mode 100644 index 0000000..94a2d1a --- /dev/null +++ b/tests/splash_screen.c @@ -0,0 +1,17 @@ +#include +#include + +#include "../Source/screen/splash.c" +#include "../Source/screen/utils.c" + +uint8_t j; + +void main(void) { + + displaySetup(); + + showSplashScreen(); + delay(500); + + EMU_BREAKPOINT; +} diff --git a/tests/splash_screen.png b/tests/splash_screen.png new file mode 100644 index 0000000..e2ac4aa Binary files /dev/null and b/tests/splash_screen.png differ diff --git a/tests/wav_test.py b/tests/wav_test.py new file mode 100644 index 0000000..4ad4def --- /dev/null +++ b/tests/wav_test.py @@ -0,0 +1,30 @@ +import pytest +from array import array + +from bgb_get_snapshot import get + + +def describe_wav_loading(): + + @pytest.mark.parametrize('snapshot', ['build/wav_test_load_and_play.gb'], indirect=True) + def it_loads_the_correct_waveform(snapshot): + _AUD3WAVERAM = get(snapshot, 'IO_REG', '__AUD3WAVERAM', 16) + + expected = array('B') + expected.frombytes(bytearray([0x22, 0x55, 0x77, 0xAA, 0xBB, 0xDD, 0xEE, 0xFF, + 0xEE, 0xDD, 0xBB, 0xAA, 0x77, 0x66, 0x44, 0x00])) + assert _AUD3WAVERAM == expected + + @pytest.mark.parametrize('snapshot', ['build/wav_test_load_and_play.gb'], indirect=True) + def it_plays_a_note(snapshot): + rAUD3LOW = get(snapshot, 'IO_REG', '_NR33_REG') + rAUD3HIGH = get(snapshot, 'IO_REG', '_NR34_REG') + + # noteIndex = 64 - 24 == 40 + # freq[noteIndex] == 1849U == 0x0739 + + retrig = 0x80 + assert rAUD3LOW == 0x39 + assert rAUD3HIGH == 0x07 | retrig + + # TODO: check frequency, env, vol etc diff --git a/tests/wav_test_load_and_play.c b/tests/wav_test_load_and_play.c new file mode 100644 index 0000000..642abc4 --- /dev/null +++ b/tests/wav_test_load_and_play.c @@ -0,0 +1,43 @@ +#include + +#include "../Source/io/midi.h" + +#include "../Source/io/serial.c" +#include "../Source/mGB.h" +#include "../Source/synth/common.c" +#include "../Source/synth/pulse.c" +#include "../Source/synth/wav.c" + +bool systemIdle = true; + +uint8_t statusByte; +uint8_t addressByte; +uint8_t valueByte; +uint8_t capturedAddress; + +uint8_t result[2] = {0U, 0U}; + +void main(void) { + rAUDENA = AUDENA_ON; + rAUDVOL = AUDVOL_VOL_LEFT(7U) | AUDVOL_VOL_RIGHT(7U); + + setOutputPan(WAV, 64U); + + wavDataOffset = 1U * 16U; + loadWav(1U * 16U); + rAUD3LEVEL = 0x00U; // mimics mGB + + addressByte = 64U; // MIDI note + valueByte = 127U; // MIDI velocity + + playNoteWav(); + updateWav(); + // updateWavSweep(); + + delay(500); + + result[0] = wavCurrentFreq; + result[1] = wavCurrentFreq >> 8U; + + EMU_BREAKPOINT; +}