"""Regression tests: decode each mode's emitted VIC-II bytes and check they reproduce the converter's own index image, and that every viewer assembles and fits. Run with `pytest` or directly: `python tests/test_roundtrip.py`. These tests exercise the byte-packing that the GUI preview deliberately does *not* touch (the preview renders from the index image), so they are the safety net that catches an encoding bug before it reaches a real C64. """ import os import sys import numpy as np sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from lenser import imageprep, palette as pal # noqa: E402 from lenser.convert import fli, hires, ifli, multicolor # noqa: E402 from lenser.viewer.assemble import SOURCES, build_viewer_prg, have_xa # noqa: E402 def _gradient(w, h): yy, xx = np.mgrid[0:h, 0:w] rgb = np.stack([(xx * 255 // w), (yy * 255 // h), ((xx + yy) * 255 // (w + h))], axis=-1) return rgb.astype(np.uint8) def _decode_mc(bitmap, screen, colram, bg): dec = np.zeros((200, 160), np.uint8) for cr in range(25): for cc in range(40): ci = cr * 40 + cc lut = [bg, screen[ci] >> 4, screen[ci] & 0xF, colram[ci] & 0xF] for r in range(8): byte = bitmap[cr * 320 + cc * 8 + r] for x in range(4): dec[cr * 8 + r, cc * 4 + x] = lut[(byte >> (6 - 2 * x)) & 3] return dec def test_multicolor_roundtrip(): img = imageprep.prepare(_imgobj(160, 200), 160, 200, 2.0, imageprep.PrepOptions()) c = multicolor.convert(img) d = c.data dec = _decode_mc(np.frombuffer(d[:8000], np.uint8), np.frombuffer(d[8000:9000], np.uint8), np.frombuffer(d[9000:10000], np.uint8), d[10000]) assert np.array_equal(dec, c.index_image) def test_hires_roundtrip(): img = imageprep.prepare(_imgobj(320, 200), 320, 200, 1.0, imageprep.PrepOptions()) c = hires.convert(img) bitmap = np.frombuffer(c.data[:8000], np.uint8) screen = np.frombuffer(c.data[8000:9000], np.uint8) dec = np.zeros((200, 320), np.uint8) for cr in range(25): for cc in range(40): ci = cr * 40 + cc fg, bgc = screen[ci] >> 4, screen[ci] & 0xF for r in range(8): byte = bitmap[cr * 320 + cc * 8 + r] for x in range(8): dec[cr * 8 + r, cc * 8 + x] = fg if (byte >> (7 - x)) & 1 else bgc assert np.array_equal(dec, c.index_image) def test_fli_roundtrip(): img = imageprep.prepare(_imgobj(160, 200), 160, 200, 2.0, imageprep.PrepOptions()) c = fli.convert(img) d = c.data screens = [np.frombuffer(d[L * 1024:L * 1024 + 1000], np.uint8) for L in range(8)] bitmap = np.frombuffer(d[8192:8192 + 8000], np.uint8) colram = np.frombuffer(d[16384:16384 + 1000], np.uint8) bg = d[17384] dec = np.zeros((200, 160), np.uint8) for cr in range(25): for cc in range(40): ci = cr * 40 + cc for r in range(8): sb = screens[r][ci] lut = [bg, sb >> 4, sb & 0xF, colram[ci] & 0xF] byte = bitmap[cr * 320 + cc * 8 + r] for x in range(4): dec[cr * 8 + r, cc * 4 + x] = lut[(byte >> (6 - 2 * x)) & 3] assert np.array_equal(dec, c.index_image) def test_interlace_blend_better(): """Interlace blend error should beat plain multicolor on a gradient.""" img = imageprep.prepare(_imgobj(160, 200), 160, 200, 2.0, imageprep.PrepOptions()) assert ifli.convert(img).error < multicolor.convert(img).error + 1e-6 assert len(ifli.convert(img).data) == 25577 def test_vic20_multicolor_roundtrip(): from lenser.vic20.convert import multicolor as vmc img = imageprep.prepare(_imgobj(vmc.WIDTH, vmc.HEIGHT), vmc.WIDTH, vmc.HEIGHT, vmc.PIXEL_ASPECT, imageprep.PrepOptions()) c = vmc.convert(img) d = c.data chardata, screen, color = d["chardata"], d["screen"], d["color"] bg, border, aux = d["bg"], d["border"], d["aux"] dec = np.zeros((vmc.HEIGHT, vmc.WIDTH), np.uint8) for cr in range(vmc.N_ROWS): for cc in range(vmc.N_COLS): ci = cr * vmc.N_COLS + cc f = color[ci] & 0x07 lut = [bg, border, f, aux] ch = int(screen[ci]) for r in range(8): byte = chardata[ch * 8 + r] for x in range(4): code = (byte >> (6 - 2 * x)) & 3 dec[cr * 8 + r, cc * 4 + x] = lut[code] assert np.array_equal(dec, c.index_image) # colour RAM must flag multicolour (bit 3) and keep fg within 0-7 assert np.all((color & 0x08) == 0x08) assert np.all((color & 0x07) <= 7) def test_vic20_hires_roundtrip(): from lenser.vic20.convert import hires as vhi img = imageprep.prepare(_imgobj(vhi.WIDTH, vhi.HEIGHT), vhi.WIDTH, vhi.HEIGHT, vhi.PIXEL_ASPECT, imageprep.PrepOptions()) c = vhi.convert(img) d = c.data chardata, screen, color = d["chardata"], d["screen"], d["color"] bg = d["bg"] dec = np.zeros((vhi.HEIGHT, vhi.WIDTH), np.uint8) for cr in range(vhi.N_ROWS): for cc in range(vhi.N_COLS): ci = cr * vhi.N_COLS + cc f = color[ci] & 0x07 ch = int(screen[ci]) for r in range(8): byte = chardata[ch * 8 + r] for x in range(8): dec[cr * 8 + r, cc * 8 + x] = f if (byte >> (7 - x)) & 1 else bg assert np.array_equal(dec, c.index_image) def test_spectrum_roundtrip(): from lenser.spectrum.convert import hires as zh from lenser.spectrum import palette as zpal img = imageprep.prepare(_imgobj(zh.WIDTH, zh.HEIGHT), zh.WIDTH, zh.HEIGHT, zh.PIXEL_ASPECT, imageprep.PrepOptions()) c = zh.convert(img) scr = c.data assert len(scr) == 6912 dec = np.zeros((zh.HEIGHT, zh.WIDTH), np.uint8) for cy in range(zh.N_ROWS): for cx in range(zh.N_COLS): attr = scr[6144 + cy * 32 + cx] bright = (attr >> 6) & 1 ink = (bright << 3) | (attr & 7) paper = (bright << 3) | ((attr >> 3) & 7) for r in range(8): y = cy * 8 + r byte = scr[zh._bitmap_offset(y, cx)] for px in range(8): bit = (byte >> (7 - px)) & 1 dec[y, cx * 8 + px] = ink if bit else paper assert np.array_equal(dec, c.index_image) # every cell's two colours must share the BRIGHT bit (no normal/bright mix) for cy in range(zh.N_ROWS): for cx in range(zh.N_COLS): attr = scr[6144 + cy * 32 + cx] assert (attr & 0x80) == 0 # FLASH never set def test_mono_modes_available_and_convert(): """EVERY platform offers a monochrome mode that converts to a luminance- matched image (indices restricted to its grey ramp).""" from lenser import platforms for p in platforms.PLATFORMS: assert "mono" in platforms.modes(p), f"{p} missing mono mode" # the mono mode actually converts (greyscale) on every platform prep0 = imageprep.PrepOptions() src = _imgobj(320, 200) for p in platforms.PLATFORMS: c = platforms.convert(p, src, "mono", platforms.palettes(p)[0], "floyd", False, prep0, "grayscale") assert c.mode == "mono", f"{p} mono did not report mode 'mono'" prep = imageprep.PrepOptions() img = _imgobj(256, 192) # ti99 + spectrum mono decode exactly like their gm2 / hires colour formats from lenser.ti99.convert import convert_image as ti c = ti(img, mode="mono", dither_mode="atkinson") assert len(c.data) == 6912 and c.mode == "mono" from lenser.spectrum.convert import convert_image as zx c = zx(img, mode="mono", dither_mode="atkinson") assert len(c.data) == 6912 # dictionary platforms: mono produces the same data dict their viewer expects from lenser.vic20.convert import convert_image as v d = v(img, mode="mono", dither_mode="floyd").data assert d["bg"] == 0 and len(d["chardata"]) == 2048 and d["color"].max() <= 7 from lenser.intv.convert import convert_image as iv d = iv(img, mode="mono", dither_mode="none").data assert len(d["gram"]) == 64 * 8 and len(d["cards"]) == 240 def test_a5200_cart(): """Atari 5200 reuses the Atari GTIA encoders and packs a 32K cartridge whose display list + bitmap ANTIC reads straight from ROM.""" from lenser.a5200.convert import convert_image from lenser.a5200.viewer.assemble import build_cart, have_xa, BITMAP_ADDR img = _imgobj(160, 192) for mode in ("gr15", "gr8", "gr9"): c = convert_image(img, mode=mode, dither_mode="floyd") assert c.mode == mode if not have_xa(): continue # all three display durations assemble + pack to 32K for disp, secs in (("forever", 0), ("key", 0), ("seconds", 10)): rom = build_cart(c.mode, bytes(c.data), display=disp, seconds=secs) assert len(rom) == 0x8000 # 32K # start vector at $BFFE-F points into the cart start = rom[0xBFFE - 0x4000] | (rom[0xBFFF - 0x4000] << 8) assert start == 0x4000 # bitmap landed at $6000 (so the 4K split maps to $6000/$7000) assert rom[BITMAP_ADDR - 0x4000:BITMAP_ADDR - 0x4000 + 8] == c.data[:8] def test_a7800_cart(): """Atari 7800 (MARIA) packs a 48K .a78 with the bitmap + display lists + DLL that MARIA DMAs from ROM.""" from lenser.a7800.convert import convert_image from lenser.a7800.viewer.assemble import (build_cart, have_xa, BITMAP_ADDR, DLL_ADDR, LINES, N_SEG) img = _imgobj(160, 192) for mode in ("c160", "mono"): c = convert_image(img, mode=mode, dither_mode="floyd") assert c.mode == mode # data = bitmap(7680) + seg_palettes(192*4) + colours(25) assert len(c.data) == 7680 + LINES * N_SEG + 25 assert all(p < 8 for p in c.data[7680:7680 + LINES * N_SEG]) # palette 0-7 if not have_xa(): continue rom = build_cart(bytes(c.data), title="t") assert len(rom) == 128 + 0xC000 # header + 48K assert rom[1:1 + 9] == b"ATARI7800" # .a78 signature body = rom[128:] # reset vector points at the viewer entry ($4000) assert body[0xFFFC - 0x4000] | (body[0xFFFD - 0x4000] << 8) == 0x4000 # bitmap landed at $8000 assert body[BITMAP_ADDR - 0x4000:BITMAP_ADDR - 0x4000 + 8] == c.data[:8] def test_spectrum_sna(): from lenser.spectrum.convert import hires as zh from lenser.spectrum import snapshot img = imageprep.prepare(_imgobj(zh.WIDTH, zh.HEIGHT), zh.WIDTH, zh.HEIGHT, zh.PIXEL_ASPECT, imageprep.PrepOptions()) sna = snapshot.build_sna(zh.convert(img).data, border=0) assert len(sna) == 27 + 49152 # header + 48K RAM ram = sna[27:] assert ram[0x8000 - 0x4000:0x8000 - 0x4000 + 3] == bytes([0xF3, 0x18, 0xFE]) # SP (header offset 0x17) points at the stub return address sp = sna[0x17] | (sna[0x18] << 8) lo = ram[sp - 0x4000]; hi = ram[sp - 0x4000 + 1] assert (lo | (hi << 8)) == 0x8000 def test_vic20_cart_builds(): from lenser.vic20.viewer.assemble import build_cart, have_xa if not have_xa(): return # xa not installed; skip from lenser.vic20.convert import multicolor as vmc img = imageprep.prepare(_imgobj(vmc.WIDTH, vmc.HEIGHT), vmc.WIDTH, vmc.HEIGHT, vmc.PIXEL_ASPECT, imageprep.PrepOptions()) rom = build_cart(vmc.convert(img).data) assert len(rom) == 0x2000 # full 8K cart assert rom[4:9] == bytes([0x41, 0x30, 0xC3, 0xC2, 0xCD]) # "A0CBM" signature def test_c128_mono_prg(): # mono is high-res greyscale via the custom-charset (font) path, like hicolor from lenser.c128.convert import mono as c128mono, hicolor as hc from lenser.c128.viewer.assemble import (build_prg_hicolor, have_xa, BASIC_START, DATA_ORG, _STUB) img = imageprep.prepare(_imgobj(c128mono.WIDTH, c128mono.HEIGHT), c128mono.WIDTH, c128mono.HEIGHT, c128mono.PIXEL_ASPECT, imageprep.PrepOptions()) conv = c128mono.convert(img) assert conv.mode == "mono" assert len(conv.data) == hc.VDC_LEN # full VDC RAM image assert conv.meta["vdc_mode"] == "hicolor" # uses the font-mode viewer # mono only uses the four greys, so every cell's ink is one of them greys = set(c128mono.GREYS) attrs = conv.data[hc.ATTR_ADDR:hc.ATTR_ADDR + hc.ROWS * hc.COLS] assert all((b & 0x0F) in greys for b in attrs) if not have_xa(): return # xa not installed; skip the assembly half prg = build_prg_hicolor(bytes(conv.data), conv.meta["fgbg"]) assert prg[:2] == bytes([BASIC_START & 0xFF, BASIC_START >> 8]) # load $1C01 assert prg[2:2 + len(_STUB)] == _STUB # BASIC 10 SYS7200 stub assert bytes(prg[-hc.VDC_LEN:]) == bytes(conv.data) assert len(prg) == 2 + (DATA_ORG - BASIC_START) + hc.VDC_LEN def test_c128_color_prg(): from lenser.c128.convert import color as c128color from lenser.c128.viewer.assemble import (build_prg_color, have_xa, BASIC_START, DATA_ORG) img = imageprep.prepare(_imgobj(c128color.WIDTH, c128color.HEIGHT), c128color.WIDTH, c128color.HEIGHT, c128color.PIXEL_ASPECT, imageprep.PrepOptions()) conv = c128color.convert(img) assert len(conv.data) == 8000 # 80x100 attribute bytes assert conv.meta["vdc_mode"] == "color" # colour lives in the high nibble; low nibble (bg) is 0 assert all((b & 0x0F) == 0 for b in conv.data) if not have_xa(): return prg = build_prg_color(bytes(conv.data), conv.meta["fgbg"]) assert prg[:2] == bytes([BASIC_START & 0xFF, BASIC_START >> 8]) assert bytes(prg[-8000:]) == bytes(conv.data) # attributes land at $2000 assert len(prg) == 2 + (DATA_ORG - BASIC_START) + 8000 def test_c128_hicolor_prg(): from lenser.c128.convert import hicolor as hc from lenser.c128.viewer.assemble import (build_prg_hicolor, have_xa, BASIC_START, DATA_ORG) img = imageprep.prepare(_imgobj(hc.WIDTH, hc.HEIGHT), hc.WIDTH, hc.HEIGHT, hc.PIXEL_ASPECT, imageprep.PrepOptions()) conv = hc.convert(img) assert len(conv.data) == hc.VDC_LEN # full 16K VDC RAM image assert conv.meta["vdc_mode"] == "hicolor" # ink in the low nibble, bit 7 may select bank 1; blink/underline/reverse off attrs = conv.data[hc.ATTR_ADDR:hc.ATTR_ADDR + hc.ROWS * hc.COLS] assert all((b & 0x70) == 0 for b in attrs) if not have_xa(): return prg = build_prg_hicolor(bytes(conv.data), conv.meta["fgbg"]) assert prg[:2] == bytes([BASIC_START & 0xFF, BASIC_START >> 8]) assert bytes(prg[-hc.VDC_LEN:]) == bytes(conv.data) assert len(prg) == 2 + (DATA_ORG - BASIC_START) + hc.VDC_LEN def test_c16_hires_prg(): from lenser.c16.convert import hires as c16h from lenser.c16.viewer.assemble import (build_prg, have_xa, BASIC_START, BITMAP_ORG, _STUB) img = imageprep.prepare(_imgobj(c16h.WIDTH, c16h.HEIGHT), c16h.WIDTH, c16h.HEIGHT, c16h.PIXEL_ASPECT, imageprep.PrepOptions()) conv = c16h.convert(img) assert conv.mode == "hires" assert len(conv.data) == 10000 # bitmap 8000 + attr 1000 + ch 1000 if not have_xa(): return prg = build_prg(bytes(conv.data)) assert prg[:2] == bytes([BASIC_START & 0xFF, BASIC_START >> 8]) # load $1001 assert prg[2:2 + len(_STUB)] == _STUB # 10 SYS4128 # the 8000-byte bitmap lands at $2000 off = 2 + (BITMAP_ORG - BASIC_START) assert bytes(prg[off:off + 8000]) == bytes(conv.data[:8000]) def test_plus4_reuses_c16(): # Plus/4 uses the same TED + BASIC 3.5 as the C16, so its encoder, modes and # .prg are identical -- the plus4 package re-exports the C16 implementation. from lenser.plus4.convert import MODES, convert_image from lenser.c16.convert import MODES as C16_MODES, convert_image as c16_convert from lenser.plus4.exporter import export_prg from lenser.c16.exporter import export_prg as c16_export_prg assert MODES == C16_MODES assert convert_image is c16_convert and export_prg is c16_export_prg img = _imgobj(320, 200) conv = convert_image(img, mode="hires", dither_mode="floyd") assert conv.mode == "hires" and len(conv.data) == 10000 def test_cpc_sna(): from lenser.cpc.convert import convert_image, MODES from lenser.cpc.exporter import export_sna from lenser.cpc import snapshot import tempfile, os img = _imgobj(320, 200) assert MODES == ["mode0", "mode1", "mono"] for m, ncol in (("mode0", 16), ("mode1", 4), ("mono", 2)): conv = convert_image(img, mode=m, dither_mode="floyd") assert len(conv.data) == 0x4000 # 16K screen at &C000 assert conv.data_addr == 0xC000 assert 1 <= len(conv.meta["inks"]) <= ncol with tempfile.TemporaryDirectory() as td: p = export_sna(conv, os.path.join(td, "x.sna")) sna = open(p, "rb").read() assert sna[:8] == b"MV - SNA" # CPC snapshot signature assert len(sna) == 0x100 + 0x10000 # 256 header + 64K RAM assert (sna[0x40] & 0x03) == conv.meta["cpc_mode"] # mode in RMR def test_coco3_cart(): from lenser.coco3.convert import convert_image, MODES from lenser.coco3.exporter import export_ccc import tempfile, os img = _imgobj(320, 192) assert MODES == ["gr16", "gr4", "mono"] for m, ncol in (("gr16", 16), ("gr4", 4), ("mono", 2)): conv = convert_image(img, mode=m, dither_mode="floyd") assert len(conv.data) == 15360 # 80 bytes/row * 192, linear assert conv.data_addr == 0x4000 assert 1 <= len(conv.meta["inks"]) <= ncol assert all(0 <= c < 64 for c in conv.meta["inks"]) # 6-bit GIME colours with tempfile.TemporaryDirectory() as td: p = export_ccc(conv, os.path.join(td, "x.ccc")) rom = open(p, "rb").read() assert len(rom) == 0x4000 # 16K cartridge assert rom[:2] == bytes([0x1A, 0x50]) # viewer starts: ORCC #$50 def test_nes_cart(): from lenser.nes.convert import convert_image, MODES from lenser.nes.exporter import export_nes from lenser.nes.cartridge import have_xa import tempfile, os img = _imgobj(256, 240) assert MODES == ["bg", "mono"] for m in MODES: conv = convert_image(img, mode=m, dither_mode="floyd") d = conv.data assert len(d["palette"]) == 32 assert len(d["nametable"]) == 1024 # 960 names + 64 attribute assert len(d["chr"]) == 8192 # 8K CHR-ROM (256 bg tiles) assert all(0 <= b < 64 for b in d["palette"]) # 6-bit NES colours if not have_xa(): continue with tempfile.TemporaryDirectory() as td: p = export_nes(conv, os.path.join(td, "x.nes")) rom = open(p, "rb").read() assert rom[:4] == b"NES\x1a" # iNES signature assert rom[4] == 1 and rom[5] == 1 # 16K PRG + 8K CHR (NROM) assert len(rom) == 16 + 0x4000 + 0x2000 # reset vector ($FFFC) -> $C000 (viewer start) assert rom[16 + 0x3FFC] == 0x00 and rom[16 + 0x3FFD] == 0xC0 def test_iigs_dsk(): from lenser.iigs.convert import convert_image, MODES from lenser.iigs.viewer.assemble import assemble_boot, have_xa from lenser.iigs.exporter import export_dsk import tempfile, os img = _imgobj(320, 200) assert MODES == ["shr", "mono"] for m in MODES: conv = convert_image(img, mode=m, dither_mode="floyd") assert len(conv.data) == 0x8000 # 32K SHR block ($2000-$9FFF) assert conv.data_addr == 0x2000 if not have_xa(): return boot = assemble_boot() assert len(boot) <= 256 # fits one boot sector assert boot[1] == 0xAD # entry: LDA dpage (abs) with tempfile.TemporaryDirectory() as td: conv = convert_image(img, mode="shr", dither_mode="floyd") p = export_dsk(conv, os.path.join(td, "x.dsk")) dsk = open(p, "rb").read() assert len(dsk) == 143360 # 140K 5.25" disk assert dsk[:len(boot)] == boot # boot sector at track 0 sector 0 def test_pet_prg(): from lenser.pet.convert import convert_image from lenser.pet import palette as petpal from lenser.pet.viewer.assemble import build_prg, have_xa, BASIC_START, _STUB # all 16 quadrant codes distinct -> a real one-to-one block mapping assert len(set(petpal.QUAD)) == 16 for cols, scr in ((40, 1000), (80, 2000)): conv = convert_image(_imgobj(320, 200), cols=cols, dither_mode="floyd") assert conv.mode == "mono" assert len(conv.data) == scr # screen-RAM bytes assert conv.data_addr == 0x8000 assert all(b in petpal.QUAD for b in conv.data) # only quadrant codes if not have_xa(): continue prg = build_prg(bytes(conv.data)) assert prg[:2] == bytes([BASIC_START & 0xFF, BASIC_START >> 8]) # $0401 assert prg[2:2 + len(_STUB)] == _STUB # 10 SYS1056 def test_sms_cart(): from lenser.sms.convert import convert_image, MODES from lenser.sms.exporter import export_sms from lenser.sms import viewer as smsv import tempfile, os img = _imgobj(256, 192) assert MODES == ["bg", "mono"] for m in MODES: conv = convert_image(img, mode=m, dither_mode="floyd") d = conv.data assert len(d["patterns"]) == 448 * 32 # <=448 tiles, no name-table clash assert len(d["nametable"]) == 32 * 24 * 2 assert len(d["palette"]) == 32 # 2 x 16 colours assert all(0 <= b < 64 for b in d["palette"]) with tempfile.TemporaryDirectory() as td: p = export_sms(conv, os.path.join(td, "x.sms")) rom = open(p, "rb").read() assert len(rom) == 0x8000 # 32K cartridge assert rom[:1] == bytes([0xF3]) # Z80 viewer starts with DI assert rom[0x7FF0:0x7FF8] == b"TMR SEGA" # SMS header signature def test_amiga_adf(): from lenser.amiga.convert import convert_image, MODES from lenser.amiga.exporter import export_adf import tempfile, os, struct img = _imgobj(320, 200) assert MODES == ["lowres", "mono"] for m, nplanes in (("lowres", 5), ("mono", 4)): conv = convert_image(img, mode=m, dither_mode="floyd") d = conv.data assert d["nplanes"] == nplanes and d["ham"] is False assert len(d["planes"]) == nplanes * 40 * 200 # contiguous bitplanes assert all(0 <= c < 4096 for c in d["colors"]) # 12-bit colours with tempfile.TemporaryDirectory() as td: p = export_adf(conv, os.path.join(td, "x.adf")) adf = open(p, "rb").read() assert len(adf) == 901120 # 880K floppy assert adf[0:4] == b"DOS\x00" # boot block id # boot block longword checksum must total $FFFFFFFF (else not bootable) s = 0 for i in range(0, 1024, 4): s += struct.unpack(">I", adf[i:i + 4])[0] if s > 0xFFFFFFFF: s = (s + 1) & 0xFFFFFFFF assert s == 0xFFFFFFFF def test_viewers_assemble_and_fit(): if not have_xa(): return # xa not installed; skip sizes = {"hires": 9000, "multicolor": 10001, "fli": 17385, "fli_ntsc": 17385, "interlace": 25577} for key in SOURCES: if key not in sizes: continue # e.g. "slideshow" is code-only (own builder/test) prg = build_viewer_prg(key, bytes(sizes[key]), 0x4000 if key.startswith("fli") else 0x2000) assert prg[:2] == bytes([0x01, 0x08]) # PRG load address $0801 def _imgobj(w, h): from PIL import Image return Image.fromarray(_gradient(w, h), "RGB") if __name__ == "__main__": fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for fn in fns: fn() print(f"PASS {fn.__name__}") print(f"\nAll {len(fns)} tests passed.")