⬅ genbadge/utils_coverage.py source

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>
5 from __future__ import division
6  
7 from .utils_badge import Badge
8  
9 try:
10 # security patch: see https://docs.python.org/3/library/xml.etree.elementtree.html
11 import defusedxml.ElementTree as defused_etree
12 except 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  
22 class 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:
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:
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  
85 def 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  
105 def 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:
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  
122 def get_coverage_badge(
123 cov_stats, # type: CoverageStats
  • E251 Unexpected spaces around keyword / parameter equals
  • E261 At least two spaces before inline comment
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  
136 def parse_cov(source):
137 """Parses the coverage.xml contents from source"""
138 return CovParser().parse(source)
139  
140  
141 class 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()
  • S101 Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
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):
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):
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  
181 def 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