The XML writer keyed each package's class elements on the file's name relative
to its own source root. Two source roots can each contain a package with the
same name, and their files then share that relative name, so the second one
replaced the first in the dict.
The line counts were accumulated separately and still included both files, so
the report advertised lines-valid=6 while containing only 3 line elements.
coverage report listed both files the whole time.
Key on the absolute filename instead. The class filename attribute still holds
the relative name, which consumers resolve against the sources roots already
emitted, and single-root output is byte-identical since the absolute paths share
a prefix there.
Closes coveragepy#1291
Closes #1291.
The bug
coverage report and coverage xml disagree about which files exist. With two
source roots that each contain a package of the same name:
coverage report lists both files, TOTAL 6 statements. coverage xml emits one
<class filename="settings/__init__.py"> holding 3 <line> elements. The other
file is absent.
The report is also internally inconsistent, which is the part that needs no
judgement call:
The document contains 3 <line> elements. Cobertura XML is what CI services read,
so a file silently vanishes from the artifact that gates merges while the totals
still count it.
Cause
xml_file() computes rel_name relative to whichever source root matched, then
stores the element as package.elements[rel_name]. Both files reduce to
settings/__init__.py, so the second replaces the first. The counters just below
it (package.lines, package.hits) are accumulated separately and still see both
files, which is exactly why the header and the body disagree.
Fix
Key on filename, the absolute path, which is unique per file. The <class filename=...> attribute still carries rel_name, so consumers resolve it against
the <sources> roots that are already emitted, and both roots are listed there.
Emitting two classes with the same relative name is not a new convention: as the
reporter notes, source = . already produces settings twice today.
Ordering is unchanged for the ordinary single-root case. The key is only used
for sorting, and with one root filename is source_path + "/" + rel_name, a
constant prefix, so the sort is identical. I checked this on a project with
pkg/zeta.py, pkg/alpha.py, pkg/mid.py and pkg/sub/deep.py, and the emitted
order is byte-for-byte the same before and after. Only the multi-root case
reorders, and that is the case that was broken.
Verification
lines-valid matches.
unmodified checkout, the difference being the new test.
The regression test asserts both that two classes are emitted and that
lines-valid equals the number of <line> elements, since the second is the
clearest statement of the defect.