summaryrefslogtreecommitdiff
path: root/tests/integration/harness.py
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-27 01:15:54 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-27 01:15:54 +0100
commit128064c48949502e63ed20ca409557b4fa5de2af (patch)
tree75031c4f2a6476aabdb41c5cf8a9722b8a72ec96 /tests/integration/harness.py
parentf9262b8d3bbb9fd4ab65d70bf8185023081f1132 (diff)
Massively increase coverage by pulling fullscrapes and sending udp packets
Diffstat (limited to 'tests/integration/harness.py')
-rw-r--r--tests/integration/harness.py38
1 files changed, 35 insertions, 3 deletions
diff --git a/tests/integration/harness.py b/tests/integration/harness.py
index 868075d..7e75574 100644
--- a/tests/integration/harness.py
+++ b/tests/integration/harness.py
@@ -60,17 +60,20 @@ def free_port():
60class Tracker: 60class Tracker:
61 """One tracker process, listening on loopback.""" 61 """One tracker process, listening on loopback."""
62 62
63 def __init__(self, extra_config=""): 63 def __init__(self, extra_config="", files=None):
64 self.port = free_port() 64 self.port = free_port()
65 self.dir = tempfile.mkdtemp(prefix="ot-integration-") 65 self.dir = tempfile.mkdtemp(prefix="ot-integration-")
66 self.log = None 66 self.log = None
67 self.process = None 67 self.process = None
68 try: 68 try:
69 for name, content in (files or {}).items():
70 with open(os.path.join(self.dir, name), "wb") as handle:
71 handle.write(content)
69 self.config = os.path.join(self.dir, "opentracker.conf") 72 self.config = os.path.join(self.dir, "opentracker.conf")
70 with open(self.config, "w") as handle: 73 with open(self.config, "w") as handle:
71 handle.write("listen.tcp 127.0.0.1:%d\n" % self.port) 74 handle.write("listen.tcp 127.0.0.1:%d\n" % self.port)
72 handle.write("listen.udp 127.0.0.1:%d\n" % self.port) 75 handle.write("listen.udp 127.0.0.1:%d\n" % self.port)
73 handle.write(extra_config) 76 handle.write(extra_config.replace("{dir}", self.dir))
74 self.log = open(os.path.join(self.dir, "tracker.log"), "w+b") 77 self.log = open(os.path.join(self.dir, "tracker.log"), "w+b")
75 # Leak detection is left at the sanitizer's own default, which is on 78 # Leak detection is left at the sanitizer's own default, which is on
76 # where LeakSanitizer exists and off where it does not. Asking for it 79 # where LeakSanitizer exists and off where it does not. Asking for it
@@ -154,6 +157,12 @@ class Tracker:
154 def stats(self, mode): 157 def stats(self, mode):
155 return self.get("/stats?mode=" + mode) 158 return self.get("/stats?mode=" + mode)
156 159
160 def get_with_headers(self, path, headers=None, timeout=10):
161 """The body and the response headers, for the compressed answers."""
162 request = urllib.request.Request("http://127.0.0.1:%d%s" % (self.port, path), headers=headers or {})
163 with urllib.request.urlopen(request, timeout=timeout) as response:
164 return response.read(), dict(response.headers)
165
157 def torrent_count(self): 166 def torrent_count(self):
158 return int(self.stats("torr").split()[0]) 167 return int(self.stats("torr").split()[0])
159 168
@@ -180,14 +189,37 @@ class Tracker:
180 assert action == 1, "udp announce answered action %d" % action 189 assert action == 1, "udp announce answered action %d" % action
181 return {"interval": interval, "leechers": leechers, "seeders": seeders, "peers": answer[20:]} 190 return {"interval": interval, "leechers": leechers, "seeders": seeders, "peers": answer[20:]}
182 191
192 def udp_scrape(self, sock, connection_id, hashes):
193 """One (seeders, completed, leechers) triple per hash, in order."""
194 sock.sendto(struct.pack(">QII", connection_id, 2, 3) + b"".join(hashes), ("127.0.0.1", self.port))
195 answer = sock.recv(2048)
196 action, _ = struct.unpack(">II", answer[:8])
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))]
199
200 def udp_raw(self, payload, timeout=1.0):
201 """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."""
203 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
204 sock.settimeout(timeout)
205 try:
206 sock.sendto(payload, ("127.0.0.1", self.port))
207 try:
208 return sock.recv(2048)
209 except socket.timeout:
210 return None
211 finally:
212 sock.close()
213
183 214
184class TrackerTestCase(unittest.TestCase): 215class TrackerTestCase(unittest.TestCase):
185 """Gives every test its own tracker and fails it on any sanitizer output.""" 216 """Gives every test its own tracker and fails it on any sanitizer output."""
186 217
187 config = "" 218 config = ""
219 files = None
188 220
189 def setUp(self): 221 def setUp(self):
190 self.tracker = Tracker(self.config) 222 self.tracker = Tracker(self.config, self.files)
191 223
192 def tearDown(self): 224 def tearDown(self):
193 complaints = self.tracker.stop() 225 complaints = self.tracker.stop()