mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
bench: every board-runnable tool ported to the bench page
The remaining bench diagnostics run from forgetest's #bench tab. The tools that also run from a LAN host share scripts/bench/gfbench.py: GF_HOST names a remote machine (host mode, sysfs through ssh, Grbl and forgectrl over the LAN); unset, the tool runs on the board itself (local mode, sysfs directly, everything on 127.0.0.1), which is how the page runs them - with GF_HOST=127.0.0.1, the panel token in GF_TOKEN and their data files under <data>/bench/ (FORGETEST_BENCH_DATA). The helper also reads a machine setting from forgectrl, or from the settings file on the board while forgectrl is stopped. Ported: pwm_sweep / pwm_hold (scope = a takeover; the latch relocked, the write refused if FIRE or LASER_ON reads active), pwm_stream_test (PASS/FAIL exit), flow_characterize, flow_recheck_char, flow_warm_validate and flow_matrix (takeovers: forgectrl owns the thermal hardware, so the page's takeover replaces the tools' own controller stop/restart, whose command line predated the supervisor; results and logs in the bench data directory), flow_sustained, fan_test, temp_calibrate (dry; watch bounded in seconds; the threshold and the coolant conversion from the shared code), flow_escalate_drill (cool_confirm_max_s shortened through forgectrl's settings for the drill and restored; the setting's minimum is the default budget), and live_fire_drills (<drill> [S] [F], all six drills, host from GF_HOST, token from the board). flow_matrix joins the registry. What stays unported cannot run against the machine at all: the two null-sink CI harnesses and the .puls decoder. Runner: a scope tool runs inside the takeover wrapper; the bench environment above is passed to every tool. Tests: test_bench_registry (registry <-> scripts/bench consistency, every ported tool builds its command line, every script compiles, gfbench host/local modes) and the server test (scope tool takeover, the environment reaching the tool). Local mode smoke-run on the bench (temp_calibrate watch, setting, token) from /tmp, removed after. No catalog consequence: bench tools are not image components (dev-only forgetest); the acceptance catalog is unchanged.
This commit is contained in:
@@ -29,6 +29,7 @@ pins; the coverage lint is `python3 -m forgetest.coverage --manifest ...`.
|
||||
| `FORGETEST_MANIFEST` | `/etc/forgefirm-manifest.json` | the image manifest |
|
||||
| `FORGETEST_PORT`, `FORGETEST_HOST` | 8090, 0.0.0.0 | listener |
|
||||
| `FORGETEST_BENCH_DIR` | `/usr/share/forgetest/bench` | the installed bench scripts |
|
||||
| `FORGETEST_BENCH_DATA` | `<FORGETEST_DATA>/bench` | passed to bench tools: where they keep their data files (with `GF_HOST=127.0.0.1` and the panel token in `GF_TOKEN`) |
|
||||
| `FORGETEST_MARKER` | `/run/forgetest.active` | takeover marker |
|
||||
| `FORGECTRL_URL`, `FORGECTRL_TOKEN_FILE` | `http://127.0.0.1:8080`, `/data/forgefirm/panel.token` | forgectrl client |
|
||||
| `GF_SYSFS_ROOT` | `/sys/glowforge/` | kernel module sysfs |
|
||||
|
||||
@@ -12,10 +12,14 @@ so the catalog of what exists is complete, with Start disabled.
|
||||
Safety classes:
|
||||
dry reads or dry motion, no emission, forgectrl stays up
|
||||
takeover needs forgectrl stopped and the pulse device free
|
||||
scope a takeover whose result only means something with the named
|
||||
instrument on the bench (a scope on LASER_PWM / LASER_ON)
|
||||
live laser emission possible (operator acknowledgment required)
|
||||
scope needs bench instrumentation on top of the class before it
|
||||
|
||||
Bench runs are recorded in <data>/bench.jsonl and never enter a campaign.
|
||||
The tools that also run from a LAN host (gfbench.py: GF_HOST) run on the
|
||||
board here with GF_HOST=127.0.0.1, the panel token in GF_TOKEN, and
|
||||
their data files under FORGETEST_BENCH_DATA (<data>/bench/). Bench runs
|
||||
are recorded in <data>/bench.jsonl and never enter a campaign.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -69,20 +73,23 @@ TOOLS = [
|
||||
"desc": "Creeps toward a rail in bounded jog segments, detects the contact jolt, jog-cancels and backs off."},
|
||||
# -- board-side, takeover / scope ------------------------------------------
|
||||
{"id": "pwm-sweep", "title": "LASER_PWM scope sweep", "script": "pwm_sweep.py",
|
||||
"safety": "scope", "where": "board", "ported": False,
|
||||
"safety": "scope", "where": "board", "ported": True,
|
||||
"args": [_arg("mode", "choice", "check", "check = read-only, sweep = duty staircase", ["check", "sweep"])],
|
||||
"desc": "check: readbacks + PWM2 dump; sweep: PWMSAR through 50/25/75/6/100 percent with 4 s holds. Locked state only."},
|
||||
"desc": "check: readbacks + PWM2 dump; sweep: PWMSAR through 50/25/75/6/100 percent with 4 s holds. "
|
||||
"Locked state (the takeover): latch relocked, refuses if FIRE or LASER_ON reads active."},
|
||||
{"id": "pwm-hold", "title": "LASER_PWM scope hold", "script": "pwm_hold.py",
|
||||
"safety": "scope", "where": "board", "ported": False,
|
||||
"safety": "scope", "where": "board", "ported": True,
|
||||
"args": [_arg("sar", "int", 64, "PWMSAR value"), _arg("seconds", "int", 10, "hold time")],
|
||||
"desc": "Holds one PWMSAR value for a scope window, then restores. Locked state only."},
|
||||
"desc": "Holds one PWMSAR value for a scope window, then restores. Locked state (the takeover): "
|
||||
"latch relocked, refuses if FIRE or LASER_ON reads active."},
|
||||
{"id": "fire-test", "title": "FIRE drop-timing test (A/B/U)", "script": "fire_test.py",
|
||||
"safety": "takeover", "where": "board", "ported": True,
|
||||
"args": [_arg("mode", "choice", "A", "A latch locked, B unlocked/unarmed, U true underrun", ["A", "B", "U"])],
|
||||
"desc": "Duty 0 throughout; refuses to unlock if HV reports good. Software witnesses + the PSU-connector LASER_ON scope point."},
|
||||
{"id": "pwm-stream", "title": "LASER_PWM stream-path test", "script": "pwm_stream_test.py",
|
||||
"safety": "takeover", "where": "board", "ported": False, "args": [],
|
||||
"desc": "Streams power bytes only (no steps, no FIRE, motor_lock=15, latch locked) through /dev/glowforge."},
|
||||
"safety": "takeover", "where": "board", "ported": True, "args": [],
|
||||
"desc": "Streams power bytes only (no steps, no FIRE, motor_lock=15, latch locked) through /dev/glowforge; "
|
||||
"PASS = counters unmoved, idle at the end, no FIRE/emission read back. The scope on LASER_PWM sees the duty steps."},
|
||||
{"id": "gate-a-kernel", "title": "Kernel laser-safety drills K1/K2/K3", "script": "gate_a_kernel_drills.py",
|
||||
"safety": "takeover", "where": "board", "ported": True,
|
||||
"args": [_arg("drill", "choice", "K1", "K1 stop floor, K2 resume honors latch, K3 mid-ramp unlock", ["K1", "K2", "K3"])],
|
||||
@@ -97,51 +104,78 @@ TOOLS = [
|
||||
"safety": "dry", "where": "board", "ported": True, "args": [],
|
||||
"desc": "One M8 session walks the verdict state machine through real pump-off transients; PASS/FAIL per transition."},
|
||||
{"id": "flow-escalate", "title": "Coolant starved re-check escalation drill", "script": "flow_escalate_drill.py",
|
||||
"safety": "dry", "where": "board", "ported": False, "args": [],
|
||||
"desc": "With the pump off the job-start check reads SUSPECT and the driver must escalate to FAULT."},
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("budget_s", "int", 60, "cool_confirm_max_s for the drill (60-3600), restored after")],
|
||||
"desc": "With the pump off the job-start check reads SUSPECT and the engine must escalate to FAULT when "
|
||||
"the confirmation budget expires; the budget setting is shortened for the drill and restored."},
|
||||
{"id": "flow-characterize", "title": "Coolant flow characterization", "script": "flow_characterize.py",
|
||||
"safety": "dry", "where": "host", "ported": False,
|
||||
"safety": "takeover", "where": "board", "ported": True,
|
||||
"args": [_arg("duty", "int", 30, "heater duty percent")],
|
||||
"desc": "Baseline -> flow -> no-flow -> recovery with the factory temperature curve; aborts past 45 C downstream."},
|
||||
"desc": "Baseline -> flow -> no-flow -> recovery with the factory temperature curve; aborts past 45 C "
|
||||
"downstream. Drives the heater and pump directly (about 9 minutes)."},
|
||||
{"id": "flow-sustained", "title": "Coolant sustained re-check run", "script": "flow_sustained.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("minutes", "float", 30.0, "how long to hold M8")],
|
||||
"desc": "Long run of the real re-check cadence via M8: verdicts, false faults, loop heat accumulation."},
|
||||
{"id": "flow-warm", "title": "Coolant warm-baseline validation", "script": "flow_warm_validate.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "Runs the real check from a heater-warmed baseline."},
|
||||
"safety": "takeover", "where": "board", "ported": True,
|
||||
"args": [_arg("cycles", "int", 3, "cycles per case (flow / no-flow)")],
|
||||
"desc": "Runs the real check (40 percent / 50 s, cut-profile fans) from a heater-warmed baseline, alternating "
|
||||
"flow and no-flow; results to the bench data directory. Slow: about 15 minutes per cycle."},
|
||||
{"id": "flow-recheck", "title": "Coolant re-check characterization", "script": "flow_recheck_char.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "Short in-run re-checks and the differential metric."},
|
||||
"safety": "takeover", "where": "board", "ported": True,
|
||||
"args": [_arg("heater_pct", "int", 50, "heater duty percent"), _arg("window_s", "int", 30, "re-check window")],
|
||||
"desc": "Short in-run re-checks and the differential metric, flow vs no-flow from a settled loop; "
|
||||
"aborts past 45 C downstream (about 5 minutes)."},
|
||||
{"id": "flow-matrix", "title": "Coolant flow-detection design matrix", "script": "flow_matrix.py",
|
||||
"safety": "takeover", "where": "board", "ported": True,
|
||||
"args": [_arg("duties", "str", "10,15,20,30,40,50", "heater duties, percent, comma-separated"),
|
||||
_arg("repeats", "int", 5, "interleaved repeats per case")],
|
||||
"desc": "duty x flow/no-flow x repeats from a common cooled baseline; cost and precision tables and a "
|
||||
"ranked shortlist (the derivation of cool_flow_rise). Very slow: about 1.6 h for the full matrix; "
|
||||
"resumable from the results file in the bench data directory."},
|
||||
{"id": "flow-sampler", "title": "Coolant sampler", "script": "flow_sampler.py",
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("duration_s", "int", 30, "capture length"), _arg("interval_s", "float", 1.0, "sample interval")],
|
||||
"desc": "Prints elapsed,raw_down,raw_up at the interval; the sampler behind the flow tools."},
|
||||
{"id": "temp-calibrate", "title": "Coolant temperature spot-check", "script": "temp_calibrate.py",
|
||||
"safety": "dry", "where": "host", "ported": False,
|
||||
"safety": "dry", "where": "board", "ported": True,
|
||||
"args": [_arg("mode", "choice", "watch", "watch / point / fit", ["watch", "point", "fit"]),
|
||||
_arg("measured_c", "float", None, "thermometer reading for point")],
|
||||
"desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line."},
|
||||
_arg("value", "str", None, "point: the thermometer reading in C; watch: seconds (default 60)")],
|
||||
"desc": "Pairs a measured temperature with averaged raw readings; fits a per-machine line. Points "
|
||||
"accumulate in the bench data directory."},
|
||||
{"id": "fan-test", "title": "Fan/coolant bench", "script": "fan_test.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "Snapshots fan PWMs/tachs/temps, drives M8 -> cut fans, M9 -> cooldown -> idle. Host-side; port pending."},
|
||||
"safety": "dry", "where": "board", "ported": True, "args": [],
|
||||
"desc": "Snapshots fan PWMs/tachs/temps, drives M8 -> cut fans, M9 -> cooldown -> idle; the tach "
|
||||
"readbacks in each snapshot are the evidence."},
|
||||
# -- laser (live) --------------------------------------------------------------
|
||||
{"id": "live-fire", "title": "LIVE laser drills", "script": "live_fire_drills.py",
|
||||
"safety": "live", "where": "board", "ported": False,
|
||||
"args": [_arg("drill", "choice", "witness", "witness / hold / faultpos", ["witness", "hold", "faultpos"])],
|
||||
"argv_fixed_after": ["127.0.0.1"],
|
||||
"desc": "Emission witness, disarm grace in Hold, stale-origin refusal. The operator's arm press is required for every drill."},
|
||||
"safety": "live", "where": "board", "ported": True,
|
||||
"args": [_arg("drill", "choice", "witness", "witness / hold / faultpos / ircut / expstop / ctrlstart",
|
||||
["witness", "hold", "faultpos", "ircut", "expstop", "ctrlstart"]),
|
||||
_arg("power", "int", 1000, "ircut: S value"), _arg("feed", "int", 300, "ircut: F value")],
|
||||
"desc": "Emission witness, disarm grace in Hold, stale-origin refusal, lid-IR characterization cut, armed "
|
||||
"kill on the expected-stop path (+ the separate controller restart). The operator's arm press is "
|
||||
"required for every drill; eye protection, fire watch, extinguisher, exhaust."},
|
||||
# -- host-side harnesses (CI) ------------------------------------------------------
|
||||
{"id": "laser-stream-test", "title": "Laser pulse-stream emission harness", "script": "laser_stream_test.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "Null-sink controller stream capture against the feeder contract. Runs in the grblHAL repo's CI."},
|
||||
"desc": "Null-sink controller stream capture against the feeder contract. A CI harness (the grblHAL repo): "
|
||||
"it needs the host-built null-sink controller, not the machine, so it is not a bench-page tool."},
|
||||
{"id": "laser-lifecycle-test", "title": "Armed-window lifecycle harness", "script": "laser_lifecycle_test.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "Arm/disarm lifecycle on the null-sink controller. Runs in the grblHAL repo's CI."},
|
||||
"desc": "Arm/disarm lifecycle on the null-sink controller. A CI harness (the grblHAL repo): needs the "
|
||||
"host-built null-sink controller, not the machine, so it is not a bench-page tool."},
|
||||
{"id": "puls-profile", "title": "Factory .puls profile decoder", "script": "puls_profile.py",
|
||||
"safety": "dry", "where": "host", "ported": False, "args": [],
|
||||
"desc": "Decodes factory pulse streams into velocity/accel profiles. Runs anywhere; needs a .puls file."},
|
||||
"desc": "Decodes factory pulse streams into velocity/accel profiles. Runs anywhere; needs a .puls file "
|
||||
"(the reference captures live off the machine), so it is not a bench-page tool."},
|
||||
]
|
||||
|
||||
# Files in scripts/bench that are not tools of their own: the helper module
|
||||
# the host/board tools share, and the C feeder + build scripts (not python).
|
||||
NOT_TOOLS = ("gfbench.py",)
|
||||
|
||||
|
||||
class Bench:
|
||||
def __init__(self, tools=None, tool_dir=None, index_path=None):
|
||||
|
||||
@@ -503,7 +503,17 @@ class Runner:
|
||||
captured = self._baseline_pre(run)
|
||||
env = dict(os.environ)
|
||||
env.setdefault("PYTHONUNBUFFERED", "1")
|
||||
takeover = Takeover(run.log, "bench:" + tool["id"]) if tool.get("safety") == "takeover" else None
|
||||
# The tools that also run from a LAN host (gfbench.py) run on
|
||||
# the board here: the machine is local, the panel token is at
|
||||
# hand, and their data files go under <data>/bench/.
|
||||
env.setdefault("GF_HOST", "127.0.0.1")
|
||||
env.setdefault("FORGETEST_BENCH_DATA", os.path.join(data_dir(), "bench"))
|
||||
if not env.get("GF_TOKEN"):
|
||||
tok = hw.Forgectrl().token
|
||||
if tok:
|
||||
env["GF_TOKEN"] = tok
|
||||
takeover = (Takeover(run.log, "bench:" + tool["id"])
|
||||
if tool.get("safety") in ("takeover", "scope") else None)
|
||||
if takeover is not None:
|
||||
takeover.__enter__()
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""The bench registry against scripts/bench: every python tool is
|
||||
registered (or named as not-a-tool), every registered script exists,
|
||||
every argument spec builds a command line, every script compiles, and
|
||||
the shared helper resolves host and local mode as documented."""
|
||||
import glob
|
||||
import os
|
||||
import py_compile
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import helpers # noqa: F401 (sys.path)
|
||||
from forgetest import bench as bench_mod
|
||||
|
||||
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
BENCH = os.path.join(REPO, "scripts", "bench")
|
||||
|
||||
|
||||
class RegistryTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.bench = bench_mod.Bench(tool_dir=BENCH, index_path=os.devnull)
|
||||
self.scripts = sorted(os.path.basename(p) for p in glob.glob(os.path.join(BENCH, "*.py")))
|
||||
|
||||
def test_every_python_tool_is_registered(self):
|
||||
registered = {t["script"] for t in bench_mod.TOOLS}
|
||||
missing = [s for s in self.scripts if s not in registered and s not in bench_mod.NOT_TOOLS]
|
||||
self.assertEqual(missing, [], "scripts/bench python files missing from the registry")
|
||||
|
||||
def test_every_registered_script_exists(self):
|
||||
for t in bench_mod.TOOLS:
|
||||
self.assertTrue(os.path.exists(os.path.join(BENCH, t["script"])), t["script"])
|
||||
|
||||
def test_ids_unique_and_fields_valid(self):
|
||||
ids = [t["id"] for t in bench_mod.TOOLS]
|
||||
self.assertEqual(len(ids), len(set(ids)))
|
||||
for t in bench_mod.TOOLS:
|
||||
self.assertIn(t["safety"], ("dry", "takeover", "scope", "live"), t["id"])
|
||||
self.assertIn(t["where"], ("board", "host"), t["id"])
|
||||
self.assertIsInstance(t["ported"], bool, t["id"])
|
||||
self.assertTrue(t["desc"], t["id"])
|
||||
for a in t.get("args", []):
|
||||
self.assertIn(a["type"], ("str", "int", "float", "choice"), (t["id"], a["name"]))
|
||||
if a["type"] == "choice":
|
||||
self.assertIn(a["default"], a["choices"], (t["id"], a["name"]))
|
||||
|
||||
def test_ported_tools_build_a_command_with_defaults(self):
|
||||
for t in bench_mod.TOOLS:
|
||||
if not t["ported"]:
|
||||
continue
|
||||
ok, argv, err = self.bench.command(t, {})
|
||||
self.assertTrue(ok, "%s: %s" % (t["id"], err))
|
||||
self.assertEqual(argv[1], os.path.join(BENCH, t["script"]))
|
||||
|
||||
def test_unported_tools_are_host_only(self):
|
||||
# what stays unported is what cannot run on the machine at all
|
||||
for t in bench_mod.TOOLS:
|
||||
if not t["ported"]:
|
||||
self.assertEqual(t["where"], "host", t["id"])
|
||||
self.assertIn("not a bench-page tool", t["desc"], t["id"])
|
||||
|
||||
def test_scripts_compile(self):
|
||||
for s in self.scripts:
|
||||
py_compile.compile(os.path.join(BENCH, s), doraise=True)
|
||||
|
||||
|
||||
class GfbenchTests(unittest.TestCase):
|
||||
"""gfbench.py in a subprocess (module-level host resolution)."""
|
||||
|
||||
def run_snippet(self, code, env_extra):
|
||||
env = dict(os.environ)
|
||||
env.pop("GF_HOST", None)
|
||||
env.pop("FORGETEST_BENCH_DATA", None)
|
||||
env.update(env_extra)
|
||||
r = subprocess.run([sys.executable, "-c", code], cwd=BENCH, env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30)
|
||||
return r.returncode, r.stdout.strip()
|
||||
|
||||
def test_host_mode(self):
|
||||
rc, out = self.run_snippet("import gfbench; print(gfbench.HOST, gfbench.LOCAL, gfbench.SSH[0])",
|
||||
{"GF_HOST": "192.0.2.7", "GF_SSH": "fakessh -x"})
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertEqual(out, "192.0.2.7 False fakessh")
|
||||
|
||||
def test_local_when_named_local(self):
|
||||
rc, out = self.run_snippet("import gfbench; print(gfbench.HOST, gfbench.LOCAL); "
|
||||
"print(gfbench.board('echo hi').strip())",
|
||||
{"GF_HOST": "127.0.0.1"})
|
||||
self.assertEqual(rc, 0, out)
|
||||
self.assertEqual(out.splitlines(), ["127.0.0.1 True", "hi"])
|
||||
|
||||
def test_refuses_without_a_machine(self):
|
||||
if os.path.isdir("/sys/glowforge"):
|
||||
self.skipTest("running on the machine")
|
||||
rc, out = self.run_snippet("import gfbench", {})
|
||||
self.assertNotEqual(rc, 0)
|
||||
self.assertIn("GF_HOST", out)
|
||||
|
||||
def test_degc_and_data_dir(self):
|
||||
import tempfile
|
||||
d = tempfile.mkdtemp(prefix="forgetest-bench-")
|
||||
rc, out = self.run_snippet(
|
||||
"import gfbench, os; print(round(gfbench.degc(700), 2), gfbench.degc('x') != gfbench.degc('x'), "
|
||||
"os.path.dirname(gfbench.data_path('f.json')) == os.environ['FORGETEST_BENCH_DATA'])",
|
||||
{"GF_HOST": "127.0.0.1", "FORGETEST_BENCH_DATA": d})
|
||||
self.assertEqual(rc, 0, out)
|
||||
val, nan, indir = out.split()
|
||||
# a plausible coolant reading (700 counts is room temperature territory)
|
||||
self.assertTrue(15.0 < float(val) < 35.0, val)
|
||||
self.assertEqual(nan, "True")
|
||||
self.assertEqual(indir, "True")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -62,7 +62,9 @@ class ServerTests(unittest.TestCase):
|
||||
cls.tooldir = os.path.join(cls.tmp, "bench")
|
||||
os.makedirs(cls.tooldir)
|
||||
with open(os.path.join(cls.tooldir, "echo_tool.py"), "w") as f:
|
||||
f.write("import sys, time\nprint('args', sys.argv[1:])\nsys.stdout.flush()\n"
|
||||
f.write("import os, sys, time\nprint('args', sys.argv[1:])\n"
|
||||
"print('env', os.environ.get('GF_HOST'), os.environ.get('FORGETEST_BENCH_DATA'))\n"
|
||||
"sys.stdout.flush()\n"
|
||||
"if 'slow' in sys.argv: time.sleep(30)\nsys.exit(0 if 'fail' not in sys.argv else 3)\n")
|
||||
tools = [{"id": "echo", "title": "Echo", "script": "echo_tool.py", "safety": "dry", "where": "board",
|
||||
"ported": True, "desc": "echo",
|
||||
@@ -73,6 +75,8 @@ class ServerTests(unittest.TestCase):
|
||||
{"id": "hot", "title": "H", "script": "echo_tool.py", "safety": "live", "where": "board",
|
||||
"ported": True, "desc": "", "args": []},
|
||||
{"id": "tk", "title": "T", "script": "echo_tool.py", "safety": "takeover", "where": "board",
|
||||
"ported": True, "desc": "", "args": []},
|
||||
{"id": "sc", "title": "S", "script": "echo_tool.py", "safety": "scope", "where": "board",
|
||||
"ported": True, "desc": "", "args": []}]
|
||||
cls.bench = bench_mod.Bench(tools, tool_dir=cls.tooldir,
|
||||
index_path=os.path.join(cls.tmp, "bench.jsonl"))
|
||||
@@ -261,7 +265,7 @@ class ServerTests(unittest.TestCase):
|
||||
st, d = self.call("GET", "/bench")
|
||||
self.assertEqual(st, 200)
|
||||
ids = [t["id"] for t in d["tools"]]
|
||||
self.assertEqual(ids, ["echo", "unported", "hot", "tk"])
|
||||
self.assertEqual(ids, ["echo", "unported", "hot", "tk", "sc"])
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "unported"})
|
||||
self.assertEqual(st, 409)
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "hot"})
|
||||
@@ -274,6 +278,9 @@ class ServerTests(unittest.TestCase):
|
||||
self.assertEqual(state["last_run"]["kind"], "bench")
|
||||
self.assertEqual(state["last_run"]["finished"]["result"], "OK")
|
||||
self.assertTrue(any("['yo', '5']" in l for l in state["last_run"]["log"]))
|
||||
# the bench environment: the machine is local, data under <data>/bench
|
||||
self.assertTrue(any(" env 127.0.0.1 " in l and l.endswith("bench")
|
||||
for l in state["last_run"]["log"]), state["last_run"]["log"])
|
||||
st, d = self.call("GET", "/bench")
|
||||
self.assertEqual(d["tools"][0]["last"]["result"]["result"], "OK")
|
||||
# a failing tool and an aborted one
|
||||
@@ -295,6 +302,13 @@ class ServerTests(unittest.TestCase):
|
||||
self.assertIn("takeover: forgectrl start", log)
|
||||
self.assertIn("baseline: forgectrl unreachable for 10 s", log)
|
||||
self.assertFalse(os.path.exists(os.environ["FORGETEST_MARKER"]))
|
||||
# a scope tool is a takeover too
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "sc"})
|
||||
self.assertEqual(st, 200, d)
|
||||
state = self.wait_idle(timeout=40)
|
||||
log = "\n".join(state["last_run"]["log"])
|
||||
self.assertIn("takeover: pulse device free", log)
|
||||
self.assertIn("takeover: forgectrl start", log)
|
||||
# bench runs never touched the acceptance log
|
||||
recs = self.log.read()
|
||||
self.assertFalse(any(r.get("t") == "result" and r.get("test") == "echo" for r in recs))
|
||||
|
||||
Reference in New Issue
Block a user