diff options
-rw-r--r-- | coverage/xmlreport.py | 13 | ||||
-rw-r--r-- | tests/goldtest.py | 19 | ||||
-rw-r--r-- | tests/test_plugins.py | 28 | ||||
-rw-r--r-- | tests/test_process.py | 19 | ||||
-rw-r--r-- | tests/test_xml.py | 292 |
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): |