77
88class _AsyncioMixin :
99
10+ # Names intentionally have a long prefix
11+ # to reduce a chance of clashing with user-defined attributes
12+ # from inherited test case
13+ #
14+ # The class doesn't call loop.run_until_complete(self.setUp()) and family
15+ # but uses a different approach:
16+ # 1. create a long-running task that reads self.setUp()
17+ # awaitable from queue along with a future
18+ # 2. await the awaitable object passing in and set the result
19+ # into the future object
20+ # 3. Outer code puts the awaitable and the future object into a queue
21+ # with waiting for the future
22+ # The trick is necessary because every run_until_complete() call
23+ # creates a new task with embedded ContextVar context.
24+ # To share contextvars between setUp(), test and tearDown() we need to execute
25+ # them inside the same task.
26+
27+ # Note: the test case modifies event loop policy if the policy was not instantiated
28+ # yet.
29+ # asyncio.get_event_loop_policy() creates a default policy on demand but never
30+ # returns None
31+ # I believe this is not an issue in user level tests but python itself for testing
32+ # should reset a policy in every test module
33+ # by calling asyncio.set_event_loop_policy(None) in tearDownModule()
34+
1035 def __init__ (self , methodName = 'runTest' ):
1136 super ().__init__ (methodName )
1237 self ._asyncioTestLoop = None
1338 self ._asyncioCallsQueue = None
1439
40+ def setUpAsyncioLoop (self ):
41+ raise NotImplementedError
42+
43+ def tearDownAsyncioLoop (self , loop ):
44+ raise NotImplementedError
45+
1546 async def asyncSetUp (self ):
1647 pass
1748
@@ -87,10 +118,24 @@ async def _asyncioLoopRunner(self, fut):
87118 fut .set_exception (ex )
88119
89120 def _setupAsyncioLoop (self ):
90- raise NotImplementedError
121+ assert self ._asyncioTestLoop is None , 'asyncio test loop already initialized'
122+ loop = self .setUpAsyncioLoop ()
123+ if loop is None :
124+ raise AssertionError (
125+ "setUpAsyncioLoop() should return a loop instance, got None"
126+ )
127+ self ._asyncioTestLoop = loop
128+ fut = loop .create_future ()
129+ self ._asyncioCallsTask = loop .create_task (self ._asyncioLoopRunner (fut ))
130+ loop .run_until_complete (fut )
91131
92132 def _tearDownAsyncioLoop (self ):
93- raise NotImplementedError
133+ assert self ._asyncioTestLoop is not None , 'asyncio test loop is not initialized'
134+ loop = self ._asyncioTestLoop
135+ self ._asyncioCallsQueue .put_nowait (None )
136+ loop .run_until_complete (self ._asyncioCallsQueue .join ())
137+ self .tearDownAsyncioLoop (loop )
138+ self ._asyncioTestLoop = None
94139
95140 def run (self , result = None ):
96141 self ._setupAsyncioLoop ()
@@ -104,50 +149,65 @@ def debug(self):
104149 super ().debug ()
105150 self ._tearDownAsyncioLoop ()
106151
152+ def __del__ (self ):
153+ if self ._asyncioTestLoop is not None :
154+ self ._tearDownAsyncioLoop ()
155+
107156
108157class IsolatedAsyncioTestCase (_AsyncioMixin , TestCase ):
109- # Names intentionally have a long prefix
110- # to reduce a chance of clashing with user-defined attributes
111- # from inherited test case
112- #
113- # The class doesn't call loop.run_until_complete(self.setUp()) and family
114- # but uses a different approach:
115- # 1. create a long-running task that reads self.setUp()
116- # awaitable from queue along with a future
117- # 2. await the awaitable object passing in and set the result
118- # into the future object
119- # 3. Outer code puts the awaitable and the future object into a queue
120- # with waiting for the future
121- # The trick is necessary because every run_until_complete() call
122- # creates a new task with embedded ContextVar context.
123- # To share contextvars between setUp(), test and tearDown() we need to execute
124- # them inside the same task.
158+ """Isolated asyncio test case.
125159
126- # Note: the test case modifies event loop policy if the policy was not instantiated
127- # yet.
128- # asyncio.get_event_loop_policy() creates a default policy on demand but never
129- # returns None
130- # I believe this is not an issue in user level tests but python itself for testing
131- # should reset a policy in every test module
132- # by calling asyncio.set_event_loop_policy(None) in tearDownModule()
160+ New event loop is created for each test.
161+ """
133162
134- def _setupAsyncioLoop (self ):
135- assert self ._asyncioTestLoop is None , 'asyncio test loop already initialized'
163+ def setUpAsyncioLoop (self ):
136164 loop = asyncio .new_event_loop ()
137165 asyncio .set_event_loop (loop )
138166 loop .set_debug (True )
139- self ._asyncioTestLoop = loop
140- fut = loop .create_future ()
141- self ._asyncioCallsTask = loop .create_task (self ._asyncioLoopRunner (fut ))
142- loop .run_until_complete (fut )
167+ return loop
143168
144- def _tearDownAsyncioLoop (self ):
145- assert self ._asyncioTestLoop is not None , 'asyncio test loop is not initialized'
146- loop = self ._asyncioTestLoop
147- self ._asyncioTestLoop = None
148- self ._asyncioCallsQueue .put_nowait (None )
149- loop .run_until_complete (self ._asyncioCallsQueue .join ())
169+ def tearDownAsyncioLoop (self , loop ):
170+ try :
171+ # cancel all tasks
172+ to_cancel = asyncio .all_tasks (loop )
173+ if not to_cancel :
174+ return
175+
176+ for task in to_cancel :
177+ task .cancel ()
150178
179+ loop .run_until_complete (
180+ asyncio .gather (* to_cancel , return_exceptions = True ))
181+
182+ for task in to_cancel :
183+ if task .cancelled ():
184+ continue
185+ if task .exception () is not None :
186+ loop .call_exception_handler ({
187+ 'message' : 'unhandled exception during test shutdown' ,
188+ 'exception' : task .exception (),
189+ 'task' : task ,
190+ })
191+ loop .run_until_complete (loop .shutdown_asyncgens ())
192+ loop .run_until_complete (loop .shutdown_default_executor ())
193+ finally :
194+ asyncio .set_event_loop (None )
195+ loop .close ()
196+
197+
198+
199+ class SharedAsyncioTestCase (_AsyncioMixin , TestCase ):
200+ _globalAsyncioLoop = None
201+
202+ @classmethod
203+ def setUpGlobalAsyncioLoop (cls ):
204+ loop = asyncio .new_event_loop ()
205+ asyncio .set_event_loop (loop )
206+ loop .set_debug (True )
207+ return loop
208+
209+ @classmethod
210+ def tearDownGlobalAsyncioLoop (cls , loop ):
151211 try :
152212 # cancel all tasks
153213 to_cancel = asyncio .all_tasks (loop )
@@ -169,12 +229,19 @@ def _tearDownAsyncioLoop(self):
169229 'exception' : task .exception (),
170230 'task' : task ,
171231 })
172- # shutdown asyncgens
173232 loop .run_until_complete (loop .shutdown_asyncgens ())
233+ loop .run_until_complete (loop .shutdown_default_executor ())
174234 finally :
175235 asyncio .set_event_loop (None )
176236 loop .close ()
177237
178- def __del__ (self ):
179- if self ._asyncioTestLoop is not None :
180- self ._tearDownAsyncioLoop ()
238+ def setUpAsyncioLoop (self ):
239+ loop = self .__class__ ._globalAsyncioLoop
240+ if loop is not None :
241+ loop = self .setUpGlobalAsyncioLoop ()
242+ self .__class__ ._globalAsyncioLoop = loop
243+ return loop
244+
245+ def tearDownAsyncioLoop (self , loop ):
246+ # The loop teardown is performed on the process teardown
247+ pass
0 commit comments