diff options
-rw-r--r-- | coverage/annotate.py | 2 | ||||
-rw-r--r-- | coverage/control.py | 4 | ||||
-rw-r--r-- | coverage/html.py | 2 | ||||
-rw-r--r-- | coverage/jsonreport.py | 2 | ||||
-rw-r--r-- | coverage/lcovreport.py | 2 | ||||
-rw-r--r-- | coverage/report.py | 364 | ||||
-rw-r--r-- | coverage/report_core.py | 117 | ||||
-rw-r--r-- | coverage/summary.py | 281 | ||||
-rw-r--r-- | coverage/xmlreport.py | 2 | ||||
-rw-r--r-- | tests/test_html.py | 2 | ||||
-rw-r--r-- | tests/test_report.py | 1107 | ||||
-rw-r--r-- | tests/test_report_core.py | 68 | ||||
-rw-r--r-- | tests/test_summary.py | 1081 |
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')) |