Skip to content

Commit 5c2f614

Browse files
committed
fix: don't measure third-party scripts
This finishes the last bit of #905 Also includes tighter logging of the reason for not tracing modules.
1 parent c196583 commit 5c2f614

File tree

3 files changed

+134
-59
lines changed

3 files changed

+134
-59
lines changed

coverage/inorout.py

+22-14
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def add_third_party_paths(paths):
177177
better_scheme = "pypy_posix" if scheme == "pypy" else scheme
178178
if os.name in better_scheme.split("_"):
179179
config_paths = sysconfig.get_paths(scheme)
180-
for path_name in ["platlib", "purelib"]:
180+
for path_name in ["platlib", "purelib", "scripts"]:
181181
paths.add(config_paths[path_name])
182182

183183

@@ -265,9 +265,6 @@ def debug(msg):
265265
against.append("modules {!r}".format(self.source_pkgs_match))
266266
debug("Source matching against " + " and ".join(against))
267267
else:
268-
if self.cover_paths:
269-
self.cover_match = TreeMatcher(self.cover_paths, "coverage")
270-
debug("Coverage code matching: {!r}".format(self.cover_match))
271268
if self.pylib_paths:
272269
self.pylib_match = TreeMatcher(self.pylib_paths, "pylib")
273270
debug("Python stdlib matching: {!r}".format(self.pylib_match))
@@ -277,9 +274,12 @@ def debug(msg):
277274
if self.omit:
278275
self.omit_match = FnmatchMatcher(self.omit, "omit")
279276
debug("Omit matching: {!r}".format(self.omit_match))
280-
if self.third_paths:
281-
self.third_match = TreeMatcher(self.third_paths, "third")
282-
debug("Third-party lib matching: {!r}".format(self.third_match))
277+
278+
self.cover_match = TreeMatcher(self.cover_paths, "coverage")
279+
debug("Coverage code matching: {!r}".format(self.cover_match))
280+
281+
self.third_match = TreeMatcher(self.third_paths, "third")
282+
debug("Third-party lib matching: {!r}".format(self.third_match))
283283

284284
# Check if the source we want to measure has been installed as a
285285
# third-party package.
@@ -429,27 +429,29 @@ def check_include_omit_etc(self, filename, frame):
429429
ok = True
430430
if not ok:
431431
return extra + "falls outside the --source spec"
432+
if self.cover_match.match(filename):
433+
return "inside --source, but is part of coverage.py"
432434
if not self.source_in_third:
433435
if self.third_match.match(filename):
434-
return "inside --source, but in third-party"
436+
return "inside --source, but is third-party"
435437
elif self.include_match:
436438
if not self.include_match.match(filename):
437439
return "falls outside the --include trees"
438440
else:
441+
# We exclude the coverage.py code itself, since a little of it
442+
# will be measured otherwise.
443+
if self.cover_match.match(filename):
444+
return "is part of coverage.py"
445+
439446
# If we aren't supposed to trace installed code, then check if this
440447
# is near the Python standard library and skip it if so.
441448
if self.pylib_match and self.pylib_match.match(filename):
442449
return "is in the stdlib"
443450

444451
# Exclude anything in the third-party installation areas.
445-
if self.third_match and self.third_match.match(filename):
452+
if self.third_match.match(filename):
446453
return "is a third-party module"
447454

448-
# We exclude the coverage.py code itself, since a little of it
449-
# will be measured otherwise.
450-
if self.cover_match and self.cover_match.match(filename):
451-
return "is part of coverage.py"
452-
453455
# Check the file against the omit pattern.
454456
if self.omit_match and self.omit_match.match(filename):
455457
return "is inside an --omit pattern"
@@ -485,6 +487,12 @@ def warn_already_imported_files(self):
485487
msg = "Already imported a file that will be measured: {}".format(filename)
486488
self.warn(msg, slug="already-imported")
487489
warned.add(filename)
490+
elif self.debug and self.debug.should('trace'):
491+
self.debug.write(
492+
"Didn't trace already imported file {!r}: {}".format(
493+
disp.original_filename, disp.reason
494+
)
495+
)
488496

489497
def warn_unimported_source(self):
490498
"""Warn about source packages that were of interest, but never traced."""

tests/helpers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def change_dir(new_dir):
240240
241241
"""
242242
old_dir = os.getcwd()
243-
os.chdir(new_dir)
243+
os.chdir(str(new_dir))
244244
try:
245245
yield
246246
finally:

tests/test_process.py

+111-44
Original file line numberDiff line numberDiff line change
@@ -1643,30 +1643,33 @@ def test_script_pkg_sub(self):
16431643
self.assert_pth_and_source_work_together('', 'pkg', 'sub')
16441644

16451645

1646-
def run_in_venv(args):
1647-
"""Run python with `args` in the "venv" virtualenv.
1646+
def run_in_venv(cmd):
1647+
r"""Run `cmd` in the virtualenv at `venv`.
1648+
1649+
The first word of the command will be adjusted to run it from the
1650+
venv/bin or venv\Scripts directory.
16481651
16491652
Returns the text output of the command.
16501653
"""
1654+
words = cmd.split()
16511655
if env.WINDOWS:
1652-
cmd = r".\venv\Scripts\python.exe "
1656+
words[0] = r"{}\Scripts\{}.exe".format("venv", words[0])
16531657
else:
1654-
cmd = "./venv/bin/python "
1655-
cmd += args
1656-
status, output = run_command(cmd)
1657-
print(output)
1658+
words[0] = "{}/bin/{}".format("venv", words[0])
1659+
status, output = run_command(" ".join(words))
16581660
assert status == 0
16591661
return output
16601662

16611663

1662-
@pytest.fixture(scope="session", name="venv_factory")
1663-
def venv_factory_fixture(tmp_path_factory):
1664-
"""Produce a function which can copy a venv template to a new directory.
1664+
@pytest.fixture(scope="session", name="venv_world")
1665+
def venv_world_fixture(tmp_path_factory):
1666+
"""Create a virtualenv with a few test packages for VirtualenvTest to use.
16651667
1666-
The function accepts one argument, the directory to use for the venv.
1668+
Returns the directory containing the "venv" virtualenv.
16671669
"""
1668-
tmpdir = tmp_path_factory.mktemp("venv_template")
1669-
with change_dir(str(tmpdir)):
1670+
1671+
venv_world = tmp_path_factory.mktemp("venv_world")
1672+
with change_dir(venv_world):
16701673
# Create a virtualenv.
16711674
run_command("python -m virtualenv venv")
16721675

@@ -1686,55 +1689,119 @@ def fourth(x):
16861689
""")
16871690

16881691
# Install the third-party packages.
1689-
run_in_venv("-m pip install --no-index ./third_pkg")
1692+
run_in_venv("python -m pip install --no-index ./third_pkg")
1693+
shutil.rmtree("third_pkg")
16901694

16911695
# Install coverage.
16921696
coverage_src = nice_file(TESTS_DIR, "..")
1693-
run_in_venv("-m pip install --no-index {}".format(coverage_src))
1697+
run_in_venv("python -m pip install --no-index {}".format(coverage_src))
16941698

1695-
def factory(dst):
1696-
"""The venv factory function.
1699+
return venv_world
16971700

1698-
Copies the venv template to `dst`.
1699-
"""
1700-
shutil.copytree(str(tmpdir / "venv"), dst, symlinks=(not env.WINDOWS))
17011701

1702-
return factory
1702+
@pytest.fixture(params=[
1703+
"coverage",
1704+
"python -m coverage",
1705+
], name="coverage_command")
1706+
def coverage_command_fixture(request):
1707+
"""Parametrized fixture to use multiple forms of "coverage" command."""
1708+
return request.param
17031709

17041710

17051711
class VirtualenvTest(CoverageTest):
17061712
"""Tests of virtualenv considerations."""
17071713

1708-
def setup_test(self):
1709-
self.make_file("myproduct.py", """\
1710-
import third
1711-
print(third.third(11))
1712-
""")
1713-
self.del_environ("COVERAGE_TESTING") # To avoid needing contracts installed.
1714-
super(VirtualenvTest, self).setup_test()
1715-
1716-
def test_third_party_venv_isnt_measured(self, venv_factory):
1717-
venv_factory("venv")
1718-
out = run_in_venv("-m coverage run --source=. myproduct.py")
1714+
@pytest.fixture(autouse=True)
1715+
def in_venv_world_fixture(self, venv_world):
1716+
"""For running tests inside venv_world, and cleaning up made files."""
1717+
with change_dir(venv_world):
1718+
self.make_file("myproduct.py", """\
1719+
import colorsys
1720+
import third
1721+
print(third.third(11))
1722+
print(sum(colorsys.rgb_to_hls(1, 0, 0)))
1723+
""")
1724+
self.expected_stdout = "33\n1.5\n" # pylint: disable=attribute-defined-outside-init
1725+
1726+
self.del_environ("COVERAGE_TESTING") # To avoid needing contracts installed.
1727+
self.set_environ("COVERAGE_DEBUG_FILE", "debug_out.txt")
1728+
self.set_environ("COVERAGE_DEBUG", "trace")
1729+
1730+
yield
1731+
1732+
for fname in os.listdir("."):
1733+
if fname != "venv":
1734+
os.remove(fname)
1735+
1736+
def get_trace_output(self):
1737+
"""Get the debug output of coverage.py"""
1738+
with open("debug_out.txt") as f:
1739+
return f.read()
1740+
1741+
def test_third_party_venv_isnt_measured(self, coverage_command):
1742+
out = run_in_venv(coverage_command + " run --source=. myproduct.py")
17191743
# In particular, this warning doesn't appear:
17201744
# Already imported a file that will be measured: .../coverage/__main__.py
1721-
assert out == "33\n"
1722-
out = run_in_venv("-m coverage report")
1745+
assert out == self.expected_stdout
1746+
1747+
# Check that our tracing was accurate. Files are mentioned because
1748+
# --source refers to a file.
1749+
debug_out = self.get_trace_output()
1750+
assert re_lines(
1751+
debug_out,
1752+
r"^Not tracing .*\bexecfile.py': inside --source, but is part of coverage.py"
1753+
)
1754+
assert re_lines(debug_out, r"^Tracing .*\bmyproduct.py")
1755+
assert re_lines(
1756+
debug_out,
1757+
r"^Not tracing .*\bcolorsys.py': falls outside the --source spec"
1758+
)
1759+
1760+
out = run_in_venv("python -m coverage report")
17231761
assert "myproduct.py" in out
17241762
assert "third" not in out
1763+
assert "coverage" not in out
1764+
assert "colorsys" not in out
1765+
1766+
def test_us_in_venv_isnt_measured(self, coverage_command):
1767+
out = run_in_venv(coverage_command + " run --source=third myproduct.py")
1768+
assert out == self.expected_stdout
1769+
1770+
# Check that our tracing was accurate. Modules are mentioned because
1771+
# --source refers to a module.
1772+
debug_out = self.get_trace_output()
1773+
assert re_lines(
1774+
debug_out,
1775+
r"^Not tracing .*\bexecfile.py': " +
1776+
"module 'coverage.execfile' falls outside the --source spec"
1777+
)
1778+
print(re_lines(debug_out, "myproduct"))
1779+
assert re_lines(
1780+
debug_out,
1781+
r"^Not tracing .*\bmyproduct.py': module u?'myproduct' falls outside the --source spec"
1782+
)
1783+
assert re_lines(
1784+
debug_out,
1785+
r"^Not tracing .*\bcolorsys.py': module u?'colorsys' falls outside the --source spec"
1786+
)
17251787

1726-
def test_us_in_venv_is_measured(self, venv_factory):
1727-
venv_factory("venv")
1728-
out = run_in_venv("-m coverage run --source=third myproduct.py")
1729-
assert out == "33\n"
1730-
out = run_in_venv("-m coverage report")
1788+
out = run_in_venv("python -m coverage report")
17311789
assert "myproduct.py" not in out
17321790
assert "third" in out
1791+
assert "coverage" not in out
1792+
assert "colorsys" not in out
1793+
1794+
def test_venv_isnt_measured(self, coverage_command):
1795+
out = run_in_venv(coverage_command + " run myproduct.py")
1796+
assert out == self.expected_stdout
1797+
1798+
debug_out = self.get_trace_output()
1799+
assert re_lines(debug_out, r"^Not tracing .*\bexecfile.py': is part of coverage.py")
1800+
assert re_lines(debug_out, r"^Tracing .*\bmyproduct.py")
1801+
assert re_lines(debug_out, r"^Not tracing .*\bcolorsys.py': is in the stdlib")
17331802

1734-
def test_venv_isnt_measured(self, venv_factory):
1735-
venv_factory("venv")
1736-
out = run_in_venv("-m coverage run myproduct.py")
1737-
assert out == "33\n"
1738-
out = run_in_venv("-m coverage report")
1803+
out = run_in_venv("python -m coverage report")
17391804
assert "myproduct.py" in out
17401805
assert "third" not in out
1806+
assert "coverage" not in out
1807+
assert "colorsys" not in out

0 commit comments

Comments
 (0)