ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
suppression_compiler_selftest.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Compiler-control assertions shared by the suppression selftest."""
4
5from __future__ import annotations
6
7import os
8from pathlib import Path
9
10from check_no_dynamic_alloc import check as check_dynamic_alloc
11from selftest_assert import expect
12from suppression_model import Inventory
13from suppression_scan import scan_paths
14
15EXPECTED_COMPILER_PRAGMA_ROWS = 12
16EXPECTED_CLANG_TIDY_CONFIG_ROWS = 1
17EXPECTED_ALLOC_PROBLEMS = 2
18EXPECTED_CPP_ALLOC_PROBLEMS = 1
19EXPECTED_CPP_ALLOC_ROWS = 2
20EXPECTED_GNU_UNUSED_ROWS = 2
21EXPECTED_GNU_SANITIZER_ROWS = 3
22
23
24def _run_tool(executable: str, arguments: list[str], output: Path) -> tuple[int, str]:
25 """Run one fixed absolute tool path and capture its complete diagnostics."""
26 flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
27 descriptor = os.open(output, flags, 0o600)
28 actions = [
29 (os.POSIX_SPAWN_DUP2, descriptor, 1),
30 (os.POSIX_SPAWN_DUP2, descriptor, 2),
31 (os.POSIX_SPAWN_CLOSE, descriptor),
32 ]
33 try:
34 process = os.posix_spawn(
35 executable,
36 [executable, *arguments],
37 os.environ,
38 file_actions=actions,
39 )
40 _, status = os.waitpid(process, 0)
41 finally:
42 os.close(descriptor)
43 return os.waitstatus_to_exitcode(status), output.read_text(encoding="utf-8")
44
45
46def _assert_control_rows(inventory: Inventory, failures: list[str]) -> None:
47 """Assert valid C-family controls fire once and string lookalikes stay inert."""
48 rows = [item for item in inventory.suppressions if item.path == "compiler_controls.c"]
49 pragmas = [item for item in rows if item.provenance == "compiler-pragma"]
50 expect(
51 len(pragmas) == EXPECTED_COMPILER_PRAGMA_ROWS,
52 "must fire: paired GCC and clang diagnostic state controls",
53 failures,
54 )
55 expect(
56 len([item for item in rows if item.directive == "[[maybe_unused]]"]) == 1
57 and len([item for item in rows if item.directive == "__attribute__((unused))"])
58 == EXPECTED_GNU_UNUSED_ROWS
59 and len([item for item in rows if item.directive == "[[gnu::unused]]"]) == 1
60 and len([item for item in rows if item.directive == "__attribute__((no_sanitize))"])
61 == EXPECTED_GNU_SANITIZER_ROWS
62 and len([item for item in rows if item.directive == "[[clang::no_sanitize]]"]) == 1,
63 "must fire: every clang-18-valid compiler attribute spelling is inventoried",
64 failures,
65 )
66 expect(
67 len([item for item in rows if item.directive == "RA8_NASA_RULE_3_OK"]) == 1,
68 "must fire: reason-bearing NASA Rule 3 waiver",
69 failures,
70 )
71 expect(
72 len([item for item in rows if item.directive == "alloc-allow"]) == 1,
73 "must fire: same-line reasoned allocation waiver",
74 failures,
75 )
76
77
78def _assert_malformed_controls(inventory: Inventory, failures: list[str]) -> None:
79 """Assert malformed, orphaned, mismatched, and unscoped controls fail closed."""
80 codes = {item.code for item in inventory.findings if item.path == "malformed_controls.c"}
81 expected = {
82 "malformed-alloc-allow",
83 "malformed-diagnostic-pragma",
84 "malformed-gnu-attribute",
85 "malformed-maybe-unused",
86 "malformed-standard-attribute",
87 "malformed-nasa-rule-3-waiver",
88 "orphan-alloc-allow",
89 "unmatched-diagnostic-pop",
90 "unmatched-region-end",
91 "unmatched-region-start",
92 "unscoped-diagnostic-control",
93 }
94 expect(
95 expected <= codes,
96 "must fire: malformed, orphaned, and unscoped compiler controls",
97 failures,
98 )
99
100
101def _assert_clang_tidy_and_alloc(inventory: Inventory, root: Path, failures: list[str]) -> None:
102 """Assert clang-tidy reasoning and lexical allocation scope."""
103 config = [
104 item
105 for item in inventory.suppressions
106 if item.path == ".clang-tidy" and item.directive == "Checks exclude"
107 ]
108 expect(
109 len(config) == EXPECTED_CLANG_TIDY_CONFIG_ROWS and not config[0].concerns,
110 "must fire: source-located reasoned clang-tidy global exclusion",
111 failures,
112 )
113 alloc_problems = check_dynamic_alloc(root / "alloc_consumer.c")
114 expect(
115 len(alloc_problems) == EXPECTED_ALLOC_PROBLEMS
116 and any("string_only" in problem for problem in alloc_problems)
117 and any("without governed allocation" in problem for problem in alloc_problems),
118 "quiet: allocation waiver works only in a lexical comment with a governed call",
119 failures,
120 )
121 cpp_rows = [
122 item
123 for item in inventory.suppressions
124 if item.path == "alloc_consumer.cpp" and item.directive == "alloc-allow"
125 ]
126 cpp_problems = check_dynamic_alloc(root / "alloc_consumer.cpp")
127 cpp_findings = [
128 item
129 for item in inventory.findings
130 if item.path == "alloc_consumer.cpp" and item.code == "orphan-alloc-allow"
131 ]
132 expect(
133 len(cpp_rows) == EXPECTED_CPP_ALLOC_ROWS
134 and {item.rule for item in cpp_rows} == {"C++ new", "C++ delete"}
135 and len(cpp_problems) == EXPECTED_CPP_ALLOC_PROBLEMS
136 and "without governed allocation" in cpp_problems[0]
137 and len(cpp_findings) == 1,
138 "must fire: C++ new/delete controls are active and orphan controls fail closed",
139 failures,
140 )
141
142
143def assert_compiler_controls(inventory: Inventory, root: Path, failures: list[str]) -> None:
144 """Assert every assigned C-family control fires without string false positives."""
145 _assert_control_rows(inventory, failures)
146 _assert_malformed_controls(inventory, failures)
147 _assert_clang_tidy_and_alloc(inventory, root, failures)
148
149
150def _clang_syntax_status(root: Path, name: str, source: str) -> int:
151 """Compile one C23 fixture with the pinned warning-strict Clang."""
152 fixture = root / f"{name}.c"
153 fixture.write_text(source, encoding="ascii")
154 status, _ = _run_tool(
155 "/usr/bin/clang-18",
156 [
157 "-std=c2x",
158 "-Wall",
159 "-Wextra",
160 "-Werror",
161 "-fsyntax-only",
162 str(fixture),
163 ],
164 root / f"{name}.log",
165 )
166 return status
167
168
169def _assert_compiler_probe(root: Path, failures: list[str]) -> None:
170 """Prove pinned Clang accepts every inventoried valid grammar."""
171 compiler = Path("/usr/bin/clang-18")
172 expect(compiler.is_file(), "must fire: pinned clang-18 is available", failures)
173 if not compiler.is_file():
174 return
175 source = """_Pragma("clang diagnostic push")
176_Pragma("clang diagnostic ignored \\"-Wunused-variable\\"")
177void probe(void) { int unused; }
178_Pragma("clang diagnostic pop")
179[[deprecated, maybe_unused]] static int maybe_value;
180[[gnu::unused]] static int scoped_gnu_value;
181__attribute((unused)) static int short_gnu_value;
182__attribute__((unused)) static int gnu_value;
183__attribute__((no_sanitize(\"undefined\"))) int identity(int value) { return value; }
184__attribute__((__no_sanitize__(\"undefined\"))) int identity_two(int value) { return value; }
185__attribute__((__no_sanitize_address__)) int identity_three(int value) { return value; }
186[[clang::no_sanitize(\"undefined\")]] int identity_four(int value) { return value; }
187"""
188 accepted_status = _clang_syntax_status(root, "compiler_probe", source)
189 expect(
190 accepted_status == 0,
191 "quiet: clang-18 accepts _Pragma and every supported attribute grammar",
192 failures,
193 )
194
195
196def _assert_compiler_rejections(root: Path, failures: list[str]) -> None:
197 """Prove pinned Clang rejects the three fail-closed grammar boundaries."""
198 if not Path("/usr/bin/clang-18").is_file():
199 return
200 invalid = (
201 ("compiler_bad_unused", "__attribute__((unused extra)) static int value;\n"),
202 (
203 "compiler_bad_sanitizer",
204 "__attribute__((no_sanitize(foo))) int identity(int value) { return value; }\n",
205 ),
206 ("compiler_bad_maybe", "[[maybe_unused bogus]] static int value;\n"),
207 )
208 for name, source in invalid:
209 rejected_status = _clang_syntax_status(root, name, source)
210 expect(
211 rejected_status != 0,
212 f"must fire: clang-18 rejects {name.removeprefix('compiler_bad_')}",
213 failures,
214 )
215
216
217def _assert_clang_tidy_probe(root: Path, failures: list[str]) -> None:
218 """Prove pinned clang-tidy accepts the inventoried negative check glob."""
219 tidy = Path("/usr/bin/clang-tidy-18")
220 expect(tidy.is_file(), "must fire: pinned clang-tidy-18 is available", failures)
221 if tidy.is_file():
222 dumped_status, dumped_output = _run_tool(
223 "/usr/bin/clang-tidy-18",
224 [
225 f"--config-file={root / '.clang-tidy'}",
226 "--dump-config",
227 ],
228 root / "clang_tidy_probe.log",
229 )
230 expect(
231 dumped_status == 0 and "-readability-fixture" in dumped_output,
232 "quiet: clang-tidy-18 activates the inventoried negative Checks glob",
233 failures,
234 )
235
236
237def assert_compiler_tool_probes(root: Path, failures: list[str]) -> None:
238 """Prove pinned compiler tools accept the inventoried grammars."""
239 _assert_compiler_probe(root, failures)
240 _assert_compiler_rejections(root, failures)
241 _assert_clang_tidy_probe(root, failures)
242
243
244def assert_clang_tidy_config_fail_closed(root: Path, failures: list[str]) -> None:
245 """Assert valid YAML shapes outside the source locator cannot disappear."""
246 config = root / ".clang-tidy"
247 original = config.read_text(encoding="ascii")
248 config.write_text("Checks: [readability-*]\n", encoding="ascii")
249 inventory = scan_paths(root, [".clang-tidy"])
250 expect(
251 any(item.code == "malformed-clang-tidy-config" for item in inventory.findings),
252 "must fire: non-string clang-tidy Checks config fails closed",
253 failures,
254 )
255 config.write_text(original, encoding="ascii")