3"""Both-directions selftest for the vendored-SOUP upstream provenance gate.
5It lives beside ``check_soup_upstream.py`` rather than inside it only because
6the checker crossed the project file-size cap; ``--selftest`` on that script is
7still the entry point, and the ``soup-upstream`` gate runs it before the scan.
9Two properties are asserted, because this claim -- "byte-identical to upstream"
10-- was stated in three places and checked by nothing, so every tree passed it
11and a drifted one would have too:
13 * **it fires.** A mutated blob, a changed file mode, a lost file, a ghost
14 manifest row, an edit on top of a reviewed patch, an undeclared deviation,
15 a stale declaration, a pin that disagrees with the SBOM's, and every
16 vacuity floor -- each is provoked one at a time against a REAL scratch git
17 repository and driven through ``run_check()``, the function CI calls.
18 * **it stays quiet.** The untouched fixture verifies clean, every record kind
19 round-trips through the manifest format, and the shipped floors are real
20 numbers rather than zero-with-a-comment.
23from __future__
import annotations
30from collections.abc
import Callable
31from pathlib
import Path
33sys.path.insert(0, str(Path(__file__).resolve().parent))
34sys.path.insert(0, str(Path(__file__).resolve().parents[1] /
"gen"))
35sys.path.insert(0, str(Path(__file__).resolve().parents[1] /
"dev"))
37from check_soup_upstream
import (
43 MIN_UPSTREAM_VERIFIED,
48from git_environment
import isolated_git_environment, trusted_git_executable
49from sbom_registry
import Component
50from soup_manifest
import (
67FIXTURE_PATH =
"libs/third_party/fixture"
68FIXTURE_FLOORS = (1, 1, 1)
71FIXTURE_VERIFIED_ROWS = 2
75FLOOR_SANITY_MIN = 1000
77 "src/a.c": b
"int a;\n",
78 "src/b.c": b
"int b;\n",
80 "patched.c": b
"int patched; /* local */\n",
81 "generated.h": b
"/* generated here, not upstream */\n",
85def _quiet(func: Callable[..., int], *args: object) -> int:
86 """Call `func` with its diagnostics captured, returning only its status.
88 The must-fire cases below deliberately provoke real failures; letting their
89 error text through would bury the pass/fail report the selftest exists to
90 print. The status is what is asserted, so only the status is kept.
93 with contextlib.redirect_stdout(sink), contextlib.redirect_stderr(sink):
97def _fixture_component(
100 upstream_commit: str =
"0" * 40,
101 modified: bool =
True,
102 patched_files: tuple[tuple[str, str], ...] = ((
"patched.c",
"selftest patch"),),
103 local_files: tuple[tuple[str, str], ...] = ((
"generated.h",
"selftest local"),),
105 """Build the selftest's synthetic registry entry."""
111 url=
"https://example.invalid/fixture",
113 provenance=
"commit-pinned-sha256",
114 description=
"selftest fixture",
115 upstream_commit=upstream_commit,
117 patched_files=patched_files,
118 local_files=local_files,
122def _git(args: list[str], cwd: Path) ->
None:
123 """Run one git command in `cwd`, discarding its output."""
125 [trusted_git_executable(), *args],
133def _write_fixture_repo(root: Path) -> dict[str, tuple[str, str]]:
134 """Materialise a REAL git repository holding the fixture vendored tree.
136 A real repository, not a stub: Git supplies the same tracked/untracked
137 worktree census the gate uses in CI, and the gate derives raw blob ids from
141 root: Scratch directory to initialise as a repository.
144 ``{rel path: (mode, blob)}`` exactly as `git_ls_files` will report it.
146 _git([
"init",
"-q",
"-b",
"main",
"."], root)
147 for rel, data
in _FIXTURE_FILES.items():
148 target = root / FIXTURE_PATH / rel
149 target.parent.mkdir(parents=
True, exist_ok=
True)
150 target.write_bytes(data)
151 _git([
"add",
"-A"], root)
152 return git_ls_files(FIXTURE_PATH, (), root)
155def _selftest_worktree_cases(
156 root: Path, original: dict[str, tuple[str, str]]
157) -> list[tuple[str, bool]]:
158 """Prove unstaged bytes, modes, additions, and deletions affect the census."""
159 source = root / FIXTURE_PATH /
"src/a.c"
160 source.write_bytes(b
"int changed;\n")
161 mutated = git_ls_files(FIXTURE_PATH, (), root)
162 source.write_bytes(_FIXTURE_FILES[
"src/a.c"])
164 added_path = root / FIXTURE_PATH /
"untracked.c"
165 added_path.write_bytes(b
"int untracked;\n")
166 added = git_ls_files(FIXTURE_PATH, (), root)
169 deleted_path = root / FIXTURE_PATH /
"src/b.c"
170 deleted_path.unlink()
171 deleted = git_ls_files(FIXTURE_PATH, (), root)
172 deleted_path.write_bytes(_FIXTURE_FILES[
"src/b.c"])
175 remoded = git_ls_files(FIXTURE_PATH, (), root)
179 "MUST FIRE: an unstaged vendored-byte mutation changes the worktree blob",
180 mutated[
"src/a.c"][1] != original[
"src/a.c"][1],
183 "MUST FIRE: an untracked vendored file enters the worktree census",
184 "untracked.c" in added,
187 "MUST FIRE: a deleted tracked vendor file leaves the worktree census",
188 "src/b.c" not in deleted,
191 "MUST FIRE: an unstaged executable-bit change changes the worktree mode",
192 remoded[
"src/a.c"][0] ==
"100755",
197def _write_fixture_manifest(root: Path, ours: dict[str, tuple[str, str]], **mutate: str) ->
None:
198 """Write the fixture's manifest, optionally corrupting one field.
201 root: Scratch repository root.
202 ours: The fixture's ``{rel path: (mode, blob)}``.
203 mutate: ``kind``/``blob``/``mode``/``drop``/``extra`` knobs the
204 must-fire cases use to break exactly one thing.
206 entries: list[Entry] = []
207 for rel, (mode, blob)
in sorted(ours.items()):
208 if rel == mutate.get(
"drop"):
210 if rel ==
"patched.c":
211 entries.append(Entry(KIND_PATCH, mode, rel, upstream_blob=
"1" * 40, local_blob=blob))
212 elif rel ==
"generated.h":
213 entries.append(Entry(KIND_LOCAL, mode, rel, local_blob=blob))
215 entries.append(Entry(KIND_OK, mode, rel, upstream_blob=blob))
219 mutate[
"mode"]
if e.rel_path == mutate.get(
"mode_of")
else e.mode,
221 upstream_blob=(
"2" * 40
if e.rel_path == mutate.get(
"blob_of")
else e.upstream_blob),
222 local_blob=(
"3" * 40
if e.rel_path == mutate.get(
"local_of")
else e.local_blob),
223 upstream_path=e.upstream_path,
227 if mutate.get(
"extra"):
228 entries.append(Entry(KIND_OK,
"100644", mutate[
"extra"], upstream_blob=
"4" * 40))
229 header = {
"upstream-url":
"https://example.invalid",
"ref":
"v0",
"commit":
"0" * 40}
230 out = root / manifest_path(
"fixture")
231 out.parent.mkdir(parents=
True, exist_ok=
True)
232 out.write_text(format_manifest(
"fixture", header, entries), encoding=
"utf-8")
235def _selftest_manifest_cases(
236 root: Path, ours: dict[str, tuple[str, str]]
237) -> list[tuple[str, bool]]:
238 """Break the MANIFEST one way at a time and assert `run_check`'s verdict.
241 root: The scratch repository from `_write_fixture_repo`.
242 ours: That fixture's ``{rel path: (mode, blob)}``.
245 One ``(label, passed)`` pair per assertion.
247 comps = (_fixture_component(),)
249 def verdict(**mutate: str) -> int:
250 _write_fixture_manifest(root, ours, **mutate)
251 return _quiet(run_check, comps, root, FIXTURE_FLOORS)
254 (
"MUST NOT FIRE: an untouched fixture verifies clean", verdict() == EXIT_OK),
256 "MUST FIRE: a vendored file that is not the upstream blob",
257 verdict(blob_of=
"src/a.c") == EXIT_FAIL,
260 "MUST FIRE: a mode that disagrees with upstream",
261 verdict(mode_of=
"src/b.c", mode=
"100755") == EXIT_FAIL,
264 "MUST FIRE: an edit on top of a reviewed patch",
265 verdict(local_of=
"patched.c") == EXIT_FAIL,
268 "MUST FIRE: a tracked file absent from the manifest",
269 verdict(drop=
"src/b.c") == EXIT_FAIL,
272 "MUST FIRE: a manifest record with no file in the tree",
273 verdict(extra=
"ghost.c") == EXIT_FAIL,
278def _selftest_registry_cases(
279 root: Path, ours: dict[str, tuple[str, str]]
280) -> list[tuple[str, bool]]:
281 """Hold the manifest correct and break the REGISTRY's declarations instead.
284 root: The scratch repository from `_write_fixture_repo`.
285 ours: That fixture's ``{rel path: (mode, blob)}``.
288 One ``(label, passed)`` pair per assertion.
290 _write_fixture_manifest(root, ours)
291 comps = (_fixture_component(),)
295 key: str =
"fixture",
296 upstream_commit: str =
"0" * 40,
297 modified: bool =
True,
298 patched_files: tuple[tuple[str, str], ...] = ((
"patched.c",
"selftest patch"),),
299 local_files: tuple[tuple[str, str], ...] = ((
"generated.h",
"selftest local"),),
301 component = _fixture_component(
303 upstream_commit=upstream_commit,
305 patched_files=patched_files,
306 local_files=local_files,
308 return _quiet(run_check, (component,), root, FIXTURE_FLOORS)
312 "MUST FIRE: a patch/local record the registry does not declare",
313 verdict(patched_files=(), local_files=()) == EXIT_FAIL,
316 "MUST FIRE: a patched file on a component recording modified=False",
317 verdict(modified=
False) == EXIT_FAIL,
320 "MUST FIRE: a declaration for a file that is byte-identical to upstream (stale)",
322 patched_files=((
"patched.c",
"why"), (
"src/a.c",
"a patch that no longer exists"))
327 "MUST FIRE: the registry pin and the verified pin are different revisions",
328 verdict(upstream_commit=
"7" * 40) == EXIT_FAIL,
331 "MUST FIRE: a vendored component with no manifest at all",
332 verdict(key=
"absent") == EXIT_VACUOUS,
335 "MUST FIRE: a manifest whose rows prove nothing against upstream",
336 _quiet(run_check, comps, root, (1, 1, 99)) == EXIT_VACUOUS,
339 "MUST FIRE: a scan covering fewer components than the floor",
340 _quiet(run_check, comps, root, (99, 1, 1)) == EXIT_VACUOUS,
350 "an undeclared file whose bytes differ from upstream",
352 {
"src/a.c": (
"100644",
"f" * 40)},
354 (
"an undeclared file upstream does not have at all",
"invented.c", {}),
356 "a 'local' file upstream turns out to publish",
358 {
"generated.h": (
"100644",
"f" * 40)},
361 "a 'local' file whose bytes exist elsewhere upstream",
363 {
"somewhere/else.h": (
"100644",
"9" * 40)},
365 (
"a 'patch' of a file upstream does not have",
"patched.c", {}),
367 "a 'patch' declaration on a file identical to upstream (stale)",
369 {
"patched.c": (
"100644",
"9" * 40)},
374def _selftest_resolve_cases() -> list[tuple[str, bool]]:
375 """Assert `_resolve_entry`'s mappings, and its refusal to invent a deviation."""
376 comp = _fixture_component()
377 tree = {
"src/a.c": (
"100644",
"a" * 40),
"moved/here.c": (
"100644",
"c" * 40)}
378 cases: list[tuple[str, bool]] = [
380 "MUST NOT FIRE: a byte-identical file resolves as 'ok'",
381 _resolve_entry(comp,
"src/a.c", (
"100644",
"a" * 40), tree).kind == KIND_OK,
384 "MUST NOT FIRE: a relocated but identical file resolves as 'moved'",
385 _resolve_entry(comp,
"flat.c", (
"100644",
"c" * 40), tree).upstream_path
389 "MUST NOT FIRE: a declared patch keeps upstream's hash alongside ours",
391 comp,
"patched.c", (
"100644",
"d" * 40), {
"patched.c": (
"100644",
"e" * 40)}
396 for label, path, tree_arg
in _REFUSAL_CASES:
399 _resolve_entry(comp, path, (
"100644",
"9" * 40), tree_arg)
400 except VacuousScanError:
402 cases.append((f
"MUST FIRE: --refresh refuses {label}", fired))
406def _selftest_format_cases() -> list[tuple[str, bool]]:
407 """Assert the manifest format round-trips and rejects malformed records."""
409 Entry(KIND_OK,
"100644",
"a.c", upstream_blob=
"a" * 40),
410 Entry(KIND_MOVED,
"120000",
"b.c", upstream_blob=
"b" * 40, upstream_path=
"up/b.c"),
411 Entry(KIND_PATCH,
"100644",
"c.c", upstream_blob=
"c" * 40, local_blob=
"d" * 40),
412 Entry(KIND_LOCAL,
"100755",
"d.c", local_blob=
"e" * 40),
414 text = format_manifest(
"fixture", {
"commit":
"0" * 40}, entries)
415 parsed = parse_manifest(
"fixture", text, Path(
"fixture"))
417 (
"MUST NOT FIRE: every record kind round-trips", list(parsed.entries) == entries),
419 "MUST NOT FIRE: only ok/moved rows count as upstream-verified",
420 parsed.verified_count() == FIXTURE_VERIFIED_ROWS,
424 (
"an unknown record kind",
"bogus 100644 " +
"a" * 40 +
" a.c"),
425 (
"a truncated blob id",
"ok 100644 abc a.c"),
426 (
"a non-octal file mode",
"ok 10x644 " +
"a" * 40 +
" a.c"),
427 (
"a duplicate path",
"ok 100644 " +
"a" * 40 +
" a.c\nok 100644 " +
"b" * 40 +
" a.c"),
431 parse_manifest(
"fixture", f
"# component: fixture\n{bad}\n", Path(
"fixture"))
432 except ManifestError:
434 cases.append((f
"MUST FIRE: the parser rejects {label}", fired))
437 "MUST FIRE: the parser rejects a manifest naming another component",
438 _raises_manifest_error(
"other",
"# component: fixture\n"),
444def _raises_manifest_error(key: str, text: str) -> bool:
445 """Return True when `parse_manifest` rejects `text` for `key`."""
447 parse_manifest(key, text, Path(
"fixture"))
448 except ManifestError:
453def _run_selftest_body() -> int:
454 """Prove the gate fires on every provenance defect and stays quiet otherwise.
456 Both directions are asserted because only one of them has ever been true of
457 this claim: "byte-identical to upstream" was stated in three places and
458 checked nowhere, so every tree passed and a corrupted one would have too.
461 ``EXIT_OK`` when every case holds, ``EXIT_VACUOUS`` otherwise.
463 with tempfile.TemporaryDirectory()
as tmp:
465 ours = _write_fixture_repo(root)
466 cases = _selftest_worktree_cases(root, ours)
467 cases.extend(_selftest_manifest_cases(root, ours))
468 cases.extend(_selftest_registry_cases(root, ours))
469 cases.extend(_selftest_resolve_cases())
470 cases.extend(_selftest_format_cases())
473 "MUST NOT FIRE: the shipped floors are below the live tree, not zero",
475 and MIN_ENTRIES > FLOOR_SANITY_MIN
476 and MIN_UPSTREAM_VERIFIED > FLOOR_SANITY_MIN,
479 failed = [label
for label, ok
in cases
if not ok]
480 for label, ok
in cases:
481 print(f
" {'ok ' if ok else 'FAIL'} {label}")
483 print(f
"check_soup_upstream: selftest FAILED ({len(failed)} case(s))", file=sys.stderr)
485 print(f
"check_soup_upstream: selftest passed ({len(cases)} cases, both directions).")
489def run_selftest() -> int:
490 """Run provenance fixtures without inheriting the caller's repository."""
491 with isolated_git_environment():
492 return _run_selftest_body()