diff --git a/plugins/magpie-setup/skills/setup/setup_preflight/lockfile.py b/plugins/magpie-setup/skills/setup/setup_preflight/lockfile.py index cf8ce38df..0167b825a 100644 --- a/plugins/magpie-setup/skills/setup/setup_preflight/lockfile.py +++ b/plugins/magpie-setup/skills/setup/setup_preflight/lockfile.py @@ -61,6 +61,7 @@ class Lock: min_version: str | None = None ref: str | None = None commit: str | None = None + sha512: str | None = None # svn-zip: the released zip's SHA-512 (locks.md) source: str | None = None plugins: list[str] = field(default_factory=list) reconciled: Reconciled | None = None @@ -84,6 +85,7 @@ def _strip_comment(line: str) -> str: def parse(text: str) -> Lock: """Parse the lock's text. Raises `MalformedLock` on anything unexpected.""" lock = Lock() + scalar_keys = {f.name for f in fields(Lock)} - {"plugins", "reconciled"} section: str | None = None for raw in text.splitlines(): line = _strip_comment(raw) @@ -101,6 +103,8 @@ def parse(text: str) -> Lock: elif key == "reconciled": section = "reconciled" lock.reconciled = Reconciled() + elif key == "sha512": + raise MalformedLock("missing value for key: 'sha512'") else: raise MalformedLock(f"unknown block: {key!r}") continue @@ -108,7 +112,7 @@ def parse(text: str) -> Lock: raise MalformedLock(f"not a key: value line: {raw!r}") key, _, value = body.partition(":") key, value = key.strip(), value.strip() - if key in {"method", "url", "min_version", "ref", "commit", "source"}: + if key in scalar_keys: setattr(lock, key, value) else: raise MalformedLock(f"unknown key: {key!r}") @@ -141,6 +145,8 @@ def parse(text: str) -> Lock: continue raise MalformedLock(f"indented line outside any block: {raw!r}") + if lock.sha512 is not None and lock.method != "svn-zip": + raise MalformedLock("'sha512' is only valid for method: 'svn-zip'") return lock diff --git a/tools/setup-preflight/tests/test_core.py b/tools/setup-preflight/tests/test_core.py index a47807f93..9805269ae 100644 --- a/tools/setup-preflight/tests/test_core.py +++ b/tools/setup-preflight/tests/test_core.py @@ -36,7 +36,7 @@ skill_findings, verify_findings, ) -from setup_preflight.lockfile import MalformedLock +from setup_preflight.lockfile import MalformedLock, parse from .conftest import write_lock, write_stamp @@ -218,6 +218,68 @@ def test_the_framework_checkout_has_nothing_to_drift(project: Path) -> None: assert project_findings(project, {"magpie-setup": "0.0.1"}) == [] +SVN_ZIP_SHA512 = "3f2a9c4e0b7d1a8e5c6f9b2d4a7e0c3f6b9d2a5e8c1f4b7d0a3e6c9f2b5d8a1e" * 2 +SVN_ZIP_URL = "https://dist.apache.org/repos/dist/release/magpie/0.2.0/apache-magpie-0.2.0-source.zip" +SVN_ZIP_PIN = f"""\ +method: svn-zip +url: {SVN_ZIP_URL} +ref: 0.2.0 +sha512: {SVN_ZIP_SHA512} +""" + + +@pytest.mark.parametrize("method_first", [True, False], ids=["method-first", "method-last"]) +def test_the_svn_zip_lock_setup_writes_parses(project: Path, method_first: bool) -> None: + """`locks.md` and `install.md` write `sha512:` for `method: svn-zip`; + rejecting it failed every skill's pre-flight for an adopter installed + from the signed source release.""" + pin = SVN_ZIP_PIN + if not method_first: + method, _, rest = pin.partition("\n") + pin = rest + method + "\n" + assert parse(pin).sha512 == SVN_ZIP_SHA512 + write_lock(project, pin) + assert codes(project_findings(project, None)) == ["snapshot-never-fetched"] + + +@pytest.mark.parametrize("fetched_commit", ["", "0.2.0"], ids=["legacy-empty", "upgrade-version"]) +def test_the_documented_svn_zip_local_lock_is_accepted(project: Path, fetched_commit: str) -> None: + """Existing local locks may record an empty commit or the fetched zip's version; + both remain readable when their recorded pin fields match.""" + write_lock(project, SVN_ZIP_PIN) + write_local_lock(project, method="svn-zip", url=SVN_ZIP_URL, ref="0.2.0", commit=fetched_commit) + assert project_findings(project, None) == [] + + +@pytest.mark.parametrize("method", ["git-tag", "git-branch", "marketplace", "local", None, "other"]) +@pytest.mark.parametrize("method_first", [True, False], ids=["method-first", "method-last"]) +def test_sha512_is_rejected_outside_svn_zip(method: str | None, method_first: bool) -> None: + """A mistyped git commit anchor must not become an ignored zip digest.""" + method_line = f"method: {method}\n" if method is not None else "" + hash_line = f"sha512: {SVN_ZIP_SHA512}\n" + pin = method_line + hash_line if method_first else hash_line + method_line + with pytest.raises(MalformedLock, match=r"sha512.*svn-zip"): + parse(pin) + + +@pytest.mark.parametrize("value", ["", " ", " # the digest is missing"]) +def test_an_empty_sha512_names_the_missing_value(value: str) -> None: + with pytest.raises(MalformedLock, match=r"missing value.*sha512"): + parse(f"method: svn-zip\nsha512:{value}\n") + + +def test_an_unknown_scalar_key_is_still_rejected(project: Path) -> None: + write_lock(project, SVN_ZIP_PIN + "sha521: abc\n") + with pytest.raises(MalformedLock, match="unknown key: 'sha521'"): + project_findings(project, None) + + +@pytest.mark.parametrize("block", ["plugins", "reconciled"]) +def test_a_structured_block_cannot_be_assigned_a_scalar(block: str) -> None: + with pytest.raises(MalformedLock, match=f"unknown key: '{block}'"): + parse(f"method: marketplace\n{block}: not-a-block\n") + + def test_a_malformed_lock_raises_rather_than_reading_as_no_lock(project: Path) -> None: write_lock(project, "method: marketplace\nnonsense\n") with pytest.raises(MalformedLock):