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()