summaryrefslogtreecommitdiff
path: root/tests/test_report.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_report.py')
-rw-r--r--tests/test_report.py1107
1 files changed, 1060 insertions, 47 deletions
diff --git a/tests/test_report.py b/tests/test_report.py
index c85c6b47..51a4fc68 100644
--- a/tests/test_report.py
+++ b/tests/test_report.py
@@ -1,68 +1,1081 @@
# 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 for helpers in report.py"""
+"""Test text-based summary reporting for coverage.py"""
from __future__ import annotations
-from typing import IO, Iterable, List, Optional, Type
+import glob
+import io
+import math
+import os
+import os.path
+import py_compile
+import re
+
+from typing import Tuple
import pytest
-from coverage.exceptions import CoverageException
-from coverage.report import render_report
-from coverage.types import TMorf
+import coverage
+from coverage import env
+from coverage.control import Coverage
+from coverage.data import CoverageData
+from coverage.exceptions import ConfigError, NoDataError, NotPython
+from coverage.files import abs_file
+from coverage.report import SummaryReporter
+from coverage.types import TConfigValueIn
+
+from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin
+from tests.helpers import assert_coverage_warnings
+
+
+class SummaryTest(UsingModulesMixin, CoverageTest):
+ """Tests of the text summary reporting for coverage.py."""
+
+ def make_mycode(self) -> None:
+ """Make the mycode.py file when needed."""
+ self.make_file("mycode.py", """\
+ import covmod1
+ import covmodzip1
+ a = 1
+ print('done')
+ """)
+
+ def test_report(self) -> None:
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ assert self.stdout() == 'done\n'
+ report = self.get_report(cov)
+
+ # Name Stmts Miss Cover
+ # ------------------------------------------------------------------
+ # c:/ned/coverage/tests/modules/covmod1.py 2 0 100%
+ # c:/ned/coverage/tests/zipmods.zip/covmodzip1.py 2 0 100%
+ # mycode.py 4 0 100%
+ # ------------------------------------------------------------------
+ # TOTAL 8 0 100%
+
+ assert "/coverage/__init__/" not in report
+ assert "/tests/modules/covmod1.py " in report
+ assert "/tests/zipmods.zip/covmodzip1.py " in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 8 0 100%"
+
+ def test_report_just_one(self) -> None:
+ # Try reporting just one module
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ report = self.get_report(cov, morfs=["mycode.py"])
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_wildcard(self) -> None:
+ # Try reporting using wildcards to get the modules.
+ self.make_mycode()
+ # Wildcard is handled by shell or cmdline.py, so use real commands
+ self.run_command("coverage run mycode.py")
+ report = self.report_from_command("coverage report my*.py")
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_omitting(self) -> None:
+ # Try reporting while omitting some modules
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ report = self.get_report(cov, omit=[f"{TESTS_DIR}/*", "*/site-packages/*"])
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_including(self) -> None:
+ # Try reporting while including some modules
+ self.make_mycode()
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "mycode")
+ report = self.get_report(cov, include=["mycode*"])
+
+ # Name Stmts Miss Cover
+ # -------------------------------
+ # mycode.py 4 0 100%
+ # -------------------------------
+ # TOTAL 4 0 100%
+
+ assert self.line_count(report) == 5
+ assert "/coverage/" not in report
+ assert "/tests/modules/covmod1.py " not in report
+ assert "/tests/zipmods.zip/covmodzip1.py " not in report
+ assert "mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 4 0 100%"
+
+ def test_report_include_relative_files_and_path(self) -> None:
+ """
+ Test that when relative_files is True and a relative path to a module
+ is included, coverage is reported for the module.
+
+ Ref: https://github.com/nedbat/coveragepy/issues/1604
+ """
+ self.make_mycode()
+ self.make_file(".coveragerc", """\
+ [run]
+ relative_files = true
+ """)
+ self.make_file("submodule/mycode.py", "import mycode")
+
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "submodule/mycode")
+ report = self.get_report(cov, include="submodule/mycode.py")
+
+ # Name Stmts Miss Cover
+ # ---------------------------------------
+ # submodule/mycode.py 1 0 100%
+ # ---------------------------------------
+ # TOTAL 1 0 100%
+
+ assert "submodule/mycode.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 1 0 100%"
+
+ def test_report_include_relative_files_and_wildcard_path(self) -> None:
+ self.make_mycode()
+ self.make_file(".coveragerc", """\
+ [run]
+ relative_files = true
+ """)
+ self.make_file("submodule/mycode.py", "import nested.submodule.mycode")
+ self.make_file("nested/submodule/mycode.py", "import mycode")
+
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "submodule/mycode")
+ report = self.get_report(cov, include="*/submodule/mycode.py")
+
+ # Name Stmts Miss Cover
+ # -------------------------------------------------
+ # nested/submodule/mycode.py 1 0 100%
+ # submodule/mycode.py 1 0 100%
+ # -------------------------------------------------
+ # TOTAL 2 0 100%
+
+ reported_files = [line.split()[0] for line in report.splitlines()[2:4]]
+ assert reported_files == [
+ "nested/submodule/mycode.py",
+ "submodule/mycode.py",
+ ]
+
+ def test_omit_files_here(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/1407
+ self.make_file("foo.py", "")
+ self.make_file("bar/bar.py", "")
+ self.make_file("tests/test_baz.py", """\
+ def test_foo():
+ assert True
+ test_foo()
+ """)
+ self.run_command("coverage run --source=. --omit='./*.py' -m tests.test_baz")
+ report = self.report_from_command("coverage report")
+
+ # Name Stmts Miss Cover
+ # ---------------------------------------
+ # tests/test_baz.py 3 0 100%
+ # ---------------------------------------
+ # TOTAL 3 0 100%
+
+ assert self.line_count(report) == 5
+ assert "foo" not in report
+ assert "bar" not in report
+ assert "tests/test_baz.py" in report
+ assert self.last_line_squeezed(report) == "TOTAL 3 0 100%"
+
+ def test_run_source_vs_report_include(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/621
+ self.make_file(".coveragerc", """\
+ [run]
+ source = .
+
+ [report]
+ include = mod/*,tests/*
+ """)
+ # It should be OK to use that configuration.
+ cov = coverage.Coverage()
+ with self.assert_warnings(cov, []):
+ cov.start()
+ cov.stop() # pragma: nested
-from tests.coveragetest import CoverageTest
+ def test_run_omit_vs_report_omit(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/622
+ # report:omit shouldn't clobber run:omit.
+ self.make_mycode()
+ self.make_file(".coveragerc", """\
+ [run]
+ omit = */covmodzip1.py
+ [report]
+ omit = */covmod1.py
+ """)
+ self.run_command("coverage run mycode.py")
-class FakeReporter:
- """A fake implementation of a one-file reporter."""
+ # Read the data written, to see that the right files have been omitted from running.
+ covdata = CoverageData()
+ covdata.read()
+ files = [os.path.basename(p) for p in covdata.measured_files()]
+ assert "covmod1.py" in files
+ assert "covmodzip1.py" not in files
- report_type = "fake report file"
+ def test_report_branches(self) -> None:
+ self.make_file("mybranch.py", """\
+ def branch(x):
+ if x:
+ print("x")
+ return x
+ branch(1)
+ """)
+ cov = coverage.Coverage(source=["."], branch=True)
+ self.start_import_stop(cov, "mybranch")
+ assert self.stdout() == 'x\n'
+ report = self.get_report(cov)
- def __init__(self, output: str = "", error: Optional[Type[Exception]] = None) -> None:
- self.output = output
- self.error = error
- self.morfs: Optional[Iterable[TMorf]] = None
+ # Name Stmts Miss Branch BrPart Cover
+ # -----------------------------------------------
+ # mybranch.py 5 0 2 1 86%
+ # -----------------------------------------------
+ # TOTAL 5 0 2 1 86%
+ assert self.line_count(report) == 5
+ assert "mybranch.py " in report
+ assert self.last_line_squeezed(report) == "TOTAL 5 0 2 1 86%"
- def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
- """Fake."""
- self.morfs = morfs
- outfile.write(self.output)
- if self.error:
- raise self.error("You asked for it!")
- return 17.25
+ def test_report_show_missing(self) -> None:
+ self.make_file("mymissing.py", """\
+ def missing(x, y):
+ if x:
+ print("x")
+ return x
+ if y:
+ print("y")
+ try:
+ print("z")
+ 1/0
+ print("Never!")
+ except ZeroDivisionError:
+ pass
+ return x
+ missing(0, 1)
+ """)
+ cov = coverage.Coverage(source=["."])
+ self.start_import_stop(cov, "mymissing")
+ assert self.stdout() == 'y\nz\n'
+ report = self.get_report(cov, show_missing=True)
+ # Name Stmts Miss Cover Missing
+ # --------------------------------------------
+ # mymissing.py 14 3 79% 3-4, 10
+ # --------------------------------------------
+ # TOTAL 14 3 79%
-class RenderReportTest(CoverageTest):
- """Tests of render_report."""
+ assert self.line_count(report) == 5
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "mymissing.py 14 3 79% 3-4, 10"
+ assert squeezed[4] == "TOTAL 14 3 79%"
- def test_stdout(self) -> None:
- fake = FakeReporter(output="Hello!\n")
- msgs: List[str] = []
- res = render_report("-", fake, [pytest, "coverage"], msgs.append)
- assert res == 17.25
- assert fake.morfs == [pytest, "coverage"]
- assert self.stdout() == "Hello!\n"
- assert not msgs
+ def test_report_show_missing_branches(self) -> None:
+ self.make_file("mybranch.py", """\
+ def branch(x, y):
+ if x:
+ print("x")
+ if y:
+ print("y")
+ branch(1, 1)
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "mybranch")
+ assert self.stdout() == 'x\ny\n'
- def test_file(self) -> None:
- fake = FakeReporter(output="Gréètings!\n")
- msgs: List[str] = []
- res = render_report("output.txt", fake, [], msgs.append)
- assert res == 17.25
+ def test_report_show_missing_branches_and_lines(self) -> None:
+ self.make_file("main.py", """\
+ import mybranch
+ """)
+ self.make_file("mybranch.py", """\
+ def branch(x, y, z):
+ if x:
+ print("x")
+ if y:
+ print("y")
+ if z:
+ if x and y:
+ print("z")
+ return x
+ branch(1, 1, 0)
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == 'x\ny\n'
+
+ def test_report_skip_covered_no_branches(self) -> None:
+ self.make_file("main.py", """
+ import not_covered
+
+ def normal():
+ print("z")
+ normal()
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered():
+ print("n")
+ """)
+ # --fail-under is handled by cmdline.py, use real commands.
+ out = self.run_command("coverage run main.py")
+ assert out == "z\n"
+ report = self.report_from_command("coverage report --skip-covered --fail-under=70")
+
+ # Name Stmts Miss Cover
+ # ------------------------------------
+ # not_covered.py 2 1 50%
+ # ------------------------------------
+ # TOTAL 6 1 83%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "not_covered.py 2 1 50%"
+ assert squeezed[4] == "TOTAL 6 1 83%"
+ assert squeezed[6] == "1 file skipped due to complete coverage."
+ assert self.last_command_status == 0
+
+ def test_report_skip_covered_branches(self) -> None:
+ self.make_file("main.py", """
+ import not_covered, covered
+
+ def normal(z):
+ if z:
+ print("z")
+ normal(True)
+ normal(False)
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ not_covered(True)
+ """)
+ self.make_file("covered.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "n\nz\n"
+ report = self.get_report(cov, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # --------------------------------------------------
+ # not_covered.py 4 0 2 1 83%
+ # --------------------------------------------------
+ # TOTAL 13 0 4 1 94%
+ #
+ # 2 files skipped due to complete coverage.
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "not_covered.py 4 0 2 1 83%"
+ assert squeezed[4] == "TOTAL 13 0 4 1 94%"
+ assert squeezed[6] == "2 files skipped due to complete coverage."
+
+ def test_report_skip_covered_branches_with_totals(self) -> None:
+ self.make_file("main.py", """
+ import not_covered
+ import also_not_run
+
+ def normal(z):
+ if z:
+ print("z")
+ normal(True)
+ normal(False)
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ not_covered(True)
+ """)
+ self.make_file("also_not_run.py", """
+ def does_not_appear_in_this_film(ni):
+ print("Ni!")
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "n\nz\n"
+ report = self.get_report(cov, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # --------------------------------------------------
+ # also_not_run.py 2 1 0 0 50%
+ # not_covered.py 4 0 2 1 83%
+ # --------------------------------------------------
+ # TOTAL 13 1 4 1 88%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 8, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "also_not_run.py 2 1 0 0 50%"
+ assert squeezed[3] == "not_covered.py 4 0 2 1 83%"
+ assert squeezed[5] == "TOTAL 13 1 4 1 88%"
+ assert squeezed[7] == "1 file skipped due to complete coverage."
+
+ def test_report_skip_covered_all_files_covered(self) -> None:
+ self.make_file("main.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(source=["."], branch=True)
+ self.start_import_stop(cov, "main")
assert self.stdout() == ""
- with open("output.txt", "rb") as f:
- assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!"
- assert msgs == ["Wrote fake report file to output.txt"]
-
- @pytest.mark.parametrize("error", [CoverageException, ZeroDivisionError])
- def test_exception(self, error: Type[Exception]) -> None:
- fake = FakeReporter(error=error)
- msgs: List[str] = []
- with pytest.raises(error, match="You asked for it!"):
- render_report("output.txt", fake, [], msgs.append)
+ report = self.get_report(cov, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # -----------------------------------------
+ # TOTAL 3 0 0 0 100%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 5, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[4] == "1 file skipped due to complete coverage."
+
+ report = self.get_report(cov, squeeze=False, skip_covered=True, output_format="markdown")
+
+ # | Name | Stmts | Miss | Branch | BrPart | Cover |
+ # |---------- | -------: | -------: | -------: | -------: | -------: |
+ # | **TOTAL** | **3** | **0** | **0** | **0** | **100%** |
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 5, report
+ assert report.split("\n")[0] == (
+ '| Name | Stmts | Miss | Branch | BrPart | Cover |'
+ )
+ assert report.split("\n")[1] == (
+ '|---------- | -------: | -------: | -------: | -------: | -------: |'
+ )
+ assert report.split("\n")[2] == (
+ '| **TOTAL** | **3** | **0** | **0** | **0** | **100%** |'
+ )
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[4] == "1 file skipped due to complete coverage."
+
+ total = self.get_report(cov, output_format="total", skip_covered=True)
+ assert total == "100\n"
+
+ def test_report_skip_covered_longfilename(self) -> None:
+ self.make_file("long_______________filename.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(source=["."], branch=True)
+ self.start_import_stop(cov, "long_______________filename")
+ assert self.stdout() == ""
+ report = self.get_report(cov, squeeze=False, skip_covered=True)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # -----------------------------------------
+ # TOTAL 3 0 0 0 100%
+ #
+ # 1 file skipped due to complete coverage.
+
+ assert self.line_count(report) == 5, report
+ lines = self.report_lines(report)
+ assert lines[0] == "Name Stmts Miss Branch BrPart Cover"
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[4] == "1 file skipped due to complete coverage."
+
+ def test_report_skip_covered_no_data(self) -> None:
+ cov = coverage.Coverage()
+ cov.load()
+ with pytest.raises(NoDataError, match="No data to report."):
+ self.get_report(cov, skip_covered=True)
+ self.assert_doesnt_exist(".coverage")
+
+ def test_report_skip_empty(self) -> None:
+ self.make_file("main.py", """
+ import submodule
+
+ def normal():
+ print("z")
+ normal()
+ """)
+ self.make_file("submodule/__init__.py", "")
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "z\n"
+ report = self.get_report(cov, skip_empty=True)
+
+ # Name Stmts Miss Cover
+ # ------------------------------------
+ # main.py 4 0 100%
+ # ------------------------------------
+ # TOTAL 4 0 100%
+ #
+ # 1 empty file skipped.
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "main.py 4 0 100%"
+ assert squeezed[4] == "TOTAL 4 0 100%"
+ assert squeezed[6] == "1 empty file skipped."
+
+ def test_report_skip_empty_no_data(self) -> None:
+ self.make_file("__init__.py", "")
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "__init__")
assert self.stdout() == ""
- self.assert_doesnt_exist("output.txt")
- assert not msgs
+ report = self.get_report(cov, skip_empty=True)
+
+ # Name Stmts Miss Cover
+ # ------------------------------------
+ # TOTAL 0 0 100%
+ #
+ # 1 empty file skipped.
+
+ assert self.line_count(report) == 5, report
+ assert report.split("\n")[2] == "TOTAL 0 0 100%"
+ assert report.split("\n")[4] == "1 empty file skipped."
+
+ def test_report_precision(self) -> None:
+ self.make_file(".coveragerc", """\
+ [report]
+ precision = 3
+ omit = */site-packages/*
+ """)
+ self.make_file("main.py", """
+ import not_covered, covered
+
+ def normal(z):
+ if z:
+ print("z")
+ normal(True)
+ normal(False)
+ """)
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ not_covered(True)
+ """)
+ self.make_file("covered.py", """
+ def foo():
+ pass
+ foo()
+ """)
+ cov = coverage.Coverage(branch=True)
+ self.start_import_stop(cov, "main")
+ assert self.stdout() == "n\nz\n"
+ report = self.get_report(cov, squeeze=False)
+
+ # Name Stmts Miss Branch BrPart Cover
+ # ------------------------------------------------------
+ # covered.py 3 0 0 0 100.000%
+ # main.py 6 0 2 0 100.000%
+ # not_covered.py 4 0 2 1 83.333%
+ # ------------------------------------------------------
+ # TOTAL 13 0 4 1 94.118%
+
+ assert self.line_count(report) == 7, report
+ squeezed = self.squeezed_lines(report)
+ assert squeezed[2] == "covered.py 3 0 0 0 100.000%"
+ assert squeezed[4] == "not_covered.py 4 0 2 1 83.333%"
+ assert squeezed[6] == "TOTAL 13 0 4 1 94.118%"
+
+ def test_report_precision_all_zero(self) -> None:
+ self.make_file("not_covered.py", """
+ def not_covered(n):
+ if n:
+ print("n")
+ """)
+ self.make_file("empty.py", "")
+ cov = coverage.Coverage(source=["."])
+ self.start_import_stop(cov, "empty")
+ report = self.get_report(cov, precision=6, squeeze=False)
+
+ # Name Stmts Miss Cover
+ # -----------------------------------------
+ # empty.py 0 0 100.000000%
+ # not_covered.py 3 3 0.000000%
+ # -----------------------------------------
+ # TOTAL 3 3 0.000000%
+
+ assert self.line_count(report) == 6, report
+ assert "empty.py 0 0 100.000000%" in report
+ assert "not_covered.py 3 3 0.000000%" in report
+ assert "TOTAL 3 3 0.000000%" in report
+
+ def test_dotpy_not_python(self) -> None:
+ # We run a .py file, and when reporting, we can't parse it as Python.
+ # We should get an error message in the report.
+
+ self.make_data_file(lines={"mycode.py": [1]})
+ self.make_file("mycode.py", "This isn't python at all!")
+ cov = coverage.Coverage()
+ cov.load()
+ msg = r"Couldn't parse '.*[/\\]mycode.py' as Python source: '.*' at line 1"
+ with pytest.raises(NotPython, match=msg):
+ self.get_report(cov, morfs=["mycode.py"])
+
+ def test_accented_directory(self) -> None:
+ # Make a file with a non-ascii character in the directory name.
+ self.make_file("\xe2/accented.py", "print('accented')")
+ self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]})
+ report_expected = (
+ "Name Stmts Miss Cover\n" +
+ "-----------------------------------\n" +
+ "\xe2/accented.py 1 0 100%\n" +
+ "-----------------------------------\n" +
+ "TOTAL 1 0 100%\n"
+ )
+ cov = coverage.Coverage()
+ cov.load()
+ output = self.get_report(cov, squeeze=False)
+ assert output == report_expected
+
+ def test_accenteddotpy_not_python(self) -> None:
+ # We run a .py file with a non-ascii name, and when reporting, we can't
+ # parse it as Python. We should get an error message in the report.
+
+ self.make_data_file(lines={"accented\xe2.py": [1]})
+ self.make_file("accented\xe2.py", "This isn't python at all!")
+ cov = coverage.Coverage()
+ cov.load()
+ msg = r"Couldn't parse '.*[/\\]accented\xe2.py' as Python source: '.*' at line 1"
+ with pytest.raises(NotPython, match=msg):
+ self.get_report(cov, morfs=["accented\xe2.py"])
+
+ def test_dotpy_not_python_ignored(self) -> None:
+ # We run a .py file, and when reporting, we can't parse it as Python,
+ # but we've said to ignore errors, so there's no error reported,
+ # though we still get a warning.
+ self.make_file("mycode.py", "This isn't python at all!")
+ self.make_data_file(lines={"mycode.py": [1]})
+ cov = coverage.Coverage()
+ cov.load()
+ with pytest.raises(NoDataError, match="No data to report."):
+ with pytest.warns(Warning) as warns:
+ self.get_report(cov, morfs=["mycode.py"], ignore_errors=True)
+ assert_coverage_warnings(
+ warns,
+ re.compile(r"Couldn't parse Python file '.*[/\\]mycode.py' \(couldnt-parse\)"),
+ )
+
+ def test_dothtml_not_python(self) -> None:
+ # We run a .html file, and when reporting, we can't parse it as
+ # Python. Since it wasn't .py, no error is reported.
+
+ # Pretend to run an html file.
+ self.make_file("mycode.html", "<h1>This isn't python at all!</h1>")
+ self.make_data_file(lines={"mycode.html": [1]})
+ cov = coverage.Coverage()
+ cov.load()
+ with pytest.raises(NoDataError, match="No data to report."):
+ self.get_report(cov, morfs=["mycode.html"])
+
+ def test_report_no_extension(self) -> None:
+ self.make_file("xxx", """\
+ # This is a python file though it doesn't look like it, like a main script.
+ a = b = c = d = 0
+ a = 3
+ b = 4
+ if not b:
+ c = 6
+ d = 7
+ print(f"xxx: {a} {b} {c} {d}")
+ """)
+ self.make_data_file(lines={abs_file("xxx"): [2, 3, 4, 5, 7, 8]})
+ cov = coverage.Coverage()
+ cov.load()
+ report = self.get_report(cov)
+ assert self.last_line_squeezed(report) == "TOTAL 7 1 86%"
+
+ def test_report_with_chdir(self) -> None:
+ self.make_file("chdir.py", """\
+ import os
+ print("Line One")
+ os.chdir("subdir")
+ print("Line Two")
+ print(open("something").read())
+ """)
+ self.make_file("subdir/something", "hello")
+ out = self.run_command("coverage run --source=. chdir.py")
+ assert out == "Line One\nLine Two\nhello\n"
+ report = self.report_from_command("coverage report")
+ assert self.last_line_squeezed(report) == "TOTAL 5 0 100%"
+ report = self.report_from_command("coverage report --format=markdown")
+ assert self.last_line_squeezed(report) == "| **TOTAL** | **5** | **0** | **100%** |"
+
+ def test_bug_156_file_not_run_should_be_zero(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/156
+ self.make_file("mybranch.py", """\
+ def branch(x):
+ if x:
+ print("x")
+ return x
+ branch(1)
+ """)
+ self.make_file("main.py", """\
+ print("y")
+ """)
+ cov = coverage.Coverage(branch=True, source=["."])
+ self.start_import_stop(cov, "main")
+ report = self.get_report(cov).splitlines()
+ assert "mybranch.py 5 5 2 0 0%" in report
+
+ def run_TheCode_and_report_it(self) -> str:
+ """A helper for the next few tests."""
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "TheCode")
+ return self.get_report(cov)
+
+ def test_bug_203_mixed_case_listed_twice_with_rc(self) -> None:
+ self.make_file("TheCode.py", "a = 1\n")
+ self.make_file(".coveragerc", "[run]\nsource = .\n")
+
+ report = self.run_TheCode_and_report_it()
+ assert "TheCode" in report
+ assert "thecode" not in report
+
+ def test_bug_203_mixed_case_listed_twice(self) -> None:
+ self.make_file("TheCode.py", "a = 1\n")
+
+ report = self.run_TheCode_and_report_it()
+
+ assert "TheCode" in report
+ assert "thecode" not in report
+
+ @pytest.mark.skipif(not env.WINDOWS, reason=".pyw files are only on Windows.")
+ def test_pyw_files(self) -> None:
+ # https://github.com/nedbat/coveragepy/issues/261
+ self.make_file("start.pyw", """\
+ import mod
+ print("In start.pyw")
+ """)
+ self.make_file("mod.pyw", """\
+ print("In mod.pyw")
+ """)
+ cov = coverage.Coverage()
+ # start_import_stop can't import the .pyw file, so use the long form.
+ cov.start()
+ import start # pragma: nested # pylint: disable=import-error, unused-import
+ cov.stop() # pragma: nested
+
+ report = self.get_report(cov)
+ assert "NoSource" not in report
+ report_lines = report.splitlines()
+ assert "start.pyw 2 0 100%" in report_lines
+ assert "mod.pyw 1 0 100%" in report_lines
+
+ def test_tracing_pyc_file(self) -> None:
+ # Create two Python files.
+ self.make_file("mod.py", "a = 1\n")
+ self.make_file("main.py", "import mod\n")
+
+ # Make one into a .pyc.
+ py_compile.compile("mod.py")
+
+ # Run the program.
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "main")
+
+ report_lines = self.get_report(cov).splitlines()
+ assert "mod.py 1 0 100%" in report_lines
+ report = self.get_report(cov, squeeze=False, output_format="markdown")
+ assert report.split("\n")[3] == "| mod.py | 1 | 0 | 100% |"
+ assert report.split("\n")[4] == "| **TOTAL** | **2** | **0** | **100%** |"
+
+ def test_missing_py_file_during_run(self) -> None:
+ # Create two Python files.
+ self.make_file("mod.py", "a = 1\n")
+ self.make_file("main.py", "import mod\n")
+
+ # Make one into a .pyc, and remove the .py.
+ py_compile.compile("mod.py")
+ os.remove("mod.py")
+
+ # Python 3 puts the .pyc files in a __pycache__ directory, and will
+ # not import from there without source. It will import a .pyc from
+ # the source location though.
+ pycs = glob.glob("__pycache__/mod.*.pyc")
+ assert len(pycs) == 1
+ os.rename(pycs[0], "mod.pyc")
+
+ # Run the program.
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "main")
+
+ # Put back the missing Python file.
+ self.make_file("mod.py", "a = 1\n")
+ report = self.get_report(cov).splitlines()
+ assert "mod.py 1 0 100%" in report
+
+ def test_empty_files(self) -> None:
+ # Shows that empty files like __init__.py are listed as having zero
+ # statements, not one statement.
+ cov = coverage.Coverage(branch=True)
+ cov.start()
+ import usepkgs # pragma: nested # pylint: disable=import-error, unused-import
+ cov.stop() # pragma: nested
+ report = self.get_report(cov)
+ assert "tests/modules/pkg1/__init__.py 1 0 0 0 100%" in report
+ assert "tests/modules/pkg2/__init__.py 0 0 0 0 100%" in report
+ report = self.get_report(cov, squeeze=False, output_format="markdown")
+ # get_report() escapes backslash so we expect forward slash escaped
+ # underscore
+ assert "tests/modules/pkg1//_/_init/_/_.py " in report
+ assert "| 1 | 0 | 0 | 0 | 100% |" in report
+ assert "tests/modules/pkg2//_/_init/_/_.py " in report
+ assert "| 0 | 0 | 0 | 0 | 100% |" in report
+
+ def test_markdown_with_missing(self) -> None:
+ self.make_file("mymissing.py", """\
+ def missing(x, y):
+ if x:
+ print("x")
+ return x
+ if y:
+ print("y")
+ try:
+ print("z")
+ 1/0
+ print("Never!")
+ except ZeroDivisionError:
+ pass
+ return x
+ missing(0, 1)
+ """)
+ cov = coverage.Coverage(source=["."])
+ self.start_import_stop(cov, "mymissing")
+ assert self.stdout() == 'y\nz\n'
+ report = self.get_report(cov, squeeze=False, output_format="markdown", show_missing=True)
+
+ # | Name | Stmts | Miss | Cover | Missing |
+ # |------------- | -------: | -------: | ------: | --------: |
+ # | mymissing.py | 14 | 3 | 79% | 3-4, 10 |
+ # | **TOTAL** | **14** | **3** | **79%** | |
+ assert self.line_count(report) == 4
+ report_lines = report.split("\n")
+ assert report_lines[2] == "| mymissing.py | 14 | 3 | 79% | 3-4, 10 |"
+ assert report_lines[3] == "| **TOTAL** | **14** | **3** | **79%** | |"
+
+ assert self.get_report(cov, output_format="total") == "79\n"
+ assert self.get_report(cov, output_format="total", precision=2) == "78.57\n"
+ assert self.get_report(cov, output_format="total", precision=4) == "78.5714\n"
+
+ def test_bug_1524(self) -> None:
+ self.make_file("bug1524.py", """\
+ class Mine:
+ @property
+ def thing(self) -> int:
+ return 17
+
+ print(Mine().thing)
+ """)
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "bug1524")
+ assert self.stdout() == "17\n"
+ report = self.get_report(cov)
+ report_lines = report.splitlines()
+ assert report_lines[2] == "bug1524.py 5 0 100%"
+
+
+class ReportingReturnValueTest(CoverageTest):
+ """Tests of reporting functions returning values."""
+
+ def run_coverage(self) -> Coverage:
+ """Run coverage on doit.py and return the coverage object."""
+ self.make_file("doit.py", """\
+ a = 1
+ b = 2
+ c = 3
+ d = 4
+ if a > 10:
+ f = 6
+ g = 7
+ """)
+
+ cov = coverage.Coverage()
+ self.start_import_stop(cov, "doit")
+ return cov
+
+ def test_report(self) -> None:
+ cov = self.run_coverage()
+ val = cov.report(include="*/doit.py")
+ assert math.isclose(val, 6 / 7 * 100)
+
+ def test_html(self) -> None:
+ cov = self.run_coverage()
+ val = cov.html_report(include="*/doit.py")
+ assert math.isclose(val, 6 / 7 * 100)
+
+ def test_xml(self) -> None:
+ cov = self.run_coverage()
+ val = cov.xml_report(include="*/doit.py")
+ assert math.isclose(val, 6 / 7 * 100)
+
+
+class SummaryReporterConfigurationTest(CoverageTest):
+ """Tests of SummaryReporter."""
+
+ def make_rigged_file(self, filename: str, stmts: int, miss: int) -> None:
+ """Create a file that will have specific results.
+
+ `stmts` and `miss` are ints, the number of statements, and
+ missed statements that should result.
+ """
+ run = stmts - miss - 1
+ dont_run = miss
+ source = ""
+ source += "a = 1\n" * run
+ source += "if a == 99:\n"
+ source += " a = 2\n" * dont_run
+ self.make_file(filename, source)
+
+ def get_summary_text(self, *options: Tuple[str, TConfigValueIn]) -> str:
+ """Get text output from the SummaryReporter.
+
+ The arguments are tuples: (name, value) for Coverage.set_option.
+ """
+ self.make_rigged_file("file1.py", 339, 155)
+ self.make_rigged_file("file2.py", 13, 3)
+ self.make_rigged_file("file10.py", 234, 228)
+ self.make_file("doit.py", "import file1, file2, file10")
+
+ cov = Coverage(source=["."], omit=["doit.py"])
+ self.start_import_stop(cov, "doit")
+ for name, value in options:
+ cov.set_option(name, value)
+ printer = SummaryReporter(cov)
+ destination = io.StringIO()
+ printer.report([], destination)
+ return destination.getvalue()
+
+ def test_test_data(self) -> None:
+ # We use our own test files as test data. Check that our assumptions
+ # about them are still valid. We want the three columns of numbers to
+ # sort in three different orders.
+ report = self.get_summary_text()
+ # Name Stmts Miss Cover
+ # ------------------------------
+ # file1.py 339 155 54%
+ # file2.py 13 3 77%
+ # file10.py 234 228 3%
+ # ------------------------------
+ # TOTAL 586 386 34%
+ lines = report.splitlines()[2:-2]
+ assert len(lines) == 3
+ nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines]
+ # [
+ # [339, 155, 54],
+ # [ 13, 3, 77],
+ # [234, 228, 3]
+ # ]
+ assert nums[1][0] < nums[2][0] < nums[0][0]
+ assert nums[1][1] < nums[0][1] < nums[2][1]
+ assert nums[2][2] < nums[0][2] < nums[1][2]
+
+ def test_defaults(self) -> None:
+ """Run the report with no configuration options."""
+ report = self.get_summary_text()
+ assert 'Missing' not in report
+ assert 'Branch' not in report
+
+ def test_print_missing(self) -> None:
+ """Run the report printing the missing lines."""
+ report = self.get_summary_text(('report:show_missing', True))
+ assert 'Missing' in report
+ assert 'Branch' not in report
+
+ def assert_ordering(self, text: str, *words: str) -> None:
+ """Assert that the `words` appear in order in `text`."""
+ indexes = list(map(text.find, words))
+ assert -1 not in indexes
+ msg = f"The words {words!r} don't appear in order in {text!r}"
+ assert indexes == sorted(indexes), msg
+
+ def test_default_sort_report(self) -> None:
+ # Sort the text report by the default (Name) column.
+ report = self.get_summary_text()
+ self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
+
+ def test_sort_report_by_name(self) -> None:
+ # Sort the text report explicitly by the Name column.
+ report = self.get_summary_text(('report:sort', 'Name'))
+ self.assert_ordering(report, "file1.py", "file2.py", "file10.py")
+
+ def test_sort_report_by_stmts(self) -> None:
+ # Sort the text report by the Stmts column.
+ report = self.get_summary_text(('report:sort', 'Stmts'))
+ self.assert_ordering(report, "file2.py", "file10.py", "file1.py")
+
+ def test_sort_report_by_missing(self) -> None:
+ # Sort the text report by the Missing column.
+ report = self.get_summary_text(('report:sort', 'Miss'))
+ self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
+
+ def test_sort_report_by_cover(self) -> None:
+ # Sort the text report by the Cover column.
+ report = self.get_summary_text(('report:sort', 'Cover'))
+ self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
+
+ def test_sort_report_by_cover_plus(self) -> None:
+ # Sort the text report by the Cover column, including the explicit + sign.
+ report = self.get_summary_text(('report:sort', '+Cover'))
+ self.assert_ordering(report, "file10.py", "file1.py", "file2.py")
+
+ def test_sort_report_by_cover_reversed(self) -> None:
+ # Sort the text report by the Cover column reversed.
+ report = self.get_summary_text(('report:sort', '-Cover'))
+ self.assert_ordering(report, "file2.py", "file1.py", "file10.py")
+
+ def test_sort_report_by_invalid_option(self) -> None:
+ # Sort the text report by a nonsense column.
+ msg = "Invalid sorting option: 'Xyzzy'"
+ with pytest.raises(ConfigError, match=msg):
+ self.get_summary_text(('report:sort', 'Xyzzy'))
+
+ def test_report_with_invalid_format(self) -> None:
+ # Ask for an invalid format.
+ msg = "Unknown report format choice: 'xyzzy'"
+ with pytest.raises(ConfigError, match=msg):
+ self.get_summary_text(('report:format', 'xyzzy'))