1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
|
#!/usr/bin/env python3
"""Driver coverage for membrowse's CI boards and the HIL board pool.
Every dcd_*/hcd_* driver under src/portable (plus ehci/ohci, minus template/)
is checked against two rosters: `.github/ci-pinned-boards.json` (which
board's `drivers` list, or `uncovered`, covers it) and the HIL rig rosters
(test/hil/tinyusb.json, test/hil/hfp.json - which board family, if any, on
the physical rig builds it).
Documented membrowse gaps and drivers with no rig board are informational.
An undocumented membrowse gap is fatal, as are validity errors: malformed
json, an unknown driver/board name, a board/driver mismatch, a board whose
family no CI toolchain actually builds (ci_set_matrix.family_list), a driver claimed by both
`boards` and `uncovered`, or an hcd_*/ehci/ohci claim on a board that
builds no host/ or dual/ example (host examples are only.txt opt-in) - are
bugs in the file, not gaps, and stay fatal: one line per error to stderr,
exit 1. Exit 0 otherwise, with any INFO/WARNING lines on stdout.
The HIL family mapping reuses tools/ci_select.py's rule-3/4 machinery
(port_families()/port_option_gates()/board_roles(), the same data and role
filter that pick which rig boards a src/portable/ diff selects) rather than
a second heuristic.
"""
import json
import os
import sys
REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, os.path.join(REPO, 'tools'))
import build # noqa: E402
import build_utils # noqa: E402
import ci_select # noqa: E402
sys.path.insert(0, os.path.join(REPO, '.github', 'scripts'))
import ci_set_matrix # noqa: E402
def _is_host_driver(d):
return d.startswith('hcd_') or d in ('ehci', 'ohci')
def _builds_host_or_dual(board, family):
"""True if `board` (of `family`) builds at least one host/ or dual/ example -
the ground truth build.py's cmake_board() gets from the skip.txt/only.txt
opt-in for a real build.
Both build.get_examples() and build_utils.skip_example() are cwd-relative, so
the chdir must scope BOTH calls, not just the first: restoring cwd before
skip_example() runs made it answer against the CALLER's cwd (e.g. running
this checker from outside the repo produced bogus FATAL "builds no host/ or
dual/ example" errors for every host/dual driver claim)."""
with ci_select._in_repo(REPO):
examples = build.get_examples(family)
return any(e.startswith(('host/', 'dual/'))
and not build_utils.skip_example(e, board)
for e in examples)
def list_driver_paths(portable_dir):
"""{stem: source path} for every dcd_*/hcd_* driver plus ehci/ohci, template
excluded. A stem collision (two files with the same driver name) silently
keeps whichever os.walk() visits last - the real tree has none today."""
paths = {}
for root, _dirs, files in os.walk(portable_dir):
if os.path.basename(root) == 'template':
continue
for f in files:
stem, ext = os.path.splitext(f)
if ext != '.c':
continue
if stem.startswith(('dcd_', 'hcd_')) or stem in ('ehci', 'ohci'):
paths[stem] = os.path.join(root, f)
return paths
def list_drivers(portable_dir):
"""Driver source basenames: dcd_*/hcd_* stems plus ehci/ohci, template excluded."""
return set(list_driver_paths(portable_dir))
def load_boards(path):
with open(path) as f:
return json.load(f)
def check(path):
"""Fatal roster validity and undocumented coverage errors, one line each."""
errors = []
data = load_boards(path)
boards = data.get('boards')
uncovered = data.get('uncovered', {})
if not isinstance(boards, list) or not boards:
return [f'{path}: "boards" must be a non-empty list']
if not isinstance(uncovered, dict):
return [f'{path}: "uncovered" must be an object of driver: reason']
driver_paths = list_driver_paths(os.path.join(REPO, 'src', 'portable'))
drivers = set(driver_paths)
# ci_set_matrix.family_list is the ground truth for which families any CI
# toolchain actually builds. espressif is a deliberate exception there (see its
# own comment): hil-build-esp builds it by board name, not through this file, so
# its absence from family_list doesn't mean it's unbuilt.
ci_families = set(ci_set_matrix.family_list) | {'espressif'}
gates_by_port = ci_select.port_option_gates(REPO)
covered = set()
for i, t in enumerate(boards):
where = f'boards[{i}]'
if not isinstance(t, dict):
# a bare string/number/list here would otherwise TypeError on t.get()
# below - one clear error beats a traceback or bogus "missing" errors
errors.append(f'{where}: must be an object, not {type(t).__name__}')
continue
for key in ('board', 'drivers'):
if key not in t:
errors.append(f'{where}: missing "{key}"')
board = t.get('board', '')
family = ci_select.board_family(board, REPO) if board else None
board_ok = family is not None
options = ci_select.board_options({'name': board}, REPO) if board_ok else set()
if board and not board_ok:
errors.append(f'{where}: unknown board "{board}" (no hw/bsp/*/boards/{board})')
if board_ok and family not in ci_families:
errors.append(
f'{where} ({board}): family "{family}" is pinned but built by no CI '
f'toolchain (not in ci_set_matrix.family_list) - no leg ever builds it, '
f'so its "drivers" coverage claim is false')
driver_list = t.get('drivers', [])
if not isinstance(driver_list, list):
# a string iterates character-by-character below (each "character
# matches no driver source file"); null/a number/an object outright
# TypeErrors on `for d in driver_list` - one clear error beats either
errors.append(f'{where}: "drivers" must be a list of driver names')
driver_list = []
for d in driver_list:
if d not in drivers:
errors.append(f'{where} ({board}): "{d}" matches no driver source file')
elif board_ok:
port = _driver_port(driver_paths[d], REPO)
families = ci_select.port_families(port, REPO) if port else set()
gates = gates_by_port.get(port, set()) if port else set()
if family not in families and not gates & options:
errors.append(
f'{where} ({board}): claims "{d}" but does not build its '
f'"{port}" port')
covered.add(d)
host_drivers = [d for d in driver_list if _is_host_driver(d)]
if host_drivers and board_ok and not _builds_host_or_dual(board, family):
errors.append(
f'{where} ({board}): claims {host_drivers} but builds no host/ or '
f'dual/ example (host examples are only.txt opt-in)')
for d, reason in uncovered.items():
if d not in drivers:
errors.append(f'uncovered: "{d}" matches no driver source file')
if d in covered:
errors.append(f'"{d}" is both covered and uncovered')
if not (isinstance(reason, str) and reason.strip()):
errors.append(f'uncovered "{d}": reason must be a non-empty string')
for d in sorted(drivers - covered - set(uncovered)):
errors.append(f'membrowse: {d} has no CI board and no uncovered entry')
return errors
def membrowse_gaps(path):
"""(level, message) for every driver `path` (ci-pinned-boards.json) does not
cover: INFO when the gap is documented in "uncovered", WARNING when it is
silently missing."""
data = load_boards(path)
drivers = list_drivers(os.path.join(REPO, 'src', 'portable'))
covered = {d for t in data.get('boards', []) for d in t.get('drivers', [])}
uncovered = data.get('uncovered', {})
out = []
for d in sorted(drivers - covered):
if d in uncovered:
out.append(('INFO', f'membrowse: {d} uncovered - {uncovered[d]}'))
else:
out.append(('WARNING',
f'membrowse: {d} has no CI board (and no uncovered entry)'))
return out
def _driver_port(driver_path, repo_root):
"""Port dir per ci_select's rule-3/4 extraction, e.g. 'synopsys/dwc2' from
'src/portable/synopsys/dwc2/dcd_dwc2.c', or None if it doesn't match (it
always should - DriverScan pins the tree shape ci_select._PORT_PATH_RE
expects)."""
rel = os.path.relpath(driver_path, repo_root).replace(os.sep, '/')
m = ci_select._PORT_PATH_RE.match(rel)
return m.group(1) if m else None
def _driver_role(d):
"""'host' for hcd_*/ehci/ohci, else 'device' - _is_host_driver's naming
convention (not ci_select._port_roles: a bare 'ehci.c'/'ohci.c' matches
neither of _port_roles' dcd_/hcd_ patterns, so it falls through to "both"
there, which would defeat the host-only filter these two need)."""
return 'host' if _is_host_driver(d) else 'device'
def hil_gaps(repo_root=REPO):
"""('INFO', message) for every driver with no board on the HIL rig.
A driver is covered when a roster board (a) has the matching role for the
driver - device for dcd_*, host for hcd_*/ehci/ohci, checked against
ci_select.board_roles(), exactly the `board_roles(b) & roles` filter
rule 3/4 applies - and (b) either its family is in the driver's port's
family set (ci_select.port_families - the same family.cmake references
rule 3/4 reads for a src/portable/ diff) or it turns on a build option
that gates the port regardless of family (ci_select.port_option_gates/
board_options - e.g. analog/max3421's MAX3421_HOST)."""
roster_boards = build_utils.hil_roster_boards(repo_root)
gates_by_port = ci_select.port_option_gates(repo_root)
driver_paths = list_driver_paths(os.path.join(repo_root, 'src', 'portable'))
out = []
for d in sorted(driver_paths):
port = _driver_port(driver_paths[d], repo_root)
fams = ci_select.port_families(port, repo_root) if port else set()
gates = gates_by_port.get(port, set()) if port else set()
role = _driver_role(d)
covered = any(
(ci_select.board_family(b['name'], repo_root) in fams or
(gates and ci_select.board_options(b, repo_root) & gates)) and
role in ci_select.board_roles(b)
for b in roster_boards)
if not covered:
out.append(('INFO', f'hil: {d} has no board on the rig'))
return out
def main():
path = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
REPO, '.github', 'ci-pinned-boards.json')
errors = check(path)
for e in errors:
print(e, file=sys.stderr)
if errors:
return 1
for level, msg in membrowse_gaps(path) + hil_gaps():
print(f'{level}: {msg}')
return 0
if __name__ == '__main__':
sys.exit(main())
|