toolchain: автотест .exe в MAME без участия человека

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 11:56:54 +03:00
parent 46bd9ad0f1
commit d652f89240
2 changed files with 164 additions and 1 deletions
+5 -1
View File
@@ -71,7 +71,11 @@ Quick wins:
- [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s - [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s
- [ ] `restore SP on EXIT` (паттерн z88dk +pps) — проверить нужность - [ ] `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: сейчас фильтруются в - [ ] linker duplicate-symbol warnings: сейчас фильтруются в
sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…); sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…);
радикально — --nostdlib с ручным списком модулей z80.lib радикально — --nostdlib с ручным списком модулей z80.lib
+159
View File
@@ -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 <path-to-exe> [--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()