# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests about understanding how third-party code is installed.""" import os import os.path import shutil import pytest from coverage import env from tests.coveragetest import CoverageTest, COVERAGE_INSTALL_ARGS from tests.helpers import change_dir, make_file from tests.helpers import re_lines, run_command def run_in_venv(cmd): r"""Run `cmd` in the virtualenv at `venv`. The first word of the command will be adjusted to run it from the venv/bin or venv\Scripts directory. Returns the text output of the command. """ words = cmd.split() if env.WINDOWS: words[0] = fr"venv\Scripts\{words[0]}.exe" else: words[0] = fr"venv/bin/{words[0]}" status, output = run_command(" ".join(words)) # Print the output so if it fails, we can tell what happened. print(output) assert status == 0 return output @pytest.fixture(scope="session", name="venv_world") def venv_world_fixture(tmp_path_factory): """Create a virtualenv with a few test packages for VirtualenvTest to use. Returns the directory containing the "venv" virtualenv. """ venv_world = tmp_path_factory.mktemp("venv_world") with change_dir(venv_world): # Create a virtualenv. run_command("python -m venv venv") # A third-party package that installs a few different packages. make_file("third_pkg/third/__init__.py", """\ import fourth def third(x): return 3 * x """) # Use plugin2.py as third.plugin with open(os.path.join(os.path.dirname(__file__), "plugin2.py")) as f: make_file("third_pkg/third/plugin.py", f.read()) # A render function for plugin2 to use for dynamic file names. make_file("third_pkg/third/render.py", """\ def render(filename, linenum): return "HTML: {}@{}".format(filename, linenum) """) # Another package that third can use. make_file("third_pkg/fourth/__init__.py", """\ def fourth(x): return 4 * x """) # Some namespace packages. make_file("third_pkg/nspkg/fifth/__init__.py", """\ def fifth(x): return 5 * x """) # The setup.py to install everything. make_file("third_pkg/setup.py", """\ import setuptools setuptools.setup( name="third", packages=["third", "fourth", "nspkg.fifth"], ) """) # Some namespace packages. make_file("another_pkg/nspkg/sixth/__init__.py", """\ def sixth(x): return 6 * x """) make_file("another_pkg/setup.py", """\ import setuptools setuptools.setup( name="another", packages=["nspkg.sixth"], ) """) # Bug888 code. make_file("bug888/app/setup.py", """\ from setuptools import setup setup( name='testcov', packages=['testcov'], namespace_packages=['testcov'], ) """) make_file("bug888/app/testcov/__init__.py", """\ try: # pragma: no cover __import__('pkg_resources').declare_namespace(__name__) except ImportError: # pragma: no cover from pkgutil import extend_path __path__ = extend_path(__path__, __name__) """) make_file("bug888/app/testcov/main.py", """\ import pkg_resources for entry_point in pkg_resources.iter_entry_points('plugins'): entry_point.load()() """) make_file("bug888/plugin/setup.py", """\ from setuptools import setup setup( name='testcov-plugin', packages=['testcov'], namespace_packages=['testcov'], entry_points={'plugins': ['testp = testcov.plugin:testp']}, ) """) make_file("bug888/plugin/testcov/__init__.py", """\ try: # pragma: no cover __import__('pkg_resources').declare_namespace(__name__) except ImportError: # pragma: no cover from pkgutil import extend_path __path__ = extend_path(__path__, __name__) """) make_file("bug888/plugin/testcov/plugin.py", """\ def testp(): print("Plugin here") """) # Install everything. run_in_venv( "python -m pip install " + "./third_pkg " + "-e ./another_pkg " + "-e ./bug888/app -e ./bug888/plugin " + COVERAGE_INSTALL_ARGS ) shutil.rmtree("third_pkg") return venv_world @pytest.fixture(params=[ "coverage", "python -m coverage", ], name="coverage_command") def coverage_command_fixture(request): """Parametrized fixture to use multiple forms of "coverage" command.""" return request.param class VirtualenvTest(CoverageTest): """Tests of virtualenv considerations.""" expected_stdout = "33\n110\n198\n1.5\n" @pytest.fixture(autouse=True) def in_venv_world_fixture(self, venv_world): """For running tests inside venv_world, and cleaning up made files.""" with change_dir(venv_world): self.make_file("myproduct.py", """\ import colorsys import third import nspkg.fifth import nspkg.sixth print(third.third(11)) print(nspkg.fifth.fifth(22)) print(nspkg.sixth.sixth(33)) print(sum(colorsys.rgb_to_hls(1, 0, 0))) """) self.del_environ("COVERAGE_TESTING") # To get realistic behavior self.set_environ("COVERAGE_DEBUG_FILE", "debug_out.txt") self.set_environ("COVERAGE_DEBUG", "trace") yield for fname in os.listdir("."): if fname not in {"venv", "another_pkg", "bug888"}: os.remove(fname) def get_trace_output(self): """Get the debug output of coverage.py""" with open("debug_out.txt") as f: return f.read() def test_third_party_venv_isnt_measured(self, coverage_command): 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 # Check that our tracing was accurate. Files are mentioned because # --source refers to a file. debug_out = self.get_trace_output() assert re_lines( r"^Not tracing .*\bexecfile.py': inside --source, but is third-party", debug_out, ) assert re_lines(r"^Tracing .*\bmyproduct.py", debug_out) assert re_lines( r"^Not tracing .*\bcolorsys.py': falls outside the --source spec", debug_out, ) out = run_in_venv(coverage_command + " report") assert "myproduct.py" in out assert "third" not in out assert "coverage" not in out assert "colorsys" not in out def test_us_in_venv_isnt_measured(self, coverage_command): out = run_in_venv(coverage_command + " run --source=third myproduct.py") assert out == self.expected_stdout # Check that our tracing was accurate. Modules are mentioned because # --source refers to a module. debug_out = self.get_trace_output() assert re_lines( r"^Not tracing .*\bexecfile.py': " + "module 'coverage.execfile' falls outside the --source spec", debug_out, ) assert re_lines( r"^Not tracing .*\bmyproduct.py': module 'myproduct' falls outside the --source spec", debug_out, ) assert re_lines( r"^Not tracing .*\bcolorsys.py': module 'colorsys' falls outside the --source spec", debug_out, ) out = run_in_venv(coverage_command + " report") assert "myproduct.py" not in out assert "third" in out assert "coverage" not in out assert "colorsys" not in out def test_venv_isnt_measured(self, coverage_command): out = run_in_venv(coverage_command + " run myproduct.py") assert out == self.expected_stdout debug_out = self.get_trace_output() assert re_lines(r"^Not tracing .*\bexecfile.py': is part of coverage.py", debug_out) assert re_lines(r"^Tracing .*\bmyproduct.py", debug_out) assert re_lines(r"^Not tracing .*\bcolorsys.py': is in the stdlib", debug_out) out = run_in_venv(coverage_command + " report") assert "myproduct.py" in out assert "third" not in out assert "coverage" not in out assert "colorsys" not in out @pytest.mark.skipif(not env.C_TRACER, reason="Plugins are only supported with the C tracer.") def test_venv_with_dynamic_plugin(self, coverage_command): # https://github.com/nedbat/coveragepy/issues/1150 # Django coverage plugin was incorrectly getting warnings: # "Already imported: ... django/template/blah.py" # It happened because coverage imported the plugin, which imported # Django, and then the Django files were reported as traceable. self.make_file(".coveragerc", "[run]\nplugins=third.plugin\n") self.make_file("myrender.py", """\ import third.render print(third.render.render("hello.html", 1723)) """) out = run_in_venv(coverage_command + " run --source=. myrender.py") # The output should not have this warning: # Already imported a file that will be measured: ...third/render.py (already-imported) assert out == "HTML: hello.html@1723\n" def test_installed_namespace_packages(self, coverage_command): # https://github.com/nedbat/coveragepy/issues/1231 # When namespace packages were installed, they were considered # third-party packages. Test that isn't still happening. out = run_in_venv(coverage_command + " run --source=nspkg 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 # Check that our tracing was accurate. Files are mentioned because # --source refers to a file. debug_out = self.get_trace_output() assert re_lines( r"^Not tracing .*\bexecfile.py': " + "module 'coverage.execfile' falls outside the --source spec", debug_out, ) assert re_lines( r"^Not tracing .*\bmyproduct.py': module 'myproduct' falls outside the --source spec", debug_out, ) assert re_lines( r"^Not tracing .*\bcolorsys.py': module 'colorsys' falls outside the --source spec", debug_out, ) out = run_in_venv(coverage_command + " report") # Name Stmts Miss Cover # ------------------------------------------------------------------------------ # another_pkg/nspkg/sixth/__init__.py 2 0 100% # venv/lib/python3.9/site-packages/nspkg/fifth/__init__.py 2 0 100% # ------------------------------------------------------------------------------ # TOTAL 4 0 100% assert "myproduct.py" not in out assert "third" not in out assert "coverage" not in out assert "colorsys" not in out assert "fifth" in out assert "sixth" in out def test_bug_888(self, coverage_command): out = run_in_venv( coverage_command + " run --source=bug888/app,bug888/plugin bug888/app/testcov/main.py" ) # When the test fails, the output includes "Already imported a file that will be measured" assert out == "Plugin here\n"