Files
computer-playbook/tests/integration/infrastructure/docker/test_compose_templates.py
T
kevinveenbirkenbachandClaude Opus 4.7 a5ba14d5b3 test(lint/cache-read): extend scope to all tests/ + convert callers
Widens the cache-read lint guard from ``tests/lint/`` to every ``.py``
under ``tests/``. Integration tests show the same compounding cost
pattern (one role's compose template / nginx conf / scrape fragment
read across many shape tests) that motivated the original lint for
lint/.

Converts 91 violations across 28 test files:

- **Cache (utils.cache.files.read_text)** for project-tracked reads
  and single-read tempdir fixtures, e.g. test_prometheus.py (32x),
  test_keycloak_import_flow.py (11x), test_dashboard_oidc.py (3x),
  test_changelog_md.py (8x), test_package_mirror.py (8x), plus
  test_logging.py / test_summarize.py / test_version_updater.py /
  test_get_url_retry.py / test_pull_specific_host.py.

- **Cache + UnicodeDecodeError handler** for project-walk scanners
  that previously used ``errors="replace"`` / ``errors="ignore"`` to
  defend against non-UTF-8 files (compose volumes, password quoting,
  template image, mailu dependency, URL reachability). Wraps each
  read in ``try/except (OSError, UnicodeDecodeError): continue``,
  which has the same observable effect (skip undecodable files).

- **Refactor away the binary heuristic** in test_no_raw_docker.py:
  ``read_bytes() + b"\x00" not in raw`` becomes
  ``read_text() / except UnicodeDecodeError``. Same files skipped,
  with cache benefit.

- **Variable extraction** in test_changelog_md.py / test_package_mirror.py
  for cases where ``self.assertEqual(path.read_text(...), expected)``
  exceeded ruff's 88-char limit. Ruff was wrapping the marker onto a
  line outside the AST span of the read_text call.

- **Compute-from-setup** for two test methods that previously read
  ``self.changelog`` twice (before/after) to verify a no-op:
  ``before`` is now derived from the known input, dropping the first
  read.

Five ``# nocheck: cache-read`` markers remain, each annotated inline
with the reason: idempotency tests where the second read must hit
disk to catch non-idempotent regressions, and tests with intermediate
``write_text`` whose subsequent read would return stale cache data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:23:32 +02:00

85 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
import unittest
from typing import ClassVar
from utils.cache.files import read_text
from . import PROJECT_ROOT
class TestDockerComposeTemplates(unittest.TestCase):
# Search for all roles/*/templates/compose.yml.j2
TEMPLATE_PATTERN = "roles/*/templates/compose.yml.j2"
# Allowed lines before BASE_INCLUDE
ALLOWED_BEFORE_BASE: ClassVar[list[re.Pattern]] = [
re.compile(r"^\s*$"), # empty line
re.compile(r"^\s*version:.*$"), # version: ...
re.compile(r"^\s*#.*$"), # YAML comment
re.compile(r"^\s*\{\#.*\#\}\s*$"), # Jinja comment {# ... #}
]
BASE_INCLUDE = "{% include 'roles/sys-svc-compose/templates/base.yml.j2' %}"
NET_INCLUDE = "{% include 'roles/sys-svc-compose/templates/networks.yml.j2' %}"
HOST_MODE = 'network_mode: "host"'
def test_docker_compose_includes(self):
"""
Verifies for each found compose.yml.j2:
1. BASE_INCLUDE is present exactly once
2. If no host‑mode is set, NET_INCLUDE must appear exactly once
3. BASE_INCLUDE appears before NET_INCLUDE when both are required
4. Only allowed lines appear before BASE_INCLUDE (invalid lines issue warnings)
"""
template_paths = sorted(PROJECT_ROOT.glob(self.TEMPLATE_PATTERN))
self.assertTrue(
template_paths, f"No templates found for pattern {self.TEMPLATE_PATTERN}"
)
for template_path in template_paths:
with self.subTest(template=template_path):
content = read_text(str(template_path))
lines = content.splitlines()
# BASE_INCLUDE must always occur exactly once
count_base = lines.count(self.BASE_INCLUDE)
self.assertEqual(
count_base,
1,
f"{template_path}: '{self.BASE_INCLUDE}' occurs {count_base} times, expected once",
)
# Determine if host‑mode is in use
host_mode = self.HOST_MODE in content
# If not host‑mode, NET_INCLUDE must occur exactly once
count_net = lines.count(self.NET_INCLUDE)
if host_mode:
# No network include needed for host mode
self.assertEqual(
count_net,
0,
f"{template_path}: '{self.NET_INCLUDE}' should be omitted when using host networking",
)
else:
# Must include networks.yml exactly once
self.assertEqual(
count_net,
1,
f"{template_path}: '{self.NET_INCLUDE}' occurs {count_net} times, expected once",
)
# If both includes are present, check order
if count_base and count_net:
idx_base = lines.index(self.BASE_INCLUDE)
idx_net = lines.index(self.NET_INCLUDE)
self.assertLess(
idx_base,
idx_net,
f"{template_path}: '{self.BASE_INCLUDE}' must come before '{self.NET_INCLUDE}'",
)
if __name__ == "__main__":
unittest.main()