"""A restored cache is usable only for the exact proven build inputs.""" from __future__ import annotations import json from pathlib import Path import subprocess import sys import pytest from scripts.termux import wheelhouse_cache def cache_tree(tmp_path: Path) -> tuple[Path, dict[str, str]]: payload = tmp_path / "payload" wheelhouse = payload / "wheelhouse" work = payload / ".work" wheelhouse.mkdir(parents=True) work.mkdir() (wheelhouse / "example-1.0-py3-none-any.whl").write_bytes(b"cache artifact") (work / "resolved.txt").write_text("example\t==1.0\t\n", encoding="utf-8") (work / "build_set.txt").write_text("example\n", encoding="utf-8") identity = {"lock": "lock-a", "python": "python-a", "builder": "image-a"} return payload, identity @pytest.mark.parametrize("bom", [b"", b"\xef\xbb\xbf"]) def test_cache_rejects_changed_inputs_and_modified_outputs(tmp_path, bom): payload, identity = cache_tree(tmp_path) wheelhouse_cache.write_manifest(payload, identity) index = payload / "index.json" index.write_bytes(bom + index.read_bytes()) assert wheelhouse_cache.is_usable(payload, identity) for key in identity: assert not wheelhouse_cache.is_usable(payload, {**identity, key: "changed"}) artifact = next((payload / "wheelhouse").glob("*.whl")) artifact.write_bytes(b"corrupt artifact") assert not wheelhouse_cache.is_usable(payload, identity) @pytest.mark.parametrize("damage", ["empty", "missing", "extra", "requirements", "build-set", "traversal", "malformed"]) def test_cache_rejects_incomplete_or_inconsistent_manifests(tmp_path, damage): payload, identity = cache_tree(tmp_path) wheelhouse_cache.write_manifest(payload, identity) manifest_path = payload / "index.json" manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if damage == "empty": manifest["wheels"] = [] manifest_path.write_text(json.dumps(manifest), encoding="utf-8") elif damage == "missing": next((payload / "wheelhouse").glob("*.whl")).unlink() elif damage == "extra": (payload / "wheelhouse/foreign-9.0-py3-none-any.whl").write_bytes(b"foreign") elif damage == "requirements": (payload / ".work/resolved.txt").write_text("other\t==9.0\t\n", encoding="utf-8") elif damage == "build-set": (payload / ".work/build_set.txt").unlink() elif damage == "traversal": manifest["wheels"][0]["name"] = "../outside.whl" manifest_path.write_text(json.dumps(manifest), encoding="utf-8") else: manifest_path.write_text("{broken", encoding="utf-8") assert not wheelhouse_cache.is_usable(payload, identity) def test_cache_cli_keeps_commit_and_tag_provenance_distinct(tmp_path): payload, _ = cache_tree(tmp_path) repo = Path(__file__).resolve().parents[2] args = [sys.executable, str(repo / 'scripts/termux/wheelhouse_cache.py'), 'write', '--payload', str(payload), '--repo', str(repo), '--builder', 'fixture-image', '--platform-tag', 'android_24_arm64_v8a', '--python-abi', 'cp314'] commit = 'a' * 40 result = subprocess.run([*args, '--commit', commit], cwd=tmp_path, capture_output=True, text=True, encoding='utf-8', timeout=30) assert result.returncode == 0, result.stdout + result.stderr manifest_path = payload / 'index.json' manifest = json.loads(manifest_path.read_text(encoding='utf-8')) assert manifest['commit'] == commit and 'tag' not in manifest assert wheelhouse_cache.is_usable(payload, manifest['inputs']) before = manifest_path.read_bytes() for flags in (['--commit', 'short'], ['--commit', commit, '--tag', 'v1.2.3']): refused = subprocess.run([*args, *flags], cwd=tmp_path, capture_output=True, timeout=30) assert refused.returncode != 0 assert manifest_path.read_bytes() == before tagged = subprocess.run([*args, '--tag', 'v1.2.3'], cwd=tmp_path, capture_output=True, text=True, encoding='utf-8', timeout=30) assert tagged.returncode == 0, tagged.stdout + tagged.stderr manifest = json.loads(manifest_path.read_text(encoding='utf-8')) assert manifest['tag'] == 'v1.2.3' and 'commit' not in manifest