Skip to content

Commit 0e1fe56

Browse files
committed
[red] cover canonical frozen source paths in retained board proof
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent 76bd1cf commit 0e1fe56

1 file changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: MIT
3+
# Part of PyBLE (https://pyble.dev) — see /LICENSE.
4+
#
5+
# [red] Frozen destinations and generated BOARD_DIR source paths are distinct.
6+
7+
from __future__ import annotations
8+
9+
import importlib.util
10+
from pathlib import Path
11+
import unittest
12+
13+
14+
FIXTURE_PATH = Path(__file__).with_name(
15+
"test_release_license_policy_v2_integration.py"
16+
)
17+
SPEC = importlib.util.spec_from_file_location(
18+
"pyble_frozen_source_path_fixtures", FIXTURE_PATH
19+
)
20+
if SPEC is None or SPEC.loader is None:
21+
raise RuntimeError("cannot load frozen reconstruction fixtures")
22+
FIXTURES = importlib.util.module_from_spec(SPEC)
23+
SPEC.loader.exec_module(FIXTURES)
24+
RELEASE = FIXTURES.RELEASE
25+
BASE = FIXTURES.BASE
26+
27+
28+
class CanonicalPybleFixture(FIXTURES.ObservationV2Fixture):
29+
"""Real retained board layout with deterministic synthetic compiler tools.
30+
31+
The observer, manifest resolver, descriptor snapshot, byte comparisons and
32+
reconstruction subprocesses are production code, not patched test models.
33+
Only the compiler inputs/tools are the existing explicitly synthetic ones.
34+
"""
35+
36+
def _install_frozen_proof_fixture(self):
37+
super()._install_frozen_proof_fixture()
38+
canonical = self.firmware / "pyble"
39+
canonical.mkdir()
40+
(canonical / "pyble_workspace.py").write_text(
41+
"# SPDX-License-Identifier: MIT\nWORKSPACE = 'canonical'\n",
42+
encoding="utf-8",
43+
)
44+
(canonical / "pyble_agent.py").write_text(
45+
"# SPDX-License-Identifier: MIT\n", encoding="utf-8"
46+
)
47+
# Deliberately unlike the generated lock-derived copy: comparing this
48+
# placeholder instead of the lock output must not become acceptable.
49+
(canonical / "_version.py").write_text(
50+
"# SPDX-License-Identifier: MIT\nAGENT_VERSION = 'placeholder'\n",
51+
encoding="utf-8",
52+
)
53+
generator = (
54+
self.firmware / "upstream/micropython/tools/makemanifest.py"
55+
)
56+
text = generator.read_text(encoding="utf-8")
57+
marker = 'manifest_bytes = b"".join(\n'
58+
if text.count(marker) != 1:
59+
raise AssertionError("synthetic generator insertion point changed")
60+
generator.write_text(
61+
text.replace(
62+
marker,
63+
'sources += (\n'
64+
' ("pyble_workspace.py", board / "pyble/pyble_workspace.py"),\n'
65+
' ("_version.py", board / "pyble/_version.py"),\n'
66+
')\n' + marker,
67+
),
68+
encoding="utf-8",
69+
)
70+
for _profile_id, target, _idf_target in FIXTURES.PROFILE_TARGETS:
71+
overlay = self.firmware / "board_overlays" / target
72+
# This selects firmware/pyble through the production resolver's
73+
# canonical fallback, not a same-named overlay source.
74+
(overlay / "pyble/pyble_agent.py").unlink()
75+
manifest = overlay / "manifest.py"
76+
manifest.write_text(
77+
manifest.read_text(encoding="utf-8")
78+
+ 'module("pyble_workspace.py", base_path="$(BOARD_DIR)/pyble", opt=3)\n'
79+
+ 'module("_version.py", base_path="$(BOARD_DIR)/pyble", opt=3)\n',
80+
encoding="utf-8",
81+
)
82+
board = (
83+
self.firmware
84+
/ "upstream/micropython/ports/esp32/boards"
85+
/ RELEASE.FROZEN_TARGET_SETTINGS[target]["board"]
86+
)
87+
(board / "manifest.py").write_bytes(manifest.read_bytes())
88+
(board / "pyble/pyble_workspace.py").write_bytes(
89+
(canonical / "pyble_workspace.py").read_bytes()
90+
)
91+
(board / "pyble/_version.py").write_bytes(
92+
b"# SPDX-License-Identifier: MIT\n"
93+
b"# GENERATED by firmware/scripts/build.sh from "
94+
b"firmware/versions.lock [pyble] \xe2\x80\x94 do not edit.\n"
95+
b'AGENT_VERSION = "0.5.0"\n'
96+
b'PROTOCOL_VERSION = "PBLE/1"\n'
97+
)
98+
self.regenerate_frozen_payload(target)
99+
100+
101+
class FrozenSourcePathTests(unittest.TestCase):
102+
@classmethod
103+
def setUpClass(cls):
104+
cls.fixture = CanonicalPybleFixture()
105+
106+
@classmethod
107+
def tearDownClass(cls):
108+
cls.fixture.close()
109+
110+
def setUp(self):
111+
self.target = "esp32"
112+
self.manifest = (
113+
self.fixture.firmware / "board_overlays/esp32/manifest.py"
114+
)
115+
self.board = (
116+
self.fixture.build_root
117+
/ ".sources/esp32/micropython/ports/esp32/boards"
118+
/ RELEASE.FROZEN_TARGET_SETTINGS[self.target]["board"]
119+
)
120+
self.workspace = self.board / "pyble/pyble_workspace.py"
121+
self.canonical = self.fixture.firmware / "pyble/pyble_workspace.py"
122+
123+
def observe(self):
124+
# Same production observer called by the all-profile license collector;
125+
# pass the independently known synthetic compiler hash to avoid an
126+
# unrelated repeated compiler rebuild, not a frozen-proof mock.
127+
compiler = (
128+
self.fixture.firmware
129+
/ "upstream/micropython/mpy-cross/build/mpy-cross"
130+
)
131+
return RELEASE._audit_manifest_evidence_record(
132+
self.manifest,
133+
self.fixture.build_root / self.target / "frozen_content.c",
134+
repo_root=self.fixture.repo,
135+
target=self.target,
136+
trusted_mpy_cross_sha256=BASE.sha256_path(compiler),
137+
)
138+
139+
def test_flat_workspace_and_prefixed_package_reach_full_observer(self):
140+
context = RELEASE._audit_observe_policy_v2_context(
141+
self.fixture.policy,
142+
repo_root=self.fixture.repo,
143+
build_root=self.fixture.build_root,
144+
)
145+
self.assertEqual(len(context["manifest_evidence"]), 4)
146+
for record in context["manifest_evidence"]:
147+
selected = {
148+
item["destination"]: item for item in record["selections"]
149+
}
150+
workspace = selected["pyble_workspace.py"]
151+
self.assertEqual(
152+
workspace["source_path"], "firmware/pyble/pyble_workspace.py"
153+
)
154+
self.assertEqual(workspace["sha256"], BASE.sha256_path(self.canonical))
155+
self.assertEqual(workspace["optimization"], 3)
156+
self.assertEqual(
157+
selected["pyble/pyble_agent.py"]["source_path"],
158+
"firmware/pyble/pyble_agent.py",
159+
)
160+
frozen = {item["destination"] for item in record["frozen_mpy"]}
161+
self.assertIn("pyble_workspace.mpy", frozen)
162+
self.assertIn("pyble/pyble_agent.mpy", frozen)
163+
self.assertNotIn("pyble/pyble_workspace.mpy", frozen)
164+
self.assertNotIn("pyble/pyble/pyble_agent.mpy", frozen)
165+
self.assertFalse((self.board / "pyble_workspace.py").exists())
166+
self.assertFalse((self.board / "pyble/pyble").exists())
167+
168+
def test_missing_nested_workspace_is_rejected(self):
169+
with BASE.removed_file(self.workspace):
170+
with self.assertRaisesRegex(RELEASE.ReleaseError, "source.*missing"):
171+
self.observe()
172+
173+
def test_matching_flat_decoy_cannot_replace_missing_nested_source(self):
174+
decoy = self.board / "pyble_workspace.py"
175+
decoy.write_bytes(self.canonical.read_bytes())
176+
try:
177+
with BASE.removed_file(self.workspace):
178+
with self.assertRaisesRegex(RELEASE.ReleaseError, "source.*missing"):
179+
self.observe()
180+
finally:
181+
decoy.unlink()
182+
183+
def test_matching_flat_decoy_cannot_hide_stale_nested_source(self):
184+
decoy = self.board / "pyble_workspace.py"
185+
decoy.write_bytes(self.canonical.read_bytes())
186+
try:
187+
with BASE.patched_bytes(self.workspace, b"# stale nested bytes\n"):
188+
with self.assertRaisesRegex(RELEASE.ReleaseError, "source.*stale"):
189+
self.observe()
190+
finally:
191+
decoy.unlink()
192+
193+
def test_nested_workspace_file_symlink_is_rejected(self):
194+
with BASE.removed_file(self.workspace):
195+
self.workspace.symlink_to(self.canonical)
196+
try:
197+
with self.assertRaisesRegex(RELEASE.ReleaseError, "symlink"):
198+
self.observe()
199+
finally:
200+
self.workspace.unlink()
201+
202+
def test_nested_pyble_directory_symlink_is_rejected(self):
203+
nested = self.board / "pyble"
204+
backup = self.board.parent / "pyble-path-test-backup"
205+
nested.rename(backup)
206+
try:
207+
nested.symlink_to(backup, target_is_directory=True)
208+
with self.assertRaisesRegex(RELEASE.ReleaseError, "symlink"):
209+
self.observe()
210+
finally:
211+
nested.unlink()
212+
backup.rename(nested)
213+
214+
def test_changed_canonical_workspace_is_rejected(self):
215+
with BASE.patched_bytes(self.canonical, b"# changed canonical bytes\n"):
216+
with self.assertRaisesRegex(RELEASE.ReleaseError, "source.*stale"):
217+
self.observe()
218+
219+
def test_already_prefixed_package_copy_is_still_byte_checked(self):
220+
with BASE.patched_bytes(
221+
self.board / "pyble/pyble_agent.py", b"# stale package\n"
222+
):
223+
with self.assertRaisesRegex(RELEASE.ReleaseError, "source.*stale"):
224+
self.observe()
225+
226+
def test_lock_version_exception_does_not_compare_placeholder(self):
227+
version = self.board / "pyble/_version.py"
228+
placeholder = self.fixture.firmware / "pyble/_version.py"
229+
self.assertNotEqual(version.read_bytes(), placeholder.read_bytes())
230+
self.observe()
231+
with BASE.patched_bytes(version, placeholder.read_bytes()):
232+
with self.assertRaisesRegex(RELEASE.ReleaseError, "lock-derived.*stale"):
233+
self.observe()
234+
235+
def test_overlay_copy_keeps_its_existing_byte_check(self):
236+
with BASE.patched_bytes(self.board / "_boot.py", b"# stale boot\n"):
237+
with self.assertRaisesRegex(RELEASE.ReleaseError, "source.*stale"):
238+
self.observe()
239+
240+
def test_duplicate_destination_remains_rejected(self):
241+
original = self.manifest.read_text(encoding="utf-8")
242+
with BASE.patched_text(
243+
self.manifest,
244+
original
245+
+ 'module("pyble_workspace.py", base_path="$(BOARD_DIR)/pyble")\n',
246+
):
247+
with self.assertRaisesRegex(RELEASE.ReleaseError, "duplicates destination"):
248+
self.observe()
249+
250+
def test_historical_overlay_package_layout_still_reconstructs(self):
251+
historical = FIXTURES.ObservationV2Fixture()
252+
try:
253+
context = RELEASE._audit_observe_policy_v2_context(
254+
historical.policy,
255+
repo_root=historical.repo,
256+
build_root=historical.build_root,
257+
)
258+
for record in context["manifest_evidence"]:
259+
selected = {
260+
item["destination"]: item for item in record["selections"]
261+
}
262+
self.assertNotIn("pyble_workspace.py", selected)
263+
self.assertEqual(
264+
selected["pyble/pyble_agent.py"]["source_path"],
265+
"firmware/board_overlays/%s/pyble/pyble_agent.py"
266+
% record["target"],
267+
)
268+
finally:
269+
historical.close()
270+
271+
272+
if __name__ == "__main__":
273+
unittest.main()

0 commit comments

Comments
 (0)