summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2022-11-28 10:12:56 -0500
committerNed Batchelder <ned@nedbatchelder.com>2022-11-29 20:08:48 -0500
commit7e7c44b4f5c484105559690b1efccd84839bc640 (patch)
treeaa91b46a3a986ba9ee1b6375e13e1c780f851714
parente955f106134029e2b8991a3ad1299377b73a0e55 (diff)
downloadpython-coveragepy-git-7e7c44b4f5c484105559690b1efccd84839bc640.tar.gz
feat: file paths are only remapped if the result exists
-rw-r--r--CHANGES.rst8
-rw-r--r--coverage/files.py9
-rw-r--r--doc/config.rst4
-rw-r--r--tests/test_api.py23
-rw-r--r--tests/test_data.py3
-rw-r--r--tests/test_files.py11
-rw-r--r--tests/test_process.py2
7 files changed, 44 insertions, 16 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 63114d2a..e20d5562 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -32,6 +32,11 @@ released.)
- Using ``--format=total`` will write a single total number to the
output. This can be useful for making badges or writing status updates.
+- When remapping file paths with the ``[paths]`` setting, a path will be
+ remapped only if the resulting path exists. The documentation has long said
+ this was the case, but it was not enforced. This fixes `issue 608`_,
+ improves `issue 649`_, and closes `issue 757`_.
+
- Reporting operations now use the ``[paths]`` setting to remap file paths
within a single data file. Combining multiple files still requires the
``coverage combine`` step, but this simplifies some situations. Closes
@@ -62,7 +67,10 @@ released.)
- The deprecated ``[run] note`` setting has been completely removed.
.. _implicit namespace packages: https://peps.python.org/pep-0420/
+.. _issue 608: https://github.com/nedbat/coveragepy/issues/608
+.. _issue 649: https://github.com/nedbat/coveragepy/issues/649
.. _issue 713: https://github.com/nedbat/coveragepy/issues/713
+.. _issue 757: https://github.com/nedbat/coveragepy/issues/757
.. _issue 1212: https://github.com/nedbat/coveragepy/issues/1212
.. _issue 1383: https://github.com/nedbat/coveragepy/issues/1383
.. _issue 1418: https://github.com/nedbat/coveragepy/issues/1418
diff --git a/coverage/files.py b/coverage/files.py
index f016a32e..14d696b6 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -408,7 +408,7 @@ class PathAliases:
result = result.rstrip(r"\/") + result_sep
self.aliases.append((original_pattern, regex, result))
- def map(self, path):
+ def map(self, path, exists=os.path.exists):
"""Map `path` through the aliases.
`path` is checked against all of the patterns. The first pattern to
@@ -419,6 +419,9 @@ class PathAliases:
The separator style in the result is made to match that of the result
in the alias.
+ `exists` is a function to determine if the resulting path actually
+ exists.
+
Returns the mapped path. If a mapping has happened, this is a
canonical path. If no mapping has happened, it is the original value
of `path` unchanged.
@@ -438,6 +441,8 @@ class PathAliases:
dot_start = result.startswith(("./", ".\\")) and len(result) > 2
if new.startswith(("./", ".\\")) and not dot_start:
new = new[2:]
+ if not exists(new):
+ continue
self.debugfn(
f"Matched path {path!r} to rule {original_pattern!r} -> {result!r}, " +
f"producing {new!r}"
@@ -455,7 +460,7 @@ class PathAliases:
result = f"{dir1}{os.sep}"
self.debugfn(f"Generating rule: {pattern!r} -> {result!r} using regex {regex!r}")
self.aliases.append((pattern, re.compile(regex), result))
- return self.map(path)
+ return self.map(path, exists=exists)
self.debugfn(f"No rules match, path {path!r} is unchanged")
return path
diff --git a/doc/config.rst b/doc/config.rst
index b387deb5..90949506 100644
--- a/doc/config.rst
+++ b/doc/config.rst
@@ -344,7 +344,9 @@ combined with data for "c:\\myproj\\src\\module.py", and will be reported
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.
+A file path will only be remapped if the result exists. If a path matches a
+list, but the result doesn't exist, the next list will be tried. The first
+list that has an existing result will be used.
Remapping will also be done during reporting, but only within the single data
file being reported. Combining multiple files requires the ``combine``
diff --git a/tests/test_api.py b/tests/test_api.py
index 84457d88..ee24aa8f 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -478,8 +478,11 @@ class ApiTest(CoverageTest):
def test_ordered_combine(self):
# https://github.com/nedbat/coveragepy/issues/649
- # The order of the [paths] setting matters
- def make_data_file():
+ # The order of the [paths] setting used to matter. Now the
+ # resulting path must exist, so the order doesn't matter.
+ def make_files():
+ self.make_file("plugins/p1.py", "")
+ self.make_file("girder/g1.py", "")
self.make_data_file(
basename=".coverage.1",
lines={
@@ -498,7 +501,7 @@ class ApiTest(CoverageTest):
return filenames
# Case 1: get the order right.
- make_data_file()
+ make_files()
self.make_file(".coveragerc", """\
[paths]
plugins =
@@ -510,8 +513,8 @@ class ApiTest(CoverageTest):
""")
assert get_combined_filenames() == {'girder/g1.py', 'plugins/p1.py'}
- # Case 2: get the order wrong.
- make_data_file()
+ # Case 2: get the order "wrong".
+ make_files()
self.make_file(".coveragerc", """\
[paths]
girder =
@@ -521,7 +524,7 @@ class ApiTest(CoverageTest):
plugins/
ci/girder/plugins/
""")
- assert get_combined_filenames() == {'girder/g1.py', 'girder/plugins/p1.py'}
+ assert get_combined_filenames() == {'girder/g1.py', 'plugins/p1.py'}
def test_warnings(self):
self.make_file("hello.py", """\
@@ -1197,6 +1200,10 @@ class RelativePathTest(CoverageTest):
cov.save()
shutil.move(glob.glob(".coverage.*")[0], "..")
+ self.make_file("foo.py", "a = 1")
+ self.make_file("bar.py", "a = 1")
+ self.make_file("modsrc/__init__.py", "x = 1")
+
self.make_file(".coveragerc", """\
[run]
relative_files = true
@@ -1209,10 +1216,6 @@ class RelativePathTest(CoverageTest):
cov.combine()
cov.save()
- self.make_file("foo.py", "a = 1")
- self.make_file("bar.py", "a = 1")
- self.make_file("modsrc/__init__.py", "x = 1")
-
cov = coverage.Coverage()
cov.load()
files = cov.get_data().measured_files()
diff --git a/tests/test_data.py b/tests/test_data.py
index 79c90420..6a6228d8 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -788,6 +788,9 @@ class CoverageDataFilesTest(CoverageTest):
self.assert_file_count(".coverage.*", 2)
+ self.make_file("a.py", "")
+ self.make_file("sub/b.py", "")
+ self.make_file("template.html", "")
covdata3 = DebugCoverageData()
aliases = PathAliases()
aliases.add("/home/ned/proj/src/", "./")
diff --git a/tests/test_files.py b/tests/test_files.py
index a69d1a4b..85fb6dbb 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -346,16 +346,16 @@ class PathAliasesTest(CoverageTest):
since aliases produce canonicalized paths by default.
"""
- mapped = aliases.map(inp)
+ mapped = aliases.map(inp, exists=lambda p: True)
if aliases.relative:
expected = out
else:
expected = files.canonical_filename(out)
assert mapped == expected
- def assert_unchanged(self, aliases, inp):
+ def assert_unchanged(self, aliases, inp, exists=True):
"""Assert that `inp` mapped through `aliases` is unchanged."""
- assert aliases.map(inp) == inp
+ assert aliases.map(inp, exists=lambda p: exists) == inp
def test_noop(self, rel_yn):
aliases = PathAliases(relative=rel_yn)
@@ -380,6 +380,11 @@ class PathAliasesTest(CoverageTest):
aliases.add('/home/*/src', './mysrc')
self.assert_unchanged(aliases, '/home/foo/srcetc')
+ def test_no_map_if_not_exist(self, rel_yn):
+ aliases = PathAliases(relative=rel_yn)
+ aliases.add('/ned/home/*/src', './mysrc')
+ self.assert_unchanged(aliases, '/ned/home/foo/src/a.py', exists=False)
+
def test_no_dotslash(self, rel_yn):
# The result shouldn't start with "./" if the map result didn't.
aliases = PathAliases(relative=rel_yn)
diff --git a/tests/test_process.py b/tests/test_process.py
index 1f134a6d..3324497d 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -217,6 +217,8 @@ class ProcessTest(CoverageTest):
self.assert_file_count(".coverage.*", 2)
+ self.make_file("src/x.py", "")
+
self.run_command("coverage combine")
self.assert_exists(".coverage")