ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_tool_selftest.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Explicit both-direction assertions for external-tool suppression syntax."""
4
5from __future__ import annotations
6
7import json
8import re
9import shutil
10import subprocess
11from pathlib import Path
12
13from selftest_assert import expect
14from suppression_model import Inventory, Suppression
15
16EXPECTED_REGION_ROWS = 2
17EXPECTED_HADOLINT_ROWS = 4
18EXPECTED_MARKDOWN_CONFIG_ROWS = 2
19REPO_ROOT = Path(__file__).resolve().parents[2]
20SHFMT_TIMEOUT_SECONDS = 10
21
22TOOL_CONTROL_FIXTURES = {
23 "docs/markdownlint_configure.md": (
24 """<!-- Suppression rationale: fixed input. -->
25<!-- markdownlint-configure-file
26{
27 "MD013": false,
28 "MD033": false
29}
30-->
31# Configured fixture
32
33<span>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
34"""
35 ),
36 "docs/markdownlint_configure_true.md": (
37 """<!-- markdownlint-configure-file { "MD013": true } -->
38# Enabled fixture
39"""
40 ),
41 "docs/markdownlint_configure_empty.md": (
42 """<!-- markdownlint-configure-file {} -->
43<!-- markdownlint-configure-file { "MD013": {} } -->
44# Empty configuration fixture
45"""
46 ),
47 "docs/markdownlint_configure_options.md": (
48 """<!-- Suppression rationale: widens generated lines. -->
49<!-- markdownlint-configure-file
50{
51 "MD013": {
52 "line_length": 100
53 }
54}
55-->
56# Option fixture
57"""
58 ),
59 "docs/markdownlint_configure_mixed.md": (
60 """<!-- Suppression rationale: generated content needs two relaxations. -->
61<!-- markdownlint-configure-file
62{
63 "MD013": false,
64 "MD033": {
65 "allowed_elements": [
66 "span"
67 ]
68 }
69}
70-->
71# Mixed fixture
72
73<span>generated</span>
74"""
75 ),
76 "docs/markdownlint_active.md": (
77 """# Active fixture
78
79<span>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
80"""
81 ),
82}
83
84
85def _assert_clang_format_behavior(root: Path, failures: list[str]) -> None:
86 """Probe empty-colon regions with the repository-pinned clang-format major."""
87 tool = shutil.which("clang-format-22")
88 if tool is None:
89 expect(
90 tool is not None,
91 "must fire: clang-format-22 is available for behavioral probes",
92 failures,
93 )
94 return
95 version = subprocess.run( # noqa: S603 -- resolved pinned binary and fixed argv
96 [tool, "--version"],
97 capture_output=True,
98 text=True,
99 check=False,
100 timeout=SHFMT_TIMEOUT_SECONDS,
101 )
102 pinned = version.returncode == 0 and "clang-format version 22." in version.stdout
103 expect(pinned, "must fire: behavioral probes use clang-format-22", failures)
104 if not pinned:
105 return
106 probe = subprocess.run( # noqa: S603 -- resolved pinned binary and fixed fixture
107 [tool, "--style=LLVM", "apps/fixture/src/clang_empty_colon_control.c"],
108 cwd=root,
109 capture_output=True,
110 text=True,
111 check=False,
112 timeout=SHFMT_TIMEOUT_SECONDS,
113 )
114 expect(
115 probe.returncode == 0
116 and "int preserved[] = {1, 2};" in probe.stdout
117 and "int reformatted[] = {1, 2};" in probe.stdout,
118 "quiet: clang-format-22 honors empty-colon off/on controls",
119 failures,
120 )
121
122
123def _assert_c_controls(
124 root: Path, by_path: dict[str, list[Suppression]], failures: list[str]
125) -> None:
126 """Assert clang-format and every documented IWYU command spelling."""
127 c_path = "apps/fixture/src/format_controls.c"
128 c_directives = {item.directive for item in by_path[c_path]}
129 expect(
130 {"clang-format off", "clang-format on"} <= c_directives,
131 "must fire: exact lowercase clang-format region delimiters",
132 failures,
133 )
134 colon_rows = by_path["apps/fixture/src/clang_colon_control.c"]
135 expect(
136 len(colon_rows) == EXPECTED_REGION_ROWS and bool(colon_rows[0].reason),
137 "must fire: clang-format's behaviorally supported colon rationale",
138 failures,
139 )
140 empty_rows = by_path["apps/fixture/src/clang_empty_colon_control.c"]
141 empty_off = next(item for item in empty_rows if item.scope == "region-start")
142 expect(
143 len(empty_rows) == EXPECTED_REGION_ROWS and "blank-reason" in empty_off.concerns,
144 "must fire: empty-colon clang-format off is a blank-reason control",
145 failures,
146 )
147 _assert_clang_format_behavior(root, failures)
148 iwyu_commands = {
149 "always_keep",
150 "associated",
151 "begin_exports",
152 "begin_keep",
153 "end_exports",
154 "end_keep",
155 "export",
156 "friend",
157 "keep",
158 "no_forward_declare",
159 "no_include",
160 "private",
161 }
162 seen_iwyu = {
163 item.directive.removeprefix("IWYU pragma: ")
164 for item in by_path[c_path]
165 if item.tool == "iwyu"
166 }
167 expect(iwyu_commands == seen_iwyu, "must fire: every documented IWYU pragma", failures)
168
169
170def _assert_yamllint(by_path: dict[str, list[Suppression]], failures: list[str]) -> None:
171 """Assert yamllint's file, line, region, and repeated-rule forms."""
172 yaml_directives = {
173 item.directive for item in by_path["format_controls.yml"] if item.tool == "yamllint"
174 }
175 expect(
176 {"yamllint disable", "yamllint disable-line", "yamllint enable"} == yaml_directives,
177 "must fire: yamllint line and region spellings",
178 failures,
179 )
180 disable_file = [item for item in by_path["disable_file.yml"] if item.tool == "yamllint"]
181 expect(
182 len(disable_file) == 1 and disable_file[0].directive == "yamllint disable-file",
183 "must fire: first-line yamllint disable-file",
184 failures,
185 )
186 expect(
187 {
188 (item.directive, item.rule)
189 for item in by_path["format_controls.yml"]
190 if item.tool == "yamllint"
191 }
192 == {
193 ("yamllint disable", "comments"),
194 ("yamllint disable", "line-length"),
195 ("yamllint disable-line", "line-length"),
196 ("yamllint disable-line", "trailing-spaces"),
197 ("yamllint enable", "comments"),
198 ("yamllint enable", "line-length"),
199 },
200 "must fire: space-separated repeated yamllint rule tokens",
201 failures,
202 )
203
204
205def _assert_shfmt_behavior(root: Path, failures: list[str]) -> None:
206 """Probe EditorConfig semantics with the exact project-pinned shfmt."""
207 dockerfile = (REPO_ROOT / ".devcontainer" / "Dockerfile").read_text(encoding="ascii")
208 pin_match = re.search(r"^ARG SHFMT_VERSION=(\S+)$", dockerfile, re.MULTILINE)
209 tool = shutil.which("shfmt")
210 if pin_match is None or tool is None:
211 expect(
212 pin_match is not None and tool is not None,
213 "must fire: pinned shfmt is available for behavioral probes",
214 failures,
215 )
216 return
217 version = subprocess.run( # noqa: S603 -- resolved absolute path and fixed argv
218 [tool, "--version"],
219 capture_output=True,
220 text=True,
221 check=False,
222 timeout=SHFMT_TIMEOUT_SECONDS,
223 )
224 pinned = version.returncode == 0 and version.stdout.strip().removeprefix(
225 "v"
226 ) == pin_match.group(1)
227 expect(pinned, "must fire: behavioral probes use the project-pinned shfmt", failures)
228 if not pinned:
229 return
230 paths = [
231 "generated/ignored.sh",
232 "generated/mixed-case.sh",
233 "generated/false.sh",
234 "generated/unset.sh",
235 "bad/generated/uppercase-value.sh",
236 ]
237 probe = subprocess.run( # noqa: S603 -- resolved pinned tool and fixed fixture paths
238 [tool, "--apply-ignore", "-l", *paths],
239 cwd=root,
240 capture_output=True,
241 text=True,
242 check=False,
243 timeout=SHFMT_TIMEOUT_SECONDS,
244 )
245 formatted = set(probe.stdout.splitlines())
246 expect(
247 probe.returncode == 1 and not probe.stderr and not formatted.intersection(paths[:2]),
248 "quiet: pinned shfmt honors lowercase and mixed-case ignore=true",
249 failures,
250 )
251 expect(
252 formatted == set(paths[2:]),
253 "must fire: pinned shfmt rejects uppercase true and keeps false/unset active",
254 failures,
255 )
256
257
258def _assert_hadolint_behavior(root: Path, failures: list[str]) -> None:
259 """Probe spaced ignore syntax with the exact project-pinned Hadolint."""
260 dockerfile = (REPO_ROOT / ".devcontainer" / "Dockerfile").read_text(encoding="ascii")
261 pin_match = re.search(r"^ARG HADOLINT_VERSION=(\S+)$", dockerfile, re.MULTILINE)
262 tool = shutil.which("hadolint")
263 if pin_match is None or tool is None:
264 expect(
265 pin_match is not None and tool is not None,
266 "must fire: pinned Hadolint is available for behavioral probes",
267 failures,
268 )
269 return
270 version = subprocess.run( # noqa: S603 -- resolved pinned binary and fixed argv
271 [tool, "--version"],
272 capture_output=True,
273 text=True,
274 check=False,
275 timeout=SHFMT_TIMEOUT_SECONDS,
276 )
277 pinned = version.returncode == 0 and version.stdout.strip().endswith(pin_match.group(1))
278 expect(pinned, "must fire: behavioral probes use project-pinned Hadolint", failures)
279 if not pinned:
280 return
281 probes = [
282 subprocess.run( # noqa: S603 -- resolved pinned binary and fixed fixture
283 [tool, "--format", "json", path],
284 cwd=root,
285 capture_output=True,
286 text=True,
287 check=False,
288 timeout=SHFMT_TIMEOUT_SECONDS,
289 )
290 for path in ("Dockerfile", "Dockerfile.hadolint-active")
291 ]
292 try:
293 ignored, active = (json.loads(probe.stdout) for probe in probes)
294 except json.JSONDecodeError:
295 ignored, active = None, None
296 active_codes = {item.get("code") for item in active} if isinstance(active, list) else set()
297 expect(
298 probes[0].returncode == 0
299 and ignored == []
300 and probes[1].returncode == 1
301 and active_codes == {"DL3003", "SC2164"},
302 "quiet: Hadolint accepts whitespace around equals and commas",
303 failures,
304 )
305
306
307def _assert_cmake_lint_behavior(root: Path, failures: list[str]) -> None:
308 """Probe cmakelang's literal first space and later whitespace."""
309 project = (REPO_ROOT / "pyproject.toml").read_text(encoding="ascii")
310 pin_match = re.search(r'"cmakelang==(\S+)"', project)
311 tool = shutil.which("cmake-lint")
312 if pin_match is None or tool is None:
313 expect(
314 pin_match is not None and tool is not None,
315 "must fire: pinned cmake-lint is available for probes",
316 failures,
317 )
318 return
319 version = subprocess.run( # noqa: S603 -- resolved pinned binary and fixed argv
320 [tool, "--version"],
321 capture_output=True,
322 text=True,
323 check=False,
324 timeout=SHFMT_TIMEOUT_SECONDS,
325 )
326 pinned = version.returncode == 0 and version.stdout.strip() == pin_match.group(1)
327 expect(pinned, "must fire: behavioral probes use project-pinned cmake-lint", failures)
328 if not pinned:
329 return
330 space, tab = [
331 subprocess.run( # noqa: S603 -- resolved pinned binary and fixed fixtures
332 [tool, path],
333 cwd=root,
334 capture_output=True,
335 text=True,
336 check=False,
337 timeout=SHFMT_TIMEOUT_SECONDS,
338 )
339 for path in ("cmake_lint_space.cmake", "cmake_lint_tab.cmake")
340 ]
341 expect(
342 space.returncode == 0
343 and "C0103" not in space.stdout
344 and "R0912" not in space.stdout
345 and tab.returncode == 1
346 and "[C0103]" in tab.stdout,
347 "must fire: cmake-lint accepts later tabs but rejects a direct post-colon tab",
348 failures,
349 )
350
351
352def _assert_build_controls(
353 root: Path,
354 inventory: Inventory,
355 by_path: dict[str, list[Suppression]],
356 failures: list[str],
357) -> None:
358 """Assert Hadolint, CMake, and shfmt's EditorConfig controls."""
359 hadolint_rows = [
360 item
361 for path in ("Dockerfile", "Dockerfile.global")
362 for item in by_path[path]
363 if item.tool == "hadolint"
364 ]
365 hadolint_rules = {(item.path, item.rule) for item in hadolint_rows}
366 expect(
367 len(hadolint_rows) == EXPECTED_HADOLINT_ROWS
368 and hadolint_rules
369 == {
370 ("Dockerfile", "DL3003"),
371 ("Dockerfile", "SC2164"),
372 ("Dockerfile.global", "DL3008"),
373 ("Dockerfile.global", "SC1091"),
374 },
375 "must fire: spaced Hadolint lists normalize to one row per rule",
376 failures,
377 )
378 _assert_hadolint_behavior(root, failures)
379 cmake_directives = {item.directive for item in by_path["format_controls.cmake"]}
380 expect(
381 {
382 "cmake-format off",
383 "cmake-format on",
384 "cmake-lint disable",
385 "cmf off",
386 "cmf on",
387 }
388 == cmake_directives,
389 "must fire: real CMake formatter regions and lint disable pragma",
390 failures,
391 )
392 cmake_space = [item for item in by_path["cmake_lint_space.cmake"] if item.tool == "cmake-lint"]
393 expect(
394 {item.rule for item in cmake_space} == {"C0103", "R0912"}
395 and not by_path.get("cmake_lint_tab.cmake"),
396 "must fire: cmake-lint requires a first space and accepts later tabs",
397 failures,
398 )
399 _assert_cmake_lint_behavior(root, failures)
400 shfmt = [item for item in by_path[".editorconfig"] if item.tool == "shfmt"]
401 expect(
402 {item.rule for item in shfmt} == {"[generated/**]", "[generated/mixed-case.sh]"},
403 "must fire: case-insensitive shfmt ignore=true properties are controls",
404 failures,
405 )
406 expect(
407 not any(item.path == ".editorconfig" for item in inventory.findings),
408 "quiet: valid shfmt false and unset properties add no findings",
409 failures,
410 )
411 _assert_shfmt_behavior(root, failures)
412
413
414def _assert_markdownlint_behavior(root: Path, failures: list[str]) -> None:
415 """Probe official configure-file behavior when a Markdown CLI is installed."""
416 tool = shutil.which("markdownlint-cli2") or shutil.which("markdownlint")
417 if tool is None:
418 return
419 configured, active = [
420 subprocess.run( # noqa: S603 -- resolved tool and fixed fixture paths
421 [tool, path],
422 cwd=root,
423 capture_output=True,
424 text=True,
425 check=False,
426 timeout=SHFMT_TIMEOUT_SECONDS,
427 )
428 for path in ("docs/markdownlint_configure.md", "docs/markdownlint_active.md")
429 ]
430 active_output = active.stdout + active.stderr
431 expect(
432 configured.returncode == 0
433 and active.returncode != 0
434 and "MD013" in active_output
435 and "MD033" in active_output,
436 "quiet: installed markdownlint honors multiline configure-file JSON",
437 failures,
438 )
439
440
441def _assert_doxygen_controls(
442 inventory: Inventory,
443 by_path: dict[str, list[Suppression]],
444 failures: list[str],
445) -> None:
446 """Assert Doxygen controls in plain authored inputs and vendored source."""
447 markdown = by_path["docs/format_controls.md"]
448 markdown_doxygen = [item for item in markdown if item.tool == "doxygen"]
449 expect(
450 len(markdown_doxygen) == EXPECTED_REGION_ROWS,
451 "quiet: Markdown HTML-comment Doxygen commands are inactive",
452 failures,
453 )
454 documentation_paths = {item.path for item in inventory.suppressions if item.tool == "doxygen"}
455 expect(
456 {
457 "apps/fixture/src/format_controls.c",
458 "docs/format_controls.dox",
459 "docs/format_controls.md",
460 "scripts/checks/suppression_selftest.py",
461 }
462 <= documentation_paths,
463 "must fire: Doxygen controls in every configured Doxyfile language",
464 failures,
465 )
466 vendor_path = "libs/third_party/vendor/include/vendor_docs.h"
467 vendor_rows = [item for item in by_path[vendor_path] if item.tool == "doxygen"]
468 expect(
469 len(vendor_rows) == EXPECTED_REGION_ROWS
470 and {item.owner for item in vendor_rows} == {"vendor"}
471 and not any(item.concerns for item in vendor_rows),
472 "quiet: vendored Doxygen conditionals are inventoried as vendor-owned",
473 failures,
474 )
475 expect(
476 not any(item.path == vendor_path for item in inventory.findings),
477 "quiet: vendored Doxygen conditionals are exempt, not unknown",
478 failures,
479 )
480
481
482def _assert_invalid_markdownlint_configures(inventory: Inventory, failures: list[str]) -> None:
483 """Assert malformed JSON and unsupported values both fail closed."""
484 malformed = [
485 item
486 for item in inventory.findings
487 if item.path == "docs/wrong_format_controls.md"
488 and "markdownlint-configure-file" in item.message
489 ]
490 expect(
491 len(malformed) == EXPECTED_MARKDOWN_CONFIG_ROWS,
492 "must fire: malformed JSON and unsupported configure-file values fail closed",
493 failures,
494 )
495
496
497def _assert_markdownlint_configures(
498 inventory: Inventory,
499 by_path: dict[str, list[Suppression]],
500 failures: list[str],
501) -> None:
502 """Assert configure-file values in both valid and invalid directions."""
503 disabled = by_path["docs/markdownlint_configure.md"]
504 expect(
505 len(disabled) == EXPECTED_MARKDOWN_CONFIG_ROWS
506 and {item.rule for item in disabled} == {"MD013", "MD033"}
507 and {item.reason for item in disabled} == {"fixed input."}
508 and not any(item.concerns for item in disabled),
509 "must fire: configure-file false values normalize one row per rule",
510 failures,
511 )
512 options = by_path["docs/markdownlint_configure_options.md"]
513 expect(
514 len(options) == 1
515 and options[0].rule == "MD013"
516 and options[0].reason == "widens generated lines."
517 and not options[0].concerns,
518 "must fire: non-empty configure-file option objects are inventoried",
519 failures,
520 )
521 mixed = by_path["docs/markdownlint_configure_mixed.md"]
522 expect(
523 len(mixed) == EXPECTED_MARKDOWN_CONFIG_ROWS
524 and {item.rule for item in mixed} == {"MD013", "MD033"}
525 and {item.reason for item in mixed} == {"generated content needs two relaxations."}
526 and not any(item.concerns for item in mixed),
527 "must fire: mixed false and option values preserve both relaxations",
528 failures,
529 )
530 quiet_paths = {
531 "docs/markdownlint_configure_true.md",
532 "docs/markdownlint_configure_empty.md",
533 }
534 expect(
535 all(not by_path.get(path) for path in quiet_paths),
536 "quiet: true and empty configure-file values add no waiver rows",
537 failures,
538 )
539 valid_paths = quiet_paths | {
540 "docs/markdownlint_configure.md",
541 "docs/markdownlint_configure_options.md",
542 "docs/markdownlint_configure_mixed.md",
543 }
544 expect(
545 not any(item.path in valid_paths for item in inventory.findings),
546 "quiet: every valid configure-file object has zero integrity findings",
547 failures,
548 )
549 _assert_invalid_markdownlint_configures(inventory, failures)
550
551
552def _assert_documentation_controls(
553 root: Path, inventory: Inventory, by_path: dict[str, list[Suppression]], failures: list[str]
554) -> None:
555 """Assert configured Markdown controls and all Doxyfile input languages."""
556 markdown = by_path["docs/format_controls.md"]
557 markdown_controls = {item.directive for item in markdown}
558 expect(
559 {
560 "markdownlint capture",
561 "markdownlint disable",
562 "markdownlint disable-file",
563 "markdownlint disable-line",
564 "markdownlint disable-next-line",
565 "markdownlint enable",
566 "markdownlint enable-file",
567 "markdownlint restore",
568 "prettier-ignore",
569 }
570 <= markdown_controls,
571 "must fire: configured Prettier and every markdownlint control spelling",
572 failures,
573 )
574 _assert_markdownlint_configures(inventory, by_path, failures)
575 _assert_doxygen_controls(inventory, by_path, failures)
576 expect(
577 not any(item.path == "docs/format_controls.md" for item in inventory.findings),
578 "quiet: valid Markdown controls have zero integrity findings",
579 failures,
580 )
581 _assert_markdownlint_behavior(root, failures)
582
583
584def _assert_invalid_controls(inventory: Inventory, failures: list[str]) -> None:
585 """Assert wrong syntax stays inactive and every invalid region fails closed."""
586 invalid_paths = {
587 "Dockerfile.bad",
588 "apps/fixture/src/wrong_controls.c",
589 "bad/.editorconfig",
590 "docs/wrong_format_controls.md",
591 "cmake_lint_tab.cmake",
592 "scripts/checks/suppression_scan.py",
593 "wrong_format_controls.cmake",
594 "wrong_format_controls.yml",
595 }
596 expect(
597 not any(item.path in invalid_paths for item in inventory.suppressions),
598 "quiet: wrong case, suffix, separator, argument, and language never count",
599 failures,
600 )
601 malformed_paths = {
602 item.path
603 for item in inventory.findings
604 if item.code in {"malformed-tool-config", "malformed-tool-control"}
605 }
606 expect(
607 invalid_paths <= malformed_paths,
608 "must fire: every invalid tool-control fixture fails closed",
609 failures,
610 )
611
612 unmatched_paths = {
613 "apps/fixture/src/unmatched_controls.c",
614 "docs/unmatched_format_controls.dox",
615 "unmatched_format_controls.cmake",
616 "unmatched_format_controls.yml",
617 }
618 region_paths = {item.path for item in inventory.findings if "region" in item.code}
619 expect(
620 unmatched_paths <= region_paths,
621 "must fire: unmatched clang-format, IWYU, Doxygen, CMake, and yamllint regions",
622 failures,
623 )
624
625
626def _assert_rationales(inventory: Inventory, failures: list[str]) -> None:
627 """Assert the exact controls whose active syntax carries no rationale."""
628 assigned_tools = {
629 "clang-format",
630 "cmake-format",
631 "cmake-lint",
632 "doxygen",
633 "hadolint",
634 "iwyu",
635 "markdownlint",
636 "prettier",
637 "shfmt",
638 "yamllint",
639 }
640 assigned = {item for item in inventory.suppressions if item.tool in assigned_tools}
641 blank = [item for item in assigned if "blank-reason" in item.concerns]
642 blank_keys = {(item.path, item.tool, item.scope) for item in blank}
643 expect(
644 blank_keys
645 == {
646 ("apps/fixture/src/clang_empty_colon_control.c", "clang-format", "region-start"),
647 ("disable_file.yml", "yamllint", "file"),
648 },
649 "must fire: empty-colon off and first-line disable-file have blank reasons",
650 failures,
651 )
652
653
654def assert_tool_control_syntax_awareness(
655 root: Path, inventory: Inventory, failures: list[str]
656) -> None:
657 """Assert every assigned tool spelling and invalid direction explicitly."""
658 by_path: dict[str, list[Suppression]] = {}
659 for item in inventory.suppressions:
660 by_path.setdefault(item.path, []).append(item)
661 _assert_c_controls(root, by_path, failures)
662 _assert_yamllint(by_path, failures)
663 _assert_build_controls(root, inventory, by_path, failures)
664 _assert_documentation_controls(root, inventory, by_path, failures)
665 _assert_invalid_controls(inventory, failures)
666 _assert_rationales(inventory, failures)