11
2+ import queue
23import threading
34import json
5+ import time
46from collections import defaultdict
57from enum import Enum
8+ from contextlib import suppress , contextmanager
9+ from copy import copy
610from typing import Callable , Mapping , Optional , Union
711from ssl import SSLContext
812
@@ -46,6 +50,44 @@ class NoMethodFoundForMatchingHeaderValueError(Error):
4650 pass
4751
4852
53+ class WaitingSettings :
54+ """Class for providing default settings and storing them in HTTPServer
55+
56+ :param raise_assertions: whether raise assertions on unexpected request or timeout or not
57+ :param stop_on_nohandler: whether stop on unexpected request or not
58+ :param timeout: time (in seconds) until time is out
59+ """
60+
61+ def __init__ (self , raise_assertions : bool = True , stop_on_nohandler : bool = True , timeout : float = 5 ):
62+ self .raise_assertions = raise_assertions
63+ self .stop_on_nohandler = stop_on_nohandler
64+ self .timeout = timeout
65+
66+
67+ class Waiting :
68+ """Class for HTTPServer.wait context manager
69+
70+ This class should not be instantiated directly."""
71+
72+ def __init__ (self ):
73+ self ._result = None
74+ self ._start = time .monotonic ()
75+ self ._stop = None
76+
77+ def complete (self , result : bool ):
78+ self ._result = result
79+ self ._stop = time .monotonic ()
80+
81+ @property
82+ def result (self ) -> bool :
83+ return self ._result
84+
85+ @property
86+ def elapsed_time (self ) -> float :
87+ """Elapsed time in seconds"""
88+ return self ._stop - self ._start
89+
90+
4991class HeaderValueMatcher :
5092 """
5193 Matcher object for the header value of incoming request.
@@ -310,6 +352,8 @@ class HTTPServer: # pylint: disable=too-many-instance-attributes
310352 :param host: the host or IP where the server will listen
311353 :param port: the TCP port where the server will listen
312354 :param ssl_context: the ssl context object to use for https connections
355+ :param default_waiting_settings: the waiting settings object to use as default settings for :py:meth:`wait` context
356+ manager
313357
314358 .. py:attribute:: log
315359
@@ -322,7 +366,8 @@ class HTTPServer: # pylint: disable=too-many-instance-attributes
322366 DEFAULT_LISTEN_HOST = "localhost"
323367 DEFAULT_LISTEN_PORT = 0 # Use ephemeral port
324368
325- def __init__ (self , host = DEFAULT_LISTEN_HOST , port = DEFAULT_LISTEN_PORT , ssl_context : Optional [SSLContext ] = None ):
369+ def __init__ (self , host = DEFAULT_LISTEN_HOST , port = DEFAULT_LISTEN_PORT , ssl_context : Optional [SSLContext ] = None ,
370+ default_waiting_settings : Optional [WaitingSettings ] = None ):
326371 """
327372 Initializes the instance.
328373
@@ -338,6 +383,12 @@ def __init__(self, host=DEFAULT_LISTEN_HOST, port=DEFAULT_LISTEN_PORT, ssl_conte
338383 self .handlers = RequestHandlerList ()
339384 self .permanently_failed = False
340385 self .ssl_context = ssl_context
386+ if default_waiting_settings is not None :
387+ self .default_waiting_settings = default_waiting_settings
388+ else :
389+ self .default_waiting_settings = WaitingSettings ()
390+ self ._waiting_settings = copy (self .default_waiting_settings )
391+ self ._waiting_result = queue .LifoQueue (maxsize = 1 )
341392
342393 def clear (self ):
343394 """
@@ -654,6 +705,8 @@ def respond_nohandler(self, request: Request):
654705 As the result, there's an assertion added (which can be raised by :py:meth:`check_assertions`).
655706
656707 """
708+ if self ._waiting_settings .stop_on_nohandler :
709+ self ._set_waiting_result (False )
657710 text = "No handler found for request {!r}.\n " .format (request )
658711 self .add_assertion (text + self .format_matchers ())
659712 return Response ("No handler found for this request" , 500 )
@@ -700,11 +753,13 @@ def dispatch(self, request: Request) -> Response:
700753 return response
701754
702755 self .ordered_handlers .pop (0 )
756+ self ._update_waiting_result ()
703757
704758 if not handler :
705759 handler = self .oneshot_handlers .match (request )
706760 if handler :
707761 self .oneshot_handlers .remove (handler )
762+ self ._update_waiting_result ()
708763 else :
709764 handler = self .handlers .match (request )
710765
@@ -720,6 +775,69 @@ def dispatch(self, request: Request) -> Response:
720775
721776 return response
722777
778+ def _set_waiting_result (self , value : bool ) -> None :
779+ """Set waiting_result
780+
781+ Setting is implemented as putting value to queue without waiting. If queue is full we simply ignore the
782+ exception, because that means that waiting_result was already set, but not read.
783+ """
784+ with suppress (queue .Full ):
785+ self ._waiting_result .put_nowait (value )
786+
787+ def _update_waiting_result (self ) -> None :
788+ if not self .oneshot_handlers and not self .ordered_handlers :
789+ self ._set_waiting_result (True )
790+
791+ @contextmanager
792+ def wait (self , raise_assertions : Optional [bool ] = None , stop_on_nohandler : Optional [bool ] = None ,
793+ timeout : Optional [float ] = None ):
794+ """Context manager to wait until the first of following event occurs: all ordered and oneshot handlers were
795+ executed, unexpected request was received (if `stop_on_nohandler` is set to `True`), or time was out
796+
797+ :param raise_assertions: whether raise assertions on unexpected request or timeout or not
798+ :param stop_on_nohandler: whether stop on unexpected request or not
799+ :param timeout: time (in seconds) until time is out
800+
801+ Example:
802+ def test_wait(httpserver):
803+ httpserver.expect_oneshot_request('/').respond_with_data('OK')
804+ with httpserver.wait(raise_assertions=False, stop_on_nohandler=False, timeout=1) as waiting:
805+ requests.get(httpserver.url_for('/'))
806+ # `waiting` is :py:class:`Waiting`
807+ assert waiting.result
808+ print('Elapsed time: {} sec'.format(waiting.elapsed_time))
809+ """
810+ if raise_assertions is None :
811+ self ._waiting_settings .raise_assertions = self .default_waiting_settings .raise_assertions
812+ else :
813+ self ._waiting_settings .raise_assertions = raise_assertions
814+ if stop_on_nohandler is None :
815+ self ._waiting_settings .stop_on_nohandler = self .default_waiting_settings .stop_on_nohandler
816+ else :
817+ self ._waiting_settings .stop_on_nohandler = stop_on_nohandler
818+ if timeout is None :
819+ self ._waiting_settings .timeout = self .default_waiting_settings .timeout
820+ else :
821+ self ._waiting_settings .timeout = timeout
822+
823+ # Ensure that waiting_result is empty
824+ with suppress (queue .Empty ):
825+ self ._waiting_result .get_nowait ()
826+
827+ waiting = Waiting ()
828+ yield waiting
829+
830+ try :
831+ waiting_result = self ._waiting_result .get (timeout = self ._waiting_settings .timeout )
832+ waiting .complete (result = waiting_result )
833+ except queue .Empty :
834+ waiting .complete (result = False )
835+ if self ._waiting_settings .raise_assertions :
836+ raise AssertionError ('Wait timeout occurred, but some handlers left:\n '
837+ '{}' .format (self .format_matchers ()))
838+ if self ._waiting_settings .raise_assertions and not waiting .result :
839+ self .check_assertions ()
840+
723841 @Request .application
724842 def application (self , request : Request ):
725843 """
0 commit comments