From e3efacd05d8a74b2f753dd8afbfe62baa1e0770e Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Fri, 30 Oct 2026 01:44:40 +0100 Subject: Check for free port in the udp integration test, also speed it up by a bit --- tests/integration/harness.py | 26 ++++++++++++++++++++------ tests/integration/test_expiry.py | 37 +++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/tests/integration/harness.py b/tests/integration/harness.py index 122c36d..b98ce83 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py @@ -49,12 +49,26 @@ def bdecode(data): return value(0)[1] -def free_port(): - probe = socket.socket() - probe.bind(("127.0.0.1", 0)) - port = probe.getsockname()[1] - probe.close() - return port +def free_port(attempts=50): + """A port free for both protocols, because the tracker binds both. + + Asking the kernel for a free tcp port says nothing about the udp port of + the same number, and anything on the machine holding that one makes the + tracker exit at once with "Address already in use". + """ + for _ in range(attempts): + tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + tcp.bind(("127.0.0.1", 0)) + udp.bind(("127.0.0.1", tcp.getsockname()[1])) + return tcp.getsockname()[1] + except OSError: + continue + finally: + tcp.close() + udp.close() + raise AssertionError("no port free for both tcp and udp after %d tries" % attempts) class Tracker: diff --git a/tests/integration/test_expiry.py b/tests/integration/test_expiry.py index 4466724..5a21e57 100644 --- a/tests/integration/test_expiry.py +++ b/tests/integration/test_expiry.py @@ -1,15 +1,19 @@ -"""What the expiry sweep finds after a connection has already gone. +"""What the expiry sweep finds, and what it must not find. -A plain request is answered and the connection closed by http_senddata, not -by the sweep. If that path leaves the descriptor's slot behind, the sweep -walks into it once the client timeout has passed and frees a cookie that is -already gone. Under the sanitizer build that is reported, and teardown turns -it into a failure. +A plain request is answered and the connection closed by http_close, not by +the sweep. If that path left the descriptor's slot behind, the sweep would +walk into it once the client timeout had passed and free a cookie that is +already gone; the sanitizer build reports that and teardown turns it into a +failure. + +Both halves share one tracker and one wait, because the wait is the whole +cost of the test. The two numbers below mirror OT_CLIENT_TIMEOUT and OT_CLIENT_TIMEOUT_CHECKINTERVAL in FEATURES_integration. """ +import socket import time import unittest @@ -20,23 +24,16 @@ CHECK_INTERVAL = 1 class ExpirySweep(TrackerTestCase): - def test_an_answered_request_leaves_nothing_behind(self): - info_hash = self.hash_for("expiry-sweep") - self.assertEqual(self.tracker.announce(info_hash)[b"complete"], 1) - - # Long enough for the slot to expire and for a sweep to visit it - time.sleep(CLIENT_TIMEOUT + 3 * CHECK_INTERVAL) - - self.assertEqual(self.tracker.torrent_count(), 1, "the tracker stopped serving while sweeping") - - def test_a_connection_that_never_speaks_is_swept(self): - """The other side of it: a slot that really is live must still go.""" - import socket - + def test_the_sweep_takes_the_idle_and_leaves_the_answered(self): idle = socket.create_connection(("127.0.0.1", self.tracker.port), timeout=5) try: + self.assertEqual(self.tracker.announce(self.hash_for("expiry-sweep"))[b"complete"], 1) + time.sleep(CLIENT_TIMEOUT + 3 * CHECK_INTERVAL) + + self.assertEqual(self.tracker.torrent_count(), 1, "the tracker stopped serving while sweeping") + idle.settimeout(5) - self.assertEqual(idle.recv(64), b"", "the tracker held a connection that never sent a request") + self.assertEqual(idle.recv(64), b"", "a connection that never sent a request was not swept") finally: idle.close() -- cgit v1.3