From 72ed8f568baf88e9a7b3f3297d4544e1d17dd8f3 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 24 Jul 2026 11:16:15 -0700 Subject: [PATCH] test: verify CPython in Chromium --- PLAN.md | 4 +-- README.md | 3 +- guest-tests/cpython/README.md | 13 ++++++++ guest-tests/cpython/verify-browser.sh | 22 +++++++++++++ web/tests/cpython.spec.ts | 46 +++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 3 deletions(-) create mode 100755 guest-tests/cpython/verify-browser.sh create mode 100644 web/tests/cpython.spec.ts diff --git a/PLAN.md b/PLAN.md index 932c906..0c2385b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,7 +1,7 @@ # AArch64 ELF-to-WebAssembly Browser Runtime ## Engineering Build Plan and Agent Handoff -**Status:** Phase 2 complete; Phase 3 filesystem services and native CPython checkpoint implemented +**Status:** Phase 3 complete; Phase 4 P-code/IR-to-Wasm translation is next **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace @@ -1857,7 +1857,7 @@ The Phase 2 control checkpoint is complete: the Worker accepts explicit C, Rust, Phase 2 is complete. `binarrow-memory-fs` supplies a bounded ephemeral `/tmp` regular-file store behind the project host filesystem trait, while `binarrow-linux-runtime` owns guest descriptors and the observed `openat`, `close`, `lseek`, `read`, and file `write` ABI. A freestanding C fixture creates, writes, seeks, reads, and closes `/tmp/roundtrip.txt` before printing the recovered bytes; native, CLI, and Chromium tests all run the identical ELF. ADR-0002 records the interpreter's interim browser memory design: project-owned sparse 64-bit guest mappings backed by bounded Wasm32 allocations, with Memory64 retained as a required feature gate for the future translator. Every Phase 2 deliverable and acceptance criterion now has an automated regression. Phase 3 begins with a mountable VFS, directory and metadata syscalls, OPFS persistence, terminal input, clock/random services, and the suspension model needed by CPython. -The first CPython checkpoint is complete and reproducible in the native runtime. `guest-tests/cpython/build.sh` checksum-pins CPython 3.12.13, isolates all build and Zig caches under the repository's ignored `.tmp` directory, cross-compiles a static AArch64 musl interpreter, links the configured dependency-free extension modules into the executable, and packages the standard library with its empty pre-dynamic-linking `lib-dynload` landmark. `guest-tests/cpython/verify.sh` checks the version and runs the bounded `-c 'print(6 * 7)'` regression with a deterministic CLI random seed. The freshly built broader module profile added focused semantics for NEON table lookup, unsigned variable shifts, extraction, and 64-bit-group reversal. The browser UI and Worker now accept externally built ELF bytes, `argv[0]`, and additional arguments, with a Chromium regression covering the upload path. Running the packaged CPython runtime and filesystem image through that path is the next acceptance task; shared native extensions remain deferred to Phase 7. +Phase 3 is complete. `guest-tests/cpython/build.sh` checksum-pins CPython 3.12.13, isolates all build and Zig caches under the repository's ignored `.tmp` directory, cross-compiles a static AArch64 musl interpreter, links the configured dependency-free extension modules into the executable, and packages the standard library with its empty pre-dynamic-linking `lib-dynload` landmark. `guest-tests/cpython/verify.sh` checks the version and runs the bounded `-c 'print(6 * 7)'` regression with a deterministic CLI random seed. The freshly built broader module profile added focused semantics for NEON table lookup, unsigned variable shifts, extraction, and 64-bit-group reversal. The browser UI and Worker accept externally built ELF bytes, `argv[0]`, and additional arguments. `guest-tests/cpython/verify-browser.sh` installs the packaged standard-library image, uploads that interpreter, and verifies `print(6 * 7)` with exit status zero in Chromium. This completes every Phase 3 acceptance criterion; Phase 4 begins with hot-block profiling and basic-block Wasm lowering, while shared native extensions remain deferred to Phase 7. Do not begin the full web IDE before item 30 passes. diff --git a/README.md b/README.md index 904fbcf..4d9278f 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ The native CPython checkpoint is reproducible without committing its large generated artifacts. `guest-tests/cpython/build.sh` creates a statically linked AArch64 musl CPython 3.12.13 executable and standard-library image entirely under `.tmp`; `guest-tests/cpython/verify.sh` runs the bounded version and -`print(6 * 7)` regressions. See +`print(6 * 7)` regressions, while `guest-tests/cpython/verify-browser.sh` runs +the packaged interpreter and standard library in Chromium. See [`guest-tests/cpython/README.md`](guest-tests/cpython/README.md) for prerequisites and packaging details. diff --git a/guest-tests/cpython/README.md b/guest-tests/cpython/README.md index cc24eca..9e19e23 100644 --- a/guest-tests/cpython/README.md +++ b/guest-tests/cpython/README.md @@ -33,3 +33,16 @@ The verification checks `Python 3.12.13`, loads the packaged `encodings` module, evaluates `print(6 * 7)`, checks for exactly `42`, and requires a clean guest exit. The current interpreter can take several minutes to complete this regression even when the native runner is built in release mode. + +Run the same packaged interpreter and standard-library image in Chromium: + +```sh +guest-tests/cpython/verify-browser.sh +``` + +This opt-in Playwright regression replaces the isolated browser project's +filesystem snapshot with `python-lib.bnfs`, uploads `python.aarch64.elf`, and +checks `print(6 * 7)` and a clean exit. It is skipped during the normal browser +suite because the generated 76 MiB of CPython artifacts are deliberately not +committed. The interpreter regression currently takes several minutes in +Chromium. diff --git a/guest-tests/cpython/verify-browser.sh b/guest-tests/cpython/verify-browser.sh new file mode 100755 index 0000000..ac14207 --- /dev/null +++ b/guest-tests/cpython/verify-browser.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu + +fixture_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +workspace_directory=$(CDPATH= cd -- "$fixture_directory/../.." && pwd) +work_directory=$workspace_directory/.tmp/cpython-3.12.13 +runtime=$work_directory/python.aarch64.elf +filesystem_image=$work_directory/python-lib.bnfs + +if [ ! -f "$runtime" ] || [ ! -f "$filesystem_image" ]; then + echo "build the CPython fixture first with guest-tests/cpython/build.sh" >&2 + exit 1 +fi + +mkdir -p "$workspace_directory/.tmp/rust-tmp" +export TMPDIR=$workspace_directory/.tmp/rust-tmp + +pnpm --dir "$workspace_directory/web" generate:probes +BINARROW_CPYTHON_RUNTIME=$runtime \ +BINARROW_CPYTHON_IMAGE=$filesystem_image \ + pnpm --dir "$workspace_directory/web" exec playwright test \ + tests/cpython.spec.ts diff --git a/web/tests/cpython.spec.ts b/web/tests/cpython.spec.ts new file mode 100644 index 0000000..6105062 --- /dev/null +++ b/web/tests/cpython.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from "@playwright/test"; + +const runtime = process.env.BINARROW_CPYTHON_RUNTIME; +const filesystemImage = process.env.BINARROW_CPYTHON_IMAGE; + +test("runs the packaged CPython runtime and standard library", async ({ + page, +}) => { + test.skip( + !runtime || !filesystemImage, + "set BINARROW_CPYTHON_RUNTIME and BINARROW_CPYTHON_IMAGE", + ); + if (!runtime || !filesystemImage) { + return; + } + test.setTimeout(10 * 60 * 1_000); + + await page.goto("/"); + await expect(page.getByRole("status")).toHaveText("Browser runtime ready"); + + await page.getByLabel("Filesystem byte limit").fill("67108864"); + await page + .getByLabel("Snapshot or package image") + .setInputFiles(filesystemImage); + await page.getByRole("button", { name: "Replace project" }).click(); + await expect(page.getByRole("status")).toHaveText( + "Project snapshot imported", + { timeout: 60_000 }, + ); + + await page.getByLabel("Fixture").selectOption("uploaded-program"); + await page.getByLabel("AArch64 ELF executable").setInputFiles(runtime); + await page.getByLabel("argv[0]").fill("/project/python/bin/python"); + await page + .getByLabel("Arguments (one per line)") + .fill("-S\n-E\n-c\nprint(6 * 7)"); + await page.getByLabel("Instruction budget").fill("100000000"); + await page.getByRole("button", { name: "Start" }).click(); + + await expect(page.getByRole("status")).toHaveText("Guest exited", { + timeout: 9 * 60 * 1_000, + }); + await expect(page.getByLabel("Guest terminal output")).toHaveText("42"); + await expect(page.locator("#exit-code")).toHaveText("0"); + await expect(page.locator("#diagnostic-code")).toHaveText("—"); +}); -- 2.51.2