Files
computer-playbook/tests/unit/utils/annotations/test_summarize.py
T
kevinveenbirkenbachandClaude Sonnet 4.6 0ce6b1347c refactor(annotations): move to utils/annotations, add step summary support
- Move utils/gha/annotations.py → utils/annotations/message.py
- Move scripts/gha/summarize_annotations.py → utils/annotations/summarize.py
- Keep utils/gha/annotations.py as compatibility shim
- Update all imports across codebase to utils.annotations.message
- Wire tee + summary step into test-code-lint, unit, and integration workflows
- Add unit tests for utils/annotations/summarize (20 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:05:24 +02:00

189 lines
6.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.
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from utils.annotations.summarize import (
Annotation,
parse_log,
parse_props,
render_markdown,
)
class TestParseProps(unittest.TestCase):
def test_single_prop(self) -> None:
self.assertEqual(parse_props("title=Foo"), {"title": "Foo"})
def test_multiple_props(self) -> None:
result = parse_props("title=My Title,file=foo.py,line=42")
self.assertEqual(result, {"title": "My Title", "file": "foo.py", "line": "42"})
def test_empty_string(self) -> None:
self.assertEqual(parse_props(""), {})
def test_none_like_empty(self) -> None:
self.assertEqual(parse_props(""), {})
class TestParseLog(unittest.TestCase):
def _write_log(self, content: str) -> Path:
f = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False)
f.write(content)
f.close()
return Path(f.name)
def test_parses_warning(self) -> None:
p = self._write_log("::warning title=T,file=f.py,line=3::bad thing\n")
result = parse_log(p)
self.assertEqual(len(result), 1)
a = result[0]
self.assertEqual(a.level, "warning")
self.assertEqual(a.title, "T")
self.assertEqual(a.file, "f.py")
self.assertEqual(a.line, 3)
self.assertEqual(a.message, "bad thing")
def test_parses_error(self) -> None:
p = self._write_log("::error title=Oops::something broke\n")
result = parse_log(p)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].level, "error")
self.assertEqual(result[0].title, "Oops")
def test_parses_notice(self) -> None:
p = self._write_log("::notice::just info\n")
result = parse_log(p)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].level, "notice")
self.assertIsNone(result[0].title)
def test_ignores_non_annotation_lines(self) -> None:
p = self._write_log("regular output\nsome other line\n")
self.assertEqual(parse_log(p), [])
def test_mixed_log(self) -> None:
content = (
"build started\n"
"::warning title=W::warn msg\n"
"some output\n"
"::error::err msg\n"
)
p = self._write_log(content)
result = parse_log(p)
self.assertEqual(len(result), 2)
self.assertEqual(result[0].level, "warning")
self.assertEqual(result[1].level, "error")
def test_multiple_annotations(self) -> None:
lines = "\n".join(f"::warning::msg {i}" for i in range(15))
p = self._write_log(lines)
self.assertEqual(len(parse_log(p)), 15)
class TestRenderMarkdown(unittest.TestCase):
def test_empty_returns_no_annotations_message(self) -> None:
md = render_markdown([], "My Title")
self.assertIn("My Title", md)
self.assertIn("No annotations found", md)
def test_warning_section_present(self) -> None:
annotations = [Annotation(level="warning", message="watch out", title="T")]
md = render_markdown(annotations, "Summary")
self.assertIn("Warnings", md)
self.assertIn("watch out", md)
self.assertIn("🟡", md)
def test_error_section_present(self) -> None:
annotations = [Annotation(level="error", message="broken")]
md = render_markdown(annotations, "Summary")
self.assertIn("Errors", md)
self.assertIn("🔴", md)
def test_notice_section_present(self) -> None:
annotations = [Annotation(level="notice", message="fyi")]
md = render_markdown(annotations, "Summary")
self.assertIn("Notices", md)
self.assertIn("🔵", md)
def test_file_with_line_shown_as_colon_path(self) -> None:
annotations = [Annotation(level="warning", message="x", file="a.py", line=7)]
md = render_markdown(annotations, "S")
self.assertIn("a.py:7", md)
def test_pipe_in_message_escaped(self) -> None:
annotations = [Annotation(level="warning", message="a|b")]
md = render_markdown(annotations, "S")
self.assertIn("a\\|b", md)
def test_count_in_heading(self) -> None:
annotations = [
Annotation(level="warning", message="w1"),
Annotation(level="warning", message="w2"),
]
md = render_markdown(annotations, "S")
self.assertIn("Warnings (2)", md)
def test_order_error_warning_notice(self) -> None:
annotations = [
Annotation(level="notice", message="n"),
Annotation(level="warning", message="w"),
Annotation(level="error", message="e"),
]
md = render_markdown(annotations, "S")
error_pos = md.index("Errors")
warning_pos = md.index("Warnings")
notice_pos = md.index("Notices")
self.assertLess(error_pos, warning_pos)
self.assertLess(warning_pos, notice_pos)
class TestMain(unittest.TestCase):
def test_writes_to_github_step_summary(self) -> None:
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as log:
log.write("::warning title=T::msg\n")
log_path = log.name
with tempfile.NamedTemporaryFile(
mode="w", suffix=".md", delete=False
) as summary:
summary_path = summary.name
import utils.annotations.summarize as mod
import sys
old_argv = sys.argv
try:
sys.argv = ["summarize", log_path, "Test Title"]
with unittest.mock.patch.dict(
os.environ, {"GITHUB_STEP_SUMMARY": summary_path}
):
mod.main()
finally:
sys.argv = old_argv
content = Path(summary_path).read_text()
self.assertIn("Test Title", content)
self.assertIn("msg", content)
def test_exits_without_args(self) -> None:
import sys
import utils.annotations.summarize as mod
old_argv = sys.argv
sys.argv = ["summarize"]
try:
with self.assertRaises(SystemExit) as cm:
mod.main()
self.assertEqual(cm.exception.code, 1)
finally:
sys.argv = old_argv
import unittest.mock # noqa: E402 – needed for TestMain
if __name__ == "__main__":
unittest.main()