From 7f88da8916721ae57d454f8db4038d4dccc4902e Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 24 Jul 2026 14:19:11 -0700 Subject: [PATCH] feat: install pure Python dependencies --- PLAN.md | 6 ++- README.md | 9 ++-- docs/architecture.md | 2 +- docs/cpython-compatibility.md | 21 ++++++-- guest-tests/cpython/README.md | 25 +++++----- guest-tests/cpython/package-packaging.sh | 42 ++++++++++++++++ guest-tests/cpython/project/main.py | 8 +++ guest-tests/cpython/verify-browser.sh | 3 ++ guest-tests/cpython/verify.sh | 13 +++-- web/src/main.ts | 62 +++++++++++++++++------- web/tests/cpython.spec.ts | 23 +++++++-- 11 files changed, 165 insertions(+), 49 deletions(-) create mode 100755 guest-tests/cpython/package-packaging.sh diff --git a/PLAN.md b/PLAN.md index c2b1f5b..17d7fb8 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 4 complete; Phase 5 multi-file imports and tracebacks implemented +**Status:** Phase 5 complete; Phase 6 in-browser C compilation next **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace @@ -1867,7 +1867,9 @@ Hot-dispatch integration is implemented for the initial scalar subset. The inter Phase 4 is complete for the planned Tier-1 scope. An opt-in Chromium benchmark runs the 17-instruction integer-mix loop through fresh interpreter-only and hot-translation sessions, warms both paths, takes five samples, and compares their medians without burdening normal CI with a timing assertion. A July 24, 2026 local run over 278,528 guest instructions measured 36.8 ms interpreted and 24.5 ms translated, a 1.50x end-to-end speedup including scalar state exchange and function-table dispatch. The benchmark requires at least a 1.2x speedup when explicitly enabled. Broader operation coverage and a larger differential corpus remain ongoing quality work, but every Phase 4 deliverable and acceptance criterion now has an implementation and regression or reproducible benchmark. Phase 5 begins with multi-file CPython programs, filesystem-backed imports, exceptions/tracebacks, pure-Python package installation, and compatibility documentation. -The first Phase 5 checkpoint is implemented. A checked-in multi-file project imports `phase5pkg.calculator` from the guest filesystem, prints a computed result, and deliberately raises an exception so one run covers package discovery, source loading, compilation, traceback paths, exception text, and nonzero exit behavior. The standard-library snapshot and project package remain separate deterministic images; Chromium installs the package through the project-image API, while the native CLI now accepts repeatable `--filesystem-install` overlays. CPython's `ioctl(FIOCLEX/FIONCLEX)` descriptor path is implemented with `ENOTTY` fallback for unrelated requests. Native and opt-in Chromium regressions both pass against the same CPython 3.12.13 runtime and images. Compatibility and current limitations are recorded in `docs/cpython-compatibility.md`. Third-party pure-Python dependency installation remains before Phase 5 is complete. +The first Phase 5 checkpoint implemented a checked-in multi-file project that imports `phase5pkg.calculator` from the guest filesystem, prints a computed result, and deliberately raises an exception so one run covers package discovery, source loading, compilation, traceback paths, exception text, and nonzero exit behavior. The standard-library snapshot and project package remain separate deterministic images; Chromium installs the package through the project-image API, while the native CLI accepts repeatable `--filesystem-install` overlays. CPython's `ioctl(FIOCLEX/FIONCLEX)` descriptor path is implemented with `ENOTTY` fallback for unrelated requests. Native and opt-in Chromium regressions both pass against the same CPython 3.12.13 runtime and images. Compatibility and current limitations are recorded in `docs/cpython-compatibility.md`. + +Phase 5 is complete. `guest-tests/cpython/package-packaging.sh` downloads the official pure-Python `packaging` 26.2 wheel into the repository's ignored `.tmp` directory, verifies its pinned SHA-256 digest, preserves its package and distribution metadata, and emits an independent filesystem image. The shared project imports that wheel, checks its version and `METADATA`, and retains the multi-file result plus deliberate traceback assertions. Native and opt-in Chromium regressions install the standard library, project, and dependency as separate ordered overlays and pass under a 120-million-instruction cap. Chromium image operations now expose a pending state, disable conflicting controls, and acknowledge request identities, closing a race discovered by consecutive installs. Project and package files persist through the existing OPFS snapshot path. Guest-side dependency resolution, network indexes, native wheels, and shared extensions remain deferred. Every Phase 5 deliverable and acceptance criterion has an implementation and regression; Phase 6 begins with a packaged AArch64 Clang/LLD/musl toolchain and child-process compiler orchestration. Do not begin the full web IDE before item 30 passes. diff --git a/README.md b/README.md index 3e09e2a..dfaa6d6 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,13 @@ 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, while `guest-tests/cpython/verify-browser.sh` runs -the packaged interpreter and standard library in Chromium. See +multi-file/package regressions, while `guest-tests/cpython/verify-browser.sh` +runs the packaged interpreter, standard library, project, and checksum-pinned +`packaging` wheel in Chromium. See [`guest-tests/cpython/README.md`](guest-tests/cpython/README.md) for prerequisites and packaging details. -The verified multi-file import, package-image, traceback behavior, and current -limitations are summarized in +The verified multi-file and third-party imports, package-image overlays, +traceback behavior, and current limitations are summarized in [`docs/cpython-compatibility.md`](docs/cpython-compatibility.md). The browser build generates its Memory64, JSPI, and P-code `.wasm` probes before starting Vite. Generated artifacts are not committed. Select **Uploaded AArch64 ELF** to run an external static executable with a chosen `argv[0]` and one argument per line; the executable is transferred directly to the runtime Worker. diff --git a/docs/architecture.md b/docs/architecture.md index a8a3ad1..545800e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -102,4 +102,4 @@ The Icicle feasibility spike is isolated under `experiments/icicle`; it is not a Phase 4 is complete for the initial Tier-1 scope: bounded profiling, scalar basic-block lowering, browser-side dynamic compilation, function-table hot dispatch, interpreter fallback, a session-local module cache, code-identity invalidation tests, and observable translation metrics are all integrated. The differential probe compares the complete scalar state ABI and next PC after interpreting and translating the same 17-instruction arithmetic/control block. An opt-in five-sample Chromium benchmark measures warmed interpreter-only and translated sessions; the recorded 278,528-instruction checkpoint is 1.50x faster through translation. Broader lowering and differential coverage remain ongoing work while Phase 5 shifts to the robust CPython environment. -Phase 5 now has a shared native/browser multi-file CPython regression. Standard-library and project package snapshots remain independently installable, and the native CLI's repeatable image-overlay option mirrors the browser install operation. The Linux runtime implements the close-on-exec ioctl requests used by CPython's directory-opening path. Filesystem-backed imports, deterministic project output, guest-path tracebacks, exception text, and exit status are verified in both hosts; third-party pure-Python dependency installation remains. +Phase 5 is complete. A shared native/browser CPython regression independently installs the standard-library snapshot, a multi-file project image, and a checksum-pinned official `packaging` 26.2 wheel image. The CLI's repeatable image-overlay option mirrors the browser install operation, while the browser disables conflicting controls until each asynchronous image request is acknowledged and persists the result in OPFS. The Linux runtime implements the close-on-exec ioctl requests used by CPython's directory-opening path. Project and third-party filesystem imports, retained wheel metadata, deterministic output, guest-path tracebacks, exception text, and exit status are verified in both hosts. Guest-side package resolution, network indexes, and native wheels remain outside this phase; Phase 6 moves to in-browser C compilation. diff --git a/docs/cpython-compatibility.md b/docs/cpython-compatibility.md index 068e225..1241765 100644 --- a/docs/cpython-compatibility.md +++ b/docs/cpython-compatibility.md @@ -13,14 +13,18 @@ directory. - Filesystem-backed package discovery and imports. - A separately packaged pure-Python project image installed over the runtime snapshot. +- A separately packaged, checksum-pinned `packaging` 26.2 wheel installed as a + second overlay, imported from the guest filesystem, and accompanied by its + original distribution metadata. - Unhandled exceptions with readable guest file paths, source lines, exception type, message, and exit status 1. - Identical project image and script behavior in the native CLI and Chromium. The regression project is in `guest-tests/cpython/project`. It imports -`phase5pkg.calculator`, prints `multi-file result: 42`, then raises a deliberate -`RuntimeError` so both successful import behavior and traceback formatting are -covered in one bounded guest run. +`phase5pkg.calculator` and `packaging`, prints `multi-file result: 42`, verifies +the installed wheel's version and `METADATA` file, then raises a deliberate +`RuntimeError`. One bounded guest run therefore covers project and third-party +package discovery, source loading, package data, and traceback formatting. ## Packaging and execution @@ -29,6 +33,11 @@ image. Browser hosts install it through the existing project-image operation. Native hosts can apply the same image over a base snapshot with the repeatable `--filesystem-install ` run option. +`guest-tests/cpython/package-packaging.sh` downloads the official pure-Python +wheel into `.tmp`, verifies its pinned SHA-256 digest, retains the wheel's +package and distribution metadata layout, and creates a separate installable +image. It does not install anything into the host Python environment. + Use these end-to-end verifiers after `guest-tests/cpython/build.sh`: ```sh @@ -44,8 +53,10 @@ about 76 MiB and the guest run currently takes several minutes. - Shared extension modules and `dlopen` are deferred to Phase 7. Configured dependency-free modules are statically linked into the interpreter. -- `pip` and third-party dependency resolution are not yet verified. Pure-Python - source trees can be installed as deterministic project images today. +- Guest-side `pip`, dependency resolution, and package-index access are not + implemented. Hosts can checksum-pin and package compatible pure-Python wheels + as deterministic installable images; `packaging` 26.2 is the verified + checkpoint. - Threaded Python execution is not supported by the single-threaded guest runtime. - Socket-backed package indexes and native wheels are outside the current diff --git a/guest-tests/cpython/README.md b/guest-tests/cpython/README.md index 197cfed..a2b7c7b 100644 --- a/guest-tests/cpython/README.md +++ b/guest-tests/cpython/README.md @@ -9,7 +9,7 @@ Prerequisites: - Zig 0.16.0 - the repository Rust toolchain -- `curl`, `make`, `tar`, `shasum`, and a native C compiler +- `curl`, `make`, `tar`, `unzip`, `shasum`, and a native C compiler Build the host bootstrap interpreter, target interpreter, and filesystem image: @@ -30,11 +30,14 @@ guest-tests/cpython/verify.sh ``` The verification checks `Python 3.12.13`, installs a separately packaged -multi-file project over the standard-library snapshot, imports its -filesystem-backed package, prints `multi-file result: 42`, and validates an -unhandled exception traceback with guest paths and exit status 1. The current -interpreter can take several minutes to complete this regression even when the -native runner is built in release mode. +multi-file project and the checksum-pinned official `packaging` 26.2 wheel over +the standard-library snapshot, imports both filesystem-backed packages, checks +the wheel's retained distribution metadata, prints their deterministic +results, and validates an unhandled exception traceback with guest paths and +exit status 1. `package-packaging.sh` keeps the wheel, unpacked contents, and +generated image under `.tmp`. 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: @@ -44,8 +47,8 @@ guest-tests/cpython/verify-browser.sh This opt-in Playwright regression replaces the isolated browser project's filesystem snapshot with `python-lib.bnfs`, installs the separately generated -`phase5-project.bnfs`, uploads `python.aarch64.elf`, and checks the same import, -output, traceback, and exit status. 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. +`phase5-project.bnfs` and `packaging-26.2.bnfs` overlays, uploads +`python.aarch64.elf`, and checks the same imports, metadata, output, traceback, +and exit status. It is skipped during the normal browser suite because the +generated CPython artifacts are deliberately not committed. The interpreter +regression currently takes several minutes in Chromium. diff --git a/guest-tests/cpython/package-packaging.sh b/guest-tests/cpython/package-packaging.sh new file mode 100755 index 0000000..de20c0c --- /dev/null +++ b/guest-tests/cpython/package-packaging.sh @@ -0,0 +1,42 @@ +#!/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 +dependency_directory=$work_directory/dependencies/packaging-26.2 +wheel=$dependency_directory/packaging-26.2-py3-none-any.whl +unpacked=$dependency_directory/unpacked +image=$work_directory/packaging-26.2.bnfs +wheel_url=https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl +wheel_sha256=5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e +runner=$workspace_directory/target/release/binarrow + +mkdir -p "$workspace_directory/.tmp/rust-tmp" "$dependency_directory" +export TMPDIR=$workspace_directory/.tmp/rust-tmp + +if [ ! -f "$wheel" ]; then + curl --fail --location --retry 3 --output "$wheel.part" "$wheel_url" + mv "$wheel.part" "$wheel" +fi +actual_sha256=$(shasum -a 256 "$wheel" | awk '{print $1}') +if [ "$actual_sha256" != "$wheel_sha256" ]; then + echo "unexpected packaging wheel checksum: $actual_sha256" >&2 + exit 1 +fi + +if [ ! -f "$unpacked/packaging/__init__.py" ]; then + mkdir -p "$unpacked" + unzip -q "$wheel" -d "$unpacked" +fi + +env TMPDIR="$TMPDIR" cargo build \ + --manifest-path "$workspace_directory/Cargo.toml" \ + --release \ + -p binarrow-cli +"$runner" image pack \ + --guest-root /project/phase5 \ + "$unpacked" \ + "$image" + +echo "$image" diff --git a/guest-tests/cpython/project/main.py b/guest-tests/cpython/project/main.py index 7d1ff49..2daa796 100644 --- a/guest-tests/cpython/project/main.py +++ b/guest-tests/cpython/project/main.py @@ -1,5 +1,13 @@ from phase5pkg.calculator import multiply +import packaging print(f"multi-file result: {multiply(6, 7)}", flush=True) +with open("/project/phase5/packaging-26.2.dist-info/METADATA", "rb") as metadata: + has_distribution_metadata = b"Name: packaging" in metadata.read() +print( + f"third-party result: packaging {packaging.__version__} " + f"{has_distribution_metadata}", + flush=True, +) raise RuntimeError("phase5 traceback") diff --git a/guest-tests/cpython/verify-browser.sh b/guest-tests/cpython/verify-browser.sh index 4c290e8..56906e1 100755 --- a/guest-tests/cpython/verify-browser.sh +++ b/guest-tests/cpython/verify-browser.sh @@ -7,6 +7,7 @@ work_directory=$workspace_directory/.tmp/cpython-3.12.13 runtime=$work_directory/python.aarch64.elf filesystem_image=$work_directory/python-lib.bnfs project_image=$work_directory/phase5-project.bnfs +dependency_image=$work_directory/packaging-26.2.bnfs project_directory=$fixture_directory/project runner=$workspace_directory/target/release/binarrow @@ -26,10 +27,12 @@ env TMPDIR="$TMPDIR" cargo build \ --guest-root /project/phase5 \ "$project_directory" \ "$project_image" +"$fixture_directory/package-packaging.sh" >/dev/null pnpm --dir "$workspace_directory/web" generate:probes BINARROW_CPYTHON_RUNTIME=$runtime \ BINARROW_CPYTHON_IMAGE=$filesystem_image \ BINARROW_CPYTHON_PROJECT_IMAGE=$project_image \ +BINARROW_CPYTHON_DEPENDENCY_IMAGE=$dependency_image \ pnpm --dir "$workspace_directory/web" exec playwright test \ tests/cpython.spec.ts diff --git a/guest-tests/cpython/verify.sh b/guest-tests/cpython/verify.sh index 27bc352..eaa4be1 100755 --- a/guest-tests/cpython/verify.sh +++ b/guest-tests/cpython/verify.sh @@ -7,6 +7,7 @@ work_directory=$workspace_directory/.tmp/cpython-3.12.13 runtime=$work_directory/python.aarch64.elf filesystem_image=$work_directory/python-lib.bnfs project_image=$work_directory/phase5-project.bnfs +dependency_image=$work_directory/packaging-26.2.bnfs project_directory=$fixture_directory/project runner=$workspace_directory/target/release/binarrow @@ -27,10 +28,11 @@ cargo build \ --guest-root /project/phase5 \ "$project_directory" \ "$project_image" +"$fixture_directory/package-packaging.sh" >/dev/null version=$( "$runner" run \ - --instruction-budget 100000000 \ + --instruction-budget 120000000 \ --filesystem-limit 67108864 \ --filesystem-snapshot "$filesystem_image" \ --random-seed 1 \ @@ -46,10 +48,11 @@ stdout=$work_directory/phase5.stdout stderr=$work_directory/phase5.stderr set +e "$runner" run \ - --instruction-budget 100000000 \ + --instruction-budget 120000000 \ --filesystem-limit 67108864 \ --filesystem-snapshot "$filesystem_image" \ --filesystem-install "$project_image" \ + --filesystem-install "$dependency_image" \ --random-seed 1 \ --argv0 /project/python/bin/python \ "$runtime" -S -E /project/phase5/main.py \ @@ -60,13 +63,15 @@ if [ "$exit_code" -ne 1 ]; then echo "unexpected CPython project exit code: $exit_code" >&2 exit 1 fi -if [ "$(cat "$stdout")" != "multi-file result: 42" ]; then +expected_output='multi-file result: 42 +third-party result: packaging 26.2 True' +if [ "$(cat "$stdout")" != "$expected_output" ]; then echo "unexpected CPython project output" >&2 cat "$stdout" >&2 exit 1 fi grep -F 'Traceback (most recent call last):' "$stderr" >/dev/null -grep -F 'File "/project/phase5/main.py", line 5' "$stderr" >/dev/null +grep -F 'File "/project/phase5/main.py", line 13' "$stderr" >/dev/null grep -F 'RuntimeError: phase5 traceback' "$stderr" >/dev/null echo "CPython 3.12.13 fixture passed" diff --git a/web/src/main.ts b/web/src/main.ts index 2e88f72..8391e75 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -74,20 +74,22 @@ let waitingInput = false; let runId = 0; let preserveDiagnosticStatus = false; let imageRequestId = 0; +let imageOperationPending = false; function setControls(): void { - startButton.disabled = !workerReady || running; + const busy = running || imageOperationPending; + startButton.disabled = !workerReady || busy; stopButton.disabled = !running; - fixture.disabled = running; - programElf.disabled = running; - programArgv0.disabled = running; - programArguments.disabled = running; + fixture.disabled = busy; + programElf.disabled = busy; + programArgv0.disabled = busy; + programArguments.disabled = busy; terminalInput.disabled = !waitingInput; sendInputButton.disabled = !waitingInput; - projectImage.disabled = !workerReady || running; - replaceImageButton.disabled = !workerReady || running; - installImageButton.disabled = !workerReady || running; - exportImageButton.disabled = !workerReady || running; + projectImage.disabled = !workerReady || busy; + replaceImageButton.disabled = !workerReady || busy; + installImageButton.disabled = !workerReady || busy; + exportImageButton.disabled = !workerReady || busy; } function updateProgramControls(): void { @@ -195,6 +197,7 @@ function readLimits(): ExecutionLimits { function createWorker(): void { workerReady = false; + imageOperationPending = false; setControls(); worker = new Worker(new URL("./probe.worker.ts", import.meta.url), { type: "module", @@ -203,6 +206,7 @@ function createWorker(): void { if (event.data.kind === "error") { running = false; waitingInput = false; + imageOperationPending = false; status.textContent = `Worker failed: ${event.data.message}`; status.dataset.state = "error"; setControls(); @@ -225,6 +229,7 @@ function createWorker(): void { if (event.data.requestId !== imageRequestId) { return; } + status.dataset.imageRequestId = String(event.data.requestId); if (event.data.operation === "export") { const url = URL.createObjectURL( new Blob([event.data.snapshot as Uint8Array], { @@ -244,6 +249,8 @@ function createWorker(): void { : "Project image installed"; } status.dataset.state = "ready"; + imageOperationPending = false; + setControls(); return; } if (event.data.runId !== runId) { @@ -260,6 +267,7 @@ function createWorker(): void { worker.addEventListener("error", (event) => { running = false; waitingInput = false; + imageOperationPending = false; status.textContent = `Worker failed: ${event.message}`; status.dataset.state = "error"; setControls(); @@ -384,15 +392,31 @@ async function importProjectImage(mode: "replace" | "install"): Promise { status.dataset.state = "error"; return; } + imageOperationPending = true; + status.textContent = + mode === "replace" + ? "Importing project snapshot…" + : "Installing project image…"; + status.dataset.state = "running"; + setControls(); imageRequestId += 1; - const command: WorkerCommand = { - kind: "filesystem-import", - requestId: imageRequestId, - maxFilesystemBytes: parseLimit(filesystemLimit), - mode, - snapshot: new Uint8Array(await file.arrayBuffer()), - }; - worker.postMessage(command); + try { + const command: WorkerCommand = { + kind: "filesystem-import", + requestId: imageRequestId, + maxFilesystemBytes: parseLimit(filesystemLimit), + mode, + snapshot: new Uint8Array(await file.arrayBuffer()), + }; + worker.postMessage(command); + } catch (error) { + imageOperationPending = false; + status.textContent = + "Cannot read project image: " + + (error instanceof Error ? error.message : String(error)); + status.dataset.state = "error"; + setControls(); + } } replaceImageButton.addEventListener("click", () => { @@ -402,6 +426,10 @@ installImageButton.addEventListener("click", () => { void importProjectImage("install"); }); exportImageButton.addEventListener("click", () => { + imageOperationPending = true; + status.textContent = "Exporting project snapshot…"; + status.dataset.state = "running"; + setControls(); imageRequestId += 1; const command: WorkerCommand = { kind: "filesystem-export", diff --git a/web/tests/cpython.spec.ts b/web/tests/cpython.spec.ts index d82f2f1..8f00d5d 100644 --- a/web/tests/cpython.spec.ts +++ b/web/tests/cpython.spec.ts @@ -3,15 +3,16 @@ import { expect, test } from "@playwright/test"; const runtime = process.env.BINARROW_CPYTHON_RUNTIME; const filesystemImage = process.env.BINARROW_CPYTHON_IMAGE; const projectImage = process.env.BINARROW_CPYTHON_PROJECT_IMAGE; +const dependencyImage = process.env.BINARROW_CPYTHON_DEPENDENCY_IMAGE; test("runs the packaged CPython runtime and standard library", async ({ page, }) => { test.skip( - !runtime || !filesystemImage || !projectImage, - "set BINARROW_CPYTHON_RUNTIME, BINARROW_CPYTHON_IMAGE, and BINARROW_CPYTHON_PROJECT_IMAGE", + !runtime || !filesystemImage || !projectImage || !dependencyImage, + "set all BINARROW_CPYTHON runtime and image variables", ); - if (!runtime || !filesystemImage || !projectImage) { + if (!runtime || !filesystemImage || !projectImage || !dependencyImage) { return; } test.setTimeout(15 * 60 * 1_000); @@ -33,6 +34,15 @@ test("runs the packaged CPython runtime and standard library", async ({ await expect(page.getByRole("status")).toHaveText("Project image installed", { timeout: 60_000, }); + await page.getByLabel("Snapshot or package image").setInputFiles(dependencyImage); + await page.getByRole("button", { name: "Install image" }).click(); + await expect(page.getByRole("status")).toHaveAttribute( + "data-image-request-id", + "3", + ); + await expect(page.getByRole("status")).toHaveText("Project image installed", { + timeout: 60_000, + }); await page.getByLabel("Fixture").selectOption("uploaded-program"); await page.getByLabel("AArch64 ELF executable").setInputFiles(runtime); @@ -40,7 +50,7 @@ test("runs the packaged CPython runtime and standard library", async ({ await page .getByLabel("Arguments (one per line)") .fill("-S\n-E\n/project/phase5/main.py"); - await page.getByLabel("Instruction budget").fill("100000000"); + await page.getByLabel("Instruction budget").fill("120000000"); await page.getByRole("button", { name: "Start" }).click(); await expect(page.getByRole("status")).toHaveText("Guest exited", { @@ -48,8 +58,11 @@ test("runs the packaged CPython runtime and standard library", async ({ }); const terminal = page.getByLabel("Guest terminal output"); await expect(terminal).toContainText("multi-file result: 42"); + await expect(terminal).toContainText( + "third-party result: packaging 26.2 True", + ); await expect(terminal).toContainText("Traceback (most recent call last):"); - await expect(terminal).toContainText('File "/project/phase5/main.py", line 5'); + await expect(terminal).toContainText('File "/project/phase5/main.py", line 13'); await expect(terminal).toContainText("RuntimeError: phase5 traceback"); await expect(page.locator("#exit-code")).toHaveText("1"); await expect(page.locator("#diagnostic-code")).toHaveText("—"); -- 2.51.2