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

Add cleanup code to event_loop that mimics the behavior of asyncio.run #309

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

Askaholic
Copy link

This is something I found useful and implemented in one of my own projects so I thought I'd make a PR in case the maintainers think this would be a good addition to the library itself. I'd be happy to modify it if only a subset of the changes is desired.

Some issues that I ran into with the current implementation:

  • Errors that occur in a task spawned during one test can leak into subsequent tests since simply calling loop.close() is not sufficient to shutdown the event loop. This can make it extremely difficult to track down which test is causing the errors, since the test that fails is not actually the one that has the problem.
  • When running with -W error under python 3.9 a similar problem happens with ResourceWarnings, where it becomes extremely difficult to figure out which test is actually opening the resources that are generating the warnings since the GC doesn't actually clean them up until some later test is already running.

Closes #200

loop.close()
# Cleanup code copied from the implementation of asyncio.run()
try:
asyncio.runners._cancel_all_tasks(loop)
Copy link
Author

Choose a reason for hiding this comment

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

Calling private functions here is a bit sketch, but it's what I chose to do in my project instead of copy/pasting the entire implementation. Might want to go with the copy/paste approach though for this library.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for the late response… There are a couple of ideas flying around, all of which address this issue in some way. That makes it hard to review your PR, so I put it off for too long.

I agree that calling _cancel_all_tasks might not be a good idea. Although we test with different CPython versions, _cancel_all_tasks is an implementation detail that could change in a patch release. Since we only test different minor releases (i.e. 3.7, 3.8, …) a change of _cancel_all_tasks could slip through unnoticed.

It would be best to replicate the behaviour of _cancel_all_tasks in pytest-asyncio. We cant't do a 1:1 copy and paste, though, because the CPython code is protected by copyright and under a different license than pytest-asyncio.

Copy link
Contributor

Choose a reason for hiding this comment

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

I still stand by my last comment on the use of _cancel_all_tasks.

@seifertm
Copy link
Contributor

Thanks for the contribution!

The linked issue is not the only request of that feature. There is also #222. We closed #222 in favour of #235, which intends to used aioloop-proxy to have a "virtualized" event loop.

The aioloop-proxy issue has not been started, as far as I know. I think it's a good idea to add clean up code for outstanding tasks independently of #235.

Could you add some tests that assert the correct behaviour of your code?

@Askaholic
Copy link
Author

Great! I've read through the issues you linked and I think I understand the current state of this feature. Sounds like eventually a more sophisticated solution will be added, but for now this PR will be useful. I'll get the development environment set up and work on writing a few tests!

@asvetlov
Copy link
Contributor

Regarding #235 (aioloop-proxy based solution).
I have a work-in-progress draft: #312
It is not finished yet, sorry. Now I'm working on improving asyncio itself, Python 3.11 feature freeze is coming.

pytest_asyncio/plugin.py Outdated Show resolved Hide resolved
tests/test_event_loop_cleanup.py Show resolved Hide resolved
@seifertm
Copy link
Contributor

seifertm commented Apr 9, 2022

Thanks for uploading the latest version. I had some time to look into the issue that causes ResourceWarnings in the tests.

The culprit seems to be test_event_loop_scope.py::test_4. The test case requests the event_loop fixture and sets the current global loop to None. Requesting event_loop initializes a new loop. The code in pytest_fixture_post_finalizer tries to retrieve the current loop via asyncio.get_event_loop_policy().get_event_loop(), but that reference was set to None in the test case. The result is a stale event loop that triggers a ResourceWarning about an unclosed socket.

I was trying to work around the issue by using fixturedef.cached_result in pytest_fixture_post_finalizer to retrieve the loop provided by the event_loop fixture. Until now that wasn't successful, though. I'll keep digging…

@codecov-commenter
Copy link

Codecov Report

Base: 93.81% // Head: 93.23% // Decreases project coverage by -0.57% ⚠️

Coverage data is based on head (5024eb8) compared to base (5697df2).
Patch has no changes to coverable lines.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #309      +/-   ##
==========================================
- Coverage   93.81%   93.23%   -0.58%     
==========================================
  Files           3        3              
  Lines         275      281       +6     
  Branches       41       43       +2     
==========================================
+ Hits          258      262       +4     
- Misses         11       12       +1     
- Partials        6        7       +1     
Impacted Files Coverage Δ
plugin.py 93.11% <0.00%> (-0.59%) ⬇️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report at Codecov.
📢 Do you have feedback about the report comment? Let us know in this issue.

@seifertm
Copy link
Contributor

I modified test_event_loop_scope.py::test_4 accordingly to avoid side-effects in other tests. The PR has been rebased and two typing issues have been addressed.

# as possible (these are triggered in various __del__ methods).
# Without this, resources opened in one test can fail other tests
# when the warning is generated.
gc.collect()
Copy link
Contributor

Choose a reason for hiding this comment

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

A bunch of people implement their own event_loop fixture, in order to control fixture scope. If we change the fixture definition, we'd have to inform them to adjust as well.

Asking users to remove loop.close() to benefit from the new cleanup code is an easy sell. Adding a call to gc.collect in the event_loop fixture not so much.

Is there away around the gc.collect call in the fixture code?

Copy link
Author

Choose a reason for hiding this comment

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

I suppose the call to gc.collect isn't directly related to task cleanup. I added this in my codebase because there were some tests that opened sockets and never closed them. Having the garbage collector run during the test teardown would make sure that the correct test failed when running pytest with -W error. But this is unrelated to pytest-asyncio so maybe it doesn't belong here.

@elupus
Copy link

elupus commented Nov 27, 2022

I've run into the issue of tasks leaking over test bounderings when i tried to get home assistant updated to later pytest-asyncio: So far i have these fixes/hacks: I've not managed to get it to run reliable as of yet thou.

Cleaning up of lingering tasks:
home-assistant/core@6e9130d

Cleaning up of lingering timers:
home-assistant/core@4d1335f

@elupus
Copy link

elupus commented Nov 27, 2022

I also just noticed that aiohttp loop fixture did trigger a GC call after test finishes: https://github.com/aio-libs/aiohttp/blob/905e0625777c20e4cf2d57fcfd4355827312f797/aiohttp/test_utils.py#L525

That likely explains some of my failures.

elupus added a commit to elupus/home-assistant that referenced this pull request Nov 29, 2022
Some test will trigger warnings on garbage collect, these warnings
spills over into next test.

Some test trigger tasks that raise errors on shutdown, these spill
over into next test.

This is to mimic older pytest-aiohttp and it's behaviour on test
cleanup.

Discussions on similar changes for pytest-aiohttp are here:
pytest-dev/pytest-asyncio#309
elupus added a commit to home-assistant/core that referenced this pull request Nov 29, 2022
* Upgrade pytest-aiohttp

* Make sure executors, tasks and timers are closed

Some test will trigger warnings on garbage collect, these warnings
spills over into next test.

Some test trigger tasks that raise errors on shutdown, these spill
over into next test.

This is to mimic older pytest-aiohttp and it's behaviour on test
cleanup.

Discussions on similar changes for pytest-aiohttp are here:
pytest-dev/pytest-asyncio#309

* Replace loop with event_loop

* Make sure time is frozen for tests

* Make sure the ConditionType is not async

  /home-assistant/homeassistant/helpers/template.py:2082: RuntimeWarning: coroutine 'AsyncMockMixin._execute_mock_call' was never awaited
    def wrapper(*args, **kwargs):
  Enable tracemalloc to get traceback where the object was allocated.
  See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.

* Increase litejet press tests with a factor 10

The times are simulated anyway, and we can't stop the normal
event from occuring.

* Use async handlers for aiohttp

tests/components/motioneye/test_camera.py::test_get_still_image_from_camera
tests/components/motioneye/test_camera.py::test_get_still_image_from_camera
tests/components/motioneye/test_camera.py::test_get_stream_from_camera
tests/components/motioneye/test_camera.py::test_get_stream_from_camera
tests/components/motioneye/test_camera.py::test_camera_option_stream_url_template
tests/components/motioneye/test_camera.py::test_camera_option_stream_url_template
  /Users/joakim/src/hass/home-assistant/venv/lib/python3.9/site-packages/aiohttp/web_urldispatcher.py:189: DeprecationWarning: Bare functions are deprecated, use async ones
    warnings.warn(

* Switch to freezegun in modbus tests

The tests allowed clock to tick in between steps

* Make sure skybell object are fully mocked

Old tests would trigger attempts to post to could services:

```
DEBUG:aioskybell:HTTP post https://cloud.myskybell.com/api/v3/login/ Request with headers: {'content-type': 'application/json', 'accept': '*/*', 'x-skybell-app-id': 'd2b542c7-a7e4-4e1e-b77d-2b76911c7c46', 'x-skybell-client-id': '1f36a3c0-6dee-4997-a6db-4e1c67338e57'}
```

* Fix sorting that broke after rebase
@matzipan
Copy link

matzipan commented Jun 9, 2023

So are the open threads the only things left for this to go forward? Or is there something else missing? I'll try to find some time for this.

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.

Clean up pending tasks when event_loop fixture leaves scope
6 participants