From d652f892407fdfa31be4238c43fb8ef55a27cf57 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Wed, 8 Jul 2026 11:56:54 +0300 Subject: [PATCH] =?UTF-8?q?toolchain:=20=D0=B0=D0=B2=D1=82=D0=BE=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=20.exe=20=D0=B2=20MAME=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20=D1=83=D1=87=D0=B0=D1=81=D1=82=D0=B8=D1=8F=20=D1=87=D0=B5?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=D0=B5=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AUTORUN.BAT chainload из system.bat (правится пользователем один раз) + mame_auto_test.py: кладёт .exe и сгенерированный AUTORUN.BAT на дискету, гоняет MAME с Lua-таймингом (register_periodic + manager.machine.time) для скриншотов и выхода по таймауту. Natural keyboard (Lua natkeyboard:post/post_coded, -autoboot_command) и прямая инъекция через ioport.fields[...]:set_value() не работают на этом драйвере — перепробовано разными способами; AUTORUN.BAT chainload оказался единственным надёжным путём запуска без участия человека. Co-Authored-By: Claude Sonnet 5 --- docs/TODO.md | 6 +- toolchain/mame_auto_test.py | 159 ++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 toolchain/mame_auto_test.py diff --git a/docs/TODO.md b/docs/TODO.md index df701c8..7fea377 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -71,7 +71,11 @@ Quick wins: - [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s - [ ] `restore SP on EXIT` (паттерн z88dk +pps) — проверить нужность -- [ ] CI: MAME с -aviwrite для screenshot-сравнения без человека +- [x] ~~CI: MAME с -aviwrite для screenshot-сравнения без человека~~ — + `toolchain/mame_auto_test.py` (2026-07-08): AUTORUN.BAT chainload + + Lua-таймер для скриншотов/выхода; сравнение пока визуальное + (Claude читает скриншот), не автоматический diff. Подробности: + memory/mame_autotest_autorun. - [ ] linker duplicate-symbol warnings: сейчас фильтруются в sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…); радикально — --nostdlib с ручным списком модулей z80.lib diff --git a/toolchain/mame_auto_test.py b/toolchain/mame_auto_test.py new file mode 100644 index 0000000..1db8a0c --- /dev/null +++ b/toolchain/mame_auto_test.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" +mame_auto_test.py — headless-прогон .exe в MAME без участия человека. + +Механизм: system.bat на системном диске (пользователь один раз +добавил в конец безусловный вызов "A:\\AUTORUN.BAT") запускает файл, +который этот скрипт кладёт на дискету A: вместе с тестируемым .exe. +Загрузка идёт полностью автоматически, без эмуляции нажатий клавиш — +natural keyboard (Lua natkeyboard:post/post_coded, -autoboot_command) +И прямая инъекция состояния через ioport.fields[...]:set_value() на +этом драйвере/сборке MAME НЕ РАБОТАЮТ (проверено многократно, разными +таймингами — см. memory/mame_autotest_autorun). AUTORUN.BAT — пока +единственный надёжный способ что-либо запустить без участия человека. + +Скрипт делает снимки экрана в заданные моменты времени (секунды +эмулированного времени от старта машины, НЕ от нажатия клавиш — их +нет) и завершает MAME по таймауту (-seconds_to_run эквивалент через +Lua manager.machine:exit()). Снимки — единственный способ проверки +результата; их читает Claude (Read с картинкой) визуально, точного +сравнения строк нет (алгоритм чтения текстового VRAM не реализован). + +ВАЖНО: имя генерируемого bat-файла на диске жёстко "AUTORUN.BAT" — +make_disk.py берёт имя файла НА ДИСКЕ из basename ЛОКАЛЬНОГО пути +(обрезая до 8.3), так что локальный файл-стейджер тоже обязан +называться ровно AUTORUN.BAT, иначе system.bat его не найдёт (баг, +уже наступали — см. memory). + +Перед каждым запуском проверяется, что не осталось висящих копий +MAME (несколько одновременных инстансов дают недостоверный результат +— тоже наступали). + +Использование: + python3 toolchain/mame_auto_test.py [--data f1 f2 ...] + [--snap t1,t2,...] [--timeout N] + +Пример: + python3 toolchain/mame_auto_test.py tests/rt_test/rt_test.exe \\ + --snap 4,6,8,10,12,14 --timeout 15 +""" + +import argparse +import os +import shutil +import subprocess +import sys + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MAME_DIR = os.path.join(PROJECT_ROOT, "mame", "v306") +IMG_DIR = os.path.join(MAME_DIR, "IMG") +MC_IMG = os.path.join(IMG_DIR, "mc.img") +SNAP_DIR = os.path.join(MAME_DIR, "snap_auto") +LUA_SCRIPT = os.path.join(MAME_DIR, "_auto_test_gen.lua") +AUTORUN_DIR = os.path.join(MAME_DIR, "_autorun_stage") +AUTORUN_BAT = os.path.join(AUTORUN_DIR, "AUTORUN.BAT") # имя ОБЯЗАНО быть точным + +COMMON_ARGS = [ + "-skip_gameinfo", "-video", "opengl", "-window", "-nofilter", + "-beta:wd179x:0", "35hd", "-beta:wd179x:1", "35hd", + "-flop1", MC_IMG, "-flop2", os.path.join(IMG_DIR, "dss171u.img"), + "-isa0", "zxbus_adapter", "-isa0:zxbus_adapter:card", "neogs", + "-hard1", os.path.join(IMG_DIR, "sp_hdd_sys.chd"), + "-hard2", os.path.join(IMG_DIR, "sp_hdd_media.chd"), + "-ata2:0", "cdrom", "-cdrom", os.path.join(IMG_DIR, "SprinterCD.iso"), + "-bios", "v3.06", +] + + +def check_no_stray_mame(): + out = subprocess.run(["pgrep", "-fl", "mame"], capture_output=True, text=True).stdout + lines = [l for l in out.splitlines() if "mame.arm" in l or "mame.exe" in l or (" mame " in l)] + if lines: + print("ОШИБКА: обнаружены запущенные копии MAME — закройте их перед автотестом:") + print(out) + sys.exit(1) + + +def gen_lua(snap_times, timeout): + lines = [ + "local start_time = nil", + "local snaps_done = {}", + "local function log(msg) print('[autotest] ' .. msg) end", + "emu.add_machine_reset_notifier(function() start_time = manager.machine.time end)", + "local snap_times = {" + ",".join(str(t) for t in snap_times) + "}", + "emu.register_periodic(function()", + " if not start_time then return end", + " local elapsed = (manager.machine.time - start_time).seconds", + " for _, st in ipairs(snap_times) do", + " if not snaps_done[st] and elapsed >= st then", + " snaps_done[st] = true", + " manager.machine.video:snapshot()", + " log('snap at t=' .. elapsed .. ' (target ' .. st .. ')')", + " end", + " end", + f" if elapsed >= {timeout} then", + " log('exiting at t=' .. elapsed)", + " manager.machine:exit()", + " end", + "end)", + ] + with open(LUA_SCRIPT, "w") as f: + f.write("\n".join(lines) + "\n") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("exe", help="путь к .exe теста") + ap.add_argument("--data", nargs="*", default=[], help="доп. файлы на дискету") + ap.add_argument("--snap", default="4,6,8,10,12", + help="секунды эмулированного времени для скриншотов, через запятую") + ap.add_argument("--timeout", type=int, default=13, help="сек. эмуляции до выхода") + args = ap.parse_args() + + check_no_stray_mame() + + exe_path = os.path.abspath(args.exe) + if not os.path.isfile(exe_path): + print(f"ОШИБКА: файл не найден: {exe_path}") + sys.exit(1) + exe_name = os.path.basename(exe_path).upper() + + os.makedirs(AUTORUN_DIR, exist_ok=True) + with open(AUTORUN_BAT, "wb") as f: + f.write(f"A:\\{exe_name}\r\n".encode("ascii")) + + snap_times = [float(x) for x in args.snap.split(",")] + gen_lua(snap_times, args.timeout) + + if os.path.exists(MC_IMG): + shutil.copy(MC_IMG, MC_IMG + ".bak") + + disk_files = [exe_path] + [os.path.abspath(p) for p in args.data] + [AUTORUN_BAT] + subprocess.run([sys.executable, os.path.join(MAME_DIR, "make_disk.py"), MC_IMG] + disk_files, + check=True, cwd=MAME_DIR) + + snap_sub = os.path.join(SNAP_DIR, "sprinter") + if os.path.isdir(snap_sub): + shutil.rmtree(snap_sub) + + cmd = ["timeout", str(args.timeout + 8), "./mame.arm", "sprinter"] + COMMON_ARGS + [ + "-snapshot_directory", SNAP_DIR, + "-autoboot_script", LUA_SCRIPT, + ] + print("Запуск:", " ".join(cmd)) + result = subprocess.run(cmd, cwd=MAME_DIR, capture_output=True, text=True) + print(result.stdout) + if result.returncode not in (0, 124): + print("STDERR:", result.stderr) + + if os.path.isdir(snap_sub): + shots = sorted(os.listdir(snap_sub)) + print("Скриншоты:") + for s in shots: + print(" ", os.path.join(snap_sub, s)) + else: + print("Скриншотов не создано.") + + +if __name__ == "__main__": + main()