1"""
2What a failed test carries away from its failure.
3
4A `Failure` is everything the report says about one failed test, already
5printed: text, and nothing that is still alive. It is made while the test's
6lifecycles are still in place and the values are as the test left them,
7and then the error, its frames and everything they held are let go.
8
9The reporter turns a `Failure` into what is printed. Anything else that
10reports a run reads the same structure.
11"""
12
13import ast
14import inspect
15import shlex
16import traceback
17import types
18from collections.abc import Sequence
19from dataclasses import dataclass
20from pathlib import Path
21
22from ..definition import TestDefinitionError
23from . import assertions
24from .collection import CollectionError, RunnableTest
25from .layout import path_as_shown, without_test_module_names
26from .output_capture import NO_OUTPUT, Output, StreamOutput
27from .printing import Describer, Diff, PrintedValue, ValuePrinter
28
29__all__ = []
30
31_RUNNER_DIRECTORY = str(Path(__file__).parent)
32
33# The names a rewritten assert keeps values under. An assert deletes its
34# own as it finishes, so these are only ever seen in a frame that an error
35# left in the middle of one.
36_KEPT_BY_AN_ASSERT = "__plain_test_"
37
38
39@dataclass(frozen=True, kw_only=True)
40class AssertedPart:
41 """One part of a failed assert's expression."""
42
43 # The part as the test file wrote it: `response.status_code`.
44 source: str
45 # How far inside the expression it is, from 0.
46 depth: int
47 # What it was. None when Python never evaluated it, as it doesn't the
48 # right side of an `and` whose left side was false.
49 value: PrintedValue | None
50
51
52@dataclass(frozen=True, kw_only=True)
53class FailedAssert:
54 """The assert that failed, and the values inside it."""
55
56 # The expression as the test file wrote it, without `assert`.
57 expression: str
58 # What the test gave after the comma, or None.
59 message: str | None
60 # The parts worth printing, outermost first, in the order written.
61 parts: tuple[AssertedPart, ...]
62 # For `assert left == right` with a side too large to read whole.
63 diff: Diff | None
64
65
66@dataclass(frozen=True, kw_only=True)
67class LocalValue:
68 """A name in the test function, and what it was when the test failed."""
69
70 name: str
71 value: PrintedValue
72
73
74@dataclass(frozen=True, kw_only=True)
75class Frame:
76 """One step of a traceback."""
77
78 # Relative to where the run started, when it is under there.
79 file: str
80 line: int
81 function: str
82
83
84@dataclass(frozen=True, kw_only=True)
85class Failure:
86 # "AssertionError", "KeyError"
87 error_type: str
88 error_message: str
89 # The statement in the test function that failed, or that called what
90 # failed. None when the test function isn't in the traceback: the error
91 # came from a lifecycle.
92 file: str | None
93 line: int | None
94 # Formatted, with the runner's own frames taken off the top.
95 traceback: str
96 # The same steps as data, outermost first. The error's own, without
97 # the ones of an error it was raised from.
98 frames: tuple[Frame, ...]
99 # None when what failed wasn't an assert in a test file.
100 failed_assert: FailedAssert | None
101 # The test function's, in the order they were bound, without the ones
102 # `failed_assert` has already printed.
103 locals: tuple[LocalValue, ...]
104 # The command that runs this test again, safe to paste.
105 rerun_command: str
106 # What the test wrote, from before its lifecycles entered to after they
107 # exited. It is added when they have exited.
108 stdout: StreamOutput = NO_OUTPUT.stdout
109 stderr: StreamOutput = NO_OUTPUT.stderr
110
111
112@dataclass(frozen=True, kw_only=True)
113class CollectionFailure:
114 """A file no tests could be collected from, and why."""
115
116 file: str
117 # Whether the file is written in a way the runner can't run. Then
118 # `message` says what is wrong and what to write instead, and there is
119 # no traceback. Otherwise it is an error like any other, raised while
120 # the file was being loaded.
121 is_definition_error: bool
122 error_type: str
123 message: str
124 traceback: str | None
125 # Where in the file, when the error says.
126 line: int | None
127 # What loading the file wrote.
128 stdout: StreamOutput = NO_OUTPUT.stdout
129 stderr: StreamOutput = NO_OUTPUT.stderr
130 # The parameters of its tests that nothing passes in, each with how many
131 # of its tests take it.
132 parameters: tuple[tuple[str, int], ...] = ()
133
134
135def describe_collection_error(
136 error: CollectionError, *, file: str, output: Output = NO_OUTPUT
137) -> CollectionFailure:
138 cause = error.error
139 if isinstance(cause, TestDefinitionError):
140 return CollectionFailure(
141 file=file,
142 is_definition_error=True,
143 error_type=type(cause).__qualname__,
144 message=str(cause),
145 traceback=None,
146 line=cause.line,
147 stdout=output.stdout,
148 stderr=output.stderr,
149 parameters=tuple(error.parameters.items()),
150 )
151
152 return CollectionFailure(
153 file=file,
154 is_definition_error=False,
155 error_type=type(cause).__qualname__,
156 message=_guarded_str(cause),
157 traceback=format_collection_traceback(cause),
158 line=_line_in_the_file(cause, path=error.path),
159 stdout=output.stdout,
160 stderr=output.stderr,
161 )
162
163
164def format_collection_traceback(cause: BaseException) -> str:
165 """
166 The traceback of an error raised while a test file was being loaded,
167 starting at the test file.
168
169 The frames above the test file are the runner loading it, by way of
170 `ast` or the import system. A SyntaxError has no frames below those: it
171 names the file and the line itself.
172 """
173 not_the_test_file = (_RUNNER_DIRECTORY, ast.__file__, "<frozen importlib")
174 tb = cause.__traceback__
175 while tb is not None:
176 if not tb.tb_frame.f_code.co_filename.startswith(not_the_test_file):
177 break
178 tb = tb.tb_next
179 formatted = "".join(traceback.format_exception(type(cause), cause, tb))
180 return without_test_module_names(formatted).rstrip()
181
182
183def _line_in_the_file(cause: BaseException, *, path: Path) -> int | None:
184 if isinstance(cause, SyntaxError):
185 return cause.lineno
186
187 # The deepest step that is in the file itself.
188 line = None
189 tb = cause.__traceback__
190 while tb is not None:
191 if tb.tb_frame.f_code.co_filename == str(path):
192 line = tb.tb_lineno
193 tb = tb.tb_next
194 return line
195
196
197def shown_path(filename: str) -> str:
198 """A path the way the run's output writes it: relative to where it started."""
199 return path_as_shown(Path(filename), root=Path.cwd())
200
201
202def describe_failure(
203 error: BaseException,
204 *,
205 test: RunnableTest,
206 describers: Sequence[Describer],
207 full_values: bool,
208) -> Failure:
209 printer = ValuePrinter(describers=describers, full_values=full_values)
210
211 failed_assert = None
212 watched = assertions.watched_assert_of(error)
213 if watched is not None:
214 failed_assert = _failed_assert(watched, printer=printer)
215
216 already_printed = set()
217 if failed_assert is not None:
218 already_printed = {part.source for part in failed_assert.parts}
219
220 file, line = _where_in_the_test(error, test=test)
221 return Failure(
222 error_type=type(error).__qualname__,
223 error_message=_guarded_str(error),
224 file=file,
225 line=line,
226 traceback=format_traceback(error),
227 frames=_frames(error),
228 failed_assert=failed_assert,
229 locals=_locals_of_the_test(
230 error, test=test, printer=printer, already_printed=already_printed
231 ),
232 rerun_command=rerun_command(test.id),
233 )
234
235
236def failure_that_could_not_be_described(
237 error: BaseException, *, test: RunnableTest, while_describing: Exception
238) -> Failure:
239 """
240 The failure with its traceback and nothing more, for when describing it
241 went wrong. The test's own error is what the reader came for.
242 """
243 file, line = _where_in_the_test(error, test=test)
244 return Failure(
245 error_type=type(error).__qualname__,
246 error_message=_guarded_str(error),
247 file=file,
248 line=line,
249 traceback=format_traceback(error)
250 + "\n(The values couldn't be printed: "
251 + f"{type(while_describing).__qualname__}: {while_describing})\n",
252 frames=_frames(error),
253 failed_assert=None,
254 locals=(),
255 rerun_command=rerun_command(test.id),
256 )
257
258
259def _guarded_str(error: BaseException) -> str:
260 try:
261 return without_test_module_names(str(error))
262 except Exception as raised:
263 return f"<its str raised {type(raised).__qualname__}: {raised}>"
264
265
266def rerun_command(test_id: str) -> str:
267 """
268 The command that runs one test, safe to paste. A case id can hold
269 anything (`test_price[annual plan]`), and a shell reads spaces, brackets,
270 quotes and `$` for itself unless the id is quoted.
271 """
272 return f"plain test {shlex.quote(test_id)}"
273
274
275def _without_the_runners_frames(error: BaseException) -> types.TracebackType | None:
276 tb = error.__traceback__
277 while tb is not None:
278 filename = tb.tb_frame.f_code.co_filename
279 if not filename.startswith(_RUNNER_DIRECTORY) and "contextlib" not in filename:
280 break
281 tb = tb.tb_next
282 return tb or error.__traceback__
283
284
285def format_traceback(error: BaseException) -> str:
286 """Format a traceback with the runner's own frames trimmed off the top."""
287 return without_test_module_names(
288 "".join(
289 traceback.format_exception(
290 type(error), error, _without_the_runners_frames(error)
291 )
292 )
293 )
294
295
296def _frames(error: BaseException) -> tuple[Frame, ...]:
297 frames = []
298 tb = _without_the_runners_frames(error)
299 while tb is not None:
300 code = tb.tb_frame.f_code
301 frames.append(
302 Frame(
303 file=shown_path(code.co_filename),
304 line=tb.tb_lineno,
305 function=code.co_qualname,
306 )
307 )
308 tb = tb.tb_next
309 return tuple(frames)
310
311
312def _where_in_the_test(
313 error: BaseException, *, test: RunnableTest
314) -> tuple[str | None, int | None]:
315 step = _step_of_the_test(error, test=test)
316 if step is None:
317 return None, None
318 return shown_path(step.tb_frame.f_code.co_filename), step.tb_lineno
319
320
321def where_defined(test: RunnableTest) -> tuple[str, int | None]:
322 """The file a test is in, and the line its definition starts on."""
323 file = test.id.partition("::")[0]
324 if test.function is None:
325 return file, None
326 return file, inspect.unwrap(test.function).__code__.co_firstlineno
327
328
329def _failed_assert(
330 watched: assertions.WatchedAssert, *, printer: ValuePrinter
331) -> FailedAssert:
332 diff = None
333 diffed: tuple[int, ...] = ()
334 if watched.equality is not None:
335 left, right = (watched.values[index] for index in watched.equality)
336 evaluated = (
337 left.value is not assertions.NOT_EVALUATED
338 and right.value is not assertions.NOT_EVALUATED
339 )
340 if evaluated:
341 diff = printer.diff(
342 left.value,
343 right.value,
344 left_source=left.source,
345 right_source=right.source,
346 )
347 if diff is not None:
348 diffed = watched.equality
349
350 parts = []
351 printed_before = set()
352 # The depth of a part that wasn't evaluated, while going through the
353 # parts inside it. They weren't evaluated either, and saying so of the
354 # whole says it of them.
355 inside_not_evaluated = None
356 for index, watched_value in enumerate(watched.values):
357 if inside_not_evaluated is not None:
358 if watched_value.depth > inside_not_evaluated:
359 continue
360 inside_not_evaluated = None
361
362 if watched_value.is_literal:
363 # Its value is what is written. It was kept for the diff.
364 continue
365 if watched_value.value is assertions.NOT_EVALUATED:
366 value = None
367 inside_not_evaluated = watched_value.depth
368 elif _says_only_where_it_is_from(watched_value):
369 continue
370 elif index in diffed:
371 # The diff says what matters about it, in less.
372 value = printer.summarized(watched_value.value)
373 else:
374 value = printer.printed(watched_value.value, name=watched_value.source)
375
376 # `a == b or a == c` has `a` in it twice, and says it once.
377 printed = (watched_value.source, value)
378 if printed in printed_before:
379 continue
380 printed_before.add(printed)
381
382 parts.append(
383 AssertedPart(
384 source=watched_value.source, depth=watched_value.depth, value=value
385 )
386 )
387
388 message = None
389 if watched.message is not None:
390 message = _guarded_str(watched.message)
391
392 return FailedAssert(
393 expression=watched.expression,
394 message=message,
395 parts=tuple(parts),
396 diff=diff,
397 )
398
399
400def _is_module_class_or_function(value: object) -> bool:
401 return (
402 inspect.ismodule(value)
403 or inspect.isclass(value)
404 or inspect.isfunction(value)
405 or inspect.ismethod(value)
406 or inspect.isbuiltin(value)
407 )
408
409
410def _says_only_where_it_is_from(watched_value: assertions.WatchedValue) -> bool:
411 """
412 A bare name for a module, a class or a function: `User` in
413 `isinstance(user, User)`. What it prints as is where it was defined,
414 which the name already says. One side of a comparison is always
415 printed, since there the value is what was compared.
416 """
417 return (
418 not watched_value.is_operand
419 and watched_value.source.isidentifier()
420 and _is_module_class_or_function(watched_value.value)
421 )
422
423
424def _locals_of_the_test(
425 error: BaseException,
426 *,
427 test: RunnableTest,
428 printer: ValuePrinter,
429 already_printed: set[str],
430) -> tuple[LocalValue, ...]:
431 step = _step_of_the_test(error, test=test)
432 if step is None:
433 return ()
434 frame = step.tb_frame
435
436 values = []
437 for name, value in frame.f_locals.items():
438 if name in already_printed or name.startswith(_KEPT_BY_AN_ASSERT):
439 continue
440 if _is_module_class_or_function(value):
441 continue
442 values.append(LocalValue(name=name, value=printer.printed(value, name=name)))
443 return tuple(values)
444
445
446def _step_of_the_test(
447 error: BaseException, *, test: RunnableTest
448) -> types.TracebackType | None:
449 """
450 The step of the traceback that is the test function. It is where the
451 failure started from, however far below it the error was raised.
452 """
453 if test.function is None:
454 return None
455 # A decorator that wraps the test (`@mock.patch`) has a frame of its
456 # own above it. The test's is the one running the test's own code.
457 code = inspect.unwrap(test.function).__code__
458
459 tb = error.__traceback__
460 while tb is not None:
461 if tb.tb_frame.f_code is code:
462 return tb
463 tb = tb.tb_next
464 return None