288 lines
18 KiB
Markdown
288 lines
18 KiB
Markdown
# Автотестирование в 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 \
|
||
--mame-home /путь/к/MAME/runtime --snap 12,14 --timeout 16
|
||
```
|
||
|
||
Инструмент сам:
|
||
1. соберёт отдельную временную FAT12-дискету с `.exe`;
|
||
2. создаст отдельные каталоги MAME для cfg/NVRAM/diff/snapshot;
|
||
3. запустит MAME с драйвером `sprinter`;
|
||
4. в момент `--launch-at` **напечатает `a:\bgitest.exe` + Enter**
|
||
(эмулируя нажатия клавиш); выбирайте время после загрузки DSS;
|
||
5. снимет скриншоты в указанные секунды эмулированного времени;
|
||
6. завершит сессию по таймауту;
|
||
7. выведет пути к PNG-скриншотам.
|
||
|
||
Скриншоты лежат в `build/mame-autotest/<session>/sprinter/` (`0000.png`,
|
||
`0001.png`, …) относительно каталога запуска. Их можно читать визуально — текст с экрана
|
||
программно НЕ распознаётся.
|
||
|
||
---
|
||
|
||
## 2. Инструмент: `mame_interactive.py`
|
||
|
||
```
|
||
python3 toolchain/mame_interactive.py [exe] [--data f ...] \
|
||
[--mame-home DIR] [--mame-bin FILE] [--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` | секунд эмуляции до принудительного выхода. По умолчанию — чуть позже последнего скриншота. |
|
||
| `--mame-home DIR` | установленная среда MAME; можно задать переменной `MAME_HOME`. |
|
||
| `--mame-bin`, `--mame-rompath`, `--mame-dss-image`, `--mame-system-hdd-image`, `--mame-bios` | выбор отдельных частей профиля; соответствуют `MAME_*` из окружения. |
|
||
| `--snapshot-dir DIR` | локальный каталог кадров вместо `build/mame-autotest`. |
|
||
|
||
**Важно:** все времена — это **секунды эмулированного времени от старта
|
||
машины** (не от нажатий, их «нет»). Загрузка 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_HOME` выбирает `sprinter` (новый MAME.HT) либо `mame.arm`
|
||
(прежняя установка), `roms/`, DSS-дискету и системный CHD. `MAME_BIN` явно
|
||
выбирает бинарник независимо от каталога ресурсов. A: — временная дискета
|
||
этого запуска; B: — DSS. При наличии в
|
||
установленной среде CD/NeoGS/медиа скрипт подключает их как необязательные
|
||
устройства. `-cfg_directory`, `-nvram_directory`, `-diff_directory` и
|
||
`-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)
|
||
|
||
Временный 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` внутри `build/mame-autotest/<session>/sprinter/`.
|
||
Каждый запуск создаёт свой каталог и печатает пути к PNG; другие сессии не
|
||
затрагиваются.
|
||
|
||
### 3.5 Завершение сессии
|
||
|
||
Два рубежа, чтобы MAME гарантированно завершился:
|
||
- в Lua: при `elapsed >= timeout` → `manager.machine:exit()` (чистый
|
||
выход);
|
||
- снаружи: `subprocess` ожидает с ограничением wall-clock времени, затем
|
||
завершает только собственный процесс MAME.
|
||
|
||
---
|
||
|
||
## 4. Предпосылки (окружение)
|
||
|
||
- **MAME**: задайте `MAME_HOME=/путь/к/MAME/runtime` с бинарником,
|
||
`roms/`, `IMG/dss171u.img` и `IMG/sp_hdd_sys.chd`. Исходники fork не нужны.
|
||
- **Загрузка должна доходить до `C:\>`.** `system.bat` на системном
|
||
диске НЕ должен автоматически запускать Flex Navigator или приложение —
|
||
иначе мы не попадём на командную строку и ввод уйдёт в чужую программу.
|
||
(Это файл на HDD-образе, вне репозитория; правится один раз.)
|
||
- **Одновременный запуск нескольких MAME не проверялся.** Автотест создаёт
|
||
отдельные A:, state и снимки и при необходимости копирует системный CHD,
|
||
но это ещё не доказывает корректность параллельной работы. Особенно не
|
||
гарантируется работа нескольких процессов через MCP bridge: выбор процесса,
|
||
идентификатор сессии и арбитраж требуют отдельной проверки.
|
||
|
||
---
|
||
|
||
## 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** — выбирать `MAME_BIOS` или отдельные
|
||
аргументы профиля.
|
||
|
||
## 10. Архивные тайминги старого MCP-моста
|
||
|
||
Ниже сохранены измерения прежнего ручного запуска через `run_bridge.sh` и
|
||
`bridge_cmd.sh`. Они не задают таймауты нового автотеста и DAP: автотест
|
||
использует изолированные носители, а DAP ждёт стабильный prompt DSS в VRAM.
|
||
|
||
| шаг | пауза ПОСЛЕ шага |
|
||
|-----|------------------|
|
||
| запустили `run_bridge.sh` | **6 с** → машина поднялась, можно слать `go` |
|
||
| `go` (снять с дебаггерного стопа) | **8 с** → DSS догрузился, принимает ввод |
|
||
| `keyseq d:{ENTER}` + `keyseq roomtest{ENTER}` | **5 с** → программа уже стартовала |
|
||
|
||
То есть весь цикл «перезапуск + старт теста» укладывается в ~20 секунд.
|
||
|
||
Пересобрал HDD-образ (`make hdd`) → MAME **обязан** полный рестарт
|
||
(`chdman -f` даёт новый inode; см. memory `mame_hdd_rebuild_restart`):
|
||
остановка через `exit` в дебаггере, затем `run_bridge.sh` заново.
|
||
|
||
## 11. Карта C/asm для отладки
|
||
|
||
`SRC_DEBUG=1` в app.mk создаёт проверенный пакет исходников и адресов без
|
||
изменения EXE в проверенных конфигурациях. Пер-модульный вариант:
|
||
`SRC_DEBUG_FILES=helper.c`. Команды карты и результаты живых экспериментов:
|
||
[mame-source-debug-status.md](mame-source-debug-status.md); полный план:
|
||
[mame-source-debug.md](mame-source-debug.md). Базовый C-attach/where/точки и
|
||
чтение простых переменных доступны через `toolchain/sdbg_session.py`.
|
||
`sdbg_server.py`, DAP MVP, VS Code launch и development-расширение уже есть;
|
||
безопасный restart и MCP C-уровня пока не готовы. `-debugger none`
|
||
автоматически продолжает CPU и не подходит для ожидания команд пошаговой
|
||
сессии.
|
||
|
||
Source-debug launcher ждёт до 10-й секунды эмулируемого времени, сохраняет
|
||
снимок готового DSS, только затем вооружает entry breakpoint и вводит команду
|
||
EXE. Пользовательские точки создаются после проверенного `main`, когда
|
||
`_bank_pages` уже заполнена. В обычной конфигурации это даёт 0 попаданий на
|
||
адрес entry во время загрузки DSS. Автоматического распознавания приглашения
|
||
`C:\\>` пока нет; `launchAt` в VS Code можно увеличить.
|