diff --git a/CHANGES.rst b/CHANGES.rst index 63114d2a0..e20d55620 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 f016a32ef..14d696b6b 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -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 @@ -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. @@ -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}" @@ -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 diff --git a/doc/config.rst b/doc/config.rst index b387deb5a..90949506a 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 84457d884..ee24aa8fd 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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={ @@ -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 = @@ -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 = @@ -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", """\ @@ -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 @@ -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() diff --git a/tests/test_data.py b/tests/test_data.py index 79c90420b..6a6228d80 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -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/", "./") diff --git a/tests/test_files.py b/tests/test_files.py index a69d1a4b0..85fb6dbb7 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -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) @@ -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) diff --git a/tests/test_process.py b/tests/test_process.py index 1f134a6da..3324497d9 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -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")