Skip to content

Commit ea16865

Browse files
SAKMZclaude
andcommitted
test: harden integration tests against transient loopback resets
Shared retry helper for both the HTTP and raw-CONNECT paths (6 tries, backoff), longer timeouts, and a small delay between rotation requests. Fixes occasional Windows-CI flakiness without changing library behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent eaf93b9 commit ea16865

1 file changed

Lines changed: 39 additions & 23 deletions

File tree

‎tests/test_relay.py‎

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -244,36 +244,51 @@ def setUpClass(cls):
244244
def tearDownClass(cls):
245245
cls.origin.shutdown()
246246

247-
def _get(self, relay, path="/hi", retries=4):
248-
# Loopback connections under CI load can occasionally reset; retry the
249-
# transient ones so the rotation/throughput tests stay deterministic.
247+
@staticmethod
248+
def _retry(fn, retries=6):
249+
# Loopback connections under CI load (especially Windows) can reset;
250+
# retry transient failures so the integration tests stay deterministic.
250251
last = None
251252
for attempt in range(retries):
252253
try:
253-
opener = urllib.request.build_opener(
254-
urllib.request.ProxyHandler({"http": relay.url}))
255-
return opener.open(
256-
f"http://127.0.0.1:{self.origin_port}{path}", timeout=10
257-
).read().decode()
254+
return fn()
258255
except Exception as e: # noqa: BLE001 - retry transient socket errors
259256
last = e
260-
time.sleep(0.2 * (attempt + 1))
257+
time.sleep(0.15 * (attempt + 1))
261258
raise last
262259

263-
def _connect_get(self, relay, path="/tunnel"):
264-
s = socket.create_connection((relay.listen_host, relay.port), timeout=10)
265-
s.sendall(f"CONNECT 127.0.0.1:{self.origin_port} HTTP/1.1\r\n"
266-
f"Host: 127.0.0.1:{self.origin_port}\r\n\r\n".encode())
267-
self.assertIn(b"200", s.recv(4096).split(b"\r\n", 1)[0])
268-
s.sendall(f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n".encode())
269-
data = b""
270-
while True:
271-
chunk = s.recv(4096)
272-
if not chunk:
273-
break
274-
data += chunk
275-
s.close()
276-
return data
260+
def _get(self, relay, path="/hi", retries=6):
261+
def once():
262+
opener = urllib.request.build_opener(
263+
urllib.request.ProxyHandler({"http": relay.url}))
264+
return opener.open(
265+
f"http://127.0.0.1:{self.origin_port}{path}", timeout=15
266+
).read().decode()
267+
return self._retry(once, retries=retries)
268+
269+
def _connect_get(self, relay, path="/tunnel", retries=6):
270+
def once():
271+
s = socket.create_connection((relay.listen_host, relay.port), timeout=15)
272+
try:
273+
s.sendall(f"CONNECT 127.0.0.1:{self.origin_port} HTTP/1.1\r\n"
274+
f"Host: 127.0.0.1:{self.origin_port}\r\n\r\n".encode())
275+
status = s.recv(4096).split(b"\r\n", 1)[0]
276+
if b"200" not in status:
277+
raise ConnectionError(f"CONNECT not established: {status!r}")
278+
s.sendall(f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1\r\n"
279+
f"Connection: close\r\n\r\n".encode())
280+
data = b""
281+
while True:
282+
chunk = s.recv(4096)
283+
if not chunk:
284+
break
285+
data += chunk
286+
if b"OK " not in data:
287+
raise ConnectionError("no response body")
288+
return data
289+
finally:
290+
s.close()
291+
return self._retry(once, retries=retries)
277292

278293

279294
class TestSocks5(_Base):
@@ -359,6 +374,7 @@ def test_roundrobin_uses_both(self):
359374
with ProxyRelay(pool, rotation="roundrobin") as relay:
360375
for _ in range(4):
361376
self.assertEqual(self._get(relay, "/x"), "OK /x")
377+
time.sleep(0.05)
362378
# Round-robin must have used BOTH upstreams (exact split can shift by one
363379
# if a transient loopback reset triggered a retry).
364380
self.assertGreaterEqual(self.a.conns, 1)

0 commit comments

Comments
 (0)