-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Save teardown exceptions for further teardowns #13002
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW such changes must be always accompanied by respective tests.
@@ -214,6 +214,9 @@ def __init__( | |||
# Deprecated alias. Was never public. Can be removed in a few releases. | |||
self._store = self.stash | |||
|
|||
#: A list of exceptions that happened during teardown | |||
self.teardown_exceptions: list[BaseException] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be a list and not an exception group?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to use a list then pass the list to the exception group, you can't append things to a group
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I didn't realize they were immutable..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guys, where to put tests? Can u suggest appropriate place? Looks like: testing/test_collection.py and teardown?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Merged, thank you. Now we have a test to check if the fixture fails and exception is added to the list
test: tests for teardown exceptions patch
for more information, see https://pre-commit.ci
Looks like we need reviewers: @nicoddemus @daara-s @graingert @Zac-HD @bluetech @jakkdl could you please review this improvement? thanks) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - just a small change in the test and needs a changelog (see contributing docs)
testing/acceptance_test.py
Outdated
def pytest_exception_interact(node, call, report): | ||
sys.stderr.write("{}".format(node.teardown_exceptions)) | ||
|
||
import pytest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor - importing pytest twice in this conftest block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems fine, just a couple small things.
if len(node.teardown_exceptions) == 1: | ||
exceptions.extend(node.teardown_exceptions) | ||
elif node.teardown_exceptions: | ||
msg = f"errors while tearing down {node!r}" | ||
exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1])) | ||
exceptions.append( | ||
BaseExceptionGroup(msg, node.teardown_exceptions[::-1]) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is perhaps something for another PR (or too late/not worth changing), but from trio's experience with strict_exception_groups
it's generally a bad design philosophy to sometimes return an exception group. This leads users to write code where they assume there's only ever one or no exceptions, and everything breaks when >1 exceptions happen to occur.
Maybe it's less of an issue here, given that we're not raising the exceptions, but I could see this complicating parsing logic nevertheless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're raising them a few lines later, after all teardowns. However, this is a design change, and probably should be done in another PR
src/_pytest/runner.py
Outdated
these_exceptions = [] | ||
node.teardown_exceptions = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this need to reinitialize the list? Shouldn't that already be done in Node.__init__
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As there's no other code interacting with this list, you are correct - this is not really needed
testing/acceptance_test.py
Outdated
def pytest_exception_interact(node, call, report): | ||
sys.stderr.write("{}".format(node.teardown_exceptions)) | ||
|
||
import pytest | ||
@pytest.fixture | ||
def mylist(): | ||
yield | ||
raise AssertionError(111) | ||
""", | ||
test_file=""" | ||
def test_func(mylist): | ||
assert True | ||
""", | ||
) | ||
result = pytester.runpytest() | ||
assert result.ret == ExitCode.TESTS_FAILED | ||
assert "AssertionError(111)" in result.stderr.str() | ||
result.stdout.fnmatch_lines(["*1 error*"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this looks a bit unreliable to me, assert "AssertionError(111)" in result.stderr.str()
could plausibly pass for other reasons than pytest_exception_interact
printing it. Printing some marker characters surrounding it should suffice though, e.g. assert "node.teardown_exceptions: `AssertionError(111)`" in result.stderr.str()
.
It's also better to use result.assert_outcomes(errors=1)
than fnmatch'ing lines
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've put this stuff because there is unresolved one #9909
btw @jakkdl please, share you mind on that: a8e0aba#diff-c2ee4e81db455299781df03980cd131ee4680fa5b6e5e484f3355dbeef8452c6R1611
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing to comment, the PR doesn't touch the pytest architecture.
oh actually, probably nice to add to the comment that it's only saved for the sake of external post-teardown inspection; and not required internally. If there's any docs for the |
Guys, are we waiting for something?) Looks like we did all stuff from reviewers |
it's usually good habit to wait a couple days to see if anybody else wants to review, though this is minor enough that I feel comfortable merging it soon |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good, I wonder though if keeping those exceptions around might cause more memory to be used than expected?
Perhaps not given those are only really related to exceptions during teardown...
An alternative for this, in case this is a problem, would be to implement a new hook, and call that hook passing the node and exception (or exceptions?), instead of storing them in the Node.
I'm not sure if this is needed, just food for thought in case storing the exceptions is something we should be more careful about.
@@ -0,0 +1 @@ | |||
Teardown fixtures now can access the information about current teardown exceptions in `node.teardown_exceptions`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit confusing as there are no "teardown fixtures", perhaps:
Teardown fixtures now can access the information about current teardown exceptions in `node.teardown_exceptions`. | |
Fixtures now can access the information about current teardown exceptions in `node.teardown_exceptions` during their own teardowns. |
Keeping the exceptions alive keeps the tracebacks alive which keeps all the locals in all the frames alive |
Indeed, that's why I'm bringing this up. Initially I was thinking this might not be a big problem, but actually this might really be catastrophic in case you have a fixture which is used a lot (say To be safe it is probably best to introduce a new hook, which would circumvent the problem and also allow plugins to handle the exceptions as they appear (possibly handling them immediately or storing them for later, depending on their need). |
nicoddemus agree, but, what if we rewrite |
What do you mean? Doesn't that require storing the exceptions anyway, even if we deliver those later as an iterator?
Might be better to create a new one, and close this one but keeping it on the repository for historical reasons. |
What about a hook? Doesn't it meanning we need to store same thing but in another place? Confused a little bit. |
No, using a hook, instead of storing the exception like this: try:
fin()
except TEST_OUTCOME as e:
node.teardown_exceptions.append(e) We just call the hook at that point, passing the exception: try:
fin()
except TEST_OUTCOME as e:
self.config.ihook.pytest_handle_teardown_exception(item=item, e=e) So pytest itself will no longer long term store the tracebacks, and whoever implements that hook can decide what to do (they might want to store the tracebacks anyway, but then it is the plugin's decision). Of course we will need to restore the Using a hook like this to forward information to plugins is central to pytest's design, and used a lot: for example test reports are not stored anywhere, they are passed to hooks, which are then processed by plugins, like the terminal and junitxml. |
This PR adds an ability to access the exceptions that happened during teardown in other teardowns.
Motivation: some plugins (playwright-pytest) must rely on current teardown status to properly execute their hooks. Currently the teardown information isn't available, so it must be extracted with different workarounds.
Doesn't change functionality, only exposes the local variable on Node object.
Allows proper implementation of microsoft/playwright-pytest#207, not relying on traceback hacks but getting the information directly.