Skip to content

Commit 007323a

Browse files
committedMay 13, 2023
refactor: file names match the commands they implement better
1 parent 8abc5a1 commit 007323a

13 files changed

+1517
-1517
lines changed
 

‎coverage/annotate.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from coverage.files import flat_rootname
1414
from coverage.misc import ensure_dir, isolate_module
1515
from coverage.plugin import FileReporter
16-
from coverage.report import get_analysis_to_report
16+
from coverage.report_core import get_analysis_to_report
1717
from coverage.results import Analysis
1818
from coverage.types import TMorf
1919

‎coverage/control.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@
4343
from coverage.plugin import FileReporter
4444
from coverage.plugin_support import Plugins
4545
from coverage.python import PythonFileReporter
46-
from coverage.report import render_report
46+
from coverage.report import SummaryReporter
47+
from coverage.report_core import render_report
4748
from coverage.results import Analysis
48-
from coverage.summary import SummaryReporter
4949
from coverage.types import (
5050
FilePath, TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut,
5151
TFileDisposition, TLineNo, TMorf,

‎coverage/html.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from coverage.files import flat_rootname
2424
from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime
2525
from coverage.misc import human_sorted, plural, stdout_link
26-
from coverage.report import get_analysis_to_report
26+
from coverage.report_core import get_analysis_to_report
2727
from coverage.results import Analysis, Numbers
2828
from coverage.templite import Templite
2929
from coverage.types import TLineNo, TMorf

‎coverage/jsonreport.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
1313

1414
from coverage import __version__
15-
from coverage.report import get_analysis_to_report
15+
from coverage.report_core import get_analysis_to_report
1616
from coverage.results import Analysis, Numbers
1717
from coverage.types import TMorf, TLineNo
1818

‎coverage/lcovreport.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import IO, Iterable, Optional, TYPE_CHECKING
1313

1414
from coverage.plugin import FileReporter
15-
from coverage.report import get_analysis_to_report
15+
from coverage.report_core import get_analysis_to_report
1616
from coverage.results import Analysis, Numbers
1717
from coverage.types import TMorf
1818

‎coverage/report.py

+264-100
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,281 @@
11
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
22
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
33

4-
"""Reporter foundation for coverage.py."""
4+
"""Summary reporting"""
55

66
from __future__ import annotations
77

88
import sys
99

10-
from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING
10+
from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING
1111

12-
from coverage.exceptions import NoDataError, NotPython
13-
from coverage.files import prep_patterns, GlobMatcher
14-
from coverage.misc import ensure_dir_for_file, file_be_gone
12+
from coverage.exceptions import ConfigError, NoDataError
13+
from coverage.misc import human_sorted_items
1514
from coverage.plugin import FileReporter
16-
from coverage.results import Analysis
17-
from coverage.types import Protocol, TMorf
15+
from coverage.report_core import get_analysis_to_report
16+
from coverage.results import Analysis, Numbers
17+
from coverage.types import TMorf
1818

1919
if TYPE_CHECKING:
2020
from coverage import Coverage
2121

2222

23-
class Reporter(Protocol):
24-
"""What we expect of reporters."""
25-
26-
report_type: str
27-
28-
def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
29-
"""Generate a report of `morfs`, written to `outfile`."""
30-
31-
32-
def render_report(
33-
output_path: str,
34-
reporter: Reporter,
35-
morfs: Optional[Iterable[TMorf]],
36-
msgfn: Callable[[str], None],
37-
) -> float:
38-
"""Run a one-file report generator, managing the output file.
39-
40-
This function ensures the output file is ready to be written to. Then writes
41-
the report to it. Then closes the file and cleans up.
42-
43-
"""
44-
file_to_close = None
45-
delete_file = False
46-
47-
if output_path == "-":
48-
outfile = sys.stdout
49-
else:
50-
# Ensure that the output directory is created; done here because this
51-
# report pre-opens the output file. HtmlReporter does this on its own
52-
# because its task is more complex, being multiple files.
53-
ensure_dir_for_file(output_path)
54-
outfile = open(output_path, "w", encoding="utf-8")
55-
file_to_close = outfile
56-
delete_file = True
57-
58-
try:
59-
ret = reporter.report(morfs, outfile=outfile)
60-
if file_to_close is not None:
61-
msgfn(f"Wrote {reporter.report_type} to {output_path}")
62-
delete_file = False
63-
return ret
64-
finally:
65-
if file_to_close is not None:
66-
file_to_close.close()
67-
if delete_file:
68-
file_be_gone(output_path) # pragma: part covered (doesn't return)
69-
70-
71-
def get_analysis_to_report(
72-
coverage: Coverage,
73-
morfs: Optional[Iterable[TMorf]],
74-
) -> Iterator[Tuple[FileReporter, Analysis]]:
75-
"""Get the files to report on.
76-
77-
For each morf in `morfs`, if it should be reported on (based on the omit
78-
and include configuration options), yield a pair, the `FileReporter` and
79-
`Analysis` for the morf.
80-
81-
"""
82-
file_reporters = coverage._get_file_reporters(morfs)
83-
config = coverage.config
84-
85-
if config.report_include:
86-
matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
87-
file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
88-
89-
if config.report_omit:
90-
matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
91-
file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
92-
93-
if not file_reporters:
94-
raise NoDataError("No data to report.")
95-
96-
for fr in sorted(file_reporters):
97-
try:
98-
analysis = coverage._analyze(fr)
99-
except NotPython:
100-
# Only report errors for .py files, and only if we didn't
101-
# explicitly suppress those errors.
102-
# NotPython is only raised by PythonFileReporter, which has a
103-
# should_be_python() method.
104-
if fr.should_be_python(): # type: ignore[attr-defined]
105-
if config.ignore_errors:
106-
msg = f"Couldn't parse Python file '{fr.filename}'"
107-
coverage._warn(msg, slug="couldnt-parse")
108-
else:
109-
raise
110-
except Exception as exc:
111-
if config.ignore_errors:
112-
msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
113-
coverage._warn(msg, slug="couldnt-parse")
23+
class SummaryReporter:
24+
"""A reporter for writing the summary report."""
25+
26+
def __init__(self, coverage: Coverage) -> None:
27+
self.coverage = coverage
28+
self.config = self.coverage.config
29+
self.branches = coverage.get_data().has_arcs()
30+
self.outfile: Optional[IO[str]] = None
31+
self.output_format = self.config.format or "text"
32+
if self.output_format not in {"text", "markdown", "total"}:
33+
raise ConfigError(f"Unknown report format choice: {self.output_format!r}")
34+
self.fr_analysis: List[Tuple[FileReporter, Analysis]] = []
35+
self.skipped_count = 0
36+
self.empty_count = 0
37+
self.total = Numbers(precision=self.config.precision)
38+
39+
def write(self, line: str) -> None:
40+
"""Write a line to the output, adding a newline."""
41+
assert self.outfile is not None
42+
self.outfile.write(line.rstrip())
43+
self.outfile.write("\n")
44+
45+
def write_items(self, items: Iterable[str]) -> None:
46+
"""Write a list of strings, joined together."""
47+
self.write("".join(items))
48+
49+
def _report_text(
50+
self,
51+
header: List[str],
52+
lines_values: List[List[Any]],
53+
total_line: List[Any],
54+
end_lines: List[str],
55+
) -> None:
56+
"""Internal method that prints report data in text format.
57+
58+
`header` is a list with captions.
59+
`lines_values` is list of lists of sortable values.
60+
`total_line` is a list with values of the total line.
61+
`end_lines` is a list of ending lines with information about skipped files.
62+
63+
"""
64+
# Prepare the formatting strings, header, and column sorting.
65+
max_name = max([len(line[0]) for line in lines_values] + [5]) + 1
66+
max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1
67+
max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values])
68+
formats = dict(
69+
Name="{:{name_len}}",
70+
Stmts="{:>7}",
71+
Miss="{:>7}",
72+
Branch="{:>7}",
73+
BrPart="{:>7}",
74+
Cover="{:>{n}}",
75+
Missing="{:>10}",
76+
)
77+
header_items = [
78+
formats[item].format(item, name_len=max_name, n=max_n)
79+
for item in header
80+
]
81+
header_str = "".join(header_items)
82+
rule = "-" * len(header_str)
83+
84+
# Write the header
85+
self.write(header_str)
86+
self.write(rule)
87+
88+
formats.update(dict(Cover="{:>{n}}%"), Missing=" {:9}")
89+
for values in lines_values:
90+
# build string with line values
91+
line_items = [
92+
formats[item].format(str(value),
93+
name_len=max_name, n=max_n-1) for item, value in zip(header, values)
94+
]
95+
self.write_items(line_items)
96+
97+
# Write a TOTAL line
98+
if lines_values:
99+
self.write(rule)
100+
101+
line_items = [
102+
formats[item].format(str(value),
103+
name_len=max_name, n=max_n-1) for item, value in zip(header, total_line)
104+
]
105+
self.write_items(line_items)
106+
107+
for end_line in end_lines:
108+
self.write(end_line)
109+
110+
def _report_markdown(
111+
self,
112+
header: List[str],
113+
lines_values: List[List[Any]],
114+
total_line: List[Any],
115+
end_lines: List[str],
116+
) -> None:
117+
"""Internal method that prints report data in markdown format.
118+
119+
`header` is a list with captions.
120+
`lines_values` is a sorted list of lists containing coverage information.
121+
`total_line` is a list with values of the total line.
122+
`end_lines` is a list of ending lines with information about skipped files.
123+
124+
"""
125+
# Prepare the formatting strings, header, and column sorting.
126+
max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0)
127+
max_name = max(max_name, len("**TOTAL**")) + 1
128+
formats = dict(
129+
Name="| {:{name_len}}|",
130+
Stmts="{:>9} |",
131+
Miss="{:>9} |",
132+
Branch="{:>9} |",
133+
BrPart="{:>9} |",
134+
Cover="{:>{n}} |",
135+
Missing="{:>10} |",
136+
)
137+
max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover "))
138+
header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header]
139+
header_str = "".join(header_items)
140+
rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, "-")] +
141+
["-: |".rjust(len(item)-1, "-") for item in header_items[1:]]
142+
)
143+
144+
# Write the header
145+
self.write(header_str)
146+
self.write(rule_str)
147+
148+
for values in lines_values:
149+
# build string with line values
150+
formats.update(dict(Cover="{:>{n}}% |"))
151+
line_items = [
152+
formats[item].format(str(value).replace("_", "\\_"), name_len=max_name, n=max_n-1)
153+
for item, value in zip(header, values)
154+
]
155+
self.write_items(line_items)
156+
157+
# Write the TOTAL line
158+
formats.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |"))
159+
total_line_items: List[str] = []
160+
for item, value in zip(header, total_line):
161+
if value == "":
162+
insert = value
163+
elif item == "Cover":
164+
insert = f" **{value}%**"
114165
else:
115-
raise
166+
insert = f" **{value}**"
167+
total_line_items += formats[item].format(insert, name_len=max_name, n=max_n)
168+
self.write_items(total_line_items)
169+
for end_line in end_lines:
170+
self.write(end_line)
171+
172+
def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float:
173+
"""Writes a report summarizing coverage statistics per module.
174+
175+
`outfile` is a text-mode file object to write the summary to.
176+
177+
"""
178+
self.outfile = outfile or sys.stdout
179+
180+
self.coverage.get_data().set_query_contexts(self.config.report_contexts)
181+
for fr, analysis in get_analysis_to_report(self.coverage, morfs):
182+
self.report_one_file(fr, analysis)
183+
184+
if not self.total.n_files and not self.skipped_count:
185+
raise NoDataError("No data to report.")
186+
187+
if self.output_format == "total":
188+
self.write(self.total.pc_covered_str)
189+
else:
190+
self.tabular_report()
191+
192+
return self.total.pc_covered
193+
194+
def tabular_report(self) -> None:
195+
"""Writes tabular report formats."""
196+
# Prepare the header line and column sorting.
197+
header = ["Name", "Stmts", "Miss"]
198+
if self.branches:
199+
header += ["Branch", "BrPart"]
200+
header += ["Cover"]
201+
if self.config.show_missing:
202+
header += ["Missing"]
203+
204+
column_order = dict(name=0, stmts=1, miss=2, cover=-1)
205+
if self.branches:
206+
column_order.update(dict(branch=3, brpart=4))
207+
208+
# `lines_values` is list of lists of sortable values.
209+
lines_values = []
210+
211+
for (fr, analysis) in self.fr_analysis:
212+
nums = analysis.numbers
213+
214+
args = [fr.relative_filename(), nums.n_statements, nums.n_missing]
215+
if self.branches:
216+
args += [nums.n_branches, nums.n_partial_branches]
217+
args += [nums.pc_covered_str]
218+
if self.config.show_missing:
219+
args += [analysis.missing_formatted(branches=True)]
220+
args += [nums.pc_covered]
221+
lines_values.append(args)
222+
223+
# Line sorting.
224+
sort_option = (self.config.sort or "name").lower()
225+
reverse = False
226+
if sort_option[0] == "-":
227+
reverse = True
228+
sort_option = sort_option[1:]
229+
elif sort_option[0] == "+":
230+
sort_option = sort_option[1:]
231+
sort_idx = column_order.get(sort_option)
232+
if sort_idx is None:
233+
raise ConfigError(f"Invalid sorting option: {self.config.sort!r}")
234+
if sort_option == "name":
235+
lines_values = human_sorted_items(lines_values, reverse=reverse)
236+
else:
237+
lines_values.sort(
238+
key=lambda line: (line[sort_idx], line[0]), # type: ignore[index]
239+
reverse=reverse,
240+
)
241+
242+
# Calculate total if we had at least one file.
243+
total_line = ["TOTAL", self.total.n_statements, self.total.n_missing]
244+
if self.branches:
245+
total_line += [self.total.n_branches, self.total.n_partial_branches]
246+
total_line += [self.total.pc_covered_str]
247+
if self.config.show_missing:
248+
total_line += [""]
249+
250+
# Create other final lines.
251+
end_lines = []
252+
if self.config.skip_covered and self.skipped_count:
253+
file_suffix = "s" if self.skipped_count>1 else ""
254+
end_lines.append(
255+
f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage."
256+
)
257+
if self.config.skip_empty and self.empty_count:
258+
file_suffix = "s" if self.empty_count > 1 else ""
259+
end_lines.append(f"\n{self.empty_count} empty file{file_suffix} skipped.")
260+
261+
if self.output_format == "markdown":
262+
formatter = self._report_markdown
263+
else:
264+
formatter = self._report_text
265+
formatter(header, lines_values, total_line, end_lines)
266+
267+
def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None:
268+
"""Report on just one file, the callback from report()."""
269+
nums = analysis.numbers
270+
self.total += nums
271+
272+
no_missing_lines = (nums.n_missing == 0)
273+
no_missing_branches = (nums.n_partial_branches == 0)
274+
if self.config.skip_covered and no_missing_lines and no_missing_branches:
275+
# Don't report on 100% files.
276+
self.skipped_count += 1
277+
elif self.config.skip_empty and nums.n_statements == 0:
278+
# Don't report on empty files.
279+
self.empty_count += 1
116280
else:
117-
yield (fr, analysis)
281+
self.fr_analysis.append((fr, analysis))

‎coverage/report_core.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2+
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3+
4+
"""Reporter foundation for coverage.py."""
5+
6+
from __future__ import annotations
7+
8+
import sys
9+
10+
from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING
11+
12+
from coverage.exceptions import NoDataError, NotPython
13+
from coverage.files import prep_patterns, GlobMatcher
14+
from coverage.misc import ensure_dir_for_file, file_be_gone
15+
from coverage.plugin import FileReporter
16+
from coverage.results import Analysis
17+
from coverage.types import Protocol, TMorf
18+
19+
if TYPE_CHECKING:
20+
from coverage import Coverage
21+
22+
23+
class Reporter(Protocol):
24+
"""What we expect of reporters."""
25+
26+
report_type: str
27+
28+
def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
29+
"""Generate a report of `morfs`, written to `outfile`."""
30+
31+
32+
def render_report(
33+
output_path: str,
34+
reporter: Reporter,
35+
morfs: Optional[Iterable[TMorf]],
36+
msgfn: Callable[[str], None],
37+
) -> float:
38+
"""Run a one-file report generator, managing the output file.
39+
40+
This function ensures the output file is ready to be written to. Then writes
41+
the report to it. Then closes the file and cleans up.
42+
43+
"""
44+
file_to_close = None
45+
delete_file = False
46+
47+
if output_path == "-":
48+
outfile = sys.stdout
49+
else:
50+
# Ensure that the output directory is created; done here because this
51+
# report pre-opens the output file. HtmlReporter does this on its own
52+
# because its task is more complex, being multiple files.
53+
ensure_dir_for_file(output_path)
54+
outfile = open(output_path, "w", encoding="utf-8")
55+
file_to_close = outfile
56+
delete_file = True
57+
58+
try:
59+
ret = reporter.report(morfs, outfile=outfile)
60+
if file_to_close is not None:
61+
msgfn(f"Wrote {reporter.report_type} to {output_path}")
62+
delete_file = False
63+
return ret
64+
finally:
65+
if file_to_close is not None:
66+
file_to_close.close()
67+
if delete_file:
68+
file_be_gone(output_path) # pragma: part covered (doesn't return)
69+
70+
71+
def get_analysis_to_report(
72+
coverage: Coverage,
73+
morfs: Optional[Iterable[TMorf]],
74+
) -> Iterator[Tuple[FileReporter, Analysis]]:
75+
"""Get the files to report on.
76+
77+
For each morf in `morfs`, if it should be reported on (based on the omit
78+
and include configuration options), yield a pair, the `FileReporter` and
79+
`Analysis` for the morf.
80+
81+
"""
82+
file_reporters = coverage._get_file_reporters(morfs)
83+
config = coverage.config
84+
85+
if config.report_include:
86+
matcher = GlobMatcher(prep_patterns(config.report_include), "report_include")
87+
file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)]
88+
89+
if config.report_omit:
90+
matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit")
91+
file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)]
92+
93+
if not file_reporters:
94+
raise NoDataError("No data to report.")
95+
96+
for fr in sorted(file_reporters):
97+
try:
98+
analysis = coverage._analyze(fr)
99+
except NotPython:
100+
# Only report errors for .py files, and only if we didn't
101+
# explicitly suppress those errors.
102+
# NotPython is only raised by PythonFileReporter, which has a
103+
# should_be_python() method.
104+
if fr.should_be_python(): # type: ignore[attr-defined]
105+
if config.ignore_errors:
106+
msg = f"Couldn't parse Python file '{fr.filename}'"
107+
coverage._warn(msg, slug="couldnt-parse")
108+
else:
109+
raise
110+
except Exception as exc:
111+
if config.ignore_errors:
112+
msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip()
113+
coverage._warn(msg, slug="couldnt-parse")
114+
else:
115+
raise
116+
else:
117+
yield (fr, analysis)

‎coverage/summary.py

-281
This file was deleted.

‎coverage/xmlreport.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from coverage import __version__, files
1818
from coverage.misc import isolate_module, human_sorted, human_sorted_items
1919
from coverage.plugin import FileReporter
20-
from coverage.report import get_analysis_to_report
20+
from coverage.report_core import get_analysis_to_report
2121
from coverage.results import Analysis
2222
from coverage.types import TMorf
2323
from coverage.version import __url__

‎tests/test_html.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from coverage.exceptions import NoDataError, NotPython, NoSource
2525
from coverage.files import abs_file, flat_rootname
2626
import coverage.html
27-
from coverage.report import get_analysis_to_report
27+
from coverage.report_core import get_analysis_to_report
2828
from coverage.types import TLineNo, TMorf
2929

3030
from tests.coveragetest import CoverageTest, TESTS_DIR

‎tests/test_report.py

+1,060-47
Large diffs are not rendered by default.

‎tests/test_report_core.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2+
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
3+
4+
"""Tests for helpers in report.py"""
5+
6+
from __future__ import annotations
7+
8+
from typing import IO, Iterable, List, Optional, Type
9+
10+
import pytest
11+
12+
from coverage.exceptions import CoverageException
13+
from coverage.report_core import render_report
14+
from coverage.types import TMorf
15+
16+
from tests.coveragetest import CoverageTest
17+
18+
19+
class FakeReporter:
20+
"""A fake implementation of a one-file reporter."""
21+
22+
report_type = "fake report file"
23+
24+
def __init__(self, output: str = "", error: Optional[Type[Exception]] = None) -> None:
25+
self.output = output
26+
self.error = error
27+
self.morfs: Optional[Iterable[TMorf]] = None
28+
29+
def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float:
30+
"""Fake."""
31+
self.morfs = morfs
32+
outfile.write(self.output)
33+
if self.error:
34+
raise self.error("You asked for it!")
35+
return 17.25
36+
37+
38+
class RenderReportTest(CoverageTest):
39+
"""Tests of render_report."""
40+
41+
def test_stdout(self) -> None:
42+
fake = FakeReporter(output="Hello!\n")
43+
msgs: List[str] = []
44+
res = render_report("-", fake, [pytest, "coverage"], msgs.append)
45+
assert res == 17.25
46+
assert fake.morfs == [pytest, "coverage"]
47+
assert self.stdout() == "Hello!\n"
48+
assert not msgs
49+
50+
def test_file(self) -> None:
51+
fake = FakeReporter(output="Gréètings!\n")
52+
msgs: List[str] = []
53+
res = render_report("output.txt", fake, [], msgs.append)
54+
assert res == 17.25
55+
assert self.stdout() == ""
56+
with open("output.txt", "rb") as f:
57+
assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!"
58+
assert msgs == ["Wrote fake report file to output.txt"]
59+
60+
@pytest.mark.parametrize("error", [CoverageException, ZeroDivisionError])
61+
def test_exception(self, error: Type[Exception]) -> None:
62+
fake = FakeReporter(error=error)
63+
msgs: List[str] = []
64+
with pytest.raises(error, match="You asked for it!"):
65+
render_report("output.txt", fake, [], msgs.append)
66+
assert self.stdout() == ""
67+
self.assert_doesnt_exist("output.txt")
68+
assert not msgs

‎tests/test_summary.py

-1,081
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.