Coverage for genbadge/utils_coverage.py: 75%

79 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-11-10 20:37 +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, 

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 

35 self.lines_covered = lines_covered 

36 self.lines_valid = lines_valid 

37 

38 @property 

39 def branch_rate(self): 

40 """ 

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

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

43 We mimic the behaviour in this field to be consistent. 

44 """ 

45 if self.branches_valid > 0: 

46 return self.branches_covered / self.branches_valid 

47 else: 

48 return 0 

49 

50 @property 

51 def line_rate(self): 

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

53 if self.lines_valid > 0: 53 ↛ 56line 53 didn't jump to line 56, because the condition on line 53 was never false

54 return self.lines_covered / self.lines_valid 

55 else: 

56 return 0 

57 

58 @property 

59 def branch_coverage(self): 

60 return self.branch_rate * 100 

61 

62 @property 

63 def line_coverage(self): 

64 return self.line_rate * 100 

65 

66 @property 

67 def total_rate(self): 

68 """ 

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

70 for the formula. 

71 

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

73 """ 

74 denom = self.lines_valid + self.branches_valid 

75 if denom > 0: 75 ↛ 78line 75 didn't jump to line 78, because the condition on line 75 was never false

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

77 else: 

78 return 0 

79 

80 @property 

81 def total_coverage(self): 

82 return self.total_rate * 100 

83 

84 

85def get_coverage_stats(coverage_xml_file): 

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

87 """ 

88 Reads a coverage.xml file 

89 

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

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

92 </coverage> 

93 """ 

94 if isinstance(coverage_xml_file, str): 

95 # assume a file path 

96 with open(coverage_xml_file) as f: 

97 cov_stats = parse_cov(f) 

98 else: 

99 # assume a stream already 

100 cov_stats = parse_cov(coverage_xml_file) 

101 

102 return cov_stats 

103 

104 

105def get_color( 

106 cov_stats # type: CoverageStats 

107): 

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

109 

110 if cov_stats.total_coverage < 50: 110 ↛ 112line 110 didn't jump to line 112, because the condition on line 110 was never false

111 color = 'red' 

112 elif cov_stats.total_coverage < 75: 

113 color = 'orange' 

114 elif cov_stats.total_coverage < 90: 

115 color = 'green' 

116 else: 

117 color = 'brightgreen' 

118 

119 return color 

120 

121 

122def get_coverage_badge( 

123 cov_stats, # type: CoverageStats 

124 left_txt= "coverage" # type: str 

125): 

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

127 """Return the badge from coverage results """ 

128 

129 color = get_color(cov_stats) 

130 

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

132 

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

134 

135 

136def parse_cov(source): 

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

138 return CovParser().parse(source) 

139 

140 

141class CovParser(object): 

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

143 

144 def parse(self, source): 

145 xml = defused_etree.parse(source) 

146 root = xml.getroot() 

147 return self.parse_root(root) 

148 

149 def parse_root(self, root): 

150 cov = CoverageStats() 

151 assert root.tag == 'coverage' 

152 

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

154 

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

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

157 

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

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

160 

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

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

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

164 

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

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

167 % (cov.branch_rate, branch_rate)) 

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

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

170 % (cov.line_rate, line_rate)) 

171 

172 # for el in root: 

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

174 # self.parse_sources(el, ts) 

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

176 # self.parse_packages(el, ts) 

177 

178 return cov 

179 

180 

181def is_close(a, b): 

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

183 return abs(a - b) <= 0.01