ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
docattach_selftest.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Synthetic fixtures proving each finding fires -- and that none over-fires.
4
5Both directions, deliberately. A doc-attachment gate that cries wolf gets
6switched off, and a switched-off gate is indistinguishable from a clean tree,
7so every defect class here has a must-fire fixture AND the tricky-but-legal
8form that must stay silent beside it.
9
10Split out of the checker (#359): the fixture table is 600-odd lines of C, and
11keeping it inline meant the rules it proves were unreachable by eye.
12"""
13
14from __future__ import annotations
15
16import os
17import sys
18import tempfile
19from pathlib import Path
20from types import ModuleType
21
22from docattach_ast import (
23 _check_declarations,
24 _require_libclang,
25 check_forward_decl_blocks,
26)
27from docattach_lex import check_banned_boilerplate, check_consecutive_blocks
28from docattach_model import Finding
29
30# ---------------------------------------------------------------------------
31# Selftest
32# ---------------------------------------------------------------------------
33#: (name, source, expected finding codes). Every defect class must be proven
34#: to FIRE, and every tricky-but-legal form proven NOT to. A gate that cries
35#: wolf gets disabled, which is worse than no gate.
36SELFTEST_CASES: list[tuple[str, str, set[str]]] = [
37 # ---------------- must FIRE ----------------
38 (
39 "param name pasted from another function",
40 """
41/**
42 * @brief Add two numbers.
43 * @param[in] reader Open reader.
44 * @param[in] tile Tile index.
45 * @return Sum.
46 */
47int add_values(int lhs, int rhs) { return lhs + rhs; }
48""",
49 {"DOC001", "DOC002"},
50 ),
51 (
52 "defect behind an RA8_* annotation macro is still seen",
53 """
54#define RA8_INTERNAL
55/**
56 * @brief Emit one char-decl pair.
57 * @param[in] conn_handle Connection handle.
58 * @param[in] pdu Raw PDU.
59 * @return Nothing.
60 */
61RA8_INTERNAL
62static void internal_emit_pair(unsigned char* resp, int pos) { (void)resp; (void)pos; }
63""",
64 {"DOC001", "DOC002"},
65 ),
66 (
67 "correct block behind an RA8_* annotation macro does not fire",
68 """
69#define RA8_INTERNAL
70/**
71 * @brief Emit one char-decl pair.
72 * @param[out] resp Response buffer.
73 * @param[in] pos Cursor into resp.
74 * @return Nothing.
75 */
76RA8_INTERNAL
77static void internal_emit_pair(unsigned char* resp, int pos) { (void)resp; (void)pos; }
78""",
79 set(),
80 ),
81 (
82 "param documented that does not exist",
83 """
84/**
85 * @brief Add two numbers.
86 * @param[in] lhs Left.
87 * @param[in] rhs Right.
88 * @param[in] carry Nonexistent.
89 * @return Sum.
90 */
91int add_values(int lhs, int rhs) { return lhs + rhs; }
92""",
93 {"DOC001"},
94 ),
95 (
96 "partially documented signature (1 of 3)",
97 """
98/**
99 * @brief Download one chapter.
100 * @param[in] url Source URL.
101 * @return Status.
102 */
103int download_chapter(const char* url, const char* dest, int retries) { return 0; }
104""",
105 {"DOC002"},
106 ),
107 (
108 "retval on a void function",
109 """
110/**
111 * @brief Reset the widget.
112 * @param[in] id Widget id.
113 * @retval 0 Success.
114 */
115void widget_reset(int id) { (void)id; }
116""",
117 {"DOC003"},
118 ),
119 (
120 "@return promising a value on a void function",
121 """
122/**
123 * @brief Reset the widget.
124 * @param[in] id Widget id.
125 * @return The number of registers cleared.
126 */
127void widget_reset(int id) { (void)id; }
128""",
129 {"DOC003"},
130 ),
131 (
132 "two doc blocks in a row (the ra8_viewer main/log_sink shape)",
133 """
134/**
135 * @brief Program entry point.
136 * @return 0 on success.
137 */
138
139/**
140 * @brief Log sink.
141 * @param[in] byte Byte to emit.
142 */
143static void log_sink(unsigned char byte) { (void)byte; }
144""",
145 {"DOC004"},
146 ),
147 (
148 "identical block pasted twice",
149 """
150/**
151 * @brief Compute the checksum.
152 * @param[in] len Length.
153 * @return Checksum.
154 */
155/**
156 * @brief Compute the checksum.
157 * @param[in] len Length.
158 * @return Checksum.
159 */
160int checksum(int len) { return len; }
161""",
162 {"DOC004"},
163 ),
164 (
165 "@fn naming a different function",
166 """
167/**
168 * @fn viewer_open_comic
169 * @brief Open an RTA1 atlas.
170 * @param[in] r Reader.
171 * @return Status.
172 */
173int viewer_open_rta1(int r) { return r; }
174""",
175 {"DOC005"},
176 ),
177 (
178 "@struct naming a different struct",
179 """
180/**
181 * @struct lcd_config_t
182 * @brief Panel timing.
183 */
184struct panel_timing_t { int hsync; };
185""",
186 {"DOC005"},
187 ),
188 (
189 "@enum naming a different enum",
190 """
191/**
192 * @enum lcd_state_t
193 * @brief Reader states.
194 */
195enum reader_state_t { k_idle = 0 };
196""",
197 {"DOC005"},
198 ),
199 (
200 "definition-site 'Implementation of `X()`' naming a different function",
201 """
202/** @brief Implementation of `ra8_err_to_str()` -- linear-scan lookup. */
203int ra8_err_to_code(int c) { return c; }
204""",
205 {"DOC005"},
206 ),
207 (
208 "block on a forward declaration separated from its definition by other code",
209 """
210/**
211 * @brief Probe and cache every tile's native size.
212 * @param[in] r Reader.
213 * @return Status.
214 */
215static int compute_tiles(int r);
216
217/**
218 * @brief Unrelated helper standing between the declaration and the body.
219 * @param[in] v Value.
220 * @return Value.
221 */
222static int passthrough(int v) { return v; }
223
224static int compute_tiles(int r) { return r; }
225""",
226 {"DOC006"},
227 ),
228 (
229 "-Wmissing-prototypes idiom: local prototype directly above its definition",
230 """
231/**
232 * @brief Non-maskable interrupt handler.
233 * @return Nothing.
234 */
235void NMI_Handler(void);
236void NMI_Handler(void) { }
237""",
238 set(),
239 ),
240 (
241 "banned pointer-only boilerplate",
242 """
243/** @brief Implementation of ra8_foo (see header for full contract). */
244int ra8_foo(void) { return 0; }
245""",
246 {"DOC007"},
247 ),
248 (
249 "two adjacent blocks are still caught when the gap is only whitespace",
250 """
251/**
252 * Enable a build option that nothing below actually declares.
253 */
254
255/**
256 * @brief Widget identifier width.
257 */
258#define WIDGET_ID_BITS 8
259""",
260 {"DOC004"},
261 ),
262 (
263 "a real comment between two blocks does not license a duplicate",
264 """
265/**
266 * Enable a build option that nothing below actually declares.
267 */
268/* an ordinary comment, not a commented-out directive */
269
270/**
271 * @brief Widget identifier width.
272 */
273#define WIDGET_ID_BITS 8
274""",
275 {"DOC004"},
276 ),
277 # ---------------- must NOT fire ----------------
278 (
279 "untagged block documenting a commented-out config option",
280 """
281/**
282 * Enable the verified implementations of ECDH primitives from Project Everest.
283 *
284 * The Everest code is Apache-2.0 only, so enabling this is incompatible with
285 * taking the library under GPL-2.0-or-later.
286 */
287//#define MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED
288
289/**
290 * \\def MBEDTLS_GCM_LARGE_TABLE
291 *
292 * Use a larger GCM table to speed up AES-GCM.
293 */
294//#define MBEDTLS_GCM_LARGE_TABLE
295""",
296 set(),
297 ),
298 (
299 "commented-out #undef also counts as the documented subject",
300 """
301/**
302 * Disable the built-in entropy sources.
303 */
304// #undef MBEDTLS_ENTROPY_C
305
306/**
307 * @brief Widget identifier width.
308 */
309#define WIDGET_ID_BITS 8
310""",
311 set(),
312 ),
313 (
314 "correct function block",
315 """
316/**
317 * @brief Add two numbers.
318 * @param[in] lhs Left operand.
319 * @param[in] rhs Right operand.
320 * @return The sum.
321 * @retval 0 Both operands were zero.
322 */
323int add_values(int lhs, int rhs) { return lhs + rhs; }
324""",
325 set(),
326 ),
327 (
328 "correct void function (no @return/@retval)",
329 """
330/**
331 * @brief Reset the widget.
332 * @param[in] id Widget id.
333 */
334void widget_reset(int id) { (void)id; }
335""",
336 set(),
337 ),
338 (
339 "@file block directly above a symbol block",
340 """
341/**
342 * @file demo.c
343 * @brief Demo translation unit.
344 */
345/**
346 * @brief Add two numbers.
347 * @param[in] lhs Left.
348 * @param[in] rhs Right.
349 * @return Sum.
350 */
351int add_values(int lhs, int rhs) { return lhs + rhs; }
352""",
353 set(),
354 ),
355 (
356 "@defgroup and @{ grouping markers between blocks",
357 """
358/**
359 * @defgroup lcd LCD driver
360 * @{
361 */
362/**
363 * @brief Clear the framebuffer.
364 * @return Status.
365 */
366int lcd_clear(void);
367/** @} */
368""",
369 set(),
370 ),
371 (
372 "@copydoc block with another symbol's parameter names",
373 """
374/** @copydoc ra8_gpio_output_init */
375int ra8_gpio_output_init_impl(int port, int pin) { return port + pin; }
376""",
377 set(),
378 ),
379 (
380 "sanctioned definition-site single-line form, correct name",
381 """
382/** @brief Implementation of `ra8_err_to_str()` -- linear-scan lookup. */
383int ra8_err_to_str(int code) { return code; }
384""",
385 set(),
386 ),
387 (
388 "undocumented parameters with no @param at all (doxy_audit's job, not ours)",
389 """
390/**
391 * @brief Add two numbers.
392 * @return Sum.
393 */
394int add_values(int lhs, int rhs) { return lhs + rhs; }
395""",
396 set(),
397 ),
398 (
399 "@param inside a @code example naming other symbols",
400 """
401/**
402 * @brief Register a handler.
403 * @param[in] handler Callback.
404 * @return Status.
405 * @code
406 * // @param[in] port Port identifier
407 * ra8_isr_register(handler);
408 * @endcode
409 */
410int ra8_isr_register(int handler) { return handler; }
411""",
412 set(),
413 ),
414 (
415 "variadic function documenting only its named parameters",
416 """
417/**
418 * @brief Formatted log.
419 * @param[in] fmt Format string.
420 * @return Bytes written.
421 */
422int ra8_logf(const char* fmt, ...) { (void)fmt; return 0; }
423""",
424 set(),
425 ),
426 (
427 "forward declaration bare, definition documented (the correct shape)",
428 """
429static int compute_tiles(int r);
430
431/**
432 * @brief Probe and cache every tile's native size.
433 * @param[in] r Reader.
434 * @return Status.
435 */
436static int compute_tiles(int r) { return r; }
437""",
438 set(),
439 ),
440 (
441 "header declaration documented, definition bare (CLAUDE.md's prescribed split)",
442 """
443/**
444 * @brief Add two numbers.
445 * @param[in] lhs Left.
446 * @param[in] rhs Right.
447 * @return Sum.
448 */
449int add_values(int lhs, int rhs);
450""",
451 set(),
452 ),
453 (
454 "namesake statics in different files must not merge (keyed per file)",
455 """
456/**
457 * @brief Zero a buffer.
458 * @param[in] len Length.
459 */
460static void internal_zero_bytes(int len) { (void)len; }
461""",
462 set(),
463 ),
464 (
465 "pointer-back note WITH a real implementation note is allowed",
466 """
467/** @brief Implementation of `ra8_foo()` -- O(1) table lookup, see HUM Ch 5.2. */
468int ra8_foo(void) { return 0; }
469""",
470 set(),
471 ),
472 (
473 "\\def block documenting a deliberately commented-out config option",
474 """
475/**
476 * \\def MBEDTLS_AES_ROM_TABLES
477 *
478 * Use precomputed AES tables stored in ROM.
479 */
480//#define MBEDTLS_AES_ROM_TABLES
481
482/**
483 * \\def MBEDTLS_AES_FEWER_TABLES
484 *
485 * Use less ROM/RAM for AES tables.
486 */
487//#define MBEDTLS_AES_FEWER_TABLES
488""",
489 set(),
490 ),
491 (
492 "@var block stranded above another symbol's block, real variable left bare",
493 """
494/**
495 * @var g_release_err
496 * @brief Captured release code.
497 */
498/** @brief Sentinel for the release code. */
499typedef enum : unsigned {
500 k_err_none = 0U, /**< None yet. */
501} err_sentinel_t;
502
503volatile unsigned g_release_err = 0U;
504""",
505 {"DOC004"},
506 ),
507 (
508 "'@return This function never returns.' on a [[noreturn]] void handler",
509 """
510/**
511 * @brief Park the core forever.
512 * @return This function never returns.
513 */
514[[noreturn]] void park_forever(void) { for (;;) { } }
515""",
516 set(),
517 ),
518 (
519 "@retval on a [[noreturn]] void handler is still a contradiction",
520 """
521/**
522 * @brief Park the core forever.
523 * @return This function never returns.
524 * @retval (none) The core spins in place.
525 */
526[[noreturn]] void park_forever(void) { for (;;) { } }
527""",
528 {"DOC003"},
529 ),
530 (
531 "house-style '@return Nothing.' on a void function is not a contradiction",
532 """
533/**
534 * @brief Reset the widget.
535 * @param[in] id Widget id.
536 * @return Nothing.
537 */
538void widget_reset(int id) { (void)id; }
539""",
540 set(),
541 ),
542 (
543 "typedef'd anonymous struct named by its @struct tag (the C23 house shape)",
544 """
545/**
546 * @struct emu_args_t
547 * @brief Parsed command line.
548 */
549typedef struct {
550 int verbose; /**< Verbosity level. */
551} emu_args_t;
552""",
553 set(),
554 ),
555 (
556 "typedef'd anonymous enum named by its @enum tag",
557 """
558/**
559 * @enum lcd_state_t
560 * @brief Panel states.
561 */
562typedef enum : unsigned char {
563 k_lcd_state_idle = 0, /**< Idle. */
564} lcd_state_t;
565""",
566 set(),
567 ),
568 (
569 "struct with correctly-named @struct tag and documented members",
570 """
571/**
572 * @struct panel_timing_t
573 * @brief Panel timing.
574 */
575struct panel_timing_t {
576 int hsync; /**< Horizontal sync width. */
577 int vsync; /**< Vertical sync width. */
578};
579""",
580 set(),
581 ),
582]
583
584
585def _findings_for(path: Path, cindex: ModuleType, args: list[str]) -> list[Finding]:
586 """Every finding for one fixture, through the same code the gate runs.
587
588 Deliberately the production helpers rather than a walk of its own. This
589 used to re-implement ``check_file``'s cursor loop inline, which meant the
590 suite could keep passing while the code the gate actually runs drifted
591 away from it -- proving the fixtures against a second implementation
592 nobody ships.
593 """
594 rel = str(path)
595 text = path.read_text(encoding="ascii")
596 own = os.path.realpath(str(path))
597 tu = cindex.Index.create().parse(str(path), args=args)
598 return [
599 *check_consecutive_blocks(rel, text),
600 *check_banned_boilerplate(rel, text),
601 *_check_declarations(tu, cindex, rel, own, text),
602 *check_forward_decl_blocks(tu, cindex, rel, own, text),
603 ]
604
605
606def selftest() -> int:
607 """Run the synthetic fixtures in both directions."""
608 cindex = _require_libclang()
609 args = ["-std=c23", "-x", "c", "-DRA8_HOST_BUILD=1"]
610 failures = 0
611 with tempfile.TemporaryDirectory() as td:
612 for idx, (name, src, expected) in enumerate(SELFTEST_CASES):
613 path = Path(td) / f"case_{idx:02d}.c"
614 path.write_text(src, encoding="ascii")
615 got = _findings_for(path, cindex, args)
616
617 codes = {f.code for f in got}
618 if codes != expected:
619 failures += 1
620 sys.stderr.write(
621 f" FAIL [{idx:02d}] {name}\n"
622 f" expected {sorted(expected) or '<clean>'}\n"
623 f" got {sorted(codes) or '<clean>'}\n"
624 )
625 for f in got:
626 sys.stderr.write(f" {f.code} {f.symbol}: {f.detail}\n")
627
628 if failures:
629 sys.stderr.write(
630 f"check_doc_attachment.py: selftest FAILED ({failures}/{len(SELFTEST_CASES)} cases).\n"
631 )
632 return 2
633 fires = sum(1 for _, _, e in SELFTEST_CASES if e)
634 clean = len(SELFTEST_CASES) - fires
635 print(
636 f"check_doc_attachment.py: selftest passed "
637 f"({len(SELFTEST_CASES)} cases: {fires} must-fire, {clean} must-not-fire)."
638 )
639 return 0