summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt9
-rw-r--r--coverage/config.py6
-rw-r--r--coverage/control.py29
-rw-r--r--coverage/files.py33
-rw-r--r--coverage/report.py5
-rw-r--r--test/test_api.py186
-rw-r--r--test/test_files.py7
7 files changed, 142 insertions, 133 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 57a7df29..0da1d949 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -5,6 +5,14 @@ Change history for Coverage.py
Version 3.5.4b1
---------------
+- Wildcards in ``include=`` and ``omit=`` arguments were not handled properly
+ in reporting functions, though they were when running. Now they are handled
+ uniformly, closing `issue 163`. **NOTE**: it is possible that your
+ configurations may now be incorrect. If you use ``include`` or ``omit``
+ during reporting, whether on the command line, through the API, or in a
+ configuration file, please check carefully that you were not relying on the
+ old broken behavior.
+
- Running an HTML report in Python 3 in the same directory as an old Python 2
HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_)
is now fixed.
@@ -15,6 +23,7 @@ Version 3.5.4b1
- Docstrings for the legacy singleton methods are more helpful. Thanks Marius
Gedminas. Closes `issue 205`_.
+.. _issue 163: https://bitbucket.org/ned/coveragepy/issue/163/problem-with-include-and-omit-filename
.. _issue 193: https://bitbucket.org/ned/coveragepy/issue/193/unicodedecodeerror-on-htmlpy
.. _issue 201: https://bitbucket.org/ned/coveragepy/issue/201/coverage-using-django-14-with-pydb-on
.. _issue 205: https://bitbucket.org/ned/coveragepy/issue/205/make-pydoc-coverage-more-friendly
diff --git a/coverage/config.py b/coverage/config.py
index 49d74e7a..0d1da5f4 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -2,6 +2,7 @@
import os
from coverage.backward import configparser # pylint: disable=W0622
+from coverage.backward import string_class # pylint: disable=W0622
# The default line exclusion regexes
DEFAULT_EXCLUDE = [
@@ -69,10 +70,14 @@ class CoverageConfig(object):
if env:
self.timid = ('--timid' in env)
+ MUST_BE_LIST = ["omit", "include"]
+
def from_args(self, **kwargs):
"""Read config values from `kwargs`."""
for k, v in kwargs.items():
if v is not None:
+ if k in self.MUST_BE_LIST and isinstance(v, string_class):
+ v = [v]
setattr(self, k, v)
def from_file(self, *files):
@@ -167,4 +172,3 @@ class CoverageConfig(object):
"""
value_list = cp.get(section, option)
return list(filter(None, value_list.split('\n')))
-
diff --git a/coverage/control.py b/coverage/control.py
index c21d885e..acca99ee 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -9,7 +9,7 @@ from coverage.collector import Collector
from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
-from coverage.files import PathAliases, find_python_files
+from coverage.files import PathAliases, find_python_files, prep_patterns
from coverage.html import HtmlReporter
from coverage.misc import CoverageException, bool_or_none, join_regex
from coverage.results import Analysis, Numbers
@@ -96,10 +96,6 @@ class coverage(object):
self.config.data_file = env_data_file
# 4: from constructor arguments:
- if isinstance(omit, string_class):
- omit = [omit]
- if isinstance(include, string_class):
- include = [include]
self.config.from_args(
data_file=data_file, cover_pylib=cover_pylib, timid=timid,
branch=branch, parallel=bool_or_none(data_suffix),
@@ -125,8 +121,8 @@ class coverage(object):
else:
self.source_pkgs.append(src)
- self.omit = self._prep_patterns(self.config.omit)
- self.include = self._prep_patterns(self.config.include)
+ self.omit = prep_patterns(self.config.omit)
+ self.include = prep_patterns(self.config.include)
self.collector = Collector(
self._should_trace, timid=self.config.timid,
@@ -281,25 +277,6 @@ class coverage(object):
self._warnings.append(msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
- def _prep_patterns(self, patterns):
- """Prepare the file patterns for use in a `FnmatchMatcher`.
-
- If a pattern starts with a wildcard, it is used as a pattern
- as-is. If it does not start with a wildcard, then it is made
- absolute with the current directory.
-
- If `patterns` is None, an empty list is returned.
-
- """
- patterns = patterns or []
- prepped = []
- for p in patterns or []:
- if p.startswith("*") or p.startswith("?"):
- prepped.append(p)
- else:
- prepped.append(self.file_locator.abs_file(p))
- return prepped
-
def _check_for_packages(self):
"""Update the source_match matcher with latest imported packages."""
# Our self.source_pkgs attribute is a list of package names we want to
diff --git a/coverage/files.py b/coverage/files.py
index 13f43930..632d6e31 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -9,16 +9,12 @@ class FileLocator(object):
def __init__(self):
# The absolute path to our current directory.
- self.relative_dir = self.abs_file(os.curdir) + os.sep
+ self.relative_dir = abs_file(os.curdir) + os.sep
# Cache of results of calling the canonical_filename() method, to
# avoid duplicating work.
self.canonical_filename_cache = {}
- def abs_file(self, filename):
- """Return the absolute normalized form of `filename`."""
- return os.path.normcase(os.path.abspath(os.path.realpath(filename)))
-
def relative_filename(self, filename):
"""Return the relative form of `filename`.
@@ -49,7 +45,7 @@ class FileLocator(object):
if os.path.exists(g):
f = g
break
- cf = self.abs_file(f)
+ cf = abs_file(f)
self.canonical_filename_cache[filename] = cf
return self.canonical_filename_cache[filename]
@@ -78,6 +74,31 @@ class FileLocator(object):
return None
+def abs_file(filename):
+ """Return the absolute normalized form of `filename`."""
+ return os.path.normcase(os.path.abspath(os.path.realpath(filename)))
+
+
+def prep_patterns(patterns):
+ """Prepare the file patterns for use in a `FnmatchMatcher`.
+
+ If a pattern starts with a wildcard, it is used as a pattern
+ as-is. If it does not start with a wildcard, then it is made
+ absolute with the current directory.
+
+ If `patterns` is None, an empty list is returned.
+
+ """
+ patterns = patterns or []
+ prepped = []
+ for p in patterns or []:
+ if p.startswith("*") or p.startswith("?"):
+ prepped.append(p)
+ else:
+ prepped.append(abs_file(p))
+ return prepped
+
+
class TreeMatcher(object):
"""A matcher for files in a tree."""
def __init__(self, directories):
diff --git a/coverage/report.py b/coverage/report.py
index e351340f..34f44422 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -2,6 +2,7 @@
import fnmatch, os
from coverage.codeunit import code_unit_factory
+from coverage.files import prep_patterns
from coverage.misc import CoverageException, NoSource, NotPython
class Reporter(object):
@@ -35,7 +36,7 @@ class Reporter(object):
self.code_units = code_unit_factory(morfs, file_locator)
if self.config.include:
- patterns = [file_locator.abs_file(p) for p in self.config.include]
+ patterns = prep_patterns(self.config.include)
filtered = []
for cu in self.code_units:
for pattern in patterns:
@@ -45,7 +46,7 @@ class Reporter(object):
self.code_units = filtered
if self.config.omit:
- patterns = [file_locator.abs_file(p) for p in self.config.omit]
+ patterns = prep_patterns(self.config.omit)
filtered = []
for cu in self.code_units:
for pattern in patterns:
diff --git a/test/test_api.py b/test/test_api.py
index 8f270098..83f82f12 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -360,29 +360,17 @@ class UsingModulesMixin(object):
super(UsingModulesMixin, self).tearDown()
-class SourceOmitIncludeTest(UsingModulesMixin, CoverageTest):
- """Test using `source`, `omit` and `include` when measuring code."""
-
- def coverage_usepkgs_summary(self, **kwargs):
- """Run coverage on usepkgs and return the line summary.
+class OmitIncludeTestsMixin(UsingModulesMixin):
+ """Test methods for coverage methods taking include and omit."""
- Arguments are passed to the `coverage.coverage` constructor.
-
- """
- cov = coverage.coverage(**kwargs)
- cov.start()
- import usepkgs # pylint: disable=F0401,W0612
- cov.stop()
- return cov.data.summary()
-
- def filenames_in_summary(self, summary, filenames):
+ def filenames_in(self, summary, filenames):
"""Assert the `filenames` are in the keys of `summary`."""
for filename in filenames.split():
self.assert_(filename in summary,
"%s should be in %r" % (filename, summary)
)
- def filenames_not_in_summary(self, summary, filenames):
+ def filenames_not_in(self, summary, filenames):
"""Assert the `filenames` are not in the keys of `summary`."""
for filename in filenames.split():
self.assert_(filename not in summary,
@@ -390,100 +378,110 @@ class SourceOmitIncludeTest(UsingModulesMixin, CoverageTest):
)
def test_nothing_specified(self):
- lines = self.coverage_usepkgs_summary()
- self.filenames_in_summary(lines,
- "p1a.py p1b.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
- self.filenames_not_in_summary(lines,
- "p1c.py"
- )
+ result = self.coverage_usepkgs()
+ self.filenames_in(result, "p1a p1b p2a p2b othera otherb osa osb")
+ self.filenames_not_in(result, "p1c")
# Because there was no source= specified, we don't search for
# unexecuted files.
- def test_source_package(self):
- lines = self.coverage_usepkgs_summary(source=["pkg1"])
- self.filenames_in_summary(lines,
- "p1a.py p1b.py"
- )
- self.filenames_not_in_summary(lines,
- "p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
- # Because source= was specified, we do search for unexecuted files.
- self.assertEqual(lines['p1c.py'], 0)
-
- def test_source_package_dotted(self):
- lines = self.coverage_usepkgs_summary(source=["pkg1.p1b"])
- self.filenames_in_summary(lines,
- "p1b.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
-
def test_include(self):
- lines = self.coverage_usepkgs_summary(include=["*/p1a.py"])
- self.filenames_in_summary(lines,
- "p1a.py"
- )
- self.filenames_not_in_summary(lines,
- "p1b.py p1c.py p2a.py p2b.py othera.py otherb.py osa.py osb.py"
- )
+ result = self.coverage_usepkgs(include=["*/p1a.py"])
+ self.filenames_in(result, "p1a")
+ self.filenames_not_in(result, "p1b p1c p2a p2b othera otherb osa osb")
def test_include_2(self):
- lines = self.coverage_usepkgs_summary(include=["*a.py"])
- self.filenames_in_summary(lines,
- "p1a.py p2a.py othera.py osa.py"
- )
- self.filenames_not_in_summary(lines,
- "p1b.py p1c.py p2b.py otherb.py osb.py"
- )
+ result = self.coverage_usepkgs(include=["*a.py"])
+ self.filenames_in(result, "p1a p2a othera osa")
+ self.filenames_not_in(result, "p1b p1c p2b otherb osb")
def test_include_as_string(self):
- lines = self.coverage_usepkgs_summary(include="*a.py")
- self.filenames_in_summary(lines,
- "p1a.py p2a.py othera.py osa.py"
- )
- self.filenames_not_in_summary(lines,
- "p1b.py p1c.py p2b.py otherb.py osb.py"
- )
+ result = self.coverage_usepkgs(include="*a.py")
+ self.filenames_in(result, "p1a p2a othera osa")
+ self.filenames_not_in(result, "p1b p1c p2b otherb osb")
def test_omit(self):
- lines = self.coverage_usepkgs_summary(omit=["*/p1a.py"])
- self.filenames_in_summary(lines,
- "p1b.py p2a.py p2b.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py"
- )
+ result = self.coverage_usepkgs(omit=["*/p1a.py"])
+ self.filenames_in(result, "p1b p2a p2b")
+ self.filenames_not_in(result, "p1a p1c")
def test_omit_2(self):
- lines = self.coverage_usepkgs_summary(omit=["*a.py"])
- self.filenames_in_summary(lines,
- "p1b.py p2b.py otherb.py osb.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py othera.py osa.py"
- )
+ result = self.coverage_usepkgs(omit=["*a.py"])
+ self.filenames_in(result, "p1b p2b otherb osb")
+ self.filenames_not_in(result, "p1a p1c p2a othera osa")
def test_omit_as_string(self):
- lines = self.coverage_usepkgs_summary(omit="*a.py")
- self.filenames_in_summary(lines,
- "p1b.py p2b.py otherb.py osb.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py othera.py osa.py"
- )
+ result = self.coverage_usepkgs(omit="*a.py")
+ self.filenames_in(result, "p1b p2b otherb osb")
+ self.filenames_not_in(result, "p1a p1c p2a othera osa")
def test_omit_and_include(self):
- lines = self.coverage_usepkgs_summary(
- include=["*/p1*"], omit=["*/p1a.py"]
- )
- self.filenames_in_summary(lines,
- "p1b.py"
- )
- self.filenames_not_in_summary(lines,
- "p1a.py p1c.py p2a.py p2b.py"
- )
+ result = self.coverage_usepkgs( include=["*/p1*"], omit=["*/p1a.py"])
+ self.filenames_in(result, "p1b")
+ self.filenames_not_in(result, "p1a p1c p2a p2b")
+
+
+class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest):
+ """Test using `source`, `omit` and `include` when measuring code."""
+
+ def coverage_usepkgs(self, **kwargs):
+ """Run coverage on usepkgs and return the line summary.
+
+ Arguments are passed to the `coverage.coverage` constructor.
+
+ """
+ cov = coverage.coverage(**kwargs)
+ cov.start()
+ import usepkgs # pylint: disable=F0401,W0612
+ cov.stop()
+ summary = cov.data.summary()
+ for k, v in summary.items():
+ assert k.endswith(".py")
+ summary[k[:-3]] = v
+ return summary
+
+ def test_source_package(self):
+ lines = self.coverage_usepkgs(source=["pkg1"])
+ self.filenames_in(lines, "p1a p1b")
+ self.filenames_not_in(lines, "p2a p2b othera otherb osa osb")
+ # Because source= was specified, we do search for unexecuted files.
+ self.assertEqual(lines['p1c'], 0)
+
+ def test_source_package_dotted(self):
+ lines = self.coverage_usepkgs(source=["pkg1.p1b"])
+ self.filenames_in(lines, "p1b")
+ self.filenames_not_in(lines, "p1a p1c p2a p2b othera otherb osa osb")
+
+
+class ReportIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest):
+ """Tests of the report include/omit functionality."""
+
+ def coverage_usepkgs(self, **kwargs):
+ """Try coverage.report()."""
+ cov = coverage.coverage()
+ cov.start()
+ import usepkgs # pylint: disable=F0401,W0612
+ cov.stop()
+ report = StringIO()
+ cov.report(file=report, **kwargs)
+ return report.getvalue()
+
+
+class XmlIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest):
+ """Tests of the xml include/omit functionality.
+
+ This also takes care of the HTML and annotate include/omit, by virtue
+ of the structure of the code.
+
+ """
+
+ def coverage_usepkgs(self, **kwargs):
+ """Try coverage.xml_report()."""
+ cov = coverage.coverage()
+ cov.start()
+ import usepkgs # pylint: disable=F0401,W0612
+ cov.stop()
+ cov.xml_report(outfile="-", **kwargs)
+ return self.stdout()
class AnalysisTest(CoverageTest):
diff --git a/test/test_files.py b/test/test_files.py
index f2f3581e..207274a2 100644
--- a/test/test_files.py
+++ b/test/test_files.py
@@ -3,7 +3,7 @@
import os, sys
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
-from coverage.files import PathAliases, find_python_files
+from coverage.files import PathAliases, find_python_files, abs_file
from coverage.backward import set # pylint: disable=W0622
from coverage.misc import CoverageException
@@ -43,10 +43,10 @@ class FileLocatorTest(CoverageTest):
# Technically, this test doesn't do that on Windows, but drive
# letters make that impractical to acheive.
fl = FileLocator()
- d = fl.abs_file(os.curdir)
+ d = abs_file(os.curdir)
trick = os.path.splitdrive(d)[1].lstrip(os.path.sep)
rel = os.path.join('sub', trick, 'file1.py')
- self.assertEqual(fl.relative_filename(fl.abs_file(rel)), rel)
+ self.assertEqual(fl.relative_filename(abs_file(rel)), rel)
class MatcherTest(CoverageTest):
@@ -168,4 +168,3 @@ class FindPythonFilesTest(CoverageTest):
"sub/a.py", "sub/b.py",
"sub/ssub/__init__.py", "sub/ssub/s.py",
])
-