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
« 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
7from .utils_badge import Badge
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()
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
32 self.branches_covered = branches_covered
33 self.branches_valid = branches_valid
34 self.branch_option = branch_option
36 self.lines_covered = lines_covered
37 self.lines_valid = lines_valid
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
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
63 @property
64 def branch_coverage(self):
65 return self.branch_rate * 100
67 @property
68 def line_coverage(self):
69 return self.line_rate * 100
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.
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
85 @property
86 def total_coverage(self):
87 return self.total_rate * 100
90def get_coverage_stats(coverage_xml_file):
91 # type: (...) -> CoverageStats
92 """
93 Reads a coverage.xml file
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)
107 return cov_stats
110def get_color(
111 cov_stats # type: CoverageStats
112):
113 """ Returns the badge color to use depending on the coverage rate """
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'
124 return color
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 """
134 color = get_color(cov_stats)
136 right_txt = "%.2f%%" % (cov_stats.total_coverage,)
138 return Badge(left_txt=left_txt, right_txt=right_txt, color=color)
141def parse_cov(source):
142 """Parses the coverage.xml contents from source"""
143 return CovParser().parse(source)
146class CovParser(object):
147 """Parser class - inspired by the code in `xunitparser`"""
149 def parse(self, source):
150 xml = defused_etree.parse(source)
151 root = xml.getroot()
152 return self.parse_root(root)
154 def parse_root(self, root):
155 cov = CoverageStats()
156 assert root.tag == 'coverage'
158 cov.complexity = float(root.attrib.get('complexity'))
160 cov.branches_covered = int(root.attrib.get('branches-covered'))
161 cov.branches_valid = int(root.attrib.get('branches-valid'))
163 cov.lines_covered = int(root.attrib.get('lines-covered'))
164 cov.lines_valid = int(root.attrib.get('lines-valid'))
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'))
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
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))
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)
187 return cov
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