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

1"""Markdown report generation for the conformance suite. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10import datetime 

11from typing import Any 

12 

13import pytest 

14 

15 

16class ConformanceReporter: 

17 """Pytest plugin that records outcomes and generates a Markdown report.""" 

18 

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) 

23 

24 # ------------------------------------------------------------------ 

25 # pytest hooks 

26 # ------------------------------------------------------------------ 

27 

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 ) 

38 

39 # ------------------------------------------------------------------ 

40 # Report rendering 

41 # ------------------------------------------------------------------ 

42 

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) 

55 

56 

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 } 

64 

65 

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 ] 

92 

93 

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 [] 

98 

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 

110 

111 

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 [] 

116 

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 

123 

124 

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