summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/xmlreport.py13
-rw-r--r--tests/goldtest.py19
-rw-r--r--tests/test_plugins.py28
-rw-r--r--tests/test_process.py19
-rw-r--r--tests/test_xml.py292
5 files changed, 226 insertions, 145 deletions
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index 6c07337a..8ecdc24a 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -6,7 +6,6 @@
import os
import os.path
-import re
import sys
import time
import xml.dom.minidom
@@ -225,16 +224,4 @@ def serialize_xml(dom):
out = dom.toprettyxml()
if env.PY2:
out = out.encode("utf8")
- # In Python 3.8, minidom lost the sorting of attributes: https://bugs.python.org/issue34160
- # For the limited kinds of XML we produce, this re-sorts them.
- if env.PYVERSION >= (3, 8):
- rx_attr = r' [\w-]+="[^"]*"'
- rx_attrs = r'(' + rx_attr + ')+'
- fixed_lines = []
- for line in out.splitlines(True):
- hollow_line = re.sub(rx_attrs, u"☺", line)
- attrs = sorted(re.findall(rx_attr, line))
- new_line = hollow_line.replace(u"☺", "".join(attrs))
- fixed_lines.append(new_line)
- out = "".join(fixed_lines)
return out
diff --git a/tests/goldtest.py b/tests/goldtest.py
index 4c6c3c96..b5e32f5f 100644
--- a/tests/goldtest.py
+++ b/tests/goldtest.py
@@ -10,6 +10,7 @@ import os
import os.path
import re
import sys
+import xml.etree.ElementTree
from unittest_mixins import change_dir # pylint: disable=unused-import
@@ -79,12 +80,19 @@ def compare(
# ourselves.
text_diff = []
for f in diff_files:
+
expected_file = os.path.join(expected_dir, f)
- actual_file = os.path.join(actual_dir, f)
with open(expected_file, READ_MODE) as fobj:
expected = fobj.read()
+ if expected_file.endswith(".xml"):
+ expected = canonicalize_xml(expected)
+
+ actual_file = os.path.join(actual_dir, f)
with open(actual_file, READ_MODE) as fobj:
actual = fobj.read()
+ if actual_file.endswith(".xml"):
+ actual = canonicalize_xml(actual)
+
if scrubs:
expected = scrub(expected, scrubs)
actual = scrub(actual, scrubs)
@@ -102,6 +110,15 @@ def compare(
assert not actual_only, "Files in %s only: %s" % (actual_dir, actual_only)
+def canonicalize_xml(xtext):
+ """Canonicalize some XML text."""
+ root = xml.etree.ElementTree.fromstring(xtext)
+ for node in root.iter():
+ node.attrib = dict(sorted(node.items()))
+ xtext = xml.etree.ElementTree.tostring(root)
+ return xtext.decode('utf8')
+
+
def contains(filename, *strlist):
"""Check that the file contains all of a list of strings.
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index d3365a6d..14d07c1a 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -4,6 +4,7 @@
"""Tests for plugins."""
import os.path
+from xml.etree import ElementTree
import coverage
from coverage import env
@@ -455,14 +456,25 @@ class GoodFileTracerTest(FileTracerTest):
total = cov.xml_report(include=["*.html"], omit=["uni*.html"])
self.assertAlmostEqual(total, 36.36, places=2)
- with open("coverage.xml") as fxml:
- xml = fxml.read()
-
- for snip in [
- 'filename="bar_4.html" line-rate="0.5" name="bar_4.html"',
- 'filename="foo_7.html" line-rate="0.2857" name="foo_7.html"',
- ]:
- self.assertIn(snip, xml)
+ dom = ElementTree.parse("coverage.xml")
+ classes = {}
+ for elt in dom.findall(".//class"):
+ classes[elt.get('name')] = elt
+
+ assert classes['bar_4.html'].attrib == {
+ 'branch-rate': '1',
+ 'complexity': '0',
+ 'filename': 'bar_4.html',
+ 'line-rate': '0.5',
+ 'name': 'bar_4.html',
+ }
+ assert classes['foo_7.html'].attrib == {
+ 'branch-rate': '1',
+ 'complexity': '0',
+ 'filename': 'foo_7.html',
+ 'line-rate': '0.2857',
+ 'name': 'foo_7.html',
+ }
def test_defer_to_python(self):
# A plugin that measures, but then wants built-in python reporting.
diff --git a/tests/test_process.py b/tests/test_process.py
index a1e71f1c..f234a9ef 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -11,6 +11,7 @@ import os.path
import re
import sys
import textwrap
+from xml.etree import ElementTree
import pytest
@@ -1266,12 +1267,18 @@ class UnicodeFilePathsTest(CoverageTest):
self.assertEqual(out, "")
with open("coverage.xml", "rb") as xmlf:
xml = xmlf.read()
- self.assertIn(u' filename="\xe2/accented.py"'.encode('utf8'), xml)
- self.assertIn(u' name="accented.py"'.encode('utf8'), xml)
- self.assertIn(
- u'<package branch-rate="0" complexity="0" line-rate="1" name="\xe2">'.encode('utf8'),
- xml
- )
+ self.assertIn(b' filename="\xc3\xa2/accented.py"', xml)
+ self.assertIn(b' name="accented.py"', xml)
+
+ dom = ElementTree.parse("coverage.xml")
+ elts = dom.findall(u".//package[@name='â']")
+ assert len(elts) == 1
+ assert elts[0].attrib == {
+ "branch-rate": u"0",
+ "complexity": u"0",
+ "line-rate": u"1",
+ "name": u"â",
+ }
report_expected = (
u"Name Stmts Miss Cover\n"
diff --git a/tests/test_xml.py b/tests/test_xml.py
index 185e6ad1..09ab2f85 100644
--- a/tests/test_xml.py
+++ b/tests/test_xml.py
@@ -7,6 +7,7 @@
import os
import os.path
import re
+from xml.etree import ElementTree
import coverage
from coverage.backward import import_local_file
@@ -14,7 +15,6 @@ from coverage.files import abs_file
from tests.coveragetest import CoverageTest
from tests.goldtest import change_dir, compare, gold_path
-from tests.helpers import re_line, re_lines
class XmlTestHelpers(CoverageTest):
@@ -30,7 +30,7 @@ class XmlTestHelpers(CoverageTest):
self.make_file("sub/__init__.py")
self.make_file("sub/doit.py", "print('doit!')")
self.make_file("main.py", "import sub.doit")
- cov = coverage.Coverage()
+ cov = coverage.Coverage(source=["."])
self.start_import_stop(cov, "main")
return cov
@@ -59,10 +59,36 @@ class XmlTestHelpers(CoverageTest):
filename = here("f{0}.py".format(i))
self.make_file(filename, "# {0}\n".format(filename))
- def assert_source(self, xml, src):
+ def assert_source(self, xmldom, src):
"""Assert that the XML has a <source> element with `src`."""
src = abs_file(src)
- self.assertRegex(xml, r'<source>\s*{0}\s*</source>'.format(re.escape(src)))
+ elts = xmldom.findall(".//sources/source")
+ assert any(elt.text == src for elt in elts)
+
+
+class XmlTestHelpersTest(XmlTestHelpers, CoverageTest):
+ """Tests of methods in XmlTestHelpers."""
+
+ def test_assert_source(self):
+ dom = ElementTree.fromstring("""\
+ <doc>
+ <src>foo</src>
+ <sources>
+ <source>{cwd}something</source>
+ <source>{cwd}another</source>
+ </sources>
+ </doc>
+ """.format(cwd=abs_file(".")+os.sep))
+
+ self.assert_source(dom, "something")
+ self.assert_source(dom, "another")
+
+ with self.assertRaises(AssertionError):
+ self.assert_source(dom, "hello")
+ with self.assertRaises(AssertionError):
+ self.assert_source(dom, "foo")
+ with self.assertRaises(AssertionError):
+ self.assert_source(dom, "thing")
class XmlReportTest(XmlTestHelpers, CoverageTest):
@@ -110,25 +136,28 @@ class XmlReportTest(XmlTestHelpers, CoverageTest):
def test_filename_format_showing_everything(self):
cov = self.run_doit()
- cov.xml_report(outfile="-")
- xml = self.stdout()
- doit_line = re_line(xml, "class.*doit")
- self.assertIn('filename="sub/doit.py"', doit_line)
+ cov.xml_report()
+ dom = ElementTree.parse("coverage.xml")
+ elts = dom.findall(".//class[@name='doit.py']")
+ assert len(elts) == 1
+ assert elts[0].get('filename') == "sub/doit.py"
def test_filename_format_including_filename(self):
cov = self.run_doit()
- cov.xml_report(["sub/doit.py"], outfile="-")
- xml = self.stdout()
- doit_line = re_line(xml, "class.*doit")
- self.assertIn('filename="sub/doit.py"', doit_line)
+ cov.xml_report(["sub/doit.py"])
+ dom = ElementTree.parse("coverage.xml")
+ elts = dom.findall(".//class[@name='doit.py']")
+ assert len(elts) == 1
+ assert elts[0].get('filename') == "sub/doit.py"
def test_filename_format_including_module(self):
cov = self.run_doit()
import sub.doit # pylint: disable=import-error
- cov.xml_report([sub.doit], outfile="-")
- xml = self.stdout()
- doit_line = re_line(xml, "class.*doit")
- self.assertIn('filename="sub/doit.py"', doit_line)
+ cov.xml_report([sub.doit])
+ dom = ElementTree.parse("coverage.xml")
+ elts = dom.findall(".//class[@name='doit.py']")
+ assert len(elts) == 1
+ assert elts[0].get('filename') == "sub/doit.py"
def test_reporting_on_nothing(self):
# Used to raise a zero division error:
@@ -136,28 +165,31 @@ class XmlReportTest(XmlTestHelpers, CoverageTest):
self.make_file("empty.py", "")
cov = coverage.Coverage()
empty = self.start_import_stop(cov, "empty")
- cov.xml_report([empty], outfile="-")
- xml = self.stdout()
- empty_line = re_line(xml, "class.*empty")
- self.assertIn('filename="empty.py"', empty_line)
- self.assertIn('line-rate="1"', empty_line)
+ cov.xml_report([empty])
+ dom = ElementTree.parse("coverage.xml")
+ elts = dom.findall(".//class[@name='empty.py']")
+ assert len(elts) == 1
+ assert elts[0].get('filename') == "empty.py"
+ assert elts[0].get('line-rate') == '1'
def test_empty_file_is_100_not_0(self):
# https://bitbucket.org/ned/coveragepy/issue/345
cov = self.run_doit()
- cov.xml_report(outfile="-")
- xml = self.stdout()
- init_line = re_line(xml, 'filename="sub/__init__.py"')
- self.assertIn('line-rate="1"', init_line)
+ cov.xml_report()
+ dom = ElementTree.parse("coverage.xml")
+ elts = dom.findall(".//class[@name='__init__.py']")
+ assert len(elts) == 1
+ assert elts[0].get('line-rate') == '1'
def test_curdir_source(self):
# With no source= option, the XML report should explain that the source
# is in the current directory.
cov = self.run_doit()
- cov.xml_report(outfile="-")
- xml = self.stdout()
- self.assert_source(xml, ".")
- self.assertEqual(xml.count('<source>'), 1)
+ cov.xml_report()
+ dom = ElementTree.parse("coverage.xml")
+ self.assert_source(dom, ".")
+ sources = dom.findall(".//source")
+ assert len(sources) == 1
def test_deep_source(self):
# When using source=, the XML report needs to mention those directories
@@ -170,21 +202,33 @@ class XmlReportTest(XmlTestHelpers, CoverageTest):
mod_foo = import_local_file("foo", "src/main/foo.py") # pragma: nested
mod_bar = import_local_file("bar", "also/over/there/bar.py") # pragma: nested
cov.stop() # pragma: nested
- cov.xml_report([mod_foo, mod_bar], outfile="-")
- xml = self.stdout()
-
- self.assert_source(xml, "src/main")
- self.assert_source(xml, "also/over/there")
- self.assertEqual(xml.count('<source>'), 2)
-
- self.assertIn(
- '<class branch-rate="0" complexity="0" filename="foo.py" line-rate="1" name="foo.py">',
- xml
- )
- self.assertIn(
- '<class branch-rate="0" complexity="0" filename="bar.py" line-rate="1" name="bar.py">',
- xml
- )
+ cov.xml_report([mod_foo, mod_bar])
+ dom = ElementTree.parse("coverage.xml")
+
+ self.assert_source(dom, "src/main")
+ self.assert_source(dom, "also/over/there")
+ sources = dom.findall(".//source")
+ assert len(sources) == 2
+
+ foo_class = dom.findall(".//class[@name='foo.py']")
+ assert len(foo_class) == 1
+ assert foo_class[0].attrib == {
+ 'branch-rate': '0',
+ 'complexity': '0',
+ 'filename': 'foo.py',
+ 'line-rate': '1',
+ 'name': 'foo.py',
+ }
+
+ bar_class = dom.findall(".//class[@name='bar.py']")
+ assert len(bar_class) == 1
+ assert bar_class[0].attrib == {
+ 'branch-rate': '0',
+ 'complexity': '0',
+ 'filename': 'bar.py',
+ 'line-rate': '1',
+ 'name': 'bar.py',
+ }
def test_nonascii_directory(self):
# https://bitbucket.org/ned/coveragepy/issues/573/cant-generate-xml-report-if-some-source
@@ -195,22 +239,33 @@ class XmlReportTest(XmlTestHelpers, CoverageTest):
cov.xml_report()
+def unbackslash(v):
+ """Find strings in `v`, and replace backslashes with slashes throughout."""
+ if isinstance(v, (tuple, list)):
+ return [unbackslash(vv) for vv in v]
+ elif isinstance(v, dict):
+ return {k: unbackslash(vv) for k, vv in v.items()}
+ else:
+ assert isinstance(v, str)
+ return v.replace("\\", "/")
+
+
class XmlPackageStructureTest(XmlTestHelpers, CoverageTest):
"""Tests about the package structure reported in the coverage.xml file."""
def package_and_class_tags(self, cov):
"""Run an XML report on `cov`, and get the package and class tags."""
- self.captured_stdout.truncate(0)
- cov.xml_report(outfile="-")
- packages_and_classes = re_lines(self.stdout(), r"<package |<class ")
- scrubs = r' branch-rate="0"| complexity="0"| line-rate="[\d.]+"'
- return clean(packages_and_classes, scrubs)
+ cov.xml_report()
+ dom = ElementTree.parse("coverage.xml")
+ for node in dom.iter():
+ if node.tag in ('package', 'class'):
+ yield (node.tag, {a:v for a,v in node.items() if a in ('name', 'filename')})
def assert_package_and_class_tags(self, cov, result):
"""Check the XML package and class tags from `cov` match `result`."""
- self.assertMultiLineEqual(
- self.package_and_class_tags(cov),
- clean(result)
+ self.assertEqual(
+ unbackslash(list(self.package_and_class_tags(cov))),
+ unbackslash(result),
)
def test_package_names(self):
@@ -220,18 +275,18 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest):
""")
cov = coverage.Coverage(source=".")
self.start_import_stop(cov, "main")
- self.assert_package_and_class_tags(cov, """\
- <package name=".">
- <class filename="main.py" name="main.py">
- <package name="d0">
- <class filename="d0/__init__.py" name="__init__.py">
- <class filename="d0/f0.py" name="f0.py">
- <package name="d0.d0">
- <class filename="d0/d0/__init__.py" name="__init__.py">
- <class filename="d0/d0/f0.py" name="f0.py">
- """)
-
- def test_package_depth(self):
+ self.assert_package_and_class_tags(cov, [
+ ('package', {'name': "."}),
+ ('class', {'filename': "main.py", 'name': "main.py"}),
+ ('package', {'name': "d0"}),
+ ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/f0.py", 'name': "f0.py"}),
+ ('package', {'name': "d0.d0"}),
+ ('class', {'filename': "d0/d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}),
+ ])
+
+ def test_package_depth_1(self):
self.make_tree(width=1, depth=4)
self.make_file("main.py", """\
from d0.d0 import f0
@@ -240,46 +295,62 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest):
self.start_import_stop(cov, "main")
cov.set_option("xml:package_depth", 1)
- self.assert_package_and_class_tags(cov, """\
- <package name=".">
- <class filename="main.py" name="main.py">
- <package name="d0">
- <class filename="d0/__init__.py" name="__init__.py">
- <class filename="d0/d0/__init__.py" name="d0/__init__.py">
- <class filename="d0/d0/d0/__init__.py" name="d0/d0/__init__.py">
- <class filename="d0/d0/d0/f0.py" name="d0/d0/f0.py">
- <class filename="d0/d0/f0.py" name="d0/f0.py">
- <class filename="d0/f0.py" name="f0.py">
+ self.assert_package_and_class_tags(cov, [
+ ('package', {'name': "."}),
+ ('class', {'filename': "main.py", 'name': "main.py"}),
+ ('package', {'name': "d0"}),
+ ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/d0/__init__.py", 'name': "d0/__init__.py"}),
+ ('class', {'filename': "d0/d0/d0/__init__.py", 'name': "d0/d0/__init__.py"}),
+ ('class', {'filename': "d0/d0/d0/f0.py", 'name': "d0/d0/f0.py"}),
+ ('class', {'filename': "d0/d0/f0.py", 'name': "d0/f0.py"}),
+ ('class', {'filename': "d0/f0.py", 'name': "f0.py"}),
+ ])
+
+ def test_package_depth_2(self):
+ self.make_tree(width=1, depth=4)
+ self.make_file("main.py", """\
+ from d0.d0 import f0
""")
+ cov = coverage.Coverage(source=".")
+ self.start_import_stop(cov, "main")
cov.set_option("xml:package_depth", 2)
- self.assert_package_and_class_tags(cov, """\
- <package name=".">
- <class filename="main.py" name="main.py">
- <package name="d0">
- <class filename="d0/__init__.py" name="__init__.py">
- <class filename="d0/f0.py" name="f0.py">
- <package name="d0.d0">
- <class filename="d0/d0/__init__.py" name="__init__.py">
- <class filename="d0/d0/d0/__init__.py" name="d0/__init__.py">
- <class filename="d0/d0/d0/f0.py" name="d0/f0.py">
- <class filename="d0/d0/f0.py" name="f0.py">
+ self.assert_package_and_class_tags(cov, [
+ ('package', {'name': "."}),
+ ('class', {'filename': "main.py", 'name': "main.py"}),
+ ('package', {'name': "d0"}),
+ ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/f0.py", 'name': "f0.py"}),
+ ('package', {'name': "d0.d0"}),
+ ('class', {'filename': "d0/d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/d0/d0/__init__.py", 'name': "d0/__init__.py"}),
+ ('class', {'filename': "d0/d0/d0/f0.py", 'name': "d0/f0.py"}),
+ ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}),
+ ])
+
+ def test_package_depth_3(self):
+ self.make_tree(width=1, depth=4)
+ self.make_file("main.py", """\
+ from d0.d0 import f0
""")
+ cov = coverage.Coverage(source=".")
+ self.start_import_stop(cov, "main")
cov.set_option("xml:package_depth", 3)
- self.assert_package_and_class_tags(cov, """\
- <package name=".">
- <class filename="main.py" name="main.py">
- <package name="d0">
- <class filename="d0/__init__.py" name="__init__.py">
- <class filename="d0/f0.py" name="f0.py">
- <package name="d0.d0">
- <class filename="d0/d0/__init__.py" name="__init__.py">
- <class filename="d0/d0/f0.py" name="f0.py">
- <package name="d0.d0.d0">
- <class filename="d0/d0/d0/__init__.py" name="__init__.py">
- <class filename="d0/d0/d0/f0.py" name="f0.py">
- """)
+ self.assert_package_and_class_tags(cov, [
+ ('package', {'name': "."}),
+ ('class', {'filename': "main.py", 'name': "main.py"}),
+ ('package', {'name': "d0"}),
+ ('class', {'filename': "d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/f0.py", 'name': "f0.py"}),
+ ('package', {'name': "d0.d0"}),
+ ('class', {'filename': "d0/d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/d0/f0.py", 'name': "f0.py"}),
+ ('package', {'name': "d0.d0.d0"}),
+ ('class', {'filename': "d0/d0/d0/__init__.py", 'name': "__init__.py"}),
+ ('class', {'filename': "d0/d0/d0/f0.py", 'name': "f0.py"}),
+ ])
def test_source_prefix(self):
# https://bitbucket.org/ned/coveragepy/issues/465
@@ -287,26 +358,13 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest):
self.make_file("src/mod.py", "print(17)")
cov = coverage.Coverage(source=["src"])
self.start_import_stop(cov, "mod", modfile="src/mod.py")
- self.assert_package_and_class_tags(cov, """\
- <package name=".">
- <class filename="mod.py" name="mod.py">
- """)
- xml = self.stdout()
- self.assert_source(xml, "src")
-
-
-def clean(text, scrub=None):
- """Clean text to prepare it for comparison.
-
- Remove text matching `scrub`, and leading whitespace. Convert backslashes
- to forward slashes.
- """
- if scrub:
- text = re.sub(scrub, "", text)
- text = re.sub(r"(?m)^\s+", "", text)
- text = re.sub(r"\\", "/", text)
- return text
+ self.assert_package_and_class_tags(cov, [
+ ('package', {'name': "."}),
+ ('class', {'filename': "mod.py", 'name': "mod.py"}),
+ ])
+ dom = ElementTree.parse("coverage.xml")
+ self.assert_source(dom, "src")
def compare_xml(expected, actual, **kwargs):