diff options
-rw-r--r-- | CHANGES.txt | 9 | ||||
-rw-r--r-- | coverage/config.py | 6 | ||||
-rw-r--r-- | coverage/control.py | 29 | ||||
-rw-r--r-- | coverage/files.py | 33 | ||||
-rw-r--r-- | coverage/report.py | 5 | ||||
-rw-r--r-- | test/test_api.py | 186 | ||||
-rw-r--r-- | test/test_files.py | 7 |
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", ]) - |