diff --git a/CHANGES.rst b/CHANGES.rst index 74f2d4bcc..b40607464 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,12 @@ development at the same time, such as 4.5.x and 5.0. Unreleased ---------- +- Fix: if a virtualenv was created inside a source directory, and a sourced + package was installed inside the virtualenv, then all of the third-party + packages inside the virtualenv would be measured. This was incorrect, but + has now been fixed: only the specified packages will be measured, thanks to + `Manuel Jacob `_. + - Fix: the ``coverage lcov`` command could create a .lcov file with incorrect LF (lines found) and LH (lines hit) totals. This is now fixed, thanks to `Ian Moore `_. @@ -28,6 +34,7 @@ Unreleased duplicate ```` elements. This is now fixed, thanks to `Benjamin Parzella `_, closing `issue 1573`_. +.. _pull 1560: https://github.com/nedbat/coveragepy/pull/1560 .. _issue 1573: https://github.com/nedbat/coveragepy/issues/1573 .. _pull 1574: https://github.com/nedbat/coveragepy/pull/1574 .. _pull 1583: https://github.com/nedbat/coveragepy/pull/1583 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index bb69b823b..a50138f85 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -107,6 +107,7 @@ Leonardo Pistone Lex Berezhny Loïc Dachary Lorenzo Micò +Manuel Jacob Marc Abramowitz Marc Legendre Marcelo Trylesinski diff --git a/coverage/inorout.py b/coverage/inorout.py index babaa3d80..d0d0ef913 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -262,7 +262,7 @@ def _debug(msg: str) -> None: # Check if the source we want to measure has been installed as a # third-party package. # Is the source inside a third-party area? - self.source_in_third = False + self.source_in_third_paths = set() with sys_modules_saved(): for pkg in self.source_pkgs: try: @@ -274,22 +274,23 @@ def _debug(msg: str) -> None: if modfile: if self.third_match.match(modfile): _debug( - f"Source is in third-party because of source_pkg {pkg!r} at {modfile!r}" + f"Source in third-party: source_pkg {pkg!r} at {modfile!r}" ) - self.source_in_third = True + self.source_in_third_paths.add(canonical_path(source_for_file(modfile))) else: for pathdir in path: if self.third_match.match(pathdir): _debug( - f"Source is in third-party because of {pkg!r} path directory " + - f"at {pathdir!r}" + f"Source in third-party: {pkg!r} path directory at {pathdir!r}" ) - self.source_in_third = True + self.source_in_third_paths.add(pathdir) for src in self.source: if self.third_match.match(src): - _debug(f"Source is in third-party because of source directory {src!r}") - self.source_in_third = True + _debug(f"Source in third-party: source directory {src!r}") + self.source_in_third_paths.add(src) + self.source_in_third_match = TreeMatcher(self.source_in_third_paths, "source_in_third") + _debug(f"Source in third-party matching: {self.source_in_third_match}") self.plugins: Plugins self.disp_class: Type[TFileDisposition] = FileDisposition @@ -419,9 +420,8 @@ def check_include_omit_etc(self, filename: str, frame: Optional[FrameType]) -> O ok = True if not ok: return extra + "falls outside the --source spec" - if not self.source_in_third: - if self.third_match.match(filename): - return "inside --source, but is third-party" + if self.third_match.match(filename) and not self.source_in_third_match.match(filename): + return "inside --source, but is third-party" elif self.include_match: if not self.include_match.match(filename): return "falls outside the --include trees" @@ -576,12 +576,13 @@ def sys_info(self) -> Iterable[Tuple[str, Any]]: ("coverage_paths", self.cover_paths), ("stdlib_paths", self.pylib_paths), ("third_party_paths", self.third_paths), + ("source_in_third_party_paths", self.source_in_third_paths), ] matcher_names = [ 'source_match', 'source_pkgs_match', 'include_match', 'omit_match', - 'cover_match', 'pylib_match', 'third_match', + 'cover_match', 'pylib_match', 'third_match', 'source_in_third_match', ] for matcher_name in matcher_names: diff --git a/tests/test_venv.py b/tests/test_venv.py index de7ebbe18..ae5b303f7 100644 --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -198,8 +198,28 @@ def get_trace_output(self) -> str: with open("debug_out.txt") as f: return f.read() - def test_third_party_venv_isnt_measured(self, coverage_command: str) -> None: - out = run_in_venv(coverage_command + " run --source=. myproduct.py") + @pytest.mark.parametrize('install_source_in_venv', [True, False]) + def test_third_party_venv_isnt_measured( + self, coverage_command: str, install_source_in_venv: bool + ) -> None: + if install_source_in_venv: + make_file("setup.py", """\ + import setuptools + setuptools.setup( + name="myproduct", + py_modules = ["myproduct"], + ) + """) + try: + run_in_venv("python -m pip install .") + finally: + shutil.rmtree("build", ignore_errors=True) + shutil.rmtree("myproduct.egg-info", ignore_errors=True) + # Ensure that coverage doesn't run the non-installed module. + os.remove('myproduct.py') + out = run_in_venv(coverage_command + " run --source=.,myproduct -m myproduct") + else: + out = run_in_venv(coverage_command + " run --source=. myproduct.py") # In particular, this warning doesn't appear: # Already imported a file that will be measured: .../coverage/__main__.py assert out == self.expected_stdout @@ -213,7 +233,7 @@ def test_third_party_venv_isnt_measured(self, coverage_command: str) -> None: ) assert re_lines(r"^Tracing .*\bmyproduct.py", debug_out) assert re_lines( - r"^Not tracing .*\bcolorsys.py': falls outside the --source spec", + r"^Not tracing .*\bcolorsys.py': (module 'colorsys' |)?falls outside the --source spec", debug_out, )