Format: text/x-python
⬇ Download File (3.4 KB)
   1  import asyncio
   2  import contextvars
   3  import time
   4  
   5  from .compatibility import guarantee_single_callable
   6  from .timeout import timeout as async_timeout
   7  
   8  
   9  class ApplicationCommunicator:
  10      """
  11      Runs an ASGI application in a test mode, allowing sending of
  12      messages to it and retrieval of messages it sends.
  13      """
  14  
  15      def __init__(self, application, scope):
  16          self.application = guarantee_single_callable(application)
  17          self.scope = scope
  18          self.input_queue = asyncio.Queue()
  19          self.output_queue = asyncio.Queue()
  20          # Clear context - this ensures that context vars set in the testing scope
  21          # are not "leaked" into the application which would normally begin with
  22          # an empty context. In Python >= 3.11 this could also be written as:
  23          # asyncio.create_task(..., context=contextvars.Context())
  24          self.future = contextvars.Context().run(
  25              asyncio.create_task,
  26              self.application(scope, self.input_queue.get, self.output_queue.put),
  27          )
  28  
  29      async def wait(self, timeout=1):
  30          """
  31          Waits for the application to stop itself and returns any exceptions.
  32          """
  33          try:
  34              async with async_timeout(timeout):
  35                  try:
  36                      await self.future
  37                      self.future.result()
  38                  except asyncio.CancelledError:
  39                      pass
  40          finally:
  41              if not self.future.done():
  42                  self.future.cancel()
  43                  try:
  44                      await self.future
  45                  except asyncio.CancelledError:
  46                      pass
  47  
  48      def stop(self, exceptions=True):
  49          if not self.future.done():
  50              self.future.cancel()
  51          elif exceptions:
  52              # Give a chance to raise any exceptions
  53              self.future.result()
  54  
  55      def __del__(self):
  56          # Clean up on deletion
  57          try:
  58              self.stop(exceptions=False)
  59          except RuntimeError:
  60              # Event loop already stopped
  61              pass
  62  
  63      async def send_input(self, message):
  64          """
  65          Sends a single message to the application
  66          """
  67          # Give it the message
  68          await self.input_queue.put(message)
  69  
  70      async def receive_output(self, timeout=1):
  71          """
  72          Receives a single message from the application, with optional timeout.
  73          """
  74          # Make sure there's not an exception to raise from the task
  75          if self.future.done():
  76              self.future.result()
  77          # Wait and receive the message
  78          try:
  79              async with async_timeout(timeout):
  80                  return await self.output_queue.get()
  81          except asyncio.TimeoutError as e:
  82              # See if we have another error to raise inside
  83              if self.future.done():
  84                  self.future.result()
  85              else:
  86                  self.future.cancel()
  87                  try:
  88                      await self.future
  89                  except asyncio.CancelledError:
  90                      pass
  91              raise e
  92  
  93      async def receive_nothing(self, timeout=0.1, interval=0.01):
  94          """
  95          Checks that there is no message to receive in the given time.
  96          """
  97          # `interval` has precedence over `timeout`
  98          start = time.monotonic()
  99          while time.monotonic() - start < timeout:
 100              if not self.output_queue.empty():
 101                  return False
 102              await asyncio.sleep(interval)
 103          return self.output_queue.empty()
 104