mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 08:41:13 -07:00
acceptance: a later FAIL blocks inheritance, ffboot is a component, the units fallback is valid
The inheritance walk skipped every record that was not a PASS on the current fingerprint, so a FAIL or ERROR recorded after a PASS on the same image was stepped over and the older PASS inherited into the next campaign. The newest record on the fingerprint now decides: a PASS is inherited, a FAIL or ERROR blocks it (reason failed-since), an ABORTED run says nothing. Unit tests for all three orders. ffboot, the tool that rewrites the boot environment on every install and slot switch, was packaged from scripts/ outside every fingerprint. It now lives in the recipe's files and the recipe inherits the manifest class; the tree manifest tool fingerprints file components the same way, and the update tests cover the component. forgectrl.settings-bounds fell back to ui_units=mm, which the whitelist refuses, so the always-required test failed on a fresh machine; the fallback is metric.
This commit is contained in:
@@ -96,16 +96,29 @@ def compute(records, tests, manifest, catalog_hash, running=None):
|
|||||||
elif t.always:
|
elif t.always:
|
||||||
reason = "always"
|
reason = "always"
|
||||||
else:
|
else:
|
||||||
|
# The newest record on the current fingerprint decides: a PASS
|
||||||
|
# is inherited, a FAIL or ERROR after it blocks the inheritance
|
||||||
|
# (the test has to be run again), an ABORTED run says nothing.
|
||||||
inh = None
|
inh = None
|
||||||
|
blocked = None
|
||||||
for r in reversed(hist):
|
for r in reversed(hist):
|
||||||
if r.get("result") != PASS or r.get("fingerprint") != fp:
|
if r.get("fingerprint") != fp:
|
||||||
continue
|
continue
|
||||||
if epoch and (r.get("ts") or "") <= epoch:
|
if epoch and (r.get("ts") or "") <= epoch:
|
||||||
continue
|
continue
|
||||||
inh = r
|
res = r.get("result")
|
||||||
break
|
if res == PASS:
|
||||||
|
inh = r
|
||||||
|
break
|
||||||
|
if res in (FAIL, ERROR):
|
||||||
|
blocked = r
|
||||||
|
break
|
||||||
if inh is not None:
|
if inh is not None:
|
||||||
status, satisfied, origin, reason = "inherited", True, inh, "inherited"
|
status, satisfied, origin, reason = "inherited", True, inh, "inherited"
|
||||||
|
elif blocked is not None and any(r.get("result") == PASS and r.get("fingerprint") == fp
|
||||||
|
and (r.get("ts") or "") < (blocked.get("ts") or "")
|
||||||
|
for r in hist):
|
||||||
|
reason = "failed-since"
|
||||||
else:
|
else:
|
||||||
reason = "domain-changed" if any(r.get("result") == PASS for r in hist) else "never-passed"
|
reason = "domain-changed" if any(r.get("result") == PASS for r in hist) else "never-passed"
|
||||||
if not satisfied and last_r is not None:
|
if not satisfied and last_r is not None:
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ def settings_bounds(ctx):
|
|||||||
key = k
|
key = k
|
||||||
break
|
break
|
||||||
if key is None:
|
if key is None:
|
||||||
key, val = "ui_units", "mm"
|
key, val = "ui_units", "metric"
|
||||||
ctx.log("no settable key is present; writing %s=%s (recorded in evidence)", key, val)
|
ctx.log("no settable key is present; writing %s=%s (recorded in evidence)", key, val)
|
||||||
else:
|
else:
|
||||||
val = before[key]
|
val = before[key]
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import tempfile
|
|||||||
from ..catalog import test
|
from ..catalog import test
|
||||||
from .. import hw
|
from .. import hw
|
||||||
|
|
||||||
_UPDATE_COVERS = [("forgectrl", "src/update.c"), ("forgectrl", "src/update.h")]
|
_UPDATE_COVERS = [("forgectrl", "src/update.c"), ("forgectrl", "src/update.h"),
|
||||||
|
("ffboot", "**")]
|
||||||
|
|
||||||
|
|
||||||
@test("update.slots-and-signature", title="Boot slots readable, unsigned/tampered archives refused",
|
@test("update.slots-and-signature", title="Boot slots readable, unsigned/tampered archives refused",
|
||||||
|
|||||||
@@ -65,6 +65,39 @@ class CampaignTests(unittest.TestCase):
|
|||||||
self.assertEqual(st["tests"][self.cool.id]["status"], "fail")
|
self.assertEqual(st["tests"][self.cool.id]["status"], "fail")
|
||||||
self.assertEqual(st["tests"][self.cool.id]["reason"], "never-passed")
|
self.assertEqual(st["tests"][self.cool.id]["reason"], "never-passed")
|
||||||
|
|
||||||
|
def test_a_fail_after_a_pass_blocks_inheritance(self):
|
||||||
|
# The ui test passes in c1, then fails on the same image (an
|
||||||
|
# intermittent gate): the FAIL closes c1. In c2 the older PASS must
|
||||||
|
# not be inherited over the newer FAIL: the test is required again.
|
||||||
|
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"),
|
||||||
|
rec_result("c1", self.ui, self.man, "FAIL", "2026-08-20T10:02:00Z"),
|
||||||
|
rec_campaign("c2", self.man, self.chash, "2026-08-20T11:00:00Z")]
|
||||||
|
st = self.compute(recs)
|
||||||
|
t = st["tests"][self.ui.id]
|
||||||
|
self.assertEqual(t["status"], "fail")
|
||||||
|
self.assertEqual(t["reason"], "failed-since")
|
||||||
|
self.assertTrue(t["required"])
|
||||||
|
self.assertFalse(t["satisfied"])
|
||||||
|
self.assertFalse(st["authorized"])
|
||||||
|
|
||||||
|
def test_an_abort_after_a_pass_does_not_block_inheritance(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"),
|
||||||
|
rec_result("c1", self.ui, self.man, "ABORTED", "2026-08-20T10:02:00Z"),
|
||||||
|
rec_campaign("c2", self.man, self.chash, "2026-08-20T11:00:00Z")]
|
||||||
|
st = self.compute(recs)
|
||||||
|
self.assertEqual(st["tests"][self.ui.id]["status"], "inherited")
|
||||||
|
|
||||||
|
def test_a_pass_after_a_fail_is_inherited(self):
|
||||||
|
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z"),
|
||||||
|
rec_result("c1", self.ui, self.man, "FAIL", "2026-08-20T10:01:00Z"),
|
||||||
|
rec_campaign("c2", self.man, self.chash, "2026-08-20T11:00:00Z"),
|
||||||
|
rec_result("c2", self.ui, self.man, "PASS", "2026-08-20T11:01:00Z"),
|
||||||
|
rec_campaign("c3", self.man, self.chash, "2026-08-20T12:00:00Z")]
|
||||||
|
st = self.compute(recs)
|
||||||
|
self.assertEqual(st["tests"][self.ui.id]["status"], "inherited")
|
||||||
|
|
||||||
def test_error_closes_aborted_does_not(self):
|
def test_error_closes_aborted_does_not(self):
|
||||||
recs = [rec_campaign("c1", self.man, self.chash, "2026-08-20T10:00:00Z"),
|
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")]
|
rec_result("c1", self.ui, self.man, "ABORTED", "2026-08-20T10:01:00Z")]
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ layout."
|
|||||||
LICENSE = "MIT"
|
LICENSE = "MIT"
|
||||||
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
|
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
|
||||||
|
|
||||||
# ffboot's canonical home is scripts/ffboot in this repo (the factory-side
|
# ffboot rewrites the boot environment on every install and slot switch,
|
||||||
# installer downloads it from there); the recipe packages that same file.
|
# so it is a fingerprinted component like the daemons: the manifest entry
|
||||||
FILESEXTRAPATHS:prepend := "${THISDIR}/../../../scripts:"
|
# records the two files, and the acceptance tests that cover ffboot are
|
||||||
|
# invalidated when either moves (the installer copies the tool out of the
|
||||||
|
# rootfs it just wrote).
|
||||||
|
inherit forgefirm-manifest
|
||||||
|
FORGEFIRM_MANIFEST_SRC = "${THISDIR}/files"
|
||||||
|
|
||||||
SRC_URI = " \
|
SRC_URI = " \
|
||||||
file://ffboot \
|
file://ffboot \
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ RECIPES = [
|
|||||||
("python3-gfhardware", "meta-glowforge-bsp/recipes-devtools/python/python3-gfhardware.bb", "meta-openglow"),
|
("python3-gfhardware", "meta-glowforge-bsp/recipes-devtools/python/python3-gfhardware.bb", "meta-openglow"),
|
||||||
("python3-gfutilities", "meta-openglow-core/recipes-devtools/python/python3-gfutilities_git.bb", "meta-openglow"),
|
("python3-gfutilities", "meta-openglow-core/recipes-devtools/python/python3-gfutilities_git.bb", "meta-openglow"),
|
||||||
]
|
]
|
||||||
|
# Components built from files in a layer (FORGEFIRM_MANIFEST_SRC in the
|
||||||
|
# recipe): (component, directory relative to the repo, layer, recipe).
|
||||||
|
FILE_COMPONENTS = [
|
||||||
|
("ffboot", "meta-forgefirm/recipes-forgefirm/ffboot/files", "forgefirm", "ffboot.bb"),
|
||||||
|
]
|
||||||
CONTENT_LAYERS = {"meta-forgefirm": ("forgefirm", "meta-forgefirm"),
|
CONTENT_LAYERS = {"meta-forgefirm": ("forgefirm", "meta-forgefirm"),
|
||||||
"meta-glowforge-bsp": ("meta-openglow", "meta-glowforge-bsp"),
|
"meta-glowforge-bsp": ("meta-openglow", "meta-glowforge-bsp"),
|
||||||
"meta-openglow-core": ("meta-openglow", "meta-openglow-core")}
|
"meta-openglow-core": ("meta-openglow", "meta-openglow-core")}
|
||||||
@@ -138,6 +143,29 @@ def ls_tree(repo, rev, url, cache, prefix, files):
|
|||||||
ls_tree(sub_repo, obj, sub_url, cache, prefix + path + "/", files)
|
ls_tree(sub_repo, obj, sub_url, cache, prefix + path + "/", files)
|
||||||
|
|
||||||
|
|
||||||
|
def dir_files(path):
|
||||||
|
"""[path, blob-id] per regular file under path, relative paths, the
|
||||||
|
same walk and hashing as forgefirm-manifest.bbclass (hash-object, so
|
||||||
|
the ids compare with tree ids)."""
|
||||||
|
paths = []
|
||||||
|
for root, dirs, fns in os.walk(path):
|
||||||
|
dirs[:] = sorted(x for x in dirs if x not in (".git", "__pycache__"))
|
||||||
|
for fn in fns:
|
||||||
|
if fn.endswith((".pyc", ".pyo")):
|
||||||
|
continue
|
||||||
|
p = os.path.join(root, fn)
|
||||||
|
if os.path.isfile(p) and not os.path.islink(p):
|
||||||
|
paths.append(os.path.relpath(p, path).replace(os.sep, "/"))
|
||||||
|
paths.sort()
|
||||||
|
if not paths:
|
||||||
|
return []
|
||||||
|
# absolute paths: hash-object --stdin-paths resolves relative ones against
|
||||||
|
# the enclosing repository's top level, not the cwd
|
||||||
|
ids = git(["hash-object", "--stdin-paths"], cwd=path,
|
||||||
|
input=(chr(10).join(os.path.join(path, p) for p in paths) + chr(10)).encode()).decode().split()
|
||||||
|
return [[p, i] for p, i in zip(paths, ids)]
|
||||||
|
|
||||||
|
|
||||||
def layer_content(path):
|
def layer_content(path):
|
||||||
out = git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], cwd=path)
|
out = git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], cwd=path)
|
||||||
paths = sorted(set(p.decode("utf-8", "replace") for p in out.split(b"\0") if p))
|
paths = sorted(set(p.decode("utf-8", "replace") for p in out.split(b"\0") if p))
|
||||||
@@ -176,6 +204,11 @@ def main(argv=None):
|
|||||||
files.sort()
|
files.sort()
|
||||||
components[name] = {"srcrev": rev, "source": url, "files": files, "recipes": [os.path.basename(rel)]}
|
components[name] = {"srcrev": rev, "source": url, "files": files, "recipes": [os.path.basename(rel)]}
|
||||||
print("%s: %s (%d files)" % (name, rev[:12], len(files)), file=sys.stderr)
|
print("%s: %s (%d files)" % (name, rev[:12], len(files)), file=sys.stderr)
|
||||||
|
for name, rel, layer, recipe in FILE_COMPONENTS:
|
||||||
|
base = REPO if layer == "forgefirm" else args.meta_openglow
|
||||||
|
files = dir_files(os.path.join(base, rel))
|
||||||
|
components[name] = {"srcrev": None, "source": "files", "files": files, "recipes": [recipe]}
|
||||||
|
print("%s: files (%d files)" % (name, len(files)), file=sys.stderr)
|
||||||
|
|
||||||
ksrc = args.kernel_srcrev
|
ksrc = args.kernel_srcrev
|
||||||
if not ksrc:
|
if not ksrc:
|
||||||
|
|||||||
Reference in New Issue
Block a user