Files
computer-playbook/tests/unit/utils/github/profile/test_role_summary.py
T
kevinveenbirkenbachandClaude Opus 4.7 255c61eec3 fix(utils/github/profile): strip ANSIBLE_LOG_PATH prefix in role_summary
The previous regex anchored the `name` capture at line start and used
lazy `.+?`, which in production captured the full
`<ts> p=<pid> u=<user> n=ansible INFO| <role>` prefix as the role name
instead of just `<role>`. The CI step summary then rendered every row
with the timestamped log header and the `total` filter silently failed
(because `name` was no longer equal to `"total"`).

Add an optional non-capturing `(?:.*\bINFO\|\s+)?` group so the
ANSIBLE_LOG_PATH prefix is consumed when present and `name` captures
just the role token (or `role : task`). Test sample didn't carry the
log prefix, so the bug slipped through — add a prefixed-format test.

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

131 lines
4.9 KiB
Python

import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from utils.cache.files import read_text
from utils.github.profile import role_summary
SAMPLE_LOG = """\
PLAY RECAP **********************************************************************
host : ok=5 changed=2
Saturday 21 May 2026 10:00:00 +0000 (0:00:01.234) 0:01:23.456 ***********
===============================================================================
web-app-keycloak ------------------------------------------------------- 42.50s
sys-svc-mail ----------------------------------------------------------- 30.10s
web-app-keycloak ------------------------------------------------------- 12.40s
docker-compose --------------------------------------------------------- 8.00s
sys-svc-mail : Some task ----------------------------------------------- 3.00s
total ------------------------------------------------------------------ 93.00s
"""
def _write_tempfile(content: str, suffix: str) -> str:
fd, name = tempfile.mkstemp(suffix=suffix)
os.close(fd)
with Path(name).open("w", encoding="utf-8") as fh:
fh.write(content)
return name
class TestParseRoleTimes(unittest.TestCase):
def _write(self, content: str) -> Path:
name = _write_tempfile(content, ".log")
self.addCleanup(os.unlink, name)
return Path(name)
def test_aggregates_duplicate_roles(self):
path = self._write(SAMPLE_LOG)
rows = role_summary.parse_role_times(path)
names = [name for name, _ in rows]
self.assertEqual(names[0], "web-app-keycloak")
self.assertAlmostEqual(dict(rows)["web-app-keycloak"], 54.90)
def test_excludes_total_line(self):
path = self._write(SAMPLE_LOG)
rows = role_summary.parse_role_times(path)
self.assertNotIn("total", dict(rows))
def test_excludes_task_entries_with_colon(self):
path = self._write(SAMPLE_LOG)
rows = role_summary.parse_role_times(path)
for name, _ in rows:
self.assertNotIn(" : ", name)
def test_sorted_descending(self):
path = self._write(SAMPLE_LOG)
rows = role_summary.parse_role_times(path)
durations = [seconds for _, seconds in rows]
self.assertEqual(durations, sorted(durations, reverse=True))
def test_strips_ansi_escape_codes(self):
coloured = "\x1b[33mrole-x\x1b[0m ------------------------------ 5.00s\n"
path = self._write(coloured)
rows = role_summary.parse_role_times(path)
self.assertEqual(rows, [("role-x", 5.00)])
def test_empty_log_returns_empty_list(self):
path = self._write("nothing here\n")
rows = role_summary.parse_role_times(path)
self.assertEqual(rows, [])
def test_strips_ansible_log_prefix(self):
prefixed = (
"2026-05-22 15:23:23,180 p=985 u=root n=ansible INFO|"
" web-app-keycloak ------------------------------------ 42.50s\n"
"2026-05-22 15:23:24,000 p=985 u=root n=ansible INFO|"
" sys-svc-mail : Some task ---------------------------- 3.00s\n"
"2026-05-22 15:23:25,000 p=985 u=root n=ansible INFO|"
" total ----------------------------------------------- 93.00s\n"
)
path = self._write(prefixed)
rows = role_summary.parse_role_times(path)
names = [name for name, _ in rows]
self.assertEqual(names, ["web-app-keycloak"])
self.assertAlmostEqual(dict(rows)["web-app-keycloak"], 42.50)
class TestFormatTable(unittest.TestCase):
def test_renders_markdown_table(self):
rows = [("alpha", 10.5), ("beta", 7.25)]
out = role_summary._format_table(rows)
self.assertIn("| 1 | `alpha` | 10.50s |", out)
self.assertIn("| 2 | `beta` | 7.25s |", out)
self.assertIn("## ⏱️ Role runtimes", out)
def test_renders_all_rows(self):
rows = [("a", 5.0), ("b", 4.0), ("c", 3.0)]
out = role_summary._format_table(rows)
self.assertIn("`a`", out)
self.assertIn("`b`", out)
self.assertIn("`c`", out)
class TestMain(unittest.TestCase):
def test_missing_log_returns_zero(self):
rc = role_summary.main(["role_summary.py", "/nonexistent/path.log"])
self.assertEqual(rc, 0)
def test_missing_argument_returns_two(self):
rc = role_summary.main(["role_summary.py"])
self.assertEqual(rc, 2)
def test_writes_to_step_summary(self):
log_name = _write_tempfile(SAMPLE_LOG, ".log")
self.addCleanup(os.unlink, log_name)
sum_name = _write_tempfile("", ".md")
self.addCleanup(os.unlink, sum_name)
with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": sum_name}):
rc = role_summary.main(["role_summary.py", log_name])
self.assertEqual(rc, 0)
content = read_text(sum_name)
self.assertIn("## ⏱️ Role runtimes", content)
self.assertIn("web-app-keycloak", content)
if __name__ == "__main__":
unittest.main()