mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-10-15 17:19:55 -07:00
The cases that tested a mock, a timer, a wording or a table, or that
replayed a judgment a host test already holds, leave forgetest/tests.
What catches a false PASS, keeps the bench safe, or guards the gate
stays.
- test_baseline: the fake sysfs tree is a mixin with no test_ methods,
so ExtensionFreeTests and BaselineModeTests no longer re-run
BaselineTests' 25 cases (133 runs, now 45). CoolPublishTests, the
lamp-fade cases and TransientNotLeftoverTests go; PositionDeadbandTests
keeps a reading past the band and an unreadable reading, both never
excused. Both latch-relock cases stay.
- test_fixture: MdnsTests, the three timing cases, the probe's log
wording, and nine prompt-routing variants go. A live test never runs
unattended, the arm-press opt-in, the short-press edge, the presence
press handing over the arm press, and the arm notice dropping at the
press stay (35 cases, now 21).
- test_cloud_suite: three false-PASS guards stay: the latch open at a
dark print's button, a lid stop that is not edge-driven, the kernel
refusing the resume. The guards for the latch unlocking in the hold
and a paused print resuming left with their catalog tests in 6a434bb.
The rest go, with the harness pieces and the hunt-lid fixture only
they read (41 cases, now 3).
- test_cooling_suite: GateOffTests, CriticalTierTests, FanGateTests and
FansQuietTests go with their harness; the flow verdict's log reads
stay: only the text after the offset, never an older verdict before it
(the flow-under-load false-PASS guard), a rotated log read whole.
- test_queue: RunClockTests and three ordering cases go, and refusals
are judged by a reason given, not its words; both live
acknowledgment cases stay.
- test_takeover_position: the six cases that judge the head and move
nothing stay.
- test_camera_home_return: the five cases where the head must not move
stay, and so do the three that check the hand-back jog's direction and
distance (homed, unhomed with the counters not counted twice, after
cloud mode): a wrong sign or a double count drives the head into the
gantry stops (15 cases, now 8).
- test_server: the keep-alive and ETag cases, the takeover and scope
half of the bench case, and the page's self-contained asserts
(test_page.py's) go.
- test_operator: the wording cases, the fixture case (test_fixture.py's)
and JournalTests go.
- test_mode: the supervisor timeout table goes.
- test_takeover_client: the listener, the offline service back, the
online client without the hunt, and GRBL under no marker stay.
- test_responsiveness: PageTests and ImplementationCostTests go; the
log cache keeps appends, a partial line, a replaced file and a fresh
list, and the fingerprint cache a changed manifest and one with no
content hash.
- test_bench_registry: GfbenchTests and two description and argument
pins go.
- test_installer: the log-format and TLS-wording cases and
InstallerShapeTests go; the die case checks the reason is in the log,
not its format.
- test_image: KernelIdentTests and the registration pin go.
Each file's __main__ guard now ends the file, and the docstrings say what
each still proves.
Proof: each of the fifteen files passes under Linux as forgetest-ci
discovers it, and pyflakes reports nothing new in them.
Acceptance: none; test-only.
161 lines
6.8 KiB
Python
161 lines
6.8 KiB
Python
# Copyright 2026 514 LLC d/b/a OpenGlow
|
|
# Written by Scott Wiederhold
|
|
# https://community.openglow.org
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""scripts/install-forgefirm.sh - the download that retries, and the log.
|
|
|
|
The installer runs once per machine, on factory firmware, over whatever
|
|
network the owner has. These tests run its own functions under sh with a
|
|
scripted curl in front of them: a transfer that fails resumes and is
|
|
tried again, the failures that waiting cannot fix end the tries at once,
|
|
the reason an install stopped is in the install log, and a log that cannot
|
|
be written never fails the install."""
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
import helpers # noqa: F401 (sys.path)
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
SCRIPT = os.path.join(REPO, "scripts", "install-forgefirm.sh")
|
|
MARK = "# The install log, before anything can fail."
|
|
|
|
# A curl that plays a script: each call takes the next "rc http bytes"
|
|
# line of $CURL_PLAN, appends that many bytes to the --output file the way
|
|
# a resumed transfer does, prints the HTTP code as -w '%{http_code}'
|
|
# would, records its arguments, and exits rc.
|
|
FAKE_CURL = r"""#!/bin/sh
|
|
echo "$*" >> "$CURL_CALLS"
|
|
OUT=""
|
|
while [ $# -gt 0 ]; do
|
|
[ "$1" = "--output" ] && OUT="$2"
|
|
shift
|
|
done
|
|
LINE=$(sed -n 1p "$CURL_PLAN")
|
|
sed -i 1d "$CURL_PLAN"
|
|
set -- $LINE
|
|
[ "${3:-0}" -gt 0 ] && dd if=/dev/zero bs=1 count="$3" 2>/dev/null | tr '\0' 'x' >> "$OUT"
|
|
printf '%s' "$2"
|
|
exit "$1"
|
|
"""
|
|
|
|
|
|
def functions():
|
|
"""The installer up to its first action: constants and functions."""
|
|
with open(SCRIPT, encoding="utf-8") as f:
|
|
text = f.read()
|
|
head, mark, _ = text.partition(MARK)
|
|
assert mark, "the installer lost the line the tests cut it at"
|
|
return head
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("sh") and os.name == "posix", "needs a POSIX sh")
|
|
class InstallerTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = tempfile.mkdtemp(prefix="ffinstall-test.")
|
|
self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
|
|
self.bin = os.path.join(self.tmp, "bin")
|
|
os.mkdir(self.bin)
|
|
for name, body in (("curl", FAKE_CURL), ("nslookup", "#!/bin/sh\nexit 1\n"),
|
|
("ip", "#!/bin/sh\nexit 0\n"), ("sleep", "#!/bin/sh\nexit 0\n")):
|
|
path = os.path.join(self.bin, name)
|
|
with open(path, "w", newline="\n") as f:
|
|
f.write(body)
|
|
os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
|
|
self.fw = os.path.join(self.tmp, "forgefirm.fw")
|
|
self.log = os.path.join(self.tmp, "log", "install.log")
|
|
self.calls = os.path.join(self.tmp, "curl.calls")
|
|
self.plan = os.path.join(self.tmp, "curl.plan")
|
|
|
|
def run_sh(self, body, plan=()):
|
|
with open(self.plan, "w", newline="\n") as f:
|
|
f.write("".join("%s\n" % line for line in plan))
|
|
open(self.calls, "w").close()
|
|
script = "%s\nFW_FILE='%s'\nLOG_DIR='%s'\nLOG_FILE='%s'\nmkdir -p \"$LOG_DIR\"\nLOG_OK=yes\n%s\n" % (
|
|
functions(), self.fw, os.path.dirname(self.log), self.log, body)
|
|
env = dict(os.environ, PATH=self.bin + os.pathsep + os.environ["PATH"],
|
|
CURL_PLAN=self.plan, CURL_CALLS=self.calls)
|
|
return subprocess.run(["sh", "-c", script], env=env, capture_output=True, text=True, timeout=60)
|
|
|
|
def download(self, plan):
|
|
r = self.run_sh('download_fw; echo "rc=$? why=$DL_WHY"', plan)
|
|
with open(self.calls) as f:
|
|
calls = f.read().splitlines()
|
|
return r.stdout, calls, self.log_lines()
|
|
|
|
def log_lines(self):
|
|
try:
|
|
with open(self.log) as f:
|
|
return f.read().splitlines()
|
|
except OSError:
|
|
return []
|
|
|
|
def test_a_clean_download_takes_one_try_and_its_name(self):
|
|
out, calls, log = self.download(["0 200 5000"])
|
|
self.assertIn("rc=0", out)
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertEqual(os.path.getsize(self.fw), 5000)
|
|
self.assertFalse(os.path.exists(self.fw + ".part"))
|
|
self.assertTrue(any("complete on try 1, 5000 bytes" in line for line in log), log)
|
|
|
|
def test_a_failed_transfer_resumes_and_is_tried_again(self):
|
|
out, calls, log = self.download(["6 000 0", "56 200 3000", "0 206 2000"])
|
|
self.assertIn("rc=0", out)
|
|
self.assertEqual(len(calls), 3)
|
|
for c in calls: # every try resumes, bounded in time
|
|
self.assertIn("-C -", c)
|
|
self.assertIn("--connect-timeout", c)
|
|
self.assertIn("--speed-time", c)
|
|
self.assertEqual(os.path.getsize(self.fw), 5000) # 3000 kept, 2000 resumed
|
|
warns = [line for line in log if " WARNING download: try " in line]
|
|
self.assertEqual(len(warns), 2, log)
|
|
self.assertIn("did not resolve", warns[0])
|
|
self.assertIn("dropped mid-transfer (3000 bytes so far)", warns[1])
|
|
self.assertIn("Trying again in 5s (try 2 of 5)", out)
|
|
self.assertIn("Trying again in 15s (try 3 of 5)", out)
|
|
|
|
def test_the_tries_run_out(self):
|
|
out, calls, log = self.download(["28 000 0"] * 5)
|
|
self.assertIn("rc=1", out)
|
|
self.assertIn("timed out or stalled", out)
|
|
self.assertEqual(len(calls), 5)
|
|
self.assertFalse(os.path.exists(self.fw))
|
|
self.assertFalse(os.path.exists(self.fw + ".part"))
|
|
# each failed try leaves what the network looked like
|
|
self.assertEqual(sum("github.com does not resolve" in line for line in log), 5, log)
|
|
|
|
def test_a_missing_release_and_a_full_disk_end_the_tries_at_once(self):
|
|
out, calls, _ = self.download(["22 404 0"])
|
|
self.assertIn("rc=1", out)
|
|
self.assertIn("HTTP 404", out)
|
|
self.assertEqual(len(calls), 1)
|
|
out, calls, _ = self.download(["23 200 100"])
|
|
self.assertIn("rc=1", out)
|
|
self.assertIn("could not be written", out)
|
|
self.assertEqual(len(calls), 1)
|
|
|
|
def test_a_partial_file_the_server_will_not_resume_starts_over(self):
|
|
out, calls, _ = self.download(["56 200 4000", "22 416 0", "0 200 5000"])
|
|
self.assertIn("rc=0", out)
|
|
self.assertEqual(len(calls), 3)
|
|
self.assertEqual(os.path.getsize(self.fw), 5000) # not 9000: the stale part went
|
|
|
|
def test_die_leaves_the_reason_in_the_log(self):
|
|
r = self.run_sh('die "archiving slot 1 failed"; echo not-reached')
|
|
self.assertEqual(r.returncode, 1)
|
|
self.assertNotIn("not-reached", r.stdout)
|
|
self.assertTrue(any("archiving slot 1 failed" in line for line in self.log_lines()))
|
|
|
|
def test_no_writable_log_never_fails_the_install(self):
|
|
r = self.run_sh('LOG_OK=""; log INFO "x"; LOG_OK=yes; LOG_FILE=/nonexistent/dir/x.log; '
|
|
'log INFO "y"; echo "still-running rc=$?"')
|
|
self.assertIn("still-running rc=0", r.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|