mirror of
https://github.com/hathach/tinyusb.git
synced 2026-08-20 12:07:07 +00:00
Confirmed by a 10-finder / 28-verifier adversarial review pass: board_lock.py — the flock is now the sole authority: drop cmd_hold's pid-liveness pre-gate (a live hil_test.py pool worker's stale record no longer blocks a genuinely free board); cmd_release probes the flock and only signals a verified holder, refuses to kill hil_test.py holders (CI mid-test), handles PermissionError; the holder daemon truncates its lock records on SIGTERM and keeps the success pipe clear of fds 0-2 (closed-stdio hold used to leave an orphan holder while reporting failure); --config default resolves beside the script. hil_test.py — truncate the lock record on per-board release (pool workers outlive their flocks); warn instead of silently failing open when the lock dir is unusable; error out on -b names absent from the config (was a silent zero-test exit 0, readable as a green HIL run); drop an emptied board row in accumulate_report (variant boards left a blank ghost row). workflows — remove the stray positional arg that made the validate size stage exit 2 on every run; wrap JSON.parse(args) in all six scripts; factor pr-babysit's drifted reply recipe into postReplyRecipe and dedup refutation replies across cycles; validate args.pr and maxCycles; driver-review rejects an empty dimensions list; hil-validate drops a dead guard clause and retries diagnostics with -v -r 1. agents/docs — port-dev scopes git clang-format to its own files (concurrent workers reformatted each other in shared checkouts); hil-operator/hil skill wording matches actual fail-fast output; the implementation plan is now a DO-NOT-EXECUTE historical record (banner + checked boxes) so plan-executing agents cannot revert shipped files. Verified: lock storm 1-winner-in-10, stale-record hold, closed-stdio hold, dead-pid cleanup, CI-holder refusal, ghost-row 4-scenario merge, unknown-board exit 1, py_compile + check.sh on all six workflows, pre-commit clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Upj4hta5TNoAbidqeC1zZ6
101 lines
4.5 KiB
JavaScript
101 lines
4.5 KiB
JavaScript
export const meta = {
|
|
name: 'validate',
|
|
description: 'Pre-PR software validation: unit tests + per-board build sweeps + code-size compare + PVS, in parallel, joined into one verdict',
|
|
whenToUse: 'Before opening or updating a PR, after any non-trivial change',
|
|
phases: [{ title: 'Validate', detail: 'unit + builds + size + pvs in parallel' }],
|
|
}
|
|
|
|
// args: { boards: string[], examples?: string, base?: string, skip?: ('unit'|'size'|'pvs')[] }
|
|
if (typeof args === 'string') { try { args = JSON.parse(args) } catch { /* not JSON: shape check below reports it */ } }
|
|
if (!args || !Array.isArray(args.boards) || args.boards.length === 0) {
|
|
throw new Error('args must be { boards: string[], examples?, base?, skip? }')
|
|
}
|
|
const skip = args.skip || []
|
|
for (const s of skip) log(`stage skipped by request: ${s}`)
|
|
const base = args.base || 'master'
|
|
const clip = (s, n = 800) =>
|
|
s.length > n ? s.slice(0, n) + ` …[truncated ${s.length - n} chars]` : s
|
|
|
|
const STAGE = {
|
|
type: 'object', additionalProperties: false,
|
|
required: ['pass', 'detail'],
|
|
properties: { pass: { type: 'boolean' }, detail: { type: 'string' } },
|
|
}
|
|
const BUILD = {
|
|
type: 'object', additionalProperties: false,
|
|
required: ['board', 'pass', 'builtCount', 'failures'],
|
|
properties: {
|
|
board: { type: 'string' }, pass: { type: 'boolean' }, builtCount: { type: 'integer' },
|
|
failures: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object', additionalProperties: false,
|
|
required: ['example', 'class', 'firstError'],
|
|
properties: { example: { type: 'string' }, class: { type: 'string' }, firstError: { type: 'string' } },
|
|
},
|
|
},
|
|
},
|
|
}
|
|
const PVS = {
|
|
type: 'object', additionalProperties: false,
|
|
required: ['pass', 'ga1', 'ga2', 'changedFindings', 'detail'],
|
|
properties: {
|
|
pass: { type: 'boolean' }, ga1: { type: 'integer' }, ga2: { type: 'integer' },
|
|
changedFindings: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object', additionalProperties: false,
|
|
required: ['file', 'line', 'rule', 'level', 'message'],
|
|
properties: {
|
|
file: { type: 'string' }, line: { type: 'integer' }, rule: { type: 'string' },
|
|
level: { type: 'integer' }, message: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
detail: { type: 'string' },
|
|
},
|
|
}
|
|
|
|
const thunks = []
|
|
|
|
if (!skip.includes('unit')) thunks.push(() =>
|
|
agent(
|
|
'Run the TinyUSB unit tests: cd test/unit-test && ceedling test:all. ' +
|
|
'pass=true only if every test passes. detail = the ceedling summary line, or the first failing test output.',
|
|
{ label: 'unit', phase: 'Validate', model: 'haiku', schema: STAGE },
|
|
).then(r => r && { stage: 'unit', ...r }))
|
|
|
|
for (const b of args.boards) thunks.push(() =>
|
|
agent(
|
|
`Build TinyUSB examples for board ${b}` + (args.examples ? ` (only: ${args.examples})` : ' (full example set)') + '.',
|
|
{ label: `build:${b}`, phase: 'Validate', agentType: 'builder', schema: BUILD },
|
|
).then(r => r && {
|
|
stage: `build:${b}`, pass: r.pass,
|
|
detail: r.pass ? `${r.builtCount} examples built` : clip(JSON.stringify(r.failures)),
|
|
}))
|
|
|
|
if (!skip.includes('size')) thunks.push(() =>
|
|
agent(
|
|
`Compare TinyUSB code size against ${base}: python3 tools/metrics_compare_base.py --base-branch ${base} -b ${args.boards[0]} -e device/cdc_msc (exactly this command — no extra positional args). ` +
|
|
'The report lands in cmake-metrics/<board>/metrics_compare.md. pass=false only if the tool itself errors; ' +
|
|
'detail = the flash/RAM delta summary from the report (mention any example that grew).',
|
|
{ label: 'size', phase: 'Validate', model: 'haiku', schema: STAGE },
|
|
).then(r => r && { stage: 'size', ...r }))
|
|
|
|
if (!skip.includes('pvs')) thunks.push(() =>
|
|
agent(
|
|
`Run PVS-Studio static analysis for board ${args.boards[0]}, gating on files changed vs ${base}. ` +
|
|
'Parallel build agents are running — use your dedicated build dir, never cmake-build-<board>.',
|
|
{ label: 'pvs', phase: 'Validate', agentType: 'static-analyzer', effort: 'low', schema: PVS },
|
|
).then(r => r && {
|
|
stage: 'pvs', pass: r.pass,
|
|
detail: r.pass ? r.detail : clip(`${r.detail} ${JSON.stringify(r.changedFindings)}`),
|
|
}))
|
|
|
|
const results = (await parallel(thunks)).filter(Boolean)
|
|
const dead = thunks.length - results.length
|
|
if (dead > 0) log(`${dead} stage agent(s) died — counted as failures`)
|
|
const failures = results.filter(r => !r.pass)
|
|
log(`${results.length}/${thunks.length} stages completed, ${failures.length} failing`)
|
|
return { pass: failures.length === 0 && dead === 0, stages: results, failures }
|