mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-09 12:17:13 +00:00
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.
107 lines
4.0 KiB
Python
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()
|