coverage 7.12.0__cp314-cp314-musllinux_1_2_i686.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coverage/__init__.py +40 -0
- coverage/__main__.py +12 -0
- coverage/annotate.py +114 -0
- coverage/bytecode.py +196 -0
- coverage/cmdline.py +1184 -0
- coverage/collector.py +486 -0
- coverage/config.py +731 -0
- coverage/context.py +74 -0
- coverage/control.py +1481 -0
- coverage/core.py +139 -0
- coverage/data.py +227 -0
- coverage/debug.py +669 -0
- coverage/disposition.py +59 -0
- coverage/env.py +135 -0
- coverage/exceptions.py +85 -0
- coverage/execfile.py +329 -0
- coverage/files.py +553 -0
- coverage/html.py +860 -0
- coverage/htmlfiles/coverage_html.js +735 -0
- coverage/htmlfiles/favicon_32.png +0 -0
- coverage/htmlfiles/index.html +199 -0
- coverage/htmlfiles/keybd_closed.png +0 -0
- coverage/htmlfiles/pyfile.html +149 -0
- coverage/htmlfiles/style.css +385 -0
- coverage/htmlfiles/style.scss +842 -0
- coverage/inorout.py +614 -0
- coverage/jsonreport.py +192 -0
- coverage/lcovreport.py +219 -0
- coverage/misc.py +373 -0
- coverage/multiproc.py +120 -0
- coverage/numbits.py +146 -0
- coverage/parser.py +1215 -0
- coverage/patch.py +166 -0
- coverage/phystokens.py +197 -0
- coverage/plugin.py +617 -0
- coverage/plugin_support.py +299 -0
- coverage/py.typed +1 -0
- coverage/python.py +272 -0
- coverage/pytracer.py +369 -0
- coverage/regions.py +127 -0
- coverage/report.py +298 -0
- coverage/report_core.py +117 -0
- coverage/results.py +502 -0
- coverage/sqldata.py +1153 -0
- coverage/sqlitedb.py +239 -0
- coverage/sysmon.py +513 -0
- coverage/templite.py +318 -0
- coverage/tomlconfig.py +210 -0
- coverage/tracer.cpython-314-i386-linux-musl.so +0 -0
- coverage/tracer.pyi +43 -0
- coverage/types.py +206 -0
- coverage/version.py +35 -0
- coverage/xmlreport.py +264 -0
- coverage-7.12.0.dist-info/METADATA +221 -0
- coverage-7.12.0.dist-info/RECORD +59 -0
- coverage-7.12.0.dist-info/WHEEL +5 -0
- coverage-7.12.0.dist-info/entry_points.txt +4 -0
- coverage-7.12.0.dist-info/licenses/LICENSE.txt +177 -0
- coverage-7.12.0.dist-info/top_level.txt +1 -0
coverage/types.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
+
# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Types for use throughout coverage.py.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import pathlib
|
|
12
|
+
from collections.abc import Iterable, Mapping
|
|
13
|
+
from types import FrameType, ModuleType
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Protocol
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from coverage.plugin import FileTracer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
AnyCallable = Callable[..., Any]
|
|
21
|
+
|
|
22
|
+
## File paths
|
|
23
|
+
|
|
24
|
+
# For arguments that are file paths:
|
|
25
|
+
FilePath = str | os.PathLike[str]
|
|
26
|
+
# For testing FilePath arguments
|
|
27
|
+
FilePathClasses = [str, pathlib.Path]
|
|
28
|
+
FilePathType = type[str] | type[pathlib.Path]
|
|
29
|
+
|
|
30
|
+
## Python tracing
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TTraceFn(Protocol):
|
|
34
|
+
"""A Python trace function."""
|
|
35
|
+
|
|
36
|
+
def __call__(
|
|
37
|
+
self,
|
|
38
|
+
frame: FrameType,
|
|
39
|
+
event: str,
|
|
40
|
+
arg: Any,
|
|
41
|
+
lineno: TLineNo | None = None, # Our own twist, see collector.py
|
|
42
|
+
) -> TTraceFn | None: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Coverage.py tracing
|
|
46
|
+
|
|
47
|
+
# Line numbers are pervasive enough that they deserve their own type.
|
|
48
|
+
TLineNo = int
|
|
49
|
+
|
|
50
|
+
# Bytecode offsets are pervasive enough that they deserve their own type.
|
|
51
|
+
TOffset = int
|
|
52
|
+
|
|
53
|
+
TArc = tuple[TLineNo, TLineNo]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TFileDisposition(Protocol):
|
|
57
|
+
"""A simple value type for recording what to do with a file."""
|
|
58
|
+
|
|
59
|
+
original_filename: str
|
|
60
|
+
canonical_filename: str
|
|
61
|
+
source_filename: str | None
|
|
62
|
+
trace: bool
|
|
63
|
+
reason: str
|
|
64
|
+
file_tracer: FileTracer | None
|
|
65
|
+
has_dynamic_filename: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# When collecting data, we use a dictionary with a few possible shapes. The
|
|
69
|
+
# keys are always file names.
|
|
70
|
+
# - If measuring line coverage, the values are sets of line numbers.
|
|
71
|
+
# - If measuring arcs in the Python tracer, the values are sets of arcs (pairs
|
|
72
|
+
# of line numbers).
|
|
73
|
+
# - If measuring arcs in the C tracer, the values are sets of packed arcs (two
|
|
74
|
+
# line numbers combined into one integer).
|
|
75
|
+
|
|
76
|
+
TTraceFileData = set[TLineNo] | set[TArc] | set[int]
|
|
77
|
+
|
|
78
|
+
TTraceData = dict[str, TTraceFileData]
|
|
79
|
+
|
|
80
|
+
# Functions passed into collectors.
|
|
81
|
+
TShouldTraceFn = Callable[[str, FrameType], TFileDisposition]
|
|
82
|
+
TCheckIncludeFn = Callable[[str, FrameType], bool]
|
|
83
|
+
TShouldStartContextFn = Callable[[FrameType], str | None]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Tracer(Protocol):
|
|
87
|
+
"""Anything that can report on Python execution."""
|
|
88
|
+
|
|
89
|
+
data: TTraceData
|
|
90
|
+
trace_arcs: bool
|
|
91
|
+
should_trace: TShouldTraceFn
|
|
92
|
+
should_trace_cache: Mapping[str, TFileDisposition | None]
|
|
93
|
+
should_start_context: TShouldStartContextFn | None
|
|
94
|
+
switch_context: Callable[[str | None], None] | None
|
|
95
|
+
lock_data: Callable[[], None]
|
|
96
|
+
unlock_data: Callable[[], None]
|
|
97
|
+
warn: TWarnFn
|
|
98
|
+
|
|
99
|
+
def __init__(self) -> None: ...
|
|
100
|
+
|
|
101
|
+
def start(self) -> TTraceFn | None:
|
|
102
|
+
"""Start this tracer, return a trace function if based on sys.settrace."""
|
|
103
|
+
|
|
104
|
+
def stop(self) -> None:
|
|
105
|
+
"""Stop this tracer."""
|
|
106
|
+
|
|
107
|
+
def activity(self) -> bool:
|
|
108
|
+
"""Has there been any activity?"""
|
|
109
|
+
|
|
110
|
+
def reset_activity(self) -> None:
|
|
111
|
+
"""Reset the activity() flag."""
|
|
112
|
+
|
|
113
|
+
def get_stats(self) -> dict[str, int] | None:
|
|
114
|
+
"""Return a dictionary of statistics, or None."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
## Coverage
|
|
118
|
+
|
|
119
|
+
# Many places use kwargs as Coverage kwargs.
|
|
120
|
+
TCovKwargs = Any
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## Configuration
|
|
124
|
+
|
|
125
|
+
# One value read from a config file.
|
|
126
|
+
TConfigValueIn = Optional[bool | int | float | str | Iterable[str] | Mapping[str, Iterable[str]]]
|
|
127
|
+
TConfigValueOut = Optional[bool | int | float | str | list[str] | dict[str, list[str]]]
|
|
128
|
+
# An entire config section, mapping option names to values.
|
|
129
|
+
TConfigSectionIn = Mapping[str, TConfigValueIn]
|
|
130
|
+
TConfigSectionOut = Mapping[str, TConfigValueOut]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TConfigurable(Protocol):
|
|
134
|
+
"""Something that can proxy to the coverage configuration settings."""
|
|
135
|
+
|
|
136
|
+
def get_option(self, option_name: str) -> TConfigValueOut | None:
|
|
137
|
+
"""Get an option from the configuration.
|
|
138
|
+
|
|
139
|
+
`option_name` is a colon-separated string indicating the section and
|
|
140
|
+
option name. For example, the ``branch`` option in the ``[run]``
|
|
141
|
+
section of the config file would be indicated with `"run:branch"`.
|
|
142
|
+
|
|
143
|
+
Returns the value of the option.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def set_option(self, option_name: str, value: TConfigValueIn | TConfigSectionIn) -> None:
|
|
148
|
+
"""Set an option in the configuration.
|
|
149
|
+
|
|
150
|
+
`option_name` is a colon-separated string indicating the section and
|
|
151
|
+
option name. For example, the ``branch`` option in the ``[run]``
|
|
152
|
+
section of the config file would be indicated with `"run:branch"`.
|
|
153
|
+
|
|
154
|
+
`value` is the new value for the option.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TPluginConfig(Protocol):
|
|
160
|
+
"""Something that can provide options to a plugin."""
|
|
161
|
+
|
|
162
|
+
def get_plugin_options(self, plugin: str) -> TConfigSectionOut:
|
|
163
|
+
"""Get the options for a plugin."""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Parsing
|
|
167
|
+
|
|
168
|
+
TMorf = ModuleType | str
|
|
169
|
+
|
|
170
|
+
TSourceTokenLines = Iterable[list[tuple[str, str]]]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## Plugins
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TPlugin(Protocol):
|
|
177
|
+
"""What all plugins have in common."""
|
|
178
|
+
|
|
179
|
+
_coverage_plugin_name: str
|
|
180
|
+
_coverage_enabled: bool
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
## Debugging
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TWarnFn(Protocol):
|
|
187
|
+
"""A callable warn() function."""
|
|
188
|
+
|
|
189
|
+
def __call__(self, msg: str, slug: str | None = None, once: bool = False) -> None: ...
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TDebugCtl(Protocol):
|
|
193
|
+
"""A DebugControl object, or something like it."""
|
|
194
|
+
|
|
195
|
+
def should(self, option: str) -> bool:
|
|
196
|
+
"""Decide whether to output debug information in category `option`."""
|
|
197
|
+
|
|
198
|
+
def write(self, msg: str) -> None:
|
|
199
|
+
"""Write a line of debug output."""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TWritable(Protocol):
|
|
203
|
+
"""Anything that can be written to."""
|
|
204
|
+
|
|
205
|
+
def write(self, msg: str) -> None:
|
|
206
|
+
"""Write a message."""
|
coverage/version.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
+
# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
|
|
3
|
+
|
|
4
|
+
"""The version and URL for coverage.py"""
|
|
5
|
+
# This file is exec'ed in setup.py, don't import anything!
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
# version_info: same semantics as sys.version_info.
|
|
10
|
+
# _dev: the .devN suffix if any.
|
|
11
|
+
version_info = (7, 12, 0, "final", 0)
|
|
12
|
+
_dev = 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _make_version(
|
|
16
|
+
major: int,
|
|
17
|
+
minor: int,
|
|
18
|
+
micro: int,
|
|
19
|
+
releaselevel: str = "final",
|
|
20
|
+
serial: int = 0,
|
|
21
|
+
dev: int = 0,
|
|
22
|
+
) -> str:
|
|
23
|
+
"""Create a readable version string from version_info tuple components."""
|
|
24
|
+
assert releaselevel in ["alpha", "beta", "candidate", "final"]
|
|
25
|
+
version = f"{major}.{minor}.{micro}"
|
|
26
|
+
if releaselevel != "final":
|
|
27
|
+
short = {"alpha": "a", "beta": "b", "candidate": "rc"}[releaselevel]
|
|
28
|
+
version += f"{short}{serial}"
|
|
29
|
+
if dev != 0:
|
|
30
|
+
version += f".dev{dev}"
|
|
31
|
+
return version
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__version__ = _make_version(*version_info, _dev)
|
|
35
|
+
__url__ = f"https://coverage.readthedocs.io/en/{__version__}"
|
coverage/xmlreport.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
|
2
|
+
# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
|
|
3
|
+
|
|
4
|
+
"""XML reporting for coverage.py"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import os.path
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
import xml.dom.minidom
|
|
13
|
+
from collections.abc import Iterable
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import IO, TYPE_CHECKING, Any
|
|
16
|
+
|
|
17
|
+
from coverage import __version__, files
|
|
18
|
+
from coverage.misc import human_sorted, human_sorted_items, isolate_module
|
|
19
|
+
from coverage.plugin import FileReporter
|
|
20
|
+
from coverage.report_core import get_analysis_to_report
|
|
21
|
+
from coverage.results import Analysis
|
|
22
|
+
from coverage.types import TMorf
|
|
23
|
+
from coverage.version import __url__
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from coverage import Coverage
|
|
27
|
+
|
|
28
|
+
os = isolate_module(os)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DTD_URL = "https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def rate(hit: int, num: int) -> str:
|
|
35
|
+
"""Return the fraction of `hit`/`num`, as a string."""
|
|
36
|
+
if num == 0:
|
|
37
|
+
return "1"
|
|
38
|
+
else:
|
|
39
|
+
return f"{hit / num:.4g}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class PackageData:
|
|
44
|
+
"""Data we keep about each "package" (in Java terms)."""
|
|
45
|
+
|
|
46
|
+
elements: dict[str, xml.dom.minidom.Element]
|
|
47
|
+
hits: int
|
|
48
|
+
lines: int
|
|
49
|
+
br_hits: int
|
|
50
|
+
branches: int
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def appendChild(parent: Any, child: Any) -> None:
|
|
54
|
+
"""Append a child to a parent, in a way mypy will shut up about."""
|
|
55
|
+
parent.appendChild(child)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class XmlReporter:
|
|
59
|
+
"""A reporter for writing Cobertura-style XML coverage results."""
|
|
60
|
+
|
|
61
|
+
report_type = "XML report"
|
|
62
|
+
|
|
63
|
+
def __init__(self, coverage: Coverage) -> None:
|
|
64
|
+
self.coverage = coverage
|
|
65
|
+
self.config = self.coverage.config
|
|
66
|
+
|
|
67
|
+
self.source_paths = set()
|
|
68
|
+
if self.config.source:
|
|
69
|
+
for src in self.config.source:
|
|
70
|
+
if os.path.exists(src):
|
|
71
|
+
if self.config.relative_files:
|
|
72
|
+
src = src.rstrip(r"\/")
|
|
73
|
+
else:
|
|
74
|
+
src = files.canonical_filename(src)
|
|
75
|
+
self.source_paths.add(src)
|
|
76
|
+
self.packages: dict[str, PackageData] = {}
|
|
77
|
+
self.xml_out: xml.dom.minidom.Document
|
|
78
|
+
|
|
79
|
+
def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str] | None = None) -> float:
|
|
80
|
+
"""Generate a Cobertura-compatible XML report for `morfs`.
|
|
81
|
+
|
|
82
|
+
`morfs` is a list of modules or file names.
|
|
83
|
+
|
|
84
|
+
`outfile` is a file object to write the XML to.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
# Initial setup.
|
|
88
|
+
outfile = outfile or sys.stdout
|
|
89
|
+
has_arcs = self.coverage.get_data().has_arcs()
|
|
90
|
+
|
|
91
|
+
# Create the DOM that will store the data.
|
|
92
|
+
impl = xml.dom.minidom.getDOMImplementation()
|
|
93
|
+
assert impl is not None
|
|
94
|
+
self.xml_out = impl.createDocument(None, "coverage", None)
|
|
95
|
+
|
|
96
|
+
# Write header stuff.
|
|
97
|
+
xcoverage = self.xml_out.documentElement
|
|
98
|
+
assert xcoverage is not None
|
|
99
|
+
xcoverage.setAttribute("version", __version__)
|
|
100
|
+
xcoverage.setAttribute("timestamp", str(int(time.time() * 1000)))
|
|
101
|
+
xcoverage.appendChild(
|
|
102
|
+
self.xml_out.createComment(
|
|
103
|
+
f" Generated by coverage.py: {__url__} ",
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
xcoverage.appendChild(self.xml_out.createComment(f" Based on {DTD_URL} "))
|
|
107
|
+
|
|
108
|
+
# Call xml_file for each file in the data.
|
|
109
|
+
for fr, analysis in get_analysis_to_report(self.coverage, morfs):
|
|
110
|
+
self.xml_file(fr, analysis, has_arcs)
|
|
111
|
+
|
|
112
|
+
xsources = self.xml_out.createElement("sources")
|
|
113
|
+
xcoverage.appendChild(xsources)
|
|
114
|
+
|
|
115
|
+
# Populate the XML DOM with the source info.
|
|
116
|
+
for path in human_sorted(self.source_paths):
|
|
117
|
+
xsource = self.xml_out.createElement("source")
|
|
118
|
+
appendChild(xsources, xsource)
|
|
119
|
+
txt = self.xml_out.createTextNode(path)
|
|
120
|
+
appendChild(xsource, txt)
|
|
121
|
+
|
|
122
|
+
lnum_tot, lhits_tot = 0, 0
|
|
123
|
+
bnum_tot, bhits_tot = 0, 0
|
|
124
|
+
|
|
125
|
+
xpackages = self.xml_out.createElement("packages")
|
|
126
|
+
xcoverage.appendChild(xpackages)
|
|
127
|
+
|
|
128
|
+
# Populate the XML DOM with the package info.
|
|
129
|
+
for pkg_name, pkg_data in human_sorted_items(self.packages.items()):
|
|
130
|
+
xpackage = self.xml_out.createElement("package")
|
|
131
|
+
appendChild(xpackages, xpackage)
|
|
132
|
+
xclasses = self.xml_out.createElement("classes")
|
|
133
|
+
appendChild(xpackage, xclasses)
|
|
134
|
+
for _, class_elt in human_sorted_items(pkg_data.elements.items()):
|
|
135
|
+
appendChild(xclasses, class_elt)
|
|
136
|
+
xpackage.setAttribute("name", pkg_name.replace(os.sep, "."))
|
|
137
|
+
xpackage.setAttribute("line-rate", rate(pkg_data.hits, pkg_data.lines))
|
|
138
|
+
if has_arcs:
|
|
139
|
+
branch_rate = rate(pkg_data.br_hits, pkg_data.branches)
|
|
140
|
+
else:
|
|
141
|
+
branch_rate = "0"
|
|
142
|
+
xpackage.setAttribute("branch-rate", branch_rate)
|
|
143
|
+
xpackage.setAttribute("complexity", "0")
|
|
144
|
+
|
|
145
|
+
lhits_tot += pkg_data.hits
|
|
146
|
+
lnum_tot += pkg_data.lines
|
|
147
|
+
bhits_tot += pkg_data.br_hits
|
|
148
|
+
bnum_tot += pkg_data.branches
|
|
149
|
+
|
|
150
|
+
xcoverage.setAttribute("lines-valid", str(lnum_tot))
|
|
151
|
+
xcoverage.setAttribute("lines-covered", str(lhits_tot))
|
|
152
|
+
xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot))
|
|
153
|
+
if has_arcs:
|
|
154
|
+
xcoverage.setAttribute("branches-valid", str(bnum_tot))
|
|
155
|
+
xcoverage.setAttribute("branches-covered", str(bhits_tot))
|
|
156
|
+
xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot))
|
|
157
|
+
else:
|
|
158
|
+
xcoverage.setAttribute("branches-covered", "0")
|
|
159
|
+
xcoverage.setAttribute("branches-valid", "0")
|
|
160
|
+
xcoverage.setAttribute("branch-rate", "0")
|
|
161
|
+
xcoverage.setAttribute("complexity", "0")
|
|
162
|
+
|
|
163
|
+
# Write the output file.
|
|
164
|
+
outfile.write(serialize_xml(self.xml_out))
|
|
165
|
+
|
|
166
|
+
# Return the total percentage.
|
|
167
|
+
denom = lnum_tot + bnum_tot
|
|
168
|
+
if denom == 0:
|
|
169
|
+
pct = 0.0
|
|
170
|
+
else:
|
|
171
|
+
pct = 100.0 * (lhits_tot + bhits_tot) / denom
|
|
172
|
+
return pct
|
|
173
|
+
|
|
174
|
+
def xml_file(self, fr: FileReporter, analysis: Analysis, has_arcs: bool) -> None:
|
|
175
|
+
"""Add to the XML report for a single file."""
|
|
176
|
+
|
|
177
|
+
if self.config.skip_empty:
|
|
178
|
+
if analysis.numbers.n_statements == 0:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Create the "lines" and "package" XML elements, which
|
|
182
|
+
# are populated later. Note that a package == a directory.
|
|
183
|
+
filename = fr.filename.replace("\\", "/")
|
|
184
|
+
for source_path in self.source_paths:
|
|
185
|
+
if not self.config.relative_files:
|
|
186
|
+
source_path = files.canonical_filename(source_path)
|
|
187
|
+
if filename.startswith(source_path.replace("\\", "/") + "/"):
|
|
188
|
+
rel_name = filename[len(source_path) + 1 :]
|
|
189
|
+
break
|
|
190
|
+
else:
|
|
191
|
+
rel_name = fr.relative_filename().replace("\\", "/")
|
|
192
|
+
self.source_paths.add(fr.filename[: -len(rel_name)].rstrip(r"\/"))
|
|
193
|
+
|
|
194
|
+
dirname = os.path.dirname(rel_name) or "."
|
|
195
|
+
dirname = "/".join(dirname.split("/")[: self.config.xml_package_depth])
|
|
196
|
+
package_name = dirname.replace("/", ".")
|
|
197
|
+
|
|
198
|
+
package = self.packages.setdefault(package_name, PackageData({}, 0, 0, 0, 0))
|
|
199
|
+
|
|
200
|
+
xclass: xml.dom.minidom.Element = self.xml_out.createElement("class")
|
|
201
|
+
|
|
202
|
+
appendChild(xclass, self.xml_out.createElement("methods"))
|
|
203
|
+
|
|
204
|
+
xlines = self.xml_out.createElement("lines")
|
|
205
|
+
appendChild(xclass, xlines)
|
|
206
|
+
|
|
207
|
+
xclass.setAttribute("name", os.path.relpath(rel_name, dirname))
|
|
208
|
+
xclass.setAttribute("filename", rel_name.replace("\\", "/"))
|
|
209
|
+
xclass.setAttribute("complexity", "0")
|
|
210
|
+
|
|
211
|
+
branch_stats = analysis.branch_stats()
|
|
212
|
+
missing_branch_arcs = analysis.missing_branch_arcs()
|
|
213
|
+
|
|
214
|
+
# For each statement, create an XML "line" element.
|
|
215
|
+
for line in sorted(analysis.statements):
|
|
216
|
+
xline = self.xml_out.createElement("line")
|
|
217
|
+
xline.setAttribute("number", str(line))
|
|
218
|
+
|
|
219
|
+
# Q: can we get info about the number of times a statement is
|
|
220
|
+
# executed? If so, that should be recorded here.
|
|
221
|
+
xline.setAttribute("hits", str(int(line not in analysis.missing)))
|
|
222
|
+
|
|
223
|
+
if has_arcs:
|
|
224
|
+
if line in branch_stats:
|
|
225
|
+
total, taken = branch_stats[line]
|
|
226
|
+
xline.setAttribute("branch", "true")
|
|
227
|
+
xline.setAttribute(
|
|
228
|
+
"condition-coverage",
|
|
229
|
+
f"{100 * taken // total}% ({taken}/{total})",
|
|
230
|
+
)
|
|
231
|
+
if line in missing_branch_arcs:
|
|
232
|
+
annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]]
|
|
233
|
+
xline.setAttribute("missing-branches", ",".join(annlines))
|
|
234
|
+
appendChild(xlines, xline)
|
|
235
|
+
|
|
236
|
+
class_lines = len(analysis.statements)
|
|
237
|
+
class_hits = class_lines - len(analysis.missing)
|
|
238
|
+
|
|
239
|
+
if has_arcs:
|
|
240
|
+
class_branches = sum(t for t, k in branch_stats.values())
|
|
241
|
+
missing_branches = sum(t - k for t, k in branch_stats.values())
|
|
242
|
+
class_br_hits = class_branches - missing_branches
|
|
243
|
+
else:
|
|
244
|
+
class_branches = 0
|
|
245
|
+
class_br_hits = 0
|
|
246
|
+
|
|
247
|
+
# Finalize the statistics that are collected in the XML DOM.
|
|
248
|
+
xclass.setAttribute("line-rate", rate(class_hits, class_lines))
|
|
249
|
+
if has_arcs:
|
|
250
|
+
branch_rate = rate(class_br_hits, class_branches)
|
|
251
|
+
else:
|
|
252
|
+
branch_rate = "0"
|
|
253
|
+
xclass.setAttribute("branch-rate", branch_rate)
|
|
254
|
+
|
|
255
|
+
package.elements[rel_name] = xclass
|
|
256
|
+
package.hits += class_hits
|
|
257
|
+
package.lines += class_lines
|
|
258
|
+
package.br_hits += class_br_hits
|
|
259
|
+
package.branches += class_branches
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def serialize_xml(dom: xml.dom.minidom.Document) -> str:
|
|
263
|
+
"""Serialize a minidom node to XML."""
|
|
264
|
+
return dom.toprettyxml()
|