summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2023-05-13 05:46:46 -0400
committerNed Batchelder <ned@nedbatchelder.com>2023-05-13 05:46:46 -0400
commit007323a22836b5e30b5c76cfae650784c05e8584 (patch)
tree9b1f87e1fa3b7c2506cc35d020d547c37c491dca
parent8abc5a12c773bba92c37c22480a8cf3db4ddbbec (diff)
downloadpython-coveragepy-git-007323a22836b5e30b5c76cfae650784c05e8584.tar.gz
refactor: file names match the commands they implement better
-rw-r--r--coverage/annotate.py2
-rw-r--r--coverage/control.py4
-rw-r--r--coverage/html.py2
-rw-r--r--coverage/jsonreport.py2
-rw-r--r--coverage/lcovreport.py2
-rw-r--r--coverage/report.py364
-rw-r--r--coverage/report_core.py117
-rw-r--r--coverage/summary.py281
-rw-r--r--coverage/xmlreport.py2
-rw-r--r--tests/test_html.py2
-rw-r--r--tests/test_report.py1107
-rw-r--r--tests/test_report_core.py68
-rw-r--r--tests/test_summary.py1081
13 files changed, 1517 insertions, 1517 deletions
diff --git a/coverage/annotate.py b/coverage/annotate.py
index b4a02cb4..2ef89c96 100644
--- a/coverage/annotate.py
+++ b/coverage/annotate.py
@@ -13,7 +13,7 @@ from typing import Iterable, Optional, TYPE_CHECKING
from coverage.files import flat_rootname
from coverage.misc import ensure_dir, isolate_module
from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis
from coverage.types import TMorf
diff --git a/coverage/control.py b/coverage/control.py
index e405a5bf..2a84ce71 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -43,9 +43,9 @@ from coverage.multiproc import patch_multiprocessing
from coverage.plugin import FileReporter
from coverage.plugin_support import Plugins
from coverage.python import PythonFileReporter
-from coverage.report import render_report
+from coverage.report import SummaryReporter
+from coverage.report_core import render_report
from coverage.results import Analysis
-from coverage.summary import SummaryReporter
from coverage.types import (
FilePath, TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut,
TFileDisposition, TLineNo, TMorf,
diff --git a/coverage/html.py b/coverage/html.py
index f11d85e1..532eb66c 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -23,7 +23,7 @@ from coverage.exceptions import NoDataError
from coverage.files import flat_rootname
from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
from coverage.misc import human_sorted, plural, stdout_link
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis, Numbers
from coverage.templite import Templite
from coverage.types import TLineNo, TMorf
diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py
index 24e33585..9780e261 100644
--- a/coverage/jsonreport.py
+++ b/coverage/jsonreport.py
@@ -12,7 +12,7 @@ import sys
from typing import Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
from coverage import __version__
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis, Numbers
from coverage.types import TMorf, TLineNo
diff --git a/coverage/lcovreport.py b/coverage/lcovreport.py
index b9fe2568..a4ad9cd9 100644
--- a/coverage/lcovreport.py
+++ b/coverage/lcovreport.py
@@ -12,7 +12,7 @@ from hashlib import md5
from typing import IO, Iterable, Optional, TYPE_CHECKING
from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis, Numbers
from coverage.types import TMorf
diff --git a/coverage/report.py b/coverage/report.py
index 09eed0a8..e1c7a071 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -1,117 +1,281 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-"""Reporter foundation for coverage.py."""
+"""Summary reporting"""
from __future__ import annotations
import sys
-from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING
+from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
-from coverage.exceptions import NoDataError, NotPython
-from coverage.files import prep_patterns, GlobMatcher
-from coverage.misc import ensure_dir_for_file, file_be_gone
+from coverage.exceptions import ConfigError, NoDataError
+from coverage.misc import human_sorted_items
from coverage.plugin import FileReporter
-from coverage.results import Analysis
-from coverage.types import Protocol, TMorf
+from coverage.report_core import get_analysis_to_report
+from coverage.results import Analysis, Numbers
+from coverage.types import TMorf
if TYPE_CHECKING:
from coverage import Coverage
-class Reporter(Protocol):
- """What we expect of reporters."""
-
- report_type: str
-
- def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
- """Generate a report of `morfs`, written to `outfile`."""
-
-
-def render_report(
- output_path: str,
- reporter: Reporter,
- morfs: Optional[Iterable[TMorf]],
- msgfn: Callable[[str], None],
-) -> float:
- """Run a one-file report generator, managing the output file.
-
- This function ensures the output file is ready to be written to. Then writes
- the report to it. Then closes the file and cleans up.
-
- """
- file_to_close = None
- delete_file = False
-
- if output_path == "-":
- outfile = sys.stdout
- else:
- # Ensure that the output directory is created; done here because this
- # report pre-opens the output file. HtmlReporter does this on its own
- # because its task is more complex, being multiple files.
- ensure_dir_for_file(output_path)
- outfile = open(output_path, "w", encoding="utf-8")
- file_to_close = outfile
- delete_file = True
-
- try:
- ret = reporter.report(morfs, outfile=outfile)
- if file_to_close is not None:
- msgfn(f"Wrote {reporter.report_type} to {output_path}")
- delete_file = False
- return ret
- finally:
- if file_to_close is not None:
- file_to_close.close()
- if delete_file:
- file_be_gone(output_path) # pragma: part covered (doesn't return)
-
-
-def get_analysis_to_report(
- coverage: Coverage,
- morfs: Optional[Iterable[TMorf]],
-) -> Iterator[Tuple[FileReporter, Analysis]]:
- """Get the files to report on.
-
- For each morf in `morfs`, if it should be reported on (based on the omit
- and include configuration options), yield a pair, the `FileReporter` and
- `Analysis` for the morf.
-
- """
- file_reporters = coverage._get_file_reporters(morfs)
- config = coverage.config
-
- if config.report_include:
- matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
- file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
-
- if config.report_omit:
- matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
- file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
-
- if not file_reporters:
- raise NoDataError("No data to report.")
-
- for fr in sorted(file_reporters):
- try:
- analysis = coverage._analyze(fr)
- except NotPython:
- # Only report errors for .py files, and only if we didn't
- # explicitly suppress those errors.
- # NotPython is only raised by PythonFileReporter, which has a
- # should_be_python() method.
- if fr.should_be_python(): # type: ignore[attr-defined]
- if config.ignore_errors:
- msg = f"Couldn't parse Python file '{fr.filename}'"
- coverage._warn(msg, slug="couldnt-parse")
- else:
- raise
- except Exception as exc:
- if config.ignore_errors:
- msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
- coverage._warn(msg, slug="couldnt-parse")
+class SummaryReporter:
+ """A reporter for writing the summary report."""
+
+ def __init__(self, coverage: Coverage) -> None:
+ self.coverage = coverage
+ self.config = self.coverage.config
+ self.branches = coverage.get_data().has_arcs()
+ self.outfile: Optional[IO[str]] = None
+ self.output_format = self.config.format or "text"
+ if self.output_format not in {"text", "markdown", "total"}:
+ raise ConfigError(f"Unknown report format choice: {self.output_format!r}")
+ self.fr_analysis: List[Tuple[FileReporter, Analysis]] = []
+ self.skipped_count = 0
+ self.empty_count = 0
+ self.total = Numbers(precision=self.config.precision)
+
+ def write(self, line: str) -> None:
+ """Write a line to the output, adding a newline."""
+ assert self.outfile is not None
+ self.outfile.write(line.rstrip())
+ self.outfile.write("\n")
+
+ def write_items(self, items: Iterable[str]) -> None:
+ """Write a list of strings, joined together."""
+ self.write("".join(items))
+
+ def _report_text(
+ self,
+ header: List[str],
+ lines_values: List[List[Any]],
+ total_line: List[Any],
+ end_lines: List[str],
+ ) -> None:
+ """Internal method that prints report data in text format.
+
+ `header` is a list with captions.
+ `lines_values` is list of lists of sortable values.
+ `total_line` is a list with values of the total line.
+ `end_lines` is a list of ending lines with information about skipped files.
+
+ """
+ # Prepare the formatting strings, header, and column sorting.
+ max_name = max([len(line[0]) for line in lines_values] + [5]) + 1
+ max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1
+ max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values])
+ formats = dict(
+ Name="{:{name_len}}",
+ Stmts="{:>7}",
+ Miss="{:>7}",
+ Branch="{:>7}",
+ BrPart="{:>7}",
+ Cover="{:>{n}}",
+ Missing="{:>10}",
+ )
+ header_items = [
+ formats[item].format(item, name_len=max_name, n=max_n)
+ for item in header
+ ]
+ header_str = "".join(header_items)
+ rule = "-" * len(header_str)
+
+ # Write the header
+ self.write(header_str)
+ self.write(rule)
+
+ formats.update(dict(Cover="{:>{n}}%"), Missing=" {:9}")
+ for values in lines_values:
+ # build string with line values
+ line_items = [
+ formats[item].format(str(value),
+ name_len=max_name, n=max_n-1) for item, value in zip(header, values)
+ ]
+ self.write_items(line_items)
+
+ # Write a TOTAL line
+ if lines_values:
+ self.write(rule)
+
+ line_items = [
+ formats[item].format(str(value),
+ name_len=max_name, n=max_n-1) for item, value in zip(header, total_line)
+ ]
+ self.write_items(line_items)
+
+ for end_line in end_lines:
+ self.write(end_line)
+
+ def _report_markdown(
+ self,
+ header: List[str],
+ lines_values: List[List[Any]],
+ total_line: List[Any],
+ end_lines: List[str],
+ ) -> None:
+ """Internal method that prints report data in markdown format.
+
+ `header` is a list with captions.
+ `lines_values` is a sorted list of lists containing coverage information.
+ `total_line` is a list with values of the total line.
+ `end_lines` is a list of ending lines with information about skipped files.
+
+ """
+ # Prepare the formatting strings, header, and column sorting.
+ max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0)
+ max_name = max(max_name, len("**TOTAL**")) + 1
+ formats = dict(
+ Name="| {:{name_len}}|",
+ Stmts="{:>9} |",
+ Miss="{:>9} |",
+ Branch="{:>9} |",
+ BrPart="{:>9} |",
+ Cover="{:>{n}} |",
+ Missing="{:>10} |",
+ )
+ max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover "))
+ header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header]
+ header_str = "".join(header_items)
+ rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, "-")] +
+ ["-: |".rjust(len(item)-1, "-") for item in header_items[1:]]
+ )
+
+ # Write the header
+ self.write(header_str)
+ self.write(rule_str)
+
+ for values in lines_values:
+ # build string with line values
+ formats.update(dict(Cover="{:>{n}}% |"))
+ line_items = [
+ formats[item].format(str(value).replace("_", "\\_"), name_len=max_name, n=max_n-1)
+ for item, value in zip(header, values)
+ ]
+ self.write_items(line_items)
+
+ # Write the TOTAL line
+ formats.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |"))
+ total_line_items: List[str] = []
+ for item, value in zip(header, total_line):
+ if value == "":
+ insert = value
+ elif item == "Cover":
+ insert = f" **{value}%**"
else:
- raise
+ insert = f" **{value}**"
+ total_line_items += formats[item].format(insert, name_len=max_name, n=max_n)
+ self.write_items(total_line_items)
+ for end_line in end_lines:
+ self.write(end_line)
+
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float:
+ """Writes a report summarizing coverage statistics per module.
+
+ `outfile` is a text-mode file object to write the summary to.
+
+ """
+ self.outfile = outfile or sys.stdout
+
+ self.coverage.get_data().set_query_contexts(self.config.report_contexts)
+ for fr, analysis in get_analysis_to_report(self.coverage, morfs):
+ self.report_one_file(fr, analysis)
+
+ if not self.total.n_files and not self.skipped_count:
+ raise NoDataError("No data to report.")
+
+ if self.output_format == "total":
+ self.write(self.total.pc_covered_str)
+ else:
+ self.tabular_report()
+
+ return self.total.pc_covered
+
+ def tabular_report(self) -> None:
+ """Writes tabular report formats."""
+ # Prepare the header line and column sorting.
+ header = ["Name", "Stmts", "Miss"]
+ if self.branches:
+ header += ["Branch", "BrPart"]
+ header += ["Cover"]
+ if self.config.show_missing:
+ header += ["Missing"]
+
+ column_order = dict(name=0, stmts=1, miss=2, cover=-1)
+ if self.branches:
+ column_order.update(dict(branch=3, brpart=4))
+
+ # `lines_values` is list of lists of sortable values.
+ lines_values = []
+
+ for (fr, analysis) in self.fr_analysis:
+ nums = analysis.numbers
+
+ args = [fr.relative_filename(), nums.n_statements, nums.n_missing]
+ if self.branches:
+ args += [nums.n_branches, nums.n_partial_branches]
+ args += [nums.pc_covered_str]
+ if self.config.show_missing:
+ args += [analysis.missing_formatted(branches=True)]
+ args += [nums.pc_covered]
+ lines_values.append(args)
+
+ # Line sorting.
+ sort_option = (self.config.sort or "name").lower()
+ reverse = False
+ if sort_option[0] == "-":
+ reverse = True
+ sort_option = sort_option[1:]
+ elif sort_option[0] == "+":
+ sort_option = sort_option[1:]
+ sort_idx = column_order.get(sort_option)
+ if sort_idx is None:
+ raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
+ if sort_option == "name":
+ lines_values = human_sorted_items(lines_values, reverse=reverse)
+ else:
+ lines_values.sort(
+ key=lambda line: (line[sort_idx], line[0]), # type: ignore[index]
+ reverse=reverse,
+ )
+
+ # Calculate total if we had at least one file.
+ total_line = ["TOTAL", self.total.n_statements, self.total.n_missing]
+ if self.branches:
+ total_line += [self.total.n_branches, self.total.n_partial_branches]
+ total_line += [self.total.pc_covered_str]
+ if self.config.show_missing:
+ total_line += [""]
+
+ # Create other final lines.
+ end_lines = []
+ if self.config.skip_covered and self.skipped_count:
+ file_suffix = "s" if self.skipped_count>1 else ""
+ end_lines.append(
+ f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage."
+ )
+ if self.config.skip_empty and self.empty_count:
+ file_suffix = "s" if self.empty_count > 1 else ""
+ end_lines.append(f"\n{self.empty_count} empty file{file_suffix} skipped.")
+
+ if self.output_format == "markdown":
+ formatter = self._report_markdown
+ else:
+ formatter = self._report_text
+ formatter(header, lines_values, total_line, end_lines)
+
+ def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None:
+ """Report on just one file, the callback from report()."""
+ nums = analysis.numbers
+ self.total += nums
+
+ no_missing_lines = (nums.n_missing == 0)
+ no_missing_branches = (nums.n_partial_branches == 0)
+ if self.config.skip_covered and no_missing_lines and no_missing_branches:
+ # Don't report on 100% files.
+ self.skipped_count += 1
+ elif self.config.skip_empty and nums.n_statements == 0:
+ # Don't report on empty files.
+ self.empty_count += 1
else:
- yield (fr, analysis)
+ self.fr_analysis.append((fr, analysis))
diff --git a/coverage/report_core.py b/coverage/report_core.py
new file mode 100644
index 00000000..09eed0a8
--- /dev/null
+++ b/coverage/report_core.py
@@ -0,0 +1,117 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Reporter foundation for coverage.py."""
+
+from __future__ import annotations
+
+import sys
+
+from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING
+
+from coverage.exceptions import NoDataError, NotPython
+from coverage.files import prep_patterns, GlobMatcher
+from coverage.misc import ensure_dir_for_file, file_be_gone
+from coverage.plugin import FileReporter
+from coverage.results import Analysis
+from coverage.types import Protocol, TMorf
+
+if TYPE_CHECKING:
+ from coverage import Coverage
+
+
+class Reporter(Protocol):
+ """What we expect of reporters."""
+
+ report_type: str
+
+ def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
+ """Generate a report of `morfs`, written to `outfile`."""
+
+
+def render_report(
+ output_path: str,
+ reporter: Reporter,
+ morfs: Optional[Iterable[TMorf]],
+ msgfn: Callable[[str], None],
+) -> float:
+ """Run a one-file report generator, managing the output file.
+
+ This function ensures the output file is ready to be written to. Then writes
+ the report to it. Then closes the file and cleans up.
+
+ """
+ file_to_close = None
+ delete_file = False
+
+ if output_path == "-":
+ outfile = sys.stdout
+ else:
+ # Ensure that the output directory is created; done here because this
+ # report pre-opens the output file. HtmlReporter does this on its own
+ # because its task is more complex, being multiple files.
+ ensure_dir_for_file(output_path)
+ outfile = open(output_path, "w", encoding="utf-8")
+ file_to_close = outfile
+ delete_file = True
+
+ try:
+ ret = reporter.report(morfs, outfile=outfile)
+ if file_to_close is not None:
+ msgfn(f"Wrote {reporter.report_type} to {output_path}")
+ delete_file = False
+ return ret
+ finally:
+ if file_to_close is not None:
+ file_to_close.close()
+ if delete_file:
+ file_be_gone(output_path) # pragma: part covered (doesn't return)
+
+
+def get_analysis_to_report(
+ coverage: Coverage,
+ morfs: Optional[Iterable[TMorf]],
+) -> Iterator[Tuple[FileReporter, Analysis]]:
+ """Get the files to report on.
+
+ For each morf in `morfs`, if it should be reported on (based on the omit
+ and include configuration options), yield a pair, the `FileReporter` and
+ `Analysis` for the morf.
+
+ """
+ file_reporters = coverage._get_file_reporters(morfs)
+ config = coverage.config
+
+ if config.report_include:
+ matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
+ file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
+
+ if config.report_omit:
+ matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
+ file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
+
+ if not file_reporters:
+ raise NoDataError("No data to report.")
+
+ for fr in sorted(file_reporters):
+ try:
+ analysis = coverage._analyze(fr)
+ except NotPython:
+ # Only report errors for .py files, and only if we didn't
+ # explicitly suppress those errors.
+ # NotPython is only raised by PythonFileReporter, which has a
+ # should_be_python() method.
+ if fr.should_be_python(): # type: ignore[attr-defined]
+ if config.ignore_errors:
+ msg = f"Couldn't parse Python file '{fr.filename}'"
+ coverage._warn(msg, slug="couldnt-parse")
+ else:
+ raise
+ except Exception as exc:
+ if config.ignore_errors:
+ msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
+ coverage._warn(msg, slug="couldnt-parse")
+ else:
+ raise
+ else:
+ yield (fr, analysis)
diff --git a/coverage/summary.py b/coverage/summary.py
deleted file mode 100644
index 5d373ec5..00000000
--- a/coverage/summary.py
+++ /dev/null
@@ -1,281 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
-"""Summary reporting"""
-
-from __future__ import annotations
-
-import sys
-
-from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
-
-from coverage.exceptions import ConfigError, NoDataError
-from coverage.misc import human_sorted_items
-from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
-from coverage.results import Analysis, Numbers
-from coverage.types import TMorf
-
-if TYPE_CHECKING:
- from coverage import Coverage
-
-
-class SummaryReporter:
- """A reporter for writing the summary report."""
-
- def __init__(self, coverage: Coverage) -> None:
- self.coverage = coverage
- self.config = self.coverage.config
- self.branches = coverage.get_data().has_arcs()
- self.outfile: Optional[IO[str]] = None
- self.output_format = self.config.format or "text"
- if self.output_format not in {"text", "markdown", "total"}:
- raise ConfigError(f"Unknown report format choice: {self.output_format!r}")
- self.fr_analysis: List[Tuple[FileReporter, Analysis]] = []
- self.skipped_count = 0
- self.empty_count = 0
- self.total = Numbers(precision=self.config.precision)
-
- def write(self, line: str) -> None:
- """Write a line to the output, adding a newline."""
- assert self.outfile is not None
- self.outfile.write(line.rstrip())
- self.outfile.write("\n")
-
- def write_items(self, items: Iterable[str]) -> None:
- """Write a list of strings, joined together."""
- self.write("".join(items))
-
- def _report_text(
- self,
- header: List[str],
- lines_values: List[List[Any]],
- total_line: List[Any],
- end_lines: List[str],
- ) -> None:
- """Internal method that prints report data in text format.
-
- `header` is a list with captions.
- `lines_values` is list of lists of sortable values.
- `total_line` is a list with values of the total line.
- `end_lines` is a list of ending lines with information about skipped files.
-
- """
- # Prepare the formatting strings, header, and column sorting.
- max_name = max([len(line[0]) for line in lines_values] + [5]) + 1
- max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1
- max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values])
- formats = dict(
- Name="{:{name_len}}",
- Stmts="{:>7}",
- Miss="{:>7}",
- Branch="{:>7}",
- BrPart="{:>7}",
- Cover="{:>{n}}",
- Missing="{:>10}",
- )
- header_items = [
- formats[item].format(item, name_len=max_name, n=max_n)
- for item in header
- ]
- header_str = "".join(header_items)
- rule = "-" * len(header_str)
-
- # Write the header
- self.write(header_str)
- self.write(rule)
-
- formats.update(dict(Cover="{:>{n}}%"), Missing=" {:9}")
- for values in lines_values:
- # build string with line values
- line_items = [
- formats[item].format(str(value),
- name_len=max_name, n=max_n-1) for item, value in zip(header, values)
- ]
- self.write_items(line_items)
-
- # Write a TOTAL line
- if lines_values:
- self.write(rule)
-
- line_items = [
- formats[item].format(str(value),
- name_len=max_name, n=max_n-1) for item, value in zip(header, total_line)
- ]
- self.write_items(line_items)
-
- for end_line in end_lines:
- self.write(end_line)
-
- def _report_markdown(
- self,
- header: List[str],
- lines_values: List[List[Any]],
- total_line: List[Any],
- end_lines: List[str],
- ) -> None:
- """Internal method that prints report data in markdown format.
-
- `header` is a list with captions.
- `lines_values` is a sorted list of lists containing coverage information.
- `total_line` is a list with values of the total line.
- `end_lines` is a list of ending lines with information about skipped files.
-
- """
- # Prepare the formatting strings, header, and column sorting.
- max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0)
- max_name = max(max_name, len("**TOTAL**")) + 1
- formats = dict(
- Name="| {:{name_len}}|",
- Stmts="{:>9} |",
- Miss="{:>9} |",
- Branch="{:>9} |",
- BrPart="{:>9} |",
- Cover="{:>{n}} |",
- Missing="{:>10} |",
- )
- max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover "))
- header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header]
- header_str = "".join(header_items)
- rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, "-")] +
- ["-: |".rjust(len(item)-1, "-") for item in header_items[1:]]
- )
-
- # Write the header
- self.write(header_str)
- self.write(rule_str)
-
- for values in lines_values:
- # build string with line values
- formats.update(dict(Cover="{:>{n}}% |"))
- line_items = [
- formats[item].format(str(value).replace("_", "\\_"), name_len=max_name, n=max_n-1)
- for item, value in zip(header, values)
- ]
- self.write_items(line_items)
-
- # Write the TOTAL line
- formats.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |"))
- total_line_items: List[str] = []
- for item, value in zip(header, total_line):
- if value == "":
- insert = value
- elif item == "Cover":
- insert = f" **{value}%**"
- else:
- insert = f" **{value}**"
- total_line_items += formats[item].format(insert, name_len=max_name, n=max_n)
- self.write_items(total_line_items)
- for end_line in end_lines:
- self.write(end_line)
-
- def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float:
- """Writes a report summarizing coverage statistics per module.
-
- `outfile` is a text-mode file object to write the summary to.
-
- """
- self.outfile = outfile or sys.stdout
-
- self.coverage.get_data().set_query_contexts(self.config.report_contexts)
- for fr, analysis in get_analysis_to_report(self.coverage, morfs):
- self.report_one_file(fr, analysis)
-
- if not self.total.n_files and not self.skipped_count:
- raise NoDataError("No data to report.")
-
- if self.output_format == "total":
- self.write(self.total.pc_covered_str)
- else:
- self.tabular_report()
-
- return self.total.pc_covered
-
- def tabular_report(self) -> None:
- """Writes tabular report formats."""
- # Prepare the header line and column sorting.
- header = ["Name", "Stmts", "Miss"]
- if self.branches:
- header += ["Branch", "BrPart"]
- header += ["Cover"]
- if self.config.show_missing:
- header += ["Missing"]
-
- column_order = dict(name=0, stmts=1, miss=2, cover=-1)
- if self.branches:
- column_order.update(dict(branch=3, brpart=4))
-
- # `lines_values` is list of lists of sortable values.
- lines_values = []
-
- for (fr, analysis) in self.fr_analysis:
- nums = analysis.numbers
-
- args = [fr.relative_filename(), nums.n_statements, nums.n_missing]
- if self.branches:
- args += [nums.n_branches, nums.n_partial_branches]
- args += [nums.pc_covered_str]
- if self.config.show_missing:
- args += [analysis.missing_formatted(branches=True)]
- args += [nums.pc_covered]
- lines_values.append(args)
-
- # Line sorting.
- sort_option = (self.config.sort or "name").lower()
- reverse = False
- if sort_option[0] == "-":
- reverse = True
- sort_option = sort_option[1:]
- elif sort_option[0] == "+":
- sort_option = sort_option[1:]
- sort_idx = column_order.get(sort_option)
- if sort_idx is None:
- raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
- if sort_option == "name":
- lines_values = human_sorted_items(lines_values, reverse=reverse)
- else:
- lines_values.sort(
- key=lambda line: (line[sort_idx], line[0]), # type: ignore[index]
- reverse=reverse,
- )
-
- # Calculate total if we had at least one file.
- total_line = ["TOTAL", self.total.n_statements, self.total.n_missing]
- if self.branches:
- total_line += [self.total.n_branches, self.total.n_partial_branches]
- total_line += [self.total.pc_covered_str]
- if self.config.show_missing:
- total_line += [""]
-
- # Create other final lines.
- end_lines = []
- if self.config.skip_covered and self.skipped_count:
- file_suffix = "s" if self.skipped_count>1 else ""
- end_lines.append(
- f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage."
- )
- if self.config.skip_empty and self.empty_count:
- file_suffix = "s" if self.empty_count > 1 else ""
- end_lines.append(f"\n{self.empty_count} empty file{file_suffix} skipped.")
-
- if self.output_format == "markdown":
- formatter = self._report_markdown
- else:
- formatter = self._report_text
- formatter(header, lines_values, total_line, end_lines)
-
- def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None:
- """Report on just one file, the callback from report()."""
- nums = analysis.numbers
- self.total += nums
-
- no_missing_lines = (nums.n_missing == 0)
- no_missing_branches = (nums.n_partial_branches == 0)
- if self.config.skip_covered and no_missing_lines and no_missing_branches:
- # Don't report on 100% files.
- self.skipped_count += 1
- elif self.config.skip_empty and nums.n_statements == 0:
- # Don't report on empty files.
- self.empty_count += 1
- else:
- self.fr_analysis.append((fr, analysis))
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index 82e60fc1..cac4c977 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -17,7 +17,7 @@ from typing import Any, Dict, IO, Iterable, Optional, TYPE_CHECKING, cast
from coverage import __version__, files
from coverage.misc import isolate_module, human_sorted, human_sorted_items
from coverage.plugin import FileReporter
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis
from coverage.types import TMorf
from coverage.version import __url__
diff --git a/tests/test_html.py b/tests/test_html.py
index 65f0cc76..476e75e8 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -24,7 +24,7 @@ from coverage import env, Coverage
from coverage.exceptions import NoDataError, NotPython, NoSource
from coverage.files import abs_file, flat_rootname
import coverage.html
-from coverage.report import get_analysis_to_report
+from coverage.report_core import get_analysis_to_report
from coverage.types import TLineNo, TMorf
from tests.coveragetest import CoverageTest, TESTS_DIR
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'))
diff --git a/tests/test_report_core.py b/tests/test_report_core.py
new file mode 100644
index 00000000..77e234b6
--- /dev/null
+++ b/tests/test_report_core.py
@@ -0,0 +1,68 @@
+# 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"""
+
+from __future__ import annotations
+
+from typing import IO, Iterable, List, Optional, Type
+
+import pytest
+
+from coverage.exceptions import CoverageException
+from coverage.report_core import render_report
+from coverage.types import TMorf
+
+from tests.coveragetest import CoverageTest
+
+
+class FakeReporter:
+ """A fake implementation of a one-file reporter."""
+
+ report_type = "fake report file"
+
+ def __init__(self, output: str = "", error: Optional[Type[Exception]] = None) -> None:
+ self.output = output
+ self.error = error
+ self.morfs: Optional[Iterable[TMorf]] = None
+
+ 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
+
+
+class RenderReportTest(CoverageTest):
+ """Tests of render_report."""
+
+ 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_file(self) -> None:
+ fake = FakeReporter(output="Gréètings!\n")
+ msgs: List[str] = []
+ res = render_report("output.txt", fake, [], msgs.append)
+ assert res == 17.25
+ 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)
+ assert self.stdout() == ""
+ self.assert_doesnt_exist("output.txt")
+ assert not msgs
diff --git a/tests/test_summary.py b/tests/test_summary.py
deleted file mode 100644
index f2158c93..00000000
--- a/tests/test_summary.py
+++ /dev/null
@@ -1,1081 +0,0 @@
-# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
-# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
-
-"""Test text-based summary reporting for coverage.py"""
-
-from __future__ import annotations
-
-import glob
-import io
-import math
-import os
-import os.path
-import py_compile
-import re
-
-from typing import Tuple
-
-import pytest
-
-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.summary 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
-
- 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")
-
- # 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
-
- 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)
-
- # 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 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%
-
- 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_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_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() == ""
- 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() == ""
- 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'))