# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for XML reports from coverage.py.""" import os import os.path import re from xml.etree import ElementTree import pytest import coverage from coverage.exceptions import NoDataError from coverage.files import abs_file from coverage.misc import import_local_file from tests.coveragetest import CoverageTest from tests.goldtest import compare, gold_path from tests.helpers import assert_coverage_warnings, change_dir class XmlTestHelpers(CoverageTest): """Methods to use from XML tests.""" def run_doit(self): """Construct a simple sub-package.""" 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(source=["."]) self.start_import_stop(cov, "main") return cov def make_tree(self, width, depth, curdir="."): """Make a tree of packages. Makes `width` directories, named d0 .. d{width-1}. Each directory has __init__.py, and `width` files, named f0.py .. f{width-1}.py. Each directory also has `width` sub-directories, in the same fashion, until a depth of `depth` is reached. """ if depth == 0: return def here(p): """A path for `p` in our currently interesting directory.""" return os.path.join(curdir, p) for i in range(width): next_dir = here(f"d{i}") self.make_tree(width, depth-1, next_dir) if curdir != ".": self.make_file(here("__init__.py"), "") for i in range(width): filename = here(f"f{i}.py") self.make_file(filename, f"# {filename}\n") def assert_source(self, xmldom, src): """Assert that the XML has a element with `src`.""" src = abs_file(src) elts = xmldom.findall(".//sources/source") assert any(elt.text == src for elt in elts) class XmlTestHelpersTest(XmlTestHelpers, CoverageTest): """Tests of methods in XmlTestHelpers.""" run_in_temp_dir = False def test_assert_source(self): dom = ElementTree.fromstring("""\ foo {cwd}something {cwd}another """.format(cwd=abs_file(".")+os.sep)) self.assert_source(dom, "something") self.assert_source(dom, "another") with pytest.raises(AssertionError): self.assert_source(dom, "hello") with pytest.raises(AssertionError): self.assert_source(dom, "foo") with pytest.raises(AssertionError): self.assert_source(dom, "thing") class XmlReportTest(XmlTestHelpers, CoverageTest): """Tests of the XML reports from coverage.py.""" def make_mycode_data(self): """Pretend that we ran mycode.py, so we can report on it.""" self.make_file("mycode.py", "print('hello')\n") self.make_data_file(lines={abs_file("mycode.py"): [1]}) def run_xml_report(self, **kwargs): """Run xml_report()""" cov = coverage.Coverage() cov.load() cov.xml_report(**kwargs) def test_default_file_placement(self): self.make_mycode_data() self.run_xml_report() self.assert_exists("coverage.xml") assert self.stdout() == "" def test_argument_affects_xml_placement(self): self.make_mycode_data() cov = coverage.Coverage(messages=True) cov.load() cov.xml_report(outfile="put_it_there.xml") assert self.stdout() == "Wrote XML report to put_it_there.xml\n" self.assert_doesnt_exist("coverage.xml") self.assert_exists("put_it_there.xml") def test_output_directory_does_not_exist(self): self.make_mycode_data() self.run_xml_report(outfile="nonexistent/put_it_there.xml") self.assert_doesnt_exist("coverage.xml") self.assert_doesnt_exist("put_it_there.xml") self.assert_exists("nonexistent/put_it_there.xml") def test_config_affects_xml_placement(self): self.make_mycode_data() self.make_file(".coveragerc", "[xml]\noutput = xml.out\n") self.run_xml_report() self.assert_doesnt_exist("coverage.xml") self.assert_exists("xml.out") def test_no_data(self): # https://github.com/nedbat/coveragepy/issues/210 with pytest.raises(NoDataError, match="No data to report."): self.run_xml_report() self.assert_doesnt_exist("coverage.xml") def test_no_source(self): # Written while investigating a bug, might as well keep it. # https://github.com/nedbat/coveragepy/issues/208 self.make_file("innocuous.py", "a = 4") cov = coverage.Coverage() self.start_import_stop(cov, "innocuous") os.remove("innocuous.py") with pytest.warns(Warning) as warns: cov.xml_report(ignore_errors=True) assert_coverage_warnings( warns, re.compile(r"Couldn't parse '.*innocuous.py'. \(couldnt-parse\)"), ) self.assert_exists("coverage.xml") def test_filename_format_showing_everything(self): cov = self.run_doit() 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"]) 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]) 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: # https://github.com/nedbat/coveragepy/issues/250 self.make_file("empty.py", "") cov = coverage.Coverage() empty = self.start_import_stop(cov, "empty") 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://github.com/nedbat/coveragepy/issues/345 cov = self.run_doit() 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_empty_file_is_skipped(self): cov = self.run_doit() cov.xml_report(skip_empty=True) dom = ElementTree.parse("coverage.xml") elts = dom.findall(".//class[@name='__init__.py']") assert len(elts) == 0 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() 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 # in the elements. # https://github.com/nedbat/coveragepy/issues/439 self.make_file("src/main/foo.py", "a = 1") self.make_file("also/over/there/bar.py", "b = 2") cov = coverage.Coverage(source=["src/main", "also/over/there", "not/really"]) cov.start() 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 with pytest.warns(Warning) as warns: cov.xml_report([mod_foo, mod_bar]) assert_coverage_warnings( warns, "Module not/really was never imported. (module-not-imported)", ) 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://github.com/nedbat/coveragepy/issues/573 self.make_file("테스트/program.py", "a = 1") with change_dir("테스트"): cov = coverage.Coverage() self.start_import_stop(cov, "program") cov.xml_report() def test_accented_dot_py(self): # Make a file with a non-ascii character in the filename. self.make_file("h\xe2t.py", "print('accented')") self.make_data_file(lines={abs_file("h\xe2t.py"): [1]}) cov = coverage.Coverage() cov.load() cov.xml_report() # The XML report is always UTF8-encoded. with open("coverage.xml", "rb") as xmlf: xml = xmlf.read() assert ' filename="h\xe2t.py"'.encode() in xml assert ' name="h\xe2t.py"'.encode() in xml def test_accented_directory(self): # Make a file with a non-ascii character in the directory name. self.make_file("\xe2/accented.py", "print('accented')") self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) # The XML report is always UTF8-encoded. cov = coverage.Coverage() cov.load() cov.xml_report() with open("coverage.xml", "rb") as xmlf: xml = xmlf.read() assert b' filename="\xc3\xa2/accented.py"' in xml assert b' name="accented.py"' in xml dom = ElementTree.parse("coverage.xml") elts = dom.findall(".//package[@name='â']") assert len(elts) == 1 assert elts[0].attrib == { "branch-rate": "0", "complexity": "0", "line-rate": "1", "name": "â", } 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.""" 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`.""" assert unbackslash(list(self.package_and_class_tags(cov))) == unbackslash(result) def test_package_names(self): self.make_tree(width=1, depth=3) self.make_file("main.py", """\ from d0.d0 import f0 """) 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_1(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", 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"}), ]) 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"}), ]) 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"}), ]) def test_source_prefix(self): # https://github.com/nedbat/coveragepy/issues/465 # https://github.com/nedbat/coveragepy/issues/526 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"}), ]) dom = ElementTree.parse("coverage.xml") self.assert_source(dom, "src") def test_relative_source(self): self.make_file("src/mod.py", "print(17)") cov = coverage.Coverage(source=["src"]) cov.set_option("run:relative_files", True) self.start_import_stop(cov, "mod", modfile="src/mod.py") cov.xml_report() with open("coverage.xml") as x: print(x.read()) dom = ElementTree.parse("coverage.xml") elts = dom.findall(".//sources/source") assert [elt.text for elt in elts] == ["src"] def compare_xml(expected, actual, **kwargs): """Specialized compare function for our XML files.""" source_path = coverage.files.relative_directory().rstrip(r"\/") scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'\s*.*?\s*', '%s' % re.escape(source_path)), (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), ] compare(expected, actual, scrubs=scrubs, **kwargs) class XmlGoldTest(CoverageTest): """Tests of XML reporting that use gold files.""" def test_a_xml_1(self): self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. a = 3 else: a = 4 """) cov = coverage.Coverage() a = self.start_import_stop(cov, "a") cov.xml_report(a, outfile="coverage.xml") compare_xml(gold_path("xml/x_xml"), ".", actual_extra=True) def test_a_xml_2(self): self.make_file("a.py", """\ if 1 < 2: # Needed a < to look at HTML entities. a = 3 else: a = 4 """) self.make_file("run_a_xml_2.ini", """\ # Put all the XML output in xml_2 [xml] output = xml_2/coverage.xml """) cov = coverage.Coverage(config_file="run_a_xml_2.ini") a = self.start_import_stop(cov, "a") cov.xml_report(a) compare_xml(gold_path("xml/x_xml"), "xml_2") def test_y_xml_branch(self): self.make_file("y.py", """\ def choice(x): if x < 2: return 3 else: return 4 assert choice(1) == 3 """) cov = coverage.Coverage(branch=True) y = self.start_import_stop(cov, "y") cov.xml_report(y, outfile="y_xml_branch/coverage.xml") compare_xml(gold_path("xml/y_xml_branch"), "y_xml_branch")