Skip to content

Commit

Permalink
feat: file paths are only remapped if the result exists
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Nov 30, 2022
1 parent e955f10 commit 7e7c44b
Show file tree
Hide file tree
Showing 7 changed files with 44 additions and 16 deletions.
8 changes: 8 additions & 0 deletions CHANGES.rst
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions coverage/files.py
Expand Up @@ -408,7 +408,7 @@ def add(self, pattern, result):
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
Expand All @@ -419,6 +419,9 @@ def map(self, path):
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.
Expand All @@ -438,6 +441,8 @@ def map(self, path):
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}"
Expand All @@ -455,7 +460,7 @@ def map(self, path):
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
Expand Down
4 changes: 3 additions & 1 deletion doc/config.rst
Expand Up @@ -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``
Expand Down
23 changes: 13 additions & 10 deletions tests/test_api.py
Expand Up @@ -478,8 +478,11 @@ def test_combining_with_a_used_coverage(self):

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={
Expand All @@ -498,7 +501,7 @@ def get_combined_filenames():
return filenames

# Case 1: get the order right.
make_data_file()
make_files()
self.make_file(".coveragerc", """\
[paths]
plugins =
Expand All @@ -510,8 +513,8 @@ def get_combined_filenames():
""")
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 =
Expand All @@ -521,7 +524,7 @@ def get_combined_filenames():
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", """\
Expand Down Expand Up @@ -1197,6 +1200,10 @@ def test_combine_relative(self):
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
Expand All @@ -1209,10 +1216,6 @@ def test_combine_relative(self):
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()
Expand Down
3 changes: 3 additions & 0 deletions tests/test_data.py
Expand Up @@ -788,6 +788,9 @@ def test_combining_with_aliases(self):

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/", "./")
Expand Down
11 changes: 8 additions & 3 deletions tests/test_files.py
Expand Up @@ -346,16 +346,16 @@ def assert_mapped(self, aliases, inp, out):
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)
Expand All @@ -380,6 +380,11 @@ def test_no_accidental_match(self, rel_yn):
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)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_process.py
Expand Up @@ -217,6 +217,8 @@ def test_combine_with_aliases(self):

self.assert_file_count(".coverage.*", 2)

self.make_file("src/x.py", "")

self.run_command("coverage combine")
self.assert_exists(".coverage")

Expand Down

0 comments on commit 7e7c44b

Please sign in to comment.