Files
firmware/qa/conftest.py
Oleksandr ShyshatskyiandClaude Opus 5 b98552386b No FlashForge component ships; patch.sh is payload.sh
The software component existed to give the mod one exec hook. It is not needed
for that any more, so a release now carries our installer and our payload and
nothing else: 26MB smaller, 21MB of which was FlashForge's own firmwareExe
travelling back to the partition it was already on, along with their klippy
tree, their wifi module and their configs.

WHAT IT GIVES BACK. /usr/prog/PROGRAM/software is no longer rewritten, so the
version directory there stays FlashForge's with their genuine firmwareExe
inside it -- and app_startup.sh's five-second watchdog, which copies one out of
that directory when nothing by that name is running, has something to copy
again. A printer whose payload fails to install now comes up on the stock UI
instead of a blank screen. That recovery has been broken since we started
deleting their binary out of a component we shipped; docs/how-it-works.md
claimed it worked throughout.

THE ROOT PASSWORD moves with it, because it used to ride in the component's
shadow file. bin/pack.sh bakes ROOT_PW_HASH into the installer, single-quoted
-- an unquoted $6$... would have the printer's shell expand $6 as a positional
parameter and ship a truncated hash -- and the installer writes it to
/usr/prog/etc/shadow. The random-password path asks one question instead of
four: FlashForge ship the same root hash on every machine, so a live shadow
still carrying it means nobody has set a password and it is ours to set. The
.prev-root-hash recording, the shipped-vs-live comparison and the restore-it-
afterwards step were all machinery for surviving a file that nothing overwrites
any more, and are gone. pack.sh checks the hardcoded constant against the stock
package it built from, because getting it wrong is silent and total: every
printer would read as "already set" and none would ever get a password.

bin/patch.sh is bin/payload.sh. It patched nothing any more -- no splice, no
shadow edit, no firmwareExe deletion -- and what is left is the payload: the
feed installed by the printer's own opkg, the seeded config, the manifest. Its
sections are renumbered 1-5 with no gaps, and the five tombstone comments
marking where removed sections used to be are gone with them.

test_recipe_layout.py loses six of its nine tests. Each compared the tree
against a description of the tree -- no fourth directory kind, each kind still
used, 3rdparty carries no files of ours, installer/ holds exactly two named
files -- and none could go red for a reason a printer would notice, while all
six went red the moment the layout legitimately changed. The two that bite are
kept: a payload/ path escaping $MODDIR, and a recipe name at both levels, which
makes pkg_dir silently build the wrong package.

SW_VER is deleted from config.env.example and both workflows: it stamped a
component that is not built.

Gates: 112 static tests pass. The generated installer is verified after
substitution -- all four lines present, and `sh -n` over the result, because a
hash with a stray character in it would otherwise reach a printer as a syntax
error whose only symptom is a package that installs nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CH7Yb4wuPhnppHAB25UX7e
2026-08-30 15:38:25 +03:00

108 lines
4.9 KiB
Python

"""The qa suite: pytest is the only framework.
This is the only suite. It decides whether a package bricks a printer. The
harness that used to stand beside it, and the host-side test/ tree it wrapped,
are both gone -- see docs/qa-migration.md.
WHAT IS DIFFERENT HERE
The suite this replaced ran one `docker run --privileged` per case script and
got one boolean back, because the assertions lived in POSIX sh inside the qemu
chroot. 6,280 lines of case-*.sh reported 13 bits between them. The rule here
is:
shell inside the chroot performs actions; python on the host asserts.
Shell in qemu is irreplaceable for DOING things -- the printer's busybox, its
tar, its OpenSSL, and the fact that our scripts survive its ash is a large part
of what the suite proves. It is a bad place to JUDGE things. Where a fact is
only observable inside the replica, qa/lib/replica.py exposes a typed probe for
it; a case script never branches on it.
LANES
static needs nothing but the checkout and the lint tools. milliseconds.
replica needs docker + qemu + the proprietary firmware. seconds to
minutes. The gates that decide whether a package bricks.
Select with `-m static` / `-m replica`, or by path.
SKIPS, AND WHY THERE IS NO FLAG CONTROLLING THEM
A missing tool, a missing daemon or a missing firmware image is a FAILURE, at
the point that needs it. Not a skip, and not a skip that some flag can be asked
to promote later.
This suite had a `--strict-skip=<lane|all>` for exactly one commit, and it was
the wrong shape. It was ALLOW_SKIP inverted -- and ALLOW_SKIP's defect was
never which direction it named things in, it was that THE DECISION LIVED IN THE
CI INVOCATION. An accept-list in ci.yml has to be edited whenever a gate moves;
a strict-list in ci.yml has to be remembered whenever a job is added. Both
degrade silently when someone gets the command line wrong, which is the same
disease as the printer-sim job that was gated on an unset secret and therefore
never ran once in its entire existence.
So the verdict lives with the knowledge. `shellcheck` missing means the machine
cannot run the check, and no configuration exists in which that is a complete
run, so the test fails and says how to fix it. Same for pyflakes, for the docker
CLI, for the daemon, and for the replica image.
A GATE THAT DID NOT RUN MUST NEVER LOOK GREEN -- and the way to guarantee
that is to not have a mode in which it can.
`pytest.skip` stays available for the case it is actually for: a question that
does not APPLY to this configuration, as opposed to one this machine happens to
be unequipped to ask. A test that only means something on a Creator5Pro is the
shape that earns it. "I could not find the tool" is not.
Three skips do not meet that bar: test_ipk.py's payload questions read
work/modpayload-root, which only bin/payload.sh produces. Noted rather than
hidden -- and note that `make test` DELETES that directory when it clears up
after its fixture build, so running the old suite quietly costs the static
lane three tests. See docs/qa-migration.md.
"""
import os
import sys
import pytest
# qa/ on the path so `from lib.replica import Printer` works from any subdir.
# Not a package install: this tree is not shipped and never will be, and a
# setup.py for it would be one more thing that can disagree with reality.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
LANES = ("static", "replica")
def pytest_configure(config):
for lane in LANES:
config.addinivalue_line(
"markers", "%s: the %s lane -- see qa/conftest.py" % (lane, lane))
_check_distribution(config)
def _check_distribution(config):
"""Refuse the xdist mode that would start a container per worker.
The `printer` fixture is module-scoped, and xdist's DEFAULT distribution
(`--dist load`) hands out individual tests. Four workers pulling tests
from one module each instantiate that module's fixture, so `-n 4` would
assemble four replicas of the same machine instead of one -- four binfmt
registrations, four mount layouts, and on a locally built replica four
stock installs under qemu. It would still pass, slowly, which is why it
needs catching here rather than in review: the symptom is a suite that
got mysteriously slower when it was parallelised.
`loadscope` groups by module (`loadfile` by file, `loadgroup` by an
explicit mark) -- any of those keeps one container per module, which is
the whole isolation model. Enforced rather than defaulted, because
silently rewriting somebody's -n flag is its own kind of surprise.
"""
dist = getattr(config.option, "dist", "no")
if dist in ("no", "loadscope", "loadfile", "loadgroup"):
return
raise pytest.UsageError(
"--dist=%s would start one replica per worker per module. Use "
"--dist=loadscope (or loadfile), which keeps each module on one "
"worker -- see _check_distribution in qa/conftest.py." % dist)