summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst5
-rw-r--r--coverage/files.py79
-rw-r--r--coverage/inorout.py6
-rw-r--r--coverage/report.py6
-rw-r--r--doc/cmd.rst40
-rw-r--r--doc/config.rst2
-rw-r--r--doc/source.rst29
-rw-r--r--tests/test_api.py1
-rw-r--r--tests/test_files.py209
-rw-r--r--tests/test_summary.py24
10 files changed, 284 insertions, 117 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 372c639d..b0ea7bb6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,6 +20,10 @@ development at the same time, such as 4.5.x and 5.0.
Unreleased
----------
+- Fixes to file pattern matching, fixing `issue 1407`_. Previously, `*` would
+ incorrectly match directory separators, making precise matching difficult.
+ This is now fixed.
+
- Improvements to combining data files when using the
:ref:`config_run_relative_files` setting:
@@ -39,6 +43,7 @@ Unreleased
implementations other than CPython or PyPy (`issue 1474`_).
.. _issue 991: https://github.com/nedbat/coveragepy/issues/991
+.. _issue 1407: https://github.com/nedbat/coveragepy/issues/1407
.. _issue 1474: https://github.com/nedbat/coveragepy/issues/1474
.. _issue 1481: https://github.com/nedbat/coveragepy/issues/1481
diff --git a/coverage/files.py b/coverage/files.py
index 2c520b8a..76ecbef9 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -3,7 +3,6 @@
"""File wrangling."""
-import fnmatch
import hashlib
import ntpath
import os
@@ -172,7 +171,7 @@ def isabs_anywhere(filename):
def prep_patterns(patterns):
- """Prepare the file patterns for use in a `FnmatchMatcher`.
+ """Prepare the file patterns for use in a `GlobMatcher`.
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
@@ -253,15 +252,15 @@ class ModuleMatcher:
return False
-class FnmatchMatcher:
+class GlobMatcher:
"""A matcher for files by file name pattern."""
def __init__(self, pats, name="unknown"):
self.pats = list(pats)
- self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS)
+ self.re = globs_to_regex(self.pats, case_insensitive=env.WINDOWS)
self.name = name
def __repr__(self):
- return f"<FnmatchMatcher {self.name} {self.pats!r}>"
+ return f"<GlobMatcher {self.name} {self.pats!r}>"
def info(self):
"""A list of strings for displaying when dumping state."""
@@ -282,12 +281,55 @@ def sep(s):
return the_sep
-def fnmatches_to_regex(patterns, case_insensitive=False, partial=False):
- """Convert fnmatch patterns to a compiled regex that matches any of them.
+# Tokenizer for _glob_to_regex.
+# None as a sub means disallowed.
+G2RX_TOKENS = [(re.compile(rx), sub) for rx, sub in [
+ (r"\*\*\*+", None), # Can't have ***
+ (r"[^/]+\*\*+", None), # Can't have x**
+ (r"\*\*+[^/]+", None), # Can't have **x
+ (r"\*\*/\*\*", None), # Can't have **/**
+ (r"^\*+/", r"(.*[/\\\\])?"), # ^*/ matches any prefix-slash, or nothing.
+ (r"/\*+$", r"[/\\\\].*"), # /*$ matches any slash-suffix.
+ (r"\*\*/", r"(.*[/\\\\])?"), # **/ matches any subdirs, including none
+ (r"/", r"[/\\\\]"), # / matches either slash or backslash
+ (r"\*", r"[^/\\\\]*"), # * matches any number of non slash-likes
+ (r"\?", r"[^/\\\\]"), # ? matches one non slash-like
+ (r"\[.*?\]", r"\g<0>"), # [a-f] matches [a-f]
+ (r"[a-zA-Z0-9_-]+", r"\g<0>"), # word chars match themselves
+ (r"[\[\]+{}]", None), # Can't have regex special chars
+ (r".", r"\\\g<0>"), # Anything else is escaped to be safe
+]]
+
+def _glob_to_regex(pattern):
+ """Convert a file-path glob pattern into a regex."""
+ # Turn all backslashes into slashes to simplify the tokenizer.
+ pattern = pattern.replace("\\", "/")
+ if "/" not in pattern:
+ pattern = "**/" + pattern
+ path_rx = []
+ pos = 0
+ while pos < len(pattern):
+ for rx, sub in G2RX_TOKENS:
+ m = rx.match(pattern, pos=pos)
+ if m:
+ if sub is None:
+ raise ConfigError(f"File pattern can't include {m[0]!r}")
+ path_rx.append(m.expand(sub))
+ pos = m.end()
+ break
+ return "".join(path_rx)
+
+
+def globs_to_regex(patterns, case_insensitive=False, partial=False):
+ """Convert glob patterns to a compiled regex that matches any of them.
Slashes are always converted to match either slash or backslash, for
Windows support, even when running elsewhere.
+ If the pattern has no slash or backslash, then it is interpreted as
+ matching a file name anywhere it appears in the tree. Otherwise, the glob
+ pattern must match the whole file path.
+
If `partial` is true, then the pattern will match if the target string
starts with the pattern. Otherwise, it must match the entire string.
@@ -295,24 +337,13 @@ def fnmatches_to_regex(patterns, case_insensitive=False, partial=False):
strings.
"""
- regexes = (fnmatch.translate(pattern) for pattern in patterns)
- # */ at the start should also match nothing.
- regexes = (re.sub(r"^\(\?s:\.\*(\\\\|/)", r"(?s:^(.*\1)?", regex) for regex in regexes)
- # Be agnostic: / can mean backslash or slash.
- regexes = (re.sub(r"/", r"[\\\\/]", regex) for regex in regexes)
-
- if partial:
- # fnmatch always adds a \Z to match the whole string, which we don't
- # want, so we remove the \Z. While removing it, we only replace \Z if
- # followed by paren (introducing flags), or at end, to keep from
- # destroying a literal \Z in the pattern.
- regexes = (re.sub(r'\\Z(\(\?|$)', r'\1', regex) for regex in regexes)
-
flags = 0
if case_insensitive:
flags |= re.IGNORECASE
- compiled = re.compile(join_regex(regexes), flags=flags)
-
+ rx = join_regex(map(_glob_to_regex, patterns))
+ if not partial:
+ rx = rf"(?:{rx})\Z"
+ compiled = re.compile(rx, flags=flags)
return compiled
@@ -342,7 +373,7 @@ class PathAliases:
def add(self, pattern, result):
"""Add the `pattern`/`result` pair to the list of aliases.
- `pattern` is an `fnmatch`-style pattern. `result` is a simple
+ `pattern` is an `glob`-style pattern. `result` is a simple
string. When mapping paths, if a path starts with a match against
`pattern`, then that match is replaced with `result`. This models
isomorphic source trees being rooted at different places on two
@@ -370,7 +401,7 @@ class PathAliases:
pattern += pattern_sep
# Make a regex from the pattern.
- regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True)
+ regex = globs_to_regex([pattern], case_insensitive=True, partial=True)
# Normalize the result: it must end with a path separator.
result_sep = sep(result)
diff --git a/coverage/inorout.py b/coverage/inorout.py
index ec89d1b4..2e534c85 100644
--- a/coverage/inorout.py
+++ b/coverage/inorout.py
@@ -16,7 +16,7 @@ import traceback
from coverage import env
from coverage.disposition import FileDisposition, disposition_init
from coverage.exceptions import CoverageException, PluginError
-from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher
+from coverage.files import TreeMatcher, GlobMatcher, ModuleMatcher
from coverage.files import prep_patterns, find_python_files, canonical_filename
from coverage.misc import sys_modules_saved
from coverage.python import source_for_file, source_for_morf
@@ -260,10 +260,10 @@ class InOrOut:
self.pylib_match = TreeMatcher(self.pylib_paths, "pylib")
debug(f"Python stdlib matching: {self.pylib_match!r}")
if self.include:
- self.include_match = FnmatchMatcher(self.include, "include")
+ self.include_match = GlobMatcher(self.include, "include")
debug(f"Include matching: {self.include_match!r}")
if self.omit:
- self.omit_match = FnmatchMatcher(self.omit, "omit")
+ self.omit_match = GlobMatcher(self.omit, "omit")
debug(f"Omit matching: {self.omit_match!r}")
self.cover_match = TreeMatcher(self.cover_paths, "coverage")
diff --git a/coverage/report.py b/coverage/report.py
index 6382eb51..0c05b044 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -6,7 +6,7 @@
import sys
from coverage.exceptions import CoverageException, NoDataError, NotPython
-from coverage.files import prep_patterns, FnmatchMatcher
+from coverage.files import prep_patterns, GlobMatcher
from coverage.misc import ensure_dir_for_file, file_be_gone
@@ -57,11 +57,11 @@ def get_analysis_to_report(coverage, morfs):
config = coverage.config
if config.report_include:
- matcher = FnmatchMatcher(prep_patterns(config.report_include), "report_include")
+ matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
if config.report_omit:
- matcher = FnmatchMatcher(prep_patterns(config.report_omit), "report_omit")
+ matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
if not file_reporters:
diff --git a/doc/cmd.rst b/doc/cmd.rst
index cb9a147e..f8de0cb3 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -342,7 +342,7 @@ single directory, and use the **combine** command to combine them into one
$ coverage combine
-You can also name directories or files on the command line::
+You can also name directories or files to be combined on the command line::
$ coverage combine data1.dat windows_data_files/
@@ -364,22 +364,6 @@ An existing combined data file is ignored and re-written. If you want to use
runs, use the ``--append`` switch on the **combine** command. This behavior
was the default before version 4.2.
-To combine data for a source file, coverage has to find its data in each of the
-data files. Different test runs may run the same source file from different
-locations. For example, different operating systems will use different paths
-for the same file, or perhaps each Python version is run from a different
-subdirectory. Coverage needs to know that different file paths are actually
-the same source file for reporting purposes.
-
-You can tell coverage.py how different source locations relate with a
-``[paths]`` section in your configuration file (see :ref:`config_paths`).
-It might be more convenient to use the ``[run] relative_files``
-setting to store relative file paths (see :ref:`relative_files
-<config_run_relative_files>`).
-
-If data isn't combining properly, you can see details about the inner workings
-with ``--debug=pathmap``.
-
If any of the data files can't be read, coverage.py will print a warning
indicating the file and the problem.
@@ -414,6 +398,28 @@ want to keep those files, use the ``--keep`` command-line option.
.. [[[end]]] (checksum: 0bdd83f647ee76363c955bedd9ddf749)
+.. _cmd_combine_remapping:
+
+Re-mapping paths
+................
+
+To combine data for a source file, coverage has to find its data in each of the
+data files. Different test runs may run the same source file from different
+locations. For example, different operating systems will use different paths
+for the same file, or perhaps each Python version is run from a different
+subdirectory. Coverage needs to know that different file paths are actually
+the same source file for reporting purposes.
+
+You can tell coverage.py how different source locations relate with a
+``[paths]`` section in your configuration file (see :ref:`config_paths`).
+It might be more convenient to use the ``[run] relative_files``
+setting to store relative file paths (see :ref:`relative_files
+<config_run_relative_files>`).
+
+If data isn't combining properly, you can see details about the inner workings
+with ``--debug=pathmap``.
+
+
.. _cmd_erase:
Erase data: ``coverage erase``
diff --git a/doc/config.rst b/doc/config.rst
index 6b753579..c6f6442a 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -357,7 +357,7 @@ The first list that has a match will be used.
The ``--debug=pathmap`` option can be used to log details of the re-mapping of
paths. See :ref:`the --debug option <cmd_run_debug>`.
-See :ref:`cmd_combine` for more information.
+See :ref:`cmd_combine_remapping` and :ref:`source_glob` for more information.
.. _config_report:
diff --git a/doc/source.rst b/doc/source.rst
index cfd0e6fc..64ebd132 100644
--- a/doc/source.rst
+++ b/doc/source.rst
@@ -59,10 +59,10 @@ removed from the set.
.. highlight:: ini
-The ``include`` and ``omit`` file name patterns follow typical shell syntax:
-``*`` matches any number of characters and ``?`` matches a single character.
-Patterns that start with a wildcard character are used as-is, other patterns
-are interpreted relative to the current directory::
+The ``include`` and ``omit`` file name patterns follow common shell syntax,
+described below in :ref:`source_glob`. Patterns that start with a wildcard
+character are used as-is, other patterns are interpreted relative to the
+current directory::
[run]
omit =
@@ -77,7 +77,7 @@ The ``source``, ``include``, and ``omit`` values all work together to determine
the source that will be measured.
If both ``source`` and ``include`` are set, the ``include`` value is ignored
-and a warning is printed on the standard output.
+and a warning is issued.
.. _source_reporting:
@@ -103,3 +103,22 @@ reporting.
Note that these are ways of specifying files to measure. You can also exclude
individual source lines. See :ref:`excluding` for details.
+
+
+.. _source_glob:
+
+File patterns
+-------------
+
+File path patterns are used for include and omit, and for combining path
+remapping. They follow common shell syntax:
+
+- ``*`` matches any number of file name characters, not including the directory
+ separator.
+
+- ``?`` matches a single file name character.
+
+- ``**`` matches any number of nested directory names, including none.
+
+- Both ``/`` and ``\`` will match either a slash or a backslash, to make
+ cross-platform matching easier.
diff --git a/tests/test_api.py b/tests/test_api.py
index 375edcec..07bd07f3 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -71,7 +71,6 @@ class ApiTest(CoverageTest):
assert missing == [1]
def test_filenames(self):
-
self.make_file("mymain.py", """\
import mymod
a = 1
diff --git a/tests/test_files.py b/tests/test_files.py
index 8fea61d0..9a4cea7f 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -3,8 +3,10 @@
"""Tests for files.py"""
+import itertools
import os
import os.path
+import re
from unittest import mock
import pytest
@@ -12,8 +14,8 @@ import pytest
from coverage import env, files
from coverage.exceptions import ConfigError
from coverage.files import (
- FnmatchMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file,
- actual_path, find_python_files, flat_rootname, fnmatches_to_regex,
+ GlobMatcher, ModuleMatcher, PathAliases, TreeMatcher, abs_file,
+ actual_path, find_python_files, flat_rootname, globs_to_regex,
)
from tests.coveragetest import CoverageTest
@@ -104,59 +106,138 @@ def test_flat_rootname(original, flat):
assert flat_rootname(original) == flat
+def globs_to_regex_params(
+ patterns, case_insensitive=False, partial=False, matches=(), nomatches=(),
+):
+ """Generate parameters for `test_globs_to_regex`.
+
+ `patterns`, `case_insensitive`, and `partial` are arguments for
+ `globs_to_regex`. `matches` is a list of strings that should match, and
+ `nomatches` is a list of strings that should not match.
+
+ Everything is yielded so that `test_globs_to_regex` can call
+ `globs_to_regex` once and check one result.
+ """
+ pat_id = "|".join(patterns)
+ for text in matches:
+ yield pytest.param(
+ patterns, case_insensitive, partial, text, True,
+ id=f"{pat_id}:ci{case_insensitive}:par{partial}:{text}:match",
+ )
+ for text in nomatches:
+ yield pytest.param(
+ patterns, case_insensitive, partial, text, False,
+ id=f"{pat_id}:ci{case_insensitive}:par{partial}:{text}:nomatch",
+ )
+
@pytest.mark.parametrize(
- "patterns, case_insensitive, partial," +
- "matches," +
- "nomatches",
-[
- (
- ["abc", "xyz"], False, False,
+ "patterns, case_insensitive, partial, text, result",
+ list(itertools.chain.from_iterable([
+ globs_to_regex_params(
["abc", "xyz"],
- ["ABC", "xYz", "abcx", "xabc", "axyz", "xyza"],
- ),
- (
- ["abc", "xyz"], True, False,
- ["abc", "xyz", "Abc", "XYZ", "AbC"],
- ["abcx", "xabc", "axyz", "xyza"],
- ),
- (
- ["abc/hi.py"], True, False,
- ["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"],
- ["abc_hi.py", "abc/hi.pyc"],
- ),
- (
- [r"abc\hi.py"], True, False,
- [r"abc\hi.py", r"ABC\hi.py"],
- ["abc/hi.py", "ABC/hi.py", "abc_hi.py", "abc/hi.pyc"],
- ),
- (
- ["abc/*/hi.py"], True, False,
- ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
- ["abc/hi.py", "abc/hi.pyc"],
- ),
- (
- ["abc/[a-f]*/hi.py"], True, False,
- ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
- ["abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc"],
- ),
- (
- ["abc/"], True, True,
- ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
- ["abcd/foo.py", "xabc/hi.py"],
- ),
- (
- ["*/foo"], False, True,
- ["abc/foo/hi.py", "foo/hi.py"],
- ["abc/xfoo/hi.py"],
- ),
-
+ matches=["abc", "xyz", "sub/mod/abc"],
+ nomatches=[
+ "ABC", "xYz", "abcx", "xabc", "axyz", "xyza", "sub/mod/abcd", "sub/abc/more",
+ ],
+ ),
+ globs_to_regex_params(
+ ["abc", "xyz"], case_insensitive=True,
+ matches=["abc", "xyz", "Abc", "XYZ", "AbC"],
+ nomatches=["abcx", "xabc", "axyz", "xyza"],
+ ),
+ globs_to_regex_params(
+ ["a*c", "x*z"],
+ matches=["abc", "xyz", "xYz", "azc", "xaz", "axyzc"],
+ nomatches=["ABC", "abcx", "xabc", "axyz", "xyza", "a/c"],
+ ),
+ globs_to_regex_params(
+ ["a?c", "x?z"],
+ matches=["abc", "xyz", "xYz", "azc", "xaz"],
+ nomatches=["ABC", "abcx", "xabc", "axyz", "xyza", "a/c"],
+ ),
+ globs_to_regex_params(
+ ["a??d"],
+ matches=["abcd", "azcd", "a12d"],
+ nomatches=["ABCD", "abcx", "axyz", "abcde"],
+ ),
+ globs_to_regex_params(
+ ["abc/hi.py"], case_insensitive=True,
+ matches=["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"],
+ nomatches=["abc_hi.py", "abc/hi.pyc"],
+ ),
+ globs_to_regex_params(
+ [r"abc\hi.py"], case_insensitive=True,
+ matches=[r"abc\hi.py", r"ABC\hi.py", "abc/hi.py", "ABC/hi.py"],
+ nomatches=["abc_hi.py", "abc/hi.pyc"],
+ ),
+ globs_to_regex_params(
+ ["abc/*/hi.py"], case_insensitive=True,
+ matches=["abc/foo/hi.py", r"ABC\foo/hi.py"],
+ nomatches=["abc/hi.py", "abc/hi.pyc", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
+ ),
+ globs_to_regex_params(
+ ["abc/**/hi.py"], case_insensitive=True,
+ matches=[
+ "abc/foo/hi.py", r"ABC\foo/hi.py", "abc/hi.py", "ABC/foo/bar/hi.py",
+ r"ABC\foo/bar/hi.py",
+ ],
+ nomatches=["abc/hi.pyc"],
+ ),
+ globs_to_regex_params(
+ ["abc/[a-f]*/hi.py"], case_insensitive=True,
+ matches=["abc/foo/hi.py", r"ABC\boo/hi.py"],
+ nomatches=[
+ "abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc", "abc/foo/bar/hi.py",
+ r"abc\foo/bar/hi.py",
+ ],
+ ),
+ globs_to_regex_params(
+ ["abc/[a-f]/hi.py"], case_insensitive=True,
+ matches=["abc/f/hi.py", r"ABC\b/hi.py"],
+ nomatches=[
+ "abc/foo/hi.py", "abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc", "abc/foo/bar/hi.py",
+ r"abc\foo/bar/hi.py",
+ ],
+ ),
+ globs_to_regex_params(
+ ["abc/"], case_insensitive=True, partial=True,
+ matches=["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"],
+ nomatches=["abcd/foo.py", "xabc/hi.py"],
+ ),
+ globs_to_regex_params(
+ ["*/foo"], case_insensitive=False, partial=True,
+ matches=["abc/foo/hi.py", "foo/hi.py"],
+ nomatches=["abc/xfoo/hi.py"],
+ ),
+ globs_to_regex_params(
+ ["**/foo"],
+ matches=["foo", "hello/foo", "hi/there/foo"],
+ nomatches=["foob", "hello/foob", "hello/Foo"],
+ ),
+ ]))
+)
+def test_globs_to_regex(patterns, case_insensitive, partial, text, result):
+ regex = globs_to_regex(patterns, case_insensitive=case_insensitive, partial=partial)
+ assert bool(regex.match(text)) == result
+
+
+@pytest.mark.parametrize("pattern, bad_word", [
+ ("***/foo.py", "***"),
+ ("bar/***/foo.py", "***"),
+ ("*****/foo.py", "*****"),
+ ("Hello]there", "]"),
+ ("Hello[there", "["),
+ ("Hello+there", "+"),
+ ("{a,b}c", "{"),
+ ("x/a**/b.py", "a**"),
+ ("x/abcd**/b.py", "abcd**"),
+ ("x/**a/b.py", "**a"),
+ ("x/**/**/b.py", "**/**"),
])
-def test_fnmatches_to_regex(patterns, case_insensitive, partial, matches, nomatches):
- regex = fnmatches_to_regex(patterns, case_insensitive=case_insensitive, partial=partial)
- for s in matches:
- assert regex.match(s)
- for s in nomatches:
- assert not regex.match(s)
+def test_invalid_globs(pattern, bad_word):
+ msg = f"File pattern can't include {bad_word!r}"
+ with pytest.raises(ConfigError, match=re.escape(msg)):
+ globs_to_regex([pattern])
class MatcherTest(CoverageTest):
@@ -217,7 +298,7 @@ class MatcherTest(CoverageTest):
for modulename, matches in matches_to_try:
assert mm.match(modulename) == matches, modulename
- def test_fnmatch_matcher(self):
+ def test_glob_matcher(self):
matches_to_try = [
(self.make_file("sub/file1.py"), True),
(self.make_file("sub/file2.c"), False),
@@ -225,23 +306,25 @@ class MatcherTest(CoverageTest):
(self.make_file("sub3/file4.py"), True),
(self.make_file("sub3/file5.c"), False),
]
- fnm = FnmatchMatcher(["*.py", "*/sub2/*"])
+ fnm = GlobMatcher(["*.py", "*/sub2/*"])
assert fnm.info() == ["*.py", "*/sub2/*"]
for filepath, matches in matches_to_try:
self.assertMatches(fnm, filepath, matches)
- def test_fnmatch_matcher_overload(self):
- fnm = FnmatchMatcher(["*x%03d*.txt" % i for i in range(500)])
+ def test_glob_matcher_overload(self):
+ fnm = GlobMatcher(["*x%03d*.txt" % i for i in range(500)])
self.assertMatches(fnm, "x007foo.txt", True)
self.assertMatches(fnm, "x123foo.txt", True)
self.assertMatches(fnm, "x798bar.txt", False)
+ self.assertMatches(fnm, "x499.txt", True)
+ self.assertMatches(fnm, "x500.txt", False)
- def test_fnmatch_windows_paths(self):
+ def test_glob_windows_paths(self):
# We should be able to match Windows paths even if we are running on
# a non-Windows OS.
- fnm = FnmatchMatcher(["*/foo.py"])
+ fnm = GlobMatcher(["*/foo.py"])
self.assertMatches(fnm, r"dir\foo.py", True)
- fnm = FnmatchMatcher([r"*\foo.py"])
+ fnm = GlobMatcher([r"*\foo.py"])
self.assertMatches(fnm, r"dir\foo.py", True)
@@ -309,9 +392,9 @@ class PathAliasesTest(CoverageTest):
assert msgs == [
"Aliases (relative=True):",
" Rule: '/home/*/src' -> './mysrc/' using regex " +
- "'(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/])'",
+ "'[/\\\\\\\\]home[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]src[/\\\\\\\\]'",
" Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
- "'(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/])'",
+ "'[/\\\\\\\\]lib[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]libsrc[/\\\\\\\\]'",
"Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " +
"producing './mysrc/a.py'",
"Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " +
@@ -321,9 +404,9 @@ class PathAliasesTest(CoverageTest):
assert msgs == [
"Aliases (relative=False):",
" Rule: '/home/*/src' -> './mysrc/' using regex " +
- "'(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/])'",
+ "'[/\\\\\\\\]home[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]src[/\\\\\\\\]'",
" Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
- "'(?s:[\\\\\\\\/]lib[\\\\\\\\/].*[\\\\\\\\/]libsrc[\\\\\\\\/])'",
+ "'[/\\\\\\\\]lib[/\\\\\\\\][^/\\\\\\\\]*[/\\\\\\\\]libsrc[/\\\\\\\\]'",
"Matched path '/home/foo/src/a.py' to rule '/home/*/src' -> './mysrc/', " +
f"producing {files.canonical_filename('./mysrc/a.py')!r}",
"Matched path '/lib/foo/libsrc/a.py' to rule '/lib/*/libsrc' -> './mylib/', " +
diff --git a/tests/test_summary.py b/tests/test_summary.py
index d603062b..ac29f517 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -138,6 +138,30 @@ class SummaryTest(UsingModulesMixin, CoverageTest):
assert "mycode.py " in report
assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+ def test_omit_files_here(self):
+ # https://github.com/nedbat/coveragepy/issues/1407
+ self.make_file("foo.py", "")
+ self.make_file("bar/bar.py", "")
+ self.make_file("tests/test_baz.py", """\
+ def test_foo():
+ assert True
+ test_foo()
+ """)
+ self.run_command("coverage run --source=. --omit='./*.py' -m tests.test_baz")
+ report = self.report_from_command("coverage report")
+
+ # Name Stmts Miss Cover
+ # ---------------------------------------
+ # tests/test_baz.py 3 0 100%
+ # ---------------------------------------
+ # TOTAL 3 0 100%
+
+ assert self.line_count(report) == 5
+ assert "foo" not in report
+ assert "bar" not in report
+ assert "tests/test_baz.py" in report
+ assert self.last_line_squeezed(report) == "TOTAL 3 0 100%"
+
def test_run_source_vs_report_include(self):
# https://github.com/nedbat/coveragepy/issues/621
self.make_file(".coveragerc", """\