From 46bd61e601b214ec0d4858001a8d7d6b2df011f1 Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 17:09:13 +0800 Subject: [PATCH 01/11] concurrent fixtures setup and teardown --- pytest_trio/plugin.py | 57 +++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 1a56a83..2de9ebc 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -161,8 +161,11 @@ def __init__(self, name, func, pytest_kwargs, is_test=False): self._func = func self._pytest_kwargs = pytest_kwargs self._is_test = is_test - self._teardown_done = trio.Event() - + # Previous and next fixture in terms of fixture topological order + # During set up, each fixture waits for previous_fixture to set its setup_done value + self.previous_fixture = None + # During tear down, each fixture waits for next_fixture to set its teardown_done value + self.next_fixture = None # These attrs are all accessed from other objects: # Downstream users read this value. self.fixture_value = None @@ -170,21 +173,32 @@ def __init__(self, name, func, pytest_kwargs, is_test=False): # Invariant: if this is set, then either fixture_value is usable *or* # test_ctx.crashed is True. self.setup_done = trio.Event() - # Downstream users *modify* this value, by adding their _teardown_done - # events to it, so we know who we need to wait for before tearing - # down. - self.user_done_events = set() + # This event notifies downstream users that we're done setting up. + # Same invariant as setup_done + self.teardown_done = trio.Event() - def register_and_collect_dependencies(self): + def collect_dependencies(self): # Returns the set of all TrioFixtures that this fixture depends on, - # directly or indirectly, and sets up all their user_done_events. - deps = set() - deps.add(self) + # directly or indirectly, in a topological order + deps = [] for value in self._pytest_kwargs.values(): if isinstance(value, TrioFixture): - value.user_done_events.add(self._teardown_done) - deps.update(value.register_and_collect_dependencies()) + for indirect_dependency in value.collect_dependencies(): + if indirect_dependency not in deps: + deps.append(indirect_dependency) + deps.append(self) return deps + + def collect_register_dependencies(self): + # Collect dependencies then register previous and next fixture + # for each fixture (in terms of its topological order) + deps = self.collect_dependencies() + for index, dep in enumerate(deps): + if index > 0: + dep.previous_fixture = deps[index - 1] + if index < len(deps) - 1: + dep.next_fixture = deps[index + 1] + return deps @asynccontextmanager async def _fixture_manager(self, test_ctx): @@ -199,7 +213,7 @@ async def _fixture_manager(self, test_ctx): test_ctx.crash(self, exc) finally: self.setup_done.set() - self._teardown_done.set() + self.teardown_done.set() async def run(self, test_ctx, contextvars_ctx): __tracebackhide__ = True @@ -219,11 +233,13 @@ async def run(self, test_ctx, contextvars_ctx): # teardone_done event, and crashing the context if there's an # unhandled exception. async with self._fixture_manager(test_ctx) as nursery_fixture: + if self.previous_fixture: + await self.previous_fixture.setup_done.wait() + # Resolve our kwargs resolved_kwargs = {} for name, value in self._pytest_kwargs.items(): if isinstance(value, TrioFixture): - await value.setup_done.wait() if value.fixture_value is NURSERY_FIXTURE_PLACEHOLDER: resolved_kwargs[name] = nursery_fixture else: @@ -244,7 +260,6 @@ async def run(self, test_ctx, contextvars_ctx): if self._is_test: # Tests are exactly like fixtures, except that they to be # regular async functions. - assert not self.user_done_events func_value = None assert not test_ctx.crashed await self._func(**resolved_kwargs) @@ -293,16 +308,16 @@ async def run(self, test_ctx, contextvars_ctx): # about cancellation. yield_outcome = outcome.Value(None) try: - for event in self.user_done_events: - await event.wait() + if self.next_fixture: + await self.next_fixture.teardown_done.wait() except BaseException as exc: assert isinstance(exc, trio.Cancelled) yield_outcome = outcome.Error(exc) test_ctx.crash(self, None) with trio.CancelScope(shield=True): - for event in self.user_done_events: - await event.wait() - + if self.next_fixture: + await self.next_fixture.teardown_done.wait() + # Do our teardown if isasyncgen(func_value): try: @@ -405,7 +420,7 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): contextvars_ctx.run(canary.set, "in correct context") async with trio.open_nursery() as nursery: - for fixture in test.register_and_collect_dependencies(): + for fixture in test.collect_register_dependencies(): nursery.start_soon( fixture.run, test_ctx, contextvars_ctx, name=fixture.name ) From 427f397c749551cf77ae06d13aed7f7216c05095 Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 17:09:54 +0800 Subject: [PATCH 02/11] add unit test for seq fixtures and fix other tests that assumes concurrent --- pytest_trio/_tests/test_fixture_ordering.py | 104 ++++++++++++++------ 1 file changed, 75 insertions(+), 29 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index a180958..41392b3 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -3,8 +3,9 @@ # Tests that: # - leaf_fix gets set up first and torn down last -# - the two fix_concurrent_{1,2} fixtures run their setup/teardown code -# at the same time -- their execution can be interleaved. +# - the two fix_{1,2} fixtures run their setup/teardown code +# in the expected order +# fix_1 setup -> fix_2 setup -> fix_2 teardown -> fix_1 teardown def test_fixture_basic_ordering(testdir): testdir.makepyfile( """ @@ -26,45 +27,45 @@ async def leaf_fix(): teardown_events.append("leaf_fix teardown") assert teardown_events == [ - "fix_concurrent_1 teardown 1", - "fix_concurrent_2 teardown 1", - "fix_concurrent_1 teardown 2", - "fix_concurrent_2 teardown 2", + "fix_2 teardown 1", + "fix_2 teardown 2", + "fix_1 teardown 1", + "fix_1 teardown 2", "leaf_fix teardown", ] @pytest.fixture - async def fix_concurrent_1(leaf_fix, seq): + async def fix_1(leaf_fix, seq): async with seq(0): - setup_events.append("fix_concurrent_1 setup 1") - async with seq(2): - setup_events.append("fix_concurrent_1 setup 2") + setup_events.append("fix_1 setup 1") + async with seq(1): + setup_events.append("fix_1 setup 2") yield - async with seq(4): - teardown_events.append("fix_concurrent_1 teardown 1") async with seq(6): - teardown_events.append("fix_concurrent_1 teardown 2") + teardown_events.append("fix_1 teardown 1") + async with seq(7): + teardown_events.append("fix_1 teardown 2") @pytest.fixture - async def fix_concurrent_2(leaf_fix, seq): - async with seq(1): - setup_events.append("fix_concurrent_2 setup 1") + async def fix_2(leaf_fix, seq): + async with seq(2): + setup_events.append("fix_2 setup 1") async with seq(3): - setup_events.append("fix_concurrent_2 setup 2") + setup_events.append("fix_2 setup 2") yield + async with seq(4): + teardown_events.append("fix_2 teardown 1") async with seq(5): - teardown_events.append("fix_concurrent_2 teardown 1") - async with seq(7): - teardown_events.append("fix_concurrent_2 teardown 2") + teardown_events.append("fix_2 teardown 2") @pytest.mark.trio - async def test_root(fix_concurrent_1, fix_concurrent_2): + async def test_root(fix_1, fix_2): assert setup_events == [ "leaf_fix setup", - "fix_concurrent_1 setup 1", - "fix_concurrent_2 setup 1", - "fix_concurrent_1 setup 2", - "fix_concurrent_2 setup 2", + "fix_1 setup 1", + "fix_1 setup 2", + "fix_2 setup 1", + "fix_2 setup 2", ] assert teardown_events == [] @@ -74,6 +75,52 @@ async def test_root(fix_concurrent_1, fix_concurrent_2): result = testdir.runpytest() result.assert_outcomes(passed=1) +def test_context_vars_modification_follows_fixture_ordering(testdir): + """ + Tests that fixtures are being set up and tore down synchronously. + + Specifically this ensures that fixtures that modify context variables + doesn't lead to a weird contextvar states + + Main assertion is that 2 async tasks in teardown (Resource.__aexit__) + doesn't crash + """ + testdir.makepyfile( + """ + import pytest + from pytest_trio import trio_fixture + import trio_asyncio + import asyncio + import trio + class Resource(): + async def __aenter__(self): + await trio_asyncio.aio_as_trio(asyncio.sleep(0)) + + async def __aexit__(self, *_): + # We need to yield and run another trio task + await trio.sleep(0.1) + await trio_asyncio.aio_as_trio(asyncio.sleep(0)) + + @pytest.fixture + async def resource(): + async with trio_asyncio.open_loop() as loop: + async with Resource(): + yield + + @pytest.fixture + async def trio_asyncio_loop(): + async with trio_asyncio.open_loop() as loop: + yield loop + + @pytest.mark.trio + async def test_root(trio_asyncio_loop, resource): + await trio.sleep(0) + assert True + """ + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) def test_nursery_fixture_teardown_ordering(testdir): testdir.makepyfile( @@ -139,6 +186,9 @@ def test_error_collection(testdir): # hasn't even started yet. Maybe we shouldn't? But for now the sleeps make # sure that all the fixtures have started before any of them start # crashing. + + # Although all fixtures are meant to crash, we only expect the first crash output + # since the teardown are synchronous. testdir.makepyfile( """ import pytest @@ -194,10 +244,6 @@ def test_followup(): result.stdout.fnmatch_lines_random( [ "*CRASH_NONGEN*", - "*CRASH_EARLY_AGEN*", - "*CRASH_LATE_AGEN*", - "*CRASH_BACKGROUND_EARLY*", - "*CRASH_BACKGROUND_LATE*", ] ) From 4a9d7eceb3bce09cdda08de8fd40b276ba06fc4d Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 17:17:36 +0800 Subject: [PATCH 03/11] better test name --- pytest_trio/_tests/test_fixture_ordering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 41392b3..aaa0ae8 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -75,7 +75,7 @@ async def test_root(fix_1, fix_2): result = testdir.runpytest() result.assert_outcomes(passed=1) -def test_context_vars_modification_follows_fixture_ordering(testdir): +def test_contextvars_modification_follows_fixture_ordering(testdir): """ Tests that fixtures are being set up and tore down synchronously. From 8037f11d84e103cbc03cc7acaeaa5354ee670f4d Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 17:17:36 +0800 Subject: [PATCH 04/11] better test name --- pytest_trio/_tests/test_fixture_ordering.py | 15 +++++++------- pytest_trio/plugin.py | 22 +++++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index aaa0ae8..8a2da0e 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -4,7 +4,7 @@ # Tests that: # - leaf_fix gets set up first and torn down last # - the two fix_{1,2} fixtures run their setup/teardown code -# in the expected order +# in the expected order # fix_1 setup -> fix_2 setup -> fix_2 teardown -> fix_1 teardown def test_fixture_basic_ordering(testdir): testdir.makepyfile( @@ -78,7 +78,7 @@ async def test_root(fix_1, fix_2): def test_contextvars_modification_follows_fixture_ordering(testdir): """ Tests that fixtures are being set up and tore down synchronously. - + Specifically this ensures that fixtures that modify context variables doesn't lead to a weird contextvar states @@ -90,16 +90,16 @@ def test_contextvars_modification_follows_fixture_ordering(testdir): import pytest from pytest_trio import trio_fixture import trio_asyncio - import asyncio + import asyncio import trio class Resource(): async def __aenter__(self): - await trio_asyncio.aio_as_trio(asyncio.sleep(0)) - + await trio_asyncio.aio_as_trio(asyncio.sleep(0)) + async def __aexit__(self, *_): # We need to yield and run another trio task await trio.sleep(0.1) - await trio_asyncio.aio_as_trio(asyncio.sleep(0)) + await trio_asyncio.aio_as_trio(asyncio.sleep(0)) @pytest.fixture async def resource(): @@ -122,6 +122,7 @@ async def test_root(trio_asyncio_loop, resource): result = testdir.runpytest() result.assert_outcomes(passed=1) + def test_nursery_fixture_teardown_ordering(testdir): testdir.makepyfile( """ @@ -188,7 +189,7 @@ def test_error_collection(testdir): # crashing. # Although all fixtures are meant to crash, we only expect the first crash output - # since the teardown are synchronous. + # since the teardown are synchronous. testdir.makepyfile( """ import pytest diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 2de9ebc..f80f64f 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -162,9 +162,11 @@ def __init__(self, name, func, pytest_kwargs, is_test=False): self._pytest_kwargs = pytest_kwargs self._is_test = is_test # Previous and next fixture in terms of fixture topological order - # During set up, each fixture waits for previous_fixture to set its setup_done value + # During set up, each fixture waits for + # its previous_fixture to set its setup_done value self.previous_fixture = None - # During tear down, each fixture waits for next_fixture to set its teardown_done value + # During tear down, each fixture waits for + # itsnext_fixture to set its teardown_done value self.next_fixture = None # These attrs are all accessed from other objects: # Downstream users read this value. @@ -183,14 +185,14 @@ def collect_dependencies(self): deps = [] for value in self._pytest_kwargs.values(): if isinstance(value, TrioFixture): - for indirect_dependency in value.collect_dependencies(): - if indirect_dependency not in deps: - deps.append(indirect_dependency) + for dependency in value.collect_dependencies(): + if dependency not in deps: + deps.append(dependency) deps.append(self) return deps - + def collect_register_dependencies(self): - # Collect dependencies then register previous and next fixture + # Collect dependencies then register previous and next fixture # for each fixture (in terms of its topological order) deps = self.collect_dependencies() for index, dep in enumerate(deps): @@ -198,7 +200,7 @@ def collect_register_dependencies(self): dep.previous_fixture = deps[index - 1] if index < len(deps) - 1: dep.next_fixture = deps[index + 1] - return deps + return deps @asynccontextmanager async def _fixture_manager(self, test_ctx): @@ -235,7 +237,7 @@ async def run(self, test_ctx, contextvars_ctx): async with self._fixture_manager(test_ctx) as nursery_fixture: if self.previous_fixture: await self.previous_fixture.setup_done.wait() - + # Resolve our kwargs resolved_kwargs = {} for name, value in self._pytest_kwargs.items(): @@ -317,7 +319,7 @@ async def run(self, test_ctx, contextvars_ctx): with trio.CancelScope(shield=True): if self.next_fixture: await self.next_fixture.teardown_done.wait() - + # Do our teardown if isasyncgen(func_value): try: From a98375c4d2af2719c019c387ae04f3bf9bb0ae62 Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 21:02:56 +0800 Subject: [PATCH 05/11] remove trio_fixture import from test --- pytest_trio/_tests/test_fixture_ordering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 8a2da0e..6630e0c 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -75,6 +75,7 @@ async def test_root(fix_1, fix_2): result = testdir.runpytest() result.assert_outcomes(passed=1) + def test_contextvars_modification_follows_fixture_ordering(testdir): """ Tests that fixtures are being set up and tore down synchronously. @@ -88,7 +89,6 @@ def test_contextvars_modification_follows_fixture_ordering(testdir): testdir.makepyfile( """ import pytest - from pytest_trio import trio_fixture import trio_asyncio import asyncio import trio From 617e6d06e78807a9f5fe87d8c17cc2f86ebe0ad7 Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 21:41:19 +0800 Subject: [PATCH 06/11] add test for more complicated dag topological order --- pytest_trio/_tests/test_fixture_ordering.py | 107 ++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 6630e0c..2ba5e43 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -76,6 +76,86 @@ async def test_root(fix_1, fix_2): result.assert_outcomes(passed=1) +# This test involves several fixtures forming +# a pretty complicated dag, make sure we got the topological +# sort in order +def test_fixture_complicated_dag_ordering(testdir): + testdir.makepyfile( + """ + import pytest + from pytest_trio import trio_fixture + + setup_events = [] + teardown_events = [] + + @trio_fixture + async def fix_6(fix_7): + setup_events.append("fix_6 setup") + yield + teardown_events.append("fix_6 teardown") + + @pytest.fixture + async def fix_7(): + setup_events.append("fix_7 setup") + yield + teardown_events.append("fix_7 teardown") + assert teardown_events == [ + "fix_4 teardown", + "fix_5 teardown", + "fix_3 teardown", + "fix_1 teardown", + "fix_2 teardown", + "fix_6 teardown", + "fix_7 teardown", + ] + @pytest.fixture + async def fix_4(fix_5, fix_6, fix_7): + setup_events.append("fix_4 setup") + yield + teardown_events.append("fix_4 teardown") + + @pytest.fixture + async def fix_5(fix_3): + setup_events.append("fix_5 setup") + yield + teardown_events.append("fix_5 teardown") + + @pytest.fixture + async def fix_3(fix_1, fix_6): + setup_events.append("fix_3 setup") + yield + teardown_events.append("fix_3 teardown") + + @pytest.fixture + async def fix_1(fix_2, fix_7): + setup_events.append("fix_1 setup") + yield + teardown_events.append("fix_1 teardown") + + @pytest.fixture + async def fix_2(fix_6, fix_7): + setup_events.append("fix_2 setup") + yield + teardown_events.append("fix_2 teardown") + + @pytest.mark.trio + async def test_root(fix_1, fix_2, fix_3, fix_4, fix_5, fix_6, fix_7): + assert setup_events == [ + "fix_7 setup", + "fix_6 setup", + "fix_2 setup", + "fix_1 setup", + "fix_3 setup", + "fix_5 setup", + "fix_4 setup", + ] + assert teardown_events == [] + """ + ) + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + def test_contextvars_modification_follows_fixture_ordering(testdir): """ Tests that fixtures are being set up and tore down synchronously. @@ -175,6 +255,33 @@ async def test_root(fix2, nursery): result.assert_outcomes(passed=1) +def test_error_message_upon_circular_dependency(testdir): + testdir.makepyfile( + """ + import pytest + from pytest_trio import trio_fixture + + @trio_fixture + def seq(leaf_fix): + pass + + @pytest.fixture + async def leaf_fix(seq): + pass + + @pytest.mark.trio + async def test_root(leaf_fix, seq): + pass + """ + ) + + result = testdir.runpytest() + result.assert_outcomes(errors=1) + result.stdout.fnmatch_lines( + ["*recursive dependency involving fixture 'leaf_fix' detected*"] + ) + + def test_error_collection(testdir): # We want to make sure that pytest ultimately reports all the different # exceptions. We call .upper() on all the exceptions so that we have From f29106a72f4488858096fed1c5a6462e229c63e3 Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 21:47:47 +0800 Subject: [PATCH 07/11] better docs --- pytest_trio/_tests/test_fixture_ordering.py | 27 +++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 2ba5e43..af4c35b 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -1,12 +1,14 @@ import pytest -# Tests that: -# - leaf_fix gets set up first and torn down last -# - the two fix_{1,2} fixtures run their setup/teardown code -# in the expected order -# fix_1 setup -> fix_2 setup -> fix_2 teardown -> fix_1 teardown def test_fixture_basic_ordering(testdir): + """ + Tests that: + - leaf_fix gets set up first and torn down last + - the two fix_{1,2} fixtures run their setup/teardown code + in the expected order + fix_1 setup -> fix_2 setup -> fix_2 teardown -> fix_1 teardown + """ testdir.makepyfile( """ import pytest @@ -76,10 +78,11 @@ async def test_root(fix_1, fix_2): result.assert_outcomes(passed=1) -# This test involves several fixtures forming -# a pretty complicated dag, make sure we got the topological -# sort in order def test_fixture_complicated_dag_ordering(testdir): + """ + This test involves several fixtures forming a pretty + complicated dag, make sure we got the topological sort in order + """ testdir.makepyfile( """ import pytest @@ -256,6 +259,10 @@ async def test_root(fix2, nursery): def test_error_message_upon_circular_dependency(testdir): + """ + Make sure that the error message is produced if there's + a circular dependency on the fixtures + """ testdir.makepyfile( """ import pytest @@ -295,8 +302,8 @@ def test_error_collection(testdir): # sure that all the fixtures have started before any of them start # crashing. - # Although all fixtures are meant to crash, we only expect the first crash output - # since the teardown are synchronous. + # Although all fixtures are meant to crash, we only expect the crash output + # of the first fixture in the teardown sequence since the teardown are synchronous. testdir.makepyfile( """ import pytest From b4f7e4a921dce455c1a1f40695c521bd39594668 Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 22:18:17 +0800 Subject: [PATCH 08/11] fix docs for test crashing fixture --- pytest_trio/_tests/test_fixture_ordering.py | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index af4c35b..7154d3b 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -81,7 +81,7 @@ async def test_root(fix_1, fix_2): def test_fixture_complicated_dag_ordering(testdir): """ This test involves several fixtures forming a pretty - complicated dag, make sure we got the topological sort in order + complicated DAG, make sure we got the topological sort in order. """ testdir.makepyfile( """ @@ -164,10 +164,10 @@ def test_contextvars_modification_follows_fixture_ordering(testdir): Tests that fixtures are being set up and tore down synchronously. Specifically this ensures that fixtures that modify context variables - doesn't lead to a weird contextvar states + doesn't lead to a race condition. Main assertion is that 2 async tasks in teardown (Resource.__aexit__) - doesn't crash + doesn't crash. """ testdir.makepyfile( """ @@ -290,20 +290,21 @@ async def test_root(leaf_fix, seq): def test_error_collection(testdir): - # We want to make sure that pytest ultimately reports all the different + # We want to make sure that pytest ultimately reports all the # exceptions. We call .upper() on all the exceptions so that we have # tokens to look for in the output corresponding to each exception, where # those tokens don't appear at all the source (so we can't get a false # positive due to pytest printing out the source file). - # We sleep at the beginning of all the fixtures b/c currently if any - # fixture crashes, we skip setting up unrelated fixtures whose setup - # hasn't even started yet. Maybe we shouldn't? But for now the sleeps make - # sure that all the fixtures have started before any of them start - # crashing. + # We sleep at the beginning of all the fixtures to give opportunity + # for all fixtures to start the setup. Maybe we shouldn't? + # But for now the sleeps make sure that all the fixtures have + # started setting up before any of them start crashing. - # Although all fixtures are meant to crash, we only expect the crash output - # of the first fixture in the teardown sequence since the teardown are synchronous. + # We only expect the crash output of the first fixture that crashes + # during the setup. This is because the setup are synchronous. + # Once the fixture has crashed the test contex, the others would + # immediately return and wouldn't even complete the setup process testdir.makepyfile( """ import pytest From eafd4188d8b4a61e234c32bff10c3be157b2d99e Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 22:20:45 +0800 Subject: [PATCH 09/11] change assert for crash test to fnmatch_lines --- pytest_trio/_tests/test_fixture_ordering.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 7154d3b..6459c52 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -357,11 +357,7 @@ def test_followup(): result = testdir.runpytest() result.assert_outcomes(passed=1, failed=1) - result.stdout.fnmatch_lines_random( - [ - "*CRASH_NONGEN*", - ] - ) + result.stdout.fnmatch_lines(["*CRASH_NONGEN*"]) @pytest.mark.parametrize("bgmode", ["nursery fixture", "manual nursery"]) From 29a489548a76978a72ef05f5f3fbf63e9676341b Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 22:52:20 +0800 Subject: [PATCH 10/11] change contextvars modification test to not use trio_asyncio --- pytest_trio/_tests/test_fixture_ordering.py | 27 +++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 6459c52..c13d6a8 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -165,6 +165,8 @@ def test_contextvars_modification_follows_fixture_ordering(testdir): Specifically this ensures that fixtures that modify context variables doesn't lead to a race condition. + This tries to simluate thing like trio_asyncio.open_loop that modifies + the contextvar. Main assertion is that 2 async tasks in teardown (Resource.__aexit__) doesn't crash. @@ -172,27 +174,38 @@ def test_contextvars_modification_follows_fixture_ordering(testdir): testdir.makepyfile( """ import pytest - import trio_asyncio - import asyncio import trio + from contextlib import asynccontextmanager + + current_value = ContextVar("variable", default=None) + + @asynccontextmanager + async def variable_setter(): + old_value = current_value.set("value") + try: + await trio.sleep(0) + yield + finally: + current_value.reset(old_value) + class Resource(): async def __aenter__(self): - await trio_asyncio.aio_as_trio(asyncio.sleep(0)) + await trio.sleep(0) async def __aexit__(self, *_): # We need to yield and run another trio task - await trio.sleep(0.1) - await trio_asyncio.aio_as_trio(asyncio.sleep(0)) + await trio.sleep(0) + assert current_value.get() is not None @pytest.fixture async def resource(): - async with trio_asyncio.open_loop() as loop: + async with variable_setter() as loop: async with Resource(): yield @pytest.fixture async def trio_asyncio_loop(): - async with trio_asyncio.open_loop() as loop: + async with variable_setter() as loop: yield loop @pytest.mark.trio From 14a6850ae4a7527a0184bf49f1567ef040f02d3a Mon Sep 17 00:00:00 2001 From: Agnes Natasya Date: Sat, 7 Oct 2023 22:56:02 +0800 Subject: [PATCH 11/11] import contextvars for windows --- pytest_trio/_tests/test_fixture_ordering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index c13d6a8..0a218d3 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -173,11 +173,11 @@ def test_contextvars_modification_follows_fixture_ordering(testdir): """ testdir.makepyfile( """ + import contextvars import pytest import trio from contextlib import asynccontextmanager - - current_value = ContextVar("variable", default=None) + current_value = contextvars.ContextVar("variable", default=None) @asynccontextmanager async def variable_setter():