test-printer-db.py is gone entirely. It started as a re-implementation of PrinterDetector's scoring formula and was later narrowed to invariants that do not model anyone's arithmetic; neither version earned the place it took up. The Makefile target, the suite entry and the docs note go with it. test-env.sh was a file whose whole job was to load two other files. Its contents move into sim-image.sh, which every replica launcher already sources or is one exec away from, so the config and the docker plumbing now arrive together instead of as two sources the caller had to remember in order. sim-install.sh and sim-roundtrip.sh source sim-image.sh directly now, which also means a missing docker is reported before a package is staged rather than after. The environment still wins over both config files -- verified in both directions, an explicit PRINTER_IMAGE beats test.env and an unset one falls through to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.2 KiB
Testing: how we know it does not brick
The suite has two halves, and only one of them can answer the question.
Half 1 — static and packaging. Needs no printer and no proprietary
firmware: a synthetic fixture (test/fixtures/make-stock-fixture.sh)
reproduces the package structure, so shell syntax, bashism and brick-pattern
lint, the packaging pipeline and the model gates all run in CI on a clean
machine.
Half 2 — the printer replica. The real rootfs.squashfs, extracted from
the stock package, chrooted under qemu-mipsel, with /usr/prog installed by
FlashForge's own updater. The installer under test runs on the printer's
busybox, tar, md5sum and unTar — not on Debian stand-ins — with a read-only
root and writable prog/data partitions, exactly like the machine, and the
package reaches it the way it reaches a real printer: on a FAT filesystem that
the machine's own boot script finds and mounts. This is the
half that can catch a brick, it needs the stock package, and
.github/workflows/release.yml refuses to publish without it.
With PROG_DUMP (in test.env) pointed at a factory image, the replica has
essentially nothing invented left: the klipper daemons, nginx, python3,
moonraker and the printer's own OpenSSL 1.0.2d are all genuine, so
package decryption is verified against the real implementation. What remains
substituted is insmod/reboot/cmd_mcu, neutered because they would act on
the host kernel or real hardware, and the partition sizes.
See printer-replica.md for what is authentic, what is stubbed, and what it still cannot tell you.
The tests
make test # everything below
| Test | What it does | Replica |
|---|---|---|
test-lint |
Scans on-printer scripts for brick patterns: raw block-device writes, rm -rf of top-level paths, unguarded rm -rf $VAR/, missing backups, a boot chain that could end with no UI |
|
test-model |
Each package's filename prefix matches the gate inside, the PID matches the model, and the two models ship different firmwareExe binaries |
|
test-install |
End-to-end. The package sits on a real FAT filesystem exposed as /dev/sda1, and the machine's own app_startup.sh runs verbatim through three boots: stick in → it installs; stick still in → it installs again (idempotence); stick pulled → the machine boots with the mod running and the stock ps-watchdog satisfied. Asserts along the way: UI present and executable, boot scripts unmodified and still parsing, Klipper owned by a service, c_helper.so still nan2008 MIPS, user printer.cfg preserved, the wrapper unchanged by a re-install |
✓ |
test-recovery |
Installs the mod, then flashes the stock package, and asserts the machine is genuinely back to stock byte-for-byte and the leftover payload is inert | ✓ |
test-ui |
Drives the UI decision logic on the printer's shell: HelixScreen missing, crash-loop, SAFE-MODE latch and release, and that SAFE-MODE means headless, not a respawn loop | ✓ |
test-applets |
Every command the payload runs exists on the printer. Its busybox has no timeout, no bash, no systemctl; reaching for one is a blank screen at boot |
✓ |
test-ash |
Parses every on-printer script with the printer's own busybox 1.31.1 ash | ✓ |
test-abi |
Every shipped MIPS binary is nan2008/mips32r2/o32 |
The replica tests need make rootfs first, which extracts the printer's
genuine root filesystem from the stock package's kernel-*.tar.xz. It is
never committed — it is FlashForge's firmware. make test skips the replica
half with a loud message when no stock package is configured;
REQUIRE_PRINTER_SIM=1 turns that skip into a failure.
Speed
Almost all of a replica run used to be setup, repeated per test case:
| per run | |
|---|---|
unpack the 182MB factory image into /usr/prog |
22s |
| install the stock package into it, under qemu | 37s |
| the test itself (three boots, install, re-install) | ~30s |
make printer-image does both of those once, at build time, and publishes the
result. test/printer/bake.sh is the part that cannot be a docker build
step — the stock install needs binfmt_misc and chroot, so it runs in a
privileged container and the result is committed. The md5 of the package that
was installed is recorded in /usr/prog/.BASELINE; entrypoint.sh reinstalls
only if a run asks for a different one.
With PRINTER_IMAGE set in test.env, a replica starts in 0.7s and the
whole end-to-end update test takes ~70s. Without it, the same test spends
a minute on setup before it begins. That image contains proprietary FlashForge
firmware.
make test end to end, measured, with the image: 5m26s, and the sections
say where it goes — run-tests.sh stamps every header with elapsed seconds:
== brick-risk lint == [0s]
== build on the fixture == [1s]
== extracting the printer rootfs == [16s]
== applets / ash / ABI / UI safety == [18s]
== end-to-end update on the replica == [111s]
== recovery: a stock package reverts the mod == [250s]
[326s]
What is left is real work: two full package builds (the payload is 55MB
through xz) and two replica runs. If it needs to get faster again,
that is where to look — not in the harness.
What was dropped, and why
Tests that cannot fail are worse than no tests, because they read as coverage:
test-abi's execution half. It wasqemu "$f" -h || [ $? -lt 126 ], which accepts nearly every exit status — it could only fail if qemu itself was missing. Running the binaries for real is what the replica does.test-ash'smd5sum -sprobe. It printed "inconclusive" on every path and could not fail.- The per-file
sh -n"POSIX" loop inrun-tests.sh.shis bash on the build image, so it proved nothing thattest-ashdoes not prove properly with the printer's own busybox. Its bashism grep also flaggedlocal, which busybox ash supports. test-install's hand-written replay ofapp_startup.sh. It re-derived the glob,MACHINEandPIDwithsedand then called the installer itself — so a mistake in our reading of the boot script could never be caught. It now runs the boot script.- The release workflow's final
sim-installloop ignored the exit status of every run, so the last gate before publishing could not fail. It now stops on the first failure and runs withREQUIRE_PRINTER_SIM=1, which turns a skip into a failure too. test-printer-db, all of it. It began as a re-implementation of PrinterDetector's scoring formula, mirroringprinter_detector.cppline for line, so a change to the HelixScreen fork's formula left the test passing while reality moved. Narrowing it to scoring-independent invariants kept it honest but not useful enough to earn its place, and the whole check is gone.- The hand-rolled bashism grep in
run-tests.sh. It knew five constructs;shellcheck -s dashknows the whole SC3xxx family and now stands in its place (the authoritative check is stilltest-ash, on the printer's own busybox). The same pass gave every replica launcher one sharedtest/sim-image.shfor the docker plumbing, which also fixed twomake test-ashbugs: it ran without the docker socket (so it always silently skipped), and it never readtest.env, so it ignoredPRINTER_IMAGEand rebuilt the local sim image every run.
Bugs these caught
Not theoretical. Writing them caught real bugs before any hardware was involved:
- backups were taken after the stock
run.shhad already overwritten the files, so a restore would have restored the modified versions rm -rf $MODDIR/binwith an unsetMODDIRexpands torm -rf /bin- a crashed UI is a zombie, and
kill -0succeeds on a zombie — the crash detector called a dead UI healthy - the packer emitted both models' filenames from one build, which would have handed a Creator 5 the Pro's firmware
- three checks in the brick lint had been quietly dead for months: two were
guarded on a
payload/boot.shthat no longer existed, and the file glob never matchedfirmwareExeor theinit.d/S*scripts at all