From 90a6638b49b42eca639d65dbd388f61207c16116 Mon Sep 17 00:00:00 2001 From: Jose Rodriguez Date: Sat, 30 May 2026 23:24:45 +0200 Subject: [PATCH 1/2] tests: add test for outfmt --- tests/outfmt/__init__.py | 0 tests/outfmt/test_binary.py | 27 +++++++++++++ tests/outfmt/test_sna.py | 57 +++++++++++++++++++++++++++ tests/outfmt/test_tap.py | 63 ++++++++++++++++++++++++++++++ tests/outfmt/test_tzx.py | 77 +++++++++++++++++++++++++++++++++++++ tests/outfmt/test_z80.py | 57 +++++++++++++++++++++++++++ 6 files changed, 281 insertions(+) create mode 100644 tests/outfmt/__init__.py create mode 100644 tests/outfmt/test_binary.py create mode 100644 tests/outfmt/test_sna.py create mode 100644 tests/outfmt/test_tap.py create mode 100644 tests/outfmt/test_tzx.py create mode 100644 tests/outfmt/test_z80.py diff --git a/tests/outfmt/__init__.py b/tests/outfmt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/outfmt/test_binary.py b/tests/outfmt/test_binary.py new file mode 100644 index 000000000..ce4ccfcb3 --- /dev/null +++ b/tests/outfmt/test_binary.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------- +# SPDX-License-Identifier: AGPL-3.0-or-later +# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors. +# See the file CONTRIBUTORS.md for copyright details. +# See https://www.gnu.org/licenses/agpl-3.0.html for details. +# -------------------------------------------------------------------- + +from src.outfmt.binary import BinaryEmitter + + +def test_binary_emitter(tmp_path): + output_file = tmp_path / "test.bin" + emitter = BinaryEmitter() + program_bytes = b"\x01\x02\x03\x04" + + emitter.emit( + output_filename=str(output_file), + program_name="test", + loader_bytes=None, + entry_point=16384, + program_bytes=program_bytes, + aux_bin_blocks=[], + aux_headless_bin_blocks=[], + ) + + assert output_file.exists() + assert output_file.read_bytes() == program_bytes diff --git a/tests/outfmt/test_sna.py b/tests/outfmt/test_sna.py new file mode 100644 index 000000000..4ec327052 --- /dev/null +++ b/tests/outfmt/test_sna.py @@ -0,0 +1,57 @@ +# -------------------------------------------------------------------- +# SPDX-License-Identifier: AGPL-3.0-or-later +# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors. +# See the file CONTRIBUTORS.md for copyright details. +# See https://www.gnu.org/licenses/agpl-3.0.html for details. +# -------------------------------------------------------------------- + +from src.outfmt.sna import SnaEmitter + + +def test_sna_emitter_generate(): + emitter = SnaEmitter() + program_bytes = b"\x00" * 100 + entry_point = 0x8000 + + # generate(self, loader_bytes, clear_addr, entry_point, program_bytes) + # SnaEmitter.emit calls generate(None, entry_point - 1, entry_point, program_bytes) + sna_data = emitter.generate(None, entry_point - 1, entry_point, program_bytes) + + assert len(sna_data) == 27 + 49152 + + # Check some header values + # snapshot.I is initialized to 0x3F in GenSnapshot + assert sna_data[0] == 0x3F + + # Border color (snapshot.outFE & 7) + # GenSnapshot.outFE = 0x0F, so 0x0F & 7 = 7 + assert sna_data[26] == 7 + + # Check SP + # GenSnapshot: SP = clear_addr - 3 = (0x8000 - 1) - 3 = 0x7FFC = 32764 + # SnaEmitter: SP = snapshot.SP - 2 = 32764 - 2 = 32762 (0x7FFA) + # sna_data[23] = 0xFA, sna_data[24] = 0x7F + assert sna_data[23] == 0xFA + assert sna_data[24] == 0x7F + + # Check PC patched on stack + # snapshot.PCL = 0x9E, snapshot.PCH = 0x1B + # Index in sna_data = 27 + 32762 - 16384 = 16405 + assert sna_data[27 + 32762 - 16384] == 0x9E + assert sna_data[27 + 32762 - 16384 + 1] == 0x1B + + +def test_sna_emitter_emit(tmp_path): + output_file = tmp_path / "test.sna" + emitter = SnaEmitter() + emitter.emit( + output_filename=str(output_file), + program_name="test", + loader_bytes=None, + entry_point=0x8000, + program_bytes=b"\x00" * 100, + aux_bin_blocks=[], + aux_headless_bin_blocks=[], + ) + assert output_file.exists() + assert len(output_file.read_bytes()) == 27 + 49152 diff --git a/tests/outfmt/test_tap.py b/tests/outfmt/test_tap.py new file mode 100644 index 000000000..e6914a6ac --- /dev/null +++ b/tests/outfmt/test_tap.py @@ -0,0 +1,63 @@ +# -------------------------------------------------------------------- +# SPDX-License-Identifier: AGPL-3.0-or-later +# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors. +# See the file CONTRIBUTORS.md for copyright details. +# See https://www.gnu.org/licenses/agpl-3.0.html for details. +# -------------------------------------------------------------------- + +from src.outfmt.tap import TAP + + +def test_tap_init(): + tap = TAP() + assert tap.output == b"" + + +def test_tap_standard_block(): + tap = TAP() + tap.standard_block(b"\x01\x02") + # Length (3 -> [0x03, 0x00]) + # Data: \x01\x02 + # Checksum: 0x01 ^ 0x02 = 0x03 + assert tap.output == b"\x03\x00\x01\x02\x03" + + +def test_tap_emit(tmp_path): + output_file = tmp_path / "test.tap" + tap = TAP() + program_bytes = b"\x00\x01" + tap.emit( + output_filename=str(output_file), + program_name="test", + loader_bytes=None, + entry_point=16384, + program_bytes=program_bytes, + aux_bin_blocks=[], + aux_headless_bin_blocks=[], + ) + assert output_file.exists() + content = output_file.read_bytes() + + # Header block + # 0x13 0x00 (Length) + # 0x00 (BLOCK_TYPE_HEADER) + # 0x03 (HEADER_TYPE_CODE) + # "test " (10 bytes) + # 0x02 0x00 (Length 2) + # 0x00 0x40 (Address 16384) + # 0x00 0x80 (32768) + # Checksum + expected_header_content = b"\x00\x03test \x02\x00\x00\x40\x00\x80" + checksum = 0 + for b in expected_header_content: + checksum ^= b + expected_header = b"\x13\x00" + expected_header_content + bytes([checksum]) + assert content.startswith(expected_header) + + # Data block + # 0x04 0x00 (Length) + # 0xFF (BLOCK_TYPE_DATA) + # 0x00 0x01 (data) + # Checksum: 0xFF ^ 0x00 ^ 0x01 = 0xFE + expected_data = b"\x04\x00\xff\x00\x01\xfe" + assert expected_data in content diff --git a/tests/outfmt/test_tzx.py b/tests/outfmt/test_tzx.py new file mode 100644 index 000000000..6bc0f8210 --- /dev/null +++ b/tests/outfmt/test_tzx.py @@ -0,0 +1,77 @@ +# -------------------------------------------------------------------- +# SPDX-License-Identifier: AGPL-3.0-or-later +# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors. +# See the file CONTRIBUTORS.md for copyright details. +# See https://www.gnu.org/licenses/agpl-3.0.html for details. +# -------------------------------------------------------------------- + +from src.outfmt.tzx import TZX + + +def test_tzx_init(): + tzx = TZX() + assert tzx.output == b"ZXTape!\x1a\x01\x15" + + +def test_tzx_lh(): + tzx = TZX() + assert tzx.LH(0x1234) == [0x34, 0x12] + assert tzx.LH(0x00FF) == [0xFF, 0x00] + + +def test_tzx_standard_block(): + tzx = TZX() + tzx.output = bytearray() + tzx.standard_block(b"\x01\x02") + # BLOCK_STANDARD (0x10) + # Pause (1000ms -> [0xE8, 0x03]) + # Length (3 -> [0x03, 0x00]) - length of data + 1 for checksum + # Data: \x01\x02 + # Checksum: 0x01 ^ 0x02 = 0x03 + assert tzx.output == b"\x10\xe8\x03\x03\x00\x01\x02\x03" + + +def test_tzx_emit(tmp_path): + output_file = tmp_path / "test.tzx" + tzx = TZX() + program_bytes = b"\x00\x01" + tzx.emit( + output_filename=str(output_file), + program_name="test", + loader_bytes=None, + entry_point=16384, + program_bytes=program_bytes, + aux_bin_blocks=[], + aux_headless_bin_blocks=[], + ) + assert output_file.exists() + content = output_file.read_bytes() + assert content.startswith(b"ZXTape!\x1a\x01\x15") + + # Header block for "test" + # 0x10 (Standard block ID) + # 0xE8 0x03 (1000ms pause) + # 0x13 0x00 (Length of header: 1 + 1 + 10 + 2 + 2 + 2 + 1 = 19 -> 0x13) + # 0x00 (BLOCK_TYPE_HEADER) + # 0x03 (HEADER_TYPE_CODE) + # "test " (10 bytes) + # 0x02 0x00 (Length 2) + # 0x00 0x40 (Address 16384) + # 0x00 0x80 (32768) + # Checksum (XOR of all bytes in block) + expected_header_content = b"\x00\x03test \x02\x00\x00\x40\x00\x80" + checksum = 0 + for b in expected_header_content: + checksum ^= b + expected_header_block = b"\x10\xe8\x03\x13\x00" + expected_header_content + bytes([checksum]) + assert expected_header_block in content + + # Data block + # 0x10 + # 0xE8 0x03 + # 0x04 0x00 (Length of data: 1 (type) + 2 (bytes) + 1 (checksum) = 4) + # 0xFF (BLOCK_TYPE_DATA) + # 0x00 0x01 (data) + # Checksum: 0xFF ^ 0x00 ^ 0x01 = 0xFE + expected_data_block = b"\x10\xe8\x03\x04\x00\xff\x00\x01\xfe" + assert expected_data_block in content diff --git a/tests/outfmt/test_z80.py b/tests/outfmt/test_z80.py new file mode 100644 index 000000000..271bf976f --- /dev/null +++ b/tests/outfmt/test_z80.py @@ -0,0 +1,57 @@ +# -------------------------------------------------------------------- +# SPDX-License-Identifier: AGPL-3.0-or-later +# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors. +# See the file CONTRIBUTORS.md for copyright details. +# See https://www.gnu.org/licenses/agpl-3.0.html for details. +# -------------------------------------------------------------------- + +from src.outfmt.z80 import Z80Emitter + + +def test_z80_emitter_generate(): + emitter = Z80Emitter() + program_bytes = b"\x00" * 100 + entry_point = 0x8000 + + # generate(self, loader_bytes, clear_addr, entry_point, program_bytes) + z80_data = emitter.generate(None, entry_point - 1, entry_point, program_bytes) + + # Header is 30 bytes + assert len(z80_data) > 30 + assert z80_data[10] == 0x3F # snapshot.I + + # PC in Z80 version 1 header at offset 6 + # GenSnapshot default PC is 0x1B9E -> PCL=0x9E, PCH=0x1B + assert z80_data[6] == 0x9E + assert z80_data[7] == 0x1B + + # End marker + assert z80_data.endswith(b"\x00\xed\xed\x00") + + +def test_z80_compression(): + emitter = Z80Emitter() + + # program_bytes with 4 EDs to test compression + program_bytes = b"\xed\xed\xed\xed" + entry_point = 0x8000 + + z80_data = emitter.generate(None, entry_point - 1, entry_point, program_bytes) + + # The 4 EDs should be compressed to ED ED 04 ED + assert b"\xed\xed\x04\xed" in z80_data + + +def test_z80_emitter_emit(tmp_path): + output_file = tmp_path / "test.z80" + emitter = Z80Emitter() + emitter.emit( + output_filename=str(output_file), + program_name="test", + loader_bytes=None, + entry_point=0x8000, + program_bytes=b"\x00" * 100, + aux_bin_blocks=[], + aux_headless_bin_blocks=[], + ) + assert output_file.exists() From 5d07d63c032cf9cfe09bebe3b23aeada9b8fb1dc Mon Sep 17 00:00:00 2001 From: Jose Rodriguez Date: Sat, 30 May 2026 23:38:25 +0200 Subject: [PATCH 2/2] typing: improve typing --- src/outfmt/binary.py | 16 ++++++++-------- src/outfmt/codeemitter.py | 12 ++++++------ src/outfmt/gensnapshot.py | 13 ++++++------- src/outfmt/sna.py | 28 ++++++++++++++-------------- src/outfmt/tzx.py | 27 ++++++++++++++------------- src/outfmt/z80.py | 14 +++++++------- 6 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/outfmt/binary.py b/src/outfmt/binary.py index 9c4a64f4f..fdd127d83 100644 --- a/src/outfmt/binary.py +++ b/src/outfmt/binary.py @@ -13,14 +13,14 @@ class BinaryEmitter(CodeEmitter): def emit( self, - output_filename, - program_name, - loader_bytes, - entry_point, - program_bytes, - aux_bin_blocks, - aux_headless_bin_blocks, + output_filename: str, + program_name: str, + loader_bytes: bytearray | None, + entry_point: int, + program_bytes: bytearray | bytes | list[int], + aux_bin_blocks: list[tuple[str, list[int]]], + aux_headless_bin_blocks: list[list[int]], ): - """Emits resulting binary file.""" + """Emits the resulting binary file.""" with open(output_filename, "wb") as f: f.write(bytearray(program_bytes)) diff --git a/src/outfmt/codeemitter.py b/src/outfmt/codeemitter.py index 42f537fee..3e1d304e6 100644 --- a/src/outfmt/codeemitter.py +++ b/src/outfmt/codeemitter.py @@ -16,10 +16,10 @@ def emit( self, output_filename: str, program_name: str, - loader_bytes: bytearray, - entry_point, - program_bytes, - aux_bin_blocks, - aux_headless_bin_blocks, - ): + loader_bytes: bytearray | None, + entry_point: int, + program_bytes: bytearray | bytes | list[int], + aux_bin_blocks: list[tuple[str, list[int]]], + aux_headless_bin_blocks: list[list[int]], + ) -> None: pass diff --git a/src/outfmt/gensnapshot.py b/src/outfmt/gensnapshot.py index cf6e2b712..12999e68a 100644 --- a/src/outfmt/gensnapshot.py +++ b/src/outfmt/gensnapshot.py @@ -28,10 +28,10 @@ def patchAddr(self, addr: int, data: bytes): def __init__( self, - loader_bytes, - clear_addr, - mc_addr, - mc_bytes, + loader_bytes: bytearray | None, + clear_addr: int, + mc_addr: int, + mc_bytes: bytearray, ): """ Creates a snapshot object ready to run a BASIC program as if RUN was just executed. @@ -69,9 +69,8 @@ def __init__( eilast: Whether the last instruction prevents an interrupt """ - self.A = self.A2 = self.B = self.B2 = self.C = self.C2 = self.D = self.D2 = self.E = self.E2 = self.H = ( - self.H2 - ) = self.L = self.L2 = self.F = self.F2 = self.R = self.IXL = self.IXH = 0 + self.A = self.A2 = self.B = self.B2 = self.C = self.C2 = self.D = self.D2 = self.E = self.E2 = self.H = 0 + self.H2 = self.L = self.L2 = self.F = self.F2 = self.R = self.IXL = self.IXH = 0 self.IYH = 0x5C self.IYL = 0x3A # 0x5C3A is the normal value of IY for ROM use diff --git a/src/outfmt/sna.py b/src/outfmt/sna.py index 9d07ccbc2..1c26c1544 100644 --- a/src/outfmt/sna.py +++ b/src/outfmt/sna.py @@ -14,11 +14,11 @@ class SnaEmitter(CodeEmitter): def generate( self, - loader_bytes, - clear_addr, - entry_point, - program_bytes, - ): + loader_bytes: bytearray | None, + clear_addr: int, + entry_point: int, + program_bytes: bytearray, + ) -> bytearray: """ Format of .SNA file: @@ -90,17 +90,17 @@ def generate( def emit( self, - output_filename, - program_name, - loader_bytes, - entry_point, - program_bytes, - aux_bin_blocks, - aux_headless_bin_blocks, - ): + output_filename: str, + program_name: str, + loader_bytes: bytearray | None, + entry_point: int, + program_bytes: bytearray | bytes | list[int], + aux_bin_blocks: list[tuple[str, list[int]]], + aux_headless_bin_blocks: list[list[int]], + ) -> None: """Emit a .SNA file with the compiled bytes; ignores loader_bytes""" - sna_data = self.generate(None, entry_point - 1, entry_point, program_bytes) + sna_data = self.generate(None, entry_point - 1, entry_point, bytearray(program_bytes)) # Write output file with open(output_filename, "wb") as f: diff --git a/src/outfmt/tzx.py b/src/outfmt/tzx.py index fa679923b..4be8c046f 100755 --- a/src/outfmt/tzx.py +++ b/src/outfmt/tzx.py @@ -32,7 +32,7 @@ class TZX(CodeEmitter): HEADER_TYPE_CODE = 3 def __init__(self): - """Initializes the object with standard header""" + """Initializes the object with a standard header""" self.output = bytearray(b"ZXTape!") self.out(0x1A) self.out([self.VERSION_MAJOR, self.VERSION_MINOR]) @@ -44,14 +44,14 @@ def LH(self, value): return [valueL, valueH] - def out(self, l): + def out(self, l: int | list[int]) -> None: """Adds a list of bytes to the output string""" if not isinstance(l, list): l = [l] self.output.extend([int(i) & 0xFF for i in l]) - def standard_block(self, _bytes): + def standard_block(self, _bytes: bytearray | bytes | list[int]) -> None: """Adds a standard block of bytes""" self.out(self.BLOCK_STANDARD) # Standard block ID self.out(self.LH(1000)) # 1000 ms standard pause @@ -64,7 +64,7 @@ def standard_block(self, _bytes): self.out(checksum) - def dump(self, fname): + def dump(self, fname: str) -> None: """Saves TZX file to fname""" with open(fname, "wb") as f: f.write(self.output) @@ -122,21 +122,22 @@ def save_program(self, title, bytes, line=32768): def emit( self, - output_filename, - program_name, - loader_bytes, - entry_point, - program_bytes, - aux_bin_blocks, - aux_headless_bin_blocks, - ): - """Emits resulting tape file.""" + output_filename: str, + program_name: str, + loader_bytes: bytearray | None, + entry_point: int, + program_bytes: bytearray | bytes | list[int], + aux_bin_blocks: list[tuple[str, list[int]]], + aux_headless_bin_blocks: list[list[int]], + ) -> None: + """Emits the resulting tape file.""" if loader_bytes is not None: self.save_program("loader", loader_bytes, line=1) # Put line 0 to protect against MERGE self.save_code(program_name, entry_point, program_bytes) for name, block in aux_bin_blocks: self.save_code(name, 0, block) + for block in aux_headless_bin_blocks: self.standard_block(block) diff --git a/src/outfmt/z80.py b/src/outfmt/z80.py index 34ae1ba96..2cb6c7db5 100644 --- a/src/outfmt/z80.py +++ b/src/outfmt/z80.py @@ -168,13 +168,13 @@ def generate( def emit( self, - output_filename, - program_name, - loader_bytes, - entry_point, - program_bytes, - aux_bin_blocks, - aux_headless_bin_blocks, + output_filename: str, + program_name: str, + loader_bytes: bytearray | None, + entry_point: int, + program_bytes: bytearray | bytes | list[int], + aux_bin_blocks: list[tuple[str, list[int]]], + aux_headless_bin_blocks: list[list[int]], ): """Save a .Z80 file with the compiled bytes; ignores loader_bytes"""