mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-22 05:12:16 -05:00
extras/stepper_resonance_tester.py opens with a bare `import numpy as np`; klippy.py:122 walks EVERY config section through a load_object whose importlib.import_module is not wrapped in except ImportError; printer.base.cfg includes FlashForge's printer.vibration.cfg, which declares [stepper_resonance_tester]. Since klippy moved to our own CPython 3.13 that has not been a printer missing resonance testing. It has been a klippy that never reaches ready. So this is not a scientific extra. anvil-klipper depends on anvil-python-numpy the way it depends on cffi, except that the others mean "klippy imports this" and this one means "klippy starts". It also gives back SHAPER_CALIBRATE and TEST_RESONANCES, dead since the same switch, and the VFA calibration behind STEPPER_RESONANCE_FACTORY_CALIBRATE that docs/notes/44-vfa-calibration.md traces end to end. WHAT WAS NOT OBVIOUS, in the order it bit. numpy 2.x builds with meson-python, and meson has never heard of _PYTHON_SYSCONFIGDATA_NAME -- the trick every other python-* recipe here leans on. pkg_mesoncross writes a real cross file instead, naming the $PKG_XW wrappers rather than the raw toolchain: the wrappers carry -EL -mnan=2008, and a cross file that reaches past them builds a tree that passes every gate which only reads symbols and is refused at exec() on the printer. pkg_pywheel grew a PKG_PY_PIP_ARGS branch to pass the file as --config-settings, which is the one thing PEP 517 does standardise. The cross file's `python =` is the BUILD python, and that is not a mistake: meson introspects an interpreter by running it and asking sysconfig, and pkg_pytarget has already made that sysconfig answer for mipsel. The old trick does reach meson, just not by its own mechanism. numpy's meson.build hard-errors below gcc 8.4 and ours is 7.2. The gate is the SciPy Toolchain Roadmap's support statement, not a requirement of the code: this toolchain compiles if constexpr, structured bindings, std::optional, std::string_view, and pocketfft_hdronly.h -- 3635 lines of the heaviest template C++ in the tree, and the half np.fft.rfft needs. Probed before the floor was lowered, and lowered rather than removed, behind a guard that fails loudly if upstream moves the check. 19 extension modules, every one at e_flags 0x70001405/0x70001407, 14MB shipped after pruning tests/, f2py/, stubs and static libs. meson-python is self-hosting, so pkg_buildpython runs meson and ninja just to install the backend -- hence ninja-build in the image and PYPKG_HOST_LIST in dependency order rather than alphabetical. And pip puts meson and cython in work/.py-host/bin, which nothing had ever looked in: without that on PATH the backend fails to install with `meson executable "meson" not found` while pip list shows it right there. THE GATE THAT WOULD HAVE CAUGHT ALL OF IT is qa/replica/test_klippy_extras_import.py: walk the real /usr/data/config include graph, map every section to its extra, import each on the printer's own interpreter. It needs no MCU, because config load happens before klippy:mcu_identify. Nothing asked that question before, which is how two comments in this repo came to assert the module guarded its own import. test_numpy.py asks for numeric answers rather than a module object -- linalg.lstsq specifically, because -Dallow-noblas selects the bundled lapack_lite and _r2_sinusoidal_fit calls it every calibration round. And test_install.py's include check read only ff-*.cfg, so FlashForge's four -- printer.vibration.cfg among them -- were never asked about at all. ff-toolchange.cfg wraps the two search commands in _FF_SHAPER_PREP, as SHAPER_CALIBRATE already was. STEPPER_RESONANCE_FACTORY_CALIBRATE is deliberately NOT wrapped: it is a gcode_macro, not a registered command, and same-named sections merge under RawConfigParser(strict=False) -- a wrapper would not wrap it, it would BE it, with rename_existing pointing at itself. It needs none, since its body is two calls to a command that is wrapped. No SAVE_CONFIG in any macro either; it restarts klippy and stays the operator's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8
437 lines
21 KiB
Python
437 lines
21 KiB
Python
"""What the machine's own installer produced, asked of the machine it produced.
|
|
|
|
The port of `case-install.sh` (579 lines, one bit) -- the gate that decides
|
|
whether a package bricks a printer.
|
|
|
|
WHY THIS MODULE LOOKS SO DIFFERENT FROM THE CASE SCRIPT. Most of that file was
|
|
DRIVING the install: attach the stick, run app_startup.sh, wait for it to
|
|
settle, do it twice more. None of that is here, because the `printer` fixture
|
|
already IS the result -- the package installed by `/usr/prog/app_startup.sh`,
|
|
verbatim, off a genuine FAT filesystem on `/dev/sda1`, exactly as a user
|
|
installs it from a USB stick (see qa/replica/conftest.py). What is left is the
|
|
part that was always the point: the assertions.
|
|
|
|
That is also why nothing here replays the install by hand. The case script's
|
|
own header records that lesson being learned once already:
|
|
|
|
An earlier version of this file replayed app_startup.sh by hand -- which
|
|
meant a bug in our reading of it could never be caught.
|
|
|
|
THREE OF THE CASE'S ASSERTIONS WERE STALE AND ARE NOT REPRODUCED. They are why
|
|
the gate was red, and each is replaced by the question that means the same
|
|
thing today:
|
|
|
|
* `grep helix firmwareExe` -- the wrapper used to exec HelixScreen itself.
|
|
It does not: it starts s6 and the UI is the `ui` service. Replaced by
|
|
test_the_wrapper_starts_the_supervisor and
|
|
test_the_ui_is_a_service_in_the_compiled_database.
|
|
* `[ -d $MODDIR/init.d ]`, else "no service dir exists to start Klipper" --
|
|
there is no init.d. Replaced by test_klipper_is_a_service_in_the_database.
|
|
* grepping the boot log for `S60nginx S62moonraker S70klipper S80ui`.
|
|
NOT replaced by grepping it for the new names. That log line reads
|
|
`s6-rc: bringing up: camera klipper moonraker nginx ui ff-startup wifi`
|
|
and names every service in the boot set INCLUDING THE ONES THAT THEN FAIL,
|
|
so grepping it would pass on a machine where klipper never started -- a
|
|
green assertion that cannot go red. The database is asked instead, which
|
|
is a fact about what was installed rather than an intention.
|
|
|
|
WHAT A REPLICA CANNOT ANSWER, so it is not asked here:
|
|
|
|
* boot 2, the re-install: the bake deletes `/stick.img` once the install has
|
|
landed, so there is no stick to install from a second time. The property
|
|
that matters -- an update keeps what the user edited and drops only what
|
|
the last package shipped -- is asserted directly in test_upgrade.py, which
|
|
drives runFirmwareExe.sh over two payloads.
|
|
* boot 3's UI liveness: `ok-all` is unreachable on a replica (no
|
|
/dev/ttyS4,5,7, no /dev/video*), so "the UI is running and the stock
|
|
watchdog is satisfied" cannot be asked. The static half of it -- that the
|
|
wrapper holds the foreground and keeps its name -- is in test_s6rc.py.
|
|
"""
|
|
import re
|
|
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.replica
|
|
|
|
MODDIR = "/usr/data/anvil"
|
|
FE = "/usr/prog/PROGRAM/software/firmwareExe"
|
|
# The other two stock paths anvil-link-prog.sh owns.
|
|
START = "/usr/prog/klipper/start.sh"
|
|
DAEMON = "/usr/prog/klipper/klipperDaemon"
|
|
APP = "/usr/prog/app_startup.sh"
|
|
SOURCE = MODDIR + "/etc/s6-rc/source"
|
|
DB = MODDIR + "/etc/s6-rc/compiled/current"
|
|
# The klipper s6-rc service execs $FF_PYTHON against $MODDIR/klipper/klippy.
|
|
# /usr/prog/klipper/klippy still holds FlashForge's stock tree; nothing reads it.
|
|
KLIPPY_DIR = MODDIR + "/klipper/klippy"
|
|
INSTALL_LOG = "/usr/data/anvil-install.log"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def box(printer):
|
|
"""The installed machine, checked once for the one thing that would make
|
|
every assertion below meaningless."""
|
|
if not printer.file(MODDIR).is_dir:
|
|
pytest.fail(
|
|
"there is no %s, so the install did not land. The `printer` "
|
|
"fixture bakes an image by running the machine's own "
|
|
"app_startup.sh over a package from work/out -- `make build` "
|
|
"first." % MODDIR)
|
|
return printer
|
|
|
|
|
|
# ------------------------------------------------- the stock boot chain is stock
|
|
|
|
def test_app_startup_is_unmodified(box):
|
|
"""Replacing firmwareExe owns the whole userspace boot, which is what lets
|
|
app_startup.sh, rcS and the init chain stay stock and never be patched. A
|
|
mod marker in here means that claim has quietly stopped being true."""
|
|
text = box.file(APP).text
|
|
assert text, "no %s -- there is no stock baseline on this machine" % APP
|
|
for marker in ("anvil", "MODDIR", "/usr/data/anvil"):
|
|
assert marker not in text, (
|
|
"%s carries the mod marker %r -- the boot scripts must stay stock"
|
|
% (APP, marker))
|
|
|
|
|
|
def test_app_startup_still_parses(box):
|
|
"""Under the printer's own busybox ash, not the host's shell."""
|
|
parsed = box.sh("sh -n %s 2>&1" % APP)
|
|
assert parsed.ok, "BRICK: %s has a syntax error: %s" % (APP, parsed.text)
|
|
|
|
|
|
def test_app_startup_still_loads_the_kernel_modules(box):
|
|
"""The insmod lines are what bring up wifi and the touch panel. An
|
|
installer that trimmed them would leave a printer with no screen."""
|
|
assert "insmod" in box.file(APP).text, (
|
|
"BRICK: no insmod lines left in %s" % APP)
|
|
|
|
|
|
# ------------------------------------------------------------- the wrapper
|
|
|
|
def test_the_wrapper_is_installed(box):
|
|
"""A shebang, not an ELF. The stock binary still sitting here means the
|
|
install did not take -- and that must fail rather than be tolerated,
|
|
because tolerating it turns "the install produced nothing" into a pass."""
|
|
head = box.sh("head -c 2 %s" % FE).out
|
|
assert head == "#!", (
|
|
"BRICK: %s is not the mod wrapper (starts %r) -- the install did not "
|
|
"replace it" % (FE, head))
|
|
|
|
|
|
def test_the_wrapper_is_executable(box):
|
|
assert box.file(FE).executable, "BRICK: %s is not executable" % FE
|
|
|
|
|
|
def test_the_wrapper_parses(box):
|
|
parsed = box.sh("sh -n %s 2>&1" % FE)
|
|
assert parsed.ok, "BRICK: wrapper syntax error: %s" % parsed.text
|
|
|
|
|
|
def test_the_wrapper_starts_the_supervisor(box):
|
|
"""Replaces the case script's `grep helix`. The wrapper no longer execs a
|
|
UI: it starts s6-svscan and brings the boot set up, and the UI is one
|
|
service in that set. Asking for HelixScreen by name here would be asking
|
|
about an architecture that has been gone since phase 8."""
|
|
text = box.file(FE).text
|
|
assert "s6-svscan" in text, (
|
|
"BRICK: the wrapper starts no supervisor -- nothing would bring the "
|
|
"printer up")
|
|
assert "s6-rc" in text, (
|
|
"BRICK: the wrapper never asks s6-rc for a transition, so no service "
|
|
"would start")
|
|
|
|
|
|
def test_the_stock_paths_are_symlinks_into_the_payload(box):
|
|
"""The property the whole seam rests on since the software component
|
|
stopped carrying firmwareExe and start.sh.
|
|
|
|
anvil-core installs all three at $MODDIR/prog/ and anvil-link-prog.sh points
|
|
the stock paths at them -- from runFirmwareExe.sh on a flash and from the
|
|
postinst on `opkg upgrade anvil-core`. A REGULAR FILE at any of these means
|
|
the link step did not run, and the printer would go on executing whatever
|
|
the last install happened to leave there while an upgrade quietly rewrote
|
|
$MODDIR.
|
|
|
|
klipperDaemon is here for a second reason. It was hand-copied by the
|
|
installer until the nice value it carried went away, and the guard on that
|
|
copy named a path the file had stopped shipping at -- so for several
|
|
releases the block was skipped in silence and every printer kept
|
|
FlashForge's own, whose `start` forks a second unsupervised klippy beside
|
|
the s6 one. A link cannot be skipped in silence.
|
|
"""
|
|
for path in (FE, START, DAEMON):
|
|
target = box.sh("readlink %s 2>/dev/null" % path).out.strip()
|
|
assert target, (
|
|
"%s is not a symlink -- anvil-link-prog.sh did not run, so an "
|
|
"`opkg upgrade anvil-core` would not change what this printer "
|
|
"executes" % path)
|
|
assert target.startswith(MODDIR + "/prog/"), (
|
|
"%s -> %r, which is not under %s/prog -- anvil-core is not what "
|
|
"this printer runs" % (path, target, MODDIR))
|
|
assert box.file(path).exists, (
|
|
"%s -> %r dangles: the link is there but the payload is not"
|
|
% (path, target))
|
|
|
|
|
|
def test_the_klipper_daemon_shim_refuses_to_start_a_second_klippy(box):
|
|
"""The whole point of shipping it. Stock's `start` runs start-stop-daemon
|
|
-b, which would put an unsupervised klippy next to the supervised one,
|
|
fighting for /dev/ttyS4 and /tmp/uds. Asked by RUNNING it, because the file
|
|
being in place says nothing about what it does."""
|
|
got = box.sh("%s start 2>&1; echo rc=$?" % DAEMON)
|
|
assert "rc=0" in got.text, (
|
|
"`klipperDaemon start` did not exit 0 -- a caller that treats failure "
|
|
"as fatal would break on it: %s" % got.text)
|
|
assert "supervised" in got.text, (
|
|
"`klipperDaemon start` did not report that klipper is supervised, so "
|
|
"this is probably FlashForge's own: %s" % got.text)
|
|
running = box.pgrep("klippy.py")
|
|
assert len(running) <= 1, (
|
|
"`klipperDaemon start` left %d klippy processes running -- the shim "
|
|
"forked one: %s" % (len(running), [p.cmdline for p in running]))
|
|
|
|
|
|
def test_every_installed_script_parses_under_the_printers_busybox(box):
|
|
"""Every shell script the mod put on the machine, with the shell that will
|
|
actually run it. A bashism here is a service that dies at boot."""
|
|
listed = box.sh(
|
|
"find %s -name '*.sh' -type f 2>/dev/null; "
|
|
"find %s -type f 2>/dev/null" % (MODDIR, SOURCE))
|
|
scripts = [s for s in listed.out.split() if s]
|
|
assert scripts, "found no installed scripts to parse -- the find is wrong"
|
|
|
|
broken = box.sh(
|
|
"for f in %s; do head -c 2 \"$f\" | grep -q '#!' || continue; "
|
|
"sh -n \"$f\" 2>&1 >/dev/null || echo \"BAD $f\"; done"
|
|
% " ".join(scripts))
|
|
assert "BAD" not in broken.text, (
|
|
"syntax errors under the printer's ash:\n%s" % broken.text)
|
|
|
|
|
|
# ------------------------------------------------ what actually starts klipper
|
|
|
|
def test_klipper_is_a_service_in_the_database(box):
|
|
"""Replaces `[ -d $MODDIR/init.d ]`. Asked of the COMPILED database rather
|
|
than of the source tree, because s6-rc-compile is free to reject or drop
|
|
what it was given -- a `down` file in a definition directory is accepted
|
|
and discarded, which is how this bit us once."""
|
|
listed = box.sh("%s/bin/s6-rc-db -c %s list services 2>&1" % (MODDIR, DB))
|
|
assert listed.ok, "could not read the compiled database: %s" % listed.text
|
|
assert "klipper" in listed.out.split(), (
|
|
"nothing starts Klipper -- the UI would boot with no motion and no "
|
|
"heaters. Services in the database: %s" % listed.out.split())
|
|
|
|
|
|
def test_the_ui_is_a_service_in_the_compiled_database(box):
|
|
"""The other half of the retired `grep helix`: the UI still gets started by
|
|
something, it is just the `ui` service now rather than a line in the
|
|
wrapper."""
|
|
listed = box.sh("%s/bin/s6-rc-db -c %s list services 2>&1" % (MODDIR, DB))
|
|
assert "ui" in listed.out.split(), (
|
|
"BRICK: no `ui` service -- the printer would boot with no interface. "
|
|
"Services: %s" % listed.out.split())
|
|
|
|
|
|
def test_klipper_depends_on_the_mcu_bringup(box):
|
|
"""The ordering the whole s6-rc graph exists for: the boards must be out of
|
|
their bootloaders before klippy opens the ports, on EVERY start, which is
|
|
what having it as a dependency buys over calling it once at boot."""
|
|
deps = box.sh("%s/bin/s6-rc-db -c %s dependencies klipper 2>&1"
|
|
% (MODDIR, DB))
|
|
assert deps.ok, "could not read klipper's dependencies: %s" % deps.text
|
|
assert "mcu-bringup" in deps.out.split(), (
|
|
"klipper does not depend on mcu-bringup, so klippy could open the "
|
|
"ports before the boards are handed over. Dependencies: %s"
|
|
% deps.out.split())
|
|
|
|
|
|
# --------------------------------------------------------------- klippy itself
|
|
|
|
def test_klippy_is_present(box):
|
|
"""In the payload, which is the only copy: anvil-klipper installs it and
|
|
the s6 service execs it there. There is no component copy to fall back
|
|
on any more, so this failing means the printer has no Klipper at all."""
|
|
assert box.file(KLIPPY_DIR + "/klippy.py").exists, (
|
|
"%s/klippy.py missing -- anvil-klipper did not install and there is "
|
|
"nothing to start" % KLIPPY_DIR)
|
|
|
|
|
|
# THE CHELPER'S ABI IS NOT ASKED HERE ANY MORE. Two tests used to read its
|
|
# ELF header a byte at a time through `xxd` -- 32-bit, little-endian, MIPS,
|
|
# then e_flags for nan2008 -- for one file out of the several hundred ELF
|
|
# objects an install puts on this machine. qa/replica/test_abi.py sweeps all
|
|
# of them on the same fixture and names c_helper.so explicitly, so what was
|
|
# here is a strict subset of what runs there.
|
|
|
|
|
|
# ------------------------------------------------------- the mod's own config
|
|
|
|
def test_the_config_include_set_is_wired_up(box):
|
|
"""printer.base.cfg must include every ff-*.cfg the package shipped, and
|
|
each one must be there. A shipped-but-unincluded file is a feature that
|
|
silently does nothing."""
|
|
base = box.file("/usr/data/config/printer.base.cfg")
|
|
assert base.exists, "no printer.base.cfg -- the mod's config is not wired up"
|
|
included = set(re.findall(r"\[include\s+(ff-[\w.-]+\.cfg)\]", base.text))
|
|
assert included, "printer.base.cfg includes no ff-*.cfg at all"
|
|
|
|
shipped = set(box.sh("ls %s/config/ff-*.cfg 2>/dev/null" % MODDIR).out.split())
|
|
shipped = {p.rsplit("/", 1)[-1] for p in shipped}
|
|
assert shipped, "the package shipped no ff-*.cfg"
|
|
assert shipped <= included, (
|
|
"shipped but never included: %s" % sorted(shipped - included))
|
|
|
|
missing = [name for name in included
|
|
if not box.file("/usr/data/config/" + name).exists]
|
|
assert not missing, "included but not installed: %s" % missing
|
|
|
|
|
|
def test_every_include_resolves_not_just_ours(box):
|
|
"""The other four.
|
|
|
|
The test above deliberately reads only `ff-*.cfg`, which is the mod's own
|
|
set -- and for a long time that was the whole check, so the FOUR
|
|
FlashForge includes at the top of printer.base.cfg (printer.filament.cfg,
|
|
printer.probe.cfg, printer.mesh.cfg, printer.vibration.cfg) were never
|
|
asked about at all. They are not ours and never will be: they arrive from
|
|
the stock firmware and we merely include them.
|
|
|
|
That is exactly why they need a gate. Klipper treats an [include] that
|
|
matches no file as FATAL, so a missing one is not a degraded printer, it is
|
|
a printer that does not start -- and it would be missing for a reason
|
|
nothing else here would notice, because no package of ours ships it and no
|
|
recipe of ours could.
|
|
|
|
printer.vibration.cfg is the one with teeth: it declares
|
|
[stepper_resonance_tester], which is why anvil-klipper depends on
|
|
anvil-python-numpy at all. See qa/replica/test_klippy_extras_import.py and
|
|
docs/notes/44-vfa-calibration.md.
|
|
"""
|
|
base = box.file("/usr/data/config/printer.base.cfg")
|
|
assert base.exists, "no printer.base.cfg -- the mod's config is not wired up"
|
|
|
|
included = set(re.findall(r"\[include\s+([^\]]+)\]", base.text))
|
|
included = {name.strip() for name in included}
|
|
assert len(included) > 4, (
|
|
"printer.base.cfg declares only %d includes (%s) -- that is fewer than "
|
|
"the four FlashForge ones alone, so this file is not the one the "
|
|
"printer reads" % (len(included), ", ".join(sorted(included))))
|
|
|
|
# `ls` rather than File.exists, because Klipper accepts a glob here and one
|
|
# of ours is a directory of per-model chamber configs.
|
|
missing = []
|
|
for name in sorted(included):
|
|
listing = box.sh("ls -1 /usr/data/config/%s 2>/dev/null" % name)
|
|
if not listing.out.strip():
|
|
missing.append(name)
|
|
assert not missing, (
|
|
"printer.base.cfg includes %s, and no such file is in "
|
|
"/usr/data/config. Klipper treats a missing include as fatal, so this "
|
|
"printer would not start. If these are FlashForge's own configs, the "
|
|
"replica's /usr/data was seeded without them -- see "
|
|
"tools/replica/printer/seed-prog.sh." % missing)
|
|
|
|
|
|
def test_the_user_printer_cfg_was_not_clobbered(box):
|
|
"""The one file on the machine that is the owner's, not ours."""
|
|
cfg = box.file("/usr/data/config/printer.cfg")
|
|
assert cfg.exists, "user printer.cfg was clobbered by the install"
|
|
|
|
|
|
# ------------------------------------------------------------ the install log
|
|
|
|
def test_the_installer_said_it_installed(box):
|
|
"""The anti-vacuity guard. Every assertion above describes a machine the
|
|
installer built; if the installer never ran, they would be describing
|
|
something else entirely and several would still pass."""
|
|
log = box.file(INSTALL_LOG)
|
|
assert log.exists, (
|
|
"no %s -- runFirmwareExe.sh never ran, so nothing here is asserting "
|
|
"against an install" % INSTALL_LOG)
|
|
assert "mod payload installed" in log.text, (
|
|
"the install log does not say the payload was installed:\n%s"
|
|
% log.text[-1500:])
|
|
|
|
|
|
def test_the_installer_left_no_payload_in_the_scratch_directory(box):
|
|
"""/usr/data/update is where app_startup.sh unpacks the package, and where
|
|
runFirmwareExe.sh finds anvil.tar.xz beside itself. MEASURED: the directory
|
|
itself survives the
|
|
install, empty, so its mere existence is not the question the case script
|
|
thought it was asking. What must not survive is its CONTENTS: an ~80MB
|
|
half-unpacked tree on the data partition that a later install could trip
|
|
over."""
|
|
left = box.sh("ls -A /usr/data/update 2>/dev/null").out.split()
|
|
assert not left, (
|
|
"the installer left its unpacked payload behind in /usr/data/update: %s"
|
|
% left)
|
|
|
|
|
|
def test_the_root_password_was_dealt_with_exactly_once(box):
|
|
"""A random root password is generated on the FIRST install only. Doing it
|
|
again on every update would mean a printer whose password changes under its
|
|
owner each time they upgrade."""
|
|
# `+`-prefixed lines are skipped because the software component's run.sh
|
|
# still runs under FlashForge's `set -x` and its trace lands in the same
|
|
# log. None of OUR echoes can appear that way any more -- the installer is
|
|
# its own file and is not spliced into anything -- but a trace line that
|
|
# merely quotes the string would still be counted, and this stays cheap.
|
|
log = box.file(INSTALL_LOG).text
|
|
generated = len([ln for ln in log.splitlines()
|
|
if "root password set (random" in ln
|
|
and not ln.lstrip().startswith("+")])
|
|
assert generated <= 1, (
|
|
"the installer generated a root password %d times -- an update must "
|
|
"preserve it, not reroll it" % generated)
|
|
|
|
|
|
# ------------------------------------- no stick, no update (the case's boot 3)
|
|
|
|
def test_a_boot_with_no_stick_does_not_try_to_update(box):
|
|
"""case-install.sh's third boot. The bake removes /stick.img once the
|
|
install has landed, so this machine IS the "stick pulled" case:
|
|
app_startup.sh must go straight to a normal boot rather than hunting for a
|
|
package that is no longer there.
|
|
|
|
`find update file` is app_startup.sh's own marker for entering the update
|
|
block -- the same string the case script keyed on, rather than a fuzzy
|
|
search for the word "update", which appears in a healthy boot too.
|
|
|
|
firmwareExe is neutered for the run, because the real one holds the
|
|
foreground forever by design. What is under test is the update block's
|
|
decision, not the boot that follows it.
|
|
|
|
The stub goes in by MOVING THE SYMLINK ASIDE, not by writing to $FE: that
|
|
path is a link into $MODDIR/prog now, so a redirection would write through
|
|
it and leave the packaged wrapper stubbed for whatever runs next. `mv`
|
|
moves the link itself, and moving it back restores it exactly.
|
|
"""
|
|
box.sh("mv -f %s /tmp/fe.link && printf '#!/bin/sh\\nexit 0\\n' > %s "
|
|
"&& chmod +x %s" % (FE, FE, FE))
|
|
try:
|
|
box.sh("sh %s > /tmp/boot-nostick.log 2>&1; echo rc=$?" % APP,
|
|
timeout=300)
|
|
log = box.file("/tmp/boot-nostick.log").text
|
|
finally:
|
|
box.sh("rm -f %s && mv -f /tmp/fe.link %s" % (FE, FE))
|
|
|
|
assert log.strip(), "app_startup.sh produced no output at all"
|
|
assert "find update file" not in log, (
|
|
"app_startup.sh entered the update block with no stick present:\n%s"
|
|
% log[-1500:])
|
|
|
|
|
|
def test_a_boot_with_no_stick_hits_no_missing_commands(box):
|
|
"""The boot log from the run above. A `not found` here is a command the
|
|
payload assumed and the printer has not got -- which on a real machine is
|
|
a step of the boot silently not happening."""
|
|
log = box.file("/tmp/boot-nostick.log").text
|
|
assert log.strip(), (
|
|
"no boot log -- test_a_boot_with_no_stick_does_not_try_to_update must "
|
|
"run first; it is the one that produces this")
|
|
bad = [ln for ln in log.splitlines()
|
|
if "not found" in ln or "Syntax error" in ln]
|
|
assert not bad, "missing commands or syntax errors in the boot:\n%s" % (
|
|
"\n".join(bad[:5]))
|