Coverage for coverage / xmlreport.py: 100.000%
165 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
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
4"""XML reporting for coverage.py"""
6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
8import os 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
9import os.path 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
10import sys 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
11import time 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
12import xml.dom.minidom 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
13from collections.abc import Iterable 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
14from dataclasses import dataclass 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
15from typing import IO, TYPE_CHECKING, Any 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
17from coverage import __version__, files 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
18from coverage.misc import human_sorted, human_sorted_items, isolate_module 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
19from coverage.plugin import FileReporter 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
20from coverage.report_core import get_analysis_to_report 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
21from coverage.results import Analysis 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
22from coverage.types import TMorf 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
23from coverage.version import __url__ 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
25if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
26 from coverage import Coverage
28os = isolate_module(os) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
31DTD_URL = "https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd" 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
34def rate(hit: int, num: int) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
35 """Return the fraction of `hit`/`num`, as a string."""
36 if num == 0: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
37 return "1" 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
38 else:
39 return f"{hit / num:.4g}" 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
42@dataclass 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
43class PackageData: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
44 """Data we keep about each "package" (in Java terms)."""
46 elements: dict[str, xml.dom.minidom.Element] 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
47 hits: int 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
48 lines: int 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
49 br_hits: int 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
50 branches: int 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
53def appendChild(parent: Any, child: Any) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
54 """Append a child to a parent, in a way mypy will shut up about."""
55 parent.appendChild(child) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
58class XmlReporter: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
59 """A reporter for writing Cobertura-style XML coverage results."""
61 report_type = "XML report" 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
63 def __init__(self, coverage: Coverage) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
64 self.coverage = coverage 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
65 self.config = self.coverage.config 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
67 self.source_paths = set() 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
68 if self.config.source: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
69 for src in self.config.source: 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMNOPQRSTUVWXYZ01234
70 if os.path.exists(src): 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMNOPQRSTUVWXYZ01234
71 if self.config.relative_files: 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMNOPQRSTUVWXYZ01234
72 src = src.rstrip(r"\/") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
73 else:
74 src = files.canonical_filename(src) 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMNOPQRSTUVWXYZ01234
75 self.source_paths.add(src) 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMNOPQRSTUVWXYZ01234
76 self.packages: dict[str, PackageData] = {} 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
77 self.xml_out: xml.dom.minidom.Document 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
79 def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str] | None = None) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
80 """Generate a Cobertura-compatible XML report for `morfs`.
82 `morfs` is a list of modules or file names.
84 `outfile` is a file object to write the XML to.
86 """
87 # Initial setup.
88 outfile = outfile or sys.stdout 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
89 has_arcs = self.coverage.get_data().has_arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
91 # Create the DOM that will store the data.
92 impl = xml.dom.minidom.getDOMImplementation() 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
93 assert impl is not None 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
94 self.xml_out = impl.createDocument(None, "coverage", None) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
96 # Write header stuff.
97 xcoverage = self.xml_out.documentElement 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
98 assert xcoverage is not None 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
99 xcoverage.setAttribute("version", __version__) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
100 xcoverage.setAttribute("timestamp", str(int(time.time() * 1000))) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
101 xcoverage.appendChild( 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
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} ")) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
108 # Call xml_file for each file in the data.
109 for fr, analysis in get_analysis_to_report(self.coverage, morfs): 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
110 self.xml_file(fr, analysis, has_arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
112 xsources = self.xml_out.createElement("sources") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
113 xcoverage.appendChild(xsources) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
115 # Populate the XML DOM with the source info.
116 for path in human_sorted(self.source_paths): 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
117 xsource = self.xml_out.createElement("source") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
118 appendChild(xsources, xsource) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
119 txt = self.xml_out.createTextNode(path) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
120 appendChild(xsource, txt) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
122 lnum_tot, lhits_tot = 0, 0 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
123 bnum_tot, bhits_tot = 0, 0 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
125 xpackages = self.xml_out.createElement("packages") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
126 xcoverage.appendChild(xpackages) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
128 # Populate the XML DOM with the package info.
129 for pkg_name, pkg_data in human_sorted_items(self.packages.items()): 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
130 xpackage = self.xml_out.createElement("package") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
131 appendChild(xpackages, xpackage) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
132 xclasses = self.xml_out.createElement("classes") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
133 appendChild(xpackage, xclasses) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
134 for _, class_elt in human_sorted_items(pkg_data.elements.items()): 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
135 appendChild(xclasses, class_elt) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
136 xpackage.setAttribute("name", pkg_name.replace(os.sep, ".")) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
137 xpackage.setAttribute("line-rate", rate(pkg_data.hits, pkg_data.lines)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
138 if has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
139 branch_rate = rate(pkg_data.br_hits, pkg_data.branches) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
140 else:
141 branch_rate = "0" 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
142 xpackage.setAttribute("branch-rate", branch_rate) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
143 xpackage.setAttribute("complexity", "0") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
145 lhits_tot += pkg_data.hits 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
146 lnum_tot += pkg_data.lines 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
147 bhits_tot += pkg_data.br_hits 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
148 bnum_tot += pkg_data.branches 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
150 xcoverage.setAttribute("lines-valid", str(lnum_tot)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
151 xcoverage.setAttribute("lines-covered", str(lhits_tot)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
152 xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
153 if has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
154 xcoverage.setAttribute("branches-valid", str(bnum_tot)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
155 xcoverage.setAttribute("branches-covered", str(bhits_tot)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
156 xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
157 else:
158 xcoverage.setAttribute("branches-covered", "0") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
159 xcoverage.setAttribute("branches-valid", "0") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
160 xcoverage.setAttribute("branch-rate", "0") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
161 xcoverage.setAttribute("complexity", "0") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
163 # Write the output file.
164 outfile.write(serialize_xml(self.xml_out)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
166 # Return the total percentage.
167 denom = lnum_tot + bnum_tot 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
168 if denom == 0: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
169 pct = 0.0 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
170 else:
171 pct = 100.0 * (lhits_tot + bhits_tot) / denom 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
172 return pct 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
174 def xml_file(self, fr: FileReporter, analysis: Analysis, has_arcs: bool) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
175 """Add to the XML report for a single file."""
177 if self.config.skip_empty: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
178 if analysis.numbers.n_statements == 0: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
179 return 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
181 # Create the "lines" and "package" XML elements, which
182 # are populated later. Note that a package == a directory.
183 filename = fr.filename.replace("\\", "/") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
184 for source_path in self.source_paths: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
185 if not self.config.relative_files: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
186 source_path = files.canonical_filename(source_path) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
187 if filename.startswith(source_path.replace("\\", "/") + "/"): 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
188 rel_name = filename[len(source_path) + 1 :] 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMNOPQR6ST7UVWXYZ01234
189 break 1abcdefghijklmnopqrstuvwxyzABCDEFGH5IJKLMNOPQR6ST7UVWXYZ01234
190 else:
191 rel_name = fr.relative_filename().replace("\\", "/") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
192 self.source_paths.add(fr.filename[: -len(rel_name)].rstrip(r"\/")) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
194 dirname = os.path.dirname(rel_name) or "." 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
195 dirname = "/".join(dirname.split("/")[: self.config.xml_package_depth]) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
196 package_name = dirname.replace("/", ".") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
198 package = self.packages.setdefault(package_name, PackageData({}, 0, 0, 0, 0)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
200 xclass: xml.dom.minidom.Element = self.xml_out.createElement("class") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
202 appendChild(xclass, self.xml_out.createElement("methods")) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
204 xlines = self.xml_out.createElement("lines") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
205 appendChild(xclass, xlines) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
207 xclass.setAttribute("name", os.path.relpath(rel_name, dirname)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
208 xclass.setAttribute("filename", rel_name.replace("\\", "/")) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
209 xclass.setAttribute("complexity", "0") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
211 branch_stats = analysis.branch_stats() 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
212 missing_branch_arcs = analysis.missing_branch_arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
214 # For each statement, create an XML "line" element.
215 for line in sorted(analysis.statements): 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
216 xline = self.xml_out.createElement("line") 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
217 xline.setAttribute("number", str(line)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
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))) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
223 if has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
224 if line in branch_stats: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
225 total, taken = branch_stats[line] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
226 xline.setAttribute("branch", "true") 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
227 xline.setAttribute( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
228 "condition-coverage",
229 f"{100 * taken // total}% ({taken}/{total})",
230 )
231 if line in missing_branch_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
232 annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
233 xline.setAttribute("missing-branches", ",".join(annlines)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
234 appendChild(xlines, xline) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
236 class_lines = len(analysis.statements) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
237 class_hits = class_lines - len(analysis.missing) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
239 if has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
240 class_branches = sum(t for t, k in branch_stats.values()) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
241 missing_branches = sum(t - k for t, k in branch_stats.values()) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
242 class_br_hits = class_branches - missing_branches 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
243 else:
244 class_branches = 0 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
245 class_br_hits = 0 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
247 # Finalize the statistics that are collected in the XML DOM.
248 xclass.setAttribute("line-rate", rate(class_hits, class_lines)) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
249 if has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
250 branch_rate = rate(class_br_hits, class_branches) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
251 else:
252 branch_rate = "0" 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
253 xclass.setAttribute("branch-rate", branch_rate) 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
255 package.elements[rel_name] = xclass 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
256 package.hits += class_hits 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
257 package.lines += class_lines 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
258 package.br_hits += class_br_hits 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
259 package.branches += class_branches 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
262def serialize_xml(dom: xml.dom.minidom.Document) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234
263 """Serialize a minidom node to XML."""
264 return dom.toprettyxml() 1abcdefghijklmnopqrstuvwxyzABCDEF8GH5IJ9KL!MN#OP$QR6ST7UV%WX'YZ(01)234