The install test used to re-implement app_startup.sh: it pulled the glob,
MACHINE and PID out of it with sed, then called the installer itself. A
mistake in our reading of the boot script could never be caught that way.
The package now sits on a real FAT filesystem on a loop device exposed as
/dev/sda1, and app_startup.sh runs verbatim through three boots -- stick in,
stick still in, stick pulled. The last one asserts what actually matters:
nginx and moonraker up, klipper launched, the UI in the foreground where
app_startup.sh's own 5-second ps watchdog can see it.
One substitution, declared in .SIMULATED: the printer's kernel has nls_cp936
and a container kernel does not, so /bin/mount retries without codepage= if
the real options are rejected.
Speed. The image carried the partitions but not the stock install, and
test-env.sh sourced the config files after the environment -- so PRINTER_IMAGE
on the command line was overwritten by the empty value in test.env and the
published image was never used. Measured, per replica case: 22s to unpack the
factory image, 37s to install the stock package, 0.7s with the image. The
stock install is now baked in at image build time (bake.sh -- it needs binfmt
and chroot, so it runs privileged and the result is committed) and recorded in
/usr/prog/.BASELINE; a run asking for a different package reinstalls.
make test: ~20m -> 5m26s. Every section header now carries elapsed seconds.
Dropped four checks that could not fail:
* test-abi's execution half was `qemu "$f" -h || [ $? -lt 126 ]`, which
accepts nearly every exit status
* test-ash's md5sum -s probe printed "inconclusive" on every path
* run-tests.sh's per-file `sh -n` "POSIX" loop ran bash, and its bashism
grep flagged `local`, which busybox ash supports
* CI's shellcheck ended in `|| true`
And one that mattered more: the release workflow's final sim-install loop
ignored every exit status, and sim-install.sh exits 0 on a skip -- so the last
gate before publishing could not fail. It now stops on the first failure and
runs with REQUIRE_PRINTER_SIM=1, which turns skips into failures throughout.
Also: the profile lists in run-tests.sh, RECOVERY_PROFILE and release.yml
still said ssh/web/full/helix and would have failed the release job; work/.sim
was shared, so two suites at once truncated each other's packages; and the
fixture half left a 380KB package in work/out where make test-install picks it
up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgNhSa1Qvwvxb2nQXT6b1Y
7.4 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, firmwareExe.stock never overwritten |
✓ |
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 selection logic on the printer's shell: helix missing, crash-loop, SAFE-MODE latch and release, and the no-UI-at-all case | ✓ |
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]
== profile: probe / default (on the fixture) == [1s]
== extracting the printer rootfs == [16s]
== applets / ash / ABI / UI fallback == [18s]
== end-to-end update on the replica: probe == [28s]
== end-to-end update on the replica: default == [111s]
== recovery: a stock package reverts the mod == [250s]
[326s]
What is left is real work: two full package builds (the default payload is
55MB through xz) and three 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.
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