#!/usr/bin/env python3 """Cells for manifest.py: the 0/1/2 contract, dir symlinks, and self-skip. Every cell runs the committed module through the CLI in a subprocess, so the exit code is the thing under test, not an in-process return value. The module path resolves from this file's own directory, never a host path. """ import json import os import shutil import subprocess import sys import tempfile import unittest MOD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "manifest.py") FILES = { "a.txt": b"hello", "sub/b.bin": b"\x00\x01\x02", "c.txt": b"world", } class ManifestTest(unittest.TestCase): def setUp(self): self.tmp = [] def tearDown(self): for d in self.tmp: shutil.rmtree(d, ignore_errors=True) def _scratch(self, prefix="manifest-test-"): d = tempfile.mkdtemp(prefix=prefix) self.tmp.append(d) return d def _tree(self): d = self._scratch("manifest-tree-") for rel, data in FILES.items(): full = os.path.join(d, rel) os.makedirs(os.path.dirname(full), exist_ok=True) with open(full, "wb") as f: f.write(data) return d def _run(self, *args): return subprocess.run([sys.executable, MOD, *args], capture_output=True, text=True) def _manifest(self, root): out = os.path.join(self._scratch("manifest-out-"), "m.json") r = self._run("make", root, out) self.assertEqual(r.returncode, 0, r.stderr + r.stdout) return out # -- 0: clean --------------------------------------------------------- def test_roundtrip_is_clean(self): root = self._tree() man = self._manifest(root) r = self._run("check", root, man) self.assertEqual(r.returncode, 0, r.stderr + r.stdout) self.assertIn("OK: manifest matches", r.stdout) def test_make_counts_every_file(self): root = self._tree() man = self._manifest(root) with open(man) as f: doc = json.load(f) self.assertEqual(sorted(e["path"] for e in doc["entries"]), sorted(FILES)) self.assertTrue(all(e["type"] == "file" for e in doc["entries"])) # -- 1: graded fault -------------------------------------------------- def test_tampered_content_exits_1(self): root = self._tree() man = self._manifest(root) with open(os.path.join(root, "a.txt"), "wb") as f: f.write(b"tampered") r = self._run("check", root, man) self.assertEqual(r.returncode, 1, r.stderr) self.assertIn("HASH_MISMATCH: a.txt", r.stdout) def test_extra_file_exits_1(self): root = self._tree() man = self._manifest(root) with open(os.path.join(root, "extra.txt"), "w") as f: f.write("x") r = self._run("check", root, man) self.assertEqual(r.returncode, 1, r.stderr) self.assertIn("EXTRA: extra.txt", r.stdout) def test_missing_file_exits_1(self): root = self._tree() man = self._manifest(root) os.remove(os.path.join(root, "sub", "b.bin")) r = self._run("check", root, man) self.assertEqual(r.returncode, 1, r.stderr) self.assertIn("MISSING: sub/b.bin", r.stdout) # -- 1: directory symlinks are entries, not invisible ------------------ def test_dir_symlink_added_after_make_exits_1(self): root = self._tree() man = self._manifest(root) outside = self._scratch("manifest-outside-") with open(os.path.join(outside, "secret.txt"), "w") as f: f.write("not in the manifest") os.symlink(outside, os.path.join(root, "linked_dir")) r = self._run("check", root, man) self.assertEqual(r.returncode, 1, r.stderr) self.assertIn("EXTRA: linked_dir", r.stdout) self.assertNotIn("secret.txt", r.stdout) def test_dir_symlink_removed_after_make_exits_1(self): root = self._tree() os.symlink(self._scratch("manifest-outside-"), os.path.join(root, "linked_dir")) man = self._manifest(root) os.remove(os.path.join(root, "linked_dir")) r = self._run("check", root, man) self.assertEqual(r.returncode, 1, r.stderr) self.assertIn("MISSING: linked_dir", r.stdout) def test_dir_symlink_recorded_with_target(self): root = self._tree() outside = self._scratch("manifest-outside-") os.symlink(outside, os.path.join(root, "linked_dir")) man = self._manifest(root) with open(man) as f: doc = json.load(f) row = [e for e in doc["entries"] if e["path"] == "linked_dir"] self.assertEqual(len(row), 1) self.assertEqual(row[0]["type"], "symlink") self.assertEqual(row[0]["target"], outside) r = self._run("check", root, man) self.assertEqual(r.returncode, 0, r.stderr + r.stdout) def test_dir_symlink_is_not_descended(self): root = self._tree() os.symlink(os.path.join(root, "sub"), os.path.join(root, "looplink")) man = self._manifest(root) with open(man) as f: doc = json.load(f) paths = [e["path"] for e in doc["entries"]] self.assertIn("looplink", paths) self.assertFalse([p for p in paths if p.startswith("looplink" + os.sep)]) r = self._run("check", root, man) self.assertEqual(r.returncode, 0, r.stderr + r.stdout) # -- 0: a manifest stored inside its own root ------------------------- def test_manifest_inside_root_is_not_an_entry(self): root = self._tree() man = os.path.join(root, "MANIFEST.json") r = self._run("make", root, man) self.assertEqual(r.returncode, 0, r.stderr) with open(man) as f: first = json.load(f) self.assertNotIn("MANIFEST.json", [e["path"] for e in first["entries"]]) self.assertEqual(len(first["entries"]), len(FILES)) # A second make must not grow the entry set, nor change a byte. with open(man, "rb") as f: before = f.read() r = self._run("make", root, man) self.assertEqual(r.returncode, 0, r.stderr) with open(man, "rb") as f: self.assertEqual(f.read(), before) with open(man) as f: self.assertEqual(len(json.load(f)["entries"]), len(FILES)) def test_manifest_inside_root_checks_clean(self): root = self._tree() man = os.path.join(root, "MANIFEST.json") self._run("make", root, man) r = self._run("check", root, man) self.assertEqual(r.returncode, 0, r.stderr + r.stdout) self.assertIn("OK: manifest matches", r.stdout) # -- 2: ungradable ---------------------------------------------------- def test_missing_root_make_exits_2_and_writes_nothing(self): out = os.path.join(self._scratch("manifest-out-"), "m.json") r = self._run("make", "/nonexistent-root-manifest-test", out) self.assertEqual(r.returncode, 2, r.stderr + r.stdout) self.assertIn("ungradable:", r.stderr) self.assertFalse(os.path.exists(out)) def test_missing_root_check_exits_2(self): root = self._tree() man = self._manifest(root) r = self._run("check", "/nonexistent-root-manifest-test", man) self.assertEqual(r.returncode, 2, r.stderr + r.stdout) self.assertIn("ungradable:", r.stderr) def test_root_that_is_a_file_exits_2(self): file_root = os.path.join(self._scratch("manifest-out-"), "not-a-dir") with open(file_root, "w") as f: f.write("x") out = os.path.join(self._scratch("manifest-out-"), "m.json") r = self._run("make", file_root, out) self.assertEqual(r.returncode, 2, r.stderr + r.stdout) self.assertFalse(os.path.exists(out)) root = self._tree() man = self._manifest(root) r = self._run("check", file_root, man) self.assertEqual(r.returncode, 2, r.stderr + r.stdout) def test_corrupt_manifest_exits_2(self): root = self._tree() bad = self._scratch("manifest-bad-") cases = { "not-json.json": "{not json", "empty-object.json": "{}", "entries-not-list.json": '{"entries": "nope"}', "unknown-type.json": '{"entries": [{"path": "a.txt", "type": "weird"}]}', "file-without-hash.json": '{"entries": [{"path": "a.txt", "type": "file"}]}', "entry-not-object.json": '{"entries": ["a.txt"]}', } for name, body in cases.items(): path = os.path.join(bad, name) with open(path, "w") as f: f.write(body) r = self._run("check", root, path) self.assertEqual(r.returncode, 2, "%s -> %d: %s" % (name, r.returncode, r.stdout)) def test_unreadable_manifest_exits_2(self): root = self._tree() r = self._run("check", root, os.path.join(self._scratch(), "no-such.json")) self.assertEqual(r.returncode, 2, r.stderr + r.stdout) self.assertIn("ungradable:", r.stderr) def test_bad_usage_exits_2(self): r = self._run() self.assertEqual(r.returncode, 2) self.assertIn("usage:", r.stderr) r = self._run("frobnicate", self._tree(), os.path.join(self._scratch(), "m.json")) self.assertEqual(r.returncode, 2) self.assertIn("unknown command", r.stderr) if __name__ == "__main__": unittest.main(verbosity=2)