-
Notifications
You must be signed in to change notification settings - Fork 1.1k
PYTHON-5071 Use one event loop for all asyncio tests #2086
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
Conversation
The PyPy variant failure looks like the same asyncio SSL bug we've been tracking in pypy/pypy#5131. The other failures are all |
I suspect the test_continuous_network_errors errors are a side effect of having too many unopened clients or perhaps too many unawaited SDAM tasks. I removed the logic to reinit the client_context on each test. Let's see if that fixes it. |
I also fixed an issue in loop management which was causing some tests to encounter |
My latest commit fixes this pytest-asyncio error when tearing down the async test suite:
To fix it, we now use scope=session in conftest and bind to pytest's global event loop. This way pytest manages creating and destroying the loop. |
I went back to package scope because I believe that's actually what we want. With "session" scope, the async teardown doesn't happen until after all the sync tests run which causes problems because the client has been paused that whole time. This actually highlights a problem with async MongoClient, if the loop is paused for a while and then resumed the client's SDAM state will be out of date. This is similar to the sigstop/sigcont behavior. |
test/asynchronous/__init__.py
Outdated
async def async_setup(): | ||
if not _IS_SYNC: | ||
global LOOP | ||
LOOP = asyncio.get_running_loop() |
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.
Is this supposed to be get_loop
instead?
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.
No, this is the correct method to call within an async function.
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.
Good catch. I think we still have a loop mismatch here, although I might be misunderstanding: this will use the loop pytest creates for our package-scoped test_setup_and_teardown
fixture. Then, our first async testcase will create its own separate loop that will be used for each test. So our async_client_context
will be on one pytest-managed loop, and all of our test cases will be on one self-managed loop created by get_loop
.
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.
Our pyproject.toml
also sets the default loop_scope to be session
instead of package
FYI.
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.
Either one is fine because get_loop calls get_running_loop first but I updated to use get_loop to be more consistent.
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.
Attempting to use "package" in pyproject.toml results in this error:
____________________________________________________ ERROR at setup of TestCommon.test_mongo_client ____________________________________________________
file /Users/shane/git/mongo-python-driver/test/asynchronous/test_common.py, line 135
async def test_mongo_client(self):
pair = await async_client_context.pair
m = await self.async_rs_or_single_client(w=0)
coll = m.pymongo_test.write_concern_test
await coll.drop()
doc = {"_id": ObjectId()}
await coll.insert_one(doc)
self.assertTrue(await coll.insert_one(doc))
coll = coll.with_options(write_concern=WriteConcern(w=1))
with self.assertRaises(OperationFailure):
await coll.insert_one(doc)
m = await self.async_rs_or_single_client()
coll = m.pymongo_test.write_concern_test
new_coll = coll.with_options(write_concern=WriteConcern(w=0))
self.assertTrue(await new_coll.insert_one(doc))
with self.assertRaises(OperationFailure):
await coll.insert_one(doc)
m = await self.async_rs_or_single_client(
f"mongodb://{pair}/", replicaSet=async_client_context.replica_set_name
)
coll = m.pymongo_test.write_concern_test
with self.assertRaises(OperationFailure):
await coll.insert_one(doc)
m = await self.async_rs_or_single_client(
f"mongodb://{pair}/?w=0", replicaSet=async_client_context.replica_set_name
)
coll = m.pymongo_test.write_concern_test
await coll.insert_one(doc)
# Equality tests
direct = await connected(await self.async_single_client(w=0))
direct2 = await connected(
await self.async_single_client(f"mongodb://{pair}/?w=0", **self.credentials)
)
self.assertEqual(direct, direct2)
self.assertFalse(direct != direct2)
file /Users/shane/git/mongo-python-driver/test/asynchronous/conftest.py, line 25
@pytest_asyncio.fixture(scope="package", autouse=True)
async def test_setup_and_teardown():
await async_setup()
yield
await async_teardown()
E fixture 'test/asynchronous::<event_loop>' not found
> available fixtures: _session_event_loop, _unittest_setUpClass_fixture_TestCommon, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, event_loop, event_loop_policy, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, test/asynchronous/test_common.py::<event_loop>, test/asynchronous/test_common.py::TestCommon::<event_loop>, test_setup_and_teardown, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, unused_tcp_port, unused_tcp_port_factory, unused_udp_port, unused_udp_port_factory
> use 'pytest --fixtures [testpath]' for help on them.
"session" seems to be the right thing for what we want there.
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 opened pytest-dev/pytest-asyncio#1052 about that exact error, it persists even if you explicitly mark a fixture as package
.
With any pytest-asyncio scope, we'll still have two separate loops. That doesn't seem to be causing test failures, but it's something to be aware of for future debugging 😅
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.
Thanks I subscribed to that issue. However I did verify that the current code only ever uses 1 loop for the whole async test suite. Printing the loop that pytest-asyncio creates shows this: event_loop_fixture_id='_session_event_loop'
which means the asyncio_default_fixture_loop_scope="session"
in pyproject is behaving as expected.
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.
You mean that get_loop
is picking up and using pytest's _session_event_loop
? Or that that loop is separate from the loop the tests use that get_loop
creates?
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.
The first one.
Ready for another look. |
PYTHON-5071 Use one event loop for all asyncio tests