docs: единый справочник по автотестам в MAME; убрать метод AUTORUN.BAT

- docs/mame-autotest.md — исчерпывающий документ: запуск, ввод команд,
  скриншоты, завершение сессий, анализ, раскладка клавиатуры, все квирки.
  Одного этого документа достаточно, чтобы работать с MAME в режиме
  автотестирования.
- mame_interactive.py теперь единственный инструмент: авто-запускает exe
  вводом пути (a:\<exe>+Enter), --step опционален (доп. ввод в программу),
  умные дефолты снимков/таймаута.
- удалён mame_auto_test.py (старый метод через AUTORUN.BAT chainload) и
  все упоминания AUTORUN.BAT в доках; интерактивный ввод его заменил.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 16:41:45 +03:00
parent 657c6d2955
commit 3bf50f7ff7
4 changed files with 319 additions and 225 deletions
-159
View File
@@ -1,159 +0,0 @@
#!/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()
+73 -62
View File
@@ -1,53 +1,54 @@
#!/usr/bin/env python3
"""
mame_interactive.py — скриптовый ИНТЕРАКТИВНЫЙ ввод в DSS через MAME.
r"""
mame_interactive.py — автотест .exe в MAME без участия человека.
В отличие от mame_auto_test.py (который запускает .exe через AUTORUN.BAT
chainload и НЕ умеет вводить с клавиатуры), этот инструмент печатает
произвольный текст в командную строку DSS, эмулируя нажатия клавиш.
Полный справочник по автотестированию в MAME: docs/mame-autotest.md.
КАК это работает (важное открытие 2026-07-08, см.
memory/mame_autotest_autorun):
у Sprinter в MAME ДВЕ клавиатуры. `IO_LINE0..7` — легаси ZX-матрица
(порт 0xFE), которую DSS для командной строки НЕ читает — именно в неё
безрезультатно били natkeyboard/-autoboot_command/set_value раньше.
Настоящая клавиатура DSS — AT/PS-2 устройство `:kbd:ms_naturl`,
подключённое последовательно к SIO Z84C015. Если через Lua
`ioport.ports[":kbd:ms_naturl:PX.Y"].fields[...]:set_value(1/0)`
нажимать/отпускать поля ЭТОГО устройства, at_keyboard сам генерит
scancode'ы → SIO → DSS. Проверено: `dir`<Enter> выполняется.
Инструмент запускает Sprinter в MAME, вводит команды в DSS эмуляцией
нажатий клавиш, снимает скриншоты в заданные моменты и завершает сессию
по таймауту. Ввод идёт в НАСТОЯЩУЮ клавиатуру DSS — AT/PS-2 устройство
`:kbd:ms_naturl` (подключено последовательно к SIO Z84C015): через Lua
`ioport.ports[":kbd:ms_naturl:PX.Y"].fields[...]:set_value(1/0)` жмём/
отпускаем поля, at_keyboard сам генерит scancode'ы → SIO → DSS.
(ZX-матрица `IO_LINE0..7` и natkeyboard/-autoboot_command командную
строку DSS НЕ достают — не использовать.)
Тонкости, на которых уже наступали:
- `attotime.seconds` — ЦЕЛОЕ (отбрасывает дробь); для субсекундного
тайминга берём `seconds + attoseconds/1e18`.
- клавишу держать коротко (~0.06с) — иначе срабатывает автоповтор
(typematic) и вместо `d` получается `dddddd`.
- между символами ~0.14с, чтобы scancode'ы не сливались.
- дискета БЕЗ AUTORUN.BAT → загрузка встаёт на приглашении `C:\>`
(с AUTORUN.BAT сразу ушла бы в автозапуск .exe).
Тонкости (детали — в docs/mame-autotest.md):
- attotime.seconds — ЦЕЛОЕ; субсекундный тайминг = seconds +
attoseconds/1e18.
- клавишу держать коротко (~0.06с), между символами ~0.14с — иначе
автоповтор (typematic) и слипание scancode'ов.
- загрузка должна встать на приглашении C:\> (system.bat не должен
автозапускать Flex Navigator/приложение).
- перед запуском проверяем, что нет висящих копий MAME.
Использование:
python3 toolchain/mame_interactive.py [exe] [--data f ...] \\
--step "T:TEXT" [--step "T:TEXT" ...] \\
[--snap t1,t2,...] [--timeout N]
python3 toolchain/mame_interactive.py [exe] [--data f ...] \
[--launch-at T] [--step "T:TEXT" ...] [--snap t1,t2,...] [--timeout N]
exe / --data — файлы, кладущиеся на дискету A: (опционально;
чтобы можно было напечатать, напр., a:\\name.exe).
--step "T:TEXT" — в момент T секунд (эмуляции) напечатать TEXT.
Можно указывать несколько раз (диалог с программой).
В TEXT поддержаны \\n (Enter) и \\t (Tab); символы
с Shift (заглавные, ! @ : \\ и т.п.) — автоматически.
--snap — секунды для скриншотов (по умолчанию — авто).
--timeout — секунд эмуляции до выхода.
exe — .exe, который кладётся на дискету A: и АВТОМАТИЧЕСКИ
запускается (печатается a:\<EXE> + Enter в момент
--launch-at). Без exe работаем на голой строке.
--data f ... — доп. файлы на дискету A:.
--launch-at T — секунда, когда печатается запуск exe (по умолч. 8).
--step "T:TEXT" — в момент T сек напечатать TEXT (можно много раз —
диалог с уже запущенной программой). В TEXT: \n =
Enter, \t = Tab; заглавные/символы с Shift — авто.
--snap — секунды для скриншотов (по умолчанию — авто).
--timeout — секунд эмуляции до выхода.
Пример (набрать DIR на приглашении и снять экран):
python3 toolchain/mame_interactive.py --step "8:dir\\n" \\
Примеры:
# запустить тест и снять экран
python3 toolchain/mame_interactive.py tests/rt_test/rt_test.exe \
--snap 12,14 --timeout 16
# набрать команду на голой командной строке
python3 toolchain/mame_interactive.py --step "8:dir\n" \
--snap 10,11 --timeout 12
Пример (запустить .exe вводом пути и дождаться вывода):
python3 toolchain/mame_interactive.py tests/rt_test/rt_test.exe \\
--step "8:a:\\rt_test.exe\\n" --snap 12,14 --timeout 16
# запустить программу и ответить на её ввод
python3 toolchain/mame_interactive.py tests/menu/menu.exe \
--step "13:2\n" --snap 15 --timeout 17
"""
import argparse
@@ -75,7 +76,7 @@ COMMON_ARGS = [
]
# --- Раскладка AT-клавиатуры :kbd:ms_naturl (tag, mask) ---
# Снято дампом ioport-полей в MAME (см. _probe_kbd.lua).
# Снято дампом ioport-полей в MAME (см. docs/mame-autotest.md).
KB = ":kbd:ms_naturl:"
PHYS = {
"a": (KB+"P1.6", 0x04), "b": (KB+"P1.5", 0x10), "c": (KB+"P1.4", 0x08),
@@ -217,46 +218,56 @@ def parse_step(s):
def main():
ap = argparse.ArgumentParser()
ap.add_argument("exe", nargs="?", help="опц. .exe на дискету A:")
ap.add_argument("exe", nargs="?", help=".exe на дискету A: (авто-запуск)")
ap.add_argument("--data", nargs="*", default=[], help="доп. файлы на дискету")
ap.add_argument("--step", action="append", type=parse_step, required=True,
help="'T:TEXT' — в момент T сек напечатать TEXT (можно несколько)")
ap.add_argument("--launch-at", type=float, default=8.0,
help="сек, когда печатается запуск exe (по умолч. 8)")
ap.add_argument("--step", action="append", default=[], type=parse_step,
help="'T:TEXT' — в момент T сек напечатать TEXT (можно много)")
ap.add_argument("--snap", default=None, help="секунды для скриншотов, через запятую")
ap.add_argument("--timeout", type=float, default=None, help="сек эмуляции до выхода")
args = ap.parse_args()
check_no_stray_mame()
events = build_events(args.step)
last_ev_t = events[-1][0] if events else max(t for t, _ in args.step)
# Собираем шаги ввода: авто-запуск exe (печать пути) + пользовательские.
steps = list(args.step)
exe_path = None
if args.exe:
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).lower()
steps.append((args.launch_at, f"a:\\{exe_name}\n"))
if not steps:
print("ОШИБКА: укажите exe для запуска или хотя бы один --step")
sys.exit(1)
events = build_events(steps)
last_ev_t = events[-1][0] if events else max(t for t, _ in steps)
if args.snap:
snap_times = [float(x) for x in args.snap.split(",")]
else:
snap_times = [last_ev_t + 1.5, last_ev_t + 3.0]
# exe: даём программе доработать (снимаем через 4 и 6 с после ввода);
# чистый ввод: сразу после последнего шага.
pad = 4.0 if exe_path else 1.5
snap_times = [last_ev_t + pad, last_ev_t + pad + 2.0]
timeout = args.timeout if args.timeout is not None else (max(snap_times) + 1.5)
gen_lua(events, snap_times, timeout)
# Дискета БЕЗ AUTORUN.BAT → приглашение C:\>
# Дискета БЕЗ авто-запускаемого bat → приглашение C:\>.
disk_files = []
if args.exe:
p = os.path.abspath(args.exe)
if not os.path.isfile(p):
print(f"ОШИБКА: не найден {p}")
sys.exit(1)
disk_files.append(p)
if exe_path:
disk_files.append(exe_path)
disk_files += [os.path.abspath(p) for p in args.data]
if os.path.exists(MC_IMG):
shutil.copy(MC_IMG, MC_IMG + ".bak")
if disk_files:
subprocess.run([sys.executable, os.path.join(MAME_DIR, "make_disk.py"), MC_IMG]
+ disk_files, check=True, cwd=MAME_DIR)
else:
# пустая дискета без AUTORUN.BAT
subprocess.run([sys.executable, os.path.join(MAME_DIR, "make_disk.py"), MC_IMG],
check=True, cwd=MAME_DIR)
make_disk_cmd = [sys.executable, os.path.join(MAME_DIR, "make_disk.py"), MC_IMG]
subprocess.run(make_disk_cmd + disk_files, check=True, cwd=MAME_DIR)
snap_sub = os.path.join(SNAP_DIR, "sprinter")
if os.path.isdir(snap_sub):
@@ -266,8 +277,8 @@ def main():
"-snapshot_directory", SNAP_DIR,
"-autoboot_script", LUA_SCRIPT,
]
print("Ввод по шагам:", [(t, repr(txt)) for t, txt in args.step])
print("Запуск:", " ".join(cmd))
print("Ввод по шагам:", [(round(t, 2), repr(txt)) for t, txt in steps])
print("Скриншоты (сек):", snap_times, " таймаут:", timeout)
result = subprocess.run(cmd, cwd=MAME_DIR, capture_output=True, text=True)
for line in result.stdout.splitlines():
if "[interactive]" in line: