Skip to content

Commit 8cdb7d8

Browse files
committed
Report data file errors in more detail: include file and directory paths, and make helpful suggestions
1 parent 5c70761 commit 8cdb7d8

13 files changed

+89
-32
lines changed

coverage/cmdline.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import sys
1414
import textwrap
1515
import traceback
16+
from contextlib import suppress
1617

1718
from typing import cast, Any, NoReturn
1819

@@ -24,7 +25,8 @@
2425
from coverage.control import DEFAULT_DATAFILE
2526
from coverage.data import combinable_files, debug_data_file
2627
from coverage.debug import info_header, short_stack, write_formatted_info
27-
from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource
28+
from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource, \
29+
NoDataFilesFoundError
2830
from coverage.execfile import PyRunner
2931
from coverage.results import display_covered, should_fail_under
3032
from coverage.version import __url__
@@ -882,9 +884,10 @@ def do_debug(self, args: list[str]) -> int:
882884
print(info_header("data"))
883885
data_file = self.coverage.config.data_file
884886
debug_data_file(data_file)
885-
for filename in combinable_files(data_file):
886-
print("-----")
887-
debug_data_file(filename)
887+
with suppress(NoDataFilesFoundError):
888+
for filename in combinable_files(data_file):
889+
print("-----")
890+
debug_data_file(filename)
888891
elif args[0] == "config":
889892
write_formatted_info(print, "config", self.coverage.config.debug_info())
890893
elif args[0] == "premain":

coverage/data.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
import glob
1616
import hashlib
1717
import os.path
18-
1918
from typing import Callable, Iterable
2019

21-
from coverage.exceptions import CoverageException, NoDataError
20+
from coverage.exceptions import CoverageException, DataFileOrDirectoryNotFoundError, \
21+
NoDataFilesFoundError, UnusableDataFilesError
2222
from coverage.files import PathAliases
2323
from coverage.misc import Hasher, file_be_gone, human_sorted, plural
2424
from coverage.sqldata import CoverageData
@@ -82,12 +82,15 @@ def combinable_files(data_file: str, data_paths: Iterable[str] | None = None) ->
8282
pattern = glob.escape(os.path.join(os.path.abspath(p), local)) +".*"
8383
files_to_combine.extend(glob.glob(pattern))
8484
else:
85-
raise NoDataError(f"Couldn't combine from non-existent path '{p}'")
85+
raise DataFileOrDirectoryNotFoundError.new_for_data_file_or_directory(p, is_combining=True)
8686

8787
# SQLite might have made journal files alongside our database files.
8888
# We never want to combine those.
8989
files_to_combine = [fnm for fnm in files_to_combine if not fnm.endswith("-journal")]
9090

91+
if not files_to_combine:
92+
raise NoDataFilesFoundError.new_for_data_directory(data_dir)
93+
9194
# Sorting isn't usually needed, since it shouldn't matter what order files
9295
# are combined, but sorting makes tests more predictable, and makes
9396
# debugging more understandable when things go wrong.
@@ -129,10 +132,12 @@ def combine_parallel_data(
129132
`message` is a function to use for printing messages to the user.
130133
131134
"""
132-
files_to_combine = combinable_files(data.base_filename(), data_paths)
133-
134-
if strict and not files_to_combine:
135-
raise NoDataError("No data to combine")
135+
try:
136+
files_to_combine = combinable_files(data.base_filename(), data_paths)
137+
except NoDataFilesFoundError:
138+
if strict:
139+
raise
140+
return
136141

137142
file_hashes = set()
138143
combined_any = False
@@ -190,7 +195,7 @@ def combine_parallel_data(
190195
file_be_gone(f)
191196

192197
if strict and not combined_any:
193-
raise NoDataError("No usable data files")
198+
raise UnusableDataFilesError.new_for_data_files(*files_to_combine)
194199

195200

196201
def debug_data_file(filename: str) -> None:

coverage/exceptions.py

+47
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55

66
from __future__ import annotations
77

8+
import os.path
9+
10+
11+
def _message_append_combine_hint(message: str, is_combining: bool) -> str:
12+
"""Append information about the combine command to error messages."""
13+
if not is_combining:
14+
message += " Perhaps `coverage combine` must be run first."
15+
return message
16+
17+
818
class _BaseCoverageException(Exception):
919
"""The base-base of all Coverage exceptions."""
1020
pass
@@ -24,11 +34,48 @@ class DataError(CoverageException):
2434
"""An error in using a data file."""
2535
pass
2636

37+
2738
class NoDataError(CoverageException):
2839
"""We didn't have data to work with."""
2940
pass
3041

3142

43+
class DataFileOrDirectoryNotFoundError(NoDataError):
44+
"""A data file or data directory could be found."""
45+
@classmethod
46+
def new_for_data_file_or_directory(cls, data_file_or_directory_path: str, *, is_combining: bool = False) -> 'DataFileOrDirectoryNotFoundError':
47+
"""
48+
Create a new instance.
49+
"""
50+
message = f"The data file or directory `{os.path.abspath(data_file_or_directory_path)}` could not be found."
51+
return cls(_message_append_combine_hint(message, is_combining))
52+
53+
54+
class NoDataFilesFoundError(NoDataError):
55+
"""No data files could be found in a data directory."""
56+
@classmethod
57+
def new_for_data_directory(cls, data_directory_path: str, *, is_combining: bool = False) -> 'NoDataFilesFoundError':
58+
"""
59+
Create a new instance.
60+
"""
61+
message = f"The data directory `{os.path.abspath(data_directory_path)}` does not contain any data files."
62+
return cls(_message_append_combine_hint(message, is_combining))
63+
64+
65+
class UnusableDataFilesError(NoDataError):
66+
"""The given data files are unusable."""
67+
@classmethod
68+
def new_for_data_files(cls, *data_file_paths: str) -> 'UnusableDataFilesError':
69+
"""
70+
Create a new instance.
71+
"""
72+
message = "The following data files are unusable, perhaps because they do not contain valid coverage information:"
73+
for data_file_path in data_file_paths:
74+
message += f"\n- `{os.path.abspath(data_file_path)}`"
75+
76+
return cls(message)
77+
78+
3279
class NoSource(CoverageException):
3380
"""We couldn't find the source for a module."""
3481
pass

coverage/html.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import coverage
2121
from coverage.data import CoverageData, add_data_to_hash
22-
from coverage.exceptions import NoDataError
22+
from coverage.exceptions import DataFileOrDirectoryNotFoundError
2323
from coverage.files import flat_rootname
2424
from coverage.misc import (
2525
ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime,
@@ -317,7 +317,7 @@ def report(self, morfs: Iterable[TMorf] | None) -> float:
317317
file_be_gone(os.path.join(self.directory, ftr.html_filename))
318318

319319
if not have_data:
320-
raise NoDataError("No data to report.")
320+
raise DataFileOrDirectoryNotFoundError.new_for_data_file_or_directory(os.path.dirname(self.coverage.get_data().base_filename()))
321321

322322
self.make_directory()
323323
self.make_local_static_report_files()

coverage/report.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
from __future__ import annotations
77

8+
import os
89
import sys
910

1011
from typing import Any, IO, Iterable, TYPE_CHECKING
1112

12-
from coverage.exceptions import ConfigError, NoDataError
13+
from coverage.exceptions import ConfigError, DataFileOrDirectoryNotFoundError
1314
from coverage.misc import human_sorted_items
1415
from coverage.plugin import FileReporter
1516
from coverage.report_core import get_analysis_to_report
@@ -182,7 +183,7 @@ def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str] | None = None)
182183
self.report_one_file(fr, analysis)
183184

184185
if not self.total.n_files and not self.skipped_count:
185-
raise NoDataError("No data to report.")
186+
raise DataFileOrDirectoryNotFoundError.new_for_data_file_or_directory(os.path.dirname(self.coverage.get_data().base_filename()))
186187

187188
if self.output_format == "total":
188189
self.write(self.total.pc_covered_str)

coverage/report_core.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55

66
from __future__ import annotations
77

8+
import os
89
import sys
910

1011
from typing import (
1112
Callable, Iterable, Iterator, IO, Protocol, TYPE_CHECKING,
1213
)
1314

14-
from coverage.exceptions import NoDataError, NotPython
15+
from coverage.exceptions import NotPython, DataFileOrDirectoryNotFoundError
1516
from coverage.files import prep_patterns, GlobMatcher
1617
from coverage.misc import ensure_dir_for_file, file_be_gone
1718
from coverage.plugin import FileReporter
@@ -93,7 +94,7 @@ def get_analysis_to_report(
9394
fr_morfs = [(fr, morf) for (fr, morf) in fr_morfs if not matcher.match(fr.filename)]
9495

9596
if not fr_morfs:
96-
raise NoDataError("No data to report.")
97+
raise DataFileOrDirectoryNotFoundError.new_for_data_file_or_directory(os.path.dirname(coverage.get_data().base_filename()))
9798

9899
for fr, morf in sorted(fr_morfs):
99100
try:

tests/test_api.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def test_empty_reporting(self) -> None:
300300
# empty summary reports raise exception, just like the xml report
301301
cov = coverage.Coverage()
302302
cov.erase()
303-
with pytest.raises(NoDataError, match="No data to report."):
303+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
304304
cov.report()
305305

306306
def test_completely_zero_reporting(self) -> None:
@@ -446,7 +446,7 @@ def test_combining_twice(self) -> None:
446446
self.assert_exists(".coverage")
447447

448448
cov2 = coverage.Coverage()
449-
with pytest.raises(NoDataError, match=r"No data to combine"):
449+
with pytest.raises(NoDataError, match=r"^The data directory `(.+?)` does not contain any data files. Perhaps `coverage combine` must be run first.$"):
450450
cov2.combine(strict=True, keep=False)
451451

452452
cov3 = coverage.Coverage()
@@ -1326,7 +1326,7 @@ def test_combine_parallel_data(self) -> None:
13261326
# Running combine again should fail, because there are no parallel data
13271327
# files to combine.
13281328
cov = coverage.Coverage()
1329-
with pytest.raises(NoDataError, match=r"No data to combine"):
1329+
with pytest.raises(NoDataError):
13301330
cov.combine(strict=True)
13311331

13321332
# And the originally combined data is still there.
@@ -1376,7 +1376,7 @@ def test_combine_no_usable_files(self) -> None:
13761376
# Combine the parallel coverage data files into .coverage, but nothing is readable.
13771377
cov = coverage.Coverage()
13781378
with pytest.warns(Warning) as warns:
1379-
with pytest.raises(NoDataError, match=r"No usable data files"):
1379+
with pytest.raises(NoDataError, match=r"^The following data files are unusable, perhaps because they do not contain valid coverage information:\n- `(.+?)`\n- `(.+?)`$"):
13801380
cov.combine(strict=True)
13811381

13821382
warn_rx = re.compile(

tests/test_coverage.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1635,19 +1635,19 @@ class ReportingTest(CoverageTest):
16351635
def test_no_data_to_report_on_annotate(self) -> None:
16361636
# Reporting with no data produces a nice message and no output
16371637
# directory.
1638-
with pytest.raises(NoDataError, match="No data to report."):
1638+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
16391639
self.command_line("annotate -d ann")
16401640
self.assert_doesnt_exist("ann")
16411641

16421642
def test_no_data_to_report_on_html(self) -> None:
16431643
# Reporting with no data produces a nice message and no output
16441644
# directory.
1645-
with pytest.raises(NoDataError, match="No data to report."):
1645+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
16461646
self.command_line("html -d htmlcov")
16471647
self.assert_doesnt_exist("htmlcov")
16481648

16491649
def test_no_data_to_report_on_xml(self) -> None:
16501650
# Reporting with no data produces a nice message.
1651-
with pytest.raises(NoDataError, match="No data to report."):
1651+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
16521652
self.command_line("xml")
16531653
self.assert_doesnt_exist("coverage.xml")

tests/test_data.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ def test_combining_from_files(self) -> None:
915915

916916
def test_combining_from_nonexistent_directories(self) -> None:
917917
covdata = DebugCoverageData()
918-
msg = "Couldn't combine from non-existent path 'xyzzy'"
918+
msg = r"^The data file or directory `(.+?)` could not be found.$"
919919
with pytest.raises(NoDataError, match=msg):
920920
combine_parallel_data(covdata, data_paths=['xyzzy'])
921921

tests/test_html.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ def test_dothtml_not_python(self) -> None:
426426
self.make_file("innocuous.html", "<h1>This isn't python at all!</h1>")
427427
cov = coverage.Coverage()
428428
cov.load()
429-
with pytest.raises(NoDataError, match="No data to report."):
429+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
430430
cov.html_report()
431431

432432
def test_execed_liar_ignored(self) -> None:

tests/test_process.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,7 @@ class FailUnderNoFilesTest(CoverageTest):
11431143
def test_report(self) -> None:
11441144
self.make_file(".coveragerc", "[report]\nfail_under = 99\n")
11451145
st, out = self.run_command_status("coverage report")
1146-
assert 'No data to report.' in out
1146+
assert re.match(r"The data file or directory `([^`]+?)` could not be found\. Perhaps `coverage combine` must be run first\.", out)
11471147
assert st == 1
11481148

11491149

tests/test_report.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ def foo():
551551
def test_report_skip_covered_no_data(self) -> None:
552552
cov = coverage.Coverage()
553553
cov.load()
554-
with pytest.raises(NoDataError, match="No data to report."):
554+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
555555
self.get_report(cov, skip_covered=True)
556556
self.assert_doesnt_exist(".coverage")
557557

@@ -716,7 +716,7 @@ def test_dotpy_not_python_ignored(self) -> None:
716716
self.make_data_file(lines={"mycode.py": [1]})
717717
cov = coverage.Coverage()
718718
cov.load()
719-
with pytest.raises(NoDataError, match="No data to report."):
719+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
720720
with pytest.warns(Warning) as warns:
721721
self.get_report(cov, morfs=["mycode.py"], ignore_errors=True)
722722
assert_coverage_warnings(
@@ -733,7 +733,7 @@ def test_dothtml_not_python(self) -> None:
733733
self.make_data_file(lines={"mycode.html": [1]})
734734
cov = coverage.Coverage()
735735
cov.load()
736-
with pytest.raises(NoDataError, match="No data to report."):
736+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
737737
self.get_report(cov, morfs=["mycode.html"])
738738

739739
def test_report_no_extension(self) -> None:

tests/test_xml.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def test_config_affects_xml_placement(self) -> None:
145145

146146
def test_no_data(self) -> None:
147147
# https://github.com/nedbat/coveragepy/issues/210
148-
with pytest.raises(NoDataError, match="No data to report."):
148+
with pytest.raises(NoDataError, match=r"^The data file or directory `(.+?)` could not be found\. Perhaps `coverage combine` must be run first\.$"):
149149
self.run_xml_report()
150150
self.assert_doesnt_exist("coverage.xml")
151151
self.assert_doesnt_exist(".coverage")

0 commit comments

Comments
 (0)