diff --git a/docs/TODO.md b/docs/TODO.md index 80228bc..6cd89e9 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -100,10 +100,10 @@ Quick wins: - [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s - [ ] `restore SP on EXIT` (паттерн z88dk +pps) — проверить нужность - [x] ~~CI: MAME с -aviwrite для screenshot-сравнения без человека~~ — - `toolchain/mame_auto_test.py` (2026-07-08): AUTORUN.BAT chainload - + Lua-таймер для скриншотов/выхода; сравнение пока визуальное - (Claude читает скриншот), не автоматический diff. Подробности: - memory/mame_autotest_autorun. + `toolchain/mame_interactive.py`: авто-запуск .exe вводом с + эмуляцией клавиатуры + Lua-таймер для скриншотов/выхода; сравнение + визуальное (Claude читает скриншот), не автоматический diff. + Полный справочник: docs/mame-autotest.md. - [ ] linker duplicate-symbol warnings: сейчас фильтруются в sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…); радикально — --nostdlib с ручным списком модулей z80.lib diff --git a/docs/mame-autotest.md b/docs/mame-autotest.md new file mode 100644 index 0000000..ff30364 --- /dev/null +++ b/docs/mame-autotest.md @@ -0,0 +1,242 @@ +# Автотестирование в MAME + +Единый справочник: как запускать программы Sprinter в эмуляторе MAME +**без участия человека**, вводить команды, снимать скриншоты, завершать +сессию и анализировать результат. Если нужно что-то про автотесты в +MAME — смотреть сюда. + +Весь механизм собран в одном инструменте: **`toolchain/mame_interactive.py`**. + +--- + +## 1. TL;DR + +```bash +# собрать .exe (пример) +make -C tests/bgitest + +# запустить в MAME, снять экран, выйти по таймауту +python3 toolchain/mame_interactive.py tests/bgitest/bgitest.exe \ + --snap 12,14 --timeout 16 +``` + +Инструмент сам: +1. проверит, что нет висящих копий MAME; +2. положит `.exe` на дискету A: (`mame/v306/IMG/mc.img`); +3. запустит MAME с драйвером `sprinter`; +4. дождётся приглашения `C:\>` и **напечатает `a:\bgitest.exe` + Enter** + (эмулируя нажатия клавиш); +5. снимет скриншоты в указанные секунды эмулированного времени; +6. завершит сессию по таймауту; +7. выведет пути к PNG-скриншотам. + +Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (`0000.png`, `0001.png`, +…). Их читает Claude визуально (инструментом Read) — текст с экрана +программно НЕ распознаётся. + +--- + +## 2. Инструмент: `mame_interactive.py` + +``` +python3 toolchain/mame_interactive.py [exe] [--data f ...] \ + [--launch-at T] [--step "T:TEXT" ...] [--snap t1,t2,...] [--timeout N] +``` + +| Аргумент | Назначение | +|----------|-----------| +| `exe` | `.exe` кладётся на A: и **авто-запускается** (печатается `a:\`+Enter в момент `--launch-at`). Без `exe` работаем на голой командной строке. | +| `--data f ...` | доп. файлы на дискету A: (данные для теста). | +| `--launch-at T` | секунда, когда печатается запуск `exe` (по умолчанию **8**). | +| `--step "T:TEXT"` | в момент `T` сек напечатать `TEXT`. Можно много раз — диалог с уже запущенной программой. В `TEXT`: `\n`=Enter, `\t`=Tab; заглавные и символы через Shift — автоматически. | +| `--snap t1,t2,...` | секунды эмуляции для скриншотов. По умолчанию: `launch_at+4` и `+6` (для `exe`), либо сразу после последнего ввода. | +| `--timeout N` | секунд эмуляции до принудительного выхода. По умолчанию — чуть позже последнего скриншота. | + +**Важно:** все времена — это **секунды эмулированного времени от старта +машины** (не от нажатий, их «нет»). Загрузка DSS до `C:\>` занимает +~7 секунд, поэтому `--launch-at 8` и скриншоты с ~12 с. + +### Типовые рецепты + +```bash +# 1. Запустить тест и снять результат (самый частый случай) +python3 toolchain/mame_interactive.py tests/rt_test/rt_test.exe \ + --snap 12,14 --timeout 16 + +# 2. Набрать команду на голой командной строке (без exe) +python3 toolchain/mame_interactive.py --step "8:dir\n" \ + --snap 10,11 --timeout 12 + +# 3. Запустить программу и ответить на её ввод (например, выбор пункта меню) +python3 toolchain/mame_interactive.py tests/menu/menu.exe \ + --step "13:2\n" --snap 15 --timeout 17 + +# 4. Тест с файлом-данными на дискете +python3 toolchain/mame_interactive.py examples/mdview2/mdview2.exe \ + --data doc.md --step "13:mdview2 doc.md\n" --snap 16 --timeout 18 +``` + +--- + +## 3. Как это работает внутри + +### 3.1 Запуск MAME + +Нативный `mame.arm` (arm64) в `mame/v306/`, драйвер `sprinter`, BIOS +v3.06. Полный набор аргументов зашит в `COMMON_ARGS` внутри скрипта: +две дискеты (A: наш `mc.img`, B: DSS 1.71u), два HDD-образа (система + +медиа), CD-ROM, ZX-Bus карта NeoGS, `-video opengl -window`, +`-snapshot_directory`, `-autoboot_script <сгенерированный .lua>`. + +### 3.2 Ввод с клавиатуры — ключевой момент + +У Sprinter в MAME **две** клавиатуры: + +- `IO_LINE0..7` — легаси ZX-Spectrum-матрица (порт `0xFE`). DSS её для + командной строки **НЕ читает**. +- `root:kbd:ms_naturl` — **настоящая AT/PS-2 клавиатура**, подключённая + последовательно к SIO Z84C015 (`sprinter.cpp:2037`). Именно её DSS + читает как поток scancode'ов. + +Поэтому **не работают** (проверено многократно): `natkeyboard:post`, +`-autoboot_command`, а также `set_value` по полям `:IO_LINE*`. Всё это +бьёт в ZX-матрицу, которую DSS игнорирует. + +**Работает** — прямое управление полями AT-клавиатуры из Lua: +```lua +manager.machine.ioport.ports[":kbd:ms_naturl:P1.4"].fields["D"]:set_value(1) -- нажать +... подождать ~0.06с ... +manager.machine.ioport.ports[":kbd:ms_naturl:P1.4"].fields["D"]:set_value(0) -- отпустить +``` +`at_keyboard` сам сгенерит make/break scancode'ы → SIO → DSS. + +Скрипт хранит раскладку `char → (порт, битовая маска)` (словарь `PHYS` + +`SHIFTED` для Shift-символов) и разворачивает строку в список +timed-событий `(время, порт, маска, значение)`. Backslash `\` в +AT-клавиатуре есть (поле `P2.1`/0x4) — путь `a:\name.exe` вводится +полностью. + +### 3.3 Тайминг (Lua) + +Генерируется `_interactive_gen.lua`, который через +`emu.register_periodic` на каждом кадре сверяет **эмулированное время** и +проигрывает события ввода, снимает скриншоты и завершает сессию. + +Время берётся как `t.seconds + t.attoseconds/1e18`, потому что +`attotime.seconds` — **целое** (дробную часть отбрасывает); если считать +по нему, все события схлопнутся в 1-секундную сетку. + +Старт отсчёта — `emu.add_machine_reset_notifier` (НЕ `emu.register_start` +— он deprecated). + +### 3.4 Скриншоты + +`manager.machine.video:snapshot()` пишет PNG в каталог из +`-snapshot_directory` (`mame/v306/snap_auto/sprinter/`). Скрипт перед +запуском чистит этот каталог, потом печатает пути к готовым файлам. +Claude открывает их инструментом Read (визуальный анализ). + +### 3.5 Завершение сессии + +Два рубежа, чтобы MAME гарантированно не «завис»: +- в Lua: при `elapsed >= timeout` → `manager.machine:exit()` (чистый + выход); +- снаружи: shell-обёртка `timeout ./mame.arm …` как страховка. + +--- + +## 4. Предпосылки (окружение) + +- **MAME**: `mame/v306/mame.arm` + образы в `mame/v306/IMG/` + (`mc.img` — наш перезаписываемый A:, `dss171u.img`, HDD `.chd`, CD + `.iso`). Каталог `mame/` целиком в `.gitignore` — поэтому инструмент + живёт в `toolchain/`, а не рядом с MAME. +- **Загрузка должна доходить до `C:\>`.** `system.bat` на системном + диске НЕ должен автоматически запускать Flex Navigator или приложение — + иначе мы не попадём на командную строку и ввод уйдёт в чужую программу. + (Это файл на HDD-образе, вне репозитория; правится один раз.) +- **Нет висящих копий MAME.** Несколько одновременных инстансов пишут в + один `mc.img` и дают недостоверный результат. Скрипт проверяет это сам + (`pgrep`), но при ручных запусках MAME — проверяйте `ps aux | grep mame`. + +--- + +## 5. Как выбирать времена + +- **Загрузка до `C:\>`:** ~7 секунд → `--launch-at 8` безопасно. +- **Набор пути `a:\name.exe`:** ~13 символов × 0.14с ≈ 1.8с → команда + уходит около 9.8с, программа стартует ~10с. +- **Скриншот:** давайте программе дорисоваться. Быстрая программа — + снимать с ~12с; если рисует долго/по частям, снимайте несколько кадров + (`--snap 12,16,20`) и смотрите, где картинка «дособралась». +- **Диалог с программой (`--step`):** времена шагов ставьте ПОСЛЕ старта + программы (например, запуск на 8с, ответ на ввод на 13–15с). + +--- + +## 6. Анализ результата + +- Скриншоты — **единственный** способ проверки: программного чтения + текстового/графического VRAM нет, OCR нет. Claude читает PNG глазами + (инструмент Read с картинкой). +- Лог MAME фильтруется по строкам `[interactive]` (моменты снимков и + выхода) — видно, в какие секунды сделаны кадры. +- Если картинка «не дособралась» — снять более поздний кадр (увеличить + `--snap`/`--timeout`). + +--- + +## 7. Раскладка клавиатуры (справочно) + +Раскладка снята дампом ioport-полей `:kbd:ms_naturl:*` живой машины. +Она зашита в `PHYS`/`SHIFTED` внутри `mame_interactive.py`. Поддержаны: +буквы (a–z, A–Z через Shift), цифры, пробел, Enter (`\n`), Tab (`\t`), +и символы ``- = [ ] \ ; ' , . / ` `` плюс их Shift-версии +`! @ # $ % ^ & * ( ) _ + { } | : " < > ? ~`. + +Если понадобится клавиша вне списка — снять её поле дампом (пример +Lua-пробы ниже) и добавить в `PHYS`: + +```lua +-- дамп всех полей клавиатуры в лог +for tag, port in pairs(manager.machine.ioport.ports) do + if tostring(tag):find("kbd") then + for fname, field in pairs(port.fields) do + print(string.format("%s mask=0x%x %q", tag, field.mask, fname)) + end + end +end +``` + +--- + +## 8. Квирки и грабли (все, на которые уже наступали) + +- **`attotime.seconds` — целое.** Субсекундный тайминг только через + `seconds + attoseconds/1e18`. +- **Автоповтор (typematic).** Клавишу держать коротко (~0.06с). Если + держать ~1с — `d` превратится в `dddddd`. +- **Слипание scancode'ов.** Между символами ~0.14с. +- **Не та клавиатура.** Ввод — только в `:kbd:ms_naturl`, НЕ в + `:IO_LINE*`, НЕ через `natkeyboard`/`-autoboot_command`. +- **Загрузка мимо `C:\>`.** Если `system.bat` что-то автозапускает — + ввод уходит в чужую программу; вернуть чистую командную строку. +- **Висящие копии MAME.** Всегда проверять перед запуском. +- **macOS-специфика (справочно):** известный баг MAME + (mamedev/mame#10612 — потеря ввода в fullscreen при движении мыши на + старте) к нам НЕ относится: работаем в `-window`, ввод скриптовый. + +--- + +## 9. На будущее (заметки, ещё не в инструменте) + +- **Быстрый накопитель.** Тестам, которым важна скорость диска (напр. + потоковое чтение), имеет смысл копировать файлы с медленной дискеты A: + на HDD `C:\TEMP` перед запуском. +- **Видео+звук.** MAME умеет писать AVI (`-aviwrite`) — для тестов с + анимацией/звуком, где скриншотов мало. Пока не подключено к скрипту. +- **Ручная отладка ввода.** Запуск MAME с `-console` даёт интерактивный + Lua-REPL — удобно нащупывать поля/тайминги вживую перед скриптованием. +- **Второй видеорежим/варианты BIOS** — при необходимости менять + `COMMON_ARGS`. +``` diff --git a/toolchain/mame_auto_test.py b/toolchain/mame_auto_test.py deleted file mode 100644 index 1db8a0c..0000000 --- a/toolchain/mame_auto_test.py +++ /dev/null @@ -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 [--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() diff --git a/toolchain/mame_interactive.py b/toolchain/mame_interactive.py index ed35ad7..b42067d 100644 --- a/toolchain/mame_interactive.py +++ b/toolchain/mame_interactive.py @@ -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` выполняется. +Инструмент запускает 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:\ + 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: