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_artifacts.py (7460B)


      1 from __future__ import annotations
      2 
      3 import copy
      4 import os
      5 import plistlib
      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_artifacts as artifacts  # noqa: E402
     17 import ffi_build as builder  # noqa: E402
     18 import ffi_provenance as provenance  # noqa: E402
     19 import ffi_source as source  # noqa: E402
     20 import package_contract as contract  # noqa: E402
     21 
     22 
     23 class NativeArtifactTests(unittest.TestCase):
     24     def setUp(self) -> None:
     25         self.temporary = tempfile.TemporaryDirectory()
     26         self.addCleanup(self.temporary.cleanup)
     27         self.root = Path(self.temporary.name).resolve()
     28         for relative in artifacts.expected_paths():
     29             path = self.root / relative
     30             path.parent.mkdir(parents=True, exist_ok=True)
     31             path.write_bytes(b"synthetic verifier fixture; not an executable library\n")
     32         self.records = {
     33             target: {
     34                 "repository": "https://github.com/radrootslabs/tera",
     35                 "source": {"tree": "a" * 40},
     36                 "build": {"target": target},
     37             }
     38             for target in artifacts.TARGETS
     39         }
     40         for target, record in self.records.items():
     41             (self.root / "source" / f"{target}.json").write_bytes(
     42                 provenance.encoded(record)
     43             )
     44         self.install_headers()
     45         self.install_framework_info()
     46         symbols = {target: ["ffi_tera_ffi_fixture"] for target in artifacts.TARGETS}
     47         (self.root / "abi_symbols.json").write_bytes(provenance.encoded(symbols))
     48         (self.root / "api/TeraKitBindings.symbols.json").write_bytes(
     49             provenance.encoded(
     50                 {
     51                     "schema": "radroots.swift-api-snapshot.v1",
     52                     "module": {"name": artifacts.MODULE},
     53                     "symbols": [{"synthetic": True}],
     54                 }
     55             )
     56         )
     57         (self.root / artifacts.MANIFEST).write_bytes(
     58             provenance.encoded(artifacts.manifest(self.root, self.records))
     59         )
     60 
     61     def test_framework_metadata_is_independent_of_xcode_library_order(self) -> None:
     62         path = self.root / artifacts.FRAMEWORK / "Info.plist"
     63         original = plistlib.loads(path.read_bytes())
     64         builder.canonicalize_framework_info(path)
     65         expected = path.read_bytes()
     66         original["AvailableLibraries"].reverse()
     67         path.write_bytes(plistlib.dumps(original, sort_keys=False))
     68         builder.canonicalize_framework_info(path)
     69         self.assertEqual(path.read_bytes(), expected)
     70 
     71     def install_headers(self) -> None:
     72         header = b"void ffi_tera_ffi_fixture(void);\n"
     73         modulemap = b'module TeraFFI { header "TeraFFI.h" export * }\n'
     74         for prefix in (
     75             "headers",
     76             "TeraFFI.xcframework/ios-arm64/Headers",
     77             "TeraFFI.xcframework/ios-arm64-simulator/Headers",
     78         ):
     79             (self.root / prefix / "TeraFFI.h").write_bytes(header)
     80             (self.root / prefix / "module.modulemap").write_bytes(modulemap)
     81         (self.root / "generated/TeraFFI.h").write_bytes(header)
     82         (self.root / "generated/TeraFFI.modulemap").write_bytes(modulemap)
     83         (self.root / "generated/TeraKitBindings.swift").write_text("import TeraFFI\n")
     84 
     85     def install_framework_info(self) -> None:
     86         libraries = []
     87         for identifier, variant in (
     88             ("ios-arm64", None),
     89             ("ios-arm64-simulator", "simulator"),
     90         ):
     91             record = {
     92                 "LibraryIdentifier": identifier,
     93                 "SupportedArchitectures": ["arm64"],
     94                 "SupportedPlatform": "ios",
     95                 "LibraryPath": "libtera_ffi.a",
     96                 "HeadersPath": "Headers",
     97             }
     98             if variant:
     99                 record["SupportedPlatformVariant"] = variant
    100             libraries.append(record)
    101         (self.root / "TeraFFI.xcframework/Info.plist").write_bytes(
    102             plistlib.dumps({"AvailableLibraries": libraries})
    103         )
    104 
    105     def test_consistent_fixture_is_checked_without_claiming_build_execution(
    106         self,
    107     ) -> None:
    108         result = artifacts.check(self.root, self.records)
    109         self.assertEqual(result["disposition"], "local_candidate_not_installed")
    110         self.assertEqual(len(result["files"]), len(artifacts.expected_paths()))
    111 
    112     def test_tampered_or_missing_built_library_is_rejected(self) -> None:
    113         path = self.root / "native/aarch64-apple-ios/libtera_ffi.a"
    114         path.write_bytes(b"changed synthetic bytes")
    115         with self.assertRaisesRegex(source.ProvenanceError, "differs from its built"):
    116             artifacts.check(self.root, self.records)
    117         path.unlink()
    118         with self.assertRaisesRegex(source.ProvenanceError, "missing"):
    119             artifacts.check(self.root, self.records)
    120 
    121     def test_mismatched_generated_and_packaged_headers_are_rejected(self) -> None:
    122         (self.root / "headers/TeraFFI.h").write_text("void other(void);\n")
    123         with self.assertRaisesRegex(source.ProvenanceError, "header differs"):
    124             artifacts.check(self.root, self.records)
    125 
    126     def test_target_source_tuple_mismatch_is_rejected(self) -> None:
    127         records = copy.deepcopy(self.records)
    128         records[artifacts.TARGETS[0]]["build"]["target"] = artifacts.TARGETS[1]
    129         with self.assertRaisesRegex(source.ProvenanceError, "tuples disagree"):
    130             artifacts.check(self.root, records)
    131 
    132     def test_cross_target_abi_mismatch_is_rejected(self) -> None:
    133         symbols = contract._read_json(self.root / "abi_symbols.json")
    134         symbols[artifacts.TARGETS[0]] = ["ffi_tera_ffi_other"]
    135         (self.root / "abi_symbols.json").write_bytes(provenance.encoded(symbols))
    136         with self.assertRaisesRegex(source.ProvenanceError, "ABI symbols differ"):
    137             artifacts.check(self.root, self.records)
    138 
    139     def test_extra_files_including_nested_provenance_are_rejected(self) -> None:
    140         (self.root / "headers/provenance.json").write_text("{}\n")
    141         with self.assertRaisesRegex(source.ProvenanceError, "inventory differs"):
    142             artifacts.check(self.root, self.records)
    143 
    144     def test_symlink_cannot_supply_artifact_bytes(self) -> None:
    145         path = self.root / "native/aarch64-apple-ios/libtera_ffi.a"
    146         path.unlink()
    147         path.symlink_to(self.root / "native/aarch64-apple-ios-sim/libtera_ffi.a")
    148         with self.assertRaisesRegex(source.ProvenanceError, "symlink"):
    149             artifacts.check(self.root, self.records)
    150 
    151     def test_build_environment_applies_recorded_flags_and_foundation_identity(
    152         self,
    153     ) -> None:
    154         config = source.producer_contract(SCRIPTS.parent)
    155         with patch.dict(os.environ, {"TERA_CONSUMER_REVISION": "f" * 40}):
    156             environment = builder.build_environment(SCRIPTS.parent, self.root, config)
    157         self.assertNotIn("TERA_CONSUMER_REVISION", environment)
    158         self.assertIn("=/tera", environment["CARGO_ENCODED_RUSTFLAGS"])
    159         self.assertEqual(
    160             environment["RADROOTS_LIB_REVISION"],
    161             contract._read_toml(SCRIPTS.parent / "radroots.lib.source-lock.v1.toml")[
    162                 "revision"
    163             ],
    164         )
    165         self.assertEqual(
    166             environment["SOURCE_DATE_EPOCH"], str(config["build"]["source_date_epoch"])
    167         )
    168 
    169 
    170 if __name__ == "__main__":
    171     unittest.main()