pytest-dev / pytest

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing
https://pytest.org
MIT License
12.2k stars 2.7k forks source link

Expressions in assert statements have the wrong line number #3463

Open alexmojaki opened 6 years ago

alexmojaki commented 6 years ago

In the below example, the normal Python traceback correctly points to line 5, where the actual division by zero happens. pytest points to line 4.

$ cat test.py
def test_thing():
    x = 0
    y = 3
    assert [1 / y,
            1 / x]

if __name__ == '__main__':
    test_thing()

$ python3.6 test.py
Traceback (most recent call last):
  File "test.py", line 8, in <module>
    test_thing()
  File "test.py", line 5, in test_thing
    1 / x]
ZeroDivisionError: division by zero

$ python3.6 -m pytest test.py
================================================================================ test session starts ================================================================================
platform darwin -- Python 3.6.3, pytest-3.5.1, py-1.5.3, pluggy-0.6.0
rootdir: /Users/alexhall, inifile:
collected 1 item                                                                                                                                                                    

test.py F                                                                                                                                                                     [100%]

===================================================================================== FAILURES ======================================================================================
____________________________________________________________________________________ test_thing _____________________________________________________________________________________

    def test_thing():
        x = 0
        y = 3
>       assert [1 / y,
                1 / x]
E       ZeroDivisionError: division by zero

test.py:4: ZeroDivisionError
============================================================================= 1 failed in 0.13 seconds ==============================================================================

Theoretically this certainly seems like a serious issue, but for the practical question of how often this is a problem, don't count me as a datapoint. I only noticed this problem while playing with some dark magic code that uses the line numbers of frames. The above example is completely contrived.

nicoddemus commented 6 years ago

Thanks for the report @alexmojaki!

blueyed commented 6 years ago

This also appears to be relevant for doctests (https://github.com/Frozenball/pytest-sugar/issues/134). It was bisected to https://github.com/pytest-dev/pytest/commit/fbc45be83f279f936121355649728f7aeec6e6a6 (https://github.com/Frozenball/pytest-sugar/issues/134#issuecomment-406067245) - /cc @will133.

blueyed commented 5 years ago

This is due to assertion rewriting, with --assert=plain pytest also reports line 5.

z00sts commented 4 years ago

Probably, bug is here: https://github.com/pytest-dev/pytest/blob/1df593f97890245a8eaaa89444a3ba7bada2a3b0/src/_pytest/assertion/rewrite.py#L891

Rewrite ignores multiline assert statements and uses assert.lineno - first assert line num (reporting do nothing wrong). Assert statement comes in one piece and there is no way for reporter to determine correct line for now. May be we can detect correct substatement line and attach to Assert object for future usage (by reporter a.k.a. error printer).

benoitryder commented 2 years ago

Another effect of what seems to be the same bug: lambda objects used in an assert always use the line of the assert. As a result, functions from the inspect module may return wrong results when used on such lambda.

I don't know whether it's used internally by pytest, but it may be used by the user to log some information. (That's how I stumbled on it.)

For instance, with the following code:

import inspect

def print_and_call(f):
    print(inspect.getsource(f))
    return f()

def test():
    assert print_and_call(
        lambda: True
    )