Regression workflow

The Test Ladder

Every compatibility fix should climb from a small DOS repro to the broader runs it can affect. The ladder keeps LainDOS caller-driven instead of collecting untested DOS stubs.

How to choose the next test

Begin with the narrowest proof that would have failed before the change. If the fix touches a DOS APIDOS APIThe program-facing DOS service contract, mostly reached through INT 21h in LainDOS., write a tiny program. If it touches shell, loader, or FAT state, add a scenario runner. If the bug only appears in a game, keep the media local and add a smoke that proves the visible path.

Layers

1. Tiny DOS programs
One real-mode program proves one API surface.

These live under tests/programs/. They set up segment registers, call INT 21hINT 21hThe main DOS API interrupt. Programs select services such as open, read, EXEC, and exit with AH. or a narrow hardware path, print PASS:/FAIL: markers, and exit with AH=4Ch so QEMUQEMUThe default fast emulator for automated LainDOS builds, tests, monitor probes, and game smoke runs. serial output can be checked automatically.

tests/programs/irqmask.asmtests/programs/execparam.asmtests/programs/findnext.asm
2. Python/QEMU runners
A host script builds a fresh image and checks serial output.

Most scripts use build_nasmNASMNetwide Assembler, the assembler used for LainDOS boot, kernel, shell, and focused test programs._test_image, run_serial_image, and check_markers from scripts/testlib.py. The image is disposable and always contains the current boot sector and kernel.

scripts/test_irqmask.pyscripts/test_execparam.pyscripts/run_tests.py
3. Shell, loader, and filesystem scenarios
Broader tests combine APIs and inspect disk state.

These cover command dispatch, EXEC parent/child restoration, writable FAT behavior, directory mutation, rollback, and persistent image contents after QEMUQEMUThe default fast emulator for automated LainDOS builds, tests, monitor probes, and game smoke runs. exits.

scripts/test_shell.pyscripts/test_savewrite.pyscripts/test_dirmut.py
4. Game smoke tests
Real games prove the integrated path reaches visible gameplay.

Game smokessmoke testA coarse end-to-end run that proves a game or workflow reaches a visible expected state without fatal markers. build generated images from local media, drive QEMUQEMUThe default fast emulator for automated LainDOS builds, tests, monitor probes, and game smoke runs. through monitor socketsmonitor socketA QEMU control channel used to send keys, capture screenshots, inspect registers, or quit scripted runs., keep host audio silent, and use serial plus framebufferframebufferThe raw pixels currently shown by VGA; smoke tests hash or inspect it when serial output cannot prove gameplay. checks to catch crashes and blank screens.

scripts/test_shell_monkey.pyscripts/test_wolf3d_smoke.pyscripts/test_shortline_smoke.py

Focused test template

The important convention is not the exact code shape; it is the serial-visible contract: one unique PASS: marker, useful FAIL: markers, and a DOS exit code.

tests/programs/example.asmNASM · 16-bit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[bits 16]
[org 0x0100]
 
start:
push cs
pop ds
 
; Call the API under test here.
 
mov dx, pass_msg
mov ah, 0x09
int 0x21
mov ax, 0x4C00
int 0x21
 
fail:
mov dx, fail_msg
mov ah, 0x09
int 0x21
mov ax, 0x4C01
int 0x21
 
pass_msg: db "PASS: EXAMPLE", 13, 10, "$"
fail_msg: db "FAIL: EXAMPLE", 13, 10, "$"
scripts/test_example.pyPython
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
import os
import sys
from testlib import build_dir, build_nasm_test_image, check_markers, run_serial_image
 
BUILDDIR = build_dir()
IMG = os.path.join(BUILDDIR, "example.img")
KERNEL = os.path.join(BUILDDIR, "example_kernel.bin")
 
def main():
build_nasm_test_image(BUILDDIR, IMG, KERNEL, "EXAMPLE COM", "tests/programs/example.asm", "example.com")
output = run_serial_image(IMG, timeout=10)
if not check_markers(output, required=("PASS: EXAMPLE", "Program exited, code=00", "HALT")):
sys.exit(1)
print("\nExample test passed.")
 
if __name__ == "__main__":
main()

Game smoke rules

Game smokes are integration tests, not media archival. Keep proprietary input ignored, build disposable images under build/, use QEMUQEMUThe default fast emulator for automated LainDOS builds, tests, monitor probes, and game smoke runs. monitor input for deterministic startup, and prefer framebufferframebufferThe raw pixels currently shown by VGA; smoke tests hash or inspect it when serial output cannot prove gameplay. checks when the serial log cannot prove that graphics reached gameplay.

checkUse qemuQEMUThe default fast emulator for automated LainDOS builds, tests, monitor probes, and game smoke runs._sb16SB16Sound Blaster 16 audio hardware. QEMU exposes the DSP path with -device sb16; some games also need the separate -device adlib OPL path._silent_args when a game expects SB16SB16Sound Blaster 16 audio hardware. QEMU exposes the DSP path with -device sb16; some games also need the separate -device adlib OPL path., or qemuQEMUThe default fast emulator for automated LainDOS builds, tests, monitor probes, and game smoke runs._sb16SB16Sound Blaster 16 audio hardware. QEMU exposes the DSP path with -device sb16; some games also need the separate -device adlib OPL path._adlibAdLibThe classic Yamaha OPL FM-synthesis sound path. QEMU exposes it with -device adlib, separately from -device sb16; attach it only for games that need that probe path._silent_args for Wolf3D-style OPLAdLibThe classic Yamaha OPL FM-synthesis sound path. QEMU exposes it with -device adlib, separately from -device sb16; attach it only for games that need that probe path./AdLibAdLibThe classic Yamaha OPL FM-synthesis sound path. QEMU exposes it with -device adlib, separately from -device sb16; attach it only for games that need that probe path. probes.
checkReject EXC, unhandled INT 21hINT 21hThe main DOS API interrupt. Programs select services such as open, read, EXEC, and exit with AH., FAIL:, and known game-level fatal markers.
checkKeep special pacing, such as Shortline's -icount run, in a dedicated Makefile target.
checkRecord non-trivial triage in docs/debug_log.md before changing approach.