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:
ScottW514
2026-09-02 08:00:51 -04:00
parent 0ca6c4be9f
commit 0e37b0812e
7 changed files with 92 additions and 8 deletions
+16 -3
View File
@@ -96,16 +96,29 @@ def compute(records, tests, manifest, catalog_hash, running=None):
elif t.always:
reason = "always"
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
blocked = None
for r in reversed(hist):
if r.get("result") != PASS or r.get("fingerprint") != fp:
if r.get("fingerprint") != fp:
continue
if epoch and (r.get("ts") or "") <= epoch:
continue
inh = r
break
res = r.get("result")
if res == PASS:
inh = r
break
if res in (FAIL, ERROR):
blocked = r
break
if inh is not None:
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:
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:
+1 -1
View File
@@ -165,7 +165,7 @@ def settings_bounds(ctx):
key = k
break
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)
else:
val = before[key]
+2 -1
View File
@@ -5,7 +5,8 @@ import tempfile
from ..catalog import test
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",
+33
View File
@@ -65,6 +65,39 @@ class CampaignTests(unittest.TestCase):
self.assertEqual(st["tests"][self.cool.id]["status"], "fail")
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):
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")]