Coverage for node / src / stigmem_conformance / report.py: 100%
54 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Markdown report generation for the conformance suite.
3The ``ConformanceReporter`` is a pytest plugin that collects test outcomes
4and renders a human-readable Markdown report suitable for embedding in
5operator documentation or committing alongside a release.
6"""
8from __future__ import annotations
10import datetime
11from typing import Any
13import pytest
16class ConformanceReporter:
17 """Pytest plugin that records outcomes and generates a Markdown report."""
19 def __init__(self, backend: str = "sqlite") -> None:
20 self.backend = backend
21 self._results: list[dict[str, Any]] = []
22 self._started_at = datetime.datetime.now(datetime.UTC)
24 # ------------------------------------------------------------------
25 # pytest hooks
26 # ------------------------------------------------------------------
28 def pytest_runtest_logreport(self, report: pytest.TestReport) -> None:
29 if report.when != "call":
30 return
31 self._results.append(
32 {
33 "nodeid": report.nodeid,
34 "outcome": report.outcome, # "passed" | "failed" | "skipped"
35 "longrepr": str(report.longrepr) if report.longrepr else "",
36 }
37 )
39 # ------------------------------------------------------------------
40 # Report rendering
41 # ------------------------------------------------------------------
43 def generate_markdown(self) -> str:
44 elapsed = datetime.datetime.now(datetime.UTC) - self._started_at
45 counts = _outcome_counts(self._results)
46 lines = _summary_lines(self.backend, self._started_at, elapsed, counts)
47 lines.extend(_failure_lines(self._results))
48 lines.extend(_skipped_lines(self._results))
49 lines.extend(_detail_lines(self._results))
50 lines.append("")
51 lines.append(
52 "_Generated by [stigmem-conformance](https://docs.stigmem.dev/guides/conformance)_"
53 )
54 return "\n".join(lines)
57def _outcome_counts(results: list[dict[str, Any]]) -> dict[str, int]:
58 return {
59 "total": len(results),
60 "passed": sum(1 for r in results if r["outcome"] == "passed"),
61 "failed": sum(1 for r in results if r["outcome"] == "failed"),
62 "skipped": sum(1 for r in results if r["outcome"] == "skipped"),
63 }
66def _summary_lines(
67 backend: str,
68 started_at: datetime.datetime,
69 elapsed: datetime.timedelta,
70 counts: dict[str, int],
71) -> list[str]:
72 status_emoji = "✅" if counts["failed"] == 0 else "❌"
73 return [
74 f"# Stigmem Conformance Report — `{backend}` backend",
75 "",
76 f"**Generated:** {started_at.strftime('%Y-%m-%d %H:%M UTC')} ",
77 f"**Duration:** {elapsed.total_seconds():.1f}s ",
78 "**Result:** "
79 f"{status_emoji} {counts['passed']}/{counts['total']} passed, "
80 f"{counts['failed']} failed, {counts['skipped']} skipped",
81 "",
82 "## Summary",
83 "",
84 "| Metric | Count |",
85 "|--------|-------|",
86 f"| Passed | {counts['passed']} |",
87 f"| Failed | {counts['failed']} |",
88 f"| Skipped | {counts['skipped']} |",
89 f"| **Total** | **{counts['total']}** |",
90 "",
91 ]
94def _failure_lines(results: list[dict[str, Any]]) -> list[str]:
95 failed = [r for r in results if r["outcome"] == "failed"]
96 if not failed:
97 return []
99 lines = ["## Failures", ""]
100 for result in failed:
101 lines.extend([
102 f"### `{result['nodeid']}`",
103 "",
104 "```",
105 result["longrepr"][:2000],
106 "```",
107 "",
108 ])
109 return lines
112def _skipped_lines(results: list[dict[str, Any]]) -> list[str]:
113 skipped = [r for r in results if r["outcome"] == "skipped"]
114 if not skipped:
115 return []
117 lines = ["## Skipped", ""]
118 for result in skipped:
119 reason = result["longrepr"].split("Skipped: ")[-1].strip() if result["longrepr"] else ""
120 lines.append(f"- `{result['nodeid']}`" + (f" — {reason}" if reason else ""))
121 lines.append("")
122 return lines
125def _detail_lines(results: list[dict[str, Any]]) -> list[str]:
126 lines = [
127 "## Test details",
128 "",
129 "| Test | Outcome |",
130 "|------|---------|",
131 ]
132 icons = {"passed": "✅", "failed": "❌", "skipped": "⏭"}
133 for result in results:
134 icon = icons.get(result["outcome"], "?")
135 short = result["nodeid"].split("::")[-1]
136 lines.append(f"| `{short}` | {icon} {result['outcome']} |")
137 return lines