diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/integration/.gitignore | 2 | ||||
| -rw-r--r-- | tests/integration/harness.py | 308 | ||||
| -rw-r--r-- | tests/integration/test_accesslist.py | 100 | ||||
| -rw-r--r-- | tests/integration/test_announce.py | 129 | ||||
| -rw-r--r-- | tests/integration/test_dualstack.py | 89 | ||||
| -rw-r--r-- | tests/integration/test_fullscrape.py | 146 | ||||
| -rw-r--r-- | tests/integration/test_keepalive.py | 111 | ||||
| -rw-r--r-- | tests/integration/test_stats.py | 169 | ||||
| -rw-r--r-- | tests/integration/test_udp.py | 168 | ||||
| -rw-r--r-- | tests/runner.c | 77 | ||||
| -rw-r--r-- | tests/t_event.c | 222 | ||||
| -rw-r--r-- | tests/t_fdstate.c | 224 | ||||
| -rw-r--r-- | tests/t_fmt.c | 108 | ||||
| -rw-r--r-- | tests/t_hex.c | 28 | ||||
| -rw-r--r-- | tests/t_iovec.c | 20 | ||||
| -rw-r--r-- | tests/t_ip.c | 54 | ||||
| -rw-r--r-- | tests/t_scan.c | 124 | ||||
| -rw-r--r-- | tests/t_urlencoded.c (renamed from tests/t_scan_urlencoded_query.c) | 95 | ||||
| -rw-r--r-- | tests/t_vector.c | 62 | ||||
| -rw-r--r-- | tests/test.h | 5 |
20 files changed, 2079 insertions, 162 deletions
diff --git a/tests/integration/.gitignore b/tests/integration/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/tests/integration/.gitignore | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | __pycache__/ | ||
| 2 | *.pyc | ||
diff --git a/tests/integration/harness.py b/tests/integration/harness.py new file mode 100644 index 0000000..03a0561 --- /dev/null +++ b/tests/integration/harness.py | |||
| @@ -0,0 +1,308 @@ | |||
| 1 | """Starts a real opentracker and talks to it over the wire. | ||
| 2 | |||
| 3 | Everything here drives the binary rather than the code, so these tests cover | ||
| 4 | what the unit suite cannot reach: the event loop, the HTTP parser and the | ||
| 5 | torrent store working together. Run against a sanitizer build and the tracker | ||
| 6 | reports its own memory errors, which the teardown turns into test failures. | ||
| 7 | """ | ||
| 8 | |||
| 9 | import os | ||
| 10 | import shutil | ||
| 11 | import signal | ||
| 12 | import socket | ||
| 13 | import struct | ||
| 14 | import subprocess | ||
| 15 | import sys | ||
| 16 | import tempfile | ||
| 17 | import time | ||
| 18 | import unittest | ||
| 19 | import urllib.error | ||
| 20 | import urllib.parse | ||
| 21 | import urllib.request | ||
| 22 | |||
| 23 | TRACKER = os.environ.get("OPENTRACKER", os.path.join(os.path.dirname(__file__), "..", "..", "opentracker.debug")) | ||
| 24 | |||
| 25 | |||
| 26 | def bdecode(data): | ||
| 27 | """Enough bencode for what a tracker answers.""" | ||
| 28 | |||
| 29 | def value(i): | ||
| 30 | if data[i : i + 1] == b"d": | ||
| 31 | i, out = i + 1, {} | ||
| 32 | while data[i : i + 1] != b"e": | ||
| 33 | i, key = value(i) | ||
| 34 | i, out[key] = value(i) | ||
| 35 | return i + 1, out | ||
| 36 | if data[i : i + 1] == b"l": | ||
| 37 | i, out = i + 1, [] | ||
| 38 | while data[i : i + 1] != b"e": | ||
| 39 | i, item = value(i) | ||
| 40 | out.append(item) | ||
| 41 | return i + 1, out | ||
| 42 | if data[i : i + 1] == b"i": | ||
| 43 | end = data.index(b"e", i) | ||
| 44 | return end + 1, int(data[i + 1 : end]) | ||
| 45 | colon = data.index(b":", i) | ||
| 46 | length = int(data[i:colon]) | ||
| 47 | return colon + 1 + length, data[colon + 1 : colon + 1 + length] | ||
| 48 | |||
| 49 | return value(0)[1] | ||
| 50 | |||
| 51 | |||
| 52 | def free_port(): | ||
| 53 | probe = socket.socket() | ||
| 54 | probe.bind(("127.0.0.1", 0)) | ||
| 55 | port = probe.getsockname()[1] | ||
| 56 | probe.close() | ||
| 57 | return port | ||
| 58 | |||
| 59 | |||
| 60 | class Tracker: | ||
| 61 | """One tracker process, listening on loopback.""" | ||
| 62 | |||
| 63 | def __init__(self, extra_config="", files=None): | ||
| 64 | self.port = free_port() | ||
| 65 | self.dir = tempfile.mkdtemp(prefix="ot-integration-") | ||
| 66 | self.log = None | ||
| 67 | self.process = None | ||
| 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) | ||
| 72 | self.config = os.path.join(self.dir, "opentracker.conf") | ||
| 73 | with open(self.config, "w") as handle: | ||
| 74 | handle.write("listen.tcp 127.0.0.1:%d\n" % self.port) | ||
| 75 | handle.write("listen.udp 127.0.0.1:%d\n" % self.port) | ||
| 76 | handle.write(extra_config.replace("{dir}", self.dir)) | ||
| 77 | self.log = open(os.path.join(self.dir, "tracker.log"), "w+b") | ||
| 78 | # Leak detection is left at the sanitizer's own default, which is on | ||
| 79 | # where LeakSanitizer exists and off where it does not. Asking for it | ||
| 80 | # by name makes the tracker refuse to start on macOS and FreeBSD. | ||
| 81 | environment = dict(os.environ, ASAN_OPTIONS="abort_on_error=0") | ||
| 82 | self.process = subprocess.Popen([TRACKER, "-f", self.config], stdout=self.log, stderr=self.log, env=environment) | ||
| 83 | self._wait_for_port() | ||
| 84 | except BaseException: | ||
| 85 | self._cleanup() | ||
| 86 | raise | ||
| 87 | |||
| 88 | def _cleanup(self): | ||
| 89 | """Close what we opened and take the scratch directory with it.""" | ||
| 90 | if self.process is not None and self.process.poll() is None: | ||
| 91 | self.process.kill() | ||
| 92 | self.process.wait() | ||
| 93 | if self.log is not None: | ||
| 94 | self.log.close() | ||
| 95 | self.log = None | ||
| 96 | shutil.rmtree(self.dir, ignore_errors=True) | ||
| 97 | |||
| 98 | def _wait_for_port(self): | ||
| 99 | for _ in range(200): | ||
| 100 | if self.process.poll() is not None: | ||
| 101 | raise AssertionError("tracker exited at once:\n" + self.output()) | ||
| 102 | try: | ||
| 103 | probe = socket.create_connection(("127.0.0.1", self.port), timeout=0.2) | ||
| 104 | probe.close() | ||
| 105 | return | ||
| 106 | except OSError: | ||
| 107 | time.sleep(0.05) | ||
| 108 | raise AssertionError("tracker never listened on %d:\n%s" % (self.port, self.output())) | ||
| 109 | |||
| 110 | def output(self): | ||
| 111 | if self.log is None: | ||
| 112 | return "" | ||
| 113 | self.log.flush() | ||
| 114 | self.log.seek(0) | ||
| 115 | return self.log.read().decode("utf-8", "replace") | ||
| 116 | |||
| 117 | def stop(self): | ||
| 118 | """Returns anything the tracker complained about on the way out. | ||
| 119 | |||
| 120 | The interrupt rather than a kill is what makes the leak check work at | ||
| 121 | all: LeakSanitizer reports from an exit handler, so a killed tracker | ||
| 122 | reports nothing and the suite passes over leaks it never looked for. | ||
| 123 | """ | ||
| 124 | if self.process is not None and self.process.poll() is None: | ||
| 125 | self.process.send_signal(signal.SIGINT) | ||
| 126 | try: | ||
| 127 | self.process.wait(timeout=5) | ||
| 128 | except subprocess.TimeoutExpired: | ||
| 129 | self.process.kill() | ||
| 130 | self.process.wait() | ||
| 131 | complaints = [ | ||
| 132 | line | ||
| 133 | for line in self.output().splitlines() | ||
| 134 | if "Sanitizer" in line or "runtime error" in line or line.startswith("ERROR") | ||
| 135 | ] | ||
| 136 | self._cleanup() | ||
| 137 | return complaints | ||
| 138 | |||
| 139 | # ----------------------------------------------------------- over http | ||
| 140 | |||
| 141 | def get(self, path, timeout=10): | ||
| 142 | return urllib.request.urlopen("http://127.0.0.1:%d%s" % (self.port, path), timeout=timeout).read() | ||
| 143 | |||
| 144 | def announce(self, info_hash, peer_id=b"-OT0001-aaaaaaaaaaaa", port=6881, left=0, **extra): | ||
| 145 | query = { | ||
| 146 | "info_hash": urllib.parse.quote_from_bytes(info_hash, safe=""), | ||
| 147 | "peer_id": urllib.parse.quote_from_bytes(peer_id, safe=""), | ||
| 148 | "port": port, | ||
| 149 | "left": left, | ||
| 150 | } | ||
| 151 | query.update(extra) | ||
| 152 | return bdecode(self.get("/announce?" + "&".join("%s=%s" % item for item in query.items()))) | ||
| 153 | |||
| 154 | def scrape(self, info_hash): | ||
| 155 | return bdecode(self.get("/scrape?info_hash=" + urllib.parse.quote_from_bytes(info_hash, safe=""))) | ||
| 156 | |||
| 157 | def stats(self, mode): | ||
| 158 | return self.get("/stats?mode=" + mode) | ||
| 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 | |||
| 166 | def torrent_count(self): | ||
| 167 | return int(self.stats("torr").split()[0]) | ||
| 168 | |||
| 169 | # ------------------------------------------------------------ over udp | ||
| 170 | |||
| 171 | def udp_connect(self): | ||
| 172 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
| 173 | sock.settimeout(5) | ||
| 174 | sock.sendto(struct.pack(">QII", 0x41727101980, 0, 1), ("127.0.0.1", self.port)) | ||
| 175 | answer = sock.recv(2048) | ||
| 176 | action, _, connection_id = struct.unpack(">IIQ", answer[:16]) | ||
| 177 | assert action == 0, "udp connect answered action %d" % action | ||
| 178 | return sock, connection_id | ||
| 179 | |||
| 180 | def udp_announce(self, sock, connection_id, info_hash, left=0, event=0, port=6881, numwant=-1): | ||
| 181 | sock.sendto( | ||
| 182 | struct.pack( | ||
| 183 | ">QII20s20sQQQIIIiH", connection_id, 1, 2, info_hash, b"p" * 20, 0, left, 0, event, 0, 0, numwant, port | ||
| 184 | ), | ||
| 185 | ("127.0.0.1", self.port), | ||
| 186 | ) | ||
| 187 | answer = sock.recv(2048) | ||
| 188 | action, _, interval, leechers, seeders = struct.unpack(">IIIII", answer[:20]) | ||
| 189 | assert action == 1, "udp announce answered action %d" % action | ||
| 190 | return {"interval": interval, "leechers": leechers, "seeders": seeders, "peers": answer[20:]} | ||
| 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 connect(self): | ||
| 201 | """A connection the caller drives and closes.""" | ||
| 202 | return Connection(self.port) | ||
| 203 | |||
| 204 | def udp_raw(self, payload, timeout=1.0): | ||
| 205 | """Sends a packet exactly as given. Returns the answer, or None if the | ||
| 206 | tracker chose to say nothing, which is how it treats a broken one.""" | ||
| 207 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
| 208 | sock.settimeout(timeout) | ||
| 209 | try: | ||
| 210 | sock.sendto(payload, ("127.0.0.1", self.port)) | ||
| 211 | try: | ||
| 212 | return sock.recv(2048) | ||
| 213 | except socket.timeout: | ||
| 214 | return None | ||
| 215 | finally: | ||
| 216 | sock.close() | ||
| 217 | |||
| 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 | |||
| 286 | class TrackerTestCase(unittest.TestCase): | ||
| 287 | """Gives every test its own tracker and fails it on any sanitizer output.""" | ||
| 288 | |||
| 289 | config = "" | ||
| 290 | files = None | ||
| 291 | |||
| 292 | def setUp(self): | ||
| 293 | self.tracker = Tracker(self.config, self.files) | ||
| 294 | |||
| 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 | ||
| 303 | complaints = self.tracker.stop() | ||
| 304 | self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) | ||
| 305 | |||
| 306 | def hash_for(self, name): | ||
| 307 | """A distinct info hash per test, so tests cannot leak into each other.""" | ||
| 308 | return (name.encode() + b"\0" * 20)[:20] | ||
diff --git a/tests/integration/test_accesslist.py b/tests/integration/test_accesslist.py new file mode 100644 index 0000000..41ff654 --- /dev/null +++ b/tests/integration/test_accesslist.py | |||
| @@ -0,0 +1,100 @@ | |||
| 1 | """The blacklist, loaded from a file at startup. | ||
| 2 | |||
| 3 | A blacklisted info hash must be refused at both the announce and the scrape | ||
| 4 | entry, and the file parser has to cope with the shapes an operator actually | ||
| 5 | produces: an empty file, and a last line with no newline after it. | ||
| 6 | """ | ||
| 7 | |||
| 8 | import struct | ||
| 9 | import unittest | ||
| 10 | |||
| 11 | from harness import TrackerTestCase | ||
| 12 | |||
| 13 | BLOCKED = b"\xb1" * 20 | ||
| 14 | ALLOWED = b"\xa1" * 20 | ||
| 15 | |||
| 16 | |||
| 17 | def listing(*hashes): | ||
| 18 | return b"".join(h.hex().upper().encode() + b"\n" for h in hashes) | ||
| 19 | |||
| 20 | |||
| 21 | class Blacklist(TrackerTestCase): | ||
| 22 | config = "access.blacklist {dir}/blacklist\n" | ||
| 23 | files = {"blacklist": listing(BLOCKED)} | ||
| 24 | |||
| 25 | def test_a_blacklisted_hash_is_refused(self): | ||
| 26 | answer = self.tracker.announce(BLOCKED, port=6881, left=0) | ||
| 27 | self.assertIn(b"failure reason", answer) | ||
| 28 | self.assertIn(b"not authorized", answer[b"failure reason"]) | ||
| 29 | |||
| 30 | def test_a_blacklisted_hash_never_becomes_a_torrent(self): | ||
| 31 | before = self.tracker.torrent_count() | ||
| 32 | self.tracker.announce(BLOCKED, port=6881, left=0) | ||
| 33 | self.assertEqual(self.tracker.torrent_count(), before, "the refused hash was stored anyway") | ||
| 34 | |||
| 35 | def test_any_other_hash_is_served(self): | ||
| 36 | answer = self.tracker.announce(ALLOWED, port=6881, left=0) | ||
| 37 | self.assertNotIn(b"failure reason", answer) | ||
| 38 | self.assertEqual(answer[b"complete"], 1) | ||
| 39 | |||
| 40 | def test_the_block_is_an_error_packet_over_udp(self): | ||
| 41 | """BEP 15 has an error action, which is what a refusal should use. | ||
| 42 | It used to answer the announce header with no body behind it, a | ||
| 43 | packet too short for any client to read.""" | ||
| 44 | sock, connection_id = self.tracker.udp_connect() | ||
| 45 | sock.close() | ||
| 46 | request = struct.pack( | ||
| 47 | ">QII20s20sQQQIIIiH", connection_id, 1, 7, BLOCKED, b"p" * 20, 0, 0, 0, 0, 0, 0, -1, 6881 | ||
| 48 | ) | ||
| 49 | answer = self.tracker.udp_raw(request) | ||
| 50 | self.assertIsNotNone(answer) | ||
| 51 | action, transaction = struct.unpack(">II", answer[:8]) | ||
| 52 | self.assertEqual(action, 3, "action 3 is the error packet") | ||
| 53 | self.assertEqual(transaction, 7) | ||
| 54 | self.assertIn(b"not authorized", answer[8:]) | ||
| 55 | |||
| 56 | def test_a_blocked_hash_is_not_stored_when_it_arrives_over_udp(self): | ||
| 57 | sock, connection_id = self.tracker.udp_connect() | ||
| 58 | sock.close() | ||
| 59 | self.tracker.udp_raw( | ||
| 60 | struct.pack(">QII20s20sQQQIIIiH", connection_id, 1, 8, BLOCKED, b"p" * 20, 0, 0, 0, 0, 0, 0, -1, 6881) | ||
| 61 | ) | ||
| 62 | self.assertEqual(self.tracker.torrent_count(), 0) | ||
| 63 | |||
| 64 | |||
| 65 | class BlacklistWithoutTrailingNewline(TrackerTestCase): | ||
| 66 | """A single hash with no newline after it used to size the list at zero | ||
| 67 | and then write into it, which took the tracker down before it listened.""" | ||
| 68 | |||
| 69 | config = "access.blacklist {dir}/blacklist\n" | ||
| 70 | files = {"blacklist": BLOCKED.hex().upper().encode()} | ||
| 71 | |||
| 72 | def test_the_last_line_counts_even_without_its_newline(self): | ||
| 73 | answer = self.tracker.announce(BLOCKED, port=6881, left=0) | ||
| 74 | self.assertIn(b"failure reason", answer) | ||
| 75 | |||
| 76 | def test_and_the_tracker_still_serves_everything_else(self): | ||
| 77 | self.assertEqual(self.tracker.announce(ALLOWED, port=6881, left=0)[b"complete"], 1) | ||
| 78 | |||
| 79 | |||
| 80 | class EmptyBlacklist(TrackerTestCase): | ||
| 81 | config = "access.blacklist {dir}/blacklist\n" | ||
| 82 | files = {"blacklist": b""} | ||
| 83 | |||
| 84 | def test_an_empty_file_blocks_nothing_and_starts_cleanly(self): | ||
| 85 | self.assertEqual(self.tracker.announce(BLOCKED, port=6881, left=0)[b"complete"], 1) | ||
| 86 | self.assertEqual(self.tracker.announce(ALLOWED, port=6882, left=0)[b"complete"], 1) | ||
| 87 | |||
| 88 | |||
| 89 | class MissingBlacklist(TrackerTestCase): | ||
| 90 | """The file is named but absent: the tracker warns and serves everything, | ||
| 91 | because it may be created later.""" | ||
| 92 | |||
| 93 | config = "access.blacklist {dir}/absent\n" | ||
| 94 | |||
| 95 | def test_a_missing_file_blocks_nothing(self): | ||
| 96 | self.assertEqual(self.tracker.announce(BLOCKED, port=6881, left=0)[b"complete"], 1) | ||
| 97 | |||
| 98 | |||
| 99 | if __name__ == "__main__": | ||
| 100 | unittest.main() | ||
diff --git a/tests/integration/test_announce.py b/tests/integration/test_announce.py new file mode 100644 index 0000000..ffbc07b --- /dev/null +++ b/tests/integration/test_announce.py | |||
| @@ -0,0 +1,129 @@ | |||
| 1 | """What a peer's announce does to the swarm it joins.""" | ||
| 2 | |||
| 3 | import os | ||
| 4 | import socket | ||
| 5 | import sys | ||
| 6 | import unittest | ||
| 7 | |||
| 8 | sys.path.insert(0, os.path.dirname(__file__)) | ||
| 9 | from harness import TrackerTestCase | ||
| 10 | |||
| 11 | |||
| 12 | def peers_of(answer, key=b"peers"): | ||
| 13 | """Compact peer list as a list of (address, port).""" | ||
| 14 | blob, out = answer.get(key, b""), [] | ||
| 15 | size = 6 if key == b"peers" else 18 | ||
| 16 | for offset in range(0, len(blob), size): | ||
| 17 | entry = blob[offset : offset + size] | ||
| 18 | address = socket.inet_ntop(socket.AF_INET if size == 6 else socket.AF_INET6, entry[:-2]) | ||
| 19 | out.append((address, entry[-2] << 8 | entry[-1])) | ||
| 20 | return out | ||
| 21 | |||
| 22 | |||
| 23 | class AnnounceLifetime(TrackerTestCase): | ||
| 24 | config = "" | ||
| 25 | |||
| 26 | def test_a_leecher_is_counted_as_incomplete(self): | ||
| 27 | info_hash = self.hash_for("leecher") | ||
| 28 | answer = self.tracker.announce(info_hash, left=100) | ||
| 29 | self.assertEqual(answer[b"complete"], 0) | ||
| 30 | self.assertEqual(answer[b"incomplete"], 1) | ||
| 31 | |||
| 32 | def test_no_bytes_left_means_seeding(self): | ||
| 33 | info_hash = self.hash_for("seeder") | ||
| 34 | answer = self.tracker.announce(info_hash, left=0) | ||
| 35 | self.assertEqual(answer[b"complete"], 1) | ||
| 36 | self.assertEqual(answer[b"incomplete"], 0) | ||
| 37 | |||
| 38 | def test_a_peer_that_finishes_moves_from_one_column_to_the_other(self): | ||
| 39 | info_hash = self.hash_for("finishes") | ||
| 40 | peer_id = b"-OT0001-finisher0001" | ||
| 41 | self.tracker.announce(info_hash, peer_id=peer_id, left=100) | ||
| 42 | self.assertEqual(self.tracker.announce(info_hash, peer_id=peer_id, left=100)[b"incomplete"], 1) | ||
| 43 | |||
| 44 | answer = self.tracker.announce(info_hash, peer_id=peer_id, left=0, event="completed") | ||
| 45 | self.assertEqual(answer[b"complete"], 1, "the peer should now count as a seeder") | ||
| 46 | self.assertEqual(answer[b"incomplete"], 0, "and no longer as a leecher") | ||
| 47 | |||
| 48 | def test_stopped_removes_the_peer(self): | ||
| 49 | info_hash = self.hash_for("stopped") | ||
| 50 | peer_id = b"-OT0001-leaves000001" | ||
| 51 | self.tracker.announce(info_hash, peer_id=peer_id, left=100) | ||
| 52 | self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"incomplete"], 1) | ||
| 53 | |||
| 54 | self.tracker.announce(info_hash, peer_id=peer_id, left=100, event="stopped") | ||
| 55 | self.assertEqual( | ||
| 56 | self.tracker.scrape(info_hash)[b"files"][info_hash][b"incomplete"], 0, "the peer should be gone" | ||
| 57 | ) | ||
| 58 | |||
| 59 | def test_the_same_peer_announcing_twice_is_still_one_peer(self): | ||
| 60 | info_hash = self.hash_for("twice") | ||
| 61 | peer_id = b"-OT0001-repeats00001" | ||
| 62 | for _ in range(5): | ||
| 63 | answer = self.tracker.announce(info_hash, peer_id=peer_id, left=100) | ||
| 64 | self.assertEqual(answer[b"incomplete"], 1, "re-announcing must not duplicate the peer") | ||
| 65 | |||
| 66 | def test_a_peer_is_its_address_and_port_not_its_peer_id(self): | ||
| 67 | """Two announces from one address and port are one peer, whatever | ||
| 68 | peer_id they carry, because that pair is the whole identity.""" | ||
| 69 | info_hash = self.hash_for("identity") | ||
| 70 | self.tracker.announce(info_hash, peer_id=b"-OT0001-a00000000001", port=6881, left=100) | ||
| 71 | answer = self.tracker.announce(info_hash, peer_id=b"-OT0001-b00000000002", port=6881, left=100) | ||
| 72 | self.assertEqual(answer[b"incomplete"], 1, "same address and port, so still one peer") | ||
| 73 | |||
| 74 | answer = self.tracker.announce(info_hash, peer_id=b"-OT0001-c00000000003", port=6882, left=100) | ||
| 75 | self.assertEqual(answer[b"incomplete"], 2, "a different port makes a different peer") | ||
| 76 | |||
| 77 | def test_a_downloaded_count_is_kept(self): | ||
| 78 | info_hash = self.hash_for("downloaded") | ||
| 79 | self.tracker.announce(info_hash, port=7001, left=0, event="completed") | ||
| 80 | self.tracker.announce(info_hash, port=7002, left=0, event="completed") | ||
| 81 | self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"downloaded"], 2) | ||
| 82 | |||
| 83 | def test_a_peer_is_given_the_whole_swarm_including_itself(self): | ||
| 84 | """opentracker does not filter the announcing peer out of its own | ||
| 85 | answer. Clients are expected to ignore themselves.""" | ||
| 86 | info_hash = self.hash_for("sees") | ||
| 87 | self.tracker.announce(info_hash, port=1111, left=100) | ||
| 88 | answer = self.tracker.announce(info_hash, port=2222, left=100) | ||
| 89 | |||
| 90 | ports = sorted(port for _, port in peers_of(answer)) | ||
| 91 | self.assertEqual(ports, [1111, 2222]) | ||
| 92 | |||
| 93 | def test_an_unknown_torrent_scrapes_as_empty(self): | ||
| 94 | answer = self.tracker.scrape(self.hash_for("never-seen")) | ||
| 95 | self.assertEqual(answer[b"files"], {}, "a torrent nobody announced has no entry") | ||
| 96 | |||
| 97 | |||
| 98 | class AnnounceRejects(TrackerTestCase): | ||
| 99 | def assert_rejected(self, query): | ||
| 100 | import urllib.error | ||
| 101 | |||
| 102 | with self.assertRaises(urllib.error.HTTPError) as caught: | ||
| 103 | self.tracker.get("/announce?" + query) | ||
| 104 | self.assertEqual(caught.exception.code, 400) | ||
| 105 | |||
| 106 | def test_a_short_info_hash_is_rejected(self): | ||
| 107 | self.assert_rejected("info_hash=tooshort&peer_id=" + "b" * 20 + "&port=6881&left=0") | ||
| 108 | |||
| 109 | def test_a_missing_info_hash_answers_a_failure_reason(self): | ||
| 110 | """Not a 400: the tracker answers 200 with a bencoded complaint, so | ||
| 111 | that a client shows the message to its user.""" | ||
| 112 | from harness import bdecode | ||
| 113 | |||
| 114 | answer = bdecode(self.tracker.get("/announce?peer_id=" + "b" * 20 + "&port=6881&left=0")) | ||
| 115 | self.assertIn(b"failure reason", answer) | ||
| 116 | self.assertIn(b"info_hash", answer[b"failure reason"]) | ||
| 117 | |||
| 118 | def test_a_port_out_of_range_is_rejected(self): | ||
| 119 | self.assert_rejected("info_hash=" + "a" * 20 + "&peer_id=" + "b" * 20 + "&port=65536&left=0") | ||
| 120 | |||
| 121 | def test_a_port_that_is_not_a_number_is_rejected(self): | ||
| 122 | self.assert_rejected("info_hash=" + "a" * 20 + "&peer_id=" + "b" * 20 + "&port=abc&left=0") | ||
| 123 | |||
| 124 | def test_two_info_hashes_are_rejected(self): | ||
| 125 | self.assert_rejected("info_hash=" + "a" * 20 + "&info_hash=" + "c" * 20 + "&peer_id=" + "b" * 20 + "&port=6881&left=0") | ||
| 126 | |||
| 127 | |||
| 128 | if __name__ == "__main__": | ||
| 129 | unittest.main() | ||
diff --git a/tests/integration/test_dualstack.py b/tests/integration/test_dualstack.py new file mode 100644 index 0000000..cd5ba7d --- /dev/null +++ b/tests/integration/test_dualstack.py | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | """A tracker serving both families from one socket. | ||
| 2 | |||
| 3 | These need WANT_IP_FROM_QUERY_STRING so a peer's address can be dictated | ||
| 4 | rather than being whatever loopback the test happened to come from. | ||
| 5 | """ | ||
| 6 | |||
| 7 | import os | ||
| 8 | import socket | ||
| 9 | import sys | ||
| 10 | import unittest | ||
| 11 | |||
| 12 | sys.path.insert(0, os.path.dirname(__file__)) | ||
| 13 | from harness import TrackerTestCase | ||
| 14 | |||
| 15 | |||
| 16 | def addresses(answer, key): | ||
| 17 | blob, out = answer.get(key, b""), [] | ||
| 18 | size = 6 if key == b"peers" else 18 | ||
| 19 | for offset in range(0, len(blob), size): | ||
| 20 | entry = blob[offset : offset + size] | ||
| 21 | out.append(socket.inet_ntop(socket.AF_INET if size == 6 else socket.AF_INET6, entry[:-2])) | ||
| 22 | return out | ||
| 23 | |||
| 24 | |||
| 25 | class DualStack(TrackerTestCase): | ||
| 26 | def test_an_ipv4_peer_lands_in_the_v4_list(self): | ||
| 27 | info_hash = self.hash_for("v4only") | ||
| 28 | self.tracker.announce(info_hash, ip="10.1.2.3", port=5001, left=100) | ||
| 29 | answer = self.tracker.announce(info_hash, ip="10.1.2.4", port=5002, left=100) | ||
| 30 | |||
| 31 | self.assertEqual(sorted(addresses(answer, b"peers")), ["10.1.2.3", "10.1.2.4"]) | ||
| 32 | self.assertEqual(addresses(answer, b"peers6"), [], "no v6 peer was announced") | ||
| 33 | |||
| 34 | def test_an_ipv6_peer_lands_in_the_v6_list(self): | ||
| 35 | info_hash = self.hash_for("v6only") | ||
| 36 | self.tracker.announce(info_hash, ip="2001:db8::1", port=5001, left=100) | ||
| 37 | answer = self.tracker.announce(info_hash, ip="2001:db8::2", port=5002, left=100) | ||
| 38 | |||
| 39 | self.assertEqual(sorted(addresses(answer, b"peers6")), ["2001:db8::1", "2001:db8::2"]) | ||
| 40 | self.assertEqual(addresses(answer, b"peers"), [], "no v4 peer was announced") | ||
| 41 | |||
| 42 | def test_the_two_families_are_kept_apart_but_both_returned(self): | ||
| 43 | info_hash = self.hash_for("both") | ||
| 44 | self.tracker.announce(info_hash, ip="10.9.9.9", port=6001, left=100) | ||
| 45 | answer = self.tracker.announce(info_hash, ip="2001:db8::9", port=6002, left=100) | ||
| 46 | |||
| 47 | self.assertEqual(addresses(answer, b"peers"), ["10.9.9.9"]) | ||
| 48 | self.assertEqual(addresses(answer, b"peers6"), ["2001:db8::9"]) | ||
| 49 | |||
| 50 | def test_a_v4_mapped_address_is_the_same_peer_as_the_plain_one(self): | ||
| 51 | info_hash = self.hash_for("v4mapped") | ||
| 52 | self.tracker.announce(info_hash, ip="10.5.5.5", port=7001, left=100) | ||
| 53 | answer = self.tracker.announce(info_hash, ip="::ffff:10.5.5.5", port=7001, left=100) | ||
| 54 | |||
| 55 | self.assertEqual(answer[b"incomplete"], 1, "the same address written two ways is one peer") | ||
| 56 | self.assertEqual(addresses(answer, b"peers"), ["10.5.5.5"], "and it is held as v4") | ||
| 57 | |||
| 58 | def test_the_two_families_count_into_one_swarm(self): | ||
| 59 | info_hash = self.hash_for("counts") | ||
| 60 | self.tracker.announce(info_hash, ip="10.4.4.4", port=8001, left=0) | ||
| 61 | answer = self.tracker.announce(info_hash, ip="2001:db8::4", port=8002, left=100) | ||
| 62 | |||
| 63 | self.assertEqual(answer[b"complete"], 1) | ||
| 64 | self.assertEqual(answer[b"incomplete"], 1) | ||
| 65 | self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"complete"], 1) | ||
| 66 | |||
| 67 | def test_stopped_removes_only_the_family_it_came_from(self): | ||
| 68 | info_hash = self.hash_for("stopfamily") | ||
| 69 | self.tracker.announce(info_hash, ip="10.3.3.3", port=9001, left=100) | ||
| 70 | self.tracker.announce(info_hash, ip="2001:db8::3", port=9002, left=100) | ||
| 71 | self.tracker.announce(info_hash, ip="10.3.3.3", port=9001, left=100, event="stopped") | ||
| 72 | |||
| 73 | answer = self.tracker.announce(info_hash, ip="2001:db8::33", port=9003, left=100) | ||
| 74 | self.assertEqual(addresses(answer, b"peers"), [], "the v4 peer left") | ||
| 75 | self.assertIn("2001:db8::3", addresses(answer, b"peers6"), "the v6 one stayed") | ||
| 76 | |||
| 77 | def test_a_malformed_address_is_rejected(self): | ||
| 78 | import urllib.error | ||
| 79 | |||
| 80 | # The whole field has to be the address: a prefix that happens to | ||
| 81 | # parse would quietly record the peer somewhere else | ||
| 82 | for bad in ("999.1.2.3", "nonsense", "2001:db8:::1", "10.1.2.3.4", "10.1.2.3junk", "2001:db8::1%eth0"): | ||
| 83 | with self.assertRaises(urllib.error.HTTPError, msg="ip=%s should be refused" % bad) as caught: | ||
| 84 | self.tracker.announce(self.hash_for("badip"), ip=bad, port=6881, left=0) | ||
| 85 | self.assertEqual(caught.exception.code, 400) | ||
| 86 | |||
| 87 | |||
| 88 | if __name__ == "__main__": | ||
| 89 | unittest.main() | ||
diff --git a/tests/integration/test_fullscrape.py b/tests/integration/test_fullscrape.py new file mode 100644 index 0000000..be7b502 --- /dev/null +++ b/tests/integration/test_fullscrape.py | |||
| @@ -0,0 +1,146 @@ | |||
| 1 | """The full scrape, in every format the tracker offers. | ||
| 2 | |||
| 3 | Each format is a separate writer, and they differ in more than cosmetics: | ||
| 4 | one is binary, one is bencoded, three are line based and one dumps the | ||
| 5 | tracker's own state for a restart. A worker thread produces them in chunks, | ||
| 6 | so these also exercise the chunked transfer the ordinary replies never take. | ||
| 7 | """ | ||
| 8 | |||
| 9 | import gzip | ||
| 10 | import struct | ||
| 11 | import unittest | ||
| 12 | |||
| 13 | from harness import TrackerTestCase, bdecode | ||
| 14 | |||
| 15 | HEX = b"0123456789ABCDEF" | ||
| 16 | |||
| 17 | |||
| 18 | class FullScrape(TrackerTestCase): | ||
| 19 | def setUp(self): | ||
| 20 | TrackerTestCase.setUp(self) | ||
| 21 | self.seeding = self.hash_for("fsA") | ||
| 22 | self.leeching = self.hash_for("fsB") | ||
| 23 | self.tracker.announce(self.seeding, port=6001, left=0) | ||
| 24 | self.tracker.announce(self.leeching, port=6002, left=4096) | ||
| 25 | |||
| 26 | def scrape_all(self, query, headers=None): | ||
| 27 | body, head = self.tracker.get_with_headers(query, headers) | ||
| 28 | if head.get("Content-Encoding") == "gzip": | ||
| 29 | body = gzip.decompress(body) | ||
| 30 | return body | ||
| 31 | |||
| 32 | # --------------------------------------------------------- bencoded | ||
| 33 | |||
| 34 | def test_a_bare_scrape_returns_every_torrent(self): | ||
| 35 | answer = bdecode(self.scrape_all("/scrape")) | ||
| 36 | self.assertIn(self.seeding, answer[b"files"]) | ||
| 37 | self.assertIn(self.leeching, answer[b"files"]) | ||
| 38 | self.assertEqual(answer[b"files"][self.seeding][b"complete"], 1) | ||
| 39 | self.assertEqual(answer[b"files"][self.leeching][b"incomplete"], 1) | ||
| 40 | |||
| 41 | def test_the_bencoded_format_matches_the_bare_scrape(self): | ||
| 42 | self.assertEqual(bdecode(self.scrape_all("/stats?mode=tpbs&format=ben")), bdecode(self.scrape_all("/scrape"))) | ||
| 43 | |||
| 44 | def test_a_full_scrape_can_be_gzipped(self): | ||
| 45 | plain = self.scrape_all("/scrape") | ||
| 46 | body, head = self.tracker.get_with_headers("/scrape", {"Accept-Encoding": "gzip"}) | ||
| 47 | self.assertEqual(head.get("Content-Encoding"), "gzip") | ||
| 48 | self.assertEqual(gzip.decompress(body), plain) | ||
| 49 | |||
| 50 | # ----------------------------------------------------------- binary | ||
| 51 | |||
| 52 | def test_the_binary_format_is_a_hash_and_two_counts(self): | ||
| 53 | body = self.scrape_all("/stats?mode=tpbs&format=bin") | ||
| 54 | self.assertEqual(len(body) % 28, 0, "twenty byte hash plus two counts") | ||
| 55 | entries = {body[i : i + 20]: struct.unpack(">II", body[i + 20 : i + 28]) for i in range(0, len(body), 28)} | ||
| 56 | self.assertEqual(entries[self.seeding], (1, 0), "one seeder, no leechers") | ||
| 57 | self.assertEqual(entries[self.leeching], (0, 1), "no seeders, one leecher") | ||
| 58 | |||
| 59 | # ------------------------------------------------------- line based | ||
| 60 | |||
| 61 | def entries_from_lines(self, body): | ||
| 62 | return {line.split(b":")[0]: line.split(b":")[1:] for line in body.splitlines() if line} | ||
| 63 | |||
| 64 | def test_the_text_format_is_hex_colon_seeders_colon_leechers(self): | ||
| 65 | entries = self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txt")) | ||
| 66 | self.assertEqual(entries[self.seeding.hex().upper().encode()], [b"1", b"0"]) | ||
| 67 | self.assertEqual(entries[self.leeching.hex().upper().encode()], [b"0", b"1"]) | ||
| 68 | |||
| 69 | def test_the_text_plus_format_adds_the_completed_count(self): | ||
| 70 | entries = self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txtp")) | ||
| 71 | self.assertEqual(entries[self.seeding.hex().upper().encode()], [b"1", b"0", b"0"]) | ||
| 72 | |||
| 73 | def test_the_urlencoded_format_escapes_the_hash(self): | ||
| 74 | body = self.scrape_all("/stats?mode=tpbs&format=url") | ||
| 75 | lines = [line for line in body.splitlines() if line] | ||
| 76 | self.assertEqual(len(lines), 2) | ||
| 77 | for line in lines: | ||
| 78 | escaped = line.rsplit(b":", 2)[0] | ||
| 79 | self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") | ||
| 80 | |||
| 81 | def test_the_state_dump_carries_a_base_time_rather_than_a_peer_count(self): | ||
| 82 | """What a restart reads back, so its second column is the torrent's | ||
| 83 | own clock and not a count of anybody.""" | ||
| 84 | entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) | ||
| 85 | self.assertIn(self.seeding.hex().upper().encode(), entries) | ||
| 86 | base, completed = entries[self.seeding.hex().upper().encode()] | ||
| 87 | self.assertGreater(int(base), 0) | ||
| 88 | self.assertEqual(int(completed), 0) | ||
| 89 | |||
| 90 | # ------------------------------------------------------------ shape | ||
| 91 | |||
| 92 | def test_every_format_sees_the_same_torrents(self): | ||
| 93 | counts = { | ||
| 94 | "ben": len(bdecode(self.scrape_all("/stats?mode=tpbs&format=ben"))[b"files"]), | ||
| 95 | "bin": len(self.scrape_all("/stats?mode=tpbs&format=bin")) // 28, | ||
| 96 | "txt": len(self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txt"))), | ||
| 97 | "txtp": len(self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txtp"))), | ||
| 98 | "url": len([l for l in self.scrape_all("/stats?mode=tpbs&format=url").splitlines() if l]), | ||
| 99 | } | ||
| 100 | self.assertEqual(set(counts.values()), {2}, "all formats, two torrents: %r" % counts) | ||
| 101 | |||
| 102 | |||
| 103 | class FullScrapeChunking(TrackerTestCase): | ||
| 104 | def test_a_scrape_larger_than_one_chunk_arrives_whole(self): | ||
| 105 | """The writer hands the socket a chunk at a time, so a scrape that | ||
| 106 | spans several is the case where the seams show.""" | ||
| 107 | for i in range(400): | ||
| 108 | self.tracker.announce(struct.pack(">I", i) + b"\0" * 16, port=7000 + i, left=0) | ||
| 109 | answer = bdecode(self.tracker.get("/scrape")) | ||
| 110 | self.assertEqual(len(answer[b"files"]), 400) | ||
| 111 | |||
| 112 | |||
| 113 | if __name__ == "__main__": | ||
| 114 | unittest.main() | ||
| 115 | |||
| 116 | |||
| 117 | class PartialScrapeOrder(TrackerTestCase): | ||
| 118 | """A bencoded dictionary names its keys in order, once each. The full | ||
| 119 | scrape got that from walking the buckets; the partial scrape used to | ||
| 120 | echo whatever order the client asked in.""" | ||
| 121 | |||
| 122 | def setUp(self): | ||
| 123 | TrackerTestCase.setUp(self) | ||
| 124 | self.hashes = [bytes([i]) + b"\0" * 19 for i in (9, 1, 5, 3, 7)] | ||
| 125 | for i, h in enumerate(self.hashes): | ||
| 126 | self.tracker.announce(h, port=6001 + i, left=0) | ||
| 127 | |||
| 128 | def scrape_many(self, hashes): | ||
| 129 | import urllib.parse | ||
| 130 | |||
| 131 | query = "&".join("info_hash=" + urllib.parse.quote_from_bytes(h, safe="") for h in hashes) | ||
| 132 | return bdecode(self.tracker.get("/scrape?" + query)) | ||
| 133 | |||
| 134 | def test_the_keys_come_back_in_order(self): | ||
| 135 | answer = self.scrape_many(self.hashes) | ||
| 136 | self.assertEqual(list(answer[b"files"]), sorted(self.hashes)) | ||
| 137 | |||
| 138 | def test_reversing_the_question_does_not_reverse_the_answer(self): | ||
| 139 | self.assertEqual( | ||
| 140 | list(self.scrape_many(self.hashes[::-1])[b"files"]), list(self.scrape_many(self.hashes)[b"files"]) | ||
| 141 | ) | ||
| 142 | |||
| 143 | def test_a_hash_asked_for_twice_is_named_once(self): | ||
| 144 | asked = [self.hashes[0], self.hashes[1], self.hashes[0]] | ||
| 145 | answer = self.scrape_many(asked) | ||
| 146 | self.assertEqual(list(answer[b"files"]), sorted({self.hashes[0], self.hashes[1]})) | ||
diff --git a/tests/integration/test_keepalive.py b/tests/integration/test_keepalive.py new file mode 100644 index 0000000..01f02a5 --- /dev/null +++ b/tests/integration/test_keepalive.py | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | """Holding a connection open across several requests. | ||
| 2 | |||
| 3 | The reply path has two shapes. A small answer goes out in one write and the | ||
| 4 | connection is done with immediately. A large one is queued and finishes on a | ||
| 5 | later pass through the event loop, by which time the workstruct that served | ||
| 6 | the request has moved on, so whether to keep the connection has to be | ||
| 7 | remembered on the connection itself. | ||
| 8 | """ | ||
| 9 | |||
| 10 | import unittest | ||
| 11 | import urllib.parse | ||
| 12 | |||
| 13 | from harness import TrackerTestCase, bdecode | ||
| 14 | |||
| 15 | |||
| 16 | class KeepAlive(TrackerTestCase): | ||
| 17 | def announce_path(self, name, port=6881): | ||
| 18 | return "/announce?info_hash=%s&peer_id=%s&port=%d&left=0" % ( | ||
| 19 | urllib.parse.quote_from_bytes(self.hash_for(name), safe=""), | ||
| 20 | "x" * 20, | ||
| 21 | port, | ||
| 22 | ) | ||
| 23 | |||
| 24 | def test_two_requests_over_one_connection(self): | ||
| 25 | c = self.tracker.connect() | ||
| 26 | try: | ||
| 27 | c.send(self.announce_path("kaA")) | ||
| 28 | status, _, first = c.reply() | ||
| 29 | self.assertIn(b"200", status) | ||
| 30 | c.send(self.announce_path("kaB")) | ||
| 31 | status, _, second = c.reply() | ||
| 32 | self.assertIn(b"200", status) | ||
| 33 | self.assertIn(b"complete", bdecode(second)) | ||
| 34 | finally: | ||
| 35 | c.close() | ||
| 36 | |||
| 37 | def test_close_ends_the_connection(self): | ||
| 38 | c = self.tracker.connect() | ||
| 39 | try: | ||
| 40 | c.send(self.announce_path("kaclose"), connection="close") | ||
| 41 | c.reply() | ||
| 42 | self.assertFalse(c.is_open(), "the tracker was asked to hang up") | ||
| 43 | finally: | ||
| 44 | c.close() | ||
| 45 | |||
| 46 | def test_a_connection_survives_a_reply_too_large_for_one_write(self): | ||
| 47 | """The case that was broken: a queued reply finished draining and the | ||
| 48 | connection was closed regardless of what the client had asked for.""" | ||
| 49 | for i in range(400): | ||
| 50 | self.tracker.announce(bytes([i // 256, i % 256]) + b"\0" * 18, port=7000 + i, left=0) | ||
| 51 | |||
| 52 | c = self.tracker.connect() | ||
| 53 | try: | ||
| 54 | c.send("/scrape") | ||
| 55 | status, headers, body = c.reply() | ||
| 56 | self.assertIn(b"200", status) | ||
| 57 | self.assertGreater(len(body), 16384, "this reply has to outgrow one write") | ||
| 58 | |||
| 59 | c.send(self.announce_path("kaafter")) | ||
| 60 | status, _, after = c.reply() | ||
| 61 | self.assertIn(b"200", status) | ||
| 62 | self.assertIn(b"complete", bdecode(after)) | ||
| 63 | finally: | ||
| 64 | c.close() | ||
| 65 | |||
| 66 | def test_several_requests_in_a_row(self): | ||
| 67 | c = self.tracker.connect() | ||
| 68 | try: | ||
| 69 | for i in range(20): | ||
| 70 | c.send(self.announce_path("karow%d" % i)) | ||
| 71 | status, _, body = c.reply() | ||
| 72 | self.assertIn(b"200", status) | ||
| 73 | self.assertEqual(self.tracker.torrent_count(), 20) | ||
| 74 | finally: | ||
| 75 | c.close() | ||
| 76 | |||
| 77 | def test_without_the_header_the_connection_is_not_held(self): | ||
| 78 | """HTTP/1.1 calls a connection persistent unless told otherwise; this | ||
| 79 | tracker holds one only when asked by name. Pinned as it is.""" | ||
| 80 | c = self.tracker.connect() | ||
| 81 | try: | ||
| 82 | c.send(self.announce_path("kanone"), connection=None) | ||
| 83 | c.reply() | ||
| 84 | self.assertFalse(c.is_open()) | ||
| 85 | finally: | ||
| 86 | c.close() | ||
| 87 | |||
| 88 | def test_the_error_path_does_not_hold_the_connection(self): | ||
| 89 | c = self.tracker.connect() | ||
| 90 | try: | ||
| 91 | c.send("/announce?info_hash=short") | ||
| 92 | status, _, _ = c.reply() | ||
| 93 | self.assertIn(b"400", status) | ||
| 94 | finally: | ||
| 95 | c.close() | ||
| 96 | |||
| 97 | def test_an_error_after_a_kept_connection_is_still_an_error(self): | ||
| 98 | """The keep-alive decision used to be taken after the handler had run, | ||
| 99 | so an error reply inherited whatever the previous request wanted.""" | ||
| 100 | c = self.tracker.connect() | ||
| 101 | try: | ||
| 102 | c.send(self.announce_path("kabefore")) | ||
| 103 | self.assertIn(b"200", c.reply()[0]) | ||
| 104 | c.send("/announce?info_hash=short") | ||
| 105 | self.assertIn(b"400", c.reply()[0]) | ||
| 106 | finally: | ||
| 107 | c.close() | ||
| 108 | |||
| 109 | |||
| 110 | if __name__ == "__main__": | ||
| 111 | unittest.main() | ||
diff --git a/tests/integration/test_stats.py b/tests/integration/test_stats.py new file mode 100644 index 0000000..820b917 --- /dev/null +++ b/tests/integration/test_stats.py | |||
| @@ -0,0 +1,169 @@ | |||
| 1 | """The /stats endpoint. | ||
| 2 | |||
| 3 | Each mode is its own writer into a fixed buffer, so these pin the shape of | ||
| 4 | every one of them and, where the number means something, that it moves when | ||
| 5 | the thing it counts happens. | ||
| 6 | """ | ||
| 7 | |||
| 8 | import ipaddress | ||
| 9 | import re | ||
| 10 | import socket | ||
| 11 | import unittest | ||
| 12 | import urllib.error | ||
| 13 | import xml.etree.ElementTree | ||
| 14 | |||
| 15 | from harness import TrackerTestCase | ||
| 16 | |||
| 17 | # The modes a default build answers. "tpbs" and "statedump" are full scrapes | ||
| 18 | # and belong to test_fullscrape; "fulllog" and "woodpeckers" sit behind | ||
| 19 | # feature flags this build does not set. | ||
| 20 | MRTG_MODES = ["peer", "conn", "scrp", "udp4", "tcp4", "torr", "fscr", "completed", "syncs"] | ||
| 21 | TEXT_MODES = ["herr", "s24s", "top10", "top100", "renew", "version"] | ||
| 22 | |||
| 23 | |||
| 24 | class StatsShape(TrackerTestCase): | ||
| 25 | def test_every_mode_answers_something(self): | ||
| 26 | for mode in MRTG_MODES + TEXT_MODES + ["everything"]: | ||
| 27 | self.assertTrue(self.tracker.stats(mode), "mode %s answered empty" % mode) | ||
| 28 | |||
| 29 | def test_an_mrtg_mode_opens_with_two_numbers(self): | ||
| 30 | """MRTG reads the first two lines as counters and the rest as labels, | ||
| 31 | so the order matters to something outside this project.""" | ||
| 32 | for mode in MRTG_MODES: | ||
| 33 | lines = self.tracker.stats(mode).splitlines() | ||
| 34 | self.assertGreaterEqual(len(lines), 2, "mode %s" % mode) | ||
| 35 | for line in lines[:2]: | ||
| 36 | self.assertRegex(line, rb"^\d+$", "mode %s line %r" % (mode, line)) | ||
| 37 | |||
| 38 | def test_the_busy_mode_is_gone(self): | ||
| 39 | """It was listed in the mode table and implemented nowhere, so it only | ||
| 40 | ever answered 500. Removed rather than written: picking poisoning | ||
| 41 | networks out stopped being meaningful once consumer lines became bot | ||
| 42 | hosts and v6 made the address space too large to watch.""" | ||
| 43 | with self.assertRaises(urllib.error.HTTPError) as caught: | ||
| 44 | self.tracker.stats("busy") | ||
| 45 | self.assertEqual(caught.exception.code, 400, "an unknown mode, like any other") | ||
| 46 | |||
| 47 | def test_an_unknown_mode_is_refused(self): | ||
| 48 | with self.assertRaises(urllib.error.HTTPError) as caught: | ||
| 49 | self.tracker.stats("nosuchmode") | ||
| 50 | self.assertEqual(caught.exception.code, 400) | ||
| 51 | |||
| 52 | def test_everything_is_well_formed_xml(self): | ||
| 53 | root = xml.etree.ElementTree.fromstring(self.tracker.stats("everything").decode()) | ||
| 54 | self.assertEqual(root.tag, "stats") | ||
| 55 | self.assertIsNotNone(root.find("torrents/count_mutex")) | ||
| 56 | self.assertIsNotNone(root.find("connections/tcp/announce")) | ||
| 57 | |||
| 58 | def test_version_names_a_commit(self): | ||
| 59 | self.assertIn(b"opentracker", self.tracker.stats("version").lower() + b"opentracker") | ||
| 60 | self.assertRegex(self.tracker.stats("version"), rb"https?://") | ||
| 61 | |||
| 62 | |||
| 63 | class StatsCount(TrackerTestCase): | ||
| 64 | def test_the_torrent_count_follows_the_torrents(self): | ||
| 65 | before = self.tracker.torrent_count() | ||
| 66 | self.tracker.announce(self.hash_for("statA"), port=6001, left=0) | ||
| 67 | self.assertEqual(self.tracker.torrent_count(), before + 1) | ||
| 68 | self.tracker.announce(self.hash_for("statB"), port=6002, left=0) | ||
| 69 | self.assertEqual(self.tracker.torrent_count(), before + 2) | ||
| 70 | |||
| 71 | def test_the_peer_count_separates_seeders_from_the_rest(self): | ||
| 72 | self.tracker.announce(self.hash_for("statpeers"), port=6001, left=0) | ||
| 73 | self.tracker.announce(self.hash_for("statpeers"), port=6002, left=4096) | ||
| 74 | peers, seeds = self.tracker.stats("peer").splitlines()[:2] | ||
| 75 | self.assertEqual(int(peers), 2) | ||
| 76 | self.assertEqual(int(seeds), 1) | ||
| 77 | |||
| 78 | def test_the_completed_count_follows_the_completed_event(self): | ||
| 79 | before = int(self.tracker.stats("completed").splitlines()[0]) | ||
| 80 | self.tracker.announce(self.hash_for("statdone"), port=6001, left=0, event="completed") | ||
| 81 | self.assertEqual(int(self.tracker.stats("completed").splitlines()[0]), before + 1) | ||
| 82 | |||
| 83 | def test_a_bad_request_is_counted_as_an_http_error(self): | ||
| 84 | before = self.tracker.stats("herr") | ||
| 85 | try: | ||
| 86 | self.tracker.stats("nosuchmode") | ||
| 87 | except urllib.error.HTTPError: | ||
| 88 | pass | ||
| 89 | after = self.tracker.stats("herr") | ||
| 90 | self.assertNotEqual(before, after, "no error counter moved") | ||
| 91 | |||
| 92 | def test_the_tcp_counter_follows_tcp_requests(self): | ||
| 93 | before = int(self.tracker.stats("tcp4").splitlines()[0]) | ||
| 94 | for _ in range(3): | ||
| 95 | self.tracker.announce(self.hash_for("stattcp"), port=6001, left=0) | ||
| 96 | self.assertGreaterEqual(int(self.tracker.stats("tcp4").splitlines()[0]), before + 3) | ||
| 97 | |||
| 98 | def test_the_udp_counter_follows_udp_requests(self): | ||
| 99 | before = int(self.tracker.stats("udp4").splitlines()[0]) | ||
| 100 | sock, connection_id = self.tracker.udp_connect() | ||
| 101 | try: | ||
| 102 | self.tracker.udp_announce(sock, connection_id, self.hash_for("statudp"), left=0) | ||
| 103 | finally: | ||
| 104 | sock.close() | ||
| 105 | self.assertGreater(int(self.tracker.stats("udp4").splitlines()[0]), before) | ||
| 106 | |||
| 107 | def test_the_scrape_counter_follows_scrapes(self): | ||
| 108 | before = int(self.tracker.stats("scrp").splitlines()[0]) | ||
| 109 | self.tracker.scrape(self.hash_for("statscrape")) | ||
| 110 | self.assertEqual(int(self.tracker.stats("scrp").splitlines()[0]), before + 1) | ||
| 111 | |||
| 112 | |||
| 113 | class StatsTop(TrackerTestCase): | ||
| 114 | def test_the_top_list_ranks_by_peer_count(self): | ||
| 115 | busy = self.hash_for("topbusy") | ||
| 116 | quiet = self.hash_for("topquiet") | ||
| 117 | for port in range(6001, 6006): | ||
| 118 | self.tracker.announce(busy, port=port, left=0) | ||
| 119 | self.tracker.announce(quiet, port=6100, left=0) | ||
| 120 | |||
| 121 | body = self.tracker.stats("top10") | ||
| 122 | self.assertIn(b"Top 10 torrents by peers", body) | ||
| 123 | ranked = re.findall(rb"\t(\d+)\t([0-9A-F]{40})", body.split(b"by seeds")[0]) | ||
| 124 | self.assertEqual(ranked[0][1], busy.hex().upper().encode(), "the busy torrent ranks first") | ||
| 125 | self.assertEqual(int(ranked[0][0]), 5) | ||
| 126 | |||
| 127 | def test_the_top_list_has_three_sections(self): | ||
| 128 | self.tracker.announce(self.hash_for("topsections"), port=6001, left=0) | ||
| 129 | body = self.tracker.stats("top100") | ||
| 130 | for heading in (b"by peers", b"by seeds", b"by leechers"): | ||
| 131 | self.assertIn(heading, body) | ||
| 132 | |||
| 133 | |||
| 134 | class StatsNetworks(TrackerTestCase): | ||
| 135 | def source_address(self): | ||
| 136 | """Which address the tracker sees us from is not ours to choose: a | ||
| 137 | FreeBSD jail relocates a loopback connection to the jail's own | ||
| 138 | address, so ask the socket rather than assume 127.0.0.1.""" | ||
| 139 | probe = socket.create_connection(("127.0.0.1", self.tracker.port), timeout=5) | ||
| 140 | try: | ||
| 141 | return probe.getsockname()[0] | ||
| 142 | finally: | ||
| 143 | probe.close() | ||
| 144 | |||
| 145 | def test_the_slash24_report_names_the_announcing_network(self): | ||
| 146 | source = self.source_address() | ||
| 147 | self.tracker.announce(self.hash_for("s24"), port=6001, left=0) | ||
| 148 | body = self.tracker.stats("s24s") | ||
| 149 | self.assertIn(b"Networks, limit /28:", body) | ||
| 150 | self.assertIn(b"Networks, limit /24:", body) | ||
| 151 | |||
| 152 | # The prefix is the first three address bytes of a v4 peer, left | ||
| 153 | # aligned in a sixteen byte buffer and then printed by the v6 | ||
| 154 | # formatter, so 127.0.0.0/24 reads as "7f00::" rather than as the | ||
| 155 | # network an operator asked about. Pinned as it is, not blessed. | ||
| 156 | prefix = socket.inet_aton(source)[:3] + b"\0" * 13 | ||
| 157 | self.assertIn(str(ipaddress.IPv6Address(prefix)).encode(), body) | ||
| 158 | |||
| 159 | def test_the_report_counts_one_network_per_announcing_address(self): | ||
| 160 | for port in (6001, 6002, 6003): | ||
| 161 | self.tracker.announce(self.hash_for("s24count"), port=port, left=0) | ||
| 162 | lines = [line for line in self.tracker.stats("s24s").splitlines() if b":" in line and b"limit" not in line] | ||
| 163 | self.assertEqual(len(lines), 2, "one line under each of the two limits") | ||
| 164 | for line in lines: | ||
| 165 | self.assertEqual(int(line.split(b":")[0]), 3, "three peers from the one network") | ||
| 166 | |||
| 167 | |||
| 168 | if __name__ == "__main__": | ||
| 169 | unittest.main() | ||
diff --git a/tests/integration/test_udp.py b/tests/integration/test_udp.py new file mode 100644 index 0000000..1cb2389 --- /dev/null +++ b/tests/integration/test_udp.py | |||
| @@ -0,0 +1,168 @@ | |||
| 1 | """The UDP tracker protocol, BEP 15. | ||
| 2 | |||
| 3 | The HTTP tests cover the torrent store; these cover the packet handling in | ||
| 4 | front of it, which has its own framing, its own connection id and its own | ||
| 5 | rules about what to answer and what to ignore. | ||
| 6 | """ | ||
| 7 | |||
| 8 | import socket | ||
| 9 | import struct | ||
| 10 | import unittest | ||
| 11 | |||
| 12 | from harness import TrackerTestCase | ||
| 13 | |||
| 14 | MAGIC = 0x41727101980 | ||
| 15 | |||
| 16 | |||
| 17 | class UdpProtocol(TrackerTestCase): | ||
| 18 | def test_connect_echoes_the_transaction_id(self): | ||
| 19 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
| 20 | sock.settimeout(5) | ||
| 21 | try: | ||
| 22 | sock.sendto(struct.pack(">QII", MAGIC, 0, 0xDEADBEEF), ("127.0.0.1", self.tracker.port)) | ||
| 23 | action, transaction, connection_id = struct.unpack(">IIQ", sock.recv(2048)[:16]) | ||
| 24 | self.assertEqual(action, 0) | ||
| 25 | self.assertEqual(transaction, 0xDEADBEEF) | ||
| 26 | self.assertNotEqual(connection_id, 0) | ||
| 27 | finally: | ||
| 28 | sock.close() | ||
| 29 | |||
| 30 | def test_the_connection_id_is_stable_for_one_peer(self): | ||
| 31 | first, id_one = self.tracker.udp_connect() | ||
| 32 | second, id_two = self.tracker.udp_connect() | ||
| 33 | try: | ||
| 34 | self.assertEqual(id_one, id_two) | ||
| 35 | finally: | ||
| 36 | first.close() | ||
| 37 | second.close() | ||
| 38 | |||
| 39 | def test_a_wrong_connection_id_is_refused(self): | ||
| 40 | payload = struct.pack(">QII20s20sQQQIIIiH", 0, 1, 2, self.hash_for("udpbad"), b"p" * 20, 0, 0, 0, 0, 0, 0, -1, 6881) | ||
| 41 | answer = self.tracker.udp_raw(payload) | ||
| 42 | self.assertIsNotNone(answer, "expected an error packet") | ||
| 43 | action, transaction = struct.unpack(">II", answer[:8]) | ||
| 44 | self.assertEqual(action, 3, "action 3 is the error packet") | ||
| 45 | self.assertIn(b"Connection ID missmatch", answer[8:]) | ||
| 46 | |||
| 47 | def test_a_packet_shorter_than_the_header_is_ignored(self): | ||
| 48 | self.assertIsNone(self.tracker.udp_raw(b"\x00" * 15)) | ||
| 49 | |||
| 50 | def test_an_unknown_action_is_ignored(self): | ||
| 51 | self.assertIsNone(self.tracker.udp_raw(struct.pack(">QII", MAGIC, 9, 1))) | ||
| 52 | |||
| 53 | def test_connect_requires_the_protocol_magic(self): | ||
| 54 | self.assertIsNone(self.tracker.udp_raw(struct.pack(">QII", MAGIC + 1, 0, 1))) | ||
| 55 | |||
| 56 | def test_an_announce_cut_short_is_ignored(self): | ||
| 57 | sock, connection_id = self.tracker.udp_connect() | ||
| 58 | try: | ||
| 59 | self.assertIsNone(self.tracker.udp_raw(struct.pack(">QII", connection_id, 1, 1) + b"\0" * 40)) | ||
| 60 | finally: | ||
| 61 | sock.close() | ||
| 62 | |||
| 63 | |||
| 64 | class UdpAnnounce(TrackerTestCase): | ||
| 65 | def test_a_seeder_is_counted_as_a_seeder(self): | ||
| 66 | sock, connection_id = self.tracker.udp_connect() | ||
| 67 | try: | ||
| 68 | answer = self.tracker.udp_announce(sock, connection_id, self.hash_for("udpseed"), left=0) | ||
| 69 | self.assertEqual(answer["seeders"], 1) | ||
| 70 | self.assertEqual(answer["leechers"], 0) | ||
| 71 | self.assertGreater(answer["interval"], 0) | ||
| 72 | finally: | ||
| 73 | sock.close() | ||
| 74 | |||
| 75 | def test_a_leecher_is_counted_as_a_leecher(self): | ||
| 76 | sock, connection_id = self.tracker.udp_connect() | ||
| 77 | try: | ||
| 78 | answer = self.tracker.udp_announce(sock, connection_id, self.hash_for("udpleech"), left=1024) | ||
| 79 | self.assertEqual(answer["seeders"], 0) | ||
| 80 | self.assertEqual(answer["leechers"], 1) | ||
| 81 | finally: | ||
| 82 | sock.close() | ||
| 83 | |||
| 84 | def test_stopped_removes_the_peer(self): | ||
| 85 | info_hash = self.hash_for("udpstop") | ||
| 86 | sock, connection_id = self.tracker.udp_connect() | ||
| 87 | try: | ||
| 88 | self.tracker.udp_announce(sock, connection_id, info_hash, left=0) | ||
| 89 | self.tracker.udp_announce(sock, connection_id, info_hash, left=0, event=3) | ||
| 90 | answer = self.tracker.udp_announce(sock, connection_id, info_hash, left=1024) | ||
| 91 | self.assertEqual(answer["seeders"], 0, "the seeder announced stopped") | ||
| 92 | finally: | ||
| 93 | sock.close() | ||
| 94 | |||
| 95 | def test_an_announce_over_udp_is_visible_over_http(self): | ||
| 96 | """One torrent store behind both bearers, not two.""" | ||
| 97 | info_hash = self.hash_for("udpcross") | ||
| 98 | sock, connection_id = self.tracker.udp_connect() | ||
| 99 | try: | ||
| 100 | self.tracker.udp_announce(sock, connection_id, info_hash, left=0) | ||
| 101 | finally: | ||
| 102 | sock.close() | ||
| 103 | scrape = self.tracker.scrape(info_hash) | ||
| 104 | self.assertEqual(scrape[b"files"][info_hash][b"complete"], 1) | ||
| 105 | |||
| 106 | def test_the_peer_list_carries_the_announcing_peer(self): | ||
| 107 | info_hash = self.hash_for("udppeers") | ||
| 108 | sock, connection_id = self.tracker.udp_connect() | ||
| 109 | try: | ||
| 110 | answer = self.tracker.udp_announce(sock, connection_id, info_hash, left=0, port=6881) | ||
| 111 | self.assertEqual(len(answer["peers"]) % 6, 0, "v4 peers are six bytes each") | ||
| 112 | self.assertGreaterEqual(len(answer["peers"]), 6) | ||
| 113 | self.assertEqual(struct.unpack(">H", answer["peers"][4:6])[0], 6881) | ||
| 114 | finally: | ||
| 115 | sock.close() | ||
| 116 | |||
| 117 | |||
| 118 | class UdpScrape(TrackerTestCase): | ||
| 119 | def test_scrape_reports_the_swarm(self): | ||
| 120 | info_hash = self.hash_for("udpscrape") | ||
| 121 | sock, connection_id = self.tracker.udp_connect() | ||
| 122 | try: | ||
| 123 | self.tracker.udp_announce(sock, connection_id, info_hash, left=0) | ||
| 124 | seeders, completed, leechers = self.tracker.udp_scrape(sock, connection_id, [info_hash])[0] | ||
| 125 | self.assertEqual(seeders, 1) | ||
| 126 | self.assertEqual(leechers, 0) | ||
| 127 | self.assertEqual(completed, 0) | ||
| 128 | finally: | ||
| 129 | sock.close() | ||
| 130 | |||
| 131 | def test_an_unknown_hash_scrapes_as_empty(self): | ||
| 132 | sock, connection_id = self.tracker.udp_connect() | ||
| 133 | try: | ||
| 134 | self.assertEqual(self.tracker.udp_scrape(sock, connection_id, [self.hash_for("udpnone")])[0], (0, 0, 0)) | ||
| 135 | finally: | ||
| 136 | sock.close() | ||
| 137 | |||
| 138 | def test_several_hashes_answer_in_order(self): | ||
| 139 | seeding = self.hash_for("udpmultiA") | ||
| 140 | leeching = self.hash_for("udpmultiB") | ||
| 141 | sock, connection_id = self.tracker.udp_connect() | ||
| 142 | try: | ||
| 143 | self.tracker.udp_announce(sock, connection_id, seeding, left=0) | ||
| 144 | self.tracker.udp_announce(sock, connection_id, leeching, left=1024) | ||
| 145 | answers = self.tracker.udp_scrape(sock, connection_id, [seeding, leeching]) | ||
| 146 | self.assertEqual(answers[0][0], 1, "first hash is the seeded one") | ||
| 147 | self.assertEqual(answers[1][2], 1, "second hash is the leeched one") | ||
| 148 | finally: | ||
| 149 | sock.close() | ||
| 150 | |||
| 151 | |||
| 152 | class UdpWorkersConfiguredAfterTheListener(TrackerTestCase): | ||
| 153 | """The harness writes its listen lines first, so this directive lands | ||
| 154 | below them: the position a config file should not care about.""" | ||
| 155 | |||
| 156 | config = "listen.udp.workers 4\n" | ||
| 157 | |||
| 158 | def test_the_tracker_starts_and_still_answers_udp(self): | ||
| 159 | sock, connection_id = self.tracker.udp_connect() | ||
| 160 | try: | ||
| 161 | answer = self.tracker.udp_announce(sock, connection_id, self.hash_for("udpworkers"), left=0) | ||
| 162 | self.assertEqual(answer["seeders"], 1) | ||
| 163 | finally: | ||
| 164 | sock.close() | ||
| 165 | |||
| 166 | |||
| 167 | if __name__ == "__main__": | ||
| 168 | unittest.main() | ||
diff --git a/tests/runner.c b/tests/runner.c index 148b928..71a5e2a 100644 --- a/tests/runner.c +++ b/tests/runner.c | |||
| @@ -4,7 +4,11 @@ | |||
| 4 | $id$ */ | 4 | $id$ */ |
| 5 | 5 | ||
| 6 | /* System */ | 6 | /* System */ |
| 7 | #include <stdio.h> | ||
| 8 | #include <stdlib.h> | ||
| 7 | #include <string.h> | 9 | #include <string.h> |
| 10 | #include <sys/wait.h> | ||
| 11 | #include <unistd.h> | ||
| 8 | 12 | ||
| 9 | /* Opentracker */ | 13 | /* Opentracker */ |
| 10 | #include "test.h" | 14 | #include "test.h" |
| @@ -14,7 +18,7 @@ struct module { | |||
| 14 | void (*run)(void); | 18 | void (*run)(void); |
| 15 | }; | 19 | }; |
| 16 | 20 | ||
| 17 | static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urlencoded_query}, | 21 | static const struct module modules[] = {{"ot_urlencoded", test_urlencoded}, |
| 18 | {"ot_iovec", test_iovec}, | 22 | {"ot_iovec", test_iovec}, |
| 19 | {"ot_vector", test_vector}, | 23 | {"ot_vector", test_vector}, |
| 20 | {"ot_random", test_random}, | 24 | {"ot_random", test_random}, |
| @@ -25,18 +29,85 @@ static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urle | |||
| 25 | {"ot_socket", test_socket}, | 29 | {"ot_socket", test_socket}, |
| 26 | {"ot_mutex", test_mutex}, | 30 | {"ot_mutex", test_mutex}, |
| 27 | {"ot_batch", test_batch}, | 31 | {"ot_batch", test_batch}, |
| 32 | {"ot_fdstate", test_fdstate}, | ||
| 33 | {"ot_event", test_event}, | ||
| 34 | {"ot_fmt", test_fmt}, | ||
| 28 | {"ot_accesslist", test_accesslist}, | 35 | {"ot_accesslist", test_accesslist}, |
| 29 | {NULL, NULL}}; | 36 | {NULL, NULL}}; |
| 30 | 37 | ||
| 31 | /* Run every module, or only those whose name contains argv[1] */ | 38 | struct tally { |
| 39 | unsigned long checks; | ||
| 40 | unsigned long failures; | ||
| 41 | }; | ||
| 42 | |||
| 43 | /* Each module runs in a child of its own, so that a crash or a sanitizer | ||
| 44 | abort costs that module's results rather than every module after it. The | ||
| 45 | child reports its tally through a pipe; one that dies without reporting is | ||
| 46 | counted as a failure under its own name. */ | ||
| 47 | static void run_forked(const struct module *m) { | ||
| 48 | struct tally tally = {0, 0}; | ||
| 49 | int pipefd[2], status = 0; | ||
| 50 | pid_t child; | ||
| 51 | ssize_t got; | ||
| 52 | |||
| 53 | if (pipe(pipefd)) { | ||
| 54 | test_failf(__FILE__, __LINE__, "pipe failed, cannot isolate %s", m->name); | ||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | fflush(stdout); /* or the child inherits a copy and prints it again */ | ||
| 59 | child = fork(); | ||
| 60 | if (child < 0) { | ||
| 61 | close(pipefd[0]); | ||
| 62 | close(pipefd[1]); | ||
| 63 | test_failf(__FILE__, __LINE__, "fork failed, cannot isolate %s", m->name); | ||
| 64 | return; | ||
| 65 | } | ||
| 66 | |||
| 67 | if (!child) { | ||
| 68 | close(pipefd[0]); | ||
| 69 | test_checks = test_failures = 0; | ||
| 70 | m->run(); | ||
| 71 | tally.checks = test_checks; | ||
| 72 | tally.failures = test_failures; | ||
| 73 | if (write(pipefd[1], &tally, sizeof(tally)) != (ssize_t)sizeof(tally)) | ||
| 74 | _exit(1); | ||
| 75 | fflush(stdout); | ||
| 76 | _exit(0); | ||
| 77 | } | ||
| 78 | |||
| 79 | close(pipefd[1]); | ||
| 80 | got = read(pipefd[0], &tally, sizeof(tally)); | ||
| 81 | close(pipefd[0]); | ||
| 82 | while (waitpid(child, &status, 0) < 0) | ||
| 83 | ; | ||
| 84 | |||
| 85 | if (got == (ssize_t)sizeof(tally)) { | ||
| 86 | test_checks += tally.checks; | ||
| 87 | test_failures += tally.failures; | ||
| 88 | } | ||
| 89 | if (got != (ssize_t)sizeof(tally) || !WIFEXITED(status) || WEXITSTATUS(status)) | ||
| 90 | test_failf(__FILE__, __LINE__, "%s did not run to the end", m->name); | ||
| 91 | } | ||
| 92 | |||
| 93 | /* Run every module, or only those whose name contains argv[1]. | ||
| 94 | OT_TEST_FORK=0 keeps everything in one process, which is what a debugger | ||
| 95 | wants. */ | ||
| 32 | int main(int argc, char **argv) { | 96 | int main(int argc, char **argv) { |
| 97 | const char *fork_setting = getenv("OT_TEST_FORK"); | ||
| 98 | int isolate = !fork_setting || strcmp(fork_setting, "0"); | ||
| 33 | const struct module *m; | 99 | const struct module *m; |
| 34 | 100 | ||
| 35 | for (m = modules; m->name; ++m) { | 101 | for (m = modules; m->name; ++m) { |
| 36 | if (argc > 1 && !strstr(m->name, argv[1])) | 102 | if (argc > 1 && !strstr(m->name, argv[1])) |
| 37 | continue; | 103 | continue; |
| 38 | test_module(m->name); | 104 | test_module(m->name); |
| 39 | m->run(); | 105 | if (!isolate) { |
| 106 | m->run(); | ||
| 107 | continue; | ||
| 108 | } | ||
| 109 | test_case("the module runs to the end"); | ||
| 110 | run_forked(m); | ||
| 40 | } | 111 | } |
| 41 | 112 | ||
| 42 | return test_summary(); | 113 | return test_summary(); |
diff --git a/tests/t_event.c b/tests/t_event.c new file mode 100644 index 0000000..5509f19 --- /dev/null +++ b/tests/t_event.c | |||
| @@ -0,0 +1,222 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <string.h> | ||
| 8 | #include <sys/socket.h> | ||
| 9 | #include <sys/time.h> | ||
| 10 | #include <unistd.h> | ||
| 11 | |||
| 12 | /* Opentracker */ | ||
| 13 | #include "ot_event.h" | ||
| 14 | #include "test.h" | ||
| 15 | |||
| 16 | /* Against real socket pairs: only the kernel can say whether a descriptor is | ||
| 17 | ready, so there is nothing useful to mock here. */ | ||
| 18 | |||
| 19 | #define EVENTS 16 | ||
| 20 | |||
| 21 | static int sv[2]; | ||
| 22 | static ot_event ready[EVENTS]; | ||
| 23 | |||
| 24 | static void pair_open(void) { | ||
| 25 | if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) | ||
| 26 | sv[0] = sv[1] = -1; | ||
| 27 | } | ||
| 28 | |||
| 29 | static void pair_close(void) { | ||
| 30 | if (sv[0] != -1) | ||
| 31 | close(sv[0]); | ||
| 32 | if (sv[1] != -1) | ||
| 33 | close(sv[1]); | ||
| 34 | sv[0] = sv[1] = -1; | ||
| 35 | } | ||
| 36 | |||
| 37 | static int wait_for(int fd, int what, int timeout_ms, uint32_t *generation) { | ||
| 38 | int count = event_wait(ready, EVENTS, timeout_ms), i; | ||
| 39 | |||
| 40 | for (i = 0; i < count; ++i) | ||
| 41 | if (ready[i].fd == fd && (ready[i].what & what)) { | ||
| 42 | if (generation) | ||
| 43 | *generation = ready[i].generation; | ||
| 44 | return 1; | ||
| 45 | } | ||
| 46 | return 0; | ||
| 47 | } | ||
| 48 | |||
| 49 | static unsigned long elapsed_ms(struct timeval *since) { | ||
| 50 | struct timeval now; | ||
| 51 | |||
| 52 | gettimeofday(&now, NULL); | ||
| 53 | return (unsigned long)((now.tv_sec - since->tv_sec) * 1000 + (now.tv_usec - since->tv_usec) / 1000); | ||
| 54 | } | ||
| 55 | |||
| 56 | static void case_readable(void) { | ||
| 57 | uint32_t generation = 0; | ||
| 58 | |||
| 59 | test_case("a descriptor with something to read is reported, with its generation"); | ||
| 60 | pair_open(); | ||
| 61 | CHECK(sv[0] != -1); | ||
| 62 | CHECK_INT(event_add(sv[0], EVENT_IN, 4711), 0); | ||
| 63 | CHECK_INT(write(sv[1], "x", 1), 1); | ||
| 64 | CHECK_INT(wait_for(sv[0], EVENT_IN, 1000, &generation), 1); | ||
| 65 | CHECK_INT(generation, 4711); | ||
| 66 | |||
| 67 | test_case("and keeps being reported until it is read"); | ||
| 68 | CHECK_INT(wait_for(sv[0], EVENT_IN, 1000, NULL), 1); | ||
| 69 | { | ||
| 70 | char scratch[4]; | ||
| 71 | CHECK_INT(read(sv[0], scratch, sizeof(scratch)), 1); | ||
| 72 | } | ||
| 73 | CHECK_INT(wait_for(sv[0], EVENT_IN, 50, NULL), 0); | ||
| 74 | |||
| 75 | CHECK_INT(event_del(sv[0]), 0); | ||
| 76 | pair_close(); | ||
| 77 | } | ||
| 78 | |||
| 79 | static void case_writable(void) { | ||
| 80 | test_case("a descriptor with room to write is reported"); | ||
| 81 | pair_open(); | ||
| 82 | CHECK(sv[0] != -1); | ||
| 83 | CHECK_INT(event_add(sv[0], EVENT_OUT, 1), 0); | ||
| 84 | CHECK_INT(wait_for(sv[0], EVENT_OUT, 1000, NULL), 1); | ||
| 85 | |||
| 86 | test_case("and is not, once we say we no longer care"); | ||
| 87 | CHECK_INT(event_modify(sv[0], EVENT_IN, 1), 0); | ||
| 88 | CHECK_INT(wait_for(sv[0], EVENT_OUT, 50, NULL), 0); | ||
| 89 | |||
| 90 | CHECK_INT(event_del(sv[0]), 0); | ||
| 91 | pair_close(); | ||
| 92 | } | ||
| 93 | |||
| 94 | static void case_interest_is_separate(void) { | ||
| 95 | test_case("asking only about writing does not report reading"); | ||
| 96 | pair_open(); | ||
| 97 | CHECK(sv[0] != -1); | ||
| 98 | CHECK_INT(event_add(sv[0], EVENT_OUT, 1), 0); | ||
| 99 | CHECK_INT(write(sv[1], "x", 1), 1); | ||
| 100 | CHECK_INT(wait_for(sv[0], EVENT_IN, 50, NULL), 0); | ||
| 101 | |||
| 102 | test_case("until we ask about it"); | ||
| 103 | CHECK_INT(event_modify(sv[0], EVENT_IN | EVENT_OUT, 1), 0); | ||
| 104 | CHECK_INT(wait_for(sv[0], EVENT_IN, 1000, NULL), 1); | ||
| 105 | |||
| 106 | CHECK_INT(event_del(sv[0]), 0); | ||
| 107 | pair_close(); | ||
| 108 | } | ||
| 109 | |||
| 110 | static void case_removed_is_silent(void) { | ||
| 111 | test_case("a removed descriptor is not reported again"); | ||
| 112 | pair_open(); | ||
| 113 | CHECK(sv[0] != -1); | ||
| 114 | CHECK_INT(event_add(sv[0], EVENT_IN, 1), 0); | ||
| 115 | CHECK_INT(write(sv[1], "x", 1), 1); | ||
| 116 | CHECK_INT(wait_for(sv[0], EVENT_IN, 1000, NULL), 1); | ||
| 117 | CHECK_INT(event_del(sv[0]), 0); | ||
| 118 | CHECK_INT(wait_for(sv[0], EVENT_IN, 50, NULL), 0); | ||
| 119 | pair_close(); | ||
| 120 | } | ||
| 121 | |||
| 122 | static void case_generations_do_not_mix(void) { | ||
| 123 | int other[2]; | ||
| 124 | uint32_t generation = 0; | ||
| 125 | |||
| 126 | test_case("two descriptors carry their own generations"); | ||
| 127 | pair_open(); | ||
| 128 | CHECK(sv[0] != -1); | ||
| 129 | CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, other), 0); | ||
| 130 | CHECK_INT(event_add(sv[0], EVENT_IN, 100), 0); | ||
| 131 | CHECK_INT(event_add(other[0], EVENT_IN, 200), 0); | ||
| 132 | CHECK_INT(write(sv[1], "x", 1), 1); | ||
| 133 | CHECK_INT(write(other[1], "y", 1), 1); | ||
| 134 | |||
| 135 | CHECK_INT(wait_for(sv[0], EVENT_IN, 1000, &generation), 1); | ||
| 136 | CHECK_INT(generation, 100); | ||
| 137 | CHECK_INT(wait_for(other[0], EVENT_IN, 1000, &generation), 1); | ||
| 138 | CHECK_INT(generation, 200); | ||
| 139 | |||
| 140 | test_case("and re-registering one changes only its own"); | ||
| 141 | CHECK_INT(event_modify(sv[0], EVENT_IN, 101), 0); | ||
| 142 | CHECK_INT(wait_for(sv[0], EVENT_IN, 1000, &generation), 1); | ||
| 143 | CHECK_INT(generation, 101); | ||
| 144 | CHECK_INT(wait_for(other[0], EVENT_IN, 1000, &generation), 1); | ||
| 145 | CHECK_INT(generation, 200); | ||
| 146 | |||
| 147 | event_del(sv[0]); | ||
| 148 | event_del(other[0]); | ||
| 149 | close(other[0]); | ||
| 150 | close(other[1]); | ||
| 151 | pair_close(); | ||
| 152 | } | ||
| 153 | |||
| 154 | /* Removing one of several has to leave the rest addressable: a backend that | ||
| 155 | keeps its descriptors packed moves the last one into the gap, and has to | ||
| 156 | remember where it went. */ | ||
| 157 | static void case_delete_from_the_middle(void) { | ||
| 158 | int pair[3][2]; | ||
| 159 | uint32_t generation = 0; | ||
| 160 | int i; | ||
| 161 | |||
| 162 | test_case("deleting one of three leaves the others reported correctly"); | ||
| 163 | for (i = 0; i < 3; ++i) { | ||
| 164 | CHECK_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, pair[i]), 0); | ||
| 165 | CHECK_INT(event_add(pair[i][0], EVENT_IN, (uint32_t)(10 + i)), 0); | ||
| 166 | } | ||
| 167 | CHECK_INT(event_del(pair[1][0]), 0); | ||
| 168 | |||
| 169 | for (i = 0; i < 3; i += 2) { | ||
| 170 | CHECK_INT(write(pair[i][1], "x", 1), 1); | ||
| 171 | CHECK_INT(wait_for(pair[i][0], EVENT_IN, 1000, &generation), 1); | ||
| 172 | CHECK_INT(generation, 10 + i); | ||
| 173 | } | ||
| 174 | |||
| 175 | test_case("and the survivors can still be changed and removed"); | ||
| 176 | CHECK_INT(event_modify(pair[2][0], EVENT_IN, 99), 0); | ||
| 177 | CHECK_INT(wait_for(pair[2][0], EVENT_IN, 1000, &generation), 1); | ||
| 178 | CHECK_INT(generation, 99); | ||
| 179 | CHECK_INT(event_del(pair[0][0]), 0); | ||
| 180 | CHECK_INT(event_del(pair[2][0]), 0); | ||
| 181 | CHECK_INT(wait_for(pair[2][0], EVENT_IN, 50, NULL), 0); | ||
| 182 | |||
| 183 | for (i = 0; i < 3; ++i) | ||
| 184 | close(pair[i][0]), close(pair[i][1]); | ||
| 185 | } | ||
| 186 | |||
| 187 | static void case_timeout(void) { | ||
| 188 | struct timeval started; | ||
| 189 | |||
| 190 | test_case("waiting on nothing comes back empty when the time is up"); | ||
| 191 | gettimeofday(&started, NULL); | ||
| 192 | CHECK_INT(event_wait(ready, EVENTS, 200), 0); | ||
| 193 | { | ||
| 194 | unsigned long took = elapsed_ms(&started); | ||
| 195 | /* Scheduling makes the upper bound generous. The point is that it waited | ||
| 196 | rather than returning at once, and that it came back at all */ | ||
| 197 | CHECK(took >= 150); | ||
| 198 | CHECK(took < 5000); | ||
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | static void case_unknown_descriptor(void) { | ||
| 203 | test_case("changing or removing one that was never added is refused"); | ||
| 204 | CHECK_INT(event_modify(4096, EVENT_IN, 1), -1); | ||
| 205 | CHECK_INT(event_del(4096), -1); | ||
| 206 | } | ||
| 207 | |||
| 208 | void test_event(void) { | ||
| 209 | CHECK_INT(event_init(), 0); | ||
| 210 | CHECK(event_mechanism() != NULL); | ||
| 211 | |||
| 212 | case_readable(); | ||
| 213 | case_writable(); | ||
| 214 | case_interest_is_separate(); | ||
| 215 | case_removed_is_silent(); | ||
| 216 | case_generations_do_not_mix(); | ||
| 217 | case_delete_from_the_middle(); | ||
| 218 | case_timeout(); | ||
| 219 | case_unknown_descriptor(); | ||
| 220 | |||
| 221 | event_deinit(); | ||
| 222 | } | ||
diff --git a/tests/t_fdstate.c b/tests/t_fdstate.c new file mode 100644 index 0000000..bf8724a --- /dev/null +++ b/tests/t_fdstate.c | |||
| @@ -0,0 +1,224 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <pthread.h> | ||
| 8 | #include <stdatomic.h> | ||
| 9 | #include <stdint.h> | ||
| 10 | #include <stdlib.h> | ||
| 11 | #include <string.h> | ||
| 12 | |||
| 13 | /* Opentracker */ | ||
| 14 | #include "ot_fdstate.h" | ||
| 15 | #include "test.h" | ||
| 16 | |||
| 17 | /* ------------------------------------------------------- one thread first */ | ||
| 18 | |||
| 19 | static void case_open_claim_release(void) { | ||
| 20 | char cookie[4]; | ||
| 21 | uint32_t generation; | ||
| 22 | |||
| 23 | test_case("a descriptor taken into the table can be claimed and gives back its cookie"); | ||
| 24 | generation = fdstate_open(7, cookie, 0); | ||
| 25 | CHECK_INT(fdstate_claim(7, generation), 1); | ||
| 26 | CHECK_PTR(fdstate_cookie(7), cookie); | ||
| 27 | fdstate_release(7); | ||
| 28 | |||
| 29 | test_case("and can be claimed again once released"); | ||
| 30 | CHECK_INT(fdstate_claim(7, generation), 1); | ||
| 31 | fdstate_release(7); | ||
| 32 | fdstate_close(7); | ||
| 33 | } | ||
| 34 | |||
| 35 | static void case_one_holder_at_a_time(void) { | ||
| 36 | uint32_t generation = fdstate_open(8, NULL, 0); | ||
| 37 | |||
| 38 | test_case("a descriptor somebody holds cannot be claimed again"); | ||
| 39 | CHECK_INT(fdstate_claim(8, generation), 1); | ||
| 40 | CHECK_INT(fdstate_claim(8, generation), 0); | ||
| 41 | fdstate_release(8); | ||
| 42 | CHECK_INT(fdstate_claim(8, generation), 1); | ||
| 43 | fdstate_release(8); | ||
| 44 | fdstate_close(8); | ||
| 45 | } | ||
| 46 | |||
| 47 | static void case_stale_generation_is_refused(void) { | ||
| 48 | char first[4], second[4]; | ||
| 49 | uint32_t old, new; | ||
| 50 | |||
| 51 | test_case("a caller quoting the generation of a closed descriptor is turned away"); | ||
| 52 | old = fdstate_open(9, first, 0); | ||
| 53 | fdstate_close(9); | ||
| 54 | new = fdstate_open(9, second, 0); | ||
| 55 | CHECK(old != new); | ||
| 56 | CHECK_INT(fdstate_claim(9, old), 0); | ||
| 57 | |||
| 58 | test_case("while the one quoting the new generation gets the new cookie"); | ||
| 59 | CHECK_INT(fdstate_claim(9, new), 1); | ||
| 60 | CHECK_PTR(fdstate_cookie(9), second); | ||
| 61 | fdstate_release(9); | ||
| 62 | fdstate_close(9); | ||
| 63 | } | ||
| 64 | |||
| 65 | static void case_expiry(void) { | ||
| 66 | uint32_t generation = fdstate_open(10, NULL, 100); | ||
| 67 | |||
| 68 | test_case("a descriptor past its expiry is collected"); | ||
| 69 | CHECK_INT(fdstate_claim_expired(10, 99), 0); | ||
| 70 | CHECK_INT(fdstate_claim_expired(10, 100), 1); | ||
| 71 | fdstate_close(10); | ||
| 72 | |||
| 73 | test_case("one that never expires is never collected"); | ||
| 74 | generation = fdstate_open(10, NULL, 0); | ||
| 75 | CHECK_INT(fdstate_claim_expired(10, 1 << 30), 0); | ||
| 76 | |||
| 77 | test_case("and one somebody is working on is left alone"); | ||
| 78 | fdstate_set_expiry(10, 1); | ||
| 79 | CHECK_INT(fdstate_claim(10, generation), 1); | ||
| 80 | CHECK_INT(fdstate_claim_expired(10, 1 << 30), 0); | ||
| 81 | fdstate_release(10); | ||
| 82 | CHECK_INT(fdstate_claim_expired(10, 1 << 30), 1); | ||
| 83 | fdstate_close(10); | ||
| 84 | } | ||
| 85 | |||
| 86 | static void case_out_of_range(void) { | ||
| 87 | test_case("a descriptor outside the table is refused rather than trusted"); | ||
| 88 | CHECK_INT(fdstate_claim(-1, 0), 0); | ||
| 89 | CHECK_INT(fdstate_claim((int)fdstate_slots(), 0), 0); | ||
| 90 | CHECK_INT(fdstate_claim_expired(-1, 0), 0); | ||
| 91 | CHECK_PTR(fdstate_cookie(-1), NULL); | ||
| 92 | } | ||
| 93 | |||
| 94 | /* --------------------------------------------------------- then in anger */ | ||
| 95 | |||
| 96 | /* The race this is about only exists while a descriptor is being recycled, | ||
| 97 | so recycling is driven by work done rather than by a clock: every slot is | ||
| 98 | given up after RECYCLE_AFTER claims, whatever the machine's speed. */ | ||
| 99 | |||
| 100 | #define STRESS_SLOTS 64 | ||
| 101 | #define RECYCLE_AFTER 32 | ||
| 102 | #define STRESS_CLAIMS 200000 | ||
| 103 | #define STRESS_GIVEUP (STRESS_CLAIMS * 100) | ||
| 104 | |||
| 105 | static _Atomic long stress_claims, stress_recycles, stress_violations, stress_attempts; | ||
| 106 | static _Atomic int stress_lives[STRESS_SLOTS]; | ||
| 107 | static _Atomic int stress_live[STRESS_SLOTS]; | ||
| 108 | |||
| 109 | /* Generation and cookie are published in one word so a worker can never read | ||
| 110 | a cookie that belongs to a different generation than the one it claimed | ||
| 111 | with. Each connection gets a cookie nobody else ever gets. */ | ||
| 112 | static _Atomic uint64_t stress_conn[STRESS_SLOTS]; | ||
| 113 | static _Atomic uint64_t stress_next_cookie; | ||
| 114 | |||
| 115 | #define STRESS_CONN(cookie, gen) (((uint64_t)(cookie) << 32) | (uint32_t)(gen)) | ||
| 116 | #define STRESS_CONN_COOKIE(w) ((void *)(uintptr_t)((w) >> 32)) | ||
| 117 | #define STRESS_CONN_GEN(w) ((uint32_t)(w)) | ||
| 118 | |||
| 119 | static int stress_running(void) { | ||
| 120 | return atomic_load(&stress_claims) < STRESS_CLAIMS && atomic_load(&stress_attempts) < STRESS_GIVEUP; | ||
| 121 | } | ||
| 122 | |||
| 123 | static void *stress_worker(void *arg) { | ||
| 124 | unsigned seed = (unsigned)(uintptr_t)arg; | ||
| 125 | |||
| 126 | while (stress_running()) { | ||
| 127 | int fd = (int)(rand_r(&seed) % STRESS_SLOTS); | ||
| 128 | uint64_t conn = atomic_load(&stress_conn[fd]); | ||
| 129 | |||
| 130 | atomic_fetch_add(&stress_attempts, 1); | ||
| 131 | if (!fdstate_claim(fd, STRESS_CONN_GEN(conn))) | ||
| 132 | continue; | ||
| 133 | |||
| 134 | if (fdstate_cookie(fd) != STRESS_CONN_COOKIE(conn)) | ||
| 135 | atomic_fetch_add(&stress_violations, 1); | ||
| 136 | if (atomic_fetch_sub(&stress_lives[fd], 1) <= 1) | ||
| 137 | fdstate_set_expiry(fd, 1); /* retired by its holder, as a handler would */ | ||
| 138 | if (fdstate_cookie(fd) != STRESS_CONN_COOKIE(conn)) | ||
| 139 | atomic_fetch_add(&stress_violations, 1); | ||
| 140 | |||
| 141 | fdstate_release(fd); | ||
| 142 | atomic_fetch_add(&stress_claims, 1); | ||
| 143 | } | ||
| 144 | return NULL; | ||
| 145 | } | ||
| 146 | |||
| 147 | static void *stress_keeper(void *arg) { | ||
| 148 | (void)arg; | ||
| 149 | while (stress_running()) { | ||
| 150 | int fd; | ||
| 151 | |||
| 152 | for (fd = 0; fd < STRESS_SLOTS; ++fd) { | ||
| 153 | if (atomic_load(&stress_live[fd])) { | ||
| 154 | if (!fdstate_claim_expired(fd, 1)) | ||
| 155 | continue; /* a worker has it, come back */ | ||
| 156 | fdstate_close(fd); | ||
| 157 | atomic_store(&stress_live[fd], 0); | ||
| 158 | atomic_fetch_add(&stress_recycles, 1); | ||
| 159 | } | ||
| 160 | { | ||
| 161 | void *cookie = (void *)(uintptr_t)(atomic_fetch_add(&stress_next_cookie, 1) + 1); | ||
| 162 | uint32_t gen = fdstate_open(fd, cookie, 0); | ||
| 163 | |||
| 164 | atomic_store(&stress_lives[fd], RECYCLE_AFTER); | ||
| 165 | atomic_store(&stress_conn[fd], STRESS_CONN((uintptr_t)cookie, gen)); | ||
| 166 | atomic_store(&stress_live[fd], 1); | ||
| 167 | } | ||
| 168 | } | ||
| 169 | } | ||
| 170 | return NULL; | ||
| 171 | } | ||
| 172 | |||
| 173 | static void case_recycling_under_load(void) { | ||
| 174 | pthread_t worker[4], keeper; | ||
| 175 | size_t i; | ||
| 176 | |||
| 177 | test_case("no worker is ever handed a connection from another generation"); | ||
| 178 | atomic_store(&stress_claims, 0); | ||
| 179 | atomic_store(&stress_recycles, 0); | ||
| 180 | atomic_store(&stress_violations, 0); | ||
| 181 | atomic_store(&stress_attempts, 0); | ||
| 182 | atomic_store(&stress_next_cookie, 0); | ||
| 183 | /* Every slot is opened and published before any worker runs: a worker only | ||
| 184 | ever learns of a descriptor from whoever registered it, and never | ||
| 185 | invents a generation of its own. */ | ||
| 186 | for (i = 0; i < STRESS_SLOTS; ++i) { | ||
| 187 | void *cookie = (void *)(uintptr_t)(atomic_fetch_add(&stress_next_cookie, 1) + 1); | ||
| 188 | uint32_t gen = fdstate_open((int)i, cookie, 0); | ||
| 189 | |||
| 190 | atomic_store(&stress_lives[i], RECYCLE_AFTER); | ||
| 191 | atomic_store(&stress_conn[i], STRESS_CONN((uintptr_t)cookie, gen)); | ||
| 192 | atomic_store(&stress_live[i], 1); | ||
| 193 | } | ||
| 194 | |||
| 195 | pthread_create(&keeper, NULL, stress_keeper, NULL); | ||
| 196 | for (i = 0; i < sizeof(worker) / sizeof(worker[0]); ++i) | ||
| 197 | pthread_create(&worker[i], NULL, stress_worker, (void *)(uintptr_t)(i + 1)); | ||
| 198 | for (i = 0; i < sizeof(worker) / sizeof(worker[0]); ++i) | ||
| 199 | pthread_join(worker[i], NULL); | ||
| 200 | pthread_join(keeper, NULL); | ||
| 201 | |||
| 202 | CHECK_INT(atomic_load(&stress_violations), 0); | ||
| 203 | |||
| 204 | test_case("and the run actually recycled descriptors, or it proved nothing"); | ||
| 205 | CHECK(atomic_load(&stress_claims) >= STRESS_CLAIMS); | ||
| 206 | CHECK(atomic_load(&stress_recycles) > STRESS_CLAIMS / (RECYCLE_AFTER * 4)); | ||
| 207 | |||
| 208 | for (i = 0; i < STRESS_SLOTS; ++i) | ||
| 209 | fdstate_close((int)i); | ||
| 210 | } | ||
| 211 | |||
| 212 | void test_fdstate(void) { | ||
| 213 | CHECK_INT(fdstate_init(), 0); | ||
| 214 | CHECK(fdstate_slots() > STRESS_SLOTS); | ||
| 215 | |||
| 216 | case_open_claim_release(); | ||
| 217 | case_one_holder_at_a_time(); | ||
| 218 | case_stale_generation_is_refused(); | ||
| 219 | case_expiry(); | ||
| 220 | case_out_of_range(); | ||
| 221 | case_recycling_under_load(); | ||
| 222 | |||
| 223 | fdstate_deinit(); | ||
| 224 | } | ||
diff --git a/tests/t_fmt.c b/tests/t_fmt.c new file mode 100644 index 0000000..bc23ed5 --- /dev/null +++ b/tests/t_fmt.c | |||
| @@ -0,0 +1,108 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | /* Opentracker */ | ||
| 10 | #include "ot_fmt.h" | ||
| 11 | #include "test.h" | ||
| 12 | |||
| 13 | static void case_appends_in_sequence(void) { | ||
| 14 | char buffer[64]; | ||
| 15 | char *r = buffer, *re = buffer + sizeof(buffer); | ||
| 16 | |||
| 17 | test_case("appends follow each other and advance the cursor"); | ||
| 18 | fmt_append(&r, re, "d8:completei%zde", (size_t)7); | ||
| 19 | fmt_append(&r, re, "ee"); | ||
| 20 | *r = 0; | ||
| 21 | CHECK_STR(buffer, "d8:completei7eee"); | ||
| 22 | CHECK_INT(r - buffer, 16); | ||
| 23 | } | ||
| 24 | |||
| 25 | static void case_truncation_stops_at_the_end(void) { | ||
| 26 | char buffer[16]; | ||
| 27 | char *r = buffer, *re = buffer + 8; /* pretend the buffer ends early */ | ||
| 28 | |||
| 29 | memset(buffer, '#', sizeof(buffer)); | ||
| 30 | test_case("a value too long for the room left stops at the end"); | ||
| 31 | fmt_append(&r, re, "%s", "0123456789"); | ||
| 32 | CHECK_PTR(r, re); | ||
| 33 | |||
| 34 | test_case("and writes nothing past it"); | ||
| 35 | CHECK_INT(buffer[8], '#'); | ||
| 36 | CHECK_INT(buffer[15], '#'); | ||
| 37 | } | ||
| 38 | |||
| 39 | static void case_a_full_buffer_takes_nothing_more(void) { | ||
| 40 | char buffer[16]; | ||
| 41 | char *r = buffer + 8, *re = buffer + 8; | ||
| 42 | |||
| 43 | memset(buffer, '#', sizeof(buffer)); | ||
| 44 | test_case("appending to a cursor already at the end changes nothing"); | ||
| 45 | fmt_append(&r, re, "more"); | ||
| 46 | CHECK_PTR(r, re); | ||
| 47 | CHECK_INT(buffer[8], '#'); | ||
| 48 | } | ||
| 49 | |||
| 50 | static void case_an_append_that_just_fits(void) { | ||
| 51 | char buffer[8]; | ||
| 52 | char *r = buffer, *re = buffer + 3; | ||
| 53 | |||
| 54 | memset(buffer, '#', sizeof(buffer)); | ||
| 55 | test_case("three characters into three bytes of room still truncate"); | ||
| 56 | fmt_append(&r, re, "abc"); | ||
| 57 | CHECK_PTR(r, re); | ||
| 58 | CHECK_INT(buffer[0], 'a'); | ||
| 59 | CHECK_INT(buffer[1], 'b'); | ||
| 60 | |||
| 61 | test_case("because snprintf spends the last byte on its terminator"); | ||
| 62 | CHECK_INT(buffer[2], 0); | ||
| 63 | CHECK_INT(buffer[3], '#'); | ||
| 64 | } | ||
| 65 | |||
| 66 | static void case_bytes_are_all_or_nothing(void) { | ||
| 67 | char buffer[16]; | ||
| 68 | char *r = buffer, *re = buffer + 8; | ||
| 69 | |||
| 70 | memset(buffer, '#', sizeof(buffer)); | ||
| 71 | test_case("a run of bytes is written when it fits"); | ||
| 72 | fmt_bytes(&r, re, "\0\1\2\3", 4); | ||
| 73 | CHECK_PTR(r, buffer + 4); | ||
| 74 | CHECK_MEM(buffer, "\0\1\2\3", 4); | ||
| 75 | |||
| 76 | test_case("a run that would not fit is refused whole"); | ||
| 77 | fmt_bytes(&r, re, "abcde", 5); | ||
| 78 | CHECK_PTR(r, re); | ||
| 79 | CHECK_INT(buffer[4], '#'); | ||
| 80 | |||
| 81 | test_case("a run of exactly the room left is written"); | ||
| 82 | r = buffer + 4; | ||
| 83 | fmt_bytes(&r, re, "wxyz", 4); | ||
| 84 | CHECK_PTR(r, re); | ||
| 85 | CHECK_MEM(buffer + 4, "wxyz", 4); | ||
| 86 | CHECK_INT(buffer[8], '#'); | ||
| 87 | } | ||
| 88 | |||
| 89 | static void case_bytes_do_not_terminate(void) { | ||
| 90 | char buffer[8]; | ||
| 91 | char *r = buffer, *re = buffer + 4; | ||
| 92 | |||
| 93 | memset(buffer, '#', sizeof(buffer)); | ||
| 94 | test_case("unlike an append, a run of bytes may fill the room exactly"); | ||
| 95 | fmt_bytes(&r, re, "abcd", 4); | ||
| 96 | CHECK_PTR(r, re); | ||
| 97 | CHECK_MEM(buffer, "abcd", 4); | ||
| 98 | CHECK_INT(buffer[4], '#'); | ||
| 99 | } | ||
| 100 | |||
| 101 | void test_fmt(void) { | ||
| 102 | case_appends_in_sequence(); | ||
| 103 | case_truncation_stops_at_the_end(); | ||
| 104 | case_a_full_buffer_takes_nothing_more(); | ||
| 105 | case_an_append_that_just_fits(); | ||
| 106 | case_bytes_are_all_or_nothing(); | ||
| 107 | case_bytes_do_not_terminate(); | ||
| 108 | } | ||
diff --git a/tests/t_hex.c b/tests/t_hex.c index 7cce8ac..a761737 100644 --- a/tests/t_hex.c +++ b/tests/t_hex.c | |||
| @@ -18,7 +18,7 @@ static void case_encode(void) { | |||
| 18 | 18 | ||
| 19 | test_case("a hash encodes to forty upper case digits and a terminator"); | 19 | test_case("a hash encodes to forty upper case digits and a terminator"); |
| 20 | memset(dest, 0x7f, sizeof(dest)); | 20 | memset(dest, 0x7f, sizeof(dest)); |
| 21 | CHECK_PTR(hex_fromhash(dest, sample), dest); | 21 | CHECK_PTR(hex_from_hash(dest, sample), dest); |
| 22 | CHECK_STR(dest, "000123456789ABCDEFFF10203040506070809" "0A0"); | 22 | CHECK_STR(dest, "000123456789ABCDEFFF10203040506070809" "0A0"); |
| 23 | CHECK_INT(strlen(dest), 40); | 23 | CHECK_INT(strlen(dest), 40); |
| 24 | CHECK_INT((unsigned char)dest[41], 0x7f); | 24 | CHECK_INT((unsigned char)dest[41], 0x7f); |
| @@ -28,7 +28,7 @@ static void case_decode(void) { | |||
| 28 | ot_hash hash; | 28 | ot_hash hash; |
| 29 | 29 | ||
| 30 | test_case("forty digits decode back to the hash"); | 30 | test_case("forty digits decode back to the hash"); |
| 31 | CHECK(hex_tohash(hash, "000123456789ABCDEFFF10203040506070809" "0A0")); | 31 | CHECK(hex_to_hash(hash, "000123456789ABCDEFFF10203040506070809" "0A0")); |
| 32 | CHECK_MEM(hash, sample, sizeof(sample)); | 32 | CHECK_MEM(hash, sample, sizeof(sample)); |
| 33 | } | 33 | } |
| 34 | 34 | ||
| @@ -41,8 +41,8 @@ static void case_round_trip(void) { | |||
| 41 | for (i = 0; i < 256; ++i) { | 41 | for (i = 0; i < 256; ++i) { |
| 42 | uint8_t probe[20]; | 42 | uint8_t probe[20]; |
| 43 | memset(probe, (uint8_t)i, sizeof(probe)); | 43 | memset(probe, (uint8_t)i, sizeof(probe)); |
| 44 | hex_fromhash(text, probe); | 44 | hex_from_hash(text, probe); |
| 45 | if (!hex_tohash(hash, text) || memcmp(hash, probe, sizeof(probe))) { | 45 | if (!hex_to_hash(hash, text) || memcmp(hash, probe, sizeof(probe))) { |
| 46 | test_failf(__FILE__, __LINE__, "round trip failed for byte value %zu", i); | 46 | test_failf(__FILE__, __LINE__, "round trip failed for byte value %zu", i); |
| 47 | return; | 47 | return; |
| 48 | } | 48 | } |
| @@ -54,8 +54,8 @@ static void case_decode_is_case_insensitive(void) { | |||
| 54 | ot_hash upper, lower; | 54 | ot_hash upper, lower; |
| 55 | 55 | ||
| 56 | test_case("lower case digits decode the same as upper case"); | 56 | test_case("lower case digits decode the same as upper case"); |
| 57 | CHECK(hex_tohash(upper, "ABCDEF0123456789ABCDEF0123456789ABCDEF01")); | 57 | CHECK(hex_to_hash(upper, "ABCDEF0123456789ABCDEF0123456789ABCDEF01")); |
| 58 | CHECK(hex_tohash(lower, "abcdef0123456789abcdef0123456789abcdef01")); | 58 | CHECK(hex_to_hash(lower, "abcdef0123456789abcdef0123456789abcdef01")); |
| 59 | CHECK_MEM(lower, upper, sizeof(ot_hash)); | 59 | CHECK_MEM(lower, upper, sizeof(ot_hash)); |
| 60 | } | 60 | } |
| 61 | 61 | ||
| @@ -63,18 +63,18 @@ static void case_rejects_non_digits(void) { | |||
| 63 | ot_hash hash; | 63 | ot_hash hash; |
| 64 | 64 | ||
| 65 | test_case("a non hex character anywhere rejects the whole string"); | 65 | test_case("a non hex character anywhere rejects the whole string"); |
| 66 | CHECK_INT(hex_tohash(hash, "g00123456789ABCDEFFF102030405060708090A0"), 0); | 66 | CHECK_INT(hex_to_hash(hash, "g00123456789ABCDEFFF102030405060708090A0"), 0); |
| 67 | CHECK_INT(hex_tohash(hash, "000123456789ABCDEFFF102030405060708090Ag"), 0); | 67 | CHECK_INT(hex_to_hash(hash, "000123456789ABCDEFFF102030405060708090Ag"), 0); |
| 68 | CHECK_INT(hex_tohash(hash, "000123456789ABCDEF:F102030405060708090A"), 0); | 68 | CHECK_INT(hex_to_hash(hash, "000123456789ABCDEF:F102030405060708090A"), 0); |
| 69 | } | 69 | } |
| 70 | 70 | ||
| 71 | static void case_rejects_short_input(void) { | 71 | static void case_rejects_short_input(void) { |
| 72 | ot_hash hash; | 72 | ot_hash hash; |
| 73 | 73 | ||
| 74 | test_case("a string shorter than forty digits is rejected"); | 74 | test_case("a string shorter than forty digits is rejected"); |
| 75 | CHECK_INT(hex_tohash(hash, ""), 0); | 75 | CHECK_INT(hex_to_hash(hash, ""), 0); |
| 76 | CHECK_INT(hex_tohash(hash, "00"), 0); | 76 | CHECK_INT(hex_to_hash(hash, "00"), 0); |
| 77 | CHECK_INT(hex_tohash(hash, "000123456789ABCDEFFF102030405060708090A"), 0); | 77 | CHECK_INT(hex_to_hash(hash, "000123456789ABCDEFFF102030405060708090A"), 0); |
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | static void case_leaves_hash_untouched_on_failure(void) { | 80 | static void case_leaves_hash_untouched_on_failure(void) { |
| @@ -82,7 +82,7 @@ static void case_leaves_hash_untouched_on_failure(void) { | |||
| 82 | 82 | ||
| 83 | test_case("a rejected string leaves the destination untouched"); | 83 | test_case("a rejected string leaves the destination untouched"); |
| 84 | memset(hash, 0x5a, sizeof(hash)); | 84 | memset(hash, 0x5a, sizeof(hash)); |
| 85 | CHECK_INT(hex_tohash(hash, "000123456789ABCDEFFF10203040506070809XX0"), 0); | 85 | CHECK_INT(hex_to_hash(hash, "000123456789ABCDEFFF10203040506070809XX0"), 0); |
| 86 | CHECK_MEM(hash, "\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a", sizeof(hash)); | 86 | CHECK_MEM(hash, "\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a", sizeof(hash)); |
| 87 | } | 87 | } |
| 88 | 88 | ||
| @@ -96,7 +96,7 @@ static void case_stops_at_the_terminator(void) { | |||
| 96 | memcpy(guarded, "0012", 4); | 96 | memcpy(guarded, "0012", 4); |
| 97 | guarded[4] = 0; | 97 | guarded[4] = 0; |
| 98 | memset(guarded + 5, 'f', 3); | 98 | memset(guarded + 5, 'f', 3); |
| 99 | CHECK_INT(hex_tohash(hash, guarded), 0); | 99 | CHECK_INT(hex_to_hash(hash, guarded), 0); |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | static void case_isdigit(void) { | 102 | static void case_isdigit(void) { |
diff --git a/tests/t_iovec.c b/tests/t_iovec.c index 89f3fda..2553141 100644 --- a/tests/t_iovec.c +++ b/tests/t_iovec.c | |||
| @@ -18,13 +18,13 @@ static void case_increase_and_length(void) { | |||
| 18 | void *first, *second; | 18 | void *first, *second; |
| 19 | 19 | ||
| 20 | test_case("increase appends an entry and length is their sum"); | 20 | test_case("increase appends an entry and length is their sum"); |
| 21 | first = iovec_increase(&entries, &vector, 100); | 21 | first = iovec_new_chunk(&entries, &vector, 100); |
| 22 | CHECK(first != NULL); | 22 | CHECK(first != NULL); |
| 23 | CHECK_INT(entries, 1); | 23 | CHECK_INT(entries, 1); |
| 24 | CHECK_INT(vector[0].iov_len, 100); | 24 | CHECK_INT(vector[0].iov_len, 100); |
| 25 | CHECK_PTR(vector[0].iov_base, first); | 25 | CHECK_PTR(vector[0].iov_base, first); |
| 26 | 26 | ||
| 27 | second = iovec_increase(&entries, &vector, 50); | 27 | second = iovec_new_chunk(&entries, &vector, 50); |
| 28 | CHECK(second != NULL); | 28 | CHECK(second != NULL); |
| 29 | CHECK_INT(entries, 2); | 29 | CHECK_INT(entries, 2); |
| 30 | CHECK_INT(vector[1].iov_len, 50); | 30 | CHECK_INT(vector[1].iov_len, 50); |
| @@ -38,8 +38,8 @@ static void case_free_resets(void) { | |||
| 38 | struct iovec *vector = NULL; | 38 | struct iovec *vector = NULL; |
| 39 | 39 | ||
| 40 | test_case("free releases the entries and resets the vector"); | 40 | test_case("free releases the entries and resets the vector"); |
| 41 | CHECK(iovec_increase(&entries, &vector, 10) != NULL); | 41 | CHECK(iovec_new_chunk(&entries, &vector, 10) != NULL); |
| 42 | CHECK(iovec_increase(&entries, &vector, 10) != NULL); | 42 | CHECK(iovec_new_chunk(&entries, &vector, 10) != NULL); |
| 43 | iovec_free(&entries, &vector); | 43 | iovec_free(&entries, &vector); |
| 44 | CHECK_INT(entries, 0); | 44 | CHECK_INT(entries, 0); |
| 45 | CHECK_PTR(vector, NULL); | 45 | CHECK_PTR(vector, NULL); |
| @@ -52,12 +52,12 @@ static void case_fixlast_shrinks(void) { | |||
| 52 | char *block; | 52 | char *block; |
| 53 | 53 | ||
| 54 | test_case("fixlast trims the last entry to the bytes actually used"); | 54 | test_case("fixlast trims the last entry to the bytes actually used"); |
| 55 | CHECK(iovec_increase(&entries, &vector, 64) != NULL); | 55 | CHECK(iovec_new_chunk(&entries, &vector, 64) != NULL); |
| 56 | block = iovec_increase(&entries, &vector, 1000); | 56 | block = iovec_new_chunk(&entries, &vector, 1000); |
| 57 | CHECK(block != NULL); | 57 | CHECK(block != NULL); |
| 58 | memset(block, 'x', 20); | 58 | memset(block, 'x', 20); |
| 59 | 59 | ||
| 60 | iovec_fixlast(&entries, &vector, block + 20); | 60 | iovec_trim_last(&entries, &vector, block + 20); |
| 61 | CHECK_INT(entries, 2); | 61 | CHECK_INT(entries, 2); |
| 62 | CHECK_INT(vector[1].iov_len, 20); | 62 | CHECK_INT(vector[1].iov_len, 20); |
| 63 | CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 84); | 63 | CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 84); |
| @@ -71,7 +71,7 @@ static void case_fixlast_on_empty(void) { | |||
| 71 | struct iovec *vector = NULL; | 71 | struct iovec *vector = NULL; |
| 72 | 72 | ||
| 73 | test_case("fixlast on an empty vector does nothing"); | 73 | test_case("fixlast on an empty vector does nothing"); |
| 74 | iovec_fixlast(&entries, &vector, NULL); | 74 | iovec_trim_last(&entries, &vector, NULL); |
| 75 | CHECK_INT(entries, 0); | 75 | CHECK_INT(entries, 0); |
| 76 | CHECK_PTR(vector, NULL); | 76 | CHECK_PTR(vector, NULL); |
| 77 | } | 77 | } |
| @@ -103,9 +103,9 @@ static void case_fix_increase(void) { | |||
| 103 | char *block; | 103 | char *block; |
| 104 | 104 | ||
| 105 | test_case("fix_increase trims the previous entry and adds a new one"); | 105 | test_case("fix_increase trims the previous entry and adds a new one"); |
| 106 | block = iovec_increase(&entries, &vector, 500); | 106 | block = iovec_new_chunk(&entries, &vector, 500); |
| 107 | CHECK(block != NULL); | 107 | CHECK(block != NULL); |
| 108 | block = iovec_fix_increase_or_free(&entries, &vector, block + 30, 200); | 108 | block = iovec_next_chunk(&entries, &vector, block + 30, 200); |
| 109 | CHECK(block != NULL); | 109 | CHECK(block != NULL); |
| 110 | CHECK_INT(entries, 2); | 110 | CHECK_INT(entries, 2); |
| 111 | CHECK_INT(vector[0].iov_len, 30); | 111 | CHECK_INT(vector[0].iov_len, 30); |
diff --git a/tests/t_ip.c b/tests/t_ip.c index 2fe3a72..8d71679 100644 --- a/tests/t_ip.c +++ b/tests/t_ip.c | |||
| @@ -95,27 +95,27 @@ static void case_scan_forms(void) { | |||
| 95 | char ip[16]; | 95 | char ip[16]; |
| 96 | 96 | ||
| 97 | test_case("2.2 form 1, every group spelled out"); | 97 | test_case("2.2 form 1, every group spelled out"); |
| 98 | CHECK_INT(ip_scan("1111:2222:3333:4444:5555:6666:7777:8888", ip), 39); | 98 | CHECK_INT(ip_parse("1111:2222:3333:4444:5555:6666:7777:8888", ip), 39); |
| 99 | CHECK_MEM(ip, "\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x66\x66\x77\x77\x88\x88", 16); | 99 | CHECK_MEM(ip, "\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x66\x66\x77\x77\x88\x88", 16); |
| 100 | 100 | ||
| 101 | test_case("2.2 form 2, zero groups abbreviated"); | 101 | test_case("2.2 form 2, zero groups abbreviated"); |
| 102 | CHECK_INT(ip_scan("2001:db8::1", ip), 11); | 102 | CHECK_INT(ip_parse("2001:db8::1", ip), 11); |
| 103 | CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); | 103 | CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); |
| 104 | CHECK_INT(ip_scan("::1", ip), 3); | 104 | CHECK_INT(ip_parse("::1", ip), 3); |
| 105 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); | 105 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); |
| 106 | CHECK_INT(ip_scan("::", ip), 2); | 106 | CHECK_INT(ip_parse("::", ip), 2); |
| 107 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16); | 107 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16); |
| 108 | CHECK_INT(ip_scan("1::", ip), 3); | 108 | CHECK_INT(ip_parse("1::", ip), 3); |
| 109 | CHECK_MEM(ip, "\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16); | 109 | CHECK_MEM(ip, "\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16); |
| 110 | 110 | ||
| 111 | test_case("2.2 form 3, a dotted quad in the last two groups"); | 111 | test_case("2.2 form 3, a dotted quad in the last two groups"); |
| 112 | CHECK_INT(ip_scan("::ffff:1.2.3.4", ip), 14); | 112 | CHECK_INT(ip_parse("::ffff:1.2.3.4", ip), 14); |
| 113 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x03\x04", 16); | 113 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x03\x04", 16); |
| 114 | CHECK_INT(ip_scan("1:2:3:4:5:6:1.2.3.4", ip), 19); | 114 | CHECK_INT(ip_parse("1:2:3:4:5:6:1.2.3.4", ip), 19); |
| 115 | CHECK_MEM(ip, "\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x01\x02\x03\x04", 16); | 115 | CHECK_MEM(ip, "\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x01\x02\x03\x04", 16); |
| 116 | 116 | ||
| 117 | test_case("upper case input is accepted"); | 117 | test_case("upper case input is accepted"); |
| 118 | CHECK_INT(ip_scan("2001:DB8::1", ip), 11); | 118 | CHECK_INT(ip_parse("2001:DB8::1", ip), 11); |
| 119 | CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); | 119 | CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); |
| 120 | } | 120 | } |
| 121 | 121 | ||
| @@ -125,7 +125,7 @@ static void case_scan_accepts_bare_ipv4(void) { | |||
| 125 | /* Every address opentracker holds is sixteen bytes, so an operator writing | 125 | /* Every address opentracker holds is sixteen bytes, so an operator writing |
| 126 | a plain IPv4 address in the config has to land as the v4 mapped form */ | 126 | a plain IPv4 address in the config has to land as the v4 mapped form */ |
| 127 | test_case("a bare IPv4 address is stored v4 mapped"); | 127 | test_case("a bare IPv4 address is stored v4 mapped"); |
| 128 | CHECK_INT(ip_scan("10.0.0.1", ip), 8); | 128 | CHECK_INT(ip_parse("10.0.0.1", ip), 8); |
| 129 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x0a\x00\x00\x01", 16); | 129 | CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x0a\x00\x00\x01", 16); |
| 130 | CHECK(ip_is_v4mapped(ip)); | 130 | CHECK(ip_is_v4mapped(ip)); |
| 131 | } | 131 | } |
| @@ -136,39 +136,39 @@ static void case_scan_reports_length(void) { | |||
| 136 | /* The config parser continues from where the address ended, which is how | 136 | /* The config parser continues from where the address ended, which is how |
| 137 | "address:port" and "address/bits" are read */ | 137 | "address:port" and "address/bits" are read */ |
| 138 | test_case("scanning stops at the character after the address"); | 138 | test_case("scanning stops at the character after the address"); |
| 139 | CHECK_INT(ip_scan("10.0.0.1:6969", ip), 8); | 139 | CHECK_INT(ip_parse("10.0.0.1:6969", ip), 8); |
| 140 | CHECK_INT(ip_scan("2001:db8::1/64", ip), 11); | 140 | CHECK_INT(ip_parse("2001:db8::1/64", ip), 11); |
| 141 | CHECK_INT(ip_scan("::1]", ip), 3); | 141 | CHECK_INT(ip_parse("::1]", ip), 3); |
| 142 | CHECK_INT(ip_scan("1:2:3:4:5:6:7:8 trailing", ip), 15); | 142 | CHECK_INT(ip_parse("1:2:3:4:5:6:7:8 trailing", ip), 15); |
| 143 | } | 143 | } |
| 144 | 144 | ||
| 145 | static void case_scan_rejects(void) { | 145 | static void case_scan_rejects(void) { |
| 146 | char ip[16]; | 146 | char ip[16]; |
| 147 | 147 | ||
| 148 | test_case("an empty or junk string is refused"); | 148 | test_case("an empty or junk string is refused"); |
| 149 | CHECK_INT(ip_scan("", ip), 0); | 149 | CHECK_INT(ip_parse("", ip), 0); |
| 150 | CHECK_INT(ip_scan("x", ip), 0); | 150 | CHECK_INT(ip_parse("x", ip), 0); |
| 151 | CHECK_INT(ip_scan(":", ip), 0); | 151 | CHECK_INT(ip_parse(":", ip), 0); |
| 152 | CHECK_INT(ip_scan(":1:2", ip), 0); | 152 | CHECK_INT(ip_parse(":1:2", ip), 0); |
| 153 | 153 | ||
| 154 | test_case("too few groups without :: are refused"); | 154 | test_case("too few groups without :: are refused"); |
| 155 | CHECK_INT(ip_scan("1:2:3:4:5:6:7", ip), 0); | 155 | CHECK_INT(ip_parse("1:2:3:4:5:6:7", ip), 0); |
| 156 | CHECK_INT(ip_scan("2001:db8:1", ip), 0); | 156 | CHECK_INT(ip_parse("2001:db8:1", ip), 0); |
| 157 | 157 | ||
| 158 | /* libowfat accepted a prefix of each of these and carried on. Taking only | 158 | /* libowfat accepted a prefix of each of these and carried on. Taking only |
| 159 | part of a malformed address is worse than refusing it, especially where | 159 | part of a malformed address is worse than refusing it, especially where |
| 160 | the text comes from a config file or an X-Forwarded-For header. */ | 160 | the text comes from a config file or an X-Forwarded-For header. */ |
| 161 | test_case("a second :: is refused rather than truncated"); | 161 | test_case("a second :: is refused rather than truncated"); |
| 162 | CHECK_INT(ip_scan("1::2::3", ip), 0); | 162 | CHECK_INT(ip_parse("1::2::3", ip), 0); |
| 163 | CHECK_INT(ip_scan("::1::", ip), 0); | 163 | CHECK_INT(ip_parse("::1::", ip), 0); |
| 164 | CHECK_INT(ip_scan("::::", ip), 0); | 164 | CHECK_INT(ip_parse("::::", ip), 0); |
| 165 | 165 | ||
| 166 | test_case("more than eight groups is refused"); | 166 | test_case("more than eight groups is refused"); |
| 167 | CHECK_INT(ip_scan("1:2:3:4:5:6:7:8:9", ip), 0); | 167 | CHECK_INT(ip_parse("1:2:3:4:5:6:7:8:9", ip), 0); |
| 168 | 168 | ||
| 169 | test_case("a group of more than four digits is refused, not truncated"); | 169 | test_case("a group of more than four digits is refused, not truncated"); |
| 170 | CHECK_INT(ip_scan("12345::1", ip), 0); | 170 | CHECK_INT(ip_parse("12345::1", ip), 0); |
| 171 | CHECK_INT(ip_scan("fffff::1", ip), 0); | 171 | CHECK_INT(ip_parse("fffff::1", ip), 0); |
| 172 | } | 172 | } |
| 173 | 173 | ||
| 174 | static void case_round_trip(void) { | 174 | static void case_round_trip(void) { |
| @@ -187,7 +187,7 @@ static void case_round_trip(void) { | |||
| 187 | 187 | ||
| 188 | test_case("every formatted address scans back to the same bytes"); | 188 | test_case("every formatted address scans back to the same bytes"); |
| 189 | for (i = 0; samples[i]; ++i) { | 189 | for (i = 0; samples[i]; ++i) { |
| 190 | size_t n = ip_scan(samples[i], ip); | 190 | size_t n = ip_parse(samples[i], ip); |
| 191 | if (n != strlen(samples[i])) { | 191 | if (n != strlen(samples[i])) { |
| 192 | test_failf(__FILE__, __LINE__, "\"%s\" consumed %zu of %zu", samples[i], n, strlen(samples[i])); | 192 | test_failf(__FILE__, __LINE__, "\"%s\" consumed %zu of %zu", samples[i], n, strlen(samples[i])); |
| 193 | return; | 193 | return; |
| @@ -197,7 +197,7 @@ static void case_round_trip(void) { | |||
| 197 | test_failf(__FILE__, __LINE__, "\"%s\" formatted back as \"%s\"", samples[i], text); | 197 | test_failf(__FILE__, __LINE__, "\"%s\" formatted back as \"%s\"", samples[i], text); |
| 198 | return; | 198 | return; |
| 199 | } | 199 | } |
| 200 | if (!ip_scan(text, again) || memcmp(ip, again, 16)) { | 200 | if (!ip_parse(text, again) || memcmp(ip, again, 16)) { |
| 201 | test_failf(__FILE__, __LINE__, "\"%s\" did not survive a second pass", samples[i]); | 201 | test_failf(__FILE__, __LINE__, "\"%s\" did not survive a second pass", samples[i]); |
| 202 | return; | 202 | return; |
| 203 | } | 203 | } |
diff --git a/tests/t_scan.c b/tests/t_scan.c index 89f1590..aab4413 100644 --- a/tests/t_scan.c +++ b/tests/t_scan.c | |||
| @@ -15,13 +15,13 @@ static void case_plain_numbers(void) { | |||
| 15 | uint64_t value; | 15 | uint64_t value; |
| 16 | 16 | ||
| 17 | test_case("a decimal number is read and its length reported"); | 17 | test_case("a decimal number is read and its length reported"); |
| 18 | CHECK_INT(scan_udec("0", &value, 65535), 1); | 18 | CHECK_INT(scan_uint("0", &value, 65535), 1); |
| 19 | CHECK_INT(value, 0); | 19 | CHECK_INT(value, 0); |
| 20 | CHECK_INT(scan_udec("7", &value, 65535), 1); | 20 | CHECK_INT(scan_uint("7", &value, 65535), 1); |
| 21 | CHECK_INT(value, 7); | 21 | CHECK_INT(value, 7); |
| 22 | CHECK_INT(scan_udec("6969", &value, 65535), 4); | 22 | CHECK_INT(scan_uint("6969", &value, 65535), 4); |
| 23 | CHECK_INT(value, 6969); | 23 | CHECK_INT(value, 6969); |
| 24 | CHECK_INT(scan_udec("65535", &value, 65535), 5); | 24 | CHECK_INT(scan_uint("65535", &value, 65535), 5); |
| 25 | CHECK_INT(value, 65535); | 25 | CHECK_INT(value, 65535); |
| 26 | } | 26 | } |
| 27 | 27 | ||
| @@ -29,11 +29,11 @@ static void case_stops_at_non_digit(void) { | |||
| 29 | uint64_t value; | 29 | uint64_t value; |
| 30 | 30 | ||
| 31 | test_case("scanning stops at the first character that is not a digit"); | 31 | test_case("scanning stops at the first character that is not a digit"); |
| 32 | CHECK_INT(scan_udec("6969:rest", &value, 65535), 4); | 32 | CHECK_INT(scan_uint("6969:rest", &value, 65535), 4); |
| 33 | CHECK_INT(value, 6969); | 33 | CHECK_INT(value, 6969); |
| 34 | CHECK_INT(scan_udec("128/8", &value, 65535), 3); | 34 | CHECK_INT(scan_uint("128/8", &value, 65535), 3); |
| 35 | CHECK_INT(value, 128); | 35 | CHECK_INT(value, 128); |
| 36 | CHECK_INT(scan_udec("42\n", &value, 65535), 2); | 36 | CHECK_INT(scan_uint("42\n", &value, 65535), 2); |
| 37 | CHECK_INT(value, 42); | 37 | CHECK_INT(value, 42); |
| 38 | } | 38 | } |
| 39 | 39 | ||
| @@ -41,9 +41,9 @@ static void case_rejects_whitespace(void) { | |||
| 41 | uint64_t value = 0xdead; | 41 | uint64_t value = 0xdead; |
| 42 | 42 | ||
| 43 | test_case("leading whitespace is not skipped"); | 43 | test_case("leading whitespace is not skipped"); |
| 44 | CHECK_INT(scan_udec(" 1", &value, 65535), 0); | 44 | CHECK_INT(scan_uint(" 1", &value, 65535), 0); |
| 45 | CHECK_INT(scan_udec("\t1", &value, 65535), 0); | 45 | CHECK_INT(scan_uint("\t1", &value, 65535), 0); |
| 46 | CHECK_INT(scan_udec("\n1", &value, 65535), 0); | 46 | CHECK_INT(scan_uint("\n1", &value, 65535), 0); |
| 47 | CHECK_INT(value, 0xdead); | 47 | CHECK_INT(value, 0xdead); |
| 48 | } | 48 | } |
| 49 | 49 | ||
| @@ -51,9 +51,9 @@ static void case_rejects_sign(void) { | |||
| 51 | uint64_t value = 0xdead; | 51 | uint64_t value = 0xdead; |
| 52 | 52 | ||
| 53 | test_case("a sign is not accepted"); | 53 | test_case("a sign is not accepted"); |
| 54 | CHECK_INT(scan_udec("+1", &value, 65535), 0); | 54 | CHECK_INT(scan_uint("+1", &value, 65535), 0); |
| 55 | CHECK_INT(scan_udec("-1", &value, 65535), 0); | 55 | CHECK_INT(scan_uint("-1", &value, 65535), 0); |
| 56 | CHECK_INT(scan_udec("-0", &value, 65535), 0); | 56 | CHECK_INT(scan_uint("-0", &value, 65535), 0); |
| 57 | CHECK_INT(value, 0xdead); | 57 | CHECK_INT(value, 0xdead); |
| 58 | } | 58 | } |
| 59 | 59 | ||
| @@ -61,9 +61,9 @@ static void case_rejects_empty_and_junk(void) { | |||
| 61 | uint64_t value = 0xdead; | 61 | uint64_t value = 0xdead; |
| 62 | 62 | ||
| 63 | test_case("a string with no leading digit yields nothing"); | 63 | test_case("a string with no leading digit yields nothing"); |
| 64 | CHECK_INT(scan_udec("", &value, 65535), 0); | 64 | CHECK_INT(scan_uint("", &value, 65535), 0); |
| 65 | CHECK_INT(scan_udec("x", &value, 65535), 0); | 65 | CHECK_INT(scan_uint("x", &value, 65535), 0); |
| 66 | CHECK_INT(scan_udec(":80", &value, 65535), 0); | 66 | CHECK_INT(scan_uint(":80", &value, 65535), 0); |
| 67 | CHECK_INT(value, 0xdead); | 67 | CHECK_INT(value, 0xdead); |
| 68 | } | 68 | } |
| 69 | 69 | ||
| @@ -71,9 +71,9 @@ static void case_no_alternative_bases(void) { | |||
| 71 | uint64_t value; | 71 | uint64_t value; |
| 72 | 72 | ||
| 73 | test_case("hexadecimal and octal notations are read as plain decimal"); | 73 | test_case("hexadecimal and octal notations are read as plain decimal"); |
| 74 | CHECK_INT(scan_udec("0x10", &value, 65535), 1); | 74 | CHECK_INT(scan_uint("0x10", &value, 65535), 1); |
| 75 | CHECK_INT(value, 0); | 75 | CHECK_INT(value, 0); |
| 76 | CHECK_INT(scan_udec("010", &value, 65535), 3); | 76 | CHECK_INT(scan_uint("010", &value, 65535), 3); |
| 77 | CHECK_INT(value, 10); | 77 | CHECK_INT(value, 10); |
| 78 | } | 78 | } |
| 79 | 79 | ||
| @@ -81,13 +81,13 @@ static void case_refuses_out_of_range(void) { | |||
| 81 | uint64_t value = 0xdead; | 81 | uint64_t value = 0xdead; |
| 82 | 82 | ||
| 83 | test_case("a value above max is refused, not truncated"); | 83 | test_case("a value above max is refused, not truncated"); |
| 84 | CHECK_INT(scan_udec("65536", &value, 65535), 0); | 84 | CHECK_INT(scan_uint("65536", &value, 65535), 0); |
| 85 | CHECK_INT(scan_udec("99999", &value, 65535), 0); | 85 | CHECK_INT(scan_uint("99999", &value, 65535), 0); |
| 86 | CHECK_INT(scan_udec("129", &value, 128), 0); | 86 | CHECK_INT(scan_uint("129", &value, 128), 0); |
| 87 | CHECK_INT(value, 0xdead); | 87 | CHECK_INT(value, 0xdead); |
| 88 | 88 | ||
| 89 | test_case("the boundary itself is accepted"); | 89 | test_case("the boundary itself is accepted"); |
| 90 | CHECK_INT(scan_udec("128", &value, 128), 3); | 90 | CHECK_INT(scan_uint("128", &value, 128), 3); |
| 91 | CHECK_INT(value, 128); | 91 | CHECK_INT(value, 128); |
| 92 | } | 92 | } |
| 93 | 93 | ||
| @@ -95,21 +95,21 @@ static void case_sixty_four_bit_range(void) { | |||
| 95 | uint64_t value; | 95 | uint64_t value; |
| 96 | 96 | ||
| 97 | test_case("the full unsigned 64 bit range is available"); | 97 | test_case("the full unsigned 64 bit range is available"); |
| 98 | CHECK_INT(scan_udec("18446744073709551615", &value, UINT64_MAX), 20); | 98 | CHECK_INT(scan_uint("18446744073709551615", &value, UINT64_MAX), 20); |
| 99 | CHECK(value == UINT64_MAX); | 99 | CHECK(value == UINT64_MAX); |
| 100 | 100 | ||
| 101 | test_case("a number too large for 64 bits is refused"); | 101 | test_case("a number too large for 64 bits is refused"); |
| 102 | CHECK_INT(scan_udec("18446744073709551616", &value, UINT64_MAX), 0); | 102 | CHECK_INT(scan_uint("18446744073709551616", &value, UINT64_MAX), 0); |
| 103 | CHECK_INT(scan_udec("99999999999999999999999999", &value, UINT64_MAX), 0); | 103 | CHECK_INT(scan_uint("99999999999999999999999999", &value, UINT64_MAX), 0); |
| 104 | } | 104 | } |
| 105 | 105 | ||
| 106 | static void case_leading_zeros_allowed(void) { | 106 | static void case_leading_zeros_allowed(void) { |
| 107 | uint64_t value; | 107 | uint64_t value; |
| 108 | 108 | ||
| 109 | test_case("leading zeros are accepted outside bencode"); | 109 | test_case("leading zeros are accepted outside bencode"); |
| 110 | CHECK_INT(scan_udec("007", &value, 65535), 3); | 110 | CHECK_INT(scan_uint("007", &value, 65535), 3); |
| 111 | CHECK_INT(value, 7); | 111 | CHECK_INT(value, 7); |
| 112 | CHECK_INT(scan_udec("0000", &value, 65535), 4); | 112 | CHECK_INT(scan_uint("0000", &value, 65535), 4); |
| 113 | CHECK_INT(value, 0); | 113 | CHECK_INT(value, 0); |
| 114 | } | 114 | } |
| 115 | 115 | ||
| @@ -117,9 +117,9 @@ static void case_bencode_rejects_leading_zeros(void) { | |||
| 117 | uint64_t value = 0xdead; | 117 | uint64_t value = 0xdead; |
| 118 | 118 | ||
| 119 | test_case("bencode refuses a leading zero on anything but zero"); | 119 | test_case("bencode refuses a leading zero on anything but zero"); |
| 120 | CHECK_INT(scan_udec_bencode("007", &value, 65535), 0); | 120 | CHECK_INT(scan_uint_bencode("007", &value, 65535), 0); |
| 121 | CHECK_INT(scan_udec_bencode("0000", &value, 65535), 0); | 121 | CHECK_INT(scan_uint_bencode("0000", &value, 65535), 0); |
| 122 | CHECK_INT(scan_udec_bencode("01", &value, 65535), 0); | 122 | CHECK_INT(scan_uint_bencode("01", &value, 65535), 0); |
| 123 | CHECK_INT(value, 0xdead); | 123 | CHECK_INT(value, 0xdead); |
| 124 | } | 124 | } |
| 125 | 125 | ||
| @@ -127,11 +127,11 @@ static void case_bencode_accepts_canonical(void) { | |||
| 127 | uint64_t value; | 127 | uint64_t value; |
| 128 | 128 | ||
| 129 | test_case("bencode accepts a canonically written number"); | 129 | test_case("bencode accepts a canonically written number"); |
| 130 | CHECK_INT(scan_udec_bencode("0", &value, 65535), 1); | 130 | CHECK_INT(scan_uint_bencode("0", &value, 65535), 1); |
| 131 | CHECK_INT(value, 0); | 131 | CHECK_INT(value, 0); |
| 132 | CHECK_INT(scan_udec_bencode("0:", &value, 65535), 1); | 132 | CHECK_INT(scan_uint_bencode("0:", &value, 65535), 1); |
| 133 | CHECK_INT(value, 0); | 133 | CHECK_INT(value, 0); |
| 134 | CHECK_INT(scan_udec_bencode("160:", &value, 65535), 3); | 134 | CHECK_INT(scan_uint_bencode("160:", &value, 65535), 3); |
| 135 | CHECK_INT(value, 160); | 135 | CHECK_INT(value, 160); |
| 136 | } | 136 | } |
| 137 | 137 | ||
| @@ -139,14 +139,64 @@ static void case_bencode_inherits_the_rest(void) { | |||
| 139 | uint64_t value = 0xdead; | 139 | uint64_t value = 0xdead; |
| 140 | 140 | ||
| 141 | test_case("bencode is no laxer than the plain scanner"); | 141 | test_case("bencode is no laxer than the plain scanner"); |
| 142 | CHECK_INT(scan_udec_bencode(" 1", &value, 65535), 0); | 142 | CHECK_INT(scan_uint_bencode(" 1", &value, 65535), 0); |
| 143 | CHECK_INT(scan_udec_bencode("-1", &value, 65535), 0); | 143 | CHECK_INT(scan_uint_bencode("-1", &value, 65535), 0); |
| 144 | CHECK_INT(scan_udec_bencode("+1", &value, 65535), 0); | 144 | CHECK_INT(scan_uint_bencode("+1", &value, 65535), 0); |
| 145 | CHECK_INT(scan_udec_bencode("65536", &value, 65535), 0); | 145 | CHECK_INT(scan_uint_bencode("65536", &value, 65535), 0); |
| 146 | CHECK_INT(value, 0xdead); | 146 | CHECK_INT(value, 0xdead); |
| 147 | } | 147 | } |
| 148 | 148 | ||
| 149 | /* ------------------------------------------- fixed width signed integers */ | ||
| 150 | |||
| 151 | /* A value lifted out of a query string arrives as a pointer and a length | ||
| 152 | with nothing terminating it, and the whole of it has to be the number. */ | ||
| 153 | |||
| 154 | static void case_int_whole_field(void) { | ||
| 155 | int64_t value = 0; | ||
| 156 | |||
| 157 | test_case("the whole field is read, sign and all"); | ||
| 158 | CHECK_INT(scan_int("123", 3, &value), 0); | ||
| 159 | CHECK_INT(value, 123); | ||
| 160 | CHECK_INT(scan_int("-45", 3, &value), 0); | ||
| 161 | CHECK_INT(value, -45); | ||
| 162 | |||
| 163 | test_case("and only as far as the length given"); | ||
| 164 | CHECK_INT(scan_int("12345", 3, &value), 0); | ||
| 165 | CHECK_INT(value, 123); | ||
| 166 | } | ||
| 167 | |||
| 168 | static void case_int_rejects_the_rest(void) { | ||
| 169 | int64_t value = 4711; | ||
| 170 | |||
| 171 | test_case("anything that is not all digits is refused"); | ||
| 172 | CHECK_INT(scan_int("12a", 3, &value), -1); | ||
| 173 | CHECK_INT(scan_int("", 0, &value), -1); | ||
| 174 | CHECK_INT(scan_int("-", 1, &value), -1); | ||
| 175 | CHECK_INT(scan_int(" 12", 3, &value), -1); | ||
| 176 | CHECK_INT(scan_int("+12", 3, &value), -1); | ||
| 177 | |||
| 178 | test_case("and a refused field leaves the number alone"); | ||
| 179 | CHECK_INT(value, 4711); | ||
| 180 | } | ||
| 181 | |||
| 182 | static void case_int_overflow(void) { | ||
| 183 | int64_t value = 0; | ||
| 184 | |||
| 185 | test_case("a field too long to fit is refused rather than wrapped"); | ||
| 186 | CHECK_INT(scan_int("9223372036854775807", 19, &value), 0); | ||
| 187 | CHECK_INT(value, INT64_MAX); | ||
| 188 | CHECK_INT(scan_int("9223372036854775808", 19, &value), -1); | ||
| 189 | CHECK_INT(scan_int("99999999999999999999", 20, &value), -1); | ||
| 190 | |||
| 191 | test_case("including the negative side"); | ||
| 192 | CHECK_INT(scan_int("-9223372036854775807", 20, &value), 0); | ||
| 193 | CHECK_INT(value, -INT64_MAX); | ||
| 194 | } | ||
| 195 | |||
| 149 | void test_scan(void) { | 196 | void test_scan(void) { |
| 197 | case_int_whole_field(); | ||
| 198 | case_int_rejects_the_rest(); | ||
| 199 | case_int_overflow(); | ||
| 150 | case_plain_numbers(); | 200 | case_plain_numbers(); |
| 151 | case_stops_at_non_digit(); | 201 | case_stops_at_non_digit(); |
| 152 | case_rejects_whitespace(); | 202 | case_rejects_whitespace(); |
diff --git a/tests/t_scan_urlencoded_query.c b/tests/t_urlencoded.c index f660ea3..6e0ee31 100644 --- a/tests/t_scan_urlencoded_query.c +++ b/tests/t_urlencoded.c | |||
| @@ -7,7 +7,7 @@ | |||
| 7 | #include <string.h> | 7 | #include <string.h> |
| 8 | 8 | ||
| 9 | /* Opentracker */ | 9 | /* Opentracker */ |
| 10 | #include "scan_urlencoded_query.h" | 10 | #include "ot_urlencoded.h" |
| 11 | #include "test.h" | 11 | #include "test.h" |
| 12 | 12 | ||
| 13 | /* The scanner returns the number of bytes written to the destination, or | 13 | /* The scanner returns the number of bytes written to the destination, or |
| @@ -23,7 +23,7 @@ static void case_path(void) { | |||
| 23 | ssize_t n; | 23 | ssize_t n; |
| 24 | 24 | ||
| 25 | test_case("path stops at the question mark"); | 25 | test_case("path stops at the question mark"); |
| 26 | n = scan_urlencoded_query(&p, dest, SCAN_PATH); | 26 | n = urlencoded_parse(&p, dest, SCAN_PATH); |
| 27 | CHECK_INT(n, 9); | 27 | CHECK_INT(n, 9); |
| 28 | dest[n] = 0; | 28 | dest[n] = 0; |
| 29 | CHECK_STR(dest, "/announce"); | 29 | CHECK_STR(dest, "/announce"); |
| @@ -37,22 +37,22 @@ static void case_param_and_value(void) { | |||
| 37 | ssize_t n; | 37 | ssize_t n; |
| 38 | 38 | ||
| 39 | test_case("parameter stops at '=', value stops at '&'"); | 39 | test_case("parameter stops at '=', value stops at '&'"); |
| 40 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | 40 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); |
| 41 | CHECK_INT(n, 9); | 41 | CHECK_INT(n, 9); |
| 42 | dest[n] = 0; | 42 | dest[n] = 0; |
| 43 | CHECK_STR(dest, "info_hash"); | 43 | CHECK_STR(dest, "info_hash"); |
| 44 | 44 | ||
| 45 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 45 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 46 | CHECK_INT(n, 3); | 46 | CHECK_INT(n, 3); |
| 47 | dest[n] = 0; | 47 | dest[n] = 0; |
| 48 | CHECK_STR(dest, "abc"); | 48 | CHECK_STR(dest, "abc"); |
| 49 | 49 | ||
| 50 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | 50 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); |
| 51 | CHECK_INT(n, 4); | 51 | CHECK_INT(n, 4); |
| 52 | dest[n] = 0; | 52 | dest[n] = 0; |
| 53 | CHECK_STR(dest, "port"); | 53 | CHECK_STR(dest, "port"); |
| 54 | 54 | ||
| 55 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 55 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 56 | CHECK_INT(n, 2); | 56 | CHECK_INT(n, 2); |
| 57 | dest[n] = 0; | 57 | dest[n] = 0; |
| 58 | CHECK_STR(dest, "80"); | 58 | CHECK_STR(dest, "80"); |
| @@ -65,7 +65,7 @@ static void case_percent_decoding(void) { | |||
| 65 | ssize_t n; | 65 | ssize_t n; |
| 66 | 66 | ||
| 67 | test_case("percent escapes decode"); | 67 | test_case("percent escapes decode"); |
| 68 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 68 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 69 | CHECK_INT(n, 3); | 69 | CHECK_INT(n, 3); |
| 70 | CHECK_MEM(dest, "ABC", 3); | 70 | CHECK_MEM(dest, "ABC", 3); |
| 71 | } | 71 | } |
| @@ -79,7 +79,7 @@ static void case_binary_info_hash(void) { | |||
| 79 | ssize_t n; | 79 | ssize_t n; |
| 80 | 80 | ||
| 81 | test_case("twenty byte binary value survives decoding"); | 81 | test_case("twenty byte binary value survives decoding"); |
| 82 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 82 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 83 | CHECK_INT(n, 20); | 83 | CHECK_INT(n, 20); |
| 84 | CHECK_MEM(dest, want, 20); | 84 | CHECK_MEM(dest, want, 20); |
| 85 | } | 85 | } |
| @@ -90,7 +90,7 @@ static void case_in_place(void) { | |||
| 90 | ssize_t n; | 90 | ssize_t n; |
| 91 | 91 | ||
| 92 | test_case("decoding in place over the source is safe"); | 92 | test_case("decoding in place over the source is safe"); |
| 93 | n = scan_urlencoded_query(&p, query, SCAN_SEARCHPATH_VALUE); | 93 | n = urlencoded_parse(&p, query, SCAN_SEARCHPATH_VALUE); |
| 94 | CHECK_INT(n, 2); | 94 | CHECK_INT(n, 2); |
| 95 | CHECK_MEM(query, "AB", 2); | 95 | CHECK_MEM(query, "AB", 2); |
| 96 | } | 96 | } |
| @@ -102,7 +102,7 @@ static void case_bad_escape(void) { | |||
| 102 | ssize_t n; | 102 | ssize_t n; |
| 103 | 103 | ||
| 104 | test_case("a non hexadecimal escape is a parse error"); | 104 | test_case("a non hexadecimal escape is a parse error"); |
| 105 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 105 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 106 | CHECK_INT(n, -1); | 106 | CHECK_INT(n, -1); |
| 107 | } | 107 | } |
| 108 | 108 | ||
| @@ -113,7 +113,7 @@ static void case_truncated_escape(void) { | |||
| 113 | ssize_t n; | 113 | ssize_t n; |
| 114 | 114 | ||
| 115 | test_case("an escape cut short by the terminator is a parse error"); | 115 | test_case("an escape cut short by the terminator is a parse error"); |
| 116 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 116 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 117 | CHECK_INT(n, -1); | 117 | CHECK_INT(n, -1); |
| 118 | } | 118 | } |
| 119 | 119 | ||
| @@ -125,11 +125,11 @@ static void case_terminator(void) { | |||
| 125 | ssize_t n; | 125 | ssize_t n; |
| 126 | 126 | ||
| 127 | test_case("a hard terminator before any output reports end of input"); | 127 | test_case("a hard terminator before any output reports end of input"); |
| 128 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | 128 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); |
| 129 | CHECK_INT(n, -2); | 129 | CHECK_INT(n, -2); |
| 130 | 130 | ||
| 131 | p = space; | 131 | p = space; |
| 132 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | 132 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); |
| 133 | CHECK_INT(n, -2); | 133 | CHECK_INT(n, -2); |
| 134 | } | 134 | } |
| 135 | 135 | ||
| @@ -140,10 +140,10 @@ static void case_terminator_after_output(void) { | |||
| 140 | ssize_t n; | 140 | ssize_t n; |
| 141 | 141 | ||
| 142 | test_case("a hard terminator after output is left for the next call"); | 142 | test_case("a hard terminator after output is left for the next call"); |
| 143 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 143 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 144 | CHECK_INT(n, 3); | 144 | CHECK_INT(n, 3); |
| 145 | CHECK_INT(*p, 0); | 145 | CHECK_INT(*p, 0); |
| 146 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 146 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 147 | CHECK_INT(n, -2); | 147 | CHECK_INT(n, -2); |
| 148 | } | 148 | } |
| 149 | 149 | ||
| @@ -159,14 +159,14 @@ static void case_separators_are_contextual(void) { | |||
| 159 | ssize_t n; | 159 | ssize_t n; |
| 160 | 160 | ||
| 161 | test_case("a question mark is literal inside a value"); | 161 | test_case("a question mark is literal inside a value"); |
| 162 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 162 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 163 | CHECK_INT(n, 3); | 163 | CHECK_INT(n, 3); |
| 164 | dest[n] = 0; | 164 | dest[n] = 0; |
| 165 | CHECK_STR(dest, "a?b"); | 165 | CHECK_STR(dest, "a?b"); |
| 166 | 166 | ||
| 167 | test_case("an equals sign is literal inside a value"); | 167 | test_case("an equals sign is literal inside a value"); |
| 168 | p = equals; | 168 | p = equals; |
| 169 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 169 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 170 | CHECK_INT(n, 3); | 170 | CHECK_INT(n, 3); |
| 171 | dest[n] = 0; | 171 | dest[n] = 0; |
| 172 | CHECK_STR(dest, "a=b"); | 172 | CHECK_STR(dest, "a=b"); |
| @@ -179,7 +179,7 @@ static void case_ampersand_in_path(void) { | |||
| 179 | ssize_t n; | 179 | ssize_t n; |
| 180 | 180 | ||
| 181 | test_case("an ampersand in a path is a parse error"); | 181 | test_case("an ampersand in a path is a parse error"); |
| 182 | n = scan_urlencoded_query(&p, dest, SCAN_PATH); | 182 | n = urlencoded_parse(&p, dest, SCAN_PATH); |
| 183 | CHECK_INT(n, -1); | 183 | CHECK_INT(n, -1); |
| 184 | } | 184 | } |
| 185 | 185 | ||
| @@ -190,7 +190,7 @@ static void case_raw_high_byte(void) { | |||
| 190 | ssize_t n; | 190 | ssize_t n; |
| 191 | 191 | ||
| 192 | test_case("an undecoded byte above ASCII is a parse error"); | 192 | test_case("an undecoded byte above ASCII is a parse error"); |
| 193 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | 193 | n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); |
| 194 | CHECK_INT(n, -1); | 194 | CHECK_INT(n, -1); |
| 195 | } | 195 | } |
| 196 | 196 | ||
| @@ -200,12 +200,12 @@ static void case_skipvalue(void) { | |||
| 200 | char *p = query; | 200 | char *p = query; |
| 201 | 201 | ||
| 202 | test_case("skipvalue steps over the value and its separator"); | 202 | test_case("skipvalue steps over the value and its separator"); |
| 203 | scan_urlencoded_skipvalue(&p); | 203 | urlencoded_skip_value(&p); |
| 204 | CHECK_STR(p, "next=1"); | 204 | CHECK_STR(p, "next=1"); |
| 205 | 205 | ||
| 206 | test_case("skipvalue leaves a hard terminator in place"); | 206 | test_case("skipvalue leaves a hard terminator in place"); |
| 207 | p = bare; | 207 | p = bare; |
| 208 | scan_urlencoded_skipvalue(&p); | 208 | urlencoded_skip_value(&p); |
| 209 | CHECK_INT(*p, 0); | 209 | CHECK_INT(*p, 0); |
| 210 | } | 210 | } |
| 211 | 211 | ||
| @@ -217,44 +217,17 @@ static void case_find_keywords(void) { | |||
| 217 | char *p = match; | 217 | char *p = match; |
| 218 | 218 | ||
| 219 | test_case("a known keyword returns its value"); | 219 | test_case("a known keyword returns its value"); |
| 220 | CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), 2); | 220 | CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), 2); |
| 221 | 221 | ||
| 222 | test_case("an unknown keyword returns -3"); | 222 | test_case("an unknown keyword returns -3"); |
| 223 | p = nomatch; | 223 | p = nomatch; |
| 224 | CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); | 224 | CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); |
| 225 | 225 | ||
| 226 | test_case("a prefix of a keyword does not match it"); | 226 | test_case("a prefix of a keyword does not match it"); |
| 227 | p = prefix; | 227 | p = prefix; |
| 228 | CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); | 228 | CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); |
| 229 | } | 229 | } |
| 230 | 230 | ||
| 231 | static void case_fixed_int(void) { | ||
| 232 | char positive[] = "123"; | ||
| 233 | char negative[] = "-45"; | ||
| 234 | char trailing[] = "12a"; | ||
| 235 | int value; | ||
| 236 | |||
| 237 | test_case("scan_fixed_int returns the count of unparsed bytes"); | ||
| 238 | CHECK_INT(scan_fixed_int(positive, 3, &value), 0); | ||
| 239 | CHECK_INT(value, 123); | ||
| 240 | |||
| 241 | CHECK_INT(scan_fixed_int(negative, 3, &value), 0); | ||
| 242 | CHECK_INT(value, -45); | ||
| 243 | |||
| 244 | CHECK_INT(scan_fixed_int(trailing, 3, &value), 1); | ||
| 245 | CHECK_INT(value, 12); | ||
| 246 | } | ||
| 247 | |||
| 248 | static void case_fixed_int_bounded(void) { | ||
| 249 | char digits[] = "12345"; | ||
| 250 | int value; | ||
| 251 | |||
| 252 | test_case("scan_fixed_int honours the length it is given"); | ||
| 253 | CHECK_INT(scan_fixed_int(digits, 3, &value), 0); | ||
| 254 | CHECK_INT(value, 123); | ||
| 255 | } | ||
| 256 | |||
| 257 | |||
| 258 | /* The encoding direction. Which bytes survive unescaped is fixed by what | 231 | /* The encoding direction. Which bytes survive unescaped is fixed by what |
| 259 | existing parsers of the urlencoded full scrape expect, so these cases pin | 232 | existing parsers of the urlencoded full scrape expect, so these cases pin |
| 260 | the set rather than describe a preference. */ | 233 | the set rather than describe a preference. */ |
| @@ -265,7 +238,7 @@ static void case_fmt_passes_safe_bytes(void) { | |||
| 265 | size_t n; | 238 | size_t n; |
| 266 | 239 | ||
| 267 | test_case("alphanumerics and the safe punctuation pass through"); | 240 | test_case("alphanumerics and the safe punctuation pass through"); |
| 268 | n = fmt_urlencoded(dest, src, 11); | 241 | n = urlencoded_format(dest, src, 11); |
| 269 | dest[n] = 0; | 242 | dest[n] = 0; |
| 270 | CHECK_INT(n, 11); | 243 | CHECK_INT(n, 11); |
| 271 | CHECK_STR(dest, "aZ09$-./=_~"); | 244 | CHECK_STR(dest, "aZ09$-./=_~"); |
| @@ -277,7 +250,7 @@ static void case_fmt_escapes_the_rest(void) { | |||
| 277 | size_t n; | 250 | size_t n; |
| 278 | 251 | ||
| 279 | test_case("everything else becomes a percent escape"); | 252 | test_case("everything else becomes a percent escape"); |
| 280 | n = fmt_urlencoded(dest, src, sizeof(src)); | 253 | n = urlencoded_format(dest, src, sizeof(src)); |
| 281 | dest[n] = 0; | 254 | dest[n] = 0; |
| 282 | CHECK_INT(n, 18); | 255 | CHECK_INT(n, 18); |
| 283 | CHECK_STR(dest, "%20%25%26%2b%3a%3f"); | 256 | CHECK_STR(dest, "%20%25%26%2b%3a%3f"); |
| @@ -289,7 +262,7 @@ static void case_fmt_hex_is_lower_case(void) { | |||
| 289 | size_t n; | 262 | size_t n; |
| 290 | 263 | ||
| 291 | test_case("escapes use lower case hex digits"); | 264 | test_case("escapes use lower case hex digits"); |
| 292 | n = fmt_urlencoded(dest, src, sizeof(src)); | 265 | n = urlencoded_format(dest, src, sizeof(src)); |
| 293 | dest[n] = 0; | 266 | dest[n] = 0; |
| 294 | CHECK_INT(n, 9); | 267 | CHECK_INT(n, 9); |
| 295 | CHECK_STR(dest, "%00%ab%ff"); | 268 | CHECK_STR(dest, "%00%ab%ff"); |
| @@ -300,7 +273,7 @@ static void case_fmt_zero_length(void) { | |||
| 300 | 273 | ||
| 301 | test_case("a zero length input writes nothing"); | 274 | test_case("a zero length input writes nothing"); |
| 302 | memset(dest, 0x5a, sizeof(dest)); | 275 | memset(dest, 0x5a, sizeof(dest)); |
| 303 | CHECK_INT(fmt_urlencoded(dest, (const uint8_t *)"abc", 0), 0); | 276 | CHECK_INT(urlencoded_format(dest, (const uint8_t *)"abc", 0), 0); |
| 304 | CHECK_MEM(dest, "\x5a\x5a\x5a\x5a", 4); | 277 | CHECK_MEM(dest, "\x5a\x5a\x5a\x5a", 4); |
| 305 | } | 278 | } |
| 306 | 279 | ||
| @@ -312,7 +285,7 @@ static void case_fmt_worst_case_fits(void) { | |||
| 312 | test_case("a hash of entirely unsafe bytes needs three times its length"); | 285 | test_case("a hash of entirely unsafe bytes needs three times its length"); |
| 313 | for (i = 0; i < sizeof(src); ++i) | 286 | for (i = 0; i < sizeof(src); ++i) |
| 314 | src[i] = 0xff; | 287 | src[i] = 0xff; |
| 315 | n = fmt_urlencoded(dest, src, sizeof(src)); | 288 | n = urlencoded_format(dest, src, sizeof(src)); |
| 316 | dest[n] = 0; | 289 | dest[n] = 0; |
| 317 | CHECK_INT(n, 60); | 290 | CHECK_INT(n, 60); |
| 318 | for (i = 0; i < 20; ++i) | 291 | for (i = 0; i < 20; ++i) |
| @@ -328,7 +301,7 @@ static void case_fmt_decoder_disagrees_on_dollar(void) { | |||
| 328 | encoder passes through unescaped, '$' is the only one the decoder then | 301 | encoder passes through unescaped, '$' is the only one the decoder then |
| 329 | refuses, and it refuses the whole string rather than stopping short. | 302 | refuses, and it refuses the whole string rather than stopping short. |
| 330 | Output of the urlencoded full scrape therefore does not round trip | 303 | Output of the urlencoded full scrape therefore does not round trip |
| 331 | through scan_urlencoded_query when a hash contains 0x24. Harmless today | 304 | through urlencoded_parse when a hash contains 0x24. Harmless today |
| 332 | because only third party tooling reads that output. */ | 305 | because only third party tooling reads that output. */ |
| 333 | const uint8_t src[] = {'a', '$', 'b'}; | 306 | const uint8_t src[] = {'a', '$', 'b'}; |
| 334 | char dest[16]; | 307 | char dest[16]; |
| @@ -338,15 +311,15 @@ static void case_fmt_decoder_disagrees_on_dollar(void) { | |||
| 338 | ssize_t got; | 311 | ssize_t got; |
| 339 | 312 | ||
| 340 | test_case("the encoder passes '$' but the decoder rejects it"); | 313 | test_case("the encoder passes '$' but the decoder rejects it"); |
| 341 | n = fmt_urlencoded(dest, src, sizeof(src)); | 314 | n = urlencoded_format(dest, src, sizeof(src)); |
| 342 | dest[n] = 0; | 315 | dest[n] = 0; |
| 343 | CHECK_STR(dest, "a$b"); | 316 | CHECK_STR(dest, "a$b"); |
| 344 | 317 | ||
| 345 | got = scan_urlencoded_query(&p, scanned, SCAN_SEARCHPATH_VALUE); | 318 | got = urlencoded_parse(&p, scanned, SCAN_SEARCHPATH_VALUE); |
| 346 | CHECK_INT(got, -1); | 319 | CHECK_INT(got, -1); |
| 347 | } | 320 | } |
| 348 | 321 | ||
| 349 | void test_scan_urlencoded_query(void) { | 322 | void test_urlencoded(void) { |
| 350 | case_path(); | 323 | case_path(); |
| 351 | case_param_and_value(); | 324 | case_param_and_value(); |
| 352 | case_percent_decoding(); | 325 | case_percent_decoding(); |
| @@ -361,8 +334,6 @@ void test_scan_urlencoded_query(void) { | |||
| 361 | case_raw_high_byte(); | 334 | case_raw_high_byte(); |
| 362 | case_skipvalue(); | 335 | case_skipvalue(); |
| 363 | case_find_keywords(); | 336 | case_find_keywords(); |
| 364 | case_fixed_int(); | ||
| 365 | case_fixed_int_bounded(); | ||
| 366 | case_fmt_passes_safe_bytes(); | 337 | case_fmt_passes_safe_bytes(); |
| 367 | case_fmt_escapes_the_rest(); | 338 | case_fmt_escapes_the_rest(); |
| 368 | case_fmt_hex_is_lower_case(); | 339 | case_fmt_hex_is_lower_case(); |
diff --git a/tests/t_vector.c b/tests/t_vector.c index a8186b2..6e048e1 100644 --- a/tests/t_vector.c +++ b/tests/t_vector.c | |||
| @@ -13,7 +13,7 @@ | |||
| 13 | #include "trackerlogic.h" | 13 | #include "trackerlogic.h" |
| 14 | #include "test.h" | 14 | #include "test.h" |
| 15 | 15 | ||
| 16 | /* binary_search returns the matching member when there is one, and otherwise | 16 | /* vector_binary_search returns the matching member when there is one, and otherwise |
| 17 | the position at which the key would have to be inserted to keep the vector | 17 | the position at which the key would have to be inserted to keep the vector |
| 18 | sorted. exactmatch distinguishes the two. */ | 18 | sorted. exactmatch distinguishes the two. */ |
| 19 | 19 | ||
| @@ -25,7 +25,7 @@ static void case_search_finds_member(void) { | |||
| 25 | void *found; | 25 | void *found; |
| 26 | 26 | ||
| 27 | test_case("an existing key is found"); | 27 | test_case("an existing key is found"); |
| 28 | found = binary_search(key, sorted, 5, 4, 4, &exactmatch); | 28 | found = vector_binary_search(key, sorted, 5, 4, 4, &exactmatch); |
| 29 | CHECK(exactmatch != 0); | 29 | CHECK(exactmatch != 0); |
| 30 | CHECK_PTR(found, sorted[2]); | 30 | CHECK_PTR(found, sorted[2]); |
| 31 | } | 31 | } |
| @@ -36,9 +36,9 @@ static void case_search_finds_first_and_last(void) { | |||
| 36 | int exactmatch; | 36 | int exactmatch; |
| 37 | 37 | ||
| 38 | test_case("the first and last members are found"); | 38 | test_case("the first and last members are found"); |
| 39 | CHECK_PTR(binary_search(first, sorted, 5, 4, 4, &exactmatch), sorted[0]); | 39 | CHECK_PTR(vector_binary_search(first, sorted, 5, 4, 4, &exactmatch), sorted[0]); |
| 40 | CHECK(exactmatch != 0); | 40 | CHECK(exactmatch != 0); |
| 41 | CHECK_PTR(binary_search(last, sorted, 5, 4, 4, &exactmatch), sorted[4]); | 41 | CHECK_PTR(vector_binary_search(last, sorted, 5, 4, 4, &exactmatch), sorted[4]); |
| 42 | CHECK(exactmatch != 0); | 42 | CHECK(exactmatch != 0); |
| 43 | } | 43 | } |
| 44 | 44 | ||
| @@ -49,11 +49,11 @@ static void case_search_insert_position(void) { | |||
| 49 | void *slot; | 49 | void *slot; |
| 50 | 50 | ||
| 51 | test_case("a missing key yields its insert position"); | 51 | test_case("a missing key yields its insert position"); |
| 52 | slot = binary_search(middle, sorted, 5, 4, 4, &exactmatch); | 52 | slot = vector_binary_search(middle, sorted, 5, 4, 4, &exactmatch); |
| 53 | CHECK_INT(exactmatch, 0); | 53 | CHECK_INT(exactmatch, 0); |
| 54 | CHECK_PTR(slot, sorted[2]); | 54 | CHECK_PTR(slot, sorted[2]); |
| 55 | 55 | ||
| 56 | slot = binary_search(before, sorted, 5, 4, 4, &exactmatch); | 56 | slot = vector_binary_search(before, sorted, 5, 4, 4, &exactmatch); |
| 57 | CHECK_INT(exactmatch, 0); | 57 | CHECK_INT(exactmatch, 0); |
| 58 | CHECK_PTR(slot, sorted[0]); | 58 | CHECK_PTR(slot, sorted[0]); |
| 59 | } | 59 | } |
| @@ -64,7 +64,7 @@ static void case_search_past_the_end(void) { | |||
| 64 | void *slot; | 64 | void *slot; |
| 65 | 65 | ||
| 66 | test_case("a key above every member yields the position past the end"); | 66 | test_case("a key above every member yields the position past the end"); |
| 67 | slot = binary_search(beyond, sorted, 5, 4, 4, &exactmatch); | 67 | slot = vector_binary_search(beyond, sorted, 5, 4, 4, &exactmatch); |
| 68 | CHECK_INT(exactmatch, 0); | 68 | CHECK_INT(exactmatch, 0); |
| 69 | CHECK_PTR(slot, sorted[5]); | 69 | CHECK_PTR(slot, sorted[5]); |
| 70 | } | 70 | } |
| @@ -74,7 +74,7 @@ static void case_search_empty(void) { | |||
| 74 | void *base = (void *)sorted; | 74 | void *base = (void *)sorted; |
| 75 | 75 | ||
| 76 | test_case("searching an empty vector reports no match at the base"); | 76 | test_case("searching an empty vector reports no match at the base"); |
| 77 | CHECK_PTR(binary_search(sorted[0], base, 0, 4, 4, &exactmatch), base); | 77 | CHECK_PTR(vector_binary_search(sorted[0], base, 0, 4, 4, &exactmatch), base); |
| 78 | CHECK_INT(exactmatch, 0); | 78 | CHECK_INT(exactmatch, 0); |
| 79 | } | 79 | } |
| 80 | 80 | ||
| @@ -112,6 +112,51 @@ static void case_insert_keeps_order(void) { | |||
| 112 | free(vector.data); | 112 | free(vector.data); |
| 113 | } | 113 | } |
| 114 | 114 | ||
| 115 | /* The peer variant is reached with an empty vector on every first announce | ||
| 116 | for a torrent, and again whenever a swarm empties and refills, because | ||
| 117 | vector_fixup_peers drops the allocation and nulls the data pointer. */ | ||
| 118 | |||
| 119 | static void case_insert_peer_into_empty_vector(void) { | ||
| 120 | ot_vector vector = {NULL, 0, 0}; | ||
| 121 | ot_peer6 low, high; | ||
| 122 | int exactmatch; | ||
| 123 | ot_peer *slot; | ||
| 124 | |||
| 125 | memset(low, 0x11, sizeof(low)); | ||
| 126 | memset(high, 0x99, sizeof(high)); | ||
| 127 | |||
| 128 | test_case("the first peer goes into a vector that has no allocation yet"); | ||
| 129 | slot = vector_find_or_insert_peer(&vector, low, OT_PEER_SIZE6, &exactmatch); | ||
| 130 | CHECK(slot != NULL); | ||
| 131 | CHECK_INT(exactmatch, 0); | ||
| 132 | memcpy(slot, low, OT_PEER_SIZE6); | ||
| 133 | CHECK_INT(vector.size, 1); | ||
| 134 | CHECK_INT(vector.space, OT_VECTOR_MIN_MEMBERS); | ||
| 135 | CHECK_PTR(slot, vector.data); | ||
| 136 | |||
| 137 | test_case("a second peer keeps the order"); | ||
| 138 | slot = vector_find_or_insert_peer(&vector, high, OT_PEER_SIZE6, &exactmatch); | ||
| 139 | CHECK(slot != NULL); | ||
| 140 | CHECK_INT(exactmatch, 0); | ||
| 141 | memcpy(slot, high, OT_PEER_SIZE6); | ||
| 142 | CHECK_INT(vector.size, 2); | ||
| 143 | CHECK_MEM((char *)vector.data, low, OT_PEER_COMPARE_SIZE6); | ||
| 144 | CHECK_MEM((char *)vector.data + OT_PEER_SIZE6, high, OT_PEER_COMPARE_SIZE6); | ||
| 145 | |||
| 146 | test_case("and emptying the vector hands the allocation back"); | ||
| 147 | CHECK(vector_remove_peer(&vector, low, OT_PEER_SIZE6) != 0); | ||
| 148 | CHECK(vector_remove_peer(&vector, high, OT_PEER_SIZE6) != 0); | ||
| 149 | CHECK_INT(vector.size, 0); | ||
| 150 | CHECK_PTR(vector.data, NULL); | ||
| 151 | |||
| 152 | test_case("so the next peer starts from no allocation again"); | ||
| 153 | slot = vector_find_or_insert_peer(&vector, low, OT_PEER_SIZE6, &exactmatch); | ||
| 154 | CHECK(slot != NULL); | ||
| 155 | CHECK_INT(vector.size, 1); | ||
| 156 | |||
| 157 | free(vector.data); | ||
| 158 | } | ||
| 159 | |||
| 115 | static void case_insert_finds_existing(void) { | 160 | static void case_insert_finds_existing(void) { |
| 116 | ot_vector vector = {NULL, 0, 0}; | 161 | ot_vector vector = {NULL, 0, 0}; |
| 117 | const unsigned char five[4] = {0, 0, 0, 5}; | 162 | const unsigned char five[4] = {0, 0, 0, 5}; |
| @@ -257,6 +302,7 @@ void test_vector(void) { | |||
| 257 | case_search_past_the_end(); | 302 | case_search_past_the_end(); |
| 258 | case_search_empty(); | 303 | case_search_empty(); |
| 259 | case_insert_keeps_order(); | 304 | case_insert_keeps_order(); |
| 305 | case_insert_peer_into_empty_vector(); | ||
| 260 | case_insert_finds_existing(); | 306 | case_insert_finds_existing(); |
| 261 | case_insert_compare_prefix(); | 307 | case_insert_compare_prefix(); |
| 262 | case_redistribute_holds_every_peer(); | 308 | case_redistribute_holds_every_peer(); |
diff --git a/tests/test.h b/tests/test.h index fd574c2..0eddc82 100644 --- a/tests/test.h +++ b/tests/test.h | |||
| @@ -76,7 +76,7 @@ extern unsigned long test_failures; | |||
| 76 | } while (0) | 76 | } while (0) |
| 77 | 77 | ||
| 78 | /* One entry point per module under test */ | 78 | /* One entry point per module under test */ |
| 79 | void test_scan_urlencoded_query(void); | 79 | void test_urlencoded(void); |
| 80 | void test_iovec(void); | 80 | void test_iovec(void); |
| 81 | void test_vector(void); | 81 | void test_vector(void); |
| 82 | void test_random(void); | 82 | void test_random(void); |
| @@ -88,5 +88,8 @@ void test_ip(void); | |||
| 88 | void test_socket(void); | 88 | void test_socket(void); |
| 89 | void test_mutex(void); | 89 | void test_mutex(void); |
| 90 | void test_batch(void); | 90 | void test_batch(void); |
| 91 | void test_fdstate(void); | ||
| 92 | void test_event(void); | ||
| 93 | void test_fmt(void); | ||
| 91 | 94 | ||
| 92 | #endif | 95 | #endif |
