summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/integration/.gitignore2
-rw-r--r--tests/integration/harness.py230
-rw-r--r--tests/integration/test_accesslist.py102
-rw-r--r--tests/integration/test_announce.py129
-rw-r--r--tests/integration/test_dualstack.py89
-rw-r--r--tests/integration/test_fullscrape.py114
-rw-r--r--tests/integration/test_stats.py170
-rw-r--r--tests/integration/test_udp.py153
-rw-r--r--tests/runner.c77
-rw-r--r--tests/t_event.c222
-rw-r--r--tests/t_fdstate.c224
-rw-r--r--tests/t_fmt.c108
-rw-r--r--tests/t_hex.c28
-rw-r--r--tests/t_iovec.c20
-rw-r--r--tests/t_ip.c54
-rw-r--r--tests/t_scan.c124
-rw-r--r--tests/t_urlencoded.c (renamed from tests/t_scan_urlencoded_query.c)95
-rw-r--r--tests/t_vector.c62
-rw-r--r--tests/test.h5
19 files changed, 1846 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..7e75574
--- /dev/null
+++ b/tests/integration/harness.py
@@ -0,0 +1,230 @@
1"""Starts a real opentracker and talks to it over the wire.
2
3Everything here drives the binary rather than the code, so these tests cover
4what the unit suite cannot reach: the event loop, the HTTP parser and the
5torrent store working together. Run against a sanitizer build and the tracker
6reports its own memory errors, which the teardown turns into test failures.
7"""
8
9import os
10import shutil
11import signal
12import socket
13import struct
14import subprocess
15import sys
16import tempfile
17import time
18import unittest
19import urllib.error
20import urllib.parse
21import urllib.request
22
23TRACKER = os.environ.get("OPENTRACKER", os.path.join(os.path.dirname(__file__), "..", "..", "opentracker.debug"))
24
25
26def 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
52def 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
60class 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 udp_raw(self, payload, timeout=1.0):
201 """Sends a packet exactly as given. Returns the answer, or None if the
202 tracker chose to say nothing, which is how it treats a broken one."""
203 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
204 sock.settimeout(timeout)
205 try:
206 sock.sendto(payload, ("127.0.0.1", self.port))
207 try:
208 return sock.recv(2048)
209 except socket.timeout:
210 return None
211 finally:
212 sock.close()
213
214
215class TrackerTestCase(unittest.TestCase):
216 """Gives every test its own tracker and fails it on any sanitizer output."""
217
218 config = ""
219 files = None
220
221 def setUp(self):
222 self.tracker = Tracker(self.config, self.files)
223
224 def tearDown(self):
225 complaints = self.tracker.stop()
226 self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints))
227
228 def hash_for(self, name):
229 """A distinct info hash per test, so tests cannot leak into each other."""
230 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..b4e134e
--- /dev/null
+++ b/tests/integration/test_accesslist.py
@@ -0,0 +1,102 @@
1"""The blacklist, loaded from a file at startup.
2
3A blacklisted info hash must be refused at both the announce and the scrape
4entry, and the file parser has to cope with the shapes an operator actually
5produces: an empty file, and a last line with no newline after it.
6"""
7
8import struct
9import unittest
10
11from harness import TrackerTestCase
12
13BLOCKED = b"\xb1" * 20
14ALLOWED = b"\xa1" * 20
15
16
17def listing(*hashes):
18 return b"".join(h.hex().upper().encode() + b"\n" for h in hashes)
19
20
21class 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_holds_over_udp_but_the_answer_is_malformed(self):
41 """The UDP path has no spelling for "not authorized": the announce
42 writes no body, so the tracker sends the eight byte header alone
43 where BEP 15 calls for at least twenty. Pinned rather than blessed —
44 an error packet or silence would both be defensible, a short announce
45 is neither."""
46 sock, connection_id = self.tracker.udp_connect()
47 sock.close()
48 request = struct.pack(
49 ">QII20s20sQQQIIIiH", connection_id, 1, 7, BLOCKED, b"p" * 20, 0, 0, 0, 0, 0, 0, -1, 6881
50 )
51 answer = self.tracker.udp_raw(request)
52 self.assertIsNotNone(answer)
53 self.assertEqual(len(answer), 8, "header only, no interval or counts")
54 action, transaction = struct.unpack(">II", answer)
55 self.assertEqual(action, 1, "it still claims to be an announce")
56 self.assertEqual(transaction, 7)
57
58 def test_a_blocked_hash_is_not_stored_when_it_arrives_over_udp(self):
59 sock, connection_id = self.tracker.udp_connect()
60 sock.close()
61 self.tracker.udp_raw(
62 struct.pack(">QII20s20sQQQIIIiH", connection_id, 1, 8, BLOCKED, b"p" * 20, 0, 0, 0, 0, 0, 0, -1, 6881)
63 )
64 self.assertEqual(self.tracker.torrent_count(), 0)
65
66
67class BlacklistWithoutTrailingNewline(TrackerTestCase):
68 """A single hash with no newline after it used to size the list at zero
69 and then write into it, which took the tracker down before it listened."""
70
71 config = "access.blacklist {dir}/blacklist\n"
72 files = {"blacklist": BLOCKED.hex().upper().encode()}
73
74 def test_the_last_line_counts_even_without_its_newline(self):
75 answer = self.tracker.announce(BLOCKED, port=6881, left=0)
76 self.assertIn(b"failure reason", answer)
77
78 def test_and_the_tracker_still_serves_everything_else(self):
79 self.assertEqual(self.tracker.announce(ALLOWED, port=6881, left=0)[b"complete"], 1)
80
81
82class EmptyBlacklist(TrackerTestCase):
83 config = "access.blacklist {dir}/blacklist\n"
84 files = {"blacklist": b""}
85
86 def test_an_empty_file_blocks_nothing_and_starts_cleanly(self):
87 self.assertEqual(self.tracker.announce(BLOCKED, port=6881, left=0)[b"complete"], 1)
88 self.assertEqual(self.tracker.announce(ALLOWED, port=6882, left=0)[b"complete"], 1)
89
90
91class MissingBlacklist(TrackerTestCase):
92 """The file is named but absent: the tracker warns and serves everything,
93 because it may be created later."""
94
95 config = "access.blacklist {dir}/absent\n"
96
97 def test_a_missing_file_blocks_nothing(self):
98 self.assertEqual(self.tracker.announce(BLOCKED, port=6881, left=0)[b"complete"], 1)
99
100
101if __name__ == "__main__":
102 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
3import os
4import socket
5import sys
6import unittest
7
8sys.path.insert(0, os.path.dirname(__file__))
9from harness import TrackerTestCase
10
11
12def 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
23class 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
98class 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
128if __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
3These need WANT_IP_FROM_QUERY_STRING so a peer's address can be dictated
4rather than being whatever loopback the test happened to come from.
5"""
6
7import os
8import socket
9import sys
10import unittest
11
12sys.path.insert(0, os.path.dirname(__file__))
13from harness import TrackerTestCase
14
15
16def 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
25class 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
88if __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..4059656
--- /dev/null
+++ b/tests/integration/test_fullscrape.py
@@ -0,0 +1,114 @@
1"""The full scrape, in every format the tracker offers.
2
3Each format is a separate writer, and they differ in more than cosmetics:
4one is binary, one is bencoded, three are line based and one dumps the
5tracker's own state for a restart. A worker thread produces them in chunks,
6so these also exercise the chunked transfer the ordinary replies never take.
7"""
8
9import gzip
10import struct
11import unittest
12
13from harness import TrackerTestCase, bdecode
14
15HEX = b"0123456789ABCDEF"
16
17
18class 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
103class 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
113if __name__ == "__main__":
114 unittest.main()
diff --git a/tests/integration/test_stats.py b/tests/integration/test_stats.py
new file mode 100644
index 0000000..7ff9a38
--- /dev/null
+++ b/tests/integration/test_stats.py
@@ -0,0 +1,170 @@
1"""The /stats endpoint.
2
3Each mode is its own writer into a fixed buffer, so these pin the shape of
4every one of them and, where the number means something, that it moves when
5the thing it counts happens.
6"""
7
8import ipaddress
9import re
10import socket
11import unittest
12import urllib.error
13import xml.etree.ElementTree
14
15from 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; "busy" is advertised but unbuilt,
20# which the next case pins.
21MRTG_MODES = ["peer", "conn", "scrp", "udp4", "tcp4", "torr", "fscr", "completed", "syncs"]
22TEXT_MODES = ["herr", "s24s", "top10", "top100", "renew", "version"]
23
24
25class StatsShape(TrackerTestCase):
26 def test_every_mode_answers_something(self):
27 for mode in MRTG_MODES + TEXT_MODES + ["everything"]:
28 self.assertTrue(self.tracker.stats(mode), "mode %s answered empty" % mode)
29
30 def test_an_mrtg_mode_opens_with_two_numbers(self):
31 """MRTG reads the first two lines as counters and the rest as labels,
32 so the order matters to something outside this project."""
33 for mode in MRTG_MODES:
34 lines = self.tracker.stats(mode).splitlines()
35 self.assertGreaterEqual(len(lines), 2, "mode %s" % mode)
36 for line in lines[:2]:
37 self.assertRegex(line, rb"^\d+$", "mode %s line %r" % (mode, line))
38
39 def test_the_busy_mode_is_advertised_but_not_built(self):
40 """TASK_STATS_BUSY_NETWORKS is in the enum and in the mode table, and
41 no switch anywhere handles it, so the worker frees the empty answer
42 and the request ends as a server error. Pinned rather than blessed:
43 either the report gets written or the mode leaves the table."""
44 with self.assertRaises(urllib.error.HTTPError) as caught:
45 self.tracker.stats("busy")
46 self.assertEqual(caught.exception.code, 500)
47
48 def test_an_unknown_mode_is_refused(self):
49 with self.assertRaises(urllib.error.HTTPError) as caught:
50 self.tracker.stats("nosuchmode")
51 self.assertEqual(caught.exception.code, 400)
52
53 def test_everything_is_well_formed_xml(self):
54 root = xml.etree.ElementTree.fromstring(self.tracker.stats("everything").decode())
55 self.assertEqual(root.tag, "stats")
56 self.assertIsNotNone(root.find("torrents/count_mutex"))
57 self.assertIsNotNone(root.find("connections/tcp/announce"))
58
59 def test_version_names_a_commit(self):
60 self.assertIn(b"opentracker", self.tracker.stats("version").lower() + b"opentracker")
61 self.assertRegex(self.tracker.stats("version"), rb"https?://")
62
63
64class StatsCount(TrackerTestCase):
65 def test_the_torrent_count_follows_the_torrents(self):
66 before = self.tracker.torrent_count()
67 self.tracker.announce(self.hash_for("statA"), port=6001, left=0)
68 self.assertEqual(self.tracker.torrent_count(), before + 1)
69 self.tracker.announce(self.hash_for("statB"), port=6002, left=0)
70 self.assertEqual(self.tracker.torrent_count(), before + 2)
71
72 def test_the_peer_count_separates_seeders_from_the_rest(self):
73 self.tracker.announce(self.hash_for("statpeers"), port=6001, left=0)
74 self.tracker.announce(self.hash_for("statpeers"), port=6002, left=4096)
75 peers, seeds = self.tracker.stats("peer").splitlines()[:2]
76 self.assertEqual(int(peers), 2)
77 self.assertEqual(int(seeds), 1)
78
79 def test_the_completed_count_follows_the_completed_event(self):
80 before = int(self.tracker.stats("completed").splitlines()[0])
81 self.tracker.announce(self.hash_for("statdone"), port=6001, left=0, event="completed")
82 self.assertEqual(int(self.tracker.stats("completed").splitlines()[0]), before + 1)
83
84 def test_a_bad_request_is_counted_as_an_http_error(self):
85 before = self.tracker.stats("herr")
86 try:
87 self.tracker.stats("nosuchmode")
88 except urllib.error.HTTPError:
89 pass
90 after = self.tracker.stats("herr")
91 self.assertNotEqual(before, after, "no error counter moved")
92
93 def test_the_tcp_counter_follows_tcp_requests(self):
94 before = int(self.tracker.stats("tcp4").splitlines()[0])
95 for _ in range(3):
96 self.tracker.announce(self.hash_for("stattcp"), port=6001, left=0)
97 self.assertGreaterEqual(int(self.tracker.stats("tcp4").splitlines()[0]), before + 3)
98
99 def test_the_udp_counter_follows_udp_requests(self):
100 before = int(self.tracker.stats("udp4").splitlines()[0])
101 sock, connection_id = self.tracker.udp_connect()
102 try:
103 self.tracker.udp_announce(sock, connection_id, self.hash_for("statudp"), left=0)
104 finally:
105 sock.close()
106 self.assertGreater(int(self.tracker.stats("udp4").splitlines()[0]), before)
107
108 def test_the_scrape_counter_follows_scrapes(self):
109 before = int(self.tracker.stats("scrp").splitlines()[0])
110 self.tracker.scrape(self.hash_for("statscrape"))
111 self.assertEqual(int(self.tracker.stats("scrp").splitlines()[0]), before + 1)
112
113
114class StatsTop(TrackerTestCase):
115 def test_the_top_list_ranks_by_peer_count(self):
116 busy = self.hash_for("topbusy")
117 quiet = self.hash_for("topquiet")
118 for port in range(6001, 6006):
119 self.tracker.announce(busy, port=port, left=0)
120 self.tracker.announce(quiet, port=6100, left=0)
121
122 body = self.tracker.stats("top10")
123 self.assertIn(b"Top 10 torrents by peers", body)
124 ranked = re.findall(rb"\t(\d+)\t([0-9A-F]{40})", body.split(b"by seeds")[0])
125 self.assertEqual(ranked[0][1], busy.hex().upper().encode(), "the busy torrent ranks first")
126 self.assertEqual(int(ranked[0][0]), 5)
127
128 def test_the_top_list_has_three_sections(self):
129 self.tracker.announce(self.hash_for("topsections"), port=6001, left=0)
130 body = self.tracker.stats("top100")
131 for heading in (b"by peers", b"by seeds", b"by leechers"):
132 self.assertIn(heading, body)
133
134
135class StatsNetworks(TrackerTestCase):
136 def source_address(self):
137 """Which address the tracker sees us from is not ours to choose: a
138 FreeBSD jail relocates a loopback connection to the jail's own
139 address, so ask the socket rather than assume 127.0.0.1."""
140 probe = socket.create_connection(("127.0.0.1", self.tracker.port), timeout=5)
141 try:
142 return probe.getsockname()[0]
143 finally:
144 probe.close()
145
146 def test_the_slash24_report_names_the_announcing_network(self):
147 source = self.source_address()
148 self.tracker.announce(self.hash_for("s24"), port=6001, left=0)
149 body = self.tracker.stats("s24s")
150 self.assertIn(b"Networks, limit /28:", body)
151 self.assertIn(b"Networks, limit /24:", body)
152
153 # The prefix is the first three address bytes of a v4 peer, left
154 # aligned in a sixteen byte buffer and then printed by the v6
155 # formatter, so 127.0.0.0/24 reads as "7f00::" rather than as the
156 # network an operator asked about. Pinned as it is, not blessed.
157 prefix = socket.inet_aton(source)[:3] + b"\0" * 13
158 self.assertIn(str(ipaddress.IPv6Address(prefix)).encode(), body)
159
160 def test_the_report_counts_one_network_per_announcing_address(self):
161 for port in (6001, 6002, 6003):
162 self.tracker.announce(self.hash_for("s24count"), port=port, left=0)
163 lines = [line for line in self.tracker.stats("s24s").splitlines() if b":" in line and b"limit" not in line]
164 self.assertEqual(len(lines), 2, "one line under each of the two limits")
165 for line in lines:
166 self.assertEqual(int(line.split(b":")[0]), 3, "three peers from the one network")
167
168
169if __name__ == "__main__":
170 unittest.main()
diff --git a/tests/integration/test_udp.py b/tests/integration/test_udp.py
new file mode 100644
index 0000000..1dfa21b
--- /dev/null
+++ b/tests/integration/test_udp.py
@@ -0,0 +1,153 @@
1"""The UDP tracker protocol, BEP 15.
2
3The HTTP tests cover the torrent store; these cover the packet handling in
4front of it, which has its own framing, its own connection id and its own
5rules about what to answer and what to ignore.
6"""
7
8import socket
9import struct
10import unittest
11
12from harness import TrackerTestCase
13
14MAGIC = 0x41727101980
15
16
17class 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
64class 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
118class 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
152if __name__ == "__main__":
153 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
17static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urlencoded_query}, 21static 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] */ 38struct 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. */
47static 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. */
32int main(int argc, char **argv) { 96int 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
21static int sv[2];
22static ot_event ready[EVENTS];
23
24static void pair_open(void) {
25 if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1)
26 sv[0] = sv[1] = -1;
27}
28
29static 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
37static 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
49static 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
56static 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
79static 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
94static 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
110static 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
122static 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. */
157static 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
187static 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
202static 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
208void 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
19static 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
35static 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
47static 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
65static 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
86static 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
105static _Atomic long stress_claims, stress_recycles, stress_violations, stress_attempts;
106static _Atomic int stress_lives[STRESS_SLOTS];
107static _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. */
112static _Atomic uint64_t stress_conn[STRESS_SLOTS];
113static _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
119static int stress_running(void) {
120 return atomic_load(&stress_claims) < STRESS_CLAIMS && atomic_load(&stress_attempts) < STRESS_GIVEUP;
121}
122
123static 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
147static 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
173static 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
212void 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
13static 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
25static 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
39static 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
50static 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
66static 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
89static 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
101void 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
71static void case_rejects_short_input(void) { 71static 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
80static void case_leaves_hash_untouched_on_failure(void) { 80static 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
102static void case_isdigit(void) { 102static 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
145static void case_scan_rejects(void) { 145static 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
174static void case_round_trip(void) { 174static 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
106static void case_leading_zeros_allowed(void) { 106static 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
154static 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
168static 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
182static 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
149void test_scan(void) { 196void 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
231static 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
248static 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
349void test_scan_urlencoded_query(void) { 322void 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
119static 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
115static void case_insert_finds_existing(void) { 160static 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 */
79void test_scan_urlencoded_query(void); 79void test_urlencoded(void);
80void test_iovec(void); 80void test_iovec(void);
81void test_vector(void); 81void test_vector(void);
82void test_random(void); 82void test_random(void);
@@ -88,5 +88,8 @@ void test_ip(void);
88void test_socket(void); 88void test_socket(void);
89void test_mutex(void); 89void test_mutex(void);
90void test_batch(void); 90void test_batch(void);
91void test_fdstate(void);
92void test_event(void);
93void test_fmt(void);
91 94
92#endif 95#endif