Repository navigation
WASM: add a WebAssembly/JupyterLite smoke-test job to CI #933
Description
Note
Updated 2026-08-21. The seed is now on main via #938 (ci/wasm/environment.yml, ci/wasm/smoke_test.py, the Playwright harness ci/wasm/test_jupyterlite.py, and the native gate in ci_wasm_smoke.yml), so the Approach below names it instead of "the config from #928", and a task list for the runner PR has been added. What that list corrects: the parked job (2026-08-19 comment) still needs triggers, the gini expectation in both suite files is inverted now that numba 0.67.0 build 1 serialises parallel=True (patch 0010, emscripten-forge/recipes#6293), the two jitted-generator tests will hang without a real guard, test_simplex_grid fails on a warm cache (#944), and smoke_test.py is not yet shipped into the kernel. The original text is preserved in the edit history.
Part of #925 (Phase 3 — keep it working).
Goal
Browser support must not regress once established: add a wasm job to GitHub Actions that runs a smoke suite in the real browser stack.
Approach
- Seed: PR WASM: JupyterLite environment config and browser smoke suite #938 (merged 2026-08-21) —
ci/wasm/environment.yml(xeus-python kernel env, emscripten-forge-4x + conda-forge channels), the sentinel-driven Playwright harnessci/wasm/test_jupyterlite.py, and the native gate jobnativein.github/workflows/ci_wasm_smoke.yml, which runsci/wasm/smoke_test.pyagainst repo source on every PR. The Emscripten runner job is parked in the 2026-08-19 comment below. - Execute in the xeus-python kernel via headless Chromium + Playwright (the route WASM: JupyterLite environment config and browser smoke suite #938 chose).
pytester, emscripten-forge's pyjs-based recipe-test harness, remains the alternative only if quantecon is later packaged on emscripten-forge (WASM: contribute a quantecon recipe to emscripten-forge #932). - pandas and pytest are both packaged for WASM, so meaningful portions of the real test suite can eventually run in-browser — start with the smoke list from WASM: JupyterLite proof-of-concept deployment and browser smoke suite #928 (
smoke_test.py) and grow from there. - Track the emscripten-forge numba/llvmlite versions in the job the way conda-forge versions are tracked today;
environment.ymlpins barenumba, so have the job log the resolved build (only 0.67.0 build 1 carries patch 0010, see task 2).
This job is also the natural watchpoint for upstreaming: the blog post signals intent to merge the WASM engine into Numba mainline, and once that ships, the patched-recipe caveats (the recipe now carries ten patches, e.g. the serial fallback for parallel=True added by patch 0010 and the ufunc caching disabled by patch 0007) may start dissolving. Until then the job tests the released conda-forge noarch quantecon, not repo source, so per-PR triggering is not useful long-term.
Tasks for the runner PR
- Add the parked
wasmjob with its own triggers. The snippet in the 2026-08-19 comment is a job block and has noon:;ci_wasm_smoke.yml'son:ispush/pull_requesttomain. Either addschedule+workflow_dispatchto that sharedon:block and guard the job withif: github.event_name != 'pull_request', or move it to its own workflow file — in which caseneeds: nativeno longer resolves (it only sees jobs in the same workflow) and must be dropped or replaced. Iterate underpull_requestfirst, as the 2026-08-19 comment describes, then flip. - Flip the gini expectation. numba 0.67.0 build 1 on emscripten-forge-4x (patch 0010, Fallback Numba parallel JIT to serial on Emscripten emscripten-forge/recipes#6293, merged 2026-08-18) demotes
@njit(parallel=True)to the serial pipeline, and the barenumbapin resolves to it.test_gini_coefficient'sxfail(IS_EMSCRIPTEN, strict=True)insmoke_test.pywould XPASS and fail;test_gini_fails_on_emscriptenintest_jupyterlite.pyasserts an error and would fail. Both become plain pass assertions (browser-unverified; inferred from the merged patch). PR Rewrite Gini coefficient in O(n log n) #937's O(n log n) rewrite (WASM: remove @njit(parallel=True) from gini_coefficient via an O(n log n) rewrite #926) is a performance change on top of this, not a prerequisite. - Guard the two jitted-generator tests.
test_support_enumerationandtest_vertex_enumerationinsmoke_test.pycall@njitgenerators (support_enumeration.py:96,vertex_enumeration.py:118) that never return on Emscripten (WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927, WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927 (comment)). Usepytest.mark.skipif(IS_EMSCRIPTEN, ...)orxfail(IS_EMSCRIPTEN, run=False, strict=True)— a plainxfailstill executes the test, andpytest-timeout(SIGALRM /threading.Timer) cannot interrupt a native loop in the single-threaded kernel. Intest_jupyterlite.py, skiptest_support_enumerationor run it last: Playwright'sEXEC_MS(180 s) bounds it host-side, but the module-scoped kernel stays wedged and the following test fails spuriously. Add the minimal bare@njitgenerator test (for i in range(n): yield i) alongside, guarded the same way, as the isolated upstream repro and regression watch. - Mark
test_simplex_gridas expected to fail on a warm cache and cite WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944. Its comment currently says "(WASM: audit 32-bit intp behaviour on wasm32 (overflow guards, simplex_index wrapping, dtypes) #929)"; the actual failure isRuntimeError: no compiled object yet for comb_jit(WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944, reported upstream as Numba:RuntimeError: no compiled object yetemscripten-forge/recipes#6309), which triggers only when acache=Truecaller links a callee restored from the persistent cache — a cold single-session run passes. A regression test needs a second session or a pre-seeded/drive/.cache/numba. - Ship
smoke_test.pyinto the kernel and run it in-kernel. The parked job runs only the Playwright harness (6 cells: kernel boot, import, tauchen,np.linalg.solve,support_enumeration, gini), so a green run fills at most 5 of WASM: JupyterLite proof-of-concept deployment and browser smoke suite #928's 13 rows. WASM: JupyterLite environment config and browser smoke suite #938 assigned in-kernel pytest execution ofsmoke_test.py(16 tests) here (commit 1e0c598 also droppedpytestfromenvironment.yml, so it has to be added back). Copy the file into the site contents, invoke pytest inside the xeus-python kernel, and surface per-test results. - Bump
mamba-org/setup-micromamba@v2to@v3in the parked YAML (v3.0.0 moved to Node 24; the inputs used —micromamba-binary-path,init-shell,generate-run-shell— are unchanged).actions/checkout@v7andactions/setup-python@v7are current.
Expected outcome of the first browser run once 1–6 are done, on numba 0.67.0 build 1: test_gini_coefficient passes; test_support_enumeration, test_vertex_enumeration and the bare-generator test are skipped/xfail(run=False) pending the upstream generator report (#927); test_simplex_grid passes cold and fails warm (#944); everything else is expected to pass. Record the per-function table in #928.
Acceptance criteria
- A CI job builds a JupyterLite deployment containing quantecon and runs the smoke suite in a headless browser
-
ci/wasm/smoke_test.pyexecutes inside the xeus-python kernel (not only the Playwright harness), so every WASM: JupyterLite proof-of-concept deployment and browser smoke suite #928 row gets a browser result - Failures are actionable — per-function results rather than a monolithic pass/fail
- Known in-browser failures (WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927 generators, WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944 warm-cache
comb_jit) are guarded so they cannot consume the job's time limit, and each guard cites its issue
Related
- WASM: JupyterLite environment config and browser smoke suite #938 — seed (merged 2026-08-21):
ci/wasm/environment.yml,smoke_test.py,test_jupyterlite.py, native gate - WASM: JupyterLite proof-of-concept deployment and browser smoke suite #928 — results table this job fills;
smoke_test.pyis its checklist - WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927 and WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927 (comment) — jitted-generator hang (
support_enumeration,vertex_enumeration); LAPACK verified working - WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944 —
cache=Truecallers of cache-restored functions fail on a warm cache (simplex_grid→num_compositions_jit→comb_jit); upstream Numba:RuntimeError: no compiled object yetemscripten-forge/recipes#6309 - WASM: remove @njit(parallel=True) from gini_coefficient via an O(n log n) rewrite #926 / PR Rewrite Gini coefficient in O(n log n) #937 — gini O(n log n) rewrite, approved and awaiting merge
- Fallback Numba parallel JIT to serial on Emscripten emscripten-forge/recipes#6293 — patch 0010, serial fallback for
parallel=True(numba 0.67.0 build 1) - WASM: contribute a quantecon recipe to emscripten-forge #932 — if quantecon is packaged on emscripten-forge, this job switches channel and
pytesterbecomes an option
Activity
CI: move Emscripten runner job to a follow-up under QuantEcon#933
MAINT: drop unused pytest from wasm env; soften smoke_test docstring
mmcky commentedon Aug 19, 2026
Parking the Emscripten/JupyterLite runner job removed from PR #938 in the split (see the review there for context). Below is that job with the review fixes already folded in, ready to seed this issue's PR — it drops
--with-deps(the browser install hung atapt-getfor the full 6-hour job limit on the one run), addspytest-timeout(the--timeoutflag was previously passed without its plugin), makes the native gate real vianeeds: native, drops the_sitecache (unpinnedquanteconmade the key meaningless and the build re-ran regardless), adds a server-readiness check before Playwright starts, bumpssetup-pythonto v7, and puts upper bounds on the unpinned build tools (the one run resolvedjupyterlite-xeusto 5.0.0, which the--XeusAddon.environment_fileflag has never been exercised against).Development approach: iterate in the PR under a
pull_requesttrigger so each attempt shows red/green, then flip toschedule+workflow_dispatchjust before merge —workflow_dispatchonly works once the workflow file exists on the default branch, and after mergegh workflow run --ref <branch>can still exercise branch versions. Note the job tests the released conda-forgequantecon(not repo source) until the package lands on emscripten-forge-4x, so per-PR triggering isn't useful long-term.The Playwright harness it runs (
ci/wasm/test_jupyterlite.py, reworked to sentinel-driven execution in PR #938) and the kernel env (ci/wasm/environment.yml) are already onmainonce #938 merges. The job to append to.github/workflows/ci_wasm_smoke.yml:Remaining known unknowns for the first iterations: the JupyterLab selectors in
test_jupyterlite.py(launcher card, console prompt, output area) have never executed against a real JupyterLite build, and neither has thejupyterlite-xeus5.x build flag — expect a few rounds. Once the job is green, record the per-function results table in #928.mmcky commentedon Aug 21, 2026
Notes for the runner PR, following #938 and the finding in #927 (#927 (comment)):
test_support_enumerationinci/wasm/smoke_test.pywill hang in the browser (jitted-generator bug, not LAPACK). Guard it so a hang can't eat the job's time limit — either a per-test timeout viapytest-timeout(add it to the kernel env) orxfail(IS_EMSCRIPTEN, strict=True)liketest_gini_coefficient.@njitgenerator test (for i in range(n): yield i) alongside it, so the mechanism is isolated fromsupport_enumerationthe way the barenp.linalg.solvetest isolates LAPACK. That becomes the upstream repro and the regression watch.np.linalg.solvetest should pass on Emscripten (verified on the numba-ecosystem demo), so it can stay unguarded.The parked runner job above already has the review fixes (no
--with-deps,timeout-minutes,needs: native, schedule/dispatch triggers).WASM: JupyterLite environment config and browser smoke suite (#938)
mmcky commentedon Aug 21, 2026
Corrections to my 2026-08-21 note above (#933 (comment)); the details are now in the body's task list.
on:, andci_wasm_smoke.yml(onmainvia WASM: JupyterLite environment config and browser smoke suite #938) triggers onpush/pull_request. The runner PR needsschedule+workflow_dispatchin that sharedon:block plus anif: github.event_name != 'pull_request'guard, or its own workflow file withoutneeds: native. The other three review fixes are in the snippet as stated.xfailstill runs the test, andpytest-timeout(SIGALRM /threading.Timer) cannot interrupt a native loop in the single-threaded kernel. Useskipif(IS_EMSCRIPTEN, ...)orxfail(IS_EMSCRIPTEN, run=False, strict=True)insmoke_test.py; intest_jupyterlite.pyskiptest_support_enumerationor run it last, because the module-scoped kernel stays wedged after Playwright's 180 s timeout fires.test_vertex_enumerationneeds the same guard —vertex_enumeration.py:118is also a@njitgenerator (WASM: support_enumeration and vertex_enumeration hang in the browser — root cause is jitted generators (numba.np.linalg._LAPACK verified working) #927).parallel=Trueto serial and the barenumbapin resolves to it, sotest_gini_coefficient's strict xfail would XPASS andtest_gini_fails_on_emscriptenwould fail. Both become plain pass assertions (inferred from the patch, not yet browser-verified).test_simplex_gridalso belongs on the expected-failure list:RuntimeError: no compiled object yet for comb_jiton a warm cache only (WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944, upstream Numba:RuntimeError: no compiled object yetemscripten-forge/recipes#6309); its comment should cite WASM: cache=True functions that link a cache-restored callee fail with "no compiled object yet" (simplex_grid → num_compositions_jit → comb_jit) #944, not WASM: audit 32-bit intp behaviour on wasm32 (overflow guards, simplex_index wrapping, dtypes) #929.The unguarded bare
np.linalg.solvetest and the minimal@njitgenerator isolation test both stand.mmcky commentedon Sep 9, 2026
Two tasks to add to the runner PR, both found while re-checking #925 against live state on 2026-09-09.
1. The gini xfail will hard-fail the first browser run.
ci/wasm/smoke_test.pymarkstest_gini_coefficientasxfail(IS_EMSCRIPTEN, strict=True)with the reason "@njit(parallel=True)not supported on Emscripten". Two independent changes have falsified that reason: patch 0010 has been in the emscripten-forge recipe since 2026-08-18 and demotesparallel=Trueto the serial pipeline rather than failing, and PR #937 (merged 2026-09-09) removedparallel=Truefromgini_coefficientaltogether. Understrict=Truean unexpected pass is reported as a failure, so this test is now guaranteed to go red the moment the runner executes — for being correct. The existing task list mentions "gini expectations inverted"; this is the specific form it takes. Drop the marker and assert the value plus a timing.2.
coverageis not packaged for WASM. #925 has been claiming that "the test tooling (pytest, pandas, coverage) is itself packaged for WASM". pytest and pandas are emscripten-forge recipes;coverageis not among the 639, and conda-forge'scoverageis notnoarchbecause it builds a C tracer per platform. The browser job should either plan for the pure-Python tracer or accept that it reports no coverage. #925 is corrected.While here: the suite's
test_import_timeis the instrument for #930's unrecorded gate. For a native reference point,import quanteconon 0c7c816 measures 1.85 s cold against 0.65 s with the Numba on-disk cache warm, so roughly 1.2 s is eager compilation — four gufuncs plus the eagerly-signedcomb_jit. In the browser the four gufuncs pay that every session, since patch 0007 force-disablescache=Trueforvectorize/guvectorize. Native seconds are not browser seconds, so treat it as a lower bound rather than the gate.