Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 31 additions & 20 deletions pytest_httpserver/httpserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,8 @@ def __init__(
self.default_waiting_settings = WaitingSettings()
self._waiting_settings = copy(self.default_waiting_settings)
self._waiting_result: queue.LifoQueue[bool] = queue.LifoQueue(maxsize=1)
self._handler_lock = threading.RLock()
self._active_waiting_handlers = 0
self.startup_timeout = startup_timeout
self._readiness_check_pending = False

Expand Down Expand Up @@ -1422,27 +1424,31 @@ def dispatch(self, request: Request) -> Response:
if self.permanently_failed:
return self.respond_permanent_failure()

handler = None
if self.ordered_handlers:
handler = self.ordered_handlers[0]
if not handler.matcher.match(request):
self.permanently_failed = True
response = self.respond_nohandler(request)
return response

self.ordered_handlers.pop(0)
self._update_waiting_result()

if not handler:
handler = self.oneshot_handlers.match(request)
if handler:
self.oneshot_handlers.remove(handler)
self._update_waiting_result()
else:
handler = self.handlers.match(request)
with self._handler_lock:
handler = None
consumed_handler = False
if self.ordered_handlers:
handler = self.ordered_handlers[0]
if not handler.matcher.match(request):
self.permanently_failed = True
response = self.respond_nohandler(request)
return response

self.ordered_handlers.pop(0)
consumed_handler = True

if not handler:
return self.respond_nohandler(request)
handler = self.oneshot_handlers.match(request)
if handler:
self.oneshot_handlers.remove(handler)
consumed_handler = True
else:
handler = self.handlers.match(request)

if not handler:
return self.respond_nohandler(request)
if consumed_handler:
self._active_waiting_handlers += 1

try:
response = handler.respond(request)
Expand All @@ -1455,6 +1461,11 @@ def dispatch(self, request: Request) -> Response:
except Exception as e:
self.handler_errors.append(e)
raise
finally:
if consumed_handler:
with self._handler_lock:
self._active_waiting_handlers -= 1
self._update_waiting_result()

if response is None:
response = Response("")
Expand All @@ -1473,7 +1484,7 @@ def _set_waiting_result(self, value: bool) -> None: # noqa: FBT001
self._waiting_result.put_nowait(value)

def _update_waiting_result(self) -> None:
if not self.oneshot_handlers and not self.ordered_handlers:
if not self.oneshot_handlers and not self.ordered_handlers and not self._active_waiting_handlers:
self._set_waiting_result(value=True)

@contextmanager
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fixes:
- |
Wait for ordered and oneshot response callbacks to finish before reporting
successful completion from ``HTTPServer.wait()``, including callbacks
executing concurrently in a threaded server.
67 changes: 67 additions & 0 deletions tests/test_wait.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from contextlib import closing
from http.client import HTTPConnection
from threading import Event

import requests
from pytest import approx
from pytest import mark
from pytest import raises
from werkzeug import Request
from werkzeug import Response

from pytest_httpserver import HTTPServer

Expand Down Expand Up @@ -70,3 +77,63 @@ def test_wait_raise_assertion_false(httpserver: HTTPServer):
assert not waiting.result
waiting_time_error = 0.1
assert waiting.elapsed_time == approx(waiting_timeout, abs=waiting_time_error)


@mark.parametrize("ordered", [False, True])
def test_wait_does_not_finish_while_last_handler_is_running(httpserver: HTTPServer, *, ordered: bool):
entered = Event()
release = Event()

def respond(_request: Request) -> Response:
entered.set()
assert release.wait(5)
return Response("finished")

handler = httpserver.expect_ordered_request("/") if ordered else httpserver.expect_oneshot_request("/")
handler.respond_with_handler(respond)
with closing(HTTPConnection(httpserver.host, httpserver.port, timeout=5)) as connection:
try:
with httpserver.wait(timeout=0.01, raise_assertions=False) as waiting:
connection.request("GET", "/")
assert entered.wait(5)
assert not waiting.result
finally:
release.set()
assert connection.getresponse().read() == b"finished"


@mark.parametrize("ordered", [False, True])
def test_wait_tracks_other_callbacks_after_handler_lists_are_empty(*, ordered: bool):
entered = [Event(), Event()]
release = [Event(), Event()]

def respond(request: Request) -> Response:
index = int(request.path[1:])
entered[index].set()
assert release[index].wait(5)
return Response("finished")

with HTTPServer(threaded=True) as server:
for index in range(2):
handler = (
server.expect_ordered_request(f"/{index}") if ordered else server.expect_oneshot_request(f"/{index}")
)
handler.respond_with_handler(respond)

with (
closing(HTTPConnection(server.host, server.port, timeout=5)) as first,
closing(HTTPConnection(server.host, server.port, timeout=5)) as second,
):
try:
with server.wait(timeout=0.01, raise_assertions=False) as waiting:
first.request("GET", "/0")
assert entered[0].wait(5)
second.request("GET", "/1")
assert entered[1].wait(5)
release[1].set()
assert second.getresponse().read() == b"finished"
assert not waiting.result
finally:
release[0].set()
release[1].set()
assert first.getresponse().read() == b"finished"
Loading