-
-
Notifications
You must be signed in to change notification settings - Fork 435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
coverage of multiline generator/set/dict comprehensions is wrong when run with pytest's assertion rewriting #515
Comments
Original comment by Andy Freeland (Bitbucket: rouge8, GitHub: rouge8) Included the output in the gist, but that test is fully covered if running pytest without assertion rewriting, |
Original comment by Artem Dayneko (Bitbucket: jamert, GitHub: jamert) Issue is reproducible under 2.7.12, 3.3.6, 3.4.5 (and all supported pypy's - 2.4, 2.6, 3-2.4). Under 3.5.2 missing coverage is not observed. With assert rewriting results looks like:
Without assert rewriting (
One thing that is strange to me is that branch count is not zero.
FYI: under python 3.5.2 where issue is not observed, branch count is 4, not 5 (all covered) |
Original comment by Andy Freeland (Bitbucket: rouge8, GitHub: rouge8)
The branches here are the possibility of exit before the generator is evaluated. In Python 2, set/dict comprehensions are implemented as generators I believe, so won't necessarily be evaluated, vs. list comprehensions which always are. If you look at the HTML coverage, the partial lines are annotated "line N didn't finish the set comprehension on line N" |
Original comment by Scott Colby (Bitbucket: scolby33, GitHub: scolby33) I'd like to +1 this issue with what I think is a similar situation (Python 2.7.12 only, I haven't yet tried to reproduce with 3.5):
this reports a missed branch and "line 44 didn't run the list comprehension on line 44" or similar. Replacing with a generator epxression:
reports full coverage. Messing with |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) Added a bug report to pytest in case the problem can be fixed on this end. |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) Here is a minimal reproducer with test_foo.py as
using python-3.5 or python-2.7.12 both create a .coverage file containing
With --assert=plain the content of the .coverage file is different when using python-2.7.12 but remains the same with python-3.5
|
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) Could it be that starting from python-3.5 sys.settrace claims that all event == line that belong to a multiline assert are from the first line of the assert ? If pytest rewrites assertions as if they were on a single line even if they are multiline, it would explain why it only breaks python versions before 3.5. It's just a theory at this point ;-) |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) For the record, updated the pytest issue with a theory. |
Original comment by Bruno Oliveira (Bitbucket: nicoddemus, GitHub: nicoddemus) Hi @dachary, Here's the rewritten AST of your examplem, courtesy of pytest-ast-back-to-python:
(I have the same AST in Python 2.7 and Python 3.4) (Also note that to reproduce this, you either have to use pytest~=2.8 or after use the branch from this PR). It seems to me the rewritten expression is correct, not sure what could be the problem here. |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) The test_foo.py file is below and virtualenv is set with python-2.7.12 and pytest-2.8.7 and coverage.py at 24aff3d7bfd5 (bb)
This could be explained because pytest rewrites the assert as a single line instead of multiline. The content of the .coverage file is created from line events sent via sys.settrace to the coverage.py trace function and reflect the code re-written by pytest. However, coverage report compiles the unmodified source and finds that some lines are not covered. |
Original comment by Bruno Oliveira (Bitbucket: nicoddemus, GitHub: nicoddemus)
This makes sense, but wouldn't this type of assert also cause the same problem? def func_call(): return 1
def func_call2(): return 1
def test_foo():
assert func_call() == \
func_call2() ? Pytest will break each operand into its own statement in that case as well:
|
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) Hum, you're right.
does not cause any problem although line 9 does not show at all in the .coverage file.
|
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) I think it may simply be because the coverage.py parser does not recognize
as a multiline expression as it should. |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) I should have paid more attention to the original description that mentions the HTML message "line N didn't finish the set comprehension on line N" and try to figure out why it happens |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) The HTML message is associated with the line when the AST is analyzed (and as shown above, pytest does not modify the AST when rewriting). When the HTML report is created, this message is displayed because the line is reported to not be covered. Therefore it does not convey more information than the summary report stating the line is not covered. |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) pytest indeed assigns the line number of the rewritten assertion to the first line number of the assertion. However, since coverage.py recognize the assertion as a multiline expression, it should not make a difference. |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary)
works because it does not contain any branch and coverage.py does not care about the last line. It records the assert as a single line which, if not run, indicates the function never returned (see arc 8, -7 below and notice the absence of arc involving line 9 which is the call to func_call2):
By contrast the assert from the original bug description contains arcs and coverage.py expects them to be run (see the arcs -3, 3 and 3, -3 which are about the second part of the assert):
|
I'm not following all the details here, but I love that you are digging into it! When you have something for me to do, leave a clear message telling me what it is! |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) @nedbat I think it happens because multiline re-numbering does not happen for negative line numbers and it could be fixed with
Without this patch the (3,-3) and (-3,3) arcs are renumbered (2,-3) and (-3,2) which is incorrect because there is no arc between line 2 and line 3. With this patch the (3,-3) and (-3,3) arcs are renumbered (2,-2) and (-2,2) and the code coverage is correct. Do you think it could be the root cause of the problem ? If you're not sure I can try to explain why it accounts for all the symptoms. |
Original comment by Loic Dachary (Bitbucket: dachary, GitHub: dachary) During coverage report the arcs found by analysing the AST tree of a source file should show one line per multiline statement because they are remapped to the first line of the statement. However, the first_line function relies on the multiline data member that only remaps positive line numbers. For example, the set comprehension at line 3 in
is added as two arcs as (3, -3) and (-3, 3) as shown by
The arcs are then renumbered by _analyze_ast and
becomes
As if the (-3, 2) and (2, -3) arcs existed, although they do not. When the arcs_missing function compares that to the actual arcs collected by coverage run which are stored in the .coverage file:
and concludes that the following arcs have not be executed
Note: this can be verified by printing the local variables of the arcs_missing function. |
Interestingly, list comprehensions behave in the opposite way: def test_set_comprehension():
# covered!
assert {i for i in range(10)} == {i for i in range(10)}
# "didn't finish the set comprehension"
assert {i for i in range(10)} == {
i for i in range(10)
}
# covered!
assert True
def test_list_comprehension():
assert [i for i in range(10)] == [i for i in range(10)]
# "didn't finish the list comprehension"
assert [i for i in range(10)] == [
i for i in range(10)
]
# covered!
assert True In |
This now shows up in even more cases with |
We are about to revert the unroll functionality for |
Created, type hinted, and tested a Star Wars character DOB function using the SWAPI api. The function searches for DOB using the name (or substring of name) and returns the DOB if name is found. I tested the function including mocking the exceptions and requests.get. I had to add a pragma to the next call on the generator expression as coverage though that variable prior to exit immediately. Seems to be a bug related to this issue nedbat/coveragepy#515.
Originally reported by Andy Freeland (Bitbucket: rouge8, GitHub: rouge8)
Code/config to reproduce available as a gist. Fails on Python 2.7 but not Python 3.5.
Essentially, given this test:
When run under pytest with assertion rewriting (the default), the multiline set comprehension is reported as partially covered, even though the comprehension on oneline is fully covered. I think this is a bug in coverage, not pytest's assertion rewriting, because this code passes:
The text was updated successfully, but these errors were encountered: