refactor: single-source the native mode policy

verify.py and generate_pack.py must reach the same verdict on the same
file, and CLAUDE.md calls any divergence critical, but each spelled out
mode == 'existence' in its own words. Both now ask nativemode whether
the frontend reads the file's bytes, which is the one fact the rest
follows from, and a test holds the two answers together.

The BaseScraper contract the wiki asks contributors to implement had no
caller anywhere, so nothing proved compare_with_config, has_changes or
test_connection still worked.
This commit is contained in:
Abdessamad Derraz committed 2026-08-11 00:54:46 +02:00
1 parent 057b0c18f2
commit ab6a3bb26d
4 files changed
+380 -6

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""The native-mode policy verify.py and generate_pack.py must share.
CLAUDE.md states the invariant as "verify.py and generate_pack.py must
produce identical results; any divergence is a critical bug". It used to rest
on both files spelling out `mode == "existence"` in their own words. Routing
both through scripts/nativemode.py makes the rule single-sourced, and these
tests make the agreement checkable instead of reviewable.
"""
from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import nativemode # noqa: E402
class ModeVocabulary(unittest.TestCase):
def test_modes_match_the_platform_schema(self):
"""A mode added to one side and not the other is the bug this catches."""
schema = json.loads(
(REPO_ROOT / "schemas" / "platform.schema.json").read_text()
)
enum = schema["properties"]["verification_mode"]["enum"]
self.assertEqual(sorted(nativemode.MODES), sorted(enum))
def test_schema_default_matches_the_module_default(self):
schema = json.loads(
(REPO_ROOT / "schemas" / "platform.schema.json").read_text()
)
self.assertEqual(
schema["properties"]["verification_mode"]["default"],
nativemode.DEFAULT_MODE,
)
def test_unknown_or_missing_mode_falls_back_to_the_default(self):
for value in (None, "", "sha256", "MD5"):
self.assertEqual(nativemode.normalize(value), nativemode.DEFAULT_MODE)
def test_every_declared_platform_mode_is_known(self):
import yaml
for path in sorted((REPO_ROOT / "platforms").glob("*.yml")):
if path.name.startswith("_"):
continue
data = yaml.safe_load(path.read_text()) or {}
mode = data.get("verification_mode")
if mode is None:
continue
self.assertIn(mode, nativemode.MODES, f"{path.name} declares {mode!r}")
class ContentReadingPolicy(unittest.TestCase):
def test_existence_reads_nothing(self):
self.assertFalse(nativemode.reads_file_contents("existence"))
self.assertIsNone(nativemode.digest_algorithm("existence"))
def test_digest_modes_read_their_digest(self):
self.assertEqual(nativemode.digest_algorithm("md5"), "md5")
self.assertEqual(nativemode.digest_algorithm("sha1"), "sha1")
self.assertTrue(nativemode.reads_file_contents("md5"))
self.assertTrue(nativemode.reads_file_contents("sha1"))
def test_exclusion_follows_content_reading_for_every_mode(self):
"""The builder omits exactly what the frontend would reject."""
for mode in nativemode.MODES:
self.assertEqual(
nativemode.hash_mismatch_excludes_file(mode),
nativemode.reads_file_contents(mode),
f"mode {mode} disagrees with itself",
)
class BothConsumersAgree(unittest.TestCase):
"""verify.py and generate_pack.py must answer the same for every mode."""
def test_builder_and_verifier_share_one_predicate(self):
from generate_pack import _intentional_hash_exclusion
from verify import compute_severity # noqa: F401 (import proves wiring)
for mode in nativemode.MODES:
# No entries is never an exclusion, whatever the mode.
self.assertFalse(_intentional_hash_exclusion([], {}, verification_mode=mode))
# The builder must refuse to exclude under existence even when asked.
self.assertFalse(
_intentional_hash_exclusion(
[{"name": "x.bin"}], {}, verification_mode="existence"
)
)
def test_severity_uses_the_shared_predicate(self):
from verify import Severity, Status, compute_severity
# Existence mode: a missing required file is a warning, not critical.
self.assertEqual(
compute_severity(Status.MISSING, True, "existence"),
Severity.WARNING,
)
# Digest mode: the same absence is critical.
self.assertEqual(
compute_severity(Status.MISSING, True, "md5"),
Severity.CRITICAL,
)
# An unknown mode must fall back to existence, not to the strictest
# reading: a typo in a platform YAML should not invent CRITICALs.
self.assertEqual(
compute_severity(Status.MISSING, True, "sha256"),
Severity.WARNING,
)
if __name__ == "__main__":
unittest.main()
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""The BaseScraper contract the wiki asks contributors to implement.
wiki/adding-a-scraper.md and wiki/adding-a-platform.md document
compare_with_config, ChangeSet.has_changes and test_connection as the
interface a new scraper plugs into. No scraper in the repository calls them,
so nothing proved they still worked; a documented contract that is never
exercised is a promise to contributors backed by nothing.
"""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from scraper.base_scraper import ( # noqa: E402
BaseScraper,
BiosRequirement,
ChangeSet,
)
class _StubScraper(BaseScraper):
"""Minimal scraper: returns what it was handed, or raises."""
def __init__(self, requirements=None, error: Exception | None = None):
super().__init__(url="https://example.test/bios.xml")
self._requirements = requirements or []
self._error = error
def fetch_requirements(self) -> list[BiosRequirement]:
if self._error is not None:
raise self._error
return self._requirements
def validate_format(self, raw_data: str) -> bool:
return True
def _config(*files: dict) -> dict:
return {"systems": {"test-sys": {"files": list(files)}}}
class ChangeSetSummary(unittest.TestCase):
def test_empty_changeset_has_no_changes(self):
changes = ChangeSet()
self.assertFalse(changes.has_changes)
self.assertEqual(changes.summary(), "no changes")
def test_summary_counts_each_kind(self):
req = BiosRequirement(name="a.bin", system="test-sys")
changes = ChangeSet(added=[req], removed=[req], modified=[(req, req)])
self.assertTrue(changes.has_changes)
self.assertEqual(changes.summary(), "+1 added, -1 removed, ~1 modified")
class CompareWithConfig(unittest.TestCase):
def test_new_upstream_file_is_reported_as_added(self):
scraper = _StubScraper([BiosRequirement(name="new.bin", system="test-sys")])
changes = scraper.compare_with_config(_config())
self.assertEqual([r.name for r in changes.added], ["new.bin"])
self.assertTrue(changes.has_changes)
def test_file_dropped_upstream_is_reported_as_removed(self):
scraper = _StubScraper([])
changes = scraper.compare_with_config(_config({"name": "old.bin"}))
self.assertEqual([r.name for r in changes.removed], ["old.bin"])
def test_changed_sha1_is_reported_as_modified(self):
scraper = _StubScraper(
[BiosRequirement(name="a.bin", system="test-sys", sha1="b" * 40)]
)
changes = scraper.compare_with_config(_config({"name": "a.bin", "sha1": "a" * 40}))
self.assertEqual(len(changes.modified), 1)
before, after = changes.modified[0]
self.assertEqual((before.sha1, after.sha1), ("a" * 40, "b" * 40))
def test_changed_md5_is_reported_when_no_sha1_is_declared(self):
scraper = _StubScraper(
[BiosRequirement(name="a.bin", system="test-sys", md5="b" * 32)]
)
changes = scraper.compare_with_config(_config({"name": "a.bin", "md5": "a" * 32}))
self.assertEqual(len(changes.modified), 1)
def test_identical_hashes_are_not_a_change(self):
scraper = _StubScraper(
[BiosRequirement(name="a.bin", system="test-sys", sha1="a" * 40)]
)
changes = scraper.compare_with_config(_config({"name": "a.bin", "sha1": "a" * 40}))
self.assertFalse(changes.has_changes)
def test_a_file_in_another_system_is_not_the_same_file(self):
scraper = _StubScraper([BiosRequirement(name="a.bin", system="other-sys")])
changes = scraper.compare_with_config(_config({"name": "a.bin"}))
self.assertEqual([r.name for r in changes.added], ["a.bin"])
self.assertEqual([r.name for r in changes.removed], ["a.bin"])
class TestConnection(unittest.TestCase):
def test_reachable_source_reports_true(self):
self.assertTrue(_StubScraper([]).test_connection())
def test_network_failure_reports_false(self):
self.assertFalse(_StubScraper(error=OSError("unreachable")).test_connection())
def test_bad_payload_reports_false(self):
self.assertFalse(_StubScraper(error=ValueError("bad format")).test_connection())
def test_an_unexpected_error_is_not_swallowed(self):
"""Only reachability failures are absorbed; a bug must surface."""
with self.assertRaises(KeyError):
_StubScraper(error=KeyError("bug")).test_connection()
if __name__ == "__main__":
unittest.main()