flashgate 0.4.0__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flashgate-0.4.0 → flashgate-0.4.2}/PKG-INFO +3 -3
- {flashgate-0.4.0 → flashgate-0.4.2}/README.md +2 -2
- flashgate-0.4.2/flashgate/__init__.py +13 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/cli.py +43 -21
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/swdsig.py +27 -3
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/PKG-INFO +3 -3
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/SOURCES.txt +1 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/pyproject.toml +1 -1
- flashgate-0.4.2/tests/test_cli.py +148 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_swdsig.py +22 -3
- flashgate-0.4.0/flashgate/__init__.py +0 -8
- {flashgate-0.4.0 → flashgate-0.4.2}/LICENSE +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/__main__.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/board.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/flasher.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/gatestate.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/mcp_server.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/probes.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/serialmon.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate/sttools.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/dependency_links.txt +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/entry_points.txt +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/requires.txt +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/flashgate.egg-info/top_level.txt +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/setup.cfg +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_board.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_gatestate.py +0 -0
- {flashgate-0.4.0 → flashgate-0.4.2}/tests/test_probes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashgate
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -79,8 +79,8 @@ fresh clone verifies out of the box once wired up.
|
|
|
79
79
|
| 2 | flash failed |
|
|
80
80
|
| 3 | board stayed silent (no banner / no signature within timeout) |
|
|
81
81
|
| 4 | error string seen on serial (HardFault, assertion) |
|
|
82
|
-
| 5 | on-board identity ≠ repo state |
|
|
83
|
-
| 6 | environment error (no ST-Link / serial / tools) |
|
|
82
|
+
| 5 | on-board identity ≠ repo state (git sha or board name) |
|
|
83
|
+
| 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
|
|
84
84
|
| 7 | functional probe failed |
|
|
85
85
|
|
|
86
86
|
## The Stop hook
|
|
@@ -63,8 +63,8 @@ fresh clone verifies out of the box once wired up.
|
|
|
63
63
|
| 2 | flash failed |
|
|
64
64
|
| 3 | board stayed silent (no banner / no signature within timeout) |
|
|
65
65
|
| 4 | error string seen on serial (HardFault, assertion) |
|
|
66
|
-
| 5 | on-board identity ≠ repo state |
|
|
67
|
-
| 6 | environment error (no ST-Link / serial / tools) |
|
|
66
|
+
| 5 | on-board identity ≠ repo state (git sha or board name) |
|
|
67
|
+
| 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
|
|
68
68
|
| 7 | functional probe failed |
|
|
69
69
|
|
|
70
70
|
## The Stop hook
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""flashgate: hardware-in-the-loop verification gate for coding agents.
|
|
2
|
+
|
|
3
|
+
The agent may not claim firmware work is done until the board itself
|
|
4
|
+
says so: build -> flash -> boot banner over serial, with exit codes
|
|
5
|
+
that an agent harness (Stop hook) can enforce.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
__version__ = _pkg_version("flashgate") # pyproject.toml is the single source
|
|
12
|
+
except PackageNotFoundError: # running from a raw source tree
|
|
13
|
+
__version__ = "0.0.0"
|
|
@@ -260,8 +260,10 @@ def cmd_verify(board: Board, probe_names: list[str] | None, evidence: str | None
|
|
|
260
260
|
|
|
261
261
|
def _verify_swd(board: Board, probe_names: list[str] | None) -> int:
|
|
262
262
|
"""Boot gate through the ST-Link alone: fixed-address RAM signature.
|
|
263
|
-
No serial cable needed
|
|
264
|
-
|
|
263
|
+
No serial cable needed. Probes explicitly requested via --probe /
|
|
264
|
+
--all-probes (as the Stop hook does) MUST run: if the console is
|
|
265
|
+
missing or unusable, verify FAILS (exit 6) — a check that didn't
|
|
266
|
+
happen must never count as a pass."""
|
|
265
267
|
print(_cyan(f"[verify] {board.name}: build -> flash -> SWD signature"))
|
|
266
268
|
rc = _build(board)
|
|
267
269
|
if rc != EXIT_OK:
|
|
@@ -287,6 +289,9 @@ def _verify_swd(board: Board, probe_names: list[str] | None) -> int:
|
|
|
287
289
|
board.flash_connect, board.sig_address, board.sig_size,
|
|
288
290
|
timeout_s=board.banner_timeout_s)
|
|
289
291
|
if info is None:
|
|
292
|
+
if "not supported" in err:
|
|
293
|
+
print(_red(f"[verify] SIGNATURE LAYOUT MISMATCH: {err}"))
|
|
294
|
+
return EXIT_ENV
|
|
290
295
|
print(_red(f"[verify] TIMEOUT: board never published its SWD signature ({err})"))
|
|
291
296
|
return EXIT_BANNER_TIMEOUT
|
|
292
297
|
|
|
@@ -300,26 +305,46 @@ def _verify_swd(board: Board, probe_names: list[str] | None) -> int:
|
|
|
300
305
|
return EXIT_SHA_MISMATCH
|
|
301
306
|
|
|
302
307
|
if probe_names is not None:
|
|
303
|
-
port,
|
|
308
|
+
port, why = _console_port(board)
|
|
304
309
|
if port is None:
|
|
305
|
-
print(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
310
|
+
print(_red("[verify] probes were explicitly requested but no console serial "
|
|
311
|
+
f"is available ({why}) — failing rather than passing unverified "
|
|
312
|
+
"(drop --probe/--all-probes, or connect the console UART)"))
|
|
313
|
+
return EXIT_ENV
|
|
314
|
+
try:
|
|
315
|
+
conn = serialmon.open_flush(port, board.baudrate)
|
|
316
|
+
except serial.SerialException as exc:
|
|
317
|
+
print(_red(f"[verify] probes were explicitly requested but {port} cannot be "
|
|
318
|
+
f"opened ({exc}) — failing rather than passing unverified"))
|
|
319
|
+
return EXIT_ENV
|
|
320
|
+
try:
|
|
321
|
+
return _run_probes(board, None if probe_names == ["all"] else probe_names, conn)
|
|
322
|
+
finally:
|
|
323
|
+
conn.close()
|
|
317
324
|
|
|
318
325
|
print(_green(f"[verify] PASS — the board's RAM itself confirms the firmware booted "
|
|
319
326
|
f"(git={info['git']}), no serial cable involved"))
|
|
320
327
|
return EXIT_OK
|
|
321
328
|
|
|
322
329
|
|
|
330
|
+
def _check_banner_identity(board: Board, info: dict) -> int | None:
|
|
331
|
+
"""Compare the banner's identity fields against the board profile.
|
|
332
|
+
Returns an exit code on contradiction, None when consistent."""
|
|
333
|
+
got_board = info.get("board")
|
|
334
|
+
if got_board and got_board != board.name:
|
|
335
|
+
print(_red(f"[verify] BOARD MISMATCH: banner says board={got_board}, "
|
|
336
|
+
f"profile expects {board.name} — the verdict would not be "
|
|
337
|
+
"about the board you configured"))
|
|
338
|
+
return EXIT_SHA_MISMATCH
|
|
339
|
+
expected = board.head_sha()
|
|
340
|
+
got = info.get("git")
|
|
341
|
+
if expected and got and expected != got:
|
|
342
|
+
print(_red(f"[verify] SHA MISMATCH: board runs {got}, repo HEAD is {expected} "
|
|
343
|
+
"(rebuild after committing?)"))
|
|
344
|
+
return EXIT_SHA_MISMATCH
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
323
348
|
def _verify_uart(board: Board, probe_names: list[str] | None) -> int:
|
|
324
349
|
all_probes = probe_names == ["all"]
|
|
325
350
|
title = "[verify] {b}: build -> flash -> boot banner" + (" -> probes" if probe_names is not None else "")
|
|
@@ -365,12 +390,9 @@ def _verify_uart(board: Board, probe_names: list[str] | None) -> int:
|
|
|
365
390
|
print(_green(f"[verify] banner OK: board={info.get('board')} git={info.get('git')} "
|
|
366
391
|
f"build={info.get('build')} rtos={info.get('rtos')}"))
|
|
367
392
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
print(_red(f"[verify] SHA MISMATCH: board runs {got}, repo HEAD is {expected} "
|
|
372
|
-
"(rebuild after committing?)"))
|
|
373
|
-
return EXIT_SHA_MISMATCH
|
|
393
|
+
mismatch = _check_banner_identity(board, info)
|
|
394
|
+
if mismatch is not None:
|
|
395
|
+
return mismatch
|
|
374
396
|
|
|
375
397
|
if probe_names is not None:
|
|
376
398
|
return _run_probes(board, None if all_probes else probe_names, conn)
|
|
@@ -18,6 +18,7 @@ from .flasher import FLASH_ATTEMPTS # reuse attempt count semantics
|
|
|
18
18
|
from .sttools import augmented_env, find_cubeprogrammer
|
|
19
19
|
|
|
20
20
|
SIG_MAGIC = 0xF1A5C0DE
|
|
21
|
+
SIG_VERSION = 1 # the only signature layout this tool decodes
|
|
21
22
|
READ_TIMEOUT_S = 30
|
|
22
23
|
|
|
23
24
|
|
|
@@ -25,6 +26,13 @@ class SwdError(Exception):
|
|
|
25
26
|
pass
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
class SignatureLayoutError(SwdError):
|
|
30
|
+
"""CRC-valid signature whose layout version this tool cannot decode.
|
|
31
|
+
|
|
32
|
+
Waiting longer cannot fix a firmware/tool generation mismatch, so
|
|
33
|
+
callers should fail fast instead of polling to the timeout."""
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
def read_ram(connect: str, address: int, size: int) -> bytes | None:
|
|
29
37
|
"""One CubeProgrammer memory read (HotPlug: does not halt the core)."""
|
|
30
38
|
cli = find_cubeprogrammer()
|
|
@@ -49,7 +57,11 @@ def read_ram(connect: str, address: int, size: int) -> bytes | None:
|
|
|
49
57
|
|
|
50
58
|
|
|
51
59
|
def parse_signature(buf: bytes) -> dict | None:
|
|
52
|
-
"""Validate magic
|
|
60
|
+
"""Validate magic, layout version and CRC; decode the identity.
|
|
61
|
+
|
|
62
|
+
None = not (yet) valid (retry). SignatureLayoutError = a complete,
|
|
63
|
+
CRC-valid signature from a layout generation this tool refuses to
|
|
64
|
+
interpret — treating it as evidence would be a lie."""
|
|
53
65
|
if len(buf) < 64:
|
|
54
66
|
return None
|
|
55
67
|
magic, version, flags = struct.unpack_from("<IHH", buf, 0)
|
|
@@ -58,6 +70,11 @@ def parse_signature(buf: bytes) -> dict | None:
|
|
|
58
70
|
crc_field = struct.unpack_from("<I", buf, 0x30)[0]
|
|
59
71
|
if zlib.crc32(buf[:0x30]) & 0xFFFFFFFF != crc_field:
|
|
60
72
|
return None # raced the boot-time write: retry
|
|
73
|
+
if version != SIG_VERSION:
|
|
74
|
+
raise SignatureLayoutError(
|
|
75
|
+
f"signature layout version {version} not supported "
|
|
76
|
+
f"(this tool decodes version {SIG_VERSION} only — "
|
|
77
|
+
"update flashgate or the firmware's bsp_signature)")
|
|
61
78
|
git = buf[0x08:0x18].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
|
62
79
|
build = buf[0x18:0x30].split(b"\x00", 1)[0].decode("ascii", errors="replace")
|
|
63
80
|
return {"version": version, "flags": flags, "git": git, "build": build}
|
|
@@ -66,7 +83,11 @@ def parse_signature(buf: bytes) -> dict | None:
|
|
|
66
83
|
def wait_for_signature(
|
|
67
84
|
connect: str, address: int, size: int = 64, timeout_s: float = 15.0
|
|
68
85
|
) -> tuple[dict | None, str]:
|
|
69
|
-
"""Poll the signature until valid or timeout. Returns (info, last_error).
|
|
86
|
+
"""Poll the signature until valid or timeout. Returns (info, last_error).
|
|
87
|
+
|
|
88
|
+
A SignatureLayoutError aborts the poll immediately (returned as
|
|
89
|
+
(None, message)): an alien layout version is a permanent firmware/tool
|
|
90
|
+
mismatch, not something retrying can resolve."""
|
|
70
91
|
deadline = time.monotonic() + timeout_s
|
|
71
92
|
last = "no valid signature yet"
|
|
72
93
|
while time.monotonic() < deadline:
|
|
@@ -76,7 +97,10 @@ def wait_for_signature(
|
|
|
76
97
|
last = str(exc)
|
|
77
98
|
time.sleep(1.0)
|
|
78
99
|
continue
|
|
79
|
-
|
|
100
|
+
try:
|
|
101
|
+
info = parse_signature(buf or b"")
|
|
102
|
+
except SignatureLayoutError as exc:
|
|
103
|
+
return None, str(exc)
|
|
80
104
|
if info is not None:
|
|
81
105
|
return info, ""
|
|
82
106
|
time.sleep(0.5)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashgate
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -79,8 +79,8 @@ fresh clone verifies out of the box once wired up.
|
|
|
79
79
|
| 2 | flash failed |
|
|
80
80
|
| 3 | board stayed silent (no banner / no signature within timeout) |
|
|
81
81
|
| 4 | error string seen on serial (HardFault, assertion) |
|
|
82
|
-
| 5 | on-board identity ≠ repo state |
|
|
83
|
-
| 6 | environment error (no ST-Link / serial / tools) |
|
|
82
|
+
| 5 | on-board identity ≠ repo state (git sha or board name) |
|
|
83
|
+
| 6 | environment error (no ST-Link / serial / tools) — including probes explicitly required via `--probe`/`--all-probes` but the console UART is unavailable: a check that cannot run never counts as a pass |
|
|
84
84
|
| 7 | functional probe failed |
|
|
85
85
|
|
|
86
86
|
## The Stop hook
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flashgate"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""cli-level fail-closed guards: a check that cannot run must fail, never pass.
|
|
2
|
+
|
|
3
|
+
Regression coverage for the false-green found in 0.4.1: --all-probes with
|
|
4
|
+
no console serial in swd mode used to print a yellow "skipped" and return 0.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
import serial
|
|
11
|
+
|
|
12
|
+
from flashgate import cli
|
|
13
|
+
from flashgate.board import Board, load_board
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
BODY = """
|
|
17
|
+
board: test-board
|
|
18
|
+
mcu: STM32F999
|
|
19
|
+
description: unit test fixture
|
|
20
|
+
firmware:
|
|
21
|
+
dir: fw
|
|
22
|
+
build: ninja -C build
|
|
23
|
+
artifact: build/fw.bin
|
|
24
|
+
serial:
|
|
25
|
+
baudrate: 9600
|
|
26
|
+
banner: 'BOOT board={board} git={git}'
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_board(tmp_path) -> Board:
|
|
31
|
+
fw = tmp_path / "fw"
|
|
32
|
+
fw.mkdir(exist_ok=True)
|
|
33
|
+
p = tmp_path / "board.yaml"
|
|
34
|
+
p.write_text(BODY, encoding="utf-8")
|
|
35
|
+
return load_board(p)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture
|
|
39
|
+
def happy_swd(monkeypatch):
|
|
40
|
+
"""Everything up to (but excluding) the probe stage succeeds: build,
|
|
41
|
+
flash, wipe, start, and a valid v1 signature answers immediately."""
|
|
42
|
+
monkeypatch.setattr(cli, "_build", lambda board: cli.EXIT_OK)
|
|
43
|
+
monkeypatch.setattr(cli.flasher, "flash",
|
|
44
|
+
lambda *a, **k: SimpleNamespace(ok=True, detail=""))
|
|
45
|
+
monkeypatch.setattr(cli.flasher, "write32", lambda *a, **k: True)
|
|
46
|
+
monkeypatch.setattr(cli.flasher, "start_app", lambda *a, **k: True)
|
|
47
|
+
monkeypatch.setattr(cli.swdsig, "wait_for_signature",
|
|
48
|
+
lambda *a, **k: ({"version": 1, "flags": 0,
|
|
49
|
+
"git": "x", "build": "y"}, ""))
|
|
50
|
+
monkeypatch.setattr(Board, "head_sha", lambda self: None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestSwdProbeGuard:
|
|
54
|
+
def test_probes_required_no_serial_fails(self, tmp_path, happy_swd, monkeypatch):
|
|
55
|
+
board = make_board(tmp_path)
|
|
56
|
+
monkeypatch.setattr(cli, "_console_port", lambda b: (None, "no serial found"))
|
|
57
|
+
assert cli._verify_swd(board, ["all"]) == cli.EXIT_ENV
|
|
58
|
+
|
|
59
|
+
def test_probes_required_port_unopenable_fails(self, tmp_path, happy_swd, monkeypatch):
|
|
60
|
+
board = make_board(tmp_path)
|
|
61
|
+
monkeypatch.setattr(cli, "_console_port", lambda b: ("COM3", "hint"))
|
|
62
|
+
|
|
63
|
+
def boom(*a, **k):
|
|
64
|
+
raise serial.SerialException("access denied")
|
|
65
|
+
monkeypatch.setattr(cli.serialmon, "open_flush", boom)
|
|
66
|
+
assert cli._verify_swd(board, ["all"]) == cli.EXIT_ENV
|
|
67
|
+
|
|
68
|
+
def test_probes_required_run_when_serial_ok(self, tmp_path, happy_swd, monkeypatch):
|
|
69
|
+
board = make_board(tmp_path)
|
|
70
|
+
monkeypatch.setattr(cli, "_console_port", lambda b: ("COM3", "hint"))
|
|
71
|
+
monkeypatch.setattr(cli.serialmon, "open_flush",
|
|
72
|
+
lambda *a, **k: SimpleNamespace(close=lambda: None))
|
|
73
|
+
seen = {}
|
|
74
|
+
def run_probes(board, names, conn):
|
|
75
|
+
seen["names"] = names
|
|
76
|
+
return cli.EXIT_OK
|
|
77
|
+
monkeypatch.setattr(cli, "_run_probes", run_probes)
|
|
78
|
+
assert cli._verify_swd(board, ["all"]) == cli.EXIT_OK
|
|
79
|
+
assert seen["names"] is None # "all" expanded, not literal
|
|
80
|
+
|
|
81
|
+
def test_no_probes_requested_still_passes_without_serial(self, tmp_path, happy_swd, monkeypatch):
|
|
82
|
+
board = make_board(tmp_path)
|
|
83
|
+
monkeypatch.setattr(cli, "_console_port", lambda b: (None, "no serial"))
|
|
84
|
+
assert cli._verify_swd(board, None) == cli.EXIT_OK
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestSignatureErrors:
|
|
88
|
+
def test_layout_mismatch_fails_fast_as_env_error(self, tmp_path, happy_swd, monkeypatch):
|
|
89
|
+
board = make_board(tmp_path)
|
|
90
|
+
monkeypatch.setattr(
|
|
91
|
+
cli.swdsig, "wait_for_signature",
|
|
92
|
+
lambda *a, **k: (None, "signature layout version 3 not supported "
|
|
93
|
+
"(this tool decodes version 1 only)"))
|
|
94
|
+
assert cli._verify_swd(board, None) == cli.EXIT_ENV
|
|
95
|
+
|
|
96
|
+
def test_plain_timeout_still_exit_3(self, tmp_path, happy_swd, monkeypatch):
|
|
97
|
+
board = make_board(tmp_path)
|
|
98
|
+
monkeypatch.setattr(cli.swdsig, "wait_for_signature",
|
|
99
|
+
lambda *a, **k: (None, "no valid signature yet"))
|
|
100
|
+
assert cli._verify_swd(board, None) == cli.EXIT_BANNER_TIMEOUT
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestBannerIdentity:
|
|
104
|
+
def test_board_mismatch_detected(self, tmp_path, monkeypatch):
|
|
105
|
+
board = make_board(tmp_path)
|
|
106
|
+
monkeypatch.setattr(Board, "head_sha", lambda self: None)
|
|
107
|
+
rc = cli._check_banner_identity(board, {"board": "other-board", "git": "x"})
|
|
108
|
+
assert rc == cli.EXIT_SHA_MISMATCH
|
|
109
|
+
|
|
110
|
+
def test_sha_mismatch_detected(self, tmp_path, monkeypatch):
|
|
111
|
+
board = make_board(tmp_path)
|
|
112
|
+
monkeypatch.setattr(Board, "head_sha", lambda self: "abc1234")
|
|
113
|
+
rc = cli._check_banner_identity(board, {"board": "test-board", "git": "deadbee"})
|
|
114
|
+
assert rc == cli.EXIT_SHA_MISMATCH
|
|
115
|
+
|
|
116
|
+
def test_consistent_identity_passes(self, tmp_path, monkeypatch):
|
|
117
|
+
board = make_board(tmp_path)
|
|
118
|
+
monkeypatch.setattr(Board, "head_sha", lambda self: "abc1234")
|
|
119
|
+
assert cli._check_banner_identity(
|
|
120
|
+
board, {"board": "test-board", "git": "abc1234"}) is None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class TestAutoRouting:
|
|
124
|
+
def test_auto_picks_swd_without_serial(self, tmp_path, monkeypatch):
|
|
125
|
+
board = make_board(tmp_path)
|
|
126
|
+
seen = {}
|
|
127
|
+
monkeypatch.setattr(cli, "_console_port", lambda b: (None, "none"))
|
|
128
|
+
def fake_swd(b, p):
|
|
129
|
+
seen["mode"] = "swd"; return cli.EXIT_OK
|
|
130
|
+
def fake_uart(b, p):
|
|
131
|
+
seen["mode"] = "uart"; return cli.EXIT_OK
|
|
132
|
+
monkeypatch.setattr(cli, "_verify_swd", fake_swd)
|
|
133
|
+
monkeypatch.setattr(cli, "_verify_uart", fake_uart)
|
|
134
|
+
assert cli.cmd_verify(board, None, None) == cli.EXIT_OK
|
|
135
|
+
assert seen["mode"] == "swd"
|
|
136
|
+
|
|
137
|
+
def test_auto_picks_uart_with_serial(self, tmp_path, monkeypatch):
|
|
138
|
+
board = make_board(tmp_path)
|
|
139
|
+
seen = {}
|
|
140
|
+
monkeypatch.setattr(cli, "_console_port", lambda b: ("COM3", "hint"))
|
|
141
|
+
def fake_swd(b, p):
|
|
142
|
+
seen["mode"] = "swd"; return cli.EXIT_OK
|
|
143
|
+
def fake_uart(b, p):
|
|
144
|
+
seen["mode"] = "uart"; return cli.EXIT_OK
|
|
145
|
+
monkeypatch.setattr(cli, "_verify_swd", fake_swd)
|
|
146
|
+
monkeypatch.setattr(cli, "_verify_uart", fake_uart)
|
|
147
|
+
assert cli.cmd_verify(board, None, None) == cli.EXIT_OK
|
|
148
|
+
assert seen["mode"] == "uart"
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
import struct
|
|
4
4
|
import zlib
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import pytest
|
|
7
7
|
|
|
8
|
+
from flashgate.swdsig import SIG_MAGIC, SignatureLayoutError, parse_signature
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
def build_sig(git=b"abc1234", build=b"2026-08-31T08:00:00Z", magic=SIG_MAGIC,
|
|
12
|
+
version=1) -> bytes:
|
|
10
13
|
buf = bytearray(64)
|
|
11
14
|
struct.pack_into("<I", buf, 0x00, magic)
|
|
12
|
-
struct.pack_into("<H", buf, 0x04,
|
|
15
|
+
struct.pack_into("<H", buf, 0x04, version)
|
|
13
16
|
struct.pack_into("<H", buf, 0x06, 1)
|
|
14
17
|
buf[0x08:0x08 + len(git)] = git
|
|
15
18
|
buf[0x18:0x18 + len(build)] = build
|
|
@@ -42,3 +45,19 @@ class TestParseSignature:
|
|
|
42
45
|
|
|
43
46
|
def test_short_buffer_rejected(self):
|
|
44
47
|
assert parse_signature(build_sig()[:48]) is None
|
|
48
|
+
|
|
49
|
+
def test_unknown_layout_version_rejected(self):
|
|
50
|
+
# CRC-valid, magic-valid — but a layout generation this tool
|
|
51
|
+
# refuses to decode must never be treated as evidence.
|
|
52
|
+
with pytest.raises(SignatureLayoutError, match="version 2"):
|
|
53
|
+
parse_signature(build_sig(version=2))
|
|
54
|
+
|
|
55
|
+
def test_wait_for_signature_fails_fast_on_unknown_version(self, monkeypatch):
|
|
56
|
+
# An alien layout is permanent: the poll must return immediately
|
|
57
|
+
# with the reason instead of spinning to the timeout.
|
|
58
|
+
from flashgate import swdsig
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr(swdsig, "read_ram", lambda *a, **k: build_sig(version=9))
|
|
61
|
+
info, err = swdsig.wait_for_signature("port=SWD", 0x2001FF00, 64, timeout_s=20)
|
|
62
|
+
assert info is None
|
|
63
|
+
assert "version 9 not supported" in err
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"""flashgate: hardware-in-the-loop verification gate for coding agents.
|
|
2
|
-
|
|
3
|
-
The agent may not claim firmware work is done until the board itself
|
|
4
|
-
says so: build -> flash -> boot banner over serial, with exit codes
|
|
5
|
-
that an agent harness (Stop hook) can enforce.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
__version__ = "0.3.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|