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

63 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"""Coverage report helpers for cikit. 

3 

4This module provides helpers for generating Python and C coverage reports, 

5registering coverage artifacts, creating badges and writing Sphinx RST 

6summary pages. 

7 

8Author: 

9 Sven Schlender 

10Date: 

11 2026-05-28 

12""" 

13 

14import logging 

15from pathlib import Path 

16from tempfile import TemporaryDirectory 

17 

18from cikit.badge import write_badge 

19from cikit.command import Command, run_command 

20from cikit.artifacts import Artifact, ArtifactType 

21from cikit.env import write_file 

22from cikit.rst_tools import RstBlock, RstDocument, get_sphinx_paths, timeline_artifacts_block 

23from cikit.xml_tools import read_cobertura_coverage 

24 

25from .common import resolve_name_label 

26 

27 

28LOGGER = logging.getLogger(__name__) 

29 

30 

31def _register_coverage_outputs(env, xml_file: Path, html_dir: Path): 

32 """Register coverage report outputs and update cikit data. 

33 

34 Coverage metrics are extracted from the generated Cobertura XML file 

35 and stored in the current run data. The XML report and HTML report 

36 directory are copied to their public artifact directories and registered 

37 for later processing. 

38 

39 Args: 

40 env: Resolved CI run environment. 

41 xml_file: Generated Cobertura XML report. 

42 html_dir: Generated HTML coverage report directory. 

43 """ 

44 env.data_update(read_cobertura_coverage(xml_file)) 

45 env.copy_artifact(xml_file, ArtifactType.XUNIT) 

46 env.copy_artifact(html_dir, ArtifactType.FOLDER) 

47 

48 

49def pycoverage(env, coverage_dir: Path): 

50 """Create Python coverage XML and HTML reports. 

51 

52 Args: 

53 env: Resolved CI run environment. 

54 coverage_dir: Directory containing parallel coverage data files. 

55 

56 Raises: 

57 CiToolsError: If coverage report generation fails. 

58 """ 

59 name = env.name 

60 

61 coverage_dir = env.resolve_path(coverage_dir) 

62 

63 with TemporaryDirectory() as temp_dir: 

64 temp_dir = Path(temp_dir) 

65 xml_file = temp_dir / f"{name}.xml" 

66 html_dir = temp_dir / name 

67 

68 coverage_file_count = sum(1 for _ in coverage_dir.glob(".coverage.*")) 

69 LOGGER.info("Combining %d coverage data files.", coverage_file_count) 

70 env.data_set("coverage_files", coverage_file_count) 

71 

72 cmd = Command("coverage", use_exec_tool=True) 

73 cmd.add_subcommand("combine", ["combine", "--keep", str(coverage_dir)]) 

74 cmd.add_subcommand("xml", ["xml", "-o", str(xml_file)]) 

75 cmd.add_subcommand("html", ["html", "-d", str(html_dir)]) 

76 

77 run_command(env.argv(cmd, "combine"), cwd=temp_dir, check_exit_code=True) 

78 run_command(env.argv(cmd, "xml"), cwd=temp_dir, check_exit_code=True) 

79 run_command(env.argv(cmd, "html"), cwd=temp_dir, check_exit_code=True) 

80 

81 _register_coverage_outputs(env, xml_file, html_dir) 

82 

83 

84def ccoverage(env, build_dir: Path): 

85 """Create C coverage XML and HTML reports with gcovr. 

86 

87 Args: 

88 env: Resolved CI run environment. 

89 build_dir: Build directory containing gcovr coverage data. 

90 

91 Raises: 

92 CiToolsError: If coverage report generation fails. 

93 """ 

94 name = env.name 

95 

96 with TemporaryDirectory() as temp_dir: 

97 temp_dir = Path(temp_dir) 

98 

99 xml_file = temp_dir / f"{name}.xml" 

100 html_dir = temp_dir / name 

101 html_dir.mkdir(parents=True, exist_ok=True) 

102 

103 args = ["--object-directory", str(build_dir), "--xml-pretty", "--xml", str(xml_file), 

104 "--html-details", str(html_dir / "index.html")] 

105 cmd = Command("gcovr", common_args=args, use_exec_tool=True) 

106 

107 run_command(env.argv(cmd), cwd=env["PROJECT_ROOT"], check_exit_code=True) 

108 

109 _register_coverage_outputs(env, xml_file, html_dir) 

110 

111 

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

113 """Generate the coverage badge. 

114 

115 Args: 

116 env: Resolved CI run environment. 

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

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

119 

120 Returns: 

121 Created and registered badge artifact. 

122 """ 

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

124 

125 coverage_value = env.data.get("coverage", 0) 

126 return write_badge(env, name=name, label=label, value=f"{coverage_value}%", 

127 thresholds={50: "red", 70: "orange", 85: "yellow", 95: "green"}) 

128 

129 

130def write_rst(env, report_title: str = None) -> Artifact: 

131 """Create an RST page for a coverage report. 

132 

133 The generated page contains a summary of the collected coverage metrics 

134 and a link to the corresponding HTML coverage report. 

135 

136 Args: 

137 env: Resolved CI run environment. 

138 report_title: Title of the RST page. If omitted, ``Coverage Report`` is used. 

139 

140 Returns: 

141 Created and registered RST artifact. 

142 

143 Raises: 

144 CiToolsError: If the corresponding HTML coverage artifact is not found or is ambiguous. 

145 """ 

146 name = env.name 

147 

148 if report_title is None: 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true

149 report_title = "Coverage Report" 

150 

151 paths = get_sphinx_paths() 

152 html_artifact = env.get_run_artifact(name, ArtifactType.FOLDER) 

153 

154 document = RstDocument(report_title) 

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

156 

157 summary = RstBlock("Summary") 

158 summary.extend([ 

159 f"- Coverage: {env.data.get('coverage')}", 

160 f"- Lines covered: {env.data.get('lines_covered')}", 

161 f"- Lines valid: {env.data.get('lines_valid')}", 

162 "", 

163 f"`HTML coverage report <{paths.rst_artifact(html_artifact, 'index.html')}>`_", 

164 ]) 

165 document.add_block(summary) 

166 

167 with TemporaryDirectory() as temp_dir: 

168 source_file = Path(temp_dir) / f"{name}.rst" 

169 write_file(source_file, document.text()) 

170 return env.copy_artifact(source_file, ArtifactType.RST)