Coverage for /builds/svesch/cikit/src/cikit/xml_tools.py: 96%

44 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''' 

3brief 

4 

5detailed 

6 

7Author: 

8 Sven Schlender 

9License: 

10 SPDX-License-Identifier: GPL-3.0-or-later 

11Date: 

12 2026-03-06 

13''' 

14 

15from pathlib import Path 

16import xml.etree.ElementTree as ET 

17 

18from cikit.errors import CiToolsError 

19 

20 

21def read_junit_totals(test_container: str, xml_file: Path) -> dict: 

22 """Read aggregated test statistics from a JUnit-style XML file. 

23 

24 Supports pytest and GoogleTest XML formats. All <testsuite> elements 

25 are scanned and attributes are summed up. GoogleTest uses "disabled" 

26 instead of "skipped", which is mapped accordingly. 

27 

28 Args: 

29 test_container: Name of the tests container 

30 xml_file: Path to the JUnit XML file. 

31 

32 Returns: 

33 Dict containing tests, failures, errors and skipped counts. 

34 

35 Raises: 

36 ValueError: If the XML file cannot be parsed. 

37 """ 

38 try: 

39 tree = ET.parse(xml_file) 

40 except ET.ParseError as exp: 

41 raise ValueError(f"Invalid XML file: {xml_file}") from exp 

42 

43 root = tree.getroot() 

44 

45 totals = { 

46 "name": test_container, 

47 "tests": 0, 

48 "failures": 0, 

49 "errors": 0, 

50 "skipped": 0, 

51 } 

52 

53 for suite in root.iter("testsuite"): 

54 for key in ("tests", "failures", "errors"): 

55 value = suite.get(key) 

56 if value: 56 ↛ 54line 56 didn't jump to line 54 because the condition on line 56 was always true

57 totals[key] += int(value) 

58 

59 # pytest: skipped 

60 skipped = suite.get("skipped") 

61 

62 # gtest: disabled 

63 disabled = suite.get("disabled") 

64 

65 if skipped: 

66 totals["skipped"] += int(skipped) 

67 elif disabled: 67 ↛ 53line 67 didn't jump to line 53 because the condition on line 67 was always true

68 totals["skipped"] += int(disabled) 

69 

70 totals["succeeded"] = (totals["tests"] - totals["failures"] - 

71 totals["errors"] - totals["skipped"]) 

72 

73 return totals 

74 

75 

76def read_cobertura_coverage(xml_file: Path) -> dict: 

77 """Read coverage statistics from a Cobertura XML file. 

78 

79 The Cobertura root element provides the overall coverage metrics as 

80 attributes. This function extracts the line coverage ratio as well as 

81 the absolute number of covered and valid lines. 

82 

83 Args: 

84 xml_file: Path to the Cobertura XML file. 

85 

86 Returns: 

87 Dict containing: 

88 coverage: Coverage percentage rounded to one decimal place (0.0 .. 100.0). 

89 lines_covered: Number of executed lines. 

90 lines_valid: Total number of executable lines. 

91 

92 Raises: 

93 CiToolsError: If the XML file cannot be parsed or required values 

94 are missing or invalid. 

95 """ 

96 try: 

97 tree = ET.parse(xml_file) 

98 except ET.ParseError as exp: 

99 raise CiToolsError(f"Invalid XML file: {xml_file}") from exp 

100 

101 root = tree.getroot() 

102 

103 raw_line_rate = root.get("line-rate") 

104 raw_lines_covered = root.get("lines-covered") 

105 raw_lines_valid = root.get("lines-valid") 

106 

107 if raw_line_rate is None: 

108 raise CiToolsError(f"Missing line-rate in XML file: {xml_file}") 

109 

110 try: 

111 line_rate = float(raw_line_rate.replace(",", ".")) 

112 except ValueError as exp: 

113 raise CiToolsError(f"Invalid line-rate value {raw_line_rate!r} in XML " 

114 f"file: {xml_file}") from exp 

115 

116 try: 

117 lines_covered = int(raw_lines_covered) if raw_lines_covered else 0 

118 lines_valid = int(raw_lines_valid) if raw_lines_valid else 0 

119 except ValueError as exp: 

120 raise CiToolsError(f"Invalid line counters in XML file: {xml_file}") from exp 

121 

122 return { 

123 "coverage": round(line_rate * 100, 1), 

124 "lines_covered": lines_covered, 

125 "lines_valid": lines_valid, 

126 }