Files
computer-playbook/tests/unit/utils/annotations/test_suppress.py
T
kevinveenbirkenbach 837d0c5335 test(suppression): unify # noqa / # nocheck marker grammar
Introduce a single grammar for per-line, per-block, and per-file
suppression markers across the test suite, replacing seven ad-hoc
syntaxes that had drifted apart.

Helper
  utils/annotations/suppress.py exposes is_suppressed_at,
  is_suppressed_in_head, is_suppressed_anywhere, line_has_rule, and
  suppressed_line_numbers. Markers may use either `noqa` or `nocheck`
  (case-insensitive synonyms), live inside `#`, `//`, `{# ... #}`, or
  `<\!-- ... -->` comments, and combine multiple kebab-case rules
  comma-separated. 17 unit tests cover the grammar in isolation.

Refactored consumers
  - tests/external/repository/test_urls_reachable.py (rule: url)
  - tests/external/docker/test_image_versions.py via
    utils/docker/version_updater.py (rule: docker-version)
  - tests/lint/repository/test_no_direct_yaml_calls.py (rule:
    direct-yaml)
  - tests/lint/ansible/test_service_shared_consistency.py (rule:
    shared)
  - tests/lint/ansible/roles/test_web_app_email_integration.py (rule:
    email)
  - tests/lint/repository/test_python_file_size.py (rule: file-size)
  - tests/lint/ansible/test_run_once_tags.py +
    tests/integration/roles/run_once/test_schema.py (rules: run-once,
    run-once-suffix)

Marker migration
  12 role tasks/main.yml files: `# run_once_<key>: deactivated`
  -> `# nocheck: run-once`. roles/web-app-suitecrm/meta/services.yml
  drops a malformed inline `noqa: intentional legacy pin` from a TODO
  comment so the new parser does not pick it up as a stray rule.

Caching wins
  Test files that previously loaded YAML / read files via raw
  yaml.safe_load + Path.read_text now route through utils.cache.yaml
  (load_yaml_any / load_yaml_str) and utils.cache.files.read_text:
    - test_service_shared_consistency.py
    - test_web_app_email_integration.py
    - tests/integration/roles/run_once/test_schema.py
    - utils/docker/version_updater.py
  This both speeds up repeated reads and stays consistent with the
  direct-yaml lint policy.

Documentation
  New SPOT page at docs/contributing/actions/testing/suppression.md
  with grammar, position semantics, the catalog of rules, examples,
  and an "adding a new rule" checklist. lint.md and external.md drop
  their per-rule tables and link to the new page. All touched docs
  conform to docs/contributing/documentation.md (no em-dash thought
  breaks, no SPOT self-labels, RFC-2119 keywords used consistently).

Settings sort tooling
  scripts/lint/sort_claude_settings.sh wraps utils/claude/settings_sort.py
  and is wired into scripts/lint/autoformat.sh. The tooling fixes the
  ASCII-ascending requirement enforced by
  tests/lint/repository/test_claude_settings_sorted.py; the test's
  failure message now points operators at the script. .claude/settings.json
  is committed in sorted form.
2026-04-30 11:54:46 +02:00

107 lines
4.0 KiB
Python

"""Unit tests for the unified suppression-marker grammar."""
from __future__ import annotations
import unittest
from utils.annotations.suppress import (
is_suppressed_anywhere,
is_suppressed_at,
is_suppressed_in_head,
line_has_rule,
suppressed_line_numbers,
)
class TestLineHasRule(unittest.TestCase):
def test_noqa_and_nocheck_are_synonyms(self):
self.assertTrue(line_has_rule("# noqa: url", "url"))
self.assertTrue(line_has_rule("# nocheck: url", "url"))
def test_case_insensitive(self):
self.assertTrue(line_has_rule("# NOQA: URL", "url"))
self.assertTrue(line_has_rule("# NoCheck: Url", "url"))
def test_html_jinja_and_slash_prefixes(self):
self.assertTrue(line_has_rule("<!-- nocheck: url -->", "url"))
self.assertTrue(line_has_rule("{# nocheck: url #}", "url"))
self.assertTrue(line_has_rule("// noqa: url", "url"))
def test_inline_marker_in_code(self):
line = 'tokens.append("https://example.org") # nocheck: url'
self.assertTrue(line_has_rule(line, "url"))
def test_multi_rule_comma_separated(self):
line = "# noqa: shared, email"
self.assertTrue(line_has_rule(line, "shared"))
self.assertTrue(line_has_rule(line, "email"))
self.assertFalse(line_has_rule(line, "url"))
def test_unrelated_text_does_not_match(self):
self.assertFalse(line_has_rule("# this is just a comment", "url"))
self.assertFalse(line_has_rule("noqa url", "url")) # missing colon
def test_word_boundary_avoids_false_positives(self):
self.assertFalse(line_has_rule("# nocheck: url-other", "url"))
self.assertTrue(line_has_rule("# nocheck: url-other", "url-other"))
class TestIsSuppressedAt(unittest.TestCase):
def test_same_line_mode(self):
lines = ["x = 1", "y = 2 # nocheck: url"]
self.assertTrue(is_suppressed_at(lines, 2, "url", mode="same-line"))
self.assertFalse(is_suppressed_at(lines, 1, "url", mode="same-line"))
def test_line_above_mode(self):
lines = ["# nocheck: docker-version", "version: 1.2.3"]
self.assertTrue(is_suppressed_at(lines, 2, "docker-version", mode="line-above"))
self.assertFalse(is_suppressed_at(lines, 2, "docker-version", mode="same-line"))
def test_line_above_skips_blank_lines(self):
lines = ["# noqa: email", "", "", "email:"]
self.assertTrue(is_suppressed_at(lines, 4, "email", mode="line-above"))
def test_default_mode_accepts_either(self):
same = ["foo # nocheck: url"]
above = ["# nocheck: url", "foo"]
self.assertTrue(is_suppressed_at(same, 1, "url"))
self.assertTrue(is_suppressed_at(above, 2, "url"))
def test_out_of_range_returns_false(self):
self.assertFalse(is_suppressed_at([], 1, "url"))
self.assertFalse(is_suppressed_at(["x"], 0, "url"))
self.assertFalse(is_suppressed_at(["x"], 999, "url"))
class TestFileLevelHelpers(unittest.TestCase):
def test_head_marker_within_scan_window(self):
lines = ['"""docstring."""', "# nocheck: file-size", "rest = 1"]
self.assertTrue(is_suppressed_in_head(lines, "file-size", scan_lines=30))
def test_head_marker_outside_scan_window_misses(self):
lines = ["x = 1"] * 40 + ["# nocheck: file-size"]
self.assertFalse(is_suppressed_in_head(lines, "file-size", scan_lines=30))
def test_anywhere_marker(self):
lines = ["x = 1"] * 100 + ["# nocheck: run-once"]
self.assertTrue(is_suppressed_anywhere(lines, "run-once"))
def test_anywhere_no_match(self):
self.assertFalse(is_suppressed_anywhere(["x", "y"], "run-once"))
class TestSuppressedLineNumbers(unittest.TestCase):
def test_returns_one_based_line_numbers(self):
lines = [
"# unrelated",
"# noqa: url",
"x # nocheck: url",
"# noqa: shared",
]
self.assertEqual(suppressed_line_numbers(lines, "url"), {2, 3})
self.assertEqual(suppressed_line_numbers(lines, "shared"), {4})
if __name__ == "__main__":
unittest.main()