v0.166.0
  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