Coverage for src/genbadge/utils_coverage.py: 78%

89 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-03-27 10:51 +0000

1# Authors: Sylvain MARIE <sylvain.marie@se.com> 

2# + All contributors to <https://github.com/smarie/python-genbadge> 

3# 

4# License: 3-clause BSD, <https://github.com/smarie/python-genbadge/blob/master/LICENSE> 

5from __future__ import division 

6 

7from .utils_badge import Badge 

8 

9try: 

10 # security patch: see https://docs.python.org/3/library/xml.etree.elementtree.html 

11 import defusedxml.ElementTree as defused_etree 

12except ImportError as e: 

13 ee = e # save it 

14 class FakeDefusedXmlImport(object): # noqa 

15 def __getattribute__(self, item): 

16 raise ImportError("Could not import `defusedxml.ElementTree`, please install `defusedxml`. " 

17 "Note that all dependencies for the coverage command can be installed with " 

18 "`pip install genbadge[coverage]`. Caught: %r" % ee) 

19 defused_etree = FakeDefusedXmlImport() 

20 

21 

22class CoverageStats(object): 

23 """ 

24 Contains the results from parsing the coverage.xml. 

25 """ 

26 def __init__(self, 

27 branches_covered=None, branches_valid=None, branch_option=None, 

28 complexity=None, lines_covered=None, lines_valid=None, 

29 ): 

30 self.complexity = complexity 

31 

32 self.branches_covered = branches_covered 

33 self.branches_valid = branches_valid 

34 self.branch_option = branch_option 

35 

36 self.lines_covered = lines_covered 

37 self.lines_valid = lines_valid 

38 

39 @property 

40 def branch_rate(self): 

41 """ 

42 Note: in --no-branch situations, the number of branches is 0. 

43 In that case, the branch rate is 0 in the coverage.xml. 

44 But in --branch situations without actual branches, 

45 the number of branches is also 0 but the branch rate is 1. 

46 We mimic both behaviours in this field to be consistent. 

47 """ 

48 if self.branches_valid > 0: 

49 return self.branches_covered / self.branches_valid 

50 elif self.branch_option: 

51 return 1 

52 else: 

53 return 0 

54 

55 @property 

56 def line_rate(self): 

57 """See branch rate for the special case of division by zero""" 

58 if self.lines_valid > 0: 58 ↛ 61line 58 didn't jump to line 61 because the condition on line 58 was always true

59 return self.lines_covered / self.lines_valid 

60 else: 

61 return 0 

62 

63 @property 

64 def branch_coverage(self): 

65 return self.branch_rate * 100 

66 

67 @property 

68 def line_coverage(self): 

69 return self.line_rate * 100 

70 

71 @property 

72 def total_rate(self): 

73 """ 

74 See XmlReport class in https://github.com/nedbat/coveragepy/blob/master/coverage/xmlreport.py 

75 for the formula. 

76 

77 See branch rate for the special case of division by zero. 

78 """ 

79 denom = self.lines_valid + self.branches_valid 

80 if denom > 0: 80 ↛ 83line 80 didn't jump to line 83 because the condition on line 80 was always true

81 return (self.lines_covered + self.branches_covered) / denom 

82 else: 

83 return 0 

84 

85 @property 

86 def total_coverage(self): 

87 return self.total_rate * 100 

88 

89 

90def get_coverage_stats(coverage_xml_file): 

91 # type: (...) -> CoverageStats 

92 """ 

93 Reads a coverage.xml file 

94 

95 <coverage branch-rate="0.6" branches-covered="24" branches-valid="40" complexity="0" line-rate="0.8586" 

96 lines-covered="170" lines-valid="198" timestamp="1620747625339" version="5.5"> 

97 </coverage> 

98 """ 

99 if isinstance(coverage_xml_file, str): 

100 # assume a file path 

101 with open(coverage_xml_file) as f: 

102 cov_stats = parse_cov(f) 

103 else: 

104 # assume a stream already 

105 cov_stats = parse_cov(coverage_xml_file) 

106 

107 return cov_stats 

108 

109 

110def get_color( 

111 cov_stats # type: CoverageStats 

112): 

113 """ Returns the badge color to use depending on the coverage rate """ 

114 

115 if cov_stats.total_coverage < 50: 115 ↛ 117line 115 didn't jump to line 117 because the condition on line 115 was always true

116 color = 'red' 

117 elif cov_stats.total_coverage < 75: 

118 color = 'orange' 

119 elif cov_stats.total_coverage < 90: 

120 color = 'green' 

121 else: 

122 color = 'brightgreen' 

123 

124 return color 

125 

126 

127def get_coverage_badge( 

128 cov_stats, # type: CoverageStats 

129 left_txt= "coverage" # type: str 

130): 

131 # type: (...) -> Badge 

132 """Return the badge from coverage results """ 

133 

134 color = get_color(cov_stats) 

135 

136 right_txt = "%.2f%%" % (cov_stats.total_coverage,) 

137 

138 return Badge(left_txt=left_txt, right_txt=right_txt, color=color) 

139 

140 

141def parse_cov(source): 

142 """Parses the coverage.xml contents from source""" 

143 return CovParser().parse(source) 

144 

145 

146class CovParser(object): 

147 """Parser class - inspired by the code in `xunitparser`""" 

148 

149 def parse(self, source): 

150 xml = defused_etree.parse(source) 

151 root = xml.getroot() 

152 return self.parse_root(root) 

153 

154 def parse_root(self, root): 

155 cov = CoverageStats() 

156 assert root.tag == 'coverage' 

157 

158 cov.complexity = float(root.attrib.get('complexity')) 

159 

160 cov.branches_covered = int(root.attrib.get('branches-covered')) 

161 cov.branches_valid = int(root.attrib.get('branches-valid')) 

162 

163 cov.lines_covered = int(root.attrib.get('lines-covered')) 

164 cov.lines_valid = int(root.attrib.get('lines-valid')) 

165 

166 # recompute the rates for more precision, but make sure that's correct 

167 branch_rate = float(root.attrib.get('branch-rate')) 

168 line_rate = float(root.attrib.get('line-rate')) 

169 

170 # detect whether the --branch option were set or not 

171 # so CoverageStats knows how to distinguish between them 

172 cov.branch_option = cov.branches_valid > 0 or branch_rate == 1.0 

173 

174 if not is_close(cov.branch_rate, branch_rate): 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true

175 raise ValueError("Computed branch rate (%s) is different from the one in the file (%s)" 

176 % (cov.branch_rate, branch_rate)) 

177 if not is_close(cov.line_rate, line_rate): 177 ↛ 178line 177 didn't jump to line 178 because the condition on line 177 was never true

178 raise ValueError("Computed line rate (%s) is different from the one in the file (%s)" 

179 % (cov.line_rate, line_rate)) 

180 

181 # for el in root: 

182 # if el.tag == 'sources': 

183 # self.parse_sources(el, ts) 

184 # if el.tag == 'packages': 

185 # self.parse_packages(el, ts) 

186 

187 return cov 

188 

189 

190def is_close(a, b): 

191 """Return True if there is at most a difference of 1 at the 2d decimal""" 

192 return abs(a - b) <= 0.01