Skip to content

Amiga port testing

Simon Dick edited this page Jun 20, 2026 · 24 revisions

MicroPython AmigaOS Port — Testing Guide

Three paths to test the port, each suited to a different stage of work:

Method When
vamos (host-side, headless) Day-to-day iteration; widest automated test coverage
Amiberry (full emulation) Real-AmigaOS behaviour — Workbench launch, icon.library, 68881 FPU, persistent history, anything serial.device-shaped
CI (cross-compile only) Confirming a branch builds in the canonical container; produces release-style artefacts

For day-to-day work, vamos. For anything that needs Kickstart, an emulator. CI gives you the same binary you'd ship.


1. Vamos (recommended for iteration)

vamos is the userspace 68k/AmigaOS emulator from the amitools package. It boots in milliseconds, has no GUI, and integrates with the standard tests/run-tests.py harness. Setup here assumes vamos is installed at ~/vamos/ and activated via pipenv.

Direct invocation

cd ~/vamos
pipenv run vamos --cpu 68020 \
    -V "mp:/path/to/micropython/tests" \
    --cwd mp:basics \
    -- /path/to/build/micropython string1.py
  • --cpu 68020 is required — vamos defaults to 68000, which faults on m68020 instructions.
  • -V name:/host/path mounts a host directory as an AmigaOS volume.
  • --cwd sets cwd; with basename invocation, sys.argv[0] / __file__ match host CPython exactly.
  • -- separates vamos options from the binary's args.

Driving run-tests.py via tools/amiga-vamos-run.sh

tools/amiga-vamos-run.sh is a MICROPY_MICROPYTHON wrapper. It mounts tests/ as the internal mp: volume, points vamos's cwd at the test's directory, replaces the script argument with its basename, uses a private --vols-base-dir so parallel workers don't collide on the auto RAM: volume, and -q so vamos logs don't pollute captured stdout.

export MICROPY_MICROPYTHON="$(pwd)/tools/amiga-vamos-run.sh"
cd tests
./run-tests.py -d basics float io micropython misc

Exclude directories that can't run on Amiga (no socket-server harness under vamos, no asyncio in the port, no inline asm for non-68k targets, etc.):

./run-tests.py -d basics float io micropython misc \
    -e "inlineasm|machine_|thread|extmod/ussl|extmod/uasync"

Variant selection

AMIGA_VARIANT=standard|68040 picks build + matching --cpu flag. Default is standard. (68020fpu builds 68881 instructions that vamos can't emulate, so it's Amiberry-only.)

AMIGA_VARIANT=68040 ./run-tests.py basics/string1.py

68020fpu cannot run under vamos — it builds 68881 instructions and vamos has no 68881/68882 emulation. The wrapper detects this and exits with an explanation; test that variant under Amiberry or real hardware.

Test-runner integration requirements

For run-tests.py to work against the Amiga binary, the port must:

  • Accept -X <option> flags as no-ops (run-tests.py always emits -X emit=bytecode; macOS adds -X realtime).
  • Return POSIX-style exit codes (MICROPY_PYEXEC_ENABLE_EXIT_CODE_HANDLING (1)).
  • Free AllocVec'd argv buffers before returning — vamos's orphan-memory check makes the process exit non-zero otherwise.

All three are already in place.

Known vamos quirks

  • Bebbo argv parser is broken under vamos for multi-arg invocations; amiga_parse_args parses pr_Arguments itself.
  • WaitForChar returns 0 immediately under vamos — the port uses plain FGetC for console reads.
  • SetMode(fh, 1) is a no-op under vamos (it already delivers one char at a time).
  • No 68881 emulation — see variant selection above.

REPL under vamos: tools/amiga-vamos-repl.sh

Vamos doesn't translate SetMode(stdin, 1) to tcsetattr on the host TTY, so an interactive REPL launched naked sees the host shell's cooked mode (cursor keys echoed as ^[[D, bytes only delivered on newline). tools/amiga-vamos-repl.sh flips the host TTY into raw / no-echo / no-isig before launching vamos and restores the original mode on exit.

tools/amiga-vamos-repl.sh                       # standard variant
AMIGA_VARIANT=68040 tools/amiga-vamos-repl.sh

Pipe input is unaffected; only interactive sessions need the wrapper.

Suite snapshot — 2026-06-02

Run via tools/amiga-vamos-run.sh against the standard variant:

Directory Files Pass Self-skip Fail Notes
basics/ 574 490 83 1 struct1.py (bebbo ABI alignment)
float/ 68 54 11 3 EXACT-mode precision at double-range edges
io/ 16 15 1 0 argv.py now passes
import/ 30 29 0 1 import_file.py (vamos host-path rewriting)
micropython/ 108 70 18 20 Remaining native_* / viper_* -- Phase 12 (NLR + viper register / pointer gaps); the bulk of the native_* set started passing after the asm68k MOVEQ fix
extmod/ 192 76 102 13 vamos socket / select / time-quantum / vfs_userfs gaps; one known-flaky (time_time_ns.py) ignored
misc/ 14 6 8 0 Skips: settrace, sys_exc_info, cexample
cmdline/ 27 9 2 14 Unix-port-specific (REPL banner, -v, terminal editing); not re-run this snapshot -- vamos's stdin/stdout fan-out across run-tests.py worker fork hangs on the REPL tests, so the row carries the previous (2026-05-12) numbers verbatim
stress/ 13 12 0 1 bytecode_limit.py parser memory pressure

Aggregate: 761 pass / 225 self-skip / 53 fail out of 1042 files. Excluding Phase-12 native/viper failures, Unix-port-specific cmdline tests, vamos emulation gaps, and the bytecode_limit.py parser edge case, 4 individual tests fail -- the same real platform differences as in the 2026-05-12 snapshot (m68k struct alignment, long-double EXACT-mode precision, vamos host-path rewriting).

Notable movement vs the 2026-05-12 snapshot:

  • micropython/: +27 pass / -27 fail. The asm_68k_moveq destination-register fix (commit fb1aeab92) unblocked every native_* test that wasn't dependent on the still-missing 68k NLR story. The remaining 20 failures all hit known Phase 12 limits: try/except / with in native (needs nlr68k.S), viper's one-register local cap, and viper pointer ops.
  • io/argv.py: now passes (was a vamos host-path rewriting failure); the path rewriting in tools/amiga-vamos-run.sh caught up.
  • extmod/: net file count dropped 205 → 192 (13 tests removed upstream); on the larger active set, pass count rose 67 → 76 as more surfaces became reachable after Phase 39's hashlib / deflate / btree additions.

Re-run + Amiberry verification — 2026-06-20

vamos re-run (standard, dirs basics float io import micropython extmod misc stress, same exclusions): 752 pass / 40 fail / 223 self-skip, no regressions — every failure is a previously-documented category.

For the first time the failing extmod/ surfaces were also run on real Amiberry via the new run-tests.py -t serial path (above). This splits the vamos "re-run under Amiberry" TODO bucket into real passes vs genuine gaps:

Test vamos Amiberry Verdict
extmod/socket_tcp_basic.py FAIL PASS vamos bsdsocket stub only
extmod/socket_badconstructor.py FAIL PASS vamos bsdsocket stub only
extmod/socket_fileno.py FAIL PASS vamos bsdsocket stub only
extmod/time_ms_us.py FAIL PASS vamos ~10 ms clock quantum
extmod/time_time_ns.py FAIL PASS vamos clock quantum (also flaky-tagged)
extmod/ssl_sslcontext.py FAIL PASS needed real AmiSSL
extmod/ssl_sslcontext_verify_mode.py FAIL PASS needed real AmiSSL
extmod/select_poll_basic.py FAIL FAIL genuine: no select()/poll() on the port
extmod/time_res.py FAIL FAIL→fixed time.time() froze: a port bug left the 68881 FPCR rounding-precision at single, so mathieeedoubbas's FPU path truncated every double. Fixed by resetting FPCR at startup; passes with the FPU enabled. See note below.
extmod/ssl_basic.py FAIL FAIL genuine: AmiSSL API gap
extmod/ssl_ioctl.py FAIL FAIL genuine: AmiSSL API gap
extmod/ssl_keycert.py FAIL FAIL genuine: AmiSSL API gap
import/import_file.py FAIL FAIL→fixed absolute __file__; fixed by the sys.path[0] change below (vamos + on-device now pass; -t still ImportError, a harness limit)

So 7 of the 13 vamos failures are emulator artifacts, not port bugs; import_file.py and time_res.py were both then fixed outright (see below). The real-hardware failure set is now: 4 platform-diffs + 20 Phase-12 native/viper + select_poll_basic + 3 AmiSSL (ssl_basic/ssl_ioctl/ssl_keycert) + vfs_userfs + platform_basic

  • stress/bytecode_limit.

time_res.py was a port bug, fixed 2026-06-20 (FPCR rounding precision). It failed because time.time() was frozen: with an FPU present, every double (epoch seconds ~1.78e9, 0.1+0.2, …) was truncated to single (~24-bit) precision, so consecutive seconds collapsed to the same value. The loss tracked FPU presence (byte-identical across host-64-bit, host-80-bit and softfloat-80-bit — the FPU-mode selector made no difference) and vanished with the FPU disabled, which initially looked like an Amiberry emulation artifact. It was not.

Root cause (found by bisect): the bebbo C startup leaves the 68881/68882 FPCR rounding-precision field set to single (0x40) on entry to the binary — a bare C program built with the same toolchain gets double (0x90). MicroPython emits no FPU control writes (all double math is soft-float library calls), but LIBS:mathieeedoubbas.library is a single on-disk binary that auto-selects software vs direct-FPU math by FPU presence, and on its direct-FPU path it honours the inherited FPCR. With rounding stuck at single, every result rounded to ~24 bits. The bisect ruled out -lm and every library MicroPython opens: FPCR read 0x40 before main and before the earliest constructor, so it was the startup, not any OpenLibrary. (Diagnostic trick: inline fmove.l %fpcr,%0 reads FPCR even in a -msoft-float build — the assembler accepts FPU opcodes regardless of codegen flags.)

Fix: amiga_fpu_fix_rounding() in ports/amiga/main.c, called first thing in main(), sets the rounding-precision field to double ((fpcr & ~0xC0) | 0x80) via inline fmove.l. Guarded by SysBase->AttnFlags & (AFF_68881|AFF_68882|AFF_FPU40) because the soft-float standard build also targets FPU-less 68020s, where fmove would trap. Verified on an FPU-enabled Amiberry: 0.1+0.2 == 0.30000000000000004, consecutive large integers stay distinct, time.time() advances, and time_res passes — no more need for a no-FPU config. (The residual ~1-2 ulp imprecision on transcendentals / repr(pi) digit count is the separate, pre-existing bebbo soft-float / dtoa tax that also causes float_format_accuracy / float_parse; present under vamos too.)

import_file.py fix (2026-06-20). Triage showed two layers: (1) over run-tests.py -t, ImportError because the board harness ships no sibling modules — inherent, not a port bug; (2) when the sibling is reachable, the import worked but import1b.__file__ came back absolute (Python:tests/import/import1b.py) vs the expected relative import1b.py. Layer (2) was a real port deviation: for a basename script main.c set sys.path[0] to the absolute cwd (NameFromLock) instead of "". Setting it to "" (the MicroPython convention, resolved relative to cwd by VfsAmiga) matches CPython/unix; import_file.py now passes under vamos and the on-device runner, all 30 import/ tests pass, sibling imports still work. Committed ports/amiga: Keep sys.path[0] relative for a basename script.

Known failures that are not port bugs

Four genuine platform differences — present whether tested under vamos, Amiberry, or real hardware. Both sides are correct; the test's expectations match CPython on x86 rather than the AmigaOS m68k ABI:

Test Cause
basics/struct1.py struct.calcsize("97sI") == 102, test expects 104. bebbo gcc on m68k uses 2-byte int alignment per the AmigaOS m68k ABI; CPython on x86 uses 4. Both platform-correct.
float/float_parse*.py Very long mantissa with very negative exponent; 1e+300 vs 9.999…e+299 differing by 2 ULP; 1e4294967301 not detected as overflow — 1–2 ULP off. Bebbo's 80-bit long-double soft-float loses just enough precision that EXACT-mode parsing can't always nail the closest double.
float/float_format_accuracy.py repr round-trip rate ~72 % vs ≥ 99.7 % expected. Same long-double precision tax.

Known failures pending port work (TODO)

Tests below fail on the current amiga-port head. None are regressions; each is a documented gap waiting on a specific piece of port work. Originally captured 2026-06-13 against commit 98a3a6e4c (standard under vamos); updated 2026-06-20 with real-Amiberry verification of the socket / select / time / ssl bucket. After verification the genuine real-hardware gaps are: 6 native, 14 viper, 3 ssl, 1 select, 1 vfs, 1 platform = 24 entries (the 3 socket tests and 2 sslcontext tests turned out to be emulator artifacts that pass on a correctly-configured target, and 2 of the time tests were vamos clock-quantum artifacts; import_file.py and time_res.py were both fixed — see the snapshot section above).

Test(s) Cause TODO
micropython/native_closure.py, native_const.py, native_gen.py, native_try.py, native_try_deep.py, native_with.py Phase 12 native emitter gap: @micropython.native needs the 68k NLR for try/except/with/closure scope-chain unwind. Land ports/amiga/nlr68k.S saving D2–D7/A2–A5 in nlr_buf_t; wire it through MICROPY_NLR_*. Tracked in the design log's "Other known limitations" table.
micropython/viper_binop_arith.py, viper_binop_bitwise_uint.py, viper_globals.py, viper_misc2.py, viper_ptr16_load.py, viper_ptr16_load_boundary.py, viper_ptr16_store_boundary.py, viper_ptr32_load.py, viper_ptr32_load_boundary.py, viper_ptr32_store.py, viper_ptr32_store_boundary.py, viper_subscr_multi.py, viper_try.py, viper_with.py Phase 12 viper limits: register allocator caps at 1 local (D7) — many tests need ≥2 simultaneously live locals — and 16/32-bit pointer load/store opcodes aren't emitted on m68k. Two strands. (a) Extend asm68k.h / emitnative.c so viper can spill across D2–D7; (b) add asm_68k_loadN / asm_68k_storeN covering byte/word/long pointer ops. The try/with subset additionally needs the NLR above.
extmod/ssl_basic.py, ssl_ioctl.py, ssl_keycert.py AmiSSL ships a subset of the OpenSSL API surface. (ssl_sslcontext.py and ssl_sslcontext_verify_mode.py were in this bucket but pass on Amiberry as of 2026-06-20 — they only failed because vamos has no AmiSSL.) Remaining suspects: certificate-format combinations and ioctl flag mappings. Triage each .py.out vs .py.exp in tests/results/ to attribute the divergence to either a real AmiSSL API gap (file an issue upstream / wrap with stubs) or a port-side wrapper bug in ports/amiga/modssl.c.
extmod/socket_tcp_basic.py, socket_badconstructor.py, socket_fileno.py vamos's bsdsocket.library stub is incomplete. Resolved 2026-06-20: all three PASS on Amiberry against a real bsdsocket.library. vamos-only failures; no port work needed.
extmod/select_poll_basic.py The port doesn't expose select() / poll() over bsdsocket. Confirmed 2026-06-20: still FAILS on Amiberry → genuine gap (not just a vamos limitation). Real TODO if select/poll support is wanted.
extmod/time_res.py time.time() was frozen. Fixed 2026-06-20 — port bug (FPCR rounding precision). The bebbo C startup left the 68881 FPCR rounding-precision field at single, so mathieeedoubbas's direct-FPU path truncated every double, collapsing time.time(). Fixed by resetting FPCR to double precision at startup (amiga_fpu_fix_rounding() in main.c, guarded by AttnFlags); passes with the FPU enabled. time_ms_us.py/time_time_ns.py also pass on Amiberry (they only failed on vamos's ~10 ms clock quantum).
import/import_file.py FIXED 2026-06-20 (vamos + on-device). The absolute-__file__ half is resolved: main.c now leaves sys.path[0] as "" for a basename script instead of the absolute NameFromLock cwd, so import1b.__file__ is the relative import1b.py matching CPython/unix. All 30 import/ tests pass under vamos. Over run-tests.py -t the test still raises ImportError — the board harness ships no sibling modules (test source sent plain in emit=bytecode), so import import1b isn't on the target's sys.path. Generic board limitation, not a port bug; nothing further to do unless -t gains sibling-shipping.
extmod/vfs_userfs.py The test mounts a Python-defined VFS at /, which collides with the port's built-in VfsAmiga already mounted at / by main.c. Either skip this test for the Amiga port, or rework vfs_amiga.c to permit a runtime unmount of the default root so userfs can take over. The first is the pragmatic answer.
extmod/platform_basic.py Port reports sys.platform = "amiga"; the test's expected-platforms list doesn't include it. Decide whether to upstream "amiga" into extmod/modplatform.c (preferred), or carry a downstream .py.exp override (tests/extmod/platform_basic.py.exp.amiga or similar).

Bebbo soft-float library bugs

bebbo gcc 6.5b on -msoft-float ships incorrect floating-point helpers in libgcc / clib2 / libnix. ports/amiga/floatconv.c overrides each one (some directly, some via --wrap because clib2 fat-packs them with __muldf3 and friends). Keep this list in mind when triaging arithmetic oddities:

Routine Bug Trigger
__floatunsidf, __floatundidf, __floatdidf High-bit-set values convert to garbage float("9"*51 + "e-39"), array.array('Q', [...]), any mp_obj_new_int_from_uint(>2³¹) → double
__eqdf2, __nedf2, __ledf2, __gedf2, __ltdf2, __gtdf2 NaN treated as ordered/equal == / != / <= / >= with NaN; math.isclose, set/dict NaN keys, x != x
pow(-1, NaN) (libnix) Returns 1.0, CPython expects NaN (-1) ** float('nan')
tgamma(-inf) (libnix) Returns +inf, CPython raises math.gamma(-inf)
__fixdfsi (clib2) Calls IEEEDPFix which under vamos aborts the whole emulator on NaN hash(float('nan')) (gcc 6.5 emits __fixdfsi for mp_float_hash's bit-level body after opt)

2. Amiberry (full-system emulation)

Use Amiberry when behaviour needs Kickstart — Workbench launch, the icon / datatypes / intuition libraries, 68881 FPU, persistent REPL history in S:, anything where AmigaDOS shell quirks matter, anything where vamos "close enough" isn't.

One-shot scripted runs use a startup hook: S:user-startup runs py0:boot if present (where py0: is the repo root mounted as an Amiberry hard drive), and boot redirects its own output to py0:boot.log that the host can read directly. Custom tooling, but handy for a single canned script.

Driving run-tests.py -t over serial (2026-06-20)

tests/run-tests.py -t does now drive the emulated Amiga like a bare-metal board, over Amiberry's TCP serial port. (Earlier the RX path looked broken — SER: delivered host→Amiga zero bytes — but that was the raw serial handler: it's single-open and doesn't assert the CTS/RTS handshake the bridge needs. The AUX console works fine.)

Setup, Amiga side — attach a shell to the serial console on boot, with an explicit baud, in S:user-startup:

newshell AUX:57600/8n1

Use 57600 (or 38400). 115200 is flaky on the Amiga. At 57600/8n1 the console is byte-clean both directions (no spurious CR doubling).

Setup, host side — tools/pyboard.py can't speak TCP, so a tiny bridge relays the socket to/from stdin/stdout as a raw serial line. It lives at tests/ports/amiga/amiga_serial.py and runs via pyboard's exec: transport:

cd tests
./run-tests.py -t "exec:python3 $PWD/ports/amiga/amiga_serial.py --bridge --wait" \
    extmod/socket_tcp_basic.py extmod/time_ms_us.py

Platform auto-detects as amiga. --wait polls for the console so run-tests is the sole connection.

Three port changes made this work (pyboard soft-resets with Ctrl-D before every test, which the port previously treated as "exit to shell"):

  • main.c factors the runtime into amiga_runtime_init/deinit and wraps the REPL in a soft-reset loop: Ctrl-D / SystemExit in the raw REPL rebuilds the VM, prints MPY: soft reboot, and re-enters (like a real board); friendly-REPL Ctrl-D still drops to the AmigaShell.
  • mphalport.c adds amiga_stdin_hit_eof() so a client disconnect (FGetC < 0) exits cleanly instead of spinning in the soft-reset loop.
  • main.c opens bsdsocket.library / AmiSSL once at startup, not per soft reset — Amiberry's bsdsocklib_Close pumps SDL/Cocoa events off the main thread and SIGABRTs the emulator on macOS, so a long sweep of per-reset closes used to crash it.

Practical limits:

  • Pacing: leave the bridge at its default 5 ms/byte. The link has no flow control; --delay 0.002 is fine for the helper's own paste-mode exec() but corrupts pyboard's raw-paste protocol (read_until timeouts, run-tests hangs minutes before the first test).
  • Per-test / small batches only. A full-suite sweep over serial is impractical — run-tests ships every test body + import prologue over the link (~1 hr for basics/) — and a single destabilising test (e.g. memoryerror.py exhausting the heap) desyncs the shell and cascades into false failures. For bulk, use vamos (above) or the on-device runner (below); the serial path is for verifying the handful of tests that need real Kickstart.
  • One run-tests session per boot. The AUX console doesn't survive a host disconnect, so reset the emulator (reset_emulation, hard) before each session; --wait then handles boot timing.

The bridge is also a standalone REPL driver: amiga_serial.py -c "code", -f file.py, --shell "cmd", or no args for the banner.

On-device test runner: tools/amiga-runtests.py

The on-device runner walks a single test directory, runs each .py test via amiga.execute("micropython … >T:…"), and compares the captured output against the matching .py.exp file. No CPython to diff against on the Amiga side, so it relies entirely on the .exp files.

Step 1 — generate the .exp reference set on the host

Upstream only ships .exp files where MicroPython output is expected to differ from CPython. Generate the rest first:

tools/amiga-gen-exp.py tests/basics tests/float tests/io \
    tests/import tests/micropython tests/misc tests/cmdline tests/stress

Existing .exp files are never overwritten — many of them use the ######## wildcard or regex matching that the test framework depends on. Delete a specific .exp and re-run if you need to regenerate one.

.exp files are not checked in. Add tests/**/*.py.exp to .git/info/exclude if the chatter in git status bothers you.

Step 2 — boot Amiberry with at least 32 KB AmigaDOS stack

The default 4–8 KB stack is too small for the deep compile-time recursion a handful of tests trigger (e.g. try_except_break.py); a stack overflow there manifests as Software Failure 8000000B (Line F trap) rather than a clean Python RuntimeError.

1> Stack 32768
1> cd py0:
1> micropython tools/amiga-runtests.py tests/basics
1> micropython tools/amiga-runtests.py tests/float T:my-results/

The second argument is the result directory (default T:mp-test-results/). On FAIL, <dir>/<test>.py.out (captured stdout) and <dir>/<test>.py.exp (expected reference) land there. On pass / skip, stale artefacts from a previous run are deleted.

T: lives in RAM under AmigaOS — a clean reboot wipes the artefacts.

What it skips

The runner skips test classes the port can't pass for structural reasons, matching the equivalent feature-check skips in run-tests.py:

  • int_big* / *_intbig.py — no arbitrary-precision ints (MICROPY_LONGINT_IMPL_LONGLONG).
  • *_endian.py — big-endian platform, most tests assume little-endian.
  • native_* / viper_* — Phase 12 (@micropython.native call support is the next emitter rework).
  • cmd_* / repl_* — need # cmdline: directives, regex .exp matching, and PTY interaction; only tests/run-tests.py can drive those.
  • A small frozenset of one-off tests that hit disabled extmod features (weakref_*, fun_code, attrtuple2.py, etc.).

Result format and CRLF

The Amiga port emits \r\n on stdout even when redirected to a file — this is mp_hal_stdout_tx_strn_cooked doing the AmigaShell-compatible thing. The on-device runner strips trailing \r line by line before comparing against the LF .exp reference, so the .exp files generated by amiga-gen-exp.py stay platform-neutral. Any host-side diff against captured output needs the same normalisation:

diff <(tr -d '\r' < captured.out) reference.exp

Variants that need Amiberry, not vamos

  • 68020fpu — needs 68881; vamos has no 68881/68882 emulation.
  • Workbench launchWBStartup, icon.library, tooltype handling. Vamos has no icon.library.
  • AmiSSL (Phase 28) — needs Kickstart and a real amisslmaster.library install (see SSL section below).

SSL tests (Phase 28)

TLS support is on in all three shipped variants. Exercising ssl on target needs:

  • AmiSSL v5 installed. amisslmaster.library must be reachable via LIBS:. The conventional install puts it at SYS:AmiSSL/Libs/amisslmaster.library with the CA bundle in a c_rehash dir at SYS:AmiSSL/certs/. AmiSSL's own installer adds Assign AmiSSL: SYS:AmiSSL and Assign LIBS: AmiSSL:Libs ADD to S:user-startup.
  • AmiSSL assigns in scope at micropython launch. Watch the ordering in user-startup: if the boot hook runs py0:boot before the AmiSSL assigns, the boot script must add them itself or import ssl will raise on SSLContext().

Recommended verify path:

import ssl
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.set_default_verify_paths()   # preferred over load_verify_locations

set_default_verify_paths() sidesteps two AmigaOS-specific gotchas with manual cert loading:

  1. Trailing-slash capath fails. load_verify_locations(capath= "AmiSSL:certs/") concatenates internally to a double-slash that AmigaDOS interprets as parent-dir reference, and lookups silently miss. Pass "AmiSSL:certs" (no trailing slash) if you want to use it explicitly.
  2. AmiSSL c_rehash uses the old (pre-OpenSSL-1.0.0) subject hash algorithm. New-hash filenames silently miss in lookups even though byte-identical files exist under their old-hash names in the same directory.

Known limitation: TLS 1.3 against modern CDNs

CERT_REQUIRED HTTPS against TLS-1.3-eager fronts (Cloudflare, GitHub) completes the handshake (cert chain verifies fine) but the server closes the connection before any application data can be written: ws.write returns EPIPE / broken pipe with zero bytes sent. Hosts that don't strict-front like this (e.g. www.python.org) work cleanly.

Reproducible with AmiSSL's own openssl s_client — same CONNECTION CLOSED BY SERVER + tls_retry_write_records:Broken pipe trace, with all four certs in the chain reporting verify return:1. So it is below the MicroPython layer. Workaround attempts inside s_client (forcing classic X25519 to skip the post-quantum MLKEM hybrid, disabling session tickets, forcing TLS 1.2) either reproduce the same close or get rejected outright at ClientHello. See docs/phase28-ssl-plan.md for the full diagnostic log.

Upstream extmod/ssl* and tls* tests

tools/amiga-runtests.py does not gate these out, so they run when the on-device runner walks extmod/. A handful require features we don't yet expose (ssl.wrap_socket module-level legacy form, cadata= parameter, server-mode handshake against a canned cert pair) and will fail; the SSLContext-shape tests (ssl_sslcontext.py, ssl_sslcontext_verify_mode.py etc.) should pass.

Intuition requester dialogs (Phase 30)

tests/ports/amiga/test_intuition_smoke.py covers the module-registration and argument-validation surface; it runs under vamos and prints OK on success. The actual modal dialog can't be tested headlessly — vamos's intuition.library stub is a no-op print, and the requester needs a real public screen.

For end-to-end visual confirmation under Amiberry, from the REPL:

>>> from amiga import intuition
>>> intuition.easy_request("Title", "Pick one.\nMulti-line works.",
...                        ["Apple", "Banana", "Cancel"])
# requester pops; left → 0, middle → 1, right → 2
>>> intuition.auto_request("Replace existing file?")
# returns True for Yes, False for No
>>> intuition.message("Done.")
# single-button requester; clicking OK returns None

intuition.library opens its own screen if no public screen is already up, so the requester appears regardless of whether Workbench is loaded. OSError(EIO) is reserved for the rare case where EasyRequestArgs returns -1 (genuine intuition-side failure, not a missing screen).

ASL file requester (Phase 31)

tests/ports/amiga/test_asl_smoke.py covers the module-registration, arg-shape validation, and the multi=True + save=True ValueError guard; it runs under vamos and prints OK on success. The actual file-pick dialog needs Amiberry or real hardware.

For end-to-end visual confirmation:

>>> from amiga import asl
>>> asl.file_request(title="Pick a script", initial_drawer="Sys:")
'Sys:Prefs/Workbench'        # picked, or
None                          # cancelled

>>> asl.file_request(save=True, initial_file="out.txt")
'Ram Disk:out.txt'

>>> asl.file_request(multi=True, pattern="#?.py")
['Work:scripts/foo.py', 'Work:scripts/bar.py']

>>> asl.file_request(drawers_only=True)
'Work:scripts'

file_request runs the underlying AllocAslRequest + AslRequest calls on a 32 KB scratch stack via StackSwap, so the dialog works even from a default-stack shell prompt — no Stack 32768 prelude required.

ARexx polish (Phase 32)

tests/ports/amiga/test_rexx_polish.py covers the C-primitive + Python-facade surface area; it runs under vamos and prints OK. Real driving of an ARexx host needs rexxsyslib.library and a running peer, so it lives under Amiberry / real hardware.

For end-to-end visual confirmation:

>>> import amiga
>>> amiga.rexx_open()              # so we have a port to be queried
'MICROPYTHON.1'
>>> amiga.rexx_exists("MICROPYTHON.1")
True
>>> "MICROPYTHON.1" in amiga.rexx_list()
True

>>> from amiga import RexxClient
>>> with RexxClient("MICROPYTHON.1") as c:
...     # ... a `rexx_serve()` loop on another task would handle this
...     pass

>>> # Forgetting close() is OK -- amiga_rexx_shutdown picks it up
>>> # on process exit so the MsgPort doesn't leak.

Driving a real host (DOpus, IBrowse, YAM) in a tight loop is what RexxClient was built for: the persistent reply MsgPort saves the CreateMsgPort / DeleteMsgPort cost per send. The one-shot amiga.rexx_send / amiga.rexx helpers are still the right shape for single sends.

Platform identity (Phase 33)

tests/ports/amiga/test_platform_smoke.py covers the CPython-shaped platform surface plus the underlying amiga.* accessors; it runs under vamos. platform.amiga_info() and amiga.chipset() need graphics.library (not stubbed by vamos) and are skipped when that fails.

For end-to-end confirmation under Amiberry:

>>> import platform
>>> platform.amiga_info()
'CPU: 68020 | FPU: 68881 | Chipset: AGA | Kickstart: 45.57 | Chip: 1856KB | Fast: 14336KB'
>>> platform.machine(), platform.release()
('68020', '45.57')

The CPU and FPU strings reflect runtime AmigaOS state -- a standard binary built -m68020 -msoft-float running on a 68040 reports '68040' / '68040', not the compile-time target. Memory values are currently free bytes (AvailMem(MEMF_*)), not total installed.

os extensions and os.path (Phase 34)

tests/ports/amiga/test_os_smoke.py covers the surface of the frozen os.py / _ospath.py extensions: module assembly via from uos import *, the _osamiga re-exports (chmod, getprotect, FIBF_*), makedirs, walk, and every os.path helper. Under vamos the test also round-trips makedirs / walk against RAM: (vamos's RAM disk maps to a host temp directory). chmod round-tripping only fully exercises on Amiberry because vamos's SetProtection is a partial stub.

For interactive REPL confirmation under Amiberry:

>>> import os
>>> os.makedirs("RAM:t/a/b", exist_ok=True)
>>> os.path.isdir("RAM:t/a/b")
True
>>> os.chmod("RAM:t/a/b", os.FIBF_DELETE)
>>> hex(os.getprotect("RAM:t/a/b"))
'0x1'
>>> os.chmod("RAM:t/a/b", 0)
>>> hex(os.getprotect("RAM:t/a/b"))
'0x0'
>>> list(os.walk("RAM:t"))
[('RAM:t', ['a'], []), ('RAM:t/a', ['b'], []), ('RAM:t/a/b', [], [])]

getprotect returns the raw fib_Protection ULONG; a set bit denies the operation for RWED, so mask == 0 means "everything allowed". The APSH bits (FIBF_ARCHIVE, FIBF_PURE, FIBF_SCRIPT, FIBF_HOLD) follow the conventional "set means yes". This matches the AmigaShell Protect command's encoding.

Icon manipulation (Phase 35)

tests/ports/amiga/test_icon_smoke.py covers the _icon module registration, all WB* constants, the error paths (missing path, non-DiskObject to write, unknown type code to new, non-dict tooltypes kwarg), and -- when the host can satisfy GetDefDiskObject (i.e. ENV:sys/def_project.info is reachable) -- a full new() → mutate → write()read() round trip against RAM:. Vamos soft-passes the round trip when ENV:sys defaults aren't available; Amiberry exercises it end-to-end.

For interactive REPL confirmation under Amiberry:

>>> from amiga import icon
>>> d = icon.read("SYS:Prefs")
>>> d.type, d.current_x, d.current_y
('drawer', 80, 24)
>>> list(d.tooltypes)
['SHOW=ICONS']
>>> d.close()
>>>
>>> new = icon.new(
...     icon.WBPROJECT,
...     default_tool="C:Ed",
...     stack_size=8192,
...     tooltypes={"WINDOW": "CON:0/0/640/256/Test", "FLAG": None},
... )
>>> icon.write("RAM:test", new)
>>> new.close()
>>> back = icon.read("RAM:test")
>>> back.default_tool, back.stack_size
('C:Ed', 8192)
>>> back.tooltypes["FLAG"]
b''
>>> back.close()

Tooltype values come back as bytes (the AmigaOS on-disk representation), assignments accept str / bytes / None (None ⇒ flag-style entry, no = separator).

Amiga-specific smoke test sweep (June 2026)

After the Phase 39 extmod opt-ins shipped, the port had targeted smoke tests for the phase-numbered surfaces (intuition / asl / icon / catalog / rexx / platform / os-extensions / hashlib / deflate / btree / wallclock time) but most of the older Amiga-specific surface had no port-local test. A sweep filled the gap, adding 17 smoke tests in tests/ports/amiga/ that cover the foundational ports of the port:

Test file Surface
test_library_proxy_smoke.py amiga.Library + .fd-driven trampoline (_amiga.lib_open / lib_call / lib_close)
test_mem_smoke.py _amiga.alloc_vec / free_vec, peek_b/w/l/bytes, poke_b/w/l/bytes, big-endian byte order, MEMF_* constants
test_amiga_introspection_smoke.py _amiga.os_version / heap_info / exists / volumes / assigns / disk_info / match / imatch
test_signal_smoke.py _amiga.signal / wait_signal + SIGBREAKF_CTRL_* constants
test_amiga_execute_smoke.py _amiga.execute (dos.library SystemTagList() wrap)
test_workbench_smoke.py _amiga.launched_from_workbench / wb_selected_files / tooltype
test_history_smoke.py REPL history ring buffer (_amiga.readline_history / readline_push_history)
test_time_monotonic_smoke.py time.ticks_ms / ticks_us / ticks_cpu / ticks_add / ticks_diff / sleep_ms / sleep_us
test_os_envvar_smoke.py os.getenv / putenv / unsetenv ↔ AmigaDOS ENV: + os.chmod round-trip via getprotect
test_vfs_smoke.py VfsAmiga file I/O: open r/w/a / b+text, write / read / readline / readlines, seek / tell, ilistdir, stat, rename, mkdir / rmdir / remove, chdir / getcwd, error paths
test_stdio_smoke.py sys.stdin / stdout / stderr -- POSIX fd numbers, write direction guards, TextIOWrapper method set
test_json_smoke.py Port-local modjson -- primitives, composites, whitespace tolerance, string escapes, error paths, dumps/loads round-trip
test_socket_smoke.py socket module surface (constants pinned to AmigaOS bsdsocket numbering, OSError when bsdsocket not present)
test_ssl_smoke.py ssl.SSLContext surface (constants pinned to CPython values, OSError when AmiSSL not present)
test_urequests_smoke.py Frozen urequests -- _parse_url, _quote, _urlencode, verb surface, Response class shape
test_floatconv_smoke.py ports/amiga/floatconv.c libgcc / libm wraps (__floatunsidf / __floatdidf / __wrap_pow / __wrap_tgamma / __fixdfsi / NaN comparison)
test_native_emitter_smoke.py Regression test for the MOVEQ encoding fix (see below)

All 17 pass under vamos on the standard variant. The test_workbench_smoke.py / test_socket_smoke.py / test_ssl_smoke.py / test_amiga_execute_smoke.py files document which assertions only fully exercise on Amiberry / real hardware (Workbench launch state, live bsdsocket I/O, live AmiSSL handshakes, live SystemTagList command execution) and the test structure accepts either the vamos-stub or live-success path so the same file is portable to the Amiberry rig.

Real bug found during the sweep. While drafting test_native_emitter_smoke.py, every @micropython.native binary op against a small-int literal in the range -64..63 was raising TypeError: unsupported types for __X__: 'int', '' or wandering into a vamos invalid-memory crash. Root cause was a one-character typo in py/asm68k.h asm_68k_moveq: the destination data register was being placed at bit position 8 instead of 9, so MOVEQ #1, D2 (wanted opcode 0x7401) was generating 0x7201 = MOVEQ #1, D1. That clobbered the operand register the lhs was about to be loaded into, and the rhs register the binary op expected stayed stale. Fixed in commit fb1aeab92; the regression test pins both sides of the MOVEQ-range boundary (literal 63 vs 64) plus <, >, ==, !=, +, -, *, branch-on-literal, while-loop with literal-bounded condition, and a viper compare for completeness. Bug had been present since the original 68k native emitter commit (42beaaeca, Phase 12).

One Amiga-specific surface remains without a targeted smoke test: Ctrl+C interrupt polling (amiga_check_ctrl_c invoked by the VM hook every 1024 bytecodes). vamos's exec.library Signal / Wait emulation doesn't deliver signals to the same task, so the round trip can only be verified on Amiberry / real hardware. The hook itself is exercised implicitly by every test that loops more than 1024 bytecodes without crashing.

Catalog lookup (Phase 36)

tests/ports/amiga/test_catalog_smoke.py covers the _catalog module registration, the language() accessor, error paths (missing catalog, missing name, non-str language kwarg), the context-manager surface, and -- when the host can satisfy a catalog open -- a lookup round trip. Vamos has no real locale.library, so it soft-passes the round-trip block; Amiberry covers it end-to-end against Sys/monitors.catalog.

For interactive REPL confirmation under Amiberry:

>>> from amiga import catalog
>>> catalog.language()
'english'
>>> # Stock Workbench is English -- declaring the binary's built-in
>>> # language as "german" forces a translation lookup that actually
>>> # loads the file rather than treating it as already in place.
>>> with catalog.open("Sys/monitors.catalog",
...                   language="english",
...                   built_in_language="german") as cat:
...     print(cat.lookup(99999, "FALLBACK"))
...
FALLBACK

Closed catalogs are safe to call lookup on — GetCatalogStr treats NULL as "no catalog" and returns the caller's default. The context manager closes on block exit so the underlying locale.library allocation is released promptly.


3. CI (cross-compile only)

.github/workflows/ports_amiga.yml runs on workflow_dispatch with a ref input (branch / tag / commit, default amiga-port). One job inside stefanreinauer/amiga-gcc:latest builds mpy-cross once, then all four variants sequentially. Each binary uploads as a separate artifact named micropython-amiga-<variant>-<ref>-<sha>.

CI does not run the test suite — it's a cross-compile gate plus an artefact producer. Functional testing happens locally against vamos / Amiberry.

Local mirror: tools/amiga-build.sh

Runs the same image with the same commands, so local and CI binaries are bit-identical:

tools/amiga-build.sh                   # all four variants
tools/amiga-build.sh standard          # one
tools/amiga-build.sh standard 68040    # several
tools/amiga-build.sh clean             # clean all build dirs

Files are written as the host user (via --user), not root.


Quick reference

# Full vamos sweep against the standard variant
cd ports/amiga && make
export MICROPY_MICROPYTHON="$(pwd)/../../tools/amiga-vamos-run.sh"
cd ../../tests
./run-tests.py -d basics float io import micropython misc \
    -e "inlineasm|machine_|thread|extmod/ussl|extmod/uasync"

# Interactive REPL under vamos
tools/amiga-vamos-repl.sh

# Generate .exp files for on-device runner
tools/amiga-gen-exp.py tests/basics tests/float tests/io tests/import \
    tests/micropython tests/misc tests/cmdline tests/stress

# Then boot Amiberry, `Stack 32768`, `cd py0:`, and
# `micropython tools/amiga-runtests.py tests/basics`

# CI-identical build all variants
tools/amiga-build.sh

Clone this wiki locally