Coverage for /builds/svesch/cikit/src/cikit/macros/reports.py: 97%

61 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-09 14:17 +0000

1# SPDX-License-Identifier: GPL-3.0-or-later 

2"""Generic reporting helpers for cikit. 

3 

4This module contains reusable helpers for generating reports from cikit run 

5data. It provides badge generation and Sphinx RST reports for test and build 

6results, including timelines, metrics, artifacts, memory usage, and logs. 

7 

8Test reporting operates on the standardized result fields produced by 

9``read_junit_totals()`` and remains independent of the underlying test 

10framework. 

11 

12Author: 

13 Sven Schlender 

14Date: 

15 2026-09-13 

16""" 

17 

18from pathlib import Path 

19from tempfile import TemporaryDirectory 

20 

21from cikit.artifacts import Artifact, ArtifactType 

22from cikit.badge import write_badge 

23from cikit.env import write_file 

24from cikit.errors import CiRecoverableError 

25from cikit.rst_tools import (RstBlock, RstDocument, artifact_links_block, log_artifacts_block, 

26 memory_usage_block, timeline_artifacts_block) 

27from cikit.xml_tools import read_junit_totals 

28 

29from .common import resolve_name_label 

30 

31 

32def badge(env, name: str = None, label: str = None) -> Artifact: 

33 """Generate a badge for one test result. 

34 

35 Args: 

36 env: Resolved CI run environment. 

37 name: Basename of the badge file. If omitted, the run name is used. 

38 label: Label of the badge. If omitted, the run name is used. 

39 

40 Returns: 

41 Created and registered badge artifact. 

42 """ 

43 name, label = resolve_name_label(env, name, label) 

44 

45 value = f"{env.data.get('succeeded', 0)} / {env.data.get('tests', 0)}" 

46 

47 color = "green" 

48 if env.data.get("failures", 0) or env.data.get("errors", 0): 

49 color = "red" 

50 elif env.data.get("skipped"): 

51 color = "yellow" 

52 

53 return write_badge(env=env, name=name, label=label, value=value, color=color) 

54 

55 

56def write_test_report(env, name: str = None, report_title: str = None) -> Artifact: 

57 """Create an RST file for a single test result page. 

58 

59 Args: 

60 env: Resolved CI run environment. 

61 name: Output file basename without extension. If omitted, the run name 

62 is used. 

63 report_title: Report title. If omitted, ``Test Report`` is used. 

64 

65 Returns: 

66 Created and registered RST artifact. 

67 """ 

68 if name is None: 68 ↛ 70line 68 didn't jump to line 70 because the condition on line 68 was always true

69 name = env.name 

70 if report_title is None: 70 ↛ 73line 70 didn't jump to line 73 because the condition on line 70 was always true

71 report_title = "Test Report" 

72 

73 document = RstDocument(report_title) 

74 document.add_block(timeline_artifacts_block(env.get_run_artifacts(ArtifactType.TIMELINE))) 

75 

76 summary = RstBlock("Summary") 

77 summary.extend([ 

78 f"- Tests: {env.data.get('tests')}", 

79 f"- Succeeded: {env.data.get('succeeded')}", 

80 f"- Failures: {env.data.get('failures')}", 

81 f"- Errors: {env.data.get('errors')}", 

82 f"- Skipped: {env.data.get('skipped')}", 

83 f"- Exit code: {env.data.get('exit_code')}", 

84 ]) 

85 document.add_block(summary) 

86 

87 with TemporaryDirectory() as temp_dir: 

88 output_file = Path(temp_dir) / f"{name}.rst" 

89 write_file(output_file, document.text()) 

90 return env.copy_artifact(output_file, ArtifactType.RST) 

91 

92 

93def write_build_report(env, name: str = None, report_title: str = None) -> Artifact: 

94 """Create an RST file for a build result page. 

95 

96 The report contains available timeline charts, build metrics, compiler 

97 metadata, memory usage, direct build artifacts, bundles, and command logs 

98 of the current run. 

99 

100 Args: 

101 env: Resolved CI run environment. 

102 name: Output file basename without extension. If omitted, the run name 

103 is used. 

104 report_title: Report title. If omitted, ``Build Report`` is used. 

105 

106 Returns: 

107 Created and registered RST artifact. 

108 """ 

109 if name is None: 

110 name = env.name 

111 if report_title is None: 

112 report_title = "Build Report" 

113 

114 document = RstDocument(report_title) 

115 document.add_block(timeline_artifacts_block(env.get_run_artifacts(ArtifactType.TIMELINE))) 

116 

117 summary = RstBlock("Summary") 

118 

119 summary_keys = { 

120 "cmake_version": ("CMake version", "%s"), 

121 "compiler": ("Compiler", "%s"), 

122 "compiler_version": ("Compiler version", "%s"), 

123 "compiler_type": ("Compiler type", "%s"), 

124 "optimization": ("Optimization", "%s"), 

125 "lto": ("LTO", "%s"), 

126 "build_files": ("Compiled files", "%s"), 

127 "warnings": ("Warnings", "%s"), 

128 "errors": ("Errors", "%s"), 

129 "configure_time": ("Configure time", "%s s"), 

130 "build_time": ("Build time", "%s s"), 

131 "total_build_time": ("Total build time", "%s s"), 

132 } 

133 

134 summary.extend([".. list-table::", " :widths: 35 65", ""]) 

135 

136 for key, (label, value_format) in summary_keys.items(): 

137 if key in env.data: 

138 summary.extend([f" * - {label}", f" - {value_format % env.data[key]}"]) 

139 

140 document.add_block(summary) 

141 

142 document.add_block(memory_usage_block(env.data)) 

143 document.add_block(artifact_links_block(env.get_run_artifacts(ArtifactType.BUILD), 

144 title="Build Artifacts")) 

145 document.add_block(artifact_links_block(env.get_run_artifacts(ArtifactType.BUNDLE), 

146 title="Bundles")) 

147 document.add_block(log_artifacts_block(env.get_run_artifacts(ArtifactType.LOG))) 

148 

149 with TemporaryDirectory() as temp_dir: 

150 output_file = Path(temp_dir) / f"{name}.rst" 

151 write_file(output_file, document.text()) 

152 return env.copy_artifact(output_file, ArtifactType.RST) 

153 

154 

155def process_test_result(env, test_alias: str, report_file: Path, returncode: int): 

156 """Process and persist a JUnit-compatible test result. 

157 

158 The JUnit report is parsed and registered as an XUnit artifact. Extracted 

159 test metrics and the process exit code are stored in the current run data. 

160 Failed test execution is reported only after all available result data has 

161 been persisted. 

162 

163 Args: 

164 env: Resolved CI run environment. 

165 test_alias: Logical test name used for result data. 

166 report_file: Generated JUnit XML report. 

167 returncode: Exit code returned by the test process. 

168 

169 Raises: 

170 CiRecoverableError: If the test process returned a non-zero exit code. 

171 """ 

172 result = read_junit_totals(test_alias, report_file) 

173 result["exit_code"] = returncode 

174 

175 env.copy_artifact(report_file, ArtifactType.XUNIT) 

176 

177 env.data_update(result) 

178 

179 if returncode: 

180 raise CiRecoverableError(f"Test has finished with errors (exit code {returncode})", 

181 exit_code=returncode)