blob: 185e6ad1ff81987b5f114c4f32477464babe3828 [file] [log] [blame]
# coding: utf-8
# 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
import coverage
from coverage.backward import import_local_file
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):
"""Methods to use from XML tests."""
def run_mycode(self):
"""Run mycode.py, so we can report on it."""
self.make_file("mycode.py", "print('hello')\n")
self.run_command("coverage run mycode.py")
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()
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("d{0}".format(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{0}.py".format(i))
self.make_file(filename, "# {0}\n".format(filename))
def assert_source(self, xml, 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)))
class XmlReportTest(XmlTestHelpers, CoverageTest):
"""Tests of the XML reports from coverage.py."""
def test_default_file_placement(self):
self.run_mycode()
self.run_command("coverage xml")
self.assert_exists("coverage.xml")
def test_argument_affects_xml_placement(self):
self.run_mycode()
self.run_command("coverage xml -o put_it_there.xml")
self.assert_doesnt_exist("coverage.xml")
self.assert_exists("put_it_there.xml")
def test_config_file_directory_does_not_exist(self):
self.run_mycode()
self.run_command("coverage xml -o 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.run_mycode()
self.make_file(".coveragerc", "[xml]\noutput = xml.out\n")
self.run_command("coverage xml")
self.assert_doesnt_exist("coverage.xml")
self.assert_exists("xml.out")
def test_no_data(self):
# https://bitbucket.org/ned/coveragepy/issue/210
self.run_command("coverage xml")
self.assert_doesnt_exist("coverage.xml")
def test_no_source(self):
# Written while investigating a bug, might as well keep it.
# https://bitbucket.org/ned/coveragepy/issue/208
self.make_file("innocuous.py", "a = 4")
cov = coverage.Coverage()
self.start_import_stop(cov, "innocuous")
os.remove("innocuous.py")
cov.xml_report(ignore_errors=True)
self.assert_exists("coverage.xml")
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)
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)
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)
def test_reporting_on_nothing(self):
# Used to raise a zero division error:
# https://bitbucket.org/ned/coveragepy/issue/250
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)
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)
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)
def test_deep_source(self):
# When using source=, the XML report needs to mention those directories
# in the <source> elements.
# https://bitbucket.org/ned/coveragepy/issues/439/incorrect-cobertura-file-sources-generated
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
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
)
def test_nonascii_directory(self):
# https://bitbucket.org/ned/coveragepy/issues/573/cant-generate-xml-report-if-some-source
self.make_file("테스트/program.py", "a = 1")
with change_dir("테스트"):
cov = coverage.Coverage()
self.start_import_stop(cov, "program")
cov.xml_report()
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)
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)
)
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(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">
""")
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">
""")
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://bitbucket.org/ned/coveragepy/issues/465
# https://bitbucket.org/ned/coveragepy/issues/526/generated-xml-invalid-paths-for-cobertura
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
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'<source>\s*.*?\s*</source>', '<source>%s</source>' % 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")