diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-27 03:06:27 +0100 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-27 03:06:27 +0100 |
| commit | 90e265483c1c73aed67ea4b8bce2b825c04d656a (patch) | |
| tree | f9f3b56ae01d38f5d03239fc7461f33c2874738a | |
| parent | fa0f7ee6dba3ba14a6fc1172e04a161f514d94b0 (diff) | |
Add new test for connection keep alive
| -rw-r--r-- | tests/integration/harness.py | 78 |
1 files changed, 78 insertions, 0 deletions
diff --git a/tests/integration/harness.py b/tests/integration/harness.py index 7e75574..03a0561 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py | |||
| @@ -197,6 +197,10 @@ class Tracker: | |||
| 197 | assert action == 2, "udp scrape answered action %d" % action | 197 | assert action == 2, "udp scrape answered action %d" % action |
| 198 | return [struct.unpack(">III", answer[8 + 12 * i : 20 + 12 * i]) for i in range(len(hashes))] | 198 | return [struct.unpack(">III", answer[8 + 12 * i : 20 + 12 * i]) for i in range(len(hashes))] |
| 199 | 199 | ||
| 200 | def connect(self): | ||
| 201 | """A connection the caller drives and closes.""" | ||
| 202 | return Connection(self.port) | ||
| 203 | |||
| 200 | def udp_raw(self, payload, timeout=1.0): | 204 | def udp_raw(self, payload, timeout=1.0): |
| 201 | """Sends a packet exactly as given. Returns the answer, or None if the | 205 | """Sends a packet exactly as given. Returns the answer, or None if the |
| 202 | tracker chose to say nothing, which is how it treats a broken one.""" | 206 | tracker chose to say nothing, which is how it treats a broken one.""" |
| @@ -212,6 +216,73 @@ class Tracker: | |||
| 212 | sock.close() | 216 | sock.close() |
| 213 | 217 | ||
| 214 | 218 | ||
| 219 | class Connection: | ||
| 220 | """One HTTP connection the caller keeps open, which urllib will not do. | ||
| 221 | |||
| 222 | Reads exactly one reply at a time, so a test can tell "the tracker | ||
| 223 | answered and kept the socket" from "the tracker answered and hung up".""" | ||
| 224 | |||
| 225 | def __init__(self, port, timeout=5): | ||
| 226 | self.sock = socket.create_connection(("127.0.0.1", port), timeout=timeout) | ||
| 227 | self.sock.settimeout(timeout) | ||
| 228 | self.buffer = b"" | ||
| 229 | |||
| 230 | def send(self, path, connection="Keep-Alive", version="1.1"): | ||
| 231 | request = "GET %s HTTP/%s\r\nHost: tracker\r\n" % (path, version) | ||
| 232 | if connection is not None: | ||
| 233 | request += "Connection: %s\r\n" % connection | ||
| 234 | self.sock.sendall((request + "\r\n").encode()) | ||
| 235 | |||
| 236 | def _fill(self): | ||
| 237 | chunk = self.sock.recv(65536) | ||
| 238 | if not chunk: | ||
| 239 | raise EOFError("the tracker closed the connection") | ||
| 240 | self.buffer += chunk | ||
| 241 | |||
| 242 | def reply(self): | ||
| 243 | """The one complete reply at the front of the stream, headers and body.""" | ||
| 244 | while b"\r\n\r\n" not in self.buffer: | ||
| 245 | self._fill() | ||
| 246 | head, rest = self.buffer.split(b"\r\n\r\n", 1) | ||
| 247 | headers = {} | ||
| 248 | for line in head.split(b"\r\n")[1:]: | ||
| 249 | name, _, value = line.partition(b":") | ||
| 250 | headers[name.strip().lower()] = value.strip() | ||
| 251 | |||
| 252 | if headers.get(b"transfer-encoding") == b"chunked": | ||
| 253 | while not rest.endswith(b"0\r\n\r\n"): | ||
| 254 | self.buffer = head + b"\r\n\r\n" + rest | ||
| 255 | self._fill() | ||
| 256 | rest = self.buffer.split(b"\r\n\r\n", 1)[1] | ||
| 257 | body, self.buffer = rest, b"" | ||
| 258 | else: | ||
| 259 | length = int(headers.get(b"content-length", b"0")) | ||
| 260 | while len(rest) < length: | ||
| 261 | self.buffer = head + b"\r\n\r\n" + rest | ||
| 262 | self._fill() | ||
| 263 | rest = self.buffer.split(b"\r\n\r\n", 1)[1] | ||
| 264 | body, self.buffer = rest[:length], rest[length:] | ||
| 265 | return head.split(b"\r\n")[0], headers, body | ||
| 266 | |||
| 267 | def is_open(self): | ||
| 268 | """True if the tracker is still holding the connection.""" | ||
| 269 | try: | ||
| 270 | self.sock.settimeout(0.4) | ||
| 271 | return self.sock.recv(1) != b"" | ||
| 272 | except socket.timeout: | ||
| 273 | return True | ||
| 274 | except OSError: | ||
| 275 | return False | ||
| 276 | finally: | ||
| 277 | try: | ||
| 278 | self.sock.settimeout(5) | ||
| 279 | except OSError: | ||
| 280 | pass | ||
| 281 | |||
| 282 | def close(self): | ||
| 283 | self.sock.close() | ||
| 284 | |||
| 285 | |||
| 215 | class TrackerTestCase(unittest.TestCase): | 286 | class TrackerTestCase(unittest.TestCase): |
| 216 | """Gives every test its own tracker and fails it on any sanitizer output.""" | 287 | """Gives every test its own tracker and fails it on any sanitizer output.""" |
| 217 | 288 | ||
| @@ -222,6 +293,13 @@ class TrackerTestCase(unittest.TestCase): | |||
| 222 | self.tracker = Tracker(self.config, self.files) | 293 | self.tracker = Tracker(self.config, self.files) |
| 223 | 294 | ||
| 224 | def tearDown(self): | 295 | def tearDown(self): |
| 296 | # A connection the test closed is only reaped when the event loop gets | ||
| 297 | # round to it, and a cookie still held at exit reads as a leak. One | ||
| 298 | # more request puts us behind that work in the loop's queue. | ||
| 299 | try: | ||
| 300 | self.tracker.torrent_count() | ||
| 301 | except Exception: | ||
| 302 | pass | ||
| 225 | complaints = self.tracker.stop() | 303 | complaints = self.tracker.stop() |
| 226 | self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) | 304 | self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) |
| 227 | 305 | ||
