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