3"""Regression tests for the annotation checker itself, on a synthetic tree.
5Every defect guarded here is one this gate has actually shipped, and they share
6a shape: the checker kept reporting a number, the number got smaller, and
7smaller read as better. So each assertion below is made in BOTH directions --
8the rule fires on the broken fixture *and* stays quiet on the correct one --
9because a rule can be "fixed" by defanging it and no single-direction test can
12The fixtures are laid out like ``libs/<module>/`` and ``tools/<tool>/`` so
13``module_of()`` and ``is_first_party()`` resolve against them, with the
14annotations spelled the way ``ra8_attributes.h`` lowers them so no project
15header is needed. Scope is pointed at the temporary tree through
16:func:`annot_scope.override_repo_root`, never by rebinding a module global:
17a rebinding is invisible to other modules' imports, which would leave this
18suite asserting against the real checkout and passing without proving anything.
21from __future__
import annotations
28from annot_clang
import _first_party_include_roots, cindex
29from annot_loopbound
import run_loopbound_selftest
30from annot_model
import AnnotatedSymbol, Violation, WalkState
31from annot_rules
import enforce_rules
32from annot_scope
import discover_translation_units, override_repo_root
33from annot_walk
import walk_tu
43_SELFTEST_SOURCES: dict[str, str] = {
49 "libs/mod_other/src/other.c":
"""
50[[clang::annotate("ra8_internal")]] static void shared_helper(unsigned char* p, unsigned short n)
52 for (unsigned short i = 0U; i < n; ++i) {
57[[clang::annotate("ra8_priv")]] void other_caller(unsigned char* p);
59void other_caller(unsigned char* p)
67 "libs/mod_priv/src/priv.c":
"""
68[[clang::annotate("ra8_priv")]] void shared_helper(unsigned char* p, unsigned int n);
69[[clang::annotate("ra8_priv")]] void priv_owner_caller(unsigned char* p);
71void shared_helper(unsigned char* p, unsigned int n)
73 for (unsigned int i = 0U; i < n; ++i) {
78void priv_owner_caller(unsigned char* p)
86 "libs/mod_stranger/src/stranger.c":
"""
87[[clang::annotate("ra8_priv")]] void stranger_caller(unsigned char* p);
89void shared_helper(unsigned char* p, unsigned int n);
91void stranger_caller(unsigned char* p)
97 "libs/mod_link/inc/mod_link.h":
"""
99void link_public_api(void);
101 "libs/mod_link/src/mod_link_internal.h":
"""
103void link_internal_declared(void);
104[[clang::annotate("ra8_priv")]] void link_internal_annotated(void);
106 "libs/mod_link/src/pass.c":
"""
108#include "mod_link_internal.h"
110/* Published by the library's public inc/ header: public API, no annotation. */
111void link_public_api(void) {}
113/* Declared in the *_internal.h AND tagged: the sanctioned cross-TU shape. */
114void link_internal_annotated(void) {}
116/* Tagged in place, declared nowhere: still fine, the tag is the statement. */
117[[clang::annotate("ra8_test_helper")]] void link_test_hook(void) {}
119/* static + RA8_INTERNAL: out of scope for the rule entirely. */
120[[clang::annotate("ra8_internal")]] static void link_file_local(void) {}
129 "libs/mod_link/src/fail.c":
"""
130#include "mod_link_internal.h"
132/* Declared library-private but never classified -> wants RA8_PRIV. */
133void link_internal_declared(void) {}
135/* Nothing declares it and no table names it -> wants static or a header. */
136void link_undeclared(void) {}
143 "libs/mod_link/src/vectors.c":
"""
144void handler_tabled(void);
145void handler_untabled(void);
147void handler_tabled(void) {}
148void handler_untabled(void) {}
150void (*const g_vector_table[])(void) = {
157 "libs/mod_alloc/src/alloc.c":
"""
158void* malloc(unsigned long n);
161/* Firmware, no waiver: both the malloc and the free must be reported. */
162[[clang::annotate("ra8_internal")]] static void fw_untagged_allocator(void)
164 void* p = malloc(16UL);
168/* Firmware WITH the documented waiver: the sweep must leave it alone. */
169[[clang::annotate("ra8_nasa_rule_3_ok")]] static void fw_waived_allocator(void)
171 void* p = malloc(16UL);
180 "libs/mod_lock/src/lock.c":
"""
181[[clang::annotate("ra8_priv")]] void lock_take(void);
182[[clang::annotate("ra8_priv")]]
183[[clang::annotate("ra8_releases_resource:bus")]] void lock_drop(void);
184[[clang::annotate("ra8_priv")]]
185[[clang::annotate("ra8_expects_lock:bus")]] void lock_guarded_body(void);
187void lock_take(void) {}
188void lock_drop(void) {}
189void lock_guarded_body(void) {}
191[[clang::annotate("ra8_priv")]]
192[[clang::annotate("ra8_releases_resource:other")]] void lock_drop_other(void);
194void lock_drop_other(void) {}
196/* PASSES: takes the lock for its whole body and discharges it. */
197[[clang::annotate("ra8_priv")]]
198[[clang::annotate("ra8_owns_resource:bus")]] void lock_owner_caller(void);
200void lock_owner_caller(void)
207/* PASSES: entered under the lock, propagating the contract upward. */
208[[clang::annotate("ra8_priv")]]
209[[clang::annotate("ra8_expects_lock:bus")]] void lock_nested_body(void);
211void lock_nested_body(void)
216/* FAILS: reaches the guarded body holding nothing. */
217[[clang::annotate("ra8_priv")]] void lock_bare_caller(void);
219void lock_bare_caller(void)
224/* FAILS: owns a DIFFERENT lock. The name has to match, or one mutex would
225 silently discharge another mutex's contract. */
226[[clang::annotate("ra8_priv")]]
227[[clang::annotate("ra8_owns_resource:other")]] void lock_wrong_name_caller(void);
229void lock_wrong_name_caller(void)
242 "tools/mod_host/src/host_tool.c":
"""
243void* malloc(unsigned long n);
246[[clang::annotate("ra8_internal")]] static void host_allocator(void)
248 void* p = malloc(16UL);
252/* Non-static, no header declares it: in scope, and a genuine gap. */
253void host_unpublished(void)
258/* RA8_PRIV inside tools/mod_host. Reaching this from another tool is the
259 same boundary violation as one library calling another's private helper. */
260[[clang::annotate("ra8_priv")]] void host_priv_helper(void);
262void host_priv_helper(void) {}
267 "apps/host/mod_alloc/src/host_alloc.c":
"""
268void* malloc(unsigned long n);
270[[clang::annotate("ra8_internal")]] static void host_product_allocator(void)
275 "apps/board/stand_alone/mod_alloc/src/board_alloc.c":
"""
276void* malloc(unsigned long n);
278[[clang::annotate("ra8_internal")]] static void board_product_allocator(void)
283 "apps/shared_libs/mod_alloc/src/shared_alloc.c":
"""
284void* malloc(unsigned long n);
286[[clang::annotate("ra8_internal")]] static void shared_product_allocator(void)
291 "apps/shared_libs/mod_alloc/tests/src/test_alloc.c":
"""
292void* malloc(unsigned long n);
295[[clang::annotate("ra8_internal")]] static void host_test_wrapper_allocator(void)
297 void* p = malloc(16UL);
306 "libs/ra8_c6link/src/ra8_media_download.pb-c.c":
"""
307void generated_unpublished(void) {}
309 "libs/ra8_c6link/src/future_generated.pb-c.c":
"""
310void future_generated_unpublished(void) {}
314 "apps/shared_libs/mod_product/src/product_core.c":
"""
315[[clang::annotate("ra8_priv")]] void product_priv_helper(void);
317void product_priv_helper(void) {}
322 "apps/host/mod_product/src/product_form.c":
"""
323[[clang::annotate("ra8_priv")]] void product_form_caller(void);
325void product_priv_helper(void);
327void product_form_caller(void)
329 product_priv_helper();
336 "apps/host/mod_stranger/src/stranger_form.c":
"""
337[[clang::annotate("ra8_priv")]] void stranger_form_caller(void);
339void product_priv_helper(void);
341void stranger_form_caller(void)
343 product_priv_helper();
348 "tools/mod_other_host/src/other_host.c":
"""
349[[clang::annotate("ra8_priv")]] void other_host_caller(void);
351void host_priv_helper(void);
353void other_host_caller(void)
359 "libs/mod_name/inc/mod_name.h":
"""
361void naming_public_entry(void);
362void internal_external_bad(void);
363void priv_public_bad(void);
364static void header_static_bad(void) {}
365static inline void naming_inline_public(void) {}
367 "libs/mod_name/src/mod_name_internal.h":
"""
369[[clang::annotate("ra8_priv")]] void priv_naming_good(void);
370[[clang::annotate("ra8_priv")]] void bad_priv_name(void);
372 "libs/mod_name/src/naming.c":
"""
374#include "mod_name_internal.h"
376[[clang::annotate("ra8_internal")]] static void internal_naming_good(void) {}
377static void internal_missing_annotation(void) {}
378[[clang::annotate("ra8_internal")]] static void wrong_internal_name(void) {}
379[[clang::annotate("ra8_internal")]] static void s_bad_function(void) {}
380[[clang::annotate("ra8_internal")]] void internal_annotated_external_bad(void) {}
381[[clang::annotate("ra8_internal")]]
382[[clang::annotate("ra8_priv")]] static void internal_conflict_bad(void) {}
383[[clang::annotate("ra8_test_helper")]] static void internal_static_test_helper_bad(void) {}
385static int s_good_data;
386static int bad_static_data;
388void priv_naming_good(void) {}
389void bad_priv_name(void) {}
390[[clang::annotate("ra8_priv")]] void priv_missing_header(void) {}
391void internal_external_bad(void) {}
392void priv_public_bad(void) {}
393[[clang::annotate("ra8_test_helper")]] void naming_test_helper_good(void) {}
395void naming_public_entry(void)
398 internal_naming_good();
399 internal_missing_annotation();
400 wrong_internal_name();
402 internal_annotated_external_bad();
403 internal_conflict_bad();
404 internal_static_test_helper_bad();
405 naming_test_helper_good();
406 naming_inline_public();
407 s_good_data = s_bad_local;
408 bad_static_data = s_good_data;
411 "libs/mod_name/src/naming_cpp.cpp":
"""
413void anonymous_namespace_bad() {}
414[[clang::annotate("ra8_internal")]] static void internal_cpp_good() {}
418 "tests/mocks/inc/mock_contract.h":
"#pragma once\n",
419 "tests/support/inc/support_contract.h":
"""
421void support_public_entry(void);
423 "tests/consumer/src/test_support_include.c":
"""
424#include "support_contract.h"
425void support_public_entry(void) {}
427 "examples/board/state/demo/inc/demo_contract.h":
"#pragma once\n",
428 "tests/mocks/src/mock_contract_internal.h":
"#pragma once\n",
432 "tests/plain/src/plain_header.h":
"#pragma once\n",
433 "tests/build/generated/inc/ignored_contract.h":
"#pragma once\n",
442_SELFTEST_LINKAGE_EXPECTED = frozenset(
444 "future_generated_unpublished",
447 "link_internal_declared",
454_SELFTEST_LOCK_EXPECTED = frozenset({
"lock_bare_caller",
"lock_wrong_name_caller"})
459_SELFTEST_LINKAGE_CLEAN = (
461 "link_internal_annotated",
469 "support_public_entry",
475def _selftest_parse(root: pathlib.Path) -> WalkState:
476 """Write the synthetic tree under ``root`` and walk every TU in it."""
477 for rel, body
in _SELFTEST_SOURCES.items():
479 path.parent.mkdir(parents=
True, exist_ok=
True)
480 path.write_text(body)
485 tu_paths = discover_translation_units()
487 include_args = [f
"-I{path}" for path
in _first_party_include_roots()]
489 index = cindex.Index.create()
490 for path
in tu_paths:
492 [
"-std=c++23",
"-x",
"c++"]
if path.suffix ==
".cpp" else [
"-std=c23",
"-x",
"c"]
496 args=[*language_args, *include_args],
497 options=cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD,
503def _names_matching(violations: list[Violation], rule: str, pattern: str) -> set[str]:
504 """Pull the symbol names out of every ``rule`` finding matching ``pattern``."""
505 out: set[str] = set()
509 m = re.search(pattern, v.message)
515def _check_priv_namesakes(
516 violations: list[Violation], symbols: dict[str, AnnotatedSymbol]
518 """RA8_PRIV must separate namesakes by USR without going toothless."""
520 pathlib.Path(v.file).name
522 if v.rule ==
"ra8_priv" and "called from outside" in v.message
524 failures: list[str] = []
525 if "other.c" in offenders:
527 "namesake regression: a module's call to its own file-local static "
528 "'shared_helper' was reported as a cross-module RA8_PRIV call"
530 if "priv.c" in offenders:
532 "same-module regression: mod_priv calling its own RA8_PRIV symbol was reported"
534 if "stranger.c" not in offenders:
536 "ra8_priv went toothless: the genuine cross-module call to RA8_PRIV "
537 "'shared_helper' from mod_stranger was NOT reported"
539 if "other_host.c" not in offenders:
541 "ra8_priv is blind under tools/: mod_other_host calls mod_host's "
542 "RA8_PRIV 'host_priv_helper' across a module boundary and it was NOT "
543 "reported -- module_of() is not resolving tools/<tool> as a module, so "
544 "every RA8_PRIV tag under tools/ is decorative"
546 if "product_form.c" in offenders:
548 "build-form regression: apps/host/mod_product driving its own "
549 "core's RA8_PRIV 'product_priv_helper' was reported -- module_of() is "
550 "keying apps/ on the category, so a product split across build forms "
551 "reads as two libraries"
553 if "stranger_form.c" not in offenders:
555 "ra8_priv went toothless under apps/: mod_stranger reaches into "
556 "mod_product's core RA8_PRIV 'product_priv_helper' and it was NOT "
557 "reported -- dropping the category from the module key must not drop "
558 "the boundary between two products"
563 namesakes = [s
for s
in symbols.values()
if s.name ==
"shared_helper"]
564 expected_namesakes = 2
565 if len(namesakes) != expected_namesakes:
567 f
"symbol table merged namesakes: expected {expected_namesakes} distinct "
568 f
"'shared_helper' entries, found {len(namesakes)}"
573def _check_naming_contract(violations: list[Violation]) -> list[str]:
574 """Naming/linkage prefixes must agree with AST storage and scope."""
575 fixture = [v
for v
in violations
if "mod_name" in pathlib.Path(v.file).parts]
580 for finding
in fixture
581 if finding.rule ==
"ra8_naming"
582 if (match := re.search(
r"'([^']+)'", finding.message))
is not None
585 "internal_missing_annotation",
586 "wrong_internal_name",
588 "internal_annotated_external_bad",
591 "priv_missing_header",
592 "internal_external_bad",
595 "internal_conflict_bad",
596 "internal_static_test_helper_bad",
597 "anonymous_namespace_bad",
600 "internal_naming_good",
603 "naming_inline_public",
605 "naming_test_helper_good",
608 f
"ra8_naming went toothless: broken fixture '{name}' was not reported"
609 for name
in sorted(expected - naming)
612 f
"ra8_naming false positive: conforming fixture '{name}' was reported"
613 for name
in sorted(clean & naming)
618 for finding
in fixture
619 if finding.rule ==
"ra8_naming" and "RA8_PRIV function" in finding.message
620 if (match := re.search(
r"'([^']+)'", finding.message))
is not None
622 if "bad_priv_name" not in priv:
623 failures.append(
"ra8_priv accepted a module-private function without the priv_ prefix")
624 if "priv_naming_good" in priv:
625 failures.append(
"ra8_priv rejected the conforming non-static priv_ fixture")
629def _check_linkage(violations: list[Violation]) -> list[str]:
630 """The linkage rule must catch both gap shapes and exempt only tabled handlers."""
631 linkage = _names_matching(violations,
"ra8_linkage",
r"'([^']+)'")
633 f
"ra8_linkage went toothless: '{name}' has external linkage that "
634 f
"nothing justifies, and the rule did not report it"
635 for name
in sorted(_SELFTEST_LINKAGE_EXPECTED - linkage)
638 f
"ra8_linkage false positive: '{name}' is a justified definition but the rule reported it"
639 for name
in sorted(linkage - _SELFTEST_LINKAGE_EXPECTED)
641 if "handler_untabled" not in linkage:
643 "vector-table exemption over-matches: 'handler_untabled' is byte-identical "
644 "to a tabled handler but appears in no table, so it must still be reported"
649def _check_expects_lock(violations: list[Violation]) -> list[str]:
650 """RA8_EXPECTS_LOCK must fire on an unheld call and stay quiet on a held one.
652 The "quiet" direction is the one that matters here: the rule shipped for
653 the life of the tree keyed on a ``RA8_TAKE_LOCK`` call that no first-party
654 file could produce, so EVERY caller of an annotated function was a
655 violation and the annotation had to go unused to keep the gate green. A
656 rule nobody can satisfy and a rule nobody wrote are indistinguishable from
659 callers = _names_matching(violations,
"ra8_expects_lock",
r"from '([^']+)'")
661 f
"ra8_expects_lock went toothless: '{name}' reaches a guarded body "
662 f
"without holding the named lock and was not reported"
663 for name
in sorted(_SELFTEST_LOCK_EXPECTED - callers)
666 f
"ra8_expects_lock false positive: '{name}' holds the named lock "
667 f
"(RA8_OWNS_RESOURCE) or was entered under it (RA8_EXPECTS_LOCK), "
668 f
"which is exactly how the macro says the contract is met"
669 for name
in sorted(callers - _SELFTEST_LOCK_EXPECTED)
674def _check_rule3(violations: list[Violation]) -> list[str]:
675 """NASA P10 Rule 3, both axes: the waiver and the firmware/host boundary."""
676 allocators = _names_matching(violations,
"ra8_nasa_rule_3_ok",
r"from '([^']+)'")
677 failures: list[str] = []
678 if "fw_untagged_allocator" not in allocators:
680 "ra8_nasa_rule_3_ok went toothless: firmware function "
681 "'fw_untagged_allocator' calls malloc/free with no waiver and was not reported"
683 if "fw_waived_allocator" in allocators:
685 "ra8_nasa_rule_3_ok false positive: 'fw_waived_allocator' carries "
686 "RA8_NASA_RULE_3_OK, which is exactly the documented waiver"
688 if "host_allocator" in allocators:
690 "ra8_nasa_rule_3_ok false positive: 'host_allocator' is under tools/, "
691 "which the host toolchain compiles and no firmware image contains -- "
692 "Rule 3 is a claim about firmware"
694 if "host_product_allocator" in allocators:
695 failures.append(
"ra8_nasa_rule_3_ok false positive: apps/host is a hosted product form")
696 if "host_test_wrapper_allocator" in allocators:
698 "ra8_nasa_rule_3_ok false positive: a host test wrapper under "
699 "apps/shared_libs/.../tests cannot enter a firmware image"
702 "ra8_nasa_rule_3_ok scope regression: "
703 f
"'{name}' can be linked into firmware but was treated as host-only"
704 for name
in (
"board_product_allocator",
"shared_product_allocator")
705 if name
not in allocators
710def _check_fixtures_parsed(symbols: dict[str, AnnotatedSymbol]) -> list[str]:
711 """Every fixture the clean-shape assertions rely on must have parsed."""
712 seen = {s.name
for s
in symbols.values()}
714 f
"selftest fixture did not parse: '{name}' is missing from the "
715 f
"symbol table, so its linkage shape was never exercised"
716 for name
in _SELFTEST_LINKAGE_CLEAN
721def _check_generated_scope(symbols: dict[str, AnnotatedSymbol]) -> list[str]:
722 """Exact generated files stay out while generated-looking neighbors stay in."""
723 seen = {symbol.name
for symbol
in symbols.values()}
724 failures: list[str] = []
725 if "generated_unpublished" in seen:
727 "generated-source exclusion failed: the exact protoc-c output was parsed as "
728 "hand-authored first-party code"
730 if "future_generated_unpublished" not in seen:
732 "generated-source exclusion over-matches: an unclassified neighboring pb-c.c "
733 "file disappeared from the annotation scope"
738def _check_include_root_discovery(root: pathlib.Path) -> list[str]:
739 """Public ``inc`` and sanctioned private ``src`` roots survive any depth."""
740 actual = {path.relative_to(root).as_posix()
for path
in _first_party_include_roots()}
742 "examples/board/state/demo/inc",
752 f
"include-root discovery missed conventional path '{path}'"
753 for path
in sorted(expected - actual)
755 forbidden = {
"tests/build/generated/inc",
"tests/plain/src"}
757 f
"include-root discovery accepted excluded or non-private path '{path}'"
758 for path
in sorted(forbidden & actual)
763def run_selftest() -> int:
764 """Regression-test the checker itself. Returns a process exit code.
766 Four classes of defect are guarded, all of which this gate has shipped:
768 * **Namesake merging.** Keying the symbol table by bare name merged
769 distinct same-named functions into one entry, so a module calling its
770 own file-local ``static`` was reported for calling another module's
772 * **A rule that cannot fire.** The linkage rule turns "nothing declares
773 this" into a failure, and a rule which reports nothing looks exactly
774 like a clean tree. The synthetic module contains one definition of
775 every passing shape and one of every failing shape.
776 * **A root silently out of scope.** ``tools/`` was absent from SCAN_DIRS,
777 so ra8_emulator, mdl and ra8_viewer were never checked and the gate
778 reported a clean tree over code it had not read. The ``tools/`` fixture
779 pins the scope from inside the rules rather than by reading the
780 constant: ``host_unpublished`` is only reachable if is_first_party()
782 * **A rule that cannot be satisfied.** ``RA8_EXPECTS_LOCK`` demanded a
783 ``RA8_TAKE_LOCK`` call that exists nowhere in this tree and could not,
784 since callee names resolve after macro expansion -- so the annotation
785 was unusable and went unused, and the gate looked clean because nobody
786 could adopt the rule. The lock fixture asserts both directions.
787 * **NASA P10 Rule 3 scope and waiver.** Rule 3 is a claim about firmware,
788 so it is asserted along both axes -- untagged firmware allocation
789 fires, the documented waiver does not, and host-only code under
790 ``tools/`` does not. Getting the second axis wrong is what turns a real
791 gate into 216 findings nobody can act on.
792 * **Generated-source scope.** The exact reproducible protoc-c output is
793 excluded by the lint-coverage registry, while an unclassified neighboring
794 ``*.pb-c.c`` remains ordinary first-party C and must still be judged.
795 * **Include-root discovery.** Public ``inc/`` directories at multiple
796 product depths and sanctioned ``src/*_internal.h`` directories are found,
797 while build output and an ordinary misplaced ``src`` header stay out.
799 with tempfile.TemporaryDirectory()
as td:
800 root = pathlib.Path(td).resolve()
801 with override_repo_root(root):
802 state = _selftest_parse(root)
803 include_root_failures = _check_include_root_discovery(root)
804 violations = enforce_rules(
806 naming_contract=
True,
810 *_check_priv_namesakes(violations, state.symbols),
811 *_check_linkage(violations),
812 *_check_expects_lock(violations),
813 *_check_rule3(violations),
814 *_check_naming_contract(violations),
815 *_check_fixtures_parsed(state.symbols),
816 *_check_generated_scope(state.symbols),
817 *include_root_failures,
820 *run_loopbound_selftest(),
824 sys.stderr.write(f
"[FAIL] check_annotations selftest: {f}\n")
825 sys.stderr.write(f
"check_annotations selftest: {len(failures)} failure(s)\n")
828 "check_annotations selftest: OK (namesakes resolved by USR; linkage rule "
829 "catches both gap shapes, reaches tools/, and exempts only tabled handlers; "
830 "NASA rule 3 fires on untagged firmware allocation and stays quiet on the "
831 "documented waiver and on host-only code; RA8_EXPECTS_LOCK fires on an "
832 "unheld call and stays quiet on RA8_OWNS_RESOURCE / propagated holders; "
833 "the exact generated protoc-c source is excluded while an unclassified "
834 "pb-c.c neighbor remains in scope; "
835 "recursive inc/ and sanctioned private src/ include roots are discovered; "
836 "linkage prefixes agree with static/data scope and their annotations; "
837 "loop-bound scan fires on a "
838 "mis-attached marker and a stale RA8_BOUNDED_LOOP statement, stays quiet on "
839 "correct markers and on #define/comment/string mentions)"