Skip to content

Commit 0a3e6ac

Browse files
committed
Add more tests
1 parent 51afae6 commit 0a3e6ac

1 file changed

Lines changed: 83 additions & 9 deletions

File tree

tests/test_server_startup.py

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import time
66

77
import pytest
8+
import requests
9+
from requests.exceptions import Timeout
810

911
from pytest_httpserver import HTTPServer
1012

1113

12-
def test_server_ready_immediately_after_start():
14+
def test_server_ready_immediately_after_start() -> None:
1315
"""Test that the server accepts connections immediately after start() returns."""
1416
server = HTTPServer(host="localhost", port=0)
1517
server.expect_request("/").respond_with_data("ok")
@@ -22,7 +24,7 @@ def test_server_ready_immediately_after_start():
2224
server.stop()
2325

2426

25-
def test_server_ready_under_load():
27+
def test_server_ready_under_load() -> None:
2628
"""Test that the server is ready even when started multiple times in succession."""
2729
for _ in range(10):
2830
server = HTTPServer(host="localhost", port=0)
@@ -38,7 +40,7 @@ def test_server_ready_under_load():
3840
class SlowStartServer(HTTPServer):
3941
"""A server subclass that simulates slow startup."""
4042

41-
def thread_target(self):
43+
def thread_target(self) -> None:
4244
time.sleep(0.5) # Simulate slow initialization
4345
self._server_ready_event.set()
4446
assert self.server is not None
@@ -48,19 +50,19 @@ def thread_target(self):
4850
class NoReadyEventServer(HTTPServer):
4951
"""A server subclass that never signals readiness."""
5052

51-
def thread_target(self):
53+
def thread_target(self) -> None:
5254
assert self.server is not None
5355
self.server.serve_forever()
5456

5557

56-
def test_slow_start_server_waits_for_ready():
58+
def test_slow_start_server_waits_for_ready() -> None:
5759
"""Test that start() waits for slow thread_target implementations."""
5860
server = SlowStartServer(host="localhost", port=0)
5961
server.expect_request("/").respond_with_data("ok")
6062

61-
start_time = time.time()
63+
start_time = time.monotonic()
6264
server.start()
63-
elapsed = time.time() - start_time
65+
elapsed = time.monotonic() - start_time
6466

6567
try:
6668
# Should have waited at least 0.5 seconds
@@ -72,7 +74,7 @@ def test_slow_start_server_waits_for_ready():
7274
server.stop()
7375

7476

75-
def test_new_event_created_for_each_start():
77+
def test_new_event_created_for_each_start() -> None:
7678
"""Test that a new event is created for each start() to isolate retries."""
7779
server = HTTPServer(host="localhost", port=0)
7880
server.expect_request("/").respond_with_data("ok")
@@ -92,7 +94,7 @@ def test_new_event_created_for_each_start():
9294
assert second_start_event is not first_start_event
9395

9496

95-
def test_warns_when_ready_event_not_set():
97+
def test_warns_when_ready_event_not_set() -> None:
9698
"""Test that a warning is emitted when the ready event is never set."""
9799
server = NoReadyEventServer(host="localhost", port=0, startup_timeout=0.0)
98100
server.expect_request("/").respond_with_data("ok")
@@ -112,3 +114,75 @@ def test_warns_when_ready_event_not_set():
112114
raise AssertionError("Server did not accept connections within 1 second")
113115
finally:
114116
server.stop()
117+
118+
119+
class SlowServeServer(HTTPServer):
120+
"""A server that delays serve_forever() but does not set ready event early.
121+
122+
This simulates the scenario where:
123+
- bind() and listen() complete (TCP connections queue in backlog)
124+
- But serve_forever() hasn't started yet (no HTTP responses)
125+
"""
126+
127+
def thread_target(self) -> None:
128+
assert self.server is not None
129+
# Delay before serve_forever - connections will queue but not be processed
130+
time.sleep(3.0)
131+
self._server_ready_event.set()
132+
self.server.serve_forever()
133+
134+
135+
def test_http_request_fails_before_serve_forever_without_wait() -> None:
136+
"""
137+
Demonstrate the race condition: TCP connects but HTTP times out.
138+
139+
This test shows why waiting for server readiness matters:
140+
- After start(), TCP connections succeed (queued in backlog)
141+
- But HTTP requests timeout because serve_forever() hasn't started
142+
- With short client timeouts (common in production), this causes failures
143+
"""
144+
# Use startup_timeout=0 to NOT wait for ready event (old behavior)
145+
server = SlowServeServer(host="localhost", port=0, startup_timeout=0.0)
146+
server.expect_request("/ping").respond_with_data("pong")
147+
148+
with pytest.warns(UserWarning, match="ready event was not set"):
149+
server.start()
150+
151+
try:
152+
# TCP connection succeeds (proves Zsolt's point about backlog)
153+
sock = socket.create_connection((server.host, server.port), timeout=1)
154+
sock.close()
155+
156+
# But HTTP request with short timeout fails!
157+
# This is the actual problem in containerized environments
158+
with pytest.raises(Timeout):
159+
requests.get(server.url_for("/ping"), timeout=(0.5, 0.5))
160+
finally:
161+
server.stop()
162+
163+
164+
def test_http_request_succeeds_when_waiting_for_ready() -> None:
165+
"""
166+
Demonstrate that waiting for ready event fixes the race condition.
167+
168+
With startup_timeout enabled (default), start() waits until
169+
serve_forever() begins, so HTTP requests succeed immediately.
170+
"""
171+
# Use default startup_timeout to wait for ready event
172+
server = SlowServeServer(host="localhost", port=0) # default startup_timeout=10.0
173+
server.expect_request("/ping").respond_with_data("pong")
174+
175+
start_time = time.monotonic()
176+
server.start()
177+
elapsed = time.monotonic() - start_time
178+
179+
try:
180+
# Should have waited for the slow startup
181+
assert elapsed >= 3.0, f"Expected to wait >= 3.0s, but only waited {elapsed}s"
182+
183+
# HTTP request succeeds because serve_forever() has started
184+
response = requests.get(server.url_for("/ping"), timeout=(0.5, 0.5))
185+
assert response.status_code == 200
186+
assert response.text == "pong"
187+
finally:
188+
server.stop()

0 commit comments

Comments
 (0)