3"""Explicit both-direction assertions for external-tool suppression syntax."""
5from __future__
import annotations
11from pathlib
import Path
13from selftest_assert
import expect
14from suppression_model
import Inventory, Suppression
16EXPECTED_REGION_ROWS = 2
17EXPECTED_HADOLINT_ROWS = 4
18EXPECTED_MARKDOWN_CONFIG_ROWS = 2
19REPO_ROOT = Path(__file__).resolve().parents[2]
20SHFMT_TIMEOUT_SECONDS = 10
22TOOL_CONTROL_FIXTURES = {
23 "docs/markdownlint_configure.md": (
24 """<!-- Suppression rationale: fixed input. -->
25<!-- markdownlint-configure-file
33<span>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
36 "docs/markdownlint_configure_true.md": (
37 """<!-- markdownlint-configure-file { "MD013": true } -->
41 "docs/markdownlint_configure_empty.md": (
42 """<!-- markdownlint-configure-file {} -->
43<!-- markdownlint-configure-file { "MD013": {} } -->
44# Empty configuration fixture
47 "docs/markdownlint_configure_options.md": (
48 """<!-- Suppression rationale: widens generated lines. -->
49<!-- markdownlint-configure-file
59 "docs/markdownlint_configure_mixed.md": (
60 """<!-- Suppression rationale: generated content needs two relaxations. -->
61<!-- markdownlint-configure-file
76 "docs/markdownlint_active.md": (
79<span>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</span>
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")
91 "must fire: clang-format-22 is available for behavioral probes",
95 version = subprocess.run(
100 timeout=SHFMT_TIMEOUT_SECONDS,
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)
106 probe = subprocess.run(
107 [tool,
"--style=LLVM",
"apps/fixture/src/clang_empty_colon_control.c"],
112 timeout=SHFMT_TIMEOUT_SECONDS,
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",
123def _assert_c_controls(
124 root: Path, by_path: dict[str, list[Suppression]], failures: list[str]
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]}
130 {
"clang-format off",
"clang-format on"} <= c_directives,
131 "must fire: exact lowercase clang-format region delimiters",
134 colon_rows = by_path[
"apps/fixture/src/clang_colon_control.c"]
136 len(colon_rows) == EXPECTED_REGION_ROWS
and bool(colon_rows[0].reason),
137 "must fire: clang-format's behaviorally supported colon rationale",
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")
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",
147 _assert_clang_format_behavior(root, failures)
158 "no_forward_declare",
163 item.directive.removeprefix(
"IWYU pragma: ")
164 for item
in by_path[c_path]
165 if item.tool ==
"iwyu"
167 expect(iwyu_commands == seen_iwyu,
"must fire: every documented IWYU pragma", failures)
170def _assert_yamllint(by_path: dict[str, list[Suppression]], failures: list[str]) ->
None:
171 """Assert yamllint's file, line, region, and repeated-rule forms."""
173 item.directive
for item
in by_path[
"format_controls.yml"]
if item.tool ==
"yamllint"
176 {
"yamllint disable",
"yamllint disable-line",
"yamllint enable"} == yaml_directives,
177 "must fire: yamllint line and region spellings",
180 disable_file = [item
for item
in by_path[
"disable_file.yml"]
if item.tool ==
"yamllint"]
182 len(disable_file) == 1
and disable_file[0].directive ==
"yamllint disable-file",
183 "must fire: first-line yamllint disable-file",
188 (item.directive, item.rule)
189 for item
in by_path[
"format_controls.yml"]
190 if item.tool ==
"yamllint"
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"),
200 "must fire: space-separated repeated yamllint rule tokens",
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:
212 pin_match
is not None and tool
is not None,
213 "must fire: pinned shfmt is available for behavioral probes",
217 version = subprocess.run(
222 timeout=SHFMT_TIMEOUT_SECONDS,
224 pinned = version.returncode == 0
and version.stdout.strip().removeprefix(
226 ) == pin_match.group(1)
227 expect(pinned,
"must fire: behavioral probes use the project-pinned shfmt", failures)
231 "generated/ignored.sh",
232 "generated/mixed-case.sh",
233 "generated/false.sh",
234 "generated/unset.sh",
235 "bad/generated/uppercase-value.sh",
237 probe = subprocess.run(
238 [tool,
"--apply-ignore",
"-l", *paths],
243 timeout=SHFMT_TIMEOUT_SECONDS,
245 formatted = set(probe.stdout.splitlines())
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",
252 formatted == set(paths[2:]),
253 "must fire: pinned shfmt rejects uppercase true and keeps false/unset active",
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:
265 pin_match
is not None and tool
is not None,
266 "must fire: pinned Hadolint is available for behavioral probes",
270 version = subprocess.run(
275 timeout=SHFMT_TIMEOUT_SECONDS,
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)
283 [tool,
"--format",
"json", path],
288 timeout=SHFMT_TIMEOUT_SECONDS,
290 for path
in (
"Dockerfile",
"Dockerfile.hadolint-active")
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()
298 probes[0].returncode == 0
300 and probes[1].returncode == 1
301 and active_codes == {
"DL3003",
"SC2164"},
302 "quiet: Hadolint accepts whitespace around equals and commas",
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:
314 pin_match
is not None and tool
is not None,
315 "must fire: pinned cmake-lint is available for probes",
319 version = subprocess.run(
324 timeout=SHFMT_TIMEOUT_SECONDS,
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)
337 timeout=SHFMT_TIMEOUT_SECONDS,
339 for path
in (
"cmake_lint_space.cmake",
"cmake_lint_tab.cmake")
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",
352def _assert_build_controls(
354 inventory: Inventory,
355 by_path: dict[str, list[Suppression]],
358 """Assert Hadolint, CMake, and shfmt's EditorConfig controls."""
361 for path
in (
"Dockerfile",
"Dockerfile.global")
362 for item
in by_path[path]
363 if item.tool ==
"hadolint"
365 hadolint_rules = {(item.path, item.rule)
for item
in hadolint_rows}
367 len(hadolint_rows) == EXPECTED_HADOLINT_ROWS
370 (
"Dockerfile",
"DL3003"),
371 (
"Dockerfile",
"SC2164"),
372 (
"Dockerfile.global",
"DL3008"),
373 (
"Dockerfile.global",
"SC1091"),
375 "must fire: spaced Hadolint lists normalize to one row per rule",
378 _assert_hadolint_behavior(root, failures)
379 cmake_directives = {item.directive
for item
in by_path[
"format_controls.cmake"]}
384 "cmake-lint disable",
389 "must fire: real CMake formatter regions and lint disable pragma",
392 cmake_space = [item
for item
in by_path[
"cmake_lint_space.cmake"]
if item.tool ==
"cmake-lint"]
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",
399 _assert_cmake_lint_behavior(root, failures)
400 shfmt = [item
for item
in by_path[
".editorconfig"]
if item.tool ==
"shfmt"]
402 {item.rule
for item
in shfmt} == {
"[generated/**]",
"[generated/mixed-case.sh]"},
403 "must fire: case-insensitive shfmt ignore=true properties are controls",
407 not any(item.path ==
".editorconfig" for item
in inventory.findings),
408 "quiet: valid shfmt false and unset properties add no findings",
411 _assert_shfmt_behavior(root, failures)
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")
419 configured, active = [
426 timeout=SHFMT_TIMEOUT_SECONDS,
428 for path
in (
"docs/markdownlint_configure.md",
"docs/markdownlint_active.md")
430 active_output = active.stdout + active.stderr
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",
441def _assert_doxygen_controls(
442 inventory: Inventory,
443 by_path: dict[str, list[Suppression]],
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"]
450 len(markdown_doxygen) == EXPECTED_REGION_ROWS,
451 "quiet: Markdown HTML-comment Doxygen commands are inactive",
454 documentation_paths = {item.path
for item
in inventory.suppressions
if item.tool ==
"doxygen"}
457 "apps/fixture/src/format_controls.c",
458 "docs/format_controls.dox",
459 "docs/format_controls.md",
460 "scripts/checks/suppression_selftest.py",
462 <= documentation_paths,
463 "must fire: Doxygen controls in every configured Doxyfile language",
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"]
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",
476 not any(item.path == vendor_path
for item
in inventory.findings),
477 "quiet: vendored Doxygen conditionals are exempt, not unknown",
482def _assert_invalid_markdownlint_configures(inventory: Inventory, failures: list[str]) ->
None:
483 """Assert malformed JSON and unsupported values both fail closed."""
486 for item
in inventory.findings
487 if item.path ==
"docs/wrong_format_controls.md"
488 and "markdownlint-configure-file" in item.message
491 len(malformed) == EXPECTED_MARKDOWN_CONFIG_ROWS,
492 "must fire: malformed JSON and unsupported configure-file values fail closed",
497def _assert_markdownlint_configures(
498 inventory: Inventory,
499 by_path: dict[str, list[Suppression]],
502 """Assert configure-file values in both valid and invalid directions."""
503 disabled = by_path[
"docs/markdownlint_configure.md"]
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",
512 options = by_path[
"docs/markdownlint_configure_options.md"]
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",
521 mixed = by_path[
"docs/markdownlint_configure_mixed.md"]
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",
531 "docs/markdownlint_configure_true.md",
532 "docs/markdownlint_configure_empty.md",
535 all(
not by_path.get(path)
for path
in quiet_paths),
536 "quiet: true and empty configure-file values add no waiver rows",
539 valid_paths = quiet_paths | {
540 "docs/markdownlint_configure.md",
541 "docs/markdownlint_configure_options.md",
542 "docs/markdownlint_configure_mixed.md",
545 not any(item.path
in valid_paths
for item
in inventory.findings),
546 "quiet: every valid configure-file object has zero integrity findings",
549 _assert_invalid_markdownlint_configures(inventory, failures)
552def _assert_documentation_controls(
553 root: Path, inventory: Inventory, by_path: dict[str, list[Suppression]], failures: list[str]
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}
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",
570 <= markdown_controls,
571 "must fire: configured Prettier and every markdownlint control spelling",
574 _assert_markdownlint_configures(inventory, by_path, failures)
575 _assert_doxygen_controls(inventory, by_path, failures)
577 not any(item.path ==
"docs/format_controls.md" for item
in inventory.findings),
578 "quiet: valid Markdown controls have zero integrity findings",
581 _assert_markdownlint_behavior(root, failures)
584def _assert_invalid_controls(inventory: Inventory, failures: list[str]) ->
None:
585 """Assert wrong syntax stays inactive and every invalid region fails closed."""
588 "apps/fixture/src/wrong_controls.c",
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",
597 not any(item.path
in invalid_paths
for item
in inventory.suppressions),
598 "quiet: wrong case, suffix, separator, argument, and language never count",
603 for item
in inventory.findings
604 if item.code
in {
"malformed-tool-config",
"malformed-tool-control"}
607 invalid_paths <= malformed_paths,
608 "must fire: every invalid tool-control fixture fails closed",
613 "apps/fixture/src/unmatched_controls.c",
614 "docs/unmatched_format_controls.dox",
615 "unmatched_format_controls.cmake",
616 "unmatched_format_controls.yml",
618 region_paths = {item.path
for item
in inventory.findings
if "region" in item.code}
620 unmatched_paths <= region_paths,
621 "must fire: unmatched clang-format, IWYU, Doxygen, CMake, and yamllint regions",
626def _assert_rationales(inventory: Inventory, failures: list[str]) ->
None:
627 """Assert the exact controls whose active syntax carries no rationale."""
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}
646 (
"apps/fixture/src/clang_empty_colon_control.c",
"clang-format",
"region-start"),
647 (
"disable_file.yml",
"yamllint",
"file"),
649 "must fire: empty-colon off and first-line disable-file have blank reasons",
654def assert_tool_control_syntax_awareness(
655 root: Path, inventory: Inventory, failures: list[str]
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)