Skip to content
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

first draft of handling marker exceptions wrapped in exceptiongroup #4110

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

jakkdl
Copy link

@jakkdl jakkdl commented Sep 19, 2024

I have barely used Hypothesis, and never worked with the codebase, so there are several gaps in code coverage and question marks on implementation decisions. If somebody more familiar wants to take a quick pass and opine it'd be very helpful :)

I can get code coverage by manually raising the specific ExceptionGroups that'll trigger the different branches, but it'd likely be better to do the actual things that causes those exceptions. So would likewise appreciate quick pointers on how to, if at all possible, cause e.g. multiple StopTest, multiple Frozen, a single Frozen, etc.

fixes #4106

Copy link
Contributor

@jobh jobh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, thanks! 👍

I think most of the build errors will go away with some of the minor commented changes, the remaining will hopefully be manageable :-)

hypothesis-python/src/hypothesis/core.py Outdated Show resolved Hide resolved
# multiple frozen exceptions
# TODO: raise a group? The first one? None of them?
raise frozen_exceptions[0] from excgroup

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the semantics here (generally) are a good question.

I'd opine:

  • If all exceptions are Frozen, raise the first (as above).
  • Else, if exactly one exception is non-Frozen, raise that one [*].
  • Otherwise, reconstruct and raise a group with just the non-frozen ones (maybe .with_traceback(eg.__traceback__)).

[*] hm, I see, that would cause an undesired unwrapping if user code raises a single-exception ExceptionGroup. Maybe the second step should be "if exactly one ... and that exception is a StopException or HypothesisException.

Copy link
Author

@jakkdl jakkdl Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all exceptions are Frozen, raise the first (as above).

Actually, I think this could cause confusion. If I spawn three tasks and I only get one Frozen, I will assume that the-thing-that-caused-Frozen only happened in one of the tasks. We should probably err on the side of not unwrapping unless necessary to get internal exception handling to work.

edit: nvm, I thought frozen didn't get swallowed by @given

edit2: I'm confused about how hypothesis works 😅

edit3: Aaaahhh.

except failure_exceptions_to_catch() as e:
# If an unhandled (i.e., non-Hypothesis) error was raised by
# Hypothesis-internal code, re-raise it as a fatal error instead
# of treating it as a test failure.
filepath = traceback.extract_tb(e.__traceback__)[-1][0]
if is_hypothesis_file(filepath) and not isinstance(e, HypothesisException):
raise
if data.frozen:
# This can happen if an error occurred in a finally
# block somewhere, suppressing our original StopTest.
# We raise a new one here to resume normal operation.
raise StopTest(data.testcounter) from e

This means that if we got our data frozen, any exceptions (no matter if inside a group or not) get disregarded. So it doesn't matter what we do if there's only frozen exceptions

edit4: and this is very bad, because it can suppress TypeError and anything. Gotta rejig some logic here

edit5: except that's what the code currently does in sync code, so... ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question is if we get multiple HypothesisException/StopException, if that's possible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we get multiple StopTest, re-raise the one with the lowest _testcounter i.e. the first one to occur.

def test_single_exc_group(data):
raise ExceptionGroup("important message for user", [ValueError()])

with RaisesGroup(ValueError, match="important message for user"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just catch BaseExceptionGroup (via hypothesis.internal.compat.BaseExceptionGroup) and check the subexceptions explicitly, to avoid the trio dependency.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ye that's probably the way to go. I'm incredibly biased on the matter as the person who wrote trio.testing.RaisesGroup and is trying to get it added to pytest 😇

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(and as a dual maintainer of Pytest and Trio, among other things, I'm pretty keen to support that when I have time 😅)

@jobh
Copy link
Contributor

jobh commented Sep 19, 2024

I can get code coverage by manually raising the specific ExceptionGroups that'll trigger the different branches, but it'd likely be better to do the actual things that causes those exceptions. So would likewise appreciate quick pointers on how to, if at all possible, cause e.g. multiple StopTest, multiple Frozen, a single Frozen, etc.

It should be possible to provoke most of these situations by playing around with multiple tasks failing in different ways. But I think it's fine to create the non-obvious situations explicitly, too.

@jobh
Copy link
Contributor

jobh commented Sep 19, 2024

It should be possible to provoke most of these situations by playing around with multiple tasks failing in different ways. But I think it's fine to create the non-obvious situations explicitly, too.

Sorry: "most" should probably be replaced by "some" 😁

@jobh
Copy link
Contributor

jobh commented Sep 19, 2024

Oh, and one other tip: hypothesis will transform/trim the tracebacks by default, which may be confusing sometimes when working on exceptions and not seeing what you'd expect. See get_trimmed_traceback().

@jakkdl
Copy link
Author

jakkdl commented Sep 24, 2024

I can get code coverage by manually raising the specific ExceptionGroups that'll trigger the different branches, but it'd likely be better to do the actual things that causes those exceptions. So would likewise appreciate quick pointers on how to, if at all possible, cause e.g. multiple StopTest, multiple Frozen, a single Frozen, etc.

It should be possible to provoke most of these situations by playing around with multiple tasks failing in different ways. But I think it's fine to create the non-obvious situations explicitly, too.

actually, make that none of the situations if we 1. Want tests to run on py<3.11 and 2. not add a dependency on trio or anyio.
asyncio.TaskGroup wasn't added until py3.11, and afaik nothing in the stdlib will raise ExceptionGroup before that.

@Zac-HD
Copy link
Member

Zac-HD commented Sep 24, 2024

I think for testing we'll want /cover/ tests which do whatever explicit super-weird code we need (we have ExceptionGroup), and also some /nocover/ tests which only run on py311+ and use asyncio.TaskGroup in a somewhat-more-natural way. Then when 310 goes end of life, we can promote them over to replace the weird cover tests.

@jakkdl jakkdl requested review from jobh and Zac-HD September 27, 2024 10:34
Copy link
Member

@Zac-HD Zac-HD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(partial review is better than no review, right?)

I've also merged a fix for #4115 so you might want to rebase on master 🙂

Comment on lines +1139 to +1141
# TODO: this should maybe inspect the stacktrace to see that the above
# mentioned story is true? I.e. reraise as StopTest iff there is a
# StopTest somewhere in the tree of e.__context__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alas, raise Whatever() from None would defeat this inspection.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would only override the __cause__ no? And the comment specifically talks about errors happening in finally, where you rarely raise explicit exceptions.
It seems pretty dangerous to ignore all exceptions in case the data is frozen, shouldn't the user be informed if there's errors happening in their teardown code?
Although thinking about it more, the problem is perhaps that the StopTest.__cause__ is not getting reraised/logged?

@@ -9,6 +9,7 @@
# obtain one at https://mozilla.org/MPL/2.0/.

"""This module provides the core primitives of Hypothesis, such as given."""
from __future__ import annotations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Hypothesis does a lot of runtime introspection, we prefer to avoid from __future__ import annotations - which will eventually go away anyway, c.f. PEPs 649 and 749.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you also want to avoid it in test files?

@jakkdl
Copy link
Author

jakkdl commented Oct 1, 2024

#4115 made me realize that unwrapping exceptions inside groups is really tricky to do right without losing the context/cause of the original exception. Though I'm unsure if/how much this matters for hypothesis, as we only try to unwrap specific marker exceptions which users wouldn't see anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Our internals should handle StopTest and other marker exceptions even when wrapped in an ExceptionGroup
3 participants