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

Fail test if there are unhandled exceptions or pending tasks. #60

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Kentzo
Copy link
Contributor

@Kentzo Kentzo commented Nov 21, 2017

This is a backport from @GreatFruitOmsk's internal async_unittest written by @amezin.

@Martiusweb
Copy link
Owner

Hi, this is a nice feature. I think that the best way to integrate it in asynctest is to use it through the @fail_on decorator:
http://asynctest.readthedocs.io/en/latest/asynctest.case.html#asynctest.case.asynctest.fail_on

That would allow users to deactivate this check when they need to: for instance, when a user overrides the default exception handler to centralize the way common exceptions are handled in its asyncio app.

If you want to update your PR and need some help doing so, don't hesitate to ask for help. I added some documentation in the _fail_on module.

@Kentzo
Copy link
Contributor Author

Kentzo commented Dec 5, 2017

@Martiusweb For this feature it's important to call gc.collect() after event loop is closed.

The _tearDown method is called to soon for that. What would you suggest?

@RemiCardona
Copy link
Contributor

My experience with gc tells me that adding a call to collect() is a bad idea.

First of all, I think this would be quite unexpected by a lot of users. Not to mention the potential effect of repeatedly calling this function on performance.

Second of all, relying on collect() feels inherently wrong to me, as the GC is an implementation detail of CPython.

Maybe there's a way to track the futures and tasks coming out of the event loop and check them at the end of the test? Subclassing? Just thinking "out loud" really.

My 2¢

Rémi

@Kentzo
Copy link
Contributor Author

Kentzo commented Dec 8, 2017 via email

@Kentzo
Copy link
Contributor Author

Kentzo commented Dec 8, 2017

On the second thought, it can be removed and users can run it manually on demand.

What has to be fixed is an ability to run checks after the loop is closed and destroyed.

@Martiusweb
Copy link
Owner

Hi, I'm quite busy this month, and I can't find enough time to correctly review the opened PRs, thus, sorry for the delays.

If you think that we should rework @fail_on to be able to perform some checks at a different time, I'm OK with this. If you need to ensure that the loop has been destroyed, I think it's doable with a finalizer (https://docs.python.org/3/library/weakref.html#weakref.finalize). However, the checks must always be triggered inside the test case, which means that we probably need to ensure that there are no other references to the loop in user's code... which is a pretty big assumption. On the other hand, I'm not sure that we really need this (ensure that the loop is destroyed).

@Kentzo
Copy link
Contributor Author

Kentzo commented Dec 21, 2017

@Martiusweb Destruction is necessary to trigger the check. We could issue a warning if loop is still alive by the end of the test.

@Kentzo
Copy link
Contributor Author

Kentzo commented Aug 15, 2018

@Martiusweb Any thoughts regarding that improvement?

@Martiusweb
Copy link
Owner

I really think this should be implemented with @fail_on:

That would allow users to deactivate this check when they need to: for instance, when a user overrides the default exception handler to centralize the way common exceptions are handled in its asyncio app.

I also have concerns with the use of gc.collect(): how does collecting a pending task raises an exception? I don't think we should rely on this.

Maybe this PR should be split in two:

  • detecting unhandled exceptions using fail_on by overriding the exception handler (as done in this PR)
  • an other fail_on check detecting pending tasks by looking at what's left in asyncio.Task.all_tasks()

@Kentzo
Copy link
Contributor Author

Kentzo commented Aug 17, 2018

I also have concerns with the use of gc.collect(): how does collecting a pending task raises an exception? I don't think we should rely on this.

These are details of CPython's asyncio implementation: The error is only triggered upon object's destruction with a pending exception.

Alternatively one could go over all the tasks and check for the _exception attribute, but that seems to rely even more on the implementation detail compared to mere object's lifetime.

Perhaps @1st1 or @vstinner could give us an advice.

@vstinner
Copy link

vstinner commented Aug 20, 2018 via email

@1st1
Copy link

1st1 commented Aug 20, 2018

I'd just iterate over all tasks using asyncio.all_tasks(). Check if task is done or not; if not - wait for it; if yes, check if it has failed.

@Kentzo
Copy link
Contributor Author

Kentzo commented Aug 20, 2018

Not sure I understood @vstinner comment.

We are trying to come up with a solution to check if there are tasks with pending exceptions by the time the test is done.

One way is to rely on CPython’s implementation that calls loop’s unhandeled exception handler upon Task’s deletion if it has a pending exception.

@Kentzo
Copy link
Contributor Author

Kentzo commented Aug 20, 2018

@1st1 We do not control the flow within a test case. Tasks with unhandled exceptions can be deleted before the execution will reach our code. These deleted tasks won't appear in all_tasks.

@1st1
Copy link

1st1 commented Aug 20, 2018

Another idea would be to catch all warnings, match them, and turn them into an error if they're coming from an asyncio task.

@Kentzo
Copy link
Contributor Author

Kentzo commented Aug 21, 2018

But again, warnings are issued upon object's destruction which may or may not happen and therefore usage of gc.collect is inevitable.

@1st1
Copy link

1st1 commented Aug 21, 2018

Well, yes, set up a custom warnings filter, and run gc.collect() after the test; record any event of logging a warning in Task.__del__, fail the test if that happens.

@Kentzo
Copy link
Contributor Author

Kentzo commented Aug 21, 2018

@1st1 Pushed one more attempt of an implementation that avoids gc.collect.

One issue though is that context passed to custom exception handler does not receive source_traceback. Neither Task._log_traceback is set.

When set, tests will fail if there are unhandled exceptions.
@Martiusweb
Copy link
Owner

I don't understand what can be captured via warnings.

The first solution we're exploring is relying on a custom exception handler in the loop and relying on gc.collect() to ensure the exception handler is called. I'm concerned about the portability and sustainability of this solution because I don't know if we can rely on gc.collect() on other implementations of python (in particular pypy). I also don't know if we should assume this behavior in other implementations of asyncio (like uvloop).

In fact, asynctest already assumes a lot about internals of the implementation of the loop, which is something I'd like to take care of (one day...).

Alternatively, we can create a wrapper around the task to track pending exceptions. Asynctest would hook itself at a higher level, which seems safer. The main downside I see is that it's only available since 3.4.4 (we can test if the feature is available and raise a warning if not).

@Martiusweb Martiusweb force-pushed the master branch 3 times, most recently from c2e9ce1 to 967481c Compare April 3, 2019 15:31
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.

5 participants