/
write.py
184 lines (149 loc) · 6 KB
/
write.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# -*- coding:utf-8 -*-
# ************************** Copyrights and license ***************************
#
# This file is part of gcovr 7.2+main, a parsing and reporting tool for gcov.
# https://gcovr.com/en/stable
#
# _____________________________________________________________________________
#
# Copyright (c) 2013-2024 the gcovr authors
# Copyright (c) 2013 Sandia Corporation.
# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
# the U.S. Government retains certain rights in this software.
#
# This software is distributed under the 3-clause BSD License.
# For more information, see the README.rst file.
#
# ****************************************************************************
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict
from lxml import etree
from ...options import Options
from ...version import __version__
from ...utils import force_unix_separator, open_binary_for_writing, presentable_filename
from ...coverage import CovData, CoverageStat, LineCoverage, SummarizedStats
def write_report(covdata: CovData, output_file: str, options: Options) -> None:
"""produce an XML report in the Cobertura format"""
stats = SummarizedStats.from_covdata(covdata)
root = etree.Element("coverage")
root.set("line-rate", _rate(stats.line))
root.set("branch-rate", _rate(stats.branch))
root.set("lines-covered", str(stats.line.covered))
root.set("lines-valid", str(stats.line.total))
root.set("branches-covered", str(stats.branch.covered))
root.set("branches-valid", str(stats.branch.total))
root.set("complexity", "0.0")
root.set("timestamp", str(int(options.timestamp.timestamp())))
root.set("version", f"gcovr {__version__}")
# Generate the <sources> element: this is either the root directory
# (specified by --root), or the CWD.
sources = etree.SubElement(root, "sources")
# Generate the coverage output (on a per-package basis)
packageXml = etree.SubElement(root, "packages")
packages: Dict[str, PackageData] = {}
for f in sorted(covdata):
data = covdata[f]
filename = presentable_filename(f, root_filter=options.root_filter)
if "/" in filename:
directory, fname = filename.rsplit("/", 1)
else:
directory, fname = "", filename
package = packages.setdefault(
directory,
PackageData(
{},
CoverageStat.new_empty(),
CoverageStat.new_empty(),
),
)
c = etree.Element("class")
# The Cobertura DTD requires a methods section, which isn't
# trivial to get from gcov (so we will leave it blank)
etree.SubElement(c, "methods")
lines = etree.SubElement(c, "lines")
# TODO should use FileCoverage.branch_coverage() calculation
class_branch = CoverageStat.new_empty()
for lineno in sorted(data.lines):
line_cov = data.lines[lineno]
if not line_cov.is_reportable:
continue
b = line_cov.branch_coverage()
if b.total:
class_branch += b
lines.append(_line_element(line_cov))
stats = SummarizedStats.from_file(data)
className = fname.replace(".", "_")
c.set("name", className)
c.set("filename", filename)
c.set("line-rate", _rate(stats.line))
c.set("branch-rate", _rate(class_branch))
c.set("complexity", "0.0")
package.classes_xml[className] = c
package.line += stats.line
package.branch += class_branch
for packageName in sorted(packages):
packageData = packages[packageName]
package = etree.Element("package")
packageXml.append(package)
classes = etree.SubElement(package, "classes")
for className in sorted(packageData.classes_xml):
classes.append(packageData.classes_xml[className])
package.set("name", packageName.replace("/", "."))
package.set("line-rate", _rate(packageData.line))
package.set("branch-rate", _rate(packageData.branch))
package.set("complexity", "0.0")
# Populate the <sources> element: this is the root directory
etree.SubElement(sources, "source").text = force_unix_separator(
options.root.strip()
)
with open_binary_for_writing(output_file, "cobertura.xml") as fh:
fh.write(
etree.tostring(
root,
pretty_print=options.cobertura_pretty,
encoding="UTF-8",
xml_declaration=True,
doctype="<!DOCTYPE coverage SYSTEM 'http://cobertura.sourceforge.net/xml/coverage-04.dtd'>",
)
)
@dataclass
class PackageData:
classes_xml: Dict[str, etree.Element]
line: CoverageStat
branch: CoverageStat
def _rate(stat: CoverageStat) -> str:
"""format a CoverageStat as a string in range 0.0 to 1.0 inclusive"""
total = stat.total
covered = stat.covered
if not total:
return "1.0"
return str(covered / total)
def _line_element(line: LineCoverage) -> etree.Element:
branch = line.branch_coverage()
elem = etree.Element("line")
elem.set("number", str(line.lineno))
elem.set("hits", str(line.count))
if not branch.total:
elem.set("branch", "false")
else:
assert branch.percent is not None
elem.set("branch", "true")
elem.set(
"condition-coverage",
f"{int(branch.percent)}% ({branch.covered}/{branch.total})",
)
elem.append(_conditions_element(branch))
return elem
def _conditions_element(branch: CoverageStat) -> etree.Element:
elem = etree.Element("conditions")
elem.append(_condition_element(branch))
return elem
def _condition_element(branch: CoverageStat) -> etree.Element:
coverage = branch.percent
assert coverage is not None
elem = etree.Element("condition")
elem.set("number", "0")
elem.set("type", "jump")
elem.set("coverage", f"{int(coverage)}%")
return elem