22
33from __future__ import annotations
44
5- from asyncio import ensure_future , sleep
5+ from asyncio import Event , Future , ensure_future , sleep
66from collections .abc import Awaitable
77
88import pytest
99
1010from graphql import build_schema
11- from graphql .execution import execute
11+ from graphql .execution import execute , subscribe
1212from graphql .language import parse
1313from graphql .pyutils import AbortController , AbortError
1414
1919 """
2020 type Todo {
2121 id: ID
22- text: String
22+ items: [ String]
2323 author: User
2424 }
2525
3030
3131 type Query {
3232 todo: Todo
33+ nonNullableTodo: Todo!
3334 }
3435
3536 type Mutation {
3637 foo: String
3738 bar: String
3839 }
40+
41+ type Subscription {
42+ foo: String
43+ }
3944 """
4045)
4146
@@ -97,7 +102,6 @@ async def stops_the_execution_when_aborted_during_object_field_completion():
97102 async def todo (_info ):
98103 return {
99104 "id" : "1" ,
100- "text" : "Hello, World!" ,
101105 "author" : must_not_be_called ,
102106 }
103107
@@ -190,7 +194,6 @@ async def stops_the_execution_when_aborted_during_completion_with_custom_error()
190194 async def todo (_info ):
191195 return {
192196 "id" : "1" ,
193- "text" : "Hello, World!" ,
194197 "author" : must_not_be_called ,
195198 }
196199
@@ -238,7 +241,6 @@ async def stops_the_execution_when_aborted_during_completion_with_custom_string(
238241 async def todo (_info ):
239242 return {
240243 "id" : "1" ,
241- "text" : "Hello, World!" ,
242244 "author" : must_not_be_called ,
243245 }
244246
@@ -290,7 +292,6 @@ async def author(_info):
290292 root_value = {
291293 "todo" : {
292294 "id" : "1" ,
293- "text" : "Hello, World!" ,
294295 "author" : author ,
295296 }
296297 },
@@ -312,6 +313,146 @@ async def author(_info):
312313 ],
313314 )
314315
316+ async def stops_the_execution_when_aborted_despite_a_hanging_resolver ():
317+ abort_controller = AbortController ()
318+ document = parse (
319+ """
320+ query {
321+ todo {
322+ id
323+ author {
324+ id
325+ }
326+ }
327+ }
328+ """
329+ )
330+
331+ started = Event ()
332+
333+ async def todo (_info ):
334+ started .set ()
335+ await Future () # will never resolve
336+
337+ awaitable_result = execute (
338+ schema ,
339+ document ,
340+ abort_signal = abort_controller .signal ,
341+ root_value = {"todo" : todo },
342+ )
343+ assert isinstance (awaitable_result , Awaitable )
344+
345+ # Abort only once the resolver is actually in flight, so that cancellation
346+ # must interrupt the hanging resolver instead of being caught up front.
347+ task = ensure_future (awaitable_result )
348+ await started .wait ()
349+ abort_controller .abort ()
350+
351+ result = await task
352+
353+ assert result .errors is not None
354+ assert isinstance (result .errors [0 ].original_error , AbortError )
355+ assert result == (
356+ {"todo" : None },
357+ [
358+ {
359+ "message" : "This operation was aborted" ,
360+ "locations" : [(3 , 9 )],
361+ "path" : ["todo" ],
362+ }
363+ ],
364+ )
365+
366+ async def stops_the_execution_when_aborted_despite_a_hanging_item ():
367+ abort_controller = AbortController ()
368+ document = parse (
369+ """
370+ query {
371+ todo {
372+ id
373+ items
374+ }
375+ }
376+ """
377+ )
378+
379+ def todo (_info ):
380+ return {
381+ "id" : "1" ,
382+ "items" : [Future ()], # will never resolve
383+ }
384+
385+ awaitable_result = execute (
386+ schema ,
387+ document ,
388+ abort_signal = abort_controller .signal ,
389+ root_value = {"todo" : todo },
390+ )
391+ assert isinstance (awaitable_result , Awaitable )
392+
393+ abort_controller .abort ()
394+
395+ result = await awaitable_result
396+
397+ assert result .errors is not None
398+ assert isinstance (result .errors [0 ].original_error , AbortError )
399+ assert result == (
400+ {"todo" : {"id" : "1" , "items" : [None ]}},
401+ [
402+ {
403+ "message" : "This operation was aborted" ,
404+ "locations" : [(5 , 11 )],
405+ "path" : ["todo" , "items" , 0 ],
406+ }
407+ ],
408+ )
409+
410+ async def stops_the_execution_when_aborted_with_proper_null_bubbling ():
411+ abort_controller = AbortController ()
412+ document = parse (
413+ """
414+ query {
415+ nonNullableTodo {
416+ id
417+ author {
418+ id
419+ }
420+ }
421+ }
422+ """
423+ )
424+
425+ async def non_nullable_todo (_info ):
426+ return {
427+ "id" : "1" ,
428+ "author" : must_not_be_called ,
429+ }
430+
431+ awaitable_result = execute (
432+ schema ,
433+ document ,
434+ abort_signal = abort_controller .signal ,
435+ root_value = {"nonNullableTodo" : non_nullable_todo },
436+ )
437+ assert isinstance (awaitable_result , Awaitable )
438+
439+ abort_controller .abort ()
440+
441+ result = await awaitable_result
442+
443+ assert result .errors is not None
444+ assert isinstance (result .errors [0 ].original_error , AbortError )
445+ assert result == (
446+ None ,
447+ [
448+ {
449+ "message" : "This operation was aborted" ,
450+ "locations" : [(3 , 9 )],
451+ "path" : ["nonNullableTodo" ],
452+ }
453+ ],
454+ )
455+
315456 async def stops_the_execution_when_aborted_mid_mutation ():
316457 abort_controller = AbortController ()
317458 document = parse (
@@ -334,9 +475,16 @@ async def foo(_info):
334475 )
335476 assert isinstance (awaitable_result , Awaitable )
336477
478+ # Let the first field resolve before aborting, so that the abort is only
479+ # observed when serially moving on to the second field (mirrors the
480+ # ``resolveOnNextTick`` calls in the GraphQL.js test).
481+ task = ensure_future (awaitable_result )
482+ for _ in range (3 ):
483+ await sleep (0 )
484+
337485 abort_controller .abort ()
338486
339- result = await awaitable_result
487+ result = await task
340488
341489 assert result == (
342490 {"foo" : "baz" , "bar" : None },
@@ -373,3 +521,39 @@ async def stops_the_execution_when_aborted_pre_execute():
373521 )
374522
375523 assert result == (None , [{"message" : "This operation was aborted" }])
524+
525+ async def stops_the_execution_when_aborted_during_subscription ():
526+ abort_controller = AbortController ()
527+ document = parse (
528+ """
529+ subscription {
530+ foo
531+ }
532+ """
533+ )
534+
535+ def foo (_info ):
536+ return Future () # will never resolve
537+
538+ awaitable_result = subscribe (
539+ schema ,
540+ document ,
541+ abort_signal = abort_controller .signal ,
542+ root_value = {"foo" : foo },
543+ )
544+ assert isinstance (awaitable_result , Awaitable )
545+
546+ abort_controller .abort ()
547+
548+ result = await awaitable_result
549+
550+ assert result == (
551+ None ,
552+ [
553+ {
554+ "message" : "This operation was aborted" ,
555+ "locations" : [(3 , 9 )],
556+ "path" : ["foo" ],
557+ }
558+ ],
559+ )
0 commit comments