4828dc754d
gates / gates (push) Successful in 4s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""R-760: every catalog service that has NO compose `healthcheck:` says why, in a comment inside its own block.
|
|
|
|
A missing healthcheck is sometimes right (the image has no shell; the image brings its own — adventurelog-frontend,
|
|
R-655), but a silent one cannot be told apart from a forgotten one: vikunja carried none for months with nothing
|
|
saying whether that was a choice. This test reads the committed composes as TEXT (catalog CI has no PyYAML) and
|
|
fails for a service block with neither a `healthcheck:` key nor a `# No healthcheck` comment.
|
|
|
|
COMPANION RED-PROOF: with the R-760 comment removed from templates/vikunja/docker-compose.yml this test fails and
|
|
names `vikunja/vikunja`."""
|
|
import glob
|
|
import io
|
|
import os
|
|
import re
|
|
import unittest
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
EXPLAINED = re.compile(r"^\s*#\s*No healthcheck", re.I)
|
|
|
|
|
|
def service_blocks(text):
|
|
"""Yield (service name, [lines]) for each service under the top-level `services:` key."""
|
|
in_services = False
|
|
name, lines = None, []
|
|
for line in text.splitlines():
|
|
if re.match(r"^\S", line): # a top-level key (or a top-level comment) ends the services block
|
|
if name:
|
|
yield name, lines
|
|
name, lines = None, []
|
|
in_services = line.startswith("services:")
|
|
continue
|
|
if not in_services:
|
|
continue
|
|
m = re.match(r"^ ([A-Za-z0-9_.-]+):\s*$", line)
|
|
if m:
|
|
if name:
|
|
yield name, lines
|
|
name, lines = m.group(1), []
|
|
elif name:
|
|
lines.append(line)
|
|
if name:
|
|
yield name, lines
|
|
|
|
|
|
def unexplained(text):
|
|
out = []
|
|
for svc, lines in service_blocks(text):
|
|
if any(re.match(r"^ healthcheck:", l) for l in lines):
|
|
continue
|
|
if any(EXPLAINED.match(l) for l in lines):
|
|
continue
|
|
out.append(svc)
|
|
return out
|
|
|
|
|
|
class HealthcheckExplained(unittest.TestCase):
|
|
def test_parser_sees_a_missing_and_an_explained_service(self):
|
|
# decoys for the parser itself: a silent service convicts, an explained one and a checked one pass
|
|
text = ("services:\n a:\n image: x:1\n b:\n image: y:1\n # No healthcheck: no shell\n"
|
|
" c:\n image: z:1\n healthcheck:\n test: [\"CMD\", \"true\"]\nvolumes:\n d:\n")
|
|
self.assertEqual(unexplained(text), ["a"])
|
|
|
|
def test_every_catalog_service_has_a_healthcheck_or_says_why(self):
|
|
paths = sorted(glob.glob(os.path.join(ROOT, "templates", "*", "docker-compose.yml")))
|
|
self.assertGreater(len(paths), 40, "the template glob found too few composes — the test would pass empty")
|
|
bad = []
|
|
for p in paths:
|
|
app = os.path.basename(os.path.dirname(p))
|
|
with io.open(p, encoding="utf-8") as f:
|
|
text = f.read()
|
|
for svc in unexplained(text):
|
|
bad.append("%s/%s" % (app, svc))
|
|
self.assertEqual(bad, [], "service(s) with no compose healthcheck and no '# No healthcheck' comment "
|
|
"saying why: %s" % bad)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|