Build & Test¶
For contributors and embedded/systems developers. The Makefile drives the build, the test pyramid, the metrics tool, and the docs site. Embedded/systems developers wanting to ship a feature-tailored binary will care about the Build section + the per-feature flags (Getting Started: Embedded). Contributors will care about everything.
Makefile reference¶
make help produces this listing dynamically. The targets, by category:
Build¶
| Target | What |
|---|---|
make build |
Debug build (zig build -Dconfig=nano). Default. ~3.5 MB binary, full DWARF, no inlining. |
make build-release |
ReleaseFast (-Doptimize=ReleaseFast). ~994 KB, full inlining + optimization. |
make build-small |
ReleaseSmall (-Doptimize=ReleaseSmall). ~436 KB. The native shipping size. |
make build-frozen |
ReleaseFast + frozen-tiny stdlib bundled: what make web builds against. |
make build-release-frozen |
Same, but ReleaseSmall. Used by the metrics tool. |
make rebuild |
clean + build. Use after a config change. |
make port |
Regenerate vendor/micropython_embed/ from sandbox/micropython via the upstream embed.mk. Automatic prerequisite of build. |
make wasm |
zig build -Dtarget=wasm32-wasi -Dconfig=nano. The raw wasm artifact. |
make web |
make wasm + wasm-opt -Oz (when binaryen is installed). Produces web/nanopython.wasm, the shipping artifact, 270 KB. |
Set CONFIG=full on any of these to select the everything-on config arm. Per-feature -Dfeature-*=on/off flags (ADR 002) compose freely with -Dconfig.
Test¶
The pyramid runs fast-first: unit → integration → e2e.
| Target | What | Result expected |
|---|---|---|
make test |
Native-only test pyramid (unit + integration + e2e) | All pass |
make test-unit |
Zig unit tests for the compiler + VM tracks | pass |
make test-unit-compiler |
Compiler-track tests only | pass |
make test-unit-vm |
VM-track tests only | pass |
make test-integration |
tests/integration.py smoke suite, native |
37/37 |
make test-integration-wasm |
Same suite, run via wasmtime | 37/37 |
make test-e2e |
Corpus + compile-exec-test | 541/541 + 540/542 |
make check |
Strict invariant gate (native corpus) | 541/541 byte-identical |
make verify |
Detailed per-test PASS/FAIL list | exits 0 |
make wasm-check |
Wasm gate against the 68-entry baseline | GREEN at 472/541 raw |
make (the aggregate) |
All four gates, both arms, plus lint | exit 0 in ~37s |
Metrics¶
Longitudinal recording of size + coverage + (opt-in) perf.
| Target | What |
|---|---|
make metrics-record |
Append a row to metrics/history.ndjson capturing the current binary sizes + corpus pass counts + golden-bucket sizes |
make metrics-record-perf |
Same, plus run the perf benchmarks (p2w subset) and append geomean ratios |
make metrics-report |
Print recent rows as a trend table |
make metrics-latest |
Just print the most-recent row |
Coverage¶
Requires kcov (brew install kcov on macOS, apt-get install kcov on Linux):
| Target | What | Report at |
|---|---|---|
make test-coverage |
Unit-test coverage, merged | coverage/unit/merged/index.html |
make test-coverage-compiler |
Compiler-track only | coverage/unit/compiler/merged/index.html |
make test-coverage-vm |
VM-track only | coverage/unit/vm/merged/index.html |
make test-coverage-e2e |
Corpus coverage (the meaningful number) | coverage/e2e/merged/index.html |
Compatibility surface¶
| Target | What |
|---|---|
make compatibility |
Regenerate docs/COMPATIBILITY.md from the current golden-programs/ bucket assignments |
make golden |
Rebootstrap the golden-programs/ buckets (slow: runs every program through three oracles) |
Browser smoke¶
The tools/browser-smoke.py harness drives the web demos under headless Chromium. Use it for catching DOM-side regressions that the corpus + integration suite can't see.
| Target | What |
|---|---|
make smoke |
Drive the standard demo set, asserting key text + no errors |
make smoke-setup |
One-time: pip install playwright + download chromium (~150 MB) |
See tools/browser-smoke.py for the full step primitives (click, type, select, eval, assert-text, screenshot, etc.).
Quality¶
| Target | What |
|---|---|
make lint |
zig fmt --check over our Zig + py_compile over tools/*.py |
make format |
Apply zig fmt to our sources (never touches vendor/ or corpus/) |
make clean |
Drop .zig-cache, zig-out, coverage/. vendor/ stays. |
make freeze |
DANGER. Refreeze corpus goldens from current binary. Only after a deliberate config change. |
The strict invariant: make check¶
$ make check
zig build -Dconfig=nano
python3 tools/progress.py --config nano ./zig-out/bin/nanopython
nanopython progress: 100.0% [########################################]
passing 541/541 corpus tests (byte-identical to the frozen reference)
command: ./zig-out/bin/nanopython
This is the gate every commit must pass.
- Numerator: tests whose stdout+stderr+exit-status matches the frozen oracle byte-for-byte.
- Denominator: tests in
corpus/nano/basics/minus those inEXCLUDED.txt. - Exit status:
make checkreports the count but always exits 0.make verifyis the strict variant that exits non-zero on any miss.
If make verify reports any miss, you broke something. Identify the failing test and either fix it or revert. The 541/541 baseline is the long-standing strangler invariant; it's been at this level since the 2026-06-11 session closed builtin_help + gc1 + string_strip.
The wasm gate: make wasm-check¶
$ make wasm-check
nanopython wasm progress: 87.2% [###################################]
passing 472/541 corpus tests
GREEN — baseline holds: 68 known fails matched exactly
This gate is strict in both directions:
- A new fail not on corpus/wasm-known-fails.txt → exits non-zero (you regressed something on wasm).
- A test on the known-fails list that now passes → exits non-zero (you fixed something; remove the entry).
The list is documented in-place: each entry has a comment explaining why it's there (bignum needing MPZ, eval/exec hitting the longjmp wall, the two deliberate deque_* divergences).
The secondary differential gate: compile-exec-test¶
$ python3 tools/compile-exec-test.py
compile-exec-test: 540/542 pass, 2 fail, 0 compile-fail, 0 timeout, 0 runner-err
For each .py source, this compiles with our zigpy-compile (Zig frontend), then runs the resulting .mpy through the upstream MicroPython unix binary, and diffs stdout/stderr/exit-status against the frozen corpus/full/basics/*.out. Tests execution equivalence; catches cases where our compiler emits subtly-wrong bytecode that the byte-exact gate misses.
The "2 fail" are harness artifacts (mixed-sign bit-or constant folding), documented in the test runner's source.
Optimization mode comparison¶
| Mode | Make target | Size (arm64) | Use case |
|---|---|---|---|
Debug |
make build |
3.5 MB | Development. Full DWARF, no DCE, runtime panics with stack traces. |
ReleaseFast |
make build-release |
994 KB | Performance. Full inlining, all optimizations. Use for benchmarks. |
ReleaseSmall |
make build-small |
436 KB | Shipping. Size-optimized. |
ReleaseSafe |
(none) | ~1.2 MB | Same optimization as Fast, keeps runtime safety checks. |
For comparison, upstream MicroPython unix-standard (arm64, their -Os default): 652 KB.
The wasm artifact: 270 KB raw post-wasm-opt -Oz (see ADR 007 for the size arc and remaining targets).
CI strategy¶
No CI is wired into the public-facing pipeline yet. Local-only quality gates:
- Before commit:
make. - Before a tagged release:
makeon a fresh checkout (make clean && make) + a manualmake smokeagainst the bundled web demos.
A future CI would run make on every PR. make verify and make wasm-check produce clean signals: diff the pass/fail list against main to localize any regression.
Adding your own tests¶
A Zig unit test¶
Add a test "..." { ... } block inline in any module under src/. Re-run make test-unit-compiler (for compiler-track files) or make test-unit-vm (for runtime-side files).
test "my_helper returns the right thing" {
const std = @import("std");
const testing = std.testing;
try testing.expectEqual(@as(usize, 42), myHelper());
}
An integration smoke test¶
Edit tests/integration.py. Each entry is (name, py_source, expected_stdout) or (name, py_source, expected_stdout, expected_exit):
("my_test_case",
"x = 5\nprint(x * 2)",
"10\n"),
("my_exits_with_code",
"import sys; sys.exit(7)",
"",
7),
Run with make test-integration (native) or make test-integration-wasm (wasm). The output of running py_source via the chosen binary is compared to expected_stdout.
Every bug fix lands with at least one regression pin here. If a wasm-only bug slipped past the corpus, that's the right place to plug the gap.
A corpus test¶
Corpus tests come from sandbox/micropython/tests/basics/*.py. Adding one is non-trivial: the goldens have to come from a known-good reference. See tools/corpus.py for the freeze logic; in practice, new corpus tests follow upstream MicroPython's test additions.
A golden program¶
Drop a .py file into the appropriate golden-programs/<bucket>/ directory with the per-program header comment:
# golden: source=mysource; needs=heapq,recursion
# golden: works-on=micropython,nanopython-native,nanopython-wasm
"""What this program demonstrates."""
Then make golden rebootstraps the buckets and re-verifies the annotations against three oracles (MicroPython, nanopython native, nanopython wasm). See ADR 008 for the bucket semantics.