summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2022-09-16 17:48:02 -0400
committerNed Batchelder <ned@nedbatchelder.com>2022-09-16 18:17:55 -0400
commit7b690aa976a6bb30261d68344104f817a812a677 (patch)
treee81568929e612ee14674d7a71abf3370c6d346b1
parent30d37e59e50b75c057a09fddf5b92869c0858949 (diff)
downloadpython-coveragepy-git-7b690aa976a6bb30261d68344104f817a812a677.tar.gz
feat: --debug=pathmap will show details of re-mapping due to [paths] setting.
-rw-r--r--CHANGES.rst3
-rw-r--r--coverage/control.py5
-rw-r--r--coverage/files.py33
-rw-r--r--coverage/sqldata.py3
-rw-r--r--doc/cmd.rst3
-rw-r--r--doc/config.rst3
-rw-r--r--tests/test_files.py28
7 files changed, 63 insertions, 15 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index b2feb990..01d9f33e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -22,6 +22,9 @@ Unreleased
- Packaging is now compliant with `PEP 517`_, closing `issue 1395`_.
+- A new debug option ``--debug=pathmap`` shows details of the remapping of
+ paths that happens during combine due to the ``[paths]`` setting.
+
- Fix an internal problem with caching of invalid Python parsing. Found by
OSS-Fuzz, fixing their `bug 50381`_.
diff --git a/coverage/control.py b/coverage/control.py
index 9c4a2ac7..5e1e54bf 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -743,7 +743,10 @@ class Coverage:
aliases = None
if self.config.paths:
- aliases = PathAliases(relative=self.config.relative_files)
+ aliases = PathAliases(
+ debugfn=(self._debug.write if self._debug.should("pathmap") else None),
+ relative=self.config.relative_files,
+ )
for paths in self.config.paths.values():
result = paths[0]
for pattern in paths[1:]:
diff --git a/coverage/files.py b/coverage/files.py
index b5895cfc..4d0c1a2b 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -3,8 +3,8 @@
"""File wrangling."""
-import hashlib
import fnmatch
+import hashlib
import ntpath
import os
import os.path
@@ -326,15 +326,17 @@ class PathAliases:
map a path through those aliases to produce a unified path.
"""
- def __init__(self, relative=False):
- self.aliases = []
+ def __init__(self, debugfn=None, relative=False):
+ self.aliases = [] # A list of (original_pattern, regex, result)
+ self.debugfn = debugfn or (lambda msg: 0)
self.relative = relative
+ self.pprinted = False
- def pprint(self): # pragma: debugging
+ def pprint(self):
"""Dump the important parts of the PathAliases, for debugging."""
- print(f"Aliases (relative={self.relative}):")
- for regex, result in self.aliases:
- print(f"{regex.pattern!r} --> {result!r}")
+ self.debugfn(f"Aliases (relative={self.relative}):")
+ for original_pattern, regex, result in self.aliases:
+ self.debugfn(f" Rule: {original_pattern!r} -> {result!r} using regex {regex.pattern!r}")
def add(self, pattern, result):
"""Add the `pattern`/`result` pair to the list of aliases.
@@ -349,6 +351,7 @@ class PathAliases:
match an entire tree, and not just its root.
"""
+ original_pattern = pattern
pattern_sep = sep(pattern)
if len(pattern) > 1:
@@ -360,8 +363,7 @@ class PathAliases:
# The pattern is meant to match a filepath. Let's make it absolute
# unless it already is, or is meant to match any prefix.
- if not pattern.startswith('*') and not isabs_anywhere(pattern +
- pattern_sep):
+ if not pattern.startswith('*') and not isabs_anywhere(pattern + pattern_sep):
pattern = abs_file(pattern)
if not pattern.endswith(pattern_sep):
pattern += pattern_sep
@@ -372,7 +374,7 @@ class PathAliases:
# Normalize the result: it must end with a path separator.
result_sep = sep(result)
result = result.rstrip(r"\/") + result_sep
- self.aliases.append((regex, result))
+ self.aliases.append((original_pattern, regex, result))
def map(self, path):
"""Map `path` through the aliases.
@@ -390,14 +392,23 @@ class PathAliases:
of `path` unchanged.
"""
- for regex, result in self.aliases:
+ if not self.pprinted:
+ self.pprint()
+ self.pprinted = True
+
+ for original_pattern, regex, result in self.aliases:
m = regex.match(path)
if m:
new = path.replace(m[0], result)
new = new.replace(sep(path), sep(result))
if not self.relative:
new = canonical_filename(new)
+ self.debugfn(
+ f"Matched path {path!r} to rule {original_pattern!r} -> {result!r}, " +
+ f"producing {new!r}"
+ )
return new
+ self.debugfn(f"No rules match, path {path!r} is unchanged")
return path
diff --git a/coverage/sqldata.py b/coverage/sqldata.py
index 564d4ec9..5d62b15b 100644
--- a/coverage/sqldata.py
+++ b/coverage/sqldata.py
@@ -609,8 +609,7 @@ class CoverageData(SimpleReprMixin):
aliases = aliases or PathAliases()
- # Force the database we're writing to to exist before we start nesting
- # contexts.
+ # Force the database we're writing to to exist before we start nesting contexts.
self._start_using()
# Collector for all arcs, lines and tracers
diff --git a/doc/cmd.rst b/doc/cmd.rst
index e2a60fc2..c05b7bce 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -1000,6 +1000,9 @@ of operation to log:
* ``multiproc``: log the start and stop of multiprocessing processes.
+* ``pathmap``: log the remapping of paths that happens during ``coverage
+ combine`` due to the ``[paths]`` setting. See :ref:`config_paths`.
+
* ``pid``: annotate all warnings and debug output with the process and thread
ids.
diff --git a/doc/config.rst b/doc/config.rst
index 1b6f128d..66b02eac 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -350,6 +350,9 @@ against the source file found at "src/module.py".
If you specify more than one list of paths, they will be considered in order.
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.
diff --git a/tests/test_files.py b/tests/test_files.py
index 9f7f6278..b1e85610 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -289,11 +289,37 @@ class PathAliasesTest(CoverageTest):
self.assert_unchanged(aliases, '/home/foo/srcetc')
def test_multiple_patterns(self, rel_yn):
- aliases = PathAliases(relative=rel_yn)
+ # also test the debugfn...
+ msgs = []
+ aliases = PathAliases(debugfn=msgs.append, relative=rel_yn)
aliases.add('/home/*/src', './mysrc')
aliases.add('/lib/*/libsrc', './mylib')
self.assert_mapped(aliases, '/home/foo/src/a.py', './mysrc/a.py', relative=rel_yn)
self.assert_mapped(aliases, '/lib/foo/libsrc/a.py', './mylib/a.py', relative=rel_yn)
+ if rel_yn:
+ assert msgs == [
+ "Aliases (relative=True):",
+ " Rule: '/home/*/src' -> './mysrc/' using regex " +
+ "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'",
+ " Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
+ "'(?:(?s:[\\\\\\\\/]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/', " +
+ "producing './mylib/a.py'",
+ ]
+ else:
+ assert msgs == [
+ "Aliases (relative=False):",
+ " Rule: '/home/*/src' -> './mysrc/' using regex " +
+ "'(?:(?s:[\\\\\\\\/]home[\\\\\\\\/].*[\\\\\\\\/]src[\\\\\\\\/]))'",
+ " Rule: '/lib/*/libsrc' -> './mylib/' using regex " +
+ "'(?:(?s:[\\\\\\\\/]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/', " +
+ f"producing {files.canonical_filename('./mylib/a.py')!r}",
+ ]
@pytest.mark.parametrize("badpat", [
"/ned/home/*",