assertion/rewrite: fix internal error on collection error due to decorated function

For decorated functions, the lineno of the FunctionDef AST node points
to the `def` line, not to the first decorator line. On the other hand,
in code objects, the `co_firstlineno` points to the first decorator
line.

Assertion rewriting inserts some imports to code it rewrites. The
imports are inserted at the lineno of the first statement in the AST. In
turn, the code object compiled from the rewritten AST uses the lineno of
the first statement (which is the first inserted import).

This means that given a module like this,

```py
@foo
@bar
def baz(): pass
```

the lineno of the code object without assertion rewriting
(`--assertion=plain`) is 1, but with assertion rewriting it is 3.

And *this* causes some issues for the exception repr when e.g. the
decorator line is invalid and raises during collection. The code becomes
confused and crashes with

INTERNALERROR>   File "_pytest/_code/code.py", line 638, in get_source
INTERNALERROR>     lines.append(space_prefix + source.lines[line_index].strip())
INTERNALERROR> IndexError: list index out of range

Fix it by special casing decorators. Maybe there are other cases like
this but off hand I can't think of another Python construct where the
lineno of the item would be after its first line, and this is the only
such issue we have had reported.
This commit is contained in:
Ran Benita 2020-09-12 22:28:19 +03:00
parent 35350e11cd
commit d18cb961cf
3 changed files with 23 additions and 1 deletions

View File

@ -0,0 +1,3 @@
Fixed an internal error crash with ``IndexError: list index out of range`` when
collecting a module which starts with a decorated function, the decorator
raises, and assertion rewriting is enabled.

View File

@ -695,7 +695,12 @@ class AssertionRewriter(ast.NodeVisitor):
else: else:
break break
pos += 1 pos += 1
lineno = item.lineno # Special case: for a decorated function, set the lineno to that of the
# first decorator, not the `def`. Issue #4984.
if isinstance(item, ast.FunctionDef) and item.decorator_list:
lineno = item.decorator_list[0].lineno
else:
lineno = item.lineno
imports = [ imports = [
ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
] ]

View File

@ -1393,3 +1393,17 @@ class TestImportModeImportlib:
"* 1 failed in *", "* 1 failed in *",
] ]
) )
def test_does_not_crash_on_error_from_decorated_function(testdir: Testdir) -> None:
"""Regression test for an issue around bad exception formatting due to
assertion rewriting mangling lineno's (#4984)."""
testdir.makepyfile(
"""
@pytest.fixture
def a(): return 4
"""
)
result = testdir.runpytest()
# Not INTERNAL_ERROR
assert result.ret == ExitCode.INTERRUPTED