@@ -351,3 +351,138 @@ async def run_asyncio_loop(nursery, *, task_status=trio.TASK_STATUS_IGNORED):
351
351
await nursery .start (run_asyncio_loop , nursery )
352
352
# Trigger KeyboardInterrupt that should propagate accross the coroutines
353
353
signal .pthread_kill (threading .get_ident (), signal .SIGINT )
354
+
355
+
356
+ @pytest .mark .trio
357
+ @pytest .mark .parametrize ("throw_another" , [False , True ])
358
+ async def test_cancel_loop (throw_another ):
359
+ """Regression test for #76: ensure that cancelling a trio-asyncio loop
360
+ does not cause any of the tasks running within it to yield a
361
+ result of Cancelled.
362
+ """
363
+ async def manage_loop (task_status ):
364
+ try :
365
+ with trio .CancelScope () as scope :
366
+ async with trio_asyncio .open_loop () as loop :
367
+ task_status .started ((loop , scope ))
368
+ await trio .sleep_forever ()
369
+ finally :
370
+ assert scope .cancelled_caught
371
+
372
+ # Trio-flavored async function. Runs as a trio-aio loop task
373
+ # and gets cancelled when the loop does.
374
+ async def trio_task ():
375
+ async with trio .open_nursery () as nursery :
376
+ nursery .start_soon (trio .sleep_forever )
377
+ try :
378
+ await trio .sleep_forever ()
379
+ except trio .Cancelled :
380
+ if throw_another :
381
+ # This will combine with the Cancelled from the
382
+ # background sleep_forever task to create a
383
+ # MultiError escaping from trio_task
384
+ raise ValueError ("hi" )
385
+
386
+ async with trio .open_nursery () as nursery :
387
+ loop , scope = await nursery .start (manage_loop )
388
+ fut = loop .trio_as_future (trio_task )
389
+ await trio .testing .wait_all_tasks_blocked ()
390
+ scope .cancel ()
391
+ assert fut .done ()
392
+ if throw_another :
393
+ with pytest .raises (ValueError , match = "hi" ):
394
+ fut .result ()
395
+ else :
396
+ assert fut .cancelled ()
397
+
398
+
399
+ @pytest .mark .trio
400
+ async def test_trio_as_fut_throws_after_cancelled ():
401
+ """If a trio_as_future() future is cancelled, any exception
402
+ thrown by the Trio task as it unwinds is ignored. (This is
403
+ somewhat infelicitous, but the asyncio Future API doesn't allow
404
+ a future to go from cancelled to some other outcome.)
405
+ """
406
+
407
+ async def trio_task ():
408
+ try :
409
+ await trio .sleep_forever ()
410
+ finally :
411
+ raise ValueError ("hi" )
412
+
413
+ async with trio_asyncio .open_loop () as loop :
414
+ fut = loop .trio_as_future (trio_task )
415
+ await trio .testing .wait_all_tasks_blocked ()
416
+ fut .cancel ()
417
+ with pytest .raises (asyncio .CancelledError ):
418
+ await fut
419
+
420
+
421
+ @pytest .mark .trio
422
+ async def test_run_trio_task_errors (monkeypatch ):
423
+ async with trio_asyncio .open_loop () as loop :
424
+ # Test never getting to start the task
425
+ handle = loop .run_trio_task (trio .sleep_forever )
426
+ handle .cancel ()
427
+
428
+ # Test cancelling the task
429
+ handle = loop .run_trio_task (trio .sleep_forever )
430
+ await trio .testing .wait_all_tasks_blocked ()
431
+ handle .cancel ()
432
+
433
+ # Helper for the rest of this test, which covers cases where
434
+ # the Trio task raises an exception
435
+ async def raise_in_aio_loop (exc ):
436
+ async def raise_it ():
437
+ raise exc
438
+
439
+ async with trio_asyncio .open_loop () as loop :
440
+ loop .run_trio_task (raise_it )
441
+
442
+ # We temporarily modify the default exception handler to collect
443
+ # the exceptions instead of logging or raising them
444
+
445
+ exceptions = []
446
+
447
+ def collect_exceptions (loop , context ):
448
+ if context .get ("exception" ):
449
+ exceptions .append (context ["exception" ])
450
+ else :
451
+ exceptions .append (RuntimeError (context .get ("message" ) or "unknown" ))
452
+
453
+ monkeypatch .setattr (
454
+ trio_asyncio .TrioEventLoop , "default_exception_handler" , collect_exceptions
455
+ )
456
+ expected = [
457
+ ValueError ("hi" ), ValueError ("lo" ), KeyError (), IndexError ()
458
+ ]
459
+ await raise_in_aio_loop (expected [0 ])
460
+ with pytest .raises (SystemExit ):
461
+ await raise_in_aio_loop (SystemExit (0 ))
462
+ with pytest .raises (SystemExit ):
463
+ await raise_in_aio_loop (trio .MultiError ([expected [1 ], SystemExit ()]))
464
+ await raise_in_aio_loop (trio .MultiError (expected [2 :]))
465
+ assert exceptions == expected
466
+
467
+
468
+ @pytest .mark .trio
469
+ @pytest .mark .skipif (sys .version_info < (3 , 7 ), reason = "needs asyncio contextvars" )
470
+ async def test_contextvars ():
471
+ import contextvars
472
+
473
+ cvar = contextvars .ContextVar ("test_cvar" )
474
+ cvar .set ("outer" )
475
+
476
+ async def fudge_in_aio ():
477
+ assert cvar .get () == "outer"
478
+ cvar .set ("middle" )
479
+ await trio_asyncio .trio_as_aio (fudge_in_trio )()
480
+ assert cvar .get () == "middle"
481
+
482
+ async def fudge_in_trio ():
483
+ assert cvar .get () == "middle"
484
+ cvar .set ("inner" )
485
+
486
+ async with trio_asyncio .open_loop () as loop :
487
+ await trio_asyncio .aio_as_trio (fudge_in_aio )()
488
+ assert cvar .get () == "outer"
0 commit comments