Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
rm -f "$SELECT_FILE"
if [ -n "${CIRCLE_PULL_REQUEST:-}" ]; then
git fetch --no-tags origin master || true
# the selector reads each roster board's flasher from agentrc's board registry
python3 tools/get_deps.py lib/agentrc
# the suites gate the selector: test_change_impact.py owns the rules,
# test_build_select.py the check_build contract, and test_ci_metrics.py the
# config2 sentinel contract this job rewrites
Expand Down
3 changes: 3 additions & 0 deletions .claude/skills/build-doc/scripts/gen_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

ROOT = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(ROOT / 'tools'))
sys.path.insert(0, str(ROOT / 'test' / 'hil'))
from get_deps import gen_doc as gen_deps_doc, rst_table # noqa: E402
from helper import hil_util # noqa: E402

HEADER = '<!-- Generated by .claude/skills/build-doc/scripts/gen_doc.py - do not edit. -->'

Expand Down Expand Up @@ -109,6 +111,7 @@ def hil_md(root):
out = [HEADER, '']
for title, src in (('ci rig', 'test/hil/tinyusb.json'), ('hfp rig', 'test/hil/hfp.json')):
entries = json.loads((root / src).read_text(encoding='utf-8')).get('boards', [])
entries = [hil_util.hil_boards().resolve(b) for b in entries] # each flasher is agentrc's
if not entries:
continue
out += [f'### {title}', '', f'{len(entries)} boards, from `{src}`.', '',
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/hil-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ htpc, a dev PC, also carries TinyUF2's rig, `test/hil/local.json` in that checko
`tusb` (ssh alias `hifiphile`) is an external rig (hosted by maintainer hifiphile), exercised by the
GitHub CI `hil-tinyusb (hfp.json)` matrix job — **never run HIL against it unless the user explicitly asks.**

A roster lists `boards` and parked `boards-skip`. Common entry fields: `name` (also the board's lock name), `uid`, `tests`, `flasher {name, uid, args, vid_pid, verify}` (only `name` and `uid` required), and optional `flasher_recover`, `variant`, `logger`, `toolchain`, `uart_uid`, `note`; `test/hil/hil_test.py`'s `Board` type is the full shape. A review may add or enable a `local.json` entry on a dev PC: name, board uid, flasher and tests, and a flashable example to run on it.
A roster lists `boards` and parked `boards-skip`. Common entry fields: `name` (also the board's lock name), `uid`, `tests`, and optional `variant`, `logger`, `toolchain`, `uart_uid`, `note`; `test/hil/hil_test.py`'s `Board` type is the full shape. A board's `flasher {name, uid, args, vid_pid, verify}` and optional `flasher_recover` are agentrc's, from its board registry (`lib/agentrc/skills/hil/boards.json`, `hil` Board registry) by `uid`, never the roster's: a rig board's wiring changes by an agentrc commit and a `lib/agentrc` pin bump. Every reader of a flasher resolves the entry through `hil_util.hil_boards()`. A review may add or enable a `local.json` entry on a dev PC: name, board uid, tests, a flashable example to run on it, and the board's own `flasher` only when the registry does not know its uid.

If `local.json` is missing on a dev PC, ask the user to supply one before a `hil_test.py` run or a farm check. An agent that cannot ask (`hil-operator`) does not run `hil_test.py`: it returns the no-run rows Results defines, `detail` naming the missing `test/hil/local.json`. Fall back to `tinyusb.json` only when the request or task scope says so; a manual session locks by board name as Reservation says.

Expand Down
10 changes: 8 additions & 2 deletions .claude/skills/hil-contract/scripts/hil_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def helper(name):
'test/hil/pyocd/README.md',
'tools/get_deps.py',
'lib/agentrc/.git/HEAD',
'lib/agentrc/skills/hil/boards.json',
'lib/agentrc/skills/hil/scripts/hil_boards.py',
'lib/agentrc/skills/hil/scripts/hil_flash.py',
'lib/agentrc/skills/hil/scripts/hil_load.py',
'lib/agentrc/skills/hil/scripts/hil_openocd_esp.py',
Expand Down Expand Up @@ -137,8 +139,12 @@ def select_boards(config, args):
fail(f'not in the config: {" ".join(unknown)} (-b takes board names, not variant names)')
selected = boards or list(roster)
if args.flasher or args.exclude_flasher:
selected = [n for n in selected if roster[n]['flasher']['name'] not in args.exclude_flasher
and (not args.flasher or roster[n]['flasher']['name'] in args.flasher)]
try: # each board's flasher is agentrc's (its board registry), as hil_test.py resolves it
kind = {n: helper('hil_util').hil_boards().resolve(roster[n])['flasher']['name'] for n in selected}
except ValueError as e:
fail(f'config: {e}')
selected = [n for n in selected if kind[n] not in args.exclude_flasher
and (not args.flasher or kind[n] in args.flasher)]
if not selected:
fail(f'no board left after the flasher filter (--flasher {args.flasher or "-"}, '
f'--exclude-flasher {args.exclude_flasher or "-"})')
Expand Down
11 changes: 6 additions & 5 deletions .claude/skills/usbtest/scripts/run_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ def kill_children(spare=()):


def resolve(config, board_name, variant):
"""(board, variant name) from the roster, or Refused naming the alternatives."""
"""(board, variant name) from the roster, its flashers from agentrc's board registry, or Refused
naming the alternatives."""
try:
cfg = json.loads(Path(config).read_text())
except (OSError, ValueError) as e:
Expand All @@ -153,10 +154,10 @@ def resolve(config, board_name, variant):
if 'device/usbtest' not in change_impact.board_tests(board):
raise Refused(f'{board_name} does not run device/usbtest in {config} (its "tests" entry); '
f'its device port may not reach this host')
flasher = board.get('flasher')
if not (isinstance(board.get('uid'), str) and board['uid'] and isinstance(flasher, dict)
and isinstance(flasher.get('name'), str) and flasher['name']):
raise Refused(f'{board_name} in {config} needs a "uid" and a "flasher" with a "name"')
try: # its flasher and flasher_recover are agentrc's, by its uid (`hil` skill, Board registry)
board = hil_util.hil_boards().resolve(board)
except ValueError as e:
raise Refused(f'{config}: {e}')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if why := hil_flash.openocd_esp_refused(board, retains_lock=False):
raise Refused(f'{board_name}: {why}')
try:
Expand Down
5 changes: 1 addition & 4 deletions .claude/skills/usbtest/scripts/wedge_drill.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,7 @@ def halt_board(board, rec_board, halt_args):
if halt_args is None:
raise Refused(f'{name}: the drill halts through openocd and this board recovers through '
f'{rec_board["flasher"]["name"]}; name the openocd halt with --halt-openocd')
uid = board['flasher'].get('uid')
if not uid:
raise Refused(f'{name}: no probe uid for openocd\'s adapter serial')
halt = {'name': 'openocd', 'uid': uid, 'args': halt_args}
halt = {'name': 'openocd', 'uid': board['flasher']['uid'], 'args': halt_args}
if not hil_flash.convoy_safe(halt):
raise Refused(f'{name}: --halt-openocd {halt_args!r} is not convoy-safe')
return {'name': name, 'flasher': halt}
Expand Down
9 changes: 5 additions & 4 deletions .claude/test/test_gen_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ def fixture(root):
(root / 'docs' / 'reference').mkdir(parents=True)
(root / 'test' / 'hil').mkdir(parents=True)
(root / 'test' / 'hil' / 'tinyusb.json').write_text(json.dumps({'boards': [
{'name': 'acme_one', 'tests': {'device': True, 'host': True, 'dual': False},
'flasher': {'name': 'jlink'}, 'variant': [{'name': 'acme_one_hs'}],
{'name': 'acme_one', 'uid': 'A1', 'tests': {'device': True, 'host': True, 'dual': False},
'flasher': {'name': 'jlink', 'uid': 'p1'}, 'variant': [{'name': 'acme_one_hs'}],
'note': 'needs a | jumper \u00f8'},
{'name': 'acme_two', 'tests': {'only': ['host/cdc_msc', 'device/hid']}},
{'name': 'acme_two', 'uid': 'A2', 'tests': {'only': ['host/cdc_msc', 'device/hid']},
'flasher': {'name': 'openocd', 'uid': 'p2'}},
]}, ensure_ascii=False), encoding='utf-8')
(root / 'test' / 'hil' / 'hfp.json').write_text(json.dumps({'boards': []}))

Expand Down Expand Up @@ -121,7 +122,7 @@ def test_writes_the_three_files_and_lists_what_it_left_out(self):
])
self.assertTrue(hil.startswith(gen_doc.HEADER + '\n\n### ci rig\n\n2 boards, from `test/hil/tinyusb.json`.'))
self.assertIn('| acme_one | device, host | jlink | acme_one_hs | needs a \\| jumper \u00f8 |', hil)
self.assertIn('| acme_two | device, host | | | |', hil)
self.assertIn('| acme_two | device, host | openocd | | |', hil)
self.assertNotIn('hfp rig', hil)
self.assertIn('lib/fatfs', deps)

Expand Down
11 changes: 7 additions & 4 deletions .claude/test/test_hil_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,16 @@
# the loader, the pin, hil_load, the core it loads and the notice the core loads; and, though
# the rig never runs it, the transport core hil_remote.py loads through that checkout's loader
REAL = ('test/hil/helper/hil_args.py', 'test/hil/helper/hil_report.py', 'test/hil/helper/agentrc.py',
'tools/get_deps.py', 'lib/agentrc/.git/HEAD', 'lib/agentrc/skills/hil/scripts/hil_load.py',
'lib/agentrc/skills/hil/scripts/hil_report_core.py', 'lib/agentrc/skills/hil/scripts/hil_report_notice.py')
'test/hil/helper/hil_util.py', 'tools/get_deps.py', 'lib/agentrc/.git/HEAD',
'lib/agentrc/skills/hil/scripts/hil_load.py', 'lib/agentrc/skills/hil/scripts/hil_report_core.py',
'lib/agentrc/skills/hil/scripts/hil_report_notice.py', 'lib/agentrc/skills/hil/scripts/hil_boards.py',
'lib/agentrc/skills/hil/boards.json', 'lib/agentrc/skills/hil/scripts/usb_observe.py')
REMOTE_CORE = 'lib/agentrc/skills/hil/scripts/hil_remote_core.py'

CONFIG = {'boards': [
{'name': 'alpha', 'flasher': {'name': 'jlink'}},
{'name': 'beta', 'flasher': {'name': 'openocd'}, 'variant': [{'name': 'beta_one'}, {'name': 'beta_two'}]},
{'name': 'alpha', 'uid': 'A', 'flasher': {'name': 'jlink', 'uid': 'pa'}},
{'name': 'beta', 'uid': 'B', 'flasher': {'name': 'openocd', 'uid': 'pb'},
'variant': [{'name': 'beta_one'}, {'name': 'beta_two'}]},
]}


Expand Down
25 changes: 15 additions & 10 deletions .claude/test/test_usbtest_run_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,22 @@
'boards': [
{'name': 'solo', 'uid': 'UID1', 'flasher': {'name': 'jlink', 'uid': 'P1', 'args': '-device X'},
'tests': {'device': True, 'host': False}},
{'name': 'duo', 'uid': 'UID2', 'flasher': {'name': 'openocd', 'uid': 'P2', 'args': ''},
{'name': 'duo', 'uid': 'UID2', 'flasher': {'name': 'openocd', 'uid': 'P2'},
'variant': [{'name': 'duo-a', 'flags': ''}, {'name': 'duo-b', 'flags': ''}],
'tests': {'only': ['device/board_test', 'device/usbtest']}},
{'name': 'hostonly', 'uid': 'UID4', 'flasher': {'name': 'openocd'}, 'tests': {'device': False, 'host': True}},
{'name': 'skips', 'uid': 'UID5', 'flasher': {'name': 'jlink'},
{'name': 'hostonly', 'uid': 'UID4', 'flasher': {'name': 'openocd', 'uid': 'P1x'}, 'tests': {'device': False, 'host': True}},
{'name': 'skips', 'uid': 'UID5', 'flasher': {'name': 'jlink', 'uid': 'P2x'},
'tests': {'device': True, 'skip': ['device/usbtest']}},
{'name': 'notlisted', 'uid': 'UID6', 'flasher': {'name': 'jlink'}, 'tests': {'only': ['device/cdc_msc']}},
{'name': 'untested', 'uid': 'UID7', 'flasher': {'name': 'jlink'}},
{'name': 'malformed', 'uid': 'UID8', 'flasher': {'name': 'jlink'}, 'tests': {'device': True},
{'name': 'notlisted', 'uid': 'UID6', 'flasher': {'name': 'jlink', 'uid': 'P3x'}, 'tests': {'only': ['device/cdc_msc']}},
{'name': 'untested', 'uid': 'UID7', 'flasher': {'name': 'jlink', 'uid': 'P4x'}},
{'name': 'malformed', 'uid': 'UID8', 'flasher': {'name': 'jlink', 'uid': 'P5x'}, 'tests': {'device': True},
'variant': [{'name': ''}]},
{'name': 'noflasher', 'uid': 'UID9', 'tests': {'only': ['device/usbtest']}},
{'name': 'nouid', 'flasher': {'name': 'jlink'}, 'tests': {'only': ['device/usbtest']}},
{'name': 'nouid', 'flasher': {'name': 'jlink', 'uid': 'P6x'}, 'tests': {'only': ['device/usbtest']}},
{'name': 'esp', 'uid': 'UID10', 'tests': {'only': ['device/usbtest']},
'flasher': {'name': 'openocd_esp', 'uid': '30:ED:A0:E0:C9:02', 'args': '-f board/esp32p4-builtin.cfg'}},
],
'boards-skip': [{'name': 'parked', 'uid': 'UID3', 'flasher': {'name': 'jlink'}}],
'boards-skip': [{'name': 'parked', 'uid': 'UID3', 'flasher': {'name': 'jlink', 'uid': 'P7x'}}],
}
PASS_JSON = {'passed': 1, 'failed': 0, 'notrun': 0, 'wedged': False,
'cases': [{'num': 29, 'name': 'toggle clear', 'status': 'PASS'}]}
Expand Down Expand Up @@ -153,8 +153,8 @@ def test_board_and_variant_resolution(self):
(['--board', 'notlisted'], 'notlisted does not run device/usbtest'),
(['--board', 'untested'], 'untested does not run device/usbtest'),
(['--board', 'malformed'], 'malformed: board malformed variant'),
(['--board', 'noflasher'], 'noflasher in'),
(['--board', 'nouid'], 'nouid in'),
(['--board', 'noflasher'], "noflasher: board UID9 is not in agentrc's board registry"),
(['--board', 'nouid'], 'nouid: no uid'),
(['--board', 'esp'], 'esp: openocd_esp is refused to a caller that does not declare'),
(['--board', 'duo'], 'duo has variants duo-a, duo-b: pass --variant'),
(['--board', 'duo', '--variant', 'duo-c'], 'duo has no variant duo-c'),
Expand All @@ -163,6 +163,11 @@ def test_board_and_variant_resolution(self):
self.refused(rig, *argv, '--tests', '29', '--after', 'leave', says=says)
self.assertEqual(rig.calls, [])

def test_a_registered_board_takes_its_flashers_from_agentrc(self):
board, variant = run_case.resolve(REPO / 'test/hil/tinyusb.json', 'ek_tm4c123gxl', None)
self.assertEqual((variant, board['flasher']['name'], board['flasher_recover']['name']),
('ek_tm4c123gxl', 'lm4flash', 'openocd'))

def test_bad_case_lists(self):
for tests in ('29,29', '30', 'x', '29,'):
self.refused(Rig(self), '--board', 'solo', '--tests', tests, '--after', 'leave',
Expand Down
2 changes: 1 addition & 1 deletion .claude/test/test_usbtest_wedge_drill.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ def test_a_non_openocd_recovery_needs_a_halt_override(self):
def test_a_bad_halt_override(self):
self.refused('--board', 'st', '--halt-openocd', ' ', says='not convoy-safe')
self.refused('--board', 'st', '--halt-openocd', '-f interface/cmsis-dap.cfg', says='not convoy-safe')
self.refused('--board', 'st-nouid', '--halt-openocd', ST_HALT, says='no probe uid')
self.refused('--board', 'st-nouid', '--halt-openocd', ST_HALT, says='flasher: uid must be a non-empty string')

def test_an_override_on_an_openocd_recovery_board(self):
self.refused('--board', 'ok', '--halt-openocd', ST_HALT, says='halts through its own')
Expand Down
11 changes: 7 additions & 4 deletions .github/scripts/hil_ci_set_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'test', 'hil', 'helper'))
import hil_report # noqa: E402 stdlib-only; board_variants() reads a roster board's builds
import hil_util # noqa: E402 ESP_IDF_FLASHERS; agentrc loads only on first use
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'test', 'hil'))
from helper import hil_report # noqa: E402 stdlib-only; board_variants() reads a roster board's builds
from helper import hil_util # noqa: E402 ESP_IDF_FLASHERS; hil_boards() needs lib/agentrc (get_deps.py)


def _resolve_config_path(config_file):
Expand Down Expand Up @@ -90,7 +90,10 @@ def append_build_arg(toolchain, build_arg):
if selected is not None and board['name'] not in selected:
continue
name = board['name']
flasher = board['flasher']
try: # the board's flasher is agentrc's, by its uid
flasher = hil_util.hil_boards().resolve(board)['flasher']
except (ImportError, ValueError) as e:
raise SystemExit(f'{config_file}: {e}')
# esptool and openocd_esp boards must build under esp-idf; others default
# to arm-gcc but may opt into another bucket via an explicit "toolchain"
# field (e.g. RISC-V boards like ch32v20x need "riscv-gcc").
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ jobs:
with:
fetch-depth: 0 # the selector diffs against the PR base or the push's before

- name: Get agentrc
# the selector and the matrix read each roster board's flasher from agentrc's board
# registry, at the commit get_deps.py pins
run: python3 tools/get_deps.py lib/agentrc

- name: Selection gate
id: gate
env:
Expand Down Expand Up @@ -87,6 +92,9 @@ jobs:
with:
fetch-depth: 0

- name: Get agentrc
run: python3 tools/get_deps.py lib/agentrc
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

- name: CI selection (PR only)
id: hil-select
if: github.event_name == 'pull_request'
Expand Down Expand Up @@ -761,6 +769,9 @@ jobs:
# merge-base(HEAD, origin/<base_ref>) for PR-scoped selection
fetch-depth: 0

- name: Get agentrc
run: python3 tools/get_deps.py lib/agentrc

# Computed BEFORE the build: the IAR build is four boards and up to 30 minutes on
# a runner that hil-tinyusb (hfp.json) also needs, so an unaffected PR must release
# it immediately instead of building everything and then skipping. The selection
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/dependencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ lib/CMSIS_5 https://github.com/ARM-software/CMSIS_
lib/CMSIS_6 https://github.com/ARM-software/CMSIS_6.git 6f0a58d01aa9bd2feba212097f9afe7acd991d52 imxrt kinetis_k32l ra stm32n6 lpc51 lpc55 mcx stm32c5
lib/FreeRTOS-Kernel https://github.com/FreeRTOS/FreeRTOS-Kernel.git 9b777ae5c5b8e9e456065a00294d1e5f5f9facf5 all
lib/SystemView https://github.com/SEGGERMicro/SystemView.git 92ca7a810c5765ba64911919acd511c61b6b083f
lib/agentrc https://github.com/hathach/agentrc.git 12d14432d35231935e9764a72c1bd19dde02c78b
lib/agentrc https://github.com/hathach/agentrc.git 4aa58e7a9c96dc4677d891cb5811ba8f50d9dd51
lib/fatfs https://github.com/abbrev/fatfs.git 30ca13c62615df0d2e9104ab41256985b96590c1 all
lib/lwip https://github.com/lwip-tcpip/lwip.git 159e31b689577dbf69cf0683bbaffbd71fa5ee10 all
lib/sct_neopixel https://github.com/gsteiert/sct_neopixel.git e73e04ca63495672d955f9268e003cffe168fcd8 lpc55
Expand Down
2 changes: 1 addition & 1 deletion test/hil/helper/farm_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def roster(host: str, config, args) -> dict:
if not path.exists():
path = REPO_ROOT / 'test' / 'hil' / name
with path.open() as f:
cfg = json.load(f)
cfg = hil_util.hil_boards().resolve_roster(json.load(f)) # each probe is agentrc's, by board uid
# boards-skip is parked hardware: checked only when -b names it
return {'path': str(path), 'boards': list(cfg['boards']), 'named_only': list(cfg.get('boards-skip', []))}

Expand Down
14 changes: 14 additions & 0 deletions test/hil/helper/hil_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,20 @@ def provider():
return _provider


_hil_boards = None


def hil_boards():
"""agentrc's board registry (hil_boards): each roster board's flasher and flasher_recover by
its uid. Loaded on first use like provider(); every reader of a board's flasher resolves
its roster entry through it, readers of names alone read the roster raw."""
global _hil_boards
if _hil_boards is None:
from . import agentrc
_hil_boards = agentrc.load_file('skills/hil/scripts/hil_boards.py')
return _hil_boards


_openocd_esp = None


Expand Down
Loading
Loading