mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
forgetest: the release acceptance tool and the bench diagnostics page
A stdlib-only daemon on the dev image (HTTP :8090) that runs the acceptance catalog against the machine from a self-contained page, keeps the append-only result log under /data/forgetest, and exports the release artifact the gate reads. Tests declare kind (auto / operator / live), hardware (api / takeover), coverage globs, prerequisites, and core membership; a test's domain fingerprint is the hash of the manifest files its globs select plus the platform and its own implementation, so a PASS stays valid exactly while nothing it covers changed. Campaign rules: a FAIL ends the campaign, the core (image health, kernel latch and drills, one live emission witness) is never inherited, invalidate-all forces a full campaign, no SKIP. Live tests need the operator acknowledgment and the physical arm press through the controller; takeover tests stop forgectrl for the duration with a crash-recoverable marker; the tool never touches the laser latch. Catalog v1: 24 tests ported from the proven bench drills with their recorded pass criteria (image, kernel K1-K3 and fire A/B/U, forgectrl API and logs, motion incl. dead-man, cooling, live laser, camera, update, cloud). The bench tab lists every scripts/bench tool and runs the board-side ones as subprocesses (takeover tools wrapped). 44 host unit tests, including the gate verification fixtures. Installed only by forgefirm-image-dev, with the bench scripts under /usr/share/forgetest/bench.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""Shared fixtures for the forgetest unit tests: synthetic manifests and
|
||||
catalog entries that never touch hardware."""
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.dirname(HERE))
|
||||
|
||||
from forgetest import catalog as catalog_mod # noqa: E402
|
||||
from forgetest import manifest as manifest_mod # noqa: E402
|
||||
|
||||
|
||||
def blob(text):
|
||||
"""A git-style blob id for text (what ls-tree would report)."""
|
||||
data = text.encode()
|
||||
return hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest()
|
||||
|
||||
|
||||
def make_manifest(components=None, platform=None, version="20260101000000 (dev)", name="forgefirm-image-dev"):
|
||||
comps = components if components is not None else {
|
||||
"forgectrl": {"srcrev": "aaa", "files": [["src/main.c", blob("main")], ["src/ui.c", blob("ui")],
|
||||
["src/auth.c", blob("auth")], ["src/cool.c", blob("cool")],
|
||||
["README.md", blob("readme")]]},
|
||||
"grblhal-glowforge": {"srcrev": "bbb", "files": [["src/driver.c", blob("drv")],
|
||||
["src/grbl", "cccc"], ["src/grbl/core.c", blob("core")]]},
|
||||
"kernel-module-glowforge": {"srcrev": "ddd", "files": [["src/cnc.c", blob("cnc")]]},
|
||||
"linux-fslc": {"srcrev": "eee", "files": [["@srcrev", "eee"], ["@config", "cfg1"]]},
|
||||
"forgetest": {"srcrev": None, "files": [["forgetest/x.py", blob("x")]]},
|
||||
}
|
||||
plat = platform if platform is not None else {
|
||||
"machine": "glowforge", "kernel_modules": ["6.12.20-fslc+g0e01ec9f0d3f"],
|
||||
"dtb": {"glowforge.dtb": "d" * 64},
|
||||
"layers": {"meta-forgefirm": {"content_sha256": "1" * 64}, "poky": {"rev": "p" * 40}},
|
||||
}
|
||||
data = {"format": 1, "image": {"name": name, "version": version},
|
||||
"components": comps, "platform": plat}
|
||||
data["content_sha256"] = manifest_mod.sha256_text(
|
||||
manifest_mod.canonical({"components": comps, "platform": plat}))
|
||||
return manifest_mod.Manifest(data)
|
||||
|
||||
|
||||
def with_file(manifest, component, path, text):
|
||||
"""A copy of the manifest with one file's content changed/added."""
|
||||
import copy
|
||||
data = copy.deepcopy(manifest.data)
|
||||
files = data["components"][component]["files"]
|
||||
for f in files:
|
||||
if f[0] == path:
|
||||
f[1] = blob(text)
|
||||
break
|
||||
else:
|
||||
files.append([path, blob(text)])
|
||||
data["content_sha256"] = manifest_mod.sha256_text(
|
||||
manifest_mod.canonical({"components": data["components"], "platform": data["platform"]}))
|
||||
return manifest_mod.Manifest(data)
|
||||
|
||||
|
||||
def with_platform(manifest, **changes):
|
||||
import copy
|
||||
data = copy.deepcopy(manifest.data)
|
||||
data["platform"].update(changes)
|
||||
data["content_sha256"] = manifest_mod.sha256_text(
|
||||
manifest_mod.canonical({"components": data["components"], "platform": data["platform"]}))
|
||||
return manifest_mod.Manifest(data)
|
||||
|
||||
|
||||
def _noop(ctx):
|
||||
pass
|
||||
|
||||
|
||||
def make_test(id, covers, always=False, requires=(), kind="auto", fn=None, subsystem=None):
|
||||
return catalog_mod.Test(id, "Title " + id, subsystem or id.split(".")[0], kind, "api",
|
||||
covers, requires, always, 1, (), "desc", fn or _noop)
|
||||
|
||||
|
||||
def registry(*tests):
|
||||
return {t.id: t for t in tests}
|
||||
@@ -0,0 +1,143 @@
|
||||
import copy
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import helpers
|
||||
from forgetest import artifact, campaign, catalog
|
||||
from forgetest import manifest as manifest_mod
|
||||
from test_campaign import rec_campaign, rec_result
|
||||
|
||||
|
||||
class ArtifactTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.man = helpers.make_manifest()
|
||||
self.core = helpers.make_test("image.health", [("linux-fslc", "**")], always=True)
|
||||
self.ui = helpers.make_test("forgectrl.panel", [("forgectrl", "src/ui.c")])
|
||||
self.cool = helpers.make_test("cooling.flow", [("forgectrl", "src/cool.c")])
|
||||
self.reg = helpers.registry(self.core, self.ui, self.cool)
|
||||
self.tests = list(self.reg.values())
|
||||
self.chash = catalog.catalog_hash(self.reg)
|
||||
# campaign 1 on the base image: everything passes
|
||||
self.recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z")]
|
||||
for i, t in enumerate(self.tests):
|
||||
self.recs.append(rec_result("c1", t, self.man, "PASS", "2026-08-20T10:0%d:00Z" % (i + 1)))
|
||||
# image 2: cool.c changed; campaign 2 reruns core + cool
|
||||
self.man2 = helpers.with_file(self.man, "forgectrl", "src/cool.c", "cool v2")
|
||||
self.recs.append(rec_campaign("c2", self.man2, self.chash, "2026-08-21T10:00:00Z"))
|
||||
self.recs.append(rec_result("c2", self.core, self.man2, "PASS", "2026-08-21T10:01:00Z"))
|
||||
self.recs.append(rec_result("c2", self.cool, self.man2, "PASS", "2026-08-21T10:02:00Z"))
|
||||
|
||||
def build(self, man=None):
|
||||
man = man or self.man2
|
||||
st = campaign.compute(self.recs, self.tests, man, self.chash)
|
||||
return artifact.build(st, self.tests, man, self.recs, self.chash), st
|
||||
|
||||
def release_manifest(self, man=None):
|
||||
"""The release build's manifest: same identity, no forgetest component."""
|
||||
man = man or self.man2
|
||||
data = copy.deepcopy(man.data)
|
||||
data["components"].pop("forgetest", None)
|
||||
data["image"] = {"name": "forgefirm-image", "version": "v0.1.0"}
|
||||
return manifest_mod.Manifest(data)
|
||||
|
||||
def test_build_and_verify(self):
|
||||
art, st = self.build()
|
||||
self.assertTrue(art["authorized"])
|
||||
by = {t["id"]: t for t in art["tests"]}
|
||||
self.assertTrue(by["forgectrl.panel"]["inherited"])
|
||||
self.assertEqual(by["forgectrl.panel"]["record"]["campaign"], "c1")
|
||||
self.assertFalse(by["image.health"]["inherited"])
|
||||
text = artifact.to_json(art)
|
||||
art2 = json.loads(text)
|
||||
ok, rows, problems = artifact.verify(art2, self.release_manifest(), self.tests, self.chash,
|
||||
expect_machine="glowforge")
|
||||
self.assertEqual(problems, [])
|
||||
self.assertTrue(ok)
|
||||
md = artifact.to_markdown(art)
|
||||
self.assertIn("Release authorized: YES", md)
|
||||
self.assertIn("forgectrl.panel", md)
|
||||
|
||||
def test_tamper_detected(self):
|
||||
art, _ = self.build()
|
||||
art2 = json.loads(artifact.to_json(art))
|
||||
art2["tests"][1]["record"]["result"] = "PASS"
|
||||
art2["counts"]["required"] = 0
|
||||
art2["authorized"] = True
|
||||
art2["tests"][2]["record"]["ts"] = "2026-08-21T10:02:01Z"
|
||||
ok, rows, problems = artifact.verify(art2, self.release_manifest(), self.tests, self.chash)
|
||||
self.assertFalse(ok)
|
||||
self.assertIn("self-hash", problems[0])
|
||||
|
||||
def test_release_differs_in_covered_file(self):
|
||||
art, _ = self.build()
|
||||
rel = self.release_manifest(helpers.with_file(self.man2, "forgectrl", "src/ui.c", "ui v3"))
|
||||
ok, rows, problems = artifact.verify(art, rel, self.tests, self.chash)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("forgectrl.panel" in p and "fingerprint" in p for p in problems), problems)
|
||||
# an uncovered change is fine
|
||||
rel2 = self.release_manifest(helpers.with_file(self.man2, "forgectrl", "README.md", "docs"))
|
||||
ok, rows, problems = artifact.verify(art, rel2, self.tests, self.chash)
|
||||
self.assertTrue(ok, problems)
|
||||
|
||||
def test_release_platform_differs(self):
|
||||
art, _ = self.build()
|
||||
rel = self.release_manifest(helpers.with_platform(self.man2, dtb={"glowforge.dtb": "0" * 64}))
|
||||
ok, rows, problems = artifact.verify(art, rel, self.tests, self.chash)
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(len([r for r in rows if not r["ok"]]), 3)
|
||||
|
||||
def test_unauthorized_artifact(self):
|
||||
self.recs.append(rec_result("c2", self.ui, self.man2, "FAIL", "2026-08-21T10:03:00Z"))
|
||||
art, st = self.build()
|
||||
self.assertFalse(art["authorized"])
|
||||
ok, rows, problems = artifact.verify(art, self.release_manifest(), self.tests, self.chash)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("does not claim authorization" in p for p in problems))
|
||||
|
||||
def test_core_inherited_refused(self):
|
||||
art, _ = self.build()
|
||||
# forge an artifact whose core record is marked inherited (self-hash recomputed)
|
||||
body = json.loads(artifact.to_json(art))
|
||||
body.pop("sha256")
|
||||
for t in body["tests"]:
|
||||
if t["id"] == "image.health":
|
||||
t["inherited"] = True
|
||||
body["sha256"] = manifest_mod.sha256_text(manifest_mod.canonical(body))
|
||||
ok, rows, problems = artifact.verify(body, self.release_manifest(), self.tests, self.chash)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("always-required" in p for p in problems), problems)
|
||||
|
||||
def test_invalidate_epoch(self):
|
||||
art, _ = self.build()
|
||||
body = json.loads(artifact.to_json(art))
|
||||
body.pop("sha256")
|
||||
body["invalidate"] = {"t": "invalidate", "ts": "2026-08-20T12:00:00Z", "reason": "tube"}
|
||||
body["sha256"] = manifest_mod.sha256_text(manifest_mod.canonical(body))
|
||||
ok, rows, problems = artifact.verify(body, self.release_manifest(), self.tests, self.chash)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("predates the invalidate" in p for p in problems), problems)
|
||||
|
||||
def test_catalog_changed(self):
|
||||
art, _ = self.build()
|
||||
reg = helpers.registry(self.core, self.ui, self.cool, helpers.make_test("new.one", []))
|
||||
tests = list(reg.values())
|
||||
ok, rows, problems = artifact.verify(art, self.release_manifest(), tests, catalog.catalog_hash(reg))
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("catalog changed" in p for p in problems))
|
||||
self.assertTrue(any("new.one" in p for p in problems))
|
||||
|
||||
def test_implementation_changed(self):
|
||||
art, _ = self.build()
|
||||
body = json.loads(artifact.to_json(art))
|
||||
body.pop("sha256")
|
||||
for t in body["tests"]:
|
||||
if t["id"] == "cooling.flow":
|
||||
t["source_sha"] = "0" * 64
|
||||
body["sha256"] = manifest_mod.sha256_text(manifest_mod.canonical(body))
|
||||
ok, rows, problems = artifact.verify(body, self.release_manifest(), self.tests, self.chash)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("implementation changed" in p for p in problems), problems)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,160 @@
|
||||
import unittest
|
||||
|
||||
import helpers
|
||||
from forgetest import campaign
|
||||
from forgetest import catalog
|
||||
|
||||
|
||||
def rec_campaign(cid, man, chash, ts):
|
||||
return {"t": "campaign", "ts": ts, "id": cid, "manifest_sha": man.content_sha,
|
||||
"catalog_hash": chash, "image": man.version}
|
||||
|
||||
|
||||
def rec_result(cid, t, man, result, ts, fp=None):
|
||||
return {"t": "result", "ts": ts, "campaign": cid, "test": t.id, "result": result,
|
||||
"fingerprint": fp or t.fingerprint(man), "manifest_sha": man.content_sha,
|
||||
"image": man.version, "duration_s": 1, "message": ""}
|
||||
|
||||
|
||||
class CampaignTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.man = helpers.make_manifest()
|
||||
self.core = helpers.make_test("image.health", [("linux-fslc", "**")], always=True)
|
||||
self.ui = helpers.make_test("forgectrl.panel", [("forgectrl", "src/ui.c")])
|
||||
self.cool = helpers.make_test("cooling.flow", [("forgectrl", "src/cool.c")])
|
||||
self.live = helpers.make_test("laser.witness", [("grblhal-glowforge", "src/**")],
|
||||
requires=("forgectrl.panel",), kind="live")
|
||||
self.reg = helpers.registry(self.core, self.ui, self.cool, self.live)
|
||||
self.tests = list(self.reg.values())
|
||||
self.chash = catalog.catalog_hash(self.reg)
|
||||
|
||||
def compute(self, records, man=None, chash=None):
|
||||
return campaign.compute(records, self.tests, man or self.man, chash or self.chash)
|
||||
|
||||
def test_empty(self):
|
||||
st = self.compute([])
|
||||
self.assertIsNone(st["campaign"])
|
||||
self.assertFalse(st["authorized"])
|
||||
for t in self.tests:
|
||||
self.assertTrue(st["tests"][t.id]["required"])
|
||||
self.assertEqual(st["tests"][t.id]["reason"], "always" if t.always else "never-passed")
|
||||
self.assertFalse(st["tests"][self.live.id]["requires_met"])
|
||||
|
||||
def test_full_campaign_authorizes(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z")]
|
||||
for i, t in enumerate(self.tests):
|
||||
recs.append(rec_result("c1", t, self.man, "PASS", "2026-08-20T10:0%d:00Z" % (i + 1)))
|
||||
st = self.compute(recs)
|
||||
self.assertTrue(st["authorized"])
|
||||
self.assertEqual(st["counts"], {"total": 4, "satisfied": 4, "inherited": 0, "required": 0})
|
||||
self.assertEqual(st["tests"][self.core.id]["status"], "pass")
|
||||
|
||||
def test_fail_closes_campaign(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z"),
|
||||
rec_result("c1", self.core, self.man, "PASS", "2026-08-20T10:01:00Z"),
|
||||
rec_result("c1", self.ui, self.man, "PASS", "2026-08-20T10:02:00Z"),
|
||||
rec_result("c1", self.cool, self.man, "FAIL", "2026-08-20T10:03:00Z")]
|
||||
st = self.compute(recs)
|
||||
self.assertIsNone(st["campaign"])
|
||||
self.assertEqual(st["closed_by"], "fail")
|
||||
self.assertFalse(st["authorized"])
|
||||
# the core PASS in the closed campaign no longer counts; the ui PASS is inheritable
|
||||
self.assertTrue(st["tests"][self.core.id]["required"])
|
||||
self.assertEqual(st["tests"][self.core.id]["reason"], "always")
|
||||
self.assertEqual(st["tests"][self.ui.id]["status"], "inherited")
|
||||
self.assertEqual(st["tests"][self.cool.id]["status"], "fail")
|
||||
self.assertEqual(st["tests"][self.cool.id]["reason"], "never-passed")
|
||||
|
||||
def test_error_closes_aborted_does_not(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z"),
|
||||
rec_result("c1", self.ui, self.man, "ABORTED", "2026-08-20T10:01:00Z")]
|
||||
st = self.compute(recs)
|
||||
self.assertIsNotNone(st["campaign"])
|
||||
self.assertEqual(st["tests"][self.ui.id]["status"], "aborted")
|
||||
recs.append(rec_result("c1", self.ui, self.man, "ERROR", "2026-08-20T10:02:00Z"))
|
||||
st = self.compute(recs)
|
||||
self.assertIsNone(st["campaign"])
|
||||
self.assertEqual(st["closed_by"], "fail")
|
||||
|
||||
def test_inheritance_follows_fingerprint(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z")]
|
||||
for i, t in enumerate(self.tests):
|
||||
recs.append(rec_result("c1", t, self.man, "PASS", "2026-08-20T10:0%d:00Z" % (i + 1)))
|
||||
# a new image with only cool.c changed
|
||||
man2 = helpers.with_file(self.man, "forgectrl", "src/cool.c", "cool v2")
|
||||
st = self.compute(recs, man=man2)
|
||||
self.assertIsNone(st["campaign"])
|
||||
self.assertEqual(st["closed_by"], "image")
|
||||
self.assertEqual(st["tests"][self.ui.id]["status"], "inherited")
|
||||
self.assertEqual(st["tests"][self.live.id]["status"], "inherited")
|
||||
self.assertEqual(st["tests"][self.cool.id]["status"], "stale")
|
||||
self.assertEqual(st["tests"][self.cool.id]["reason"], "domain-changed")
|
||||
self.assertEqual(st["tests"][self.core.id]["reason"], "always")
|
||||
# open a campaign on the new image, run the core + cool -> authorized
|
||||
recs.append(rec_campaign("c2", man2, self.chash, "2026-08-21T10:00:00Z"))
|
||||
recs.append(rec_result("c2", self.core, man2, "PASS", "2026-08-21T10:01:00Z"))
|
||||
st = self.compute(recs, man=man2)
|
||||
self.assertFalse(st["authorized"])
|
||||
recs.append(rec_result("c2", self.cool, man2, "PASS", "2026-08-21T10:02:00Z"))
|
||||
st = self.compute(recs, man=man2)
|
||||
self.assertTrue(st["authorized"])
|
||||
self.assertEqual(st["counts"]["inherited"], 2)
|
||||
self.assertEqual(st["tests"][self.ui.id]["origin"]["campaign"], "c1")
|
||||
|
||||
def test_platform_change_invalidates_everything(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z")]
|
||||
for i, t in enumerate(self.tests):
|
||||
recs.append(rec_result("c1", t, self.man, "PASS", "2026-08-20T10:0%d:00Z" % (i + 1)))
|
||||
man2 = helpers.with_platform(self.man, dtb={"glowforge.dtb": "f" * 64})
|
||||
st = self.compute(recs, man=man2)
|
||||
for t in self.tests:
|
||||
self.assertTrue(st["tests"][t.id]["required"], t.id)
|
||||
|
||||
def test_invalidate_all(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z")]
|
||||
for i, t in enumerate(self.tests):
|
||||
recs.append(rec_result("c1", t, self.man, "PASS", "2026-08-20T10:0%d:00Z" % (i + 1)))
|
||||
recs.append({"t": "invalidate", "ts": "2026-08-22T09:00:00Z", "reason": "new tube"})
|
||||
st = self.compute(recs)
|
||||
self.assertIsNone(st["campaign"])
|
||||
self.assertEqual(st["closed_by"], "invalidate")
|
||||
self.assertEqual(st["invalidate"]["reason"], "new tube")
|
||||
for t in self.tests:
|
||||
self.assertTrue(st["tests"][t.id]["required"], t.id)
|
||||
self.assertNotEqual(st["tests"][t.id]["status"], "inherited")
|
||||
# after the invalidate, a new campaign's passes count and later ones inherit again
|
||||
recs.append(rec_campaign("c2", self.man, self.chash, "2026-08-22T10:00:00Z"))
|
||||
for i, t in enumerate(self.tests):
|
||||
recs.append(rec_result("c2", t, self.man, "PASS", "2026-08-22T10:0%d:00Z" % (i + 1)))
|
||||
st = self.compute(recs)
|
||||
self.assertTrue(st["authorized"])
|
||||
|
||||
def test_reset_and_catalog_change(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z"),
|
||||
rec_result("c1", self.core, self.man, "PASS", "2026-08-20T10:01:00Z"),
|
||||
{"t": "reset", "ts": "2026-08-20T11:00:00Z", "reason": "x"}]
|
||||
st = self.compute(recs)
|
||||
self.assertIsNone(st["campaign"])
|
||||
self.assertEqual(st["closed_by"], "reset")
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z")]
|
||||
st = self.compute(recs, chash="different")
|
||||
self.assertIsNone(st["campaign"])
|
||||
self.assertEqual(st["closed_by"], "catalog")
|
||||
|
||||
def test_requires_uses_satisfied(self):
|
||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z"),
|
||||
rec_result("c1", self.ui, self.man, "PASS", "2026-08-20T10:01:00Z")]
|
||||
st = self.compute(recs)
|
||||
self.assertTrue(st["tests"][self.live.id]["requires_met"])
|
||||
man2 = helpers.with_file(self.man, "forgectrl", "src/ui.c", "ui v2")
|
||||
st = self.compute(recs, man=man2)
|
||||
self.assertFalse(st["tests"][self.live.id]["requires_met"])
|
||||
self.assertEqual(st["tests"][self.live.id]["missing_requires"], ["forgectrl.panel"])
|
||||
|
||||
def test_running_marker(self):
|
||||
st = campaign.compute([], self.tests, self.man, self.chash, running=self.ui.id)
|
||||
self.assertEqual(st["tests"][self.ui.id]["status"], "running")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,153 @@
|
||||
import unittest
|
||||
|
||||
import helpers
|
||||
from forgetest import manifest as m
|
||||
from forgetest import catalog
|
||||
|
||||
|
||||
class GlobTests(unittest.TestCase):
|
||||
def test_star_and_doublestar(self):
|
||||
rx = m.glob_to_regex("src/*.c")
|
||||
self.assertTrue(rx.match("src/main.c"))
|
||||
self.assertFalse(rx.match("src/sub/main.c"))
|
||||
rx = m.glob_to_regex("src/**")
|
||||
self.assertTrue(rx.match("src/main.c"))
|
||||
self.assertTrue(rx.match("src/sub/deep/x.h"))
|
||||
self.assertFalse(rx.match("docs/x"))
|
||||
rx = m.glob_to_regex("**/*.md")
|
||||
self.assertTrue(rx.match("README.md"))
|
||||
self.assertTrue(rx.match("docs/a/b.md"))
|
||||
self.assertFalse(rx.match("docs/a/b.txt"))
|
||||
rx = m.glob_to_regex("**")
|
||||
self.assertTrue(rx.match("anything/at/all"))
|
||||
rx = m.glob_to_regex("src/gfcool*")
|
||||
self.assertTrue(rx.match("src/gfcool_client.c"))
|
||||
self.assertFalse(rx.match("src/x/gfcool.c"))
|
||||
|
||||
def test_question_mark_and_escaping(self):
|
||||
rx = m.glob_to_regex("a?c.d")
|
||||
self.assertTrue(rx.match("abc.d"))
|
||||
self.assertFalse(rx.match("abcxd"))
|
||||
self.assertFalse(rx.match("a/c.d"))
|
||||
|
||||
|
||||
class FingerprintTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.man = helpers.make_manifest()
|
||||
|
||||
def test_stable_and_order_independent(self):
|
||||
c1 = [("forgectrl", "src/ui.c"), ("forgectrl", "src/auth.c")]
|
||||
c2 = list(reversed(c1))
|
||||
self.assertEqual(m.fingerprint(self.man, c1), m.fingerprint(self.man, c2))
|
||||
self.assertEqual(m.fingerprint(self.man, c1), m.fingerprint(self.man, c1))
|
||||
|
||||
def test_changes_only_when_covered_file_changes(self):
|
||||
covers = [("forgectrl", "src/ui.c")]
|
||||
base = m.fingerprint(self.man, covers)
|
||||
other = helpers.with_file(self.man, "forgectrl", "src/cool.c", "cool v2")
|
||||
self.assertEqual(base, m.fingerprint(other, covers), "an uncovered change must not move the fingerprint")
|
||||
changed = helpers.with_file(self.man, "forgectrl", "src/ui.c", "ui v2")
|
||||
self.assertNotEqual(base, m.fingerprint(changed, covers))
|
||||
added = helpers.with_file(self.man, "forgectrl", "src/ui_extra.c", "new")
|
||||
self.assertEqual(base, m.fingerprint(added, covers), "an unmatched new file does not move it")
|
||||
added2 = helpers.with_file(self.man, "forgectrl", "src/x.c", "new")
|
||||
self.assertNotEqual(base, m.fingerprint(added2, [("forgectrl", "src/*.c")]))
|
||||
|
||||
def test_platform_is_always_in(self):
|
||||
covers = [("forgectrl", "src/ui.c")]
|
||||
base = m.fingerprint(self.man, covers)
|
||||
p2 = helpers.with_platform(self.man, dtb={"glowforge.dtb": "e" * 64})
|
||||
self.assertNotEqual(base, m.fingerprint(p2, covers))
|
||||
p3 = helpers.with_platform(self.man, layers={"meta-forgefirm": {"content_sha256": "2" * 64},
|
||||
"poky": {"rev": "p" * 40}})
|
||||
self.assertNotEqual(base, m.fingerprint(p3, covers))
|
||||
|
||||
def test_kernel_pseudo_files(self):
|
||||
covers = [("linux-fslc", "**")]
|
||||
base = m.fingerprint(self.man, covers)
|
||||
k2 = helpers.with_file(self.man, "linux-fslc", "@config", "cfg2")
|
||||
self.assertNotEqual(base, m.fingerprint(k2, covers))
|
||||
|
||||
def test_missing_component_marker(self):
|
||||
f1 = m.fingerprint(self.man, [("nonexistent", "**")])
|
||||
f2 = m.fingerprint(self.man, [("forgectrl", "nothing-matches-*")])
|
||||
self.assertNotEqual(f1, f2)
|
||||
|
||||
def test_dev_only_refused(self):
|
||||
with self.assertRaises(ValueError):
|
||||
m.fingerprint(self.man, [("forgetest", "**")])
|
||||
|
||||
def test_extra_folds_in(self):
|
||||
covers = [("forgectrl", "src/ui.c")]
|
||||
self.assertNotEqual(m.fingerprint(self.man, covers, extra=["a"]),
|
||||
m.fingerprint(self.man, covers, extra=["b"]))
|
||||
|
||||
def test_submodule_gitlink_and_files(self):
|
||||
covers = [("grblhal-glowforge", "src/grbl/**")]
|
||||
base = m.fingerprint(self.man, covers)
|
||||
core = helpers.with_file(self.man, "grblhal-glowforge", "src/grbl/core.c", "core v2")
|
||||
self.assertNotEqual(base, m.fingerprint(core, covers))
|
||||
link = [("grblhal-glowforge", "src/grbl")]
|
||||
self.assertEqual(m.fingerprint(self.man, link), m.fingerprint(core, link),
|
||||
"the gitlink alone does not see a file-level change (fixture keeps the link id)")
|
||||
|
||||
def test_identity_sha_ignores_dev_only(self):
|
||||
a = self.man.identity_sha()
|
||||
b = helpers.with_file(self.man, "forgetest", "forgetest/x.py", "changed").identity_sha()
|
||||
self.assertEqual(a, b)
|
||||
c = helpers.with_file(self.man, "forgectrl", "src/ui.c", "changed").identity_sha()
|
||||
self.assertNotEqual(a, c)
|
||||
|
||||
|
||||
class CoverageReportTests(unittest.TestCase):
|
||||
def test_report(self):
|
||||
man = helpers.make_manifest()
|
||||
t1 = helpers.make_test("a.one", [("forgectrl", "src/ui.c"), ("forgectrl", "src/auth.c")])
|
||||
t2 = helpers.make_test("a.two", [("grblhal-glowforge", "**"), ("kernel-module-glowforge", "**"),
|
||||
("linux-fslc", "**")])
|
||||
rep = m.coverage_report(man, [t1, t2], allow=[("*", "**/*.md")])
|
||||
self.assertEqual(set(rep), {"forgectrl"})
|
||||
self.assertEqual(rep["forgectrl"], ["src/cool.c", "src/main.c"])
|
||||
rep2 = m.coverage_report(man, [t1, t2, helpers.make_test("a.three", [("forgectrl", "src/**")])],
|
||||
allow=[("*", "**/*.md")])
|
||||
self.assertEqual(rep2, {})
|
||||
self.assertNotIn("forgetest", m.coverage_report(man, [], allow=[]),
|
||||
"dev-only components are outside the report")
|
||||
|
||||
|
||||
class CatalogTests(unittest.TestCase):
|
||||
def test_catalog_hash_is_definition_only(self):
|
||||
r1 = helpers.registry(helpers.make_test("a.one", [("forgectrl", "src/ui.c")]))
|
||||
r2 = helpers.registry(helpers.make_test("a.one", [("forgectrl", "src/ui.c")], fn=lambda ctx: 1))
|
||||
self.assertEqual(catalog.catalog_hash(r1), catalog.catalog_hash(r2))
|
||||
r3 = helpers.registry(helpers.make_test("a.one", [("forgectrl", "src/**")]))
|
||||
self.assertNotEqual(catalog.catalog_hash(r1), catalog.catalog_hash(r3))
|
||||
r4 = helpers.registry(helpers.make_test("a.one", [("forgectrl", "src/ui.c")], always=True))
|
||||
self.assertNotEqual(catalog.catalog_hash(r1), catalog.catalog_hash(r4))
|
||||
|
||||
def test_validate(self):
|
||||
r = helpers.registry(helpers.make_test("a.one", [], requires=("a.two",)))
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.validate(r)
|
||||
r = helpers.registry(helpers.make_test("a.one", [], requires=("a.two",)),
|
||||
helpers.make_test("a.two", [], requires=("a.one",)))
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.validate(r)
|
||||
|
||||
def test_decorator_rules(self):
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.test("bad id", title="x", subsystem="s")(lambda c: None)
|
||||
with self.assertRaises(ValueError):
|
||||
catalog.test("s.x", title="x", subsystem="s", covers=[("forgetest", "**")])(lambda c: None)
|
||||
|
||||
def test_real_suite_loads_and_validates(self):
|
||||
reg = catalog.load_suite()
|
||||
self.assertIn("image.health", reg)
|
||||
self.assertTrue(reg["image.health"].always)
|
||||
catalog.validate(reg)
|
||||
for t in catalog.all_tests(reg):
|
||||
self.assertEqual(len(t.source_sha), 64)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Runner + HTTP API end to end on localhost with a fake catalog and a
|
||||
fake bench tool. No hardware, no forgectrl."""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import helpers
|
||||
from forgetest import bench as bench_mod
|
||||
from forgetest import catalog, server
|
||||
from forgetest.log import Log
|
||||
from forgetest.runner import Failed, Runner
|
||||
|
||||
|
||||
def t_pass(ctx):
|
||||
ctx.log("hello")
|
||||
ctx.evidence["k"] = 1
|
||||
|
||||
|
||||
def t_prompt(ctx):
|
||||
ans = ctx.prompt("Did the light blink?", ("Yes", "No"))
|
||||
if ans != "Yes":
|
||||
raise Failed("operator said no")
|
||||
|
||||
|
||||
def t_fail(ctx):
|
||||
ctx.check(False, "deliberate")
|
||||
|
||||
|
||||
def t_slow(ctx):
|
||||
ctx.sleep(30)
|
||||
|
||||
|
||||
def t_error(ctx):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
|
||||
class ServerTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.tmp = tempfile.mkdtemp(prefix="forgetest-")
|
||||
os.environ["FORGETEST_DATA"] = cls.tmp
|
||||
os.environ["FORGETEST_MARKER"] = os.path.join(cls.tmp, "marker")
|
||||
cls.man = helpers.make_manifest()
|
||||
cls.reg = helpers.registry(
|
||||
helpers.make_test("fake.pass", [("forgectrl", "src/ui.c")], always=True, fn=t_pass),
|
||||
helpers.make_test("fake.prompt", [("forgectrl", "src/auth.c")], fn=t_prompt, kind="operator"),
|
||||
helpers.make_test("fake.fail", [("forgectrl", "src/cool.c")], fn=t_fail),
|
||||
helpers.make_test("fake.slow", [("forgectrl", "src/main.c")], fn=t_slow),
|
||||
helpers.make_test("fake.error", [("forgectrl", "src/main.c")], fn=t_error),
|
||||
helpers.make_test("fake.live", [("grblhal-glowforge", "src/**")], fn=t_pass, kind="live"),
|
||||
helpers.make_test("fake.needs", [("kernel-module-glowforge", "**")], fn=t_pass,
|
||||
requires=("fake.prompt",)),
|
||||
)
|
||||
# a fake bench tool
|
||||
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"
|
||||
"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",
|
||||
"args": [{"name": "word", "type": "str", "default": "hi", "help": ""},
|
||||
{"name": "n", "type": "int", "default": 2, "help": ""}]},
|
||||
{"id": "unported", "title": "U", "script": "nope.py", "safety": "dry", "where": "host",
|
||||
"ported": False, "desc": "", "args": []},
|
||||
{"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": []}]
|
||||
cls.bench = bench_mod.Bench(tools, tool_dir=cls.tooldir,
|
||||
index_path=os.path.join(cls.tmp, "bench.jsonl"))
|
||||
cls.log = Log(os.path.join(cls.tmp, "results.jsonl"))
|
||||
cls.runner = Runner(cls.log, cls.man, cls.reg, cls.bench)
|
||||
cls.token = server.load_token(os.path.join(cls.tmp, "token"))
|
||||
cls.app = server.App(cls.runner, cls.token, export_dir=os.path.join(cls.tmp, "export"))
|
||||
cls.srv = server.make_server(cls.app, "127.0.0.1", 0)
|
||||
cls.port = cls.srv.server_address[1]
|
||||
cls.th = threading.Thread(target=cls.srv.serve_forever, daemon=True)
|
||||
cls.th.start()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.srv.shutdown()
|
||||
cls.srv.server_close()
|
||||
shutil.rmtree(cls.tmp, ignore_errors=True)
|
||||
|
||||
# -- helpers ----------------------------------------------------------
|
||||
def call(self, method, path, body=None, token=True, headers=None):
|
||||
url = "http://127.0.0.1:%d%s" % (self.port, path)
|
||||
hdrs = {"Host": "127.0.0.1:%d" % self.port}
|
||||
if token:
|
||||
hdrs["X-ForgeFIRM-Token"] = self.token
|
||||
data = None
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode()
|
||||
hdrs["Content-Type"] = "application/json"
|
||||
if headers:
|
||||
hdrs.update(headers)
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=hdrs)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
raw = r.read()
|
||||
st = r.status
|
||||
ct = r.headers.get("Content-Type", "")
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read()
|
||||
st = e.code
|
||||
ct = e.headers.get("Content-Type", "")
|
||||
if "json" in ct:
|
||||
return st, json.loads(raw.decode())
|
||||
return st, raw
|
||||
|
||||
def wait_idle(self, timeout=10):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
st, d = self.call("GET", "/state")
|
||||
if not d["running"]:
|
||||
return d
|
||||
time.sleep(0.1)
|
||||
self.fail("run did not finish")
|
||||
|
||||
def wait_prompt(self, timeout=10):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
st, d = self.call("GET", "/state")
|
||||
if d["running"] and d["running"]["prompt"]:
|
||||
return d["running"]["prompt"]
|
||||
time.sleep(0.05)
|
||||
self.fail("no prompt appeared")
|
||||
|
||||
# -- tests -----------------------------------------------------------------
|
||||
def test_01_auth_and_page(self):
|
||||
st, d = self.call("GET", "/state", token=False)
|
||||
self.assertEqual(st, 200)
|
||||
st, d = self.call("GET", "/", token=False)
|
||||
self.assertEqual(st, 200)
|
||||
self.assertIn(self.token.encode(), d)
|
||||
st, d = self.call("GET", "/state", headers={"Host": "evil.example.net"})
|
||||
self.assertEqual(st, 403)
|
||||
st, d = self.call("GET", "/state", headers={"Origin": "http://evil.example.net"})
|
||||
self.assertEqual(st, 403)
|
||||
st, d = self.call("GET", "/state", headers={"Sec-Fetch-Site": "cross-site"})
|
||||
self.assertEqual(st, 403)
|
||||
st, d = self.call("POST", "/start", {"test": "fake.pass"}, token=False)
|
||||
self.assertEqual(st, 403)
|
||||
self.assertEqual(d["error"], "authentication required")
|
||||
st, d = self.call("GET", "/catalog")
|
||||
self.assertEqual(st, 200)
|
||||
self.assertEqual(len(d["tests"]), 7)
|
||||
st, d = self.call("GET", "/nope")
|
||||
self.assertEqual(st, 404)
|
||||
|
||||
def test_02_run_pass_opens_campaign(self):
|
||||
st, d = self.call("GET", "/state")
|
||||
self.assertIsNone(d["campaign"])
|
||||
st, d = self.call("POST", "/start", {"test": "fake.pass"})
|
||||
self.assertEqual(st, 200, d)
|
||||
state = self.wait_idle()
|
||||
self.assertIsNotNone(state["campaign"])
|
||||
self.assertEqual(state["tests"]["fake.pass"]["status"], "pass")
|
||||
self.assertEqual(state["last_run"]["finished"]["result"], "PASS")
|
||||
st, rec = self.call("GET", "/result?test=fake.pass")
|
||||
self.assertEqual(st, 200)
|
||||
self.assertEqual(rec["evidence"], {"k": 1})
|
||||
self.assertTrue(any("hello" in l for l in rec["log"]))
|
||||
|
||||
def test_03_requires_and_live_gate(self):
|
||||
st, d = self.call("POST", "/start", {"test": "fake.needs"})
|
||||
self.assertEqual(st, 409)
|
||||
self.assertIn("prerequisites", d["message"])
|
||||
st, d = self.call("POST", "/start", {"test": "fake.live"})
|
||||
self.assertEqual(st, 409)
|
||||
self.assertIn("live", d["message"])
|
||||
st, d = self.call("POST", "/start", {"test": "fake.live", "ack_live": True})
|
||||
self.assertEqual(st, 200)
|
||||
state = self.wait_idle()
|
||||
self.assertEqual(state["tests"]["fake.live"]["status"], "pass")
|
||||
st, rec = self.call("GET", "/result?test=fake.live")
|
||||
self.assertTrue(rec["evidence"]["operator"]["ack_live"])
|
||||
|
||||
def test_04_prompt_flow(self):
|
||||
st, d = self.call("POST", "/start", {"test": "fake.prompt"})
|
||||
self.assertEqual(st, 200)
|
||||
p = self.wait_prompt()
|
||||
self.assertEqual(p["options"], ["Yes", "No"])
|
||||
st, d = self.call("POST", "/answer", {"prompt_id": p["id"], "value": "Maybe"})
|
||||
self.assertEqual(st, 409)
|
||||
st, d = self.call("POST", "/answer", {"prompt_id": p["id"], "value": "Yes"})
|
||||
self.assertEqual(st, 200)
|
||||
state = self.wait_idle()
|
||||
self.assertEqual(state["tests"]["fake.prompt"]["status"], "pass")
|
||||
st, rec = self.call("GET", "/result?test=fake.prompt")
|
||||
self.assertEqual(rec["answers"][0]["answer"], "Yes")
|
||||
# now the dependent test may start
|
||||
st, d = self.call("POST", "/start", {"test": "fake.needs"})
|
||||
self.assertEqual(st, 200)
|
||||
self.wait_idle()
|
||||
|
||||
def test_05_busy_abort(self):
|
||||
st, d = self.call("POST", "/start", {"test": "fake.slow"})
|
||||
self.assertEqual(st, 200)
|
||||
st, d = self.call("POST", "/start", {"test": "fake.pass"})
|
||||
self.assertEqual(st, 409)
|
||||
st, d = self.call("POST", "/abort")
|
||||
self.assertEqual(st, 200)
|
||||
state = self.wait_idle()
|
||||
self.assertEqual(state["tests"]["fake.slow"]["status"], "aborted")
|
||||
self.assertIsNotNone(state["campaign"], "an abort does not close the campaign")
|
||||
|
||||
def test_06_error_and_fail_close_campaign(self):
|
||||
st, d = self.call("GET", "/state")
|
||||
cid = d["campaign"]["id"]
|
||||
st, d = self.call("POST", "/start", {"test": "fake.error"})
|
||||
state = self.wait_idle()
|
||||
self.assertEqual(state["tests"]["fake.error"]["status"], "error")
|
||||
self.assertIsNone(state["campaign"])
|
||||
self.assertEqual(state["closed_by"], "fail")
|
||||
# inherited passes survive; the core is required again
|
||||
self.assertEqual(state["tests"]["fake.prompt"]["status"], "inherited")
|
||||
self.assertTrue(state["tests"]["fake.pass"]["required"])
|
||||
# a new start opens a new campaign
|
||||
st, d = self.call("POST", "/start", {"test": "fake.pass"})
|
||||
state = self.wait_idle()
|
||||
self.assertIsNotNone(state["campaign"])
|
||||
self.assertNotEqual(state["campaign"]["id"], cid)
|
||||
|
||||
def test_07_export_and_invalidate(self):
|
||||
st, d = self.call("POST", "/export")
|
||||
self.assertEqual(st, 200)
|
||||
self.assertFalse(d["authorized"])
|
||||
st, raw = self.call("GET", "/export/acceptance.json")
|
||||
self.assertEqual(st, 200)
|
||||
art = raw if isinstance(raw, dict) else json.loads(raw)
|
||||
self.assertEqual(art["manifest_sha"], self.man.content_sha)
|
||||
st, raw = self.call("GET", "/export/acceptance.md")
|
||||
self.assertEqual(st, 200)
|
||||
self.assertIn(b"Release authorized: NO", raw)
|
||||
st, d = self.call("POST", "/invalidate", {"reason": ""})
|
||||
self.assertEqual(st, 400)
|
||||
st, d = self.call("POST", "/invalidate", {"reason": "new tube"})
|
||||
self.assertEqual(st, 200)
|
||||
st, d = self.call("GET", "/state")
|
||||
self.assertEqual(d["invalidate"]["reason"], "new tube")
|
||||
self.assertIsNone(d["campaign"])
|
||||
for tid, ts in d["tests"].items():
|
||||
self.assertNotEqual(ts["status"], "inherited", tid)
|
||||
st, raw = self.call("GET", "/log", token=False)
|
||||
self.assertEqual(st, 200)
|
||||
self.assertIn(b'"t": "invalidate"'.replace(b" ", b""), raw.replace(b" ", b""))
|
||||
|
||||
def test_08_bench(self):
|
||||
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"])
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "unported"})
|
||||
self.assertEqual(st, 409)
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "hot"})
|
||||
self.assertEqual(st, 409)
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "echo", "args": {"word": "yo", "n": "x"}})
|
||||
self.assertEqual(st, 409)
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "echo", "args": {"word": "yo", "n": 5}})
|
||||
self.assertEqual(st, 200, d)
|
||||
state = self.wait_idle()
|
||||
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"]))
|
||||
st, d = self.call("GET", "/bench")
|
||||
self.assertEqual(d["tools"][0]["last"]["result"]["result"], "OK")
|
||||
# a failing tool and an aborted one
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "echo", "args": {"word": "fail", "n": 1}})
|
||||
state = self.wait_idle()
|
||||
self.assertEqual(state["last_run"]["finished"]["result"], "EXIT 3")
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "echo", "args": {"word": "slow", "n": 1}})
|
||||
self.assertEqual(st, 200)
|
||||
time.sleep(0.5)
|
||||
st, d = self.call("POST", "/abort")
|
||||
state = self.wait_idle()
|
||||
self.assertEqual(state["last_run"]["finished"]["result"], "ABORTED")
|
||||
# a takeover tool runs inside the takeover wrapper (init.d is absent on the host: rc 127)
|
||||
st, d = self.call("POST", "/bench/start", {"tool": "tk"})
|
||||
self.assertEqual(st, 200, d)
|
||||
state = self.wait_idle()
|
||||
log = "\n".join(state["last_run"]["log"])
|
||||
self.assertIn("takeover: pulse device free", log)
|
||||
self.assertIn("takeover: forgectrl start", log)
|
||||
self.assertFalse(os.path.exists(os.environ["FORGETEST_MARKER"]))
|
||||
# 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))
|
||||
|
||||
def test_09_recovery_marker(self):
|
||||
marker = os.environ["FORGETEST_MARKER"]
|
||||
with open(marker, "w") as f:
|
||||
f.write("x fake.slow\n")
|
||||
r = Runner(self.log, self.man, self.reg, self.bench)
|
||||
self.assertTrue(any("recovered" in m for m in r.messages))
|
||||
self.assertFalse(os.path.exists(marker))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user