summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst2
-rw-r--r--coverage/control.py5
-rw-r--r--coverage/exceptions.py5
-rw-r--r--coverage/inorout.py13
-rw-r--r--coverage/pytracer.py6
-rw-r--r--tests/helpers.py15
-rw-r--r--tests/test_api.py76
-rw-r--r--tests/test_html.py15
-rw-r--r--tests/test_oddball.py26
-rw-r--r--tests/test_plugins.py23
-rw-r--r--tests/test_process.py14
-rw-r--r--tests/test_summary.py5
-rw-r--r--tests/test_testing.py58
-rw-r--r--tests/test_xml.py9
14 files changed, 183 insertions, 89 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 1dd5c175..205ef0ab 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -33,6 +33,8 @@ Unreleased
imported a file that will be measured" warnings about Django itself. These
have been fixed, closing `issue 1150`_.
+- Warnings generated by coverage.py are now real Python warnings.
+
- The ``COVERAGE_DEBUG_FILE`` environment variable now accepts ``stdout`` and
``stderr`` to write to those destinations.
diff --git a/coverage/control.py b/coverage/control.py
index bf91d447..b13acf45 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -11,6 +11,7 @@ import os.path
import platform
import sys
import time
+import warnings
from coverage import env
from coverage.annotate import AnnotateReporter
@@ -20,7 +21,7 @@ from coverage.context import should_start_context_test_function, combine_context
from coverage.data import CoverageData, combine_parallel_data
from coverage.debug import DebugControl, short_stack, write_formatted_info
from coverage.disposition import disposition_debug_msg
-from coverage.exceptions import CoverageException
+from coverage.exceptions import CoverageException, CoverageWarning
from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
from coverage.html import HtmlReporter
from coverage.inorout import InOrOut
@@ -362,7 +363,7 @@ class Coverage:
msg = f"{msg} ({slug})"
if self._debug.should('pid'):
msg = f"[{os.getpid()}] {msg}"
- sys.stderr.write(f"Coverage.py warning: {msg}\n")
+ warnings.warn(msg, category=CoverageWarning, stacklevel=2)
if once:
self._no_warn_slugs.append(slug)
diff --git a/coverage/exceptions.py b/coverage/exceptions.py
index ed96fb21..6631e1ad 100644
--- a/coverage/exceptions.py
+++ b/coverage/exceptions.py
@@ -46,3 +46,8 @@ class StopEverything(BaseCoverageException):
"""
pass
+
+
+class CoverageWarning(Warning):
+ """A warning from Coverage.py."""
+ pass
diff --git a/coverage/inorout.py b/coverage/inorout.py
index fae9ef18..32eb9079 100644
--- a/coverage/inorout.py
+++ b/coverage/inorout.py
@@ -356,10 +356,9 @@ class InOrOut:
)
break
except Exception:
- self.warn(
- "Disabling plug-in %r due to an exception:" % (plugin._coverage_plugin_name)
- )
- traceback.print_exc()
+ plugin_name = plugin._coverage_plugin_name
+ tb = traceback.format_exc()
+ self.warn(f"Disabling plug-in {plugin_name!r} due to an exception:\n{tb}")
plugin._coverage_enabled = False
continue
else:
@@ -503,10 +502,8 @@ class InOrOut:
# The module was in sys.modules, and seems like a module with code, but
# we never measured it. I guess that means it was imported before
# coverage even started.
- self.warn(
- "Module %s was previously imported, but not measured" % pkg,
- slug="module-not-measured",
- )
+ msg = f"Module {pkg} was previously imported, but not measured"
+ self.warn(msg, slug="module-not-measured")
def find_possibly_unexecuted_files(self):
"""Find files in the areas of interest that might be untraced.
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index 51f08a1b..540df68c 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -254,10 +254,8 @@ class PyTracer:
# has changed to None.
dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None)
if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable
- self.warn(
- f"Trace function changed, measurement is likely wrong: {tf!r}",
- slug="trace-changed",
- )
+ msg = f"Trace function changed, measurement is likely wrong: {tf!r}"
+ self.warn(msg, slug="trace-changed")
def activity(self):
"""Has there been any activity?"""
diff --git a/tests/helpers.py b/tests/helpers.py
index 21459cd4..369875b9 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -14,6 +14,7 @@ import textwrap
from unittest import mock
+from coverage.exceptions import CoverageWarning
from coverage.misc import output_encoding
@@ -262,3 +263,17 @@ def assert_count_equal(a, b):
This only works for hashable elements.
"""
assert collections.Counter(list(a)) == collections.Counter(list(b))
+
+
+def assert_coverage_warnings(warns, *msgs):
+ """
+ Assert that `warns` are all CoverageWarning's, and have `msgs` as messages.
+ """
+ assert msgs # don't call this without some messages.
+ assert len(warns) == len(msgs)
+ assert all(w.category == CoverageWarning for w in warns)
+ for actual, expected in zip((w.message.args[0] for w in warns), msgs):
+ if hasattr(expected, "search"):
+ assert expected.search(actual), f"{actual!r} didn't match {expected!r}"
+ else:
+ assert expected == actual
diff --git a/tests/test_api.py b/tests/test_api.py
index d6a9c08a..885f3370 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -23,7 +23,7 @@ from coverage.files import abs_file, relative_filename
from coverage.misc import import_local_file
from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
-from tests.helpers import assert_count_equal, change_dir, nice_file
+from tests.helpers import assert_count_equal, assert_coverage_warnings, change_dir, nice_file
class ApiTest(CoverageTest):
@@ -300,9 +300,11 @@ class ApiTest(CoverageTest):
# If nothing was measured, the file-touching didn't happen properly.
self.make_file("foo/bar.py", "print('Never run')")
self.make_file("test.py", "assert True")
- cov = coverage.Coverage(source=["foo"])
- self.start_import_stop(cov, "test")
- cov.report()
+ with pytest.warns(Warning) as warns:
+ cov = coverage.Coverage(source=["foo"])
+ self.start_import_stop(cov, "test")
+ cov.report()
+ assert_coverage_warnings(warns, "No data was collected. (no-data-collected)")
# Name Stmts Miss Cover
# --------------------------------
# foo/bar.py 1 1 0%
@@ -517,18 +519,19 @@ class ApiTest(CoverageTest):
import sys, os
print("Hello")
""")
- cov = coverage.Coverage(source=["sys", "xyzzy", "quux"])
- self.start_import_stop(cov, "hello")
- cov.get_data()
-
- out, err = self.stdouterr()
- assert "Hello\n" in out
- assert textwrap.dedent("""\
- Coverage.py warning: Module sys has no Python source. (module-not-python)
- Coverage.py warning: Module xyzzy was never imported. (module-not-imported)
- Coverage.py warning: Module quux was never imported. (module-not-imported)
- Coverage.py warning: No data was collected. (no-data-collected)
- """) in err
+ with pytest.warns(Warning) as warns:
+ cov = coverage.Coverage(source=["sys", "xyzzy", "quux"])
+ self.start_import_stop(cov, "hello")
+ cov.get_data()
+
+ assert "Hello\n" == self.stdout()
+ assert_coverage_warnings(
+ warns,
+ "Module sys has no Python source. (module-not-python)",
+ "Module xyzzy was never imported. (module-not-imported)",
+ "Module quux was never imported. (module-not-imported)",
+ "No data was collected. (no-data-collected)",
+ )
def test_warnings_suppressed(self):
self.make_file("hello.py", """\
@@ -539,24 +542,25 @@ class ApiTest(CoverageTest):
[run]
disable_warnings = no-data-collected, module-not-imported
""")
- cov = coverage.Coverage(source=["sys", "xyzzy", "quux"])
- self.start_import_stop(cov, "hello")
- cov.get_data()
+ with pytest.warns(Warning) as warns:
+ cov = coverage.Coverage(source=["sys", "xyzzy", "quux"])
+ self.start_import_stop(cov, "hello")
+ cov.get_data()
- out, err = self.stdouterr()
- assert "Hello\n" in out
- assert "Coverage.py warning: Module sys has no Python source. (module-not-python)" in err
- assert "module-not-imported" not in err
- assert "no-data-collected" not in err
+ assert "Hello\n" == self.stdout()
+ assert_coverage_warnings(warns, "Module sys has no Python source. (module-not-python)")
+ # No "module-not-imported" in warns
+ # No "no-data-collected" in warns
def test_warn_once(self):
- cov = coverage.Coverage()
- cov.load()
- cov._warn("Warning, warning 1!", slug="bot", once=True)
- cov._warn("Warning, warning 2!", slug="bot", once=True)
- err = self.stderr()
- assert "Warning, warning 1!" in err
- assert "Warning, warning 2!" not in err
+ with pytest.warns(Warning) as warns:
+ cov = coverage.Coverage()
+ cov.load()
+ cov._warn("Warning, warning 1!", slug="bot", once=True)
+ cov._warn("Warning, warning 2!", slug="bot", once=True)
+
+ assert_coverage_warnings(warns, "Warning, warning 1! (bot)")
+ # No "Warning, warning 2!" in warns
def test_source_and_include_dont_conflict(self):
# A bad fix made this case fail: https://github.com/nedbat/coveragepy/issues/541
@@ -683,12 +687,12 @@ class ApiTest(CoverageTest):
cov = coverage.Coverage(source=["."])
cov.set_option("run:dynamic_context", "test_function")
cov.start()
- # Switch twice, but only get one warning.
- cov.switch_context("test1") # pragma: nested
- cov.switch_context("test2") # pragma: nested
- expected = "Coverage.py warning: Conflicting dynamic contexts (dynamic-conflict)\n"
- assert expected == self.stderr()
+ with pytest.warns(Warning) as warns:
+ # Switch twice, but only get one warning.
+ cov.switch_context("test1") # pragma: nested
+ cov.switch_context("test2") # pragma: nested
cov.stop() # pragma: nested
+ assert_coverage_warnings(warns, "Conflicting dynamic contexts (dynamic-conflict)")
def test_switch_context_unstarted(self):
# Coverage must be started to switch context
diff --git a/tests/test_html.py b/tests/test_html.py
index c9dbacc8..56519a64 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -24,7 +24,7 @@ from coverage.report import get_analysis_to_report
from tests.coveragetest import CoverageTest, TESTS_DIR
from tests.goldtest import gold_path
from tests.goldtest import compare, contains, doesnt_contain, contains_any
-from tests.helpers import change_dir
+from tests.helpers import assert_coverage_warnings, change_dir
class HtmlTestHelpers(CoverageTest):
@@ -341,13 +341,14 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest):
self.make_file("innocuous.py", "a = 2")
cov = coverage.Coverage()
self.start_import_stop(cov, "main")
+
self.make_file("innocuous.py", "<h1>This isn't python!</h1>")
- cov.html_report(ignore_errors=True)
- msg = "Expected a warning to be thrown when an invalid python file is parsed"
- assert 1 == len(cov._warnings), msg
- msg = "Warning message should be in 'invalid file' warning"
- assert "Couldn't parse Python file" in cov._warnings[0], msg
- assert "innocuous.py" in cov._warnings[0], "Filename should be in 'invalid file' warning"
+ with pytest.warns(Warning) as warns:
+ cov.html_report(ignore_errors=True)
+ assert_coverage_warnings(
+ warns,
+ re.compile(r"Couldn't parse Python file '.*innocuous.py' \(couldnt-parse\)"),
+ )
self.assert_exists("htmlcov/index.html")
# This would be better as a glob, if the HTML layout changes:
self.assert_doesnt_exist("htmlcov/innocuous.html")
diff --git a/tests/test_oddball.py b/tests/test_oddball.py
index 52f80734..a97fc190 100644
--- a/tests/test_oddball.py
+++ b/tests/test_oddball.py
@@ -81,17 +81,18 @@ class RecursionTest(CoverageTest):
def test_long_recursion(self):
# We can't finish a very deep recursion, but we don't crash.
with pytest.raises(RuntimeError):
- self.check_coverage("""\
- def recur(n):
- if n == 0:
- return 0
- else:
- return recur(n-1)+1
-
- recur(100000) # This is definitely too many frames.
- """,
- [1, 2, 3, 5, 7], ""
- )
+ with pytest.warns(None):
+ self.check_coverage("""\
+ def recur(n):
+ if n == 0:
+ return 0
+ else:
+ return recur(n-1)+1
+
+ recur(100000) # This is definitely too many frames.
+ """,
+ [1, 2, 3, 5, 7], ""
+ )
def test_long_recursion_recovery(self):
# Test the core of bug 93: https://github.com/nedbat/coveragepy/issues/93
@@ -117,7 +118,8 @@ class RecursionTest(CoverageTest):
""")
cov = coverage.Coverage()
- self.start_import_stop(cov, "recur")
+ with pytest.warns(None):
+ self.start_import_stop(cov, "recur")
pytrace = (cov._collector.tracer_name() == "PyTracer")
expected_missing = [3]
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 3401895b..b15ee45b 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -14,7 +14,7 @@ import coverage
from coverage import env
from coverage.control import Plugins
from coverage.data import line_counts
-from coverage.exceptions import CoverageException
+from coverage.exceptions import CoverageException, CoverageWarning
from coverage.misc import import_local_file
import coverage.plugin
@@ -193,7 +193,9 @@ class PluginTest(CoverageTest):
cov = coverage.Coverage(debug=["sys"])
cov._debug_file = debug_out
cov.set_option("run:plugins", ["plugin_sys_info"])
- cov.start()
+ with pytest.warns(None):
+ # Catch warnings so we don't see "plugins aren't supported on PyTracer"
+ cov.start()
cov.stop() # pragma: nested
out_lines = [line.strip() for line in debug_out.getvalue().splitlines()]
@@ -631,28 +633,29 @@ class BadFileTracerTest(FileTracerTest):
explaining why.
"""
- self.run_plugin(module_name)
+ with pytest.warns(Warning) as warns:
+ self.run_plugin(module_name)
stderr = self.stderr()
-
+ stderr += "".join(w.message.args[0] for w in warns)
if our_error:
- errors = stderr.count("# Oh noes!")
# The exception we're causing should only appear once.
- assert errors == 1
+ assert stderr.count("# Oh noes!") == 1
# There should be a warning explaining what's happening, but only one.
# The message can be in two forms:
# Disabling plug-in '...' due to previous exception
# or:
# Disabling plug-in '...' due to an exception:
- msg = f"Disabling plug-in '{module_name}.{plugin_name}' due to "
- warnings = stderr.count(msg)
- assert warnings == 1
+ assert len(warns) == 1
+ assert issubclass(warns[0].category, CoverageWarning)
+ warnmsg = warns[0].message.args[0]
+ assert f"Disabling plug-in '{module_name}.{plugin_name}' due to " in warnmsg
if excmsg:
assert excmsg in stderr
if excmsgs:
- assert any(em in stderr for em in excmsgs), "expected one of %r" % excmsgs
+ assert any(em in stderr for em in excmsgs), f"expected one of {excmsgs} in stderr"
def test_file_tracer_has_no_file_tracer_method(self):
self.make_file("bad_plugin.py", """\
diff --git a/tests/test_process.py b/tests/test_process.py
index a912debb..54bf345d 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -130,7 +130,7 @@ class ProcessTest(CoverageTest):
self.assert_exists(".coverage")
self.assert_exists(".coverage.bad")
warning_regex = (
- r"Coverage.py warning: Couldn't use data file '.*\.coverage\.bad': "
+ r"CoverageWarning: Couldn't use data file '.*\.coverage\.bad': "
r"file (is encrypted or )?is not a database"
)
assert re.search(warning_regex, out)
@@ -163,7 +163,7 @@ class ProcessTest(CoverageTest):
for n in "12":
self.assert_exists(f".coverage.bad{n}")
warning_regex = (
- r"Coverage.py warning: Couldn't use data file '.*\.coverage.bad{}': "
+ r"CoverageWarning: Couldn't use data file '.*\.coverage.bad{}': "
r"file (is encrypted or )?is not a database"
.format(n)
)
@@ -725,7 +725,7 @@ class ProcessTest(CoverageTest):
assert "Goodbye!" in out
msg = (
- "Coverage.py warning: "
+ "CoverageWarning: "
"Already imported a file that will be measured: {} "
"(already-imported)").format(goodbye_path)
assert msg in out
@@ -815,10 +815,14 @@ class ProcessTest(CoverageTest):
inst.save()
""")
out = self.run_command("python run_twice.py")
+ # Remove the file location and source line from the warning.
+ out = re.sub(r"(?m)^[\\/\w.:~_-]+:\d+: CoverageWarning: ", "f:d: CoverageWarning: ", out)
+ out = re.sub(r"(?m)^\s+self.warn.*$\n", "", out)
+ print("out:", repr(out))
expected = (
"Run 1\n" +
"Run 2\n" +
- "Coverage.py warning: Module foo was previously imported, but not measured " +
+ "f:d: CoverageWarning: Module foo was previously imported, but not measured " +
"(module-not-measured)\n"
)
assert expected == out
@@ -920,7 +924,7 @@ class EnvironmentTest(CoverageTest):
def test_coverage_run_dashm_superset_of_doubledashsource(self):
"""Edge case: --source foo -m foo.bar"""
# Ugh: without this config file, we'll get a warning about
- # Coverage.py warning: Module process_test was previously imported,
+ # CoverageWarning: Module process_test was previously imported,
# but not measured (module-not-measured)
#
# This is because process_test/__init__.py is imported while looking
diff --git a/tests/test_summary.py b/tests/test_summary.py
index a6384c46..b71921c7 100644
--- a/tests/test_summary.py
+++ b/tests/test_summary.py
@@ -595,12 +595,13 @@ class SummaryTest(UsingModulesMixin, CoverageTest):
self.make_file("mycode.py", "This isn't python at all!")
report = self.report_from_command("coverage report -i mycode.py")
- # Coverage.py warning: Couldn't parse Python file blah_blah/mycode.py (couldnt-parse)
+ # CoverageWarning: Couldn't parse Python file blah_blah/mycode.py (couldnt-parse)
+ # (source line)
# Name Stmts Miss Cover
# ----------------------------
# No data to report.
- assert self.line_count(report) == 4
+ assert self.line_count(report) == 5
assert 'No data to report.' in report
assert '(couldnt-parse)' in report
diff --git a/tests/test_testing.py b/tests/test_testing.py
index 3a563efe..7219ff0b 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -7,16 +7,18 @@ import datetime
import os
import re
import sys
+import warnings
import pytest
import coverage
from coverage import tomlconfig
+from coverage.exceptions import CoverageWarning
from coverage.files import actual_path
from tests.coveragetest import CoverageTest
from tests.helpers import (
- arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal,
+ arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, assert_coverage_warnings,
CheckUniqueFilenames, re_lines, re_line, without_module,
)
@@ -383,3 +385,57 @@ class ArczTest(CoverageTest):
])
def test_arcs_to_arcz_repr(self, arcs, arcz_repr):
assert arcs_to_arcz_repr(arcs) == arcz_repr
+
+
+class AssertCoverageWarningsTest(CoverageTest):
+ """Tests of assert_coverage_warnings"""
+
+ def test_one_warning(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("Hello there", category=CoverageWarning)
+ assert_coverage_warnings(warns, "Hello there")
+
+ def test_many_warnings(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("The first", category=CoverageWarning)
+ warnings.warn("The second", category=CoverageWarning)
+ warnings.warn("The third", category=CoverageWarning)
+ assert_coverage_warnings(warns, "The first", "The second", "The third")
+
+ def test_wrong_type(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("Not ours", category=Warning)
+ with pytest.raises(AssertionError):
+ assert_coverage_warnings(warns, "Not ours")
+
+ def test_wrong_message(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("Goodbye", category=CoverageWarning)
+ with pytest.raises(AssertionError):
+ assert_coverage_warnings(warns, "Hello there")
+
+ def test_wrong_number_too_many(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("The first", category=CoverageWarning)
+ warnings.warn("The second", category=CoverageWarning)
+ with pytest.raises(AssertionError):
+ assert_coverage_warnings(warns, "The first", "The second", "The third")
+
+ def test_wrong_number_too_few(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("The first", category=CoverageWarning)
+ warnings.warn("The second", category=CoverageWarning)
+ warnings.warn("The third", category=CoverageWarning)
+ with pytest.raises(AssertionError):
+ assert_coverage_warnings(warns, "The first", "The second")
+
+ def test_regex_matches(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("The first", category=CoverageWarning)
+ assert_coverage_warnings(warns, re.compile("f?rst"))
+
+ def test_regex_doesnt_match(self):
+ with pytest.warns(Warning) as warns:
+ warnings.warn("The first", category=CoverageWarning)
+ with pytest.raises(AssertionError):
+ assert_coverage_warnings(warns, re.compile("second"))
diff --git a/tests/test_xml.py b/tests/test_xml.py
index 9c6cfb58..a03257a2 100644
--- a/tests/test_xml.py
+++ b/tests/test_xml.py
@@ -16,7 +16,7 @@ from coverage.misc import import_local_file
from tests.coveragetest import CoverageTest
from tests.goldtest import compare, gold_path
-from tests.helpers import change_dir
+from tests.helpers import assert_coverage_warnings, change_dir
class XmlTestHelpers(CoverageTest):
@@ -213,7 +213,12 @@ class XmlReportTest(XmlTestHelpers, CoverageTest):
mod_foo = import_local_file("foo", "src/main/foo.py") # pragma: nested
mod_bar = import_local_file("bar", "also/over/there/bar.py") # pragma: nested
cov.stop() # pragma: nested
- cov.xml_report([mod_foo, mod_bar])
+ with pytest.warns(Warning) as warns:
+ cov.xml_report([mod_foo, mod_bar])
+ assert_coverage_warnings(
+ warns,
+ "Module not/really was never imported. (module-not-imported)",
+ )
dom = ElementTree.parse("coverage.xml")
self.assert_source(dom, "src/main")