field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

test_ffi_provenance.py (11247B)


      1 from __future__ import annotations
      2 
      3 import copy
      4 import os
      5 import subprocess
      6 import sys
      7 import tempfile
      8 import unittest
      9 from pathlib import Path
     10 from unittest.mock import patch
     11 
     12 SCRIPTS = Path(__file__).resolve().parent
     13 if str(SCRIPTS) not in sys.path:
     14     sys.path.insert(0, str(SCRIPTS))
     15 
     16 import ffi_build as builder  # noqa: E402
     17 import ffi_provenance as provenance  # noqa: E402
     18 import ffi_source as source  # noqa: E402
     19 import package_contract as contract  # noqa: E402
     20 
     21 
     22 class ProducerSourceTests(unittest.TestCase):
     23     def setUp(self) -> None:
     24         self.temporary = tempfile.TemporaryDirectory()
     25         self.addCleanup(self.temporary.cleanup)
     26         self.root = Path(self.temporary.name).resolve()
     27         self.git("init", "--quiet")
     28         (self.root / "core").mkdir()
     29         (self.root / "core/lib.rs").write_text("pub fn value() -> u8 { 1 }\n")
     30         (self.root / "Cargo.lock").write_text("# synthetic source-capture fixture\n")
     31         self.git("add", "core", "Cargo.lock")
     32         self.inputs = ["core", "Cargo.lock"]
     33 
     34     def git(self, *args: str) -> str:
     35         return subprocess.check_output(
     36             ["git", *args], cwd=self.root, text=True, stderr=subprocess.DEVNULL
     37         ).strip()
     38 
     39     def snapshot(self) -> dict:
     40         return source.source_snapshot(self.root, self.inputs)
     41 
     42     def test_tree_is_real_and_deterministic_without_an_introducing_commit(self) -> None:
     43         first = self.snapshot()
     44         self.assertEqual(first, self.snapshot())
     45         self.assertEqual(self.git("write-tree"), first["tree"])
     46         self.assertEqual(self.git("cat-file", "-t", first["tree"]), "tree")
     47         # Generated evidence never changes its own source tree identity.
     48         (self.root / "provenance.json").write_bytes(provenance.encoded(first))
     49         self.git("add", "provenance.json")
     50         self.assertEqual(first, self.snapshot())
     51 
     52     def test_worktree_change_requires_an_explicit_staged_transaction(self) -> None:
     53         first = self.snapshot()
     54         (self.root / "core/lib.rs").write_text("pub fn value() -> u8 { 2 }\n")
     55         with self.assertRaisesRegex(source.ProvenanceError, "differs from staged"):
     56             self.snapshot()
     57         self.git("add", "core/lib.rs")
     58         self.assertNotEqual(first["tree"], self.snapshot()["tree"])
     59 
     60     def test_untracked_and_missing_source_fail_closed(self) -> None:
     61         (self.root / "core/extra.rs").write_text("// untracked input\n")
     62         with self.assertRaisesRegex(source.ProvenanceError, "untracked"):
     63             self.snapshot()
     64         (self.root / "core/extra.rs").unlink()
     65         (self.root / "Cargo.lock").unlink()
     66         self.git("add", "-u", "Cargo.lock")
     67         with self.assertRaisesRegex(source.ProvenanceError, "missing from index"):
     68             self.snapshot()
     69 
     70     def test_symlink_and_file_mode_changes_are_rejected(self) -> None:
     71         path = self.root / "core/lib.rs"
     72         path.chmod(0o755)
     73         with self.assertRaisesRegex(source.ProvenanceError, "differs from staged"):
     74             self.snapshot()
     75         path.unlink()
     76         path.symlink_to("../Cargo.lock")
     77         with self.assertRaisesRegex(source.ProvenanceError, "symlink"):
     78             self.snapshot()
     79         self.git("add", "core/lib.rs")
     80         with self.assertRaisesRegex(source.ProvenanceError, "unsupported mode"):
     81             self.snapshot()
     82 
     83     def test_source_byte_bound_prevents_unbounded_capture(self) -> None:
     84         (self.root / "core/lib.rs").write_bytes(b"x" * (source.MAX_BYTES + 1))
     85         with self.assertRaisesRegex(contract.PackageContractError, "byte limit"):
     86             self.snapshot()
     87 
     88     def test_gitignore_cannot_hide_a_producer_input(self) -> None:
     89         (self.root / ".gitignore").write_text("core/hidden.rs\n")
     90         (self.root / "core/hidden.rs").write_text("// ignored source input\n")
     91         with self.assertRaisesRegex(source.ProvenanceError, "ignored source"):
     92             self.snapshot()
     93 
     94     def test_provenance_rejects_every_source_or_build_tuple_mutation(self) -> None:
     95         expected = {
     96             "source": self.snapshot(),
     97             "cargo_lock_sha256": "a" * 64,
     98             "foundation": {"revision": "b" * 40},
     99             "build": {
    100                 "target": "aarch64-apple-ios",
    101                 "rustc": "rustc 1.97.1",
    102                 "features": [],
    103                 "package_rust_flags": [],
    104                 "feature_graph": ["tera_core|mobile-social"],
    105             },
    106         }
    107         provenance.verify_record(provenance.encoded(expected), expected)
    108         for section, key, value in (
    109             ("source", "tree", "c" * 40),
    110             ("foundation", "revision", "d" * 40),
    111             ("build", "target", "aarch64-apple-ios-sim"),
    112             ("build", "rustc", "rustc 1.96.0"),
    113             ("build", "features", ["extra"]),
    114             ("build", "package_rust_flags", ["-Copt-level=0"]),
    115             ("build", "feature_graph", []),
    116         ):
    117             with self.subTest(section=section, key=key):
    118                 changed = copy.deepcopy(expected)
    119                 changed[section][key] = value
    120                 with self.assertRaisesRegex(
    121                     source.ProvenanceError, "exact source/build tuple"
    122                 ):
    123                     provenance.verify_record(provenance.encoded(changed), expected)
    124         changed = {**expected, "cargo_lock_sha256": "f" * 64}
    125         with self.assertRaises(source.ProvenanceError):
    126             provenance.verify_record(provenance.encoded(changed), expected)
    127 
    128     def test_output_rejects_repository_paths_escapes_and_symlinks(self) -> None:
    129         external = self.root / "external"
    130         external.mkdir()
    131         repository = self.root / "repository"
    132         repository.mkdir()
    133         with patch.dict(
    134             os.environ,
    135             {
    136                 "EXT_BUILD_RUN_ACTIVE": "1",
    137                 "EXT_BUILD_PROJECT_DIR": str(external),
    138             },
    139         ):
    140             for output in (repository / "evidence.json", external / "../escape.json"):
    141                 with self.assertRaises(source.ProvenanceError):
    142                     provenance.output_path(repository, "aarch64-apple-ios", str(output))
    143             (external / "link").symlink_to(repository, target_is_directory=True)
    144             with self.assertRaises(source.ProvenanceError):
    145                 provenance.output_path(
    146                     repository, "aarch64-apple-ios", str(external / "link/value.json")
    147                 )
    148 
    149     def test_ungoverned_build_flags_are_rejected(self) -> None:
    150         for key in (
    151             "RUSTFLAGS",
    152             "RUSTC_WRAPPER",
    153             "CARGO_BUILD_RUSTFLAGS",
    154             "CARGO_TARGET_AARCH64_APPLE_IOS_LINKER",
    155         ):
    156             with self.subTest(key=key), patch.dict(os.environ, {key: "synthetic"}):
    157                 with self.assertRaisesRegex(source.ProvenanceError, "ungoverned"):
    158                     source.reject_build_overrides()
    159 
    160     def test_local_cargo_build_configuration_cannot_escape_source_identity(
    161         self,
    162     ) -> None:
    163         (self.root / ".cargo").mkdir()
    164         config = self.root / ".cargo/config.toml"
    165         config.write_text('[build]\nrustflags = ["--cfg", "unrecorded"]\n')
    166         with self.assertRaisesRegex(source.ProvenanceError, "Cargo configuration"):
    167             source.reject_cargo_configuration(self.root)
    168 
    169     def test_current_contract_separates_foundation_and_application(self) -> None:
    170         config = source.producer_contract(SCRIPTS.parent)
    171         self.assertEqual(config["ffi"]["package"], "tera_ffi")
    172         self.assertEqual(config["repository"], "https://github.com/radrootslabs/tera")
    173         cargo = contract._read_toml(SCRIPTS.parent / "Cargo.toml")
    174         lock = contract._read_toml(SCRIPTS.parent / config["foundation_lock"])
    175         changed = copy.deepcopy(cargo)
    176         changed["workspace"]["dependencies"]["radroots_sdk"]["rev"] = "f" * 40
    177         with self.assertRaisesRegex(
    178             contract.PackageContractError, "foundation Cargo revision"
    179         ):
    180             source.validate_foundation(changed, lock)
    181 
    182     def test_mismatched_foundation_consumer_lock_fails_before_source_capture(
    183         self,
    184     ) -> None:
    185         read_toml = contract._read_toml
    186 
    187         def mismatched(path: Path) -> dict:
    188             value = read_toml(path)
    189             if path.name == "radroots.lib.source-lock.v1.toml":
    190                 value["lockfile_sha256"] = "0" * 64
    191             return value
    192 
    193         with patch.object(contract, "_read_toml", side_effect=mismatched):
    194             with self.assertRaisesRegex(
    195                 contract.PackageContractError, "foundation consumer Cargo lock digest"
    196             ):
    197                 source.producer_contract(SCRIPTS.parent)
    198 
    199     def test_host_install_name_is_fixed_and_scoped_to_the_ffi_library(self) -> None:
    200         config = source.producer_contract(SCRIPTS.parent)
    201         build = config["build"]
    202         flags = [
    203             "-Clink-arg=-Wl,-install_name,@rpath/libtera_ffi.dylib",
    204             "-Clink-arg=-Wl,-reproducible",
    205             "-Clink-arg=-Wl,-oso_prefix,{extbuild_root}",
    206         ]
    207         project = Path("/external/test-producer")
    208         for target in build["targets"]:
    209             with self.subTest(target=target):
    210                 command = builder.library_command(SCRIPTS.parent, target, project)
    211                 self.assertIn("--lib", command)
    212                 self.assertEqual(command[command.index("-p") + 1], "tera_ffi")
    213                 if target == build["host"]:
    214                     self.assertEqual(command[1], "rustc")
    215                     self.assertEqual(
    216                         command[-4:],
    217                         ["--", *(flag.format(extbuild_root=project) for flag in flags)],
    218                     )
    219                     self.assertEqual(source.library_rust_flags(build, target), flags)
    220                 else:
    221                     self.assertEqual(command[1], "build")
    222                     self.assertNotIn("--", command)
    223                     self.assertEqual(source.library_rust_flags(build, target), [])
    224         self.assertFalse(any("install_name" in flag for flag in build["rust_flags"]))
    225         for install_name in (
    226             "/tmp/libtera_ffi.dylib",
    227             "libtera_ffi.dylib",
    228             "@rpath/other.dylib",
    229         ):
    230             changed = copy.deepcopy(build)
    231             changed["host_dylib_install_name"] = install_name
    232             with (
    233                 self.subTest(install_name=install_name),
    234                 self.assertRaises(contract.PackageContractError),
    235             ):
    236                 source.validate_build(changed)
    237         for prefix in ("/tmp", "{producer_root}", ""):
    238             changed = copy.deepcopy(build)
    239             changed["host_oso_prefix"] = prefix
    240             with (
    241                 self.subTest(prefix=prefix),
    242                 self.assertRaises(contract.PackageContractError),
    243             ):
    244                 source.validate_build(changed)
    245         for reproducible in (False, 1, "true"):
    246             changed = copy.deepcopy(build)
    247             changed["host_linker_reproducible"] = reproducible
    248             with (
    249                 self.subTest(reproducible=reproducible),
    250                 self.assertRaises(contract.PackageContractError),
    251             ):
    252                 source.validate_build(changed)
    253 
    254 
    255 if __name__ == "__main__":
    256     unittest.main()