88import copy
99import hashlib
1010import importlib .util
11+ import io
1112import json
1213from pathlib import Path
14+ import tarfile
15+ import tempfile
1316import tomllib
1417import unittest
18+ from unittest import mock
1519
1620
1721ROOT = Path (__file__ ).resolve ().parents [3 ]
118122 ),
119123}
120124
125+ FIXTURE_SOURCE_ARCHIVE_NAME = "arm-gnu-toolchain-source-fixture.tar.xz"
126+ FIXTURE_SOURCE_MANIFEST = b"fixture-source.c\t fixture source archive member\n "
127+ FIXTURE_SOURCE = b"int pyble_arm_runtime_fixture(void) { return 0; }\n "
128+
121129
122130def sha256 (path : Path ) -> str :
123131 return hashlib .sha256 (path .read_bytes ()).hexdigest ()
124132
125133
134+ def sha256_bytes (value : bytes ) -> str :
135+ return hashlib .sha256 (value ).hexdigest ()
136+
137+
138+ def source_archive_fixture () -> bytes :
139+ output = io .BytesIO ()
140+ with tarfile .open (fileobj = output , mode = "w:xz" ) as archive :
141+ info = tarfile .TarInfo ("arm-gnu-source-fixture/fixture-source.c" )
142+ info .size = len (FIXTURE_SOURCE )
143+ info .mode = 0o644
144+ info .mtime = 0
145+ archive .addfile (info , io .BytesIO (FIXTURE_SOURCE ))
146+ return output .getvalue ()
147+
148+
126149def load_release_module ():
127150 spec = importlib .util .spec_from_file_location (
128151 "release_bundle_arm_runtime_red" , RELEASE_BUNDLE
@@ -140,6 +163,72 @@ class RP2ArmRuntimeClosureTests(unittest.TestCase):
140163 @classmethod
141164 def setUpClass (cls ) -> None :
142165 cls .attribution = json .loads (ATTRIBUTION .read_text (encoding = "utf-8" ))
166+ cls ._temporary = tempfile .TemporaryDirectory (
167+ prefix = "pyble-rp2-arm-runtime-closure-"
168+ )
169+ cls .addClassCleanup (cls ._temporary .cleanup )
170+ cls .validation_root = Path (cls ._temporary .name )
171+
172+ archive_raw = source_archive_fixture ()
173+ archive_path = (
174+ cls .validation_root
175+ / "firmware/.arm-gnu/.pyble-dist"
176+ / FIXTURE_SOURCE_ARCHIVE_NAME
177+ )
178+ archive_path .parent .mkdir (parents = True )
179+ archive_path .write_bytes (archive_raw )
180+
181+ cls .validation_attribution = copy .deepcopy (cls .attribution )
182+ identity = cls .validation_attribution ["identity" ]
183+ identity ["source_archive" ] = {
184+ "bytes" : len (archive_raw ),
185+ "filename" : FIXTURE_SOURCE_ARCHIVE_NAME ,
186+ "sha256" : sha256_bytes (archive_raw ),
187+ }
188+ manifest_relative = identity ["source_manifest" ]["path" ]
189+ identity ["source_manifest" ]["sha256" ] = sha256_bytes (
190+ FIXTURE_SOURCE_MANIFEST
191+ )
192+
193+ evidence_path = (
194+ cls .validation_root
195+ / "firmware/licenses/evidence/rp2/arm-gnu-toolchain/14.2.rel1"
196+ )
197+ evidence_path .mkdir (parents = True )
198+ (cls .validation_root / manifest_relative ).write_bytes (
199+ FIXTURE_SOURCE_MANIFEST
200+ )
201+ fixture_attribution = evidence_path / "runtime-attribution-v1.json"
202+ fixture_attribution .write_text (
203+ json .dumps (cls .validation_attribution , indent = 2 ) + "\n " ,
204+ encoding = "utf-8" ,
205+ )
206+ cls .validation_attribution_sha256 = sha256 (fixture_attribution )
207+
208+ lock_path = cls .validation_root / "firmware/versions.lock"
209+ lock_path .write_text (
210+ "# SPDX-License-Identifier: MIT\n "
211+ "[micropython]\n "
212+ 'repo = "https://example.invalid/micropython.git"\n '
213+ 'ref = "fixture"\n '
214+ f'commit = "{ "1" * 40 } "\n \n '
215+ "[esp_idf]\n "
216+ 'repo = "https://example.invalid/esp-idf.git"\n '
217+ 'ref = "fixture"\n '
218+ f'commit = "{ "2" * 40 } "\n \n '
219+ "[pyble]\n "
220+ 'agent_version = "0.6.0"\n '
221+ 'protocol_version = "PBLE/1"\n \n '
222+ "[arm_gnu_toolchain]\n "
223+ f'source_archive_filename = "{ FIXTURE_SOURCE_ARCHIVE_NAME } "\n '
224+ f"source_archive_bytes = { len (archive_raw )} \n "
225+ 'source_archive_format = "tar.xz"\n '
226+ f'source_archive_sha256 = "{ sha256_bytes (archive_raw )} "\n '
227+ f'source_manifest_path = "{ manifest_relative } "\n '
228+ "source_manifest_sha256 = "
229+ f'"{ sha256_bytes (FIXTURE_SOURCE_MANIFEST )} "\n ' ,
230+ encoding = "utf-8" ,
231+ )
143232
144233 def test_spec_freezes_build_input_vs_shipped_contribution (self ) -> None :
145234 value = SPEC .read_text (encoding = "utf-8" )
@@ -365,7 +454,7 @@ def test_versions_lock_pins_the_replayable_official_source_archive(self) -> None
365454 self .assertEqual (arm .get (key ), expected )
366455
367456 def expected_observation (self ) -> dict [str , object ]:
368- attribution = self .attribution
457+ attribution = self .validation_attribution
369458 return {
370459 "identity" : copy .deepcopy (attribution ["identity" ]),
371460 "build_tools" : copy .deepcopy (attribution ["build_tools" ]),
@@ -408,7 +497,7 @@ def expected_observation(self) -> dict[str, object]:
408497 attribution ["specs_inputs" ]
409498 ),
410499 "source_archive_sha256" : (
411- SOURCE_LOCK_CONTRACT [ "source_archive_sha256 " ]
500+ attribution [ "identity" ][ "source_archive" ][ "sha256 " ]
412501 ),
413502 },
414503 }
@@ -422,11 +511,16 @@ def validator(self):
422511 return value
423512
424513 def validate (self , observed : dict [str , object ]) -> dict [str , object ]:
425- return self .validator ()(
426- copy .deepcopy (self .attribution ),
427- observed ,
428- repo_root = ROOT ,
429- )
514+ with mock .patch .object (
515+ RELEASE ,
516+ "_RP2_ARM_RUNTIME_ATTRIBUTION_SHA256" ,
517+ self .validation_attribution_sha256 ,
518+ ):
519+ return self .validator ()(
520+ copy .deepcopy (self .validation_attribution ),
521+ observed ,
522+ repo_root = self .validation_root ,
523+ )
430524
431525 def test_validator_derives_notice_scope_from_contribution_not_load (self ) -> None :
432526 result = self .validate (self .expected_observation ())
0 commit comments