diff --git a/scripts/generate_pack.py b/scripts/generate_pack.py index 354964481..458fa968b 100644 --- a/scripts/generate_pack.py +++ b/scripts/generate_pack.py @@ -1135,7 +1135,10 @@ def generate_pack( config.get("hash_rule", ""), ) if refusal: - file_status[dedup_key] = "excluded" + # An earlier declaration of this path shipped a file this + # one refuses: it is in the pack, untested as verify + # counts it, not excluded. + file_status[dedup_key] = "untested" if already_packed else "excluded" file_reasons[dedup_key] = refusal continue if not hash_mismatch_excludes_file(verification_mode) and ( @@ -1171,6 +1174,9 @@ def generate_pack( if already_packed: continue + if file_status.get(dedup_key) == "excluded": + # An earlier declaration refused the file this one ships. + file_status[dedup_key] = "untested" seen_destinations.add(dedup_key) _register_path(dedup_key, seen_destinations, seen_parents) if case_insensitive: diff --git a/tests/test_twin_declaration_status.py b/tests/test_twin_declaration_status.py new file mode 100644 index 000000000..d9967a2b8 --- /dev/null +++ b/tests/test_twin_declaration_status.py @@ -0,0 +1,98 @@ +"""A path declared twice, once unpinned and once with a pin the copy misses. + +The unpinned declaration ships the file and the pinned one refuses it. The +builder wrote "excluded" over the shipped path, so its summary said "1 unsafe +excluded" for a file inside the archive, while verify counted it untested +and listed no exclusion. Both orders of the two systems are checked: the +refusal read first left "excluded" behind the file the second one ships. +""" + +from __future__ import annotations + +import contextlib +import hashlib +import io +import os +import sys +import tempfile +import unittest +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +import common # noqa: E402 +import generate_db # noqa: E402 +import generate_pack as builder # noqa: E402 +from verify import Status, verify_platform # noqa: E402 + +PAYLOAD = b"a console firmware" +SHA1 = hashlib.sha1(PAYLOAD).hexdigest() + + +class TwinDeclarationStatus(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") + self._cwd = os.getcwd() + os.chdir(self._tmp.name) + for d in ("emulators", "platforms", "bios/Twin"): + Path(d).mkdir(parents=True) + Path("bios/Twin/fw.bin").write_bytes(PAYLOAD) + files = {SHA1: { + "path": "bios/Twin/fw.bin", "name": "fw.bin", "size": len(PAYLOAD), + "sha1": SHA1, "md5": hashlib.md5(PAYLOAD).hexdigest(), + "sha256": hashlib.sha256(PAYLOAD).hexdigest(), "crc32": "00000001", + }} + self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})} + Path("platforms/_registry.yml").write_text( + yaml.dump({"platforms": {"twin": {"status": "active"}}}) + ) + self.addCleanup(common._platform_config_cache.clear) + + def tearDown(self): + os.chdir(self._cwd) + self._tmp.cleanup() + + def _config(self, unpinned: str, pinned: str) -> dict: + config = { + "platform": "Twin", "verification_mode": "md5", + "systems": { + unpinned: {"files": [{"name": "fw.bin", "destination": "fw.bin"}]}, + pinned: {"files": [{ + "name": "fw.bin", "destination": "fw.bin", "md5": "0" * 32, + }]}, + }, + } + Path("platforms/twin.yml").write_text(yaml.dump(config)) + common._platform_config_cache.clear() + return config + + def _check(self, config: dict) -> None: + record = builder.PackRecord() + out = io.StringIO() + with contextlib.redirect_stdout(out): + builder.generate_pack( + "twin", "platforms", self.db, "bios", "out", + emulators_dir="emulators", emu_profiles={}, record=record, + offline=True, + ) + self.assertIn("fw.bin", record.members) + summary = out.getvalue() + self.assertNotIn("excluded", summary.lower()) + self.assertIn("0/1 files OK, 1 untested", summary) + self.assertIn("UNTESTED: fw.bin", summary) + report = verify_platform(config, self.db, "emulators", {}) + self.assertEqual(report["status_counts"], {Status.UNTESTED: 1}) + self.assertEqual(report["excluded_files"], []) + + def test_unpinned_declaration_read_first(self): + self._check(self._config("aaa", "bbb")) + + def test_pinned_declaration_read_first(self): + self._check(self._config("zzz", "bbb")) + + +if __name__ == "__main__": + unittest.main()