summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-27 01:16:37 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-10-27 01:16:37 +0100
commit9d21cb476d07ab5583c99b1418d4ffb54fbb2a10 (patch)
tree9b251b28208e3dc3273d5a62b08b9e51f84ea6e3 /tests
parent128064c48949502e63ed20ca409557b4fa5de2af (diff)
Adding the actual tests to do aforementioned 'Massively increase coverage by pulling fullscrapes and sending udp packets'
Diffstat (limited to 'tests')
-rw-r--r--tests/integration/test_accesslist.py102
-rw-r--r--tests/integration/test_fullscrape.py114
-rw-r--r--tests/integration/test_stats.py146
-rw-r--r--tests/integration/test_udp.py153
4 files changed, 515 insertions, 0 deletions
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_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..920d199
--- /dev/null
+++ b/tests/integration/test_stats.py
@@ -0,0 +1,146 @@
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 re
9import unittest
10import urllib.error
11import xml.etree.ElementTree
12
13from harness import TrackerTestCase
14
15# The modes a default build answers. "tpbs" and "statedump" are full scrapes
16# and belong to test_fullscrape; "fulllog" and "woodpeckers" sit behind
17# feature flags this build does not set; "busy" is advertised but unbuilt,
18# which the next case pins.
19MRTG_MODES = ["peer", "conn", "scrp", "udp4", "tcp4", "torr", "fscr", "completed", "syncs"]
20TEXT_MODES = ["herr", "s24s", "top10", "top100", "renew", "version"]
21
22
23class StatsShape(TrackerTestCase):
24 def test_every_mode_answers_something(self):
25 for mode in MRTG_MODES + TEXT_MODES + ["everything"]:
26 self.assertTrue(self.tracker.stats(mode), "mode %s answered empty" % mode)
27
28 def test_an_mrtg_mode_opens_with_two_numbers(self):
29 """MRTG reads the first two lines as counters and the rest as labels,
30 so the order matters to something outside this project."""
31 for mode in MRTG_MODES:
32 lines = self.tracker.stats(mode).splitlines()
33 self.assertGreaterEqual(len(lines), 2, "mode %s" % mode)
34 for line in lines[:2]:
35 self.assertRegex(line, rb"^\d+$", "mode %s line %r" % (mode, line))
36
37 def test_the_busy_mode_is_advertised_but_not_built(self):
38 """TASK_STATS_BUSY_NETWORKS is in the enum and in the mode table, and
39 no switch anywhere handles it, so the worker frees the empty answer
40 and the request ends as a server error. Pinned rather than blessed:
41 either the report gets written or the mode leaves the table."""
42 with self.assertRaises(urllib.error.HTTPError) as caught:
43 self.tracker.stats("busy")
44 self.assertEqual(caught.exception.code, 500)
45
46 def test_an_unknown_mode_is_refused(self):
47 with self.assertRaises(urllib.error.HTTPError) as caught:
48 self.tracker.stats("nosuchmode")
49 self.assertEqual(caught.exception.code, 400)
50
51 def test_everything_is_well_formed_xml(self):
52 root = xml.etree.ElementTree.fromstring(self.tracker.stats("everything").decode())
53 self.assertEqual(root.tag, "stats")
54 self.assertIsNotNone(root.find("torrents/count_mutex"))
55 self.assertIsNotNone(root.find("connections/tcp/announce"))
56
57 def test_version_names_a_commit(self):
58 self.assertIn(b"opentracker", self.tracker.stats("version").lower() + b"opentracker")
59 self.assertRegex(self.tracker.stats("version"), rb"https?://")
60
61
62class StatsCount(TrackerTestCase):
63 def test_the_torrent_count_follows_the_torrents(self):
64 before = self.tracker.torrent_count()
65 self.tracker.announce(self.hash_for("statA"), port=6001, left=0)
66 self.assertEqual(self.tracker.torrent_count(), before + 1)
67 self.tracker.announce(self.hash_for("statB"), port=6002, left=0)
68 self.assertEqual(self.tracker.torrent_count(), before + 2)
69
70 def test_the_peer_count_separates_seeders_from_the_rest(self):
71 self.tracker.announce(self.hash_for("statpeers"), port=6001, left=0)
72 self.tracker.announce(self.hash_for("statpeers"), port=6002, left=4096)
73 peers, seeds = self.tracker.stats("peer").splitlines()[:2]
74 self.assertEqual(int(peers), 2)
75 self.assertEqual(int(seeds), 1)
76
77 def test_the_completed_count_follows_the_completed_event(self):
78 before = int(self.tracker.stats("completed").splitlines()[0])
79 self.tracker.announce(self.hash_for("statdone"), port=6001, left=0, event="completed")
80 self.assertEqual(int(self.tracker.stats("completed").splitlines()[0]), before + 1)
81
82 def test_a_bad_request_is_counted_as_an_http_error(self):
83 before = self.tracker.stats("herr")
84 try:
85 self.tracker.stats("nosuchmode")
86 except urllib.error.HTTPError:
87 pass
88 after = self.tracker.stats("herr")
89 self.assertNotEqual(before, after, "no error counter moved")
90
91 def test_the_tcp_counter_follows_tcp_requests(self):
92 before = int(self.tracker.stats("tcp4").splitlines()[0])
93 for _ in range(3):
94 self.tracker.announce(self.hash_for("stattcp"), port=6001, left=0)
95 self.assertGreaterEqual(int(self.tracker.stats("tcp4").splitlines()[0]), before + 3)
96
97 def test_the_udp_counter_follows_udp_requests(self):
98 before = int(self.tracker.stats("udp4").splitlines()[0])
99 sock, connection_id = self.tracker.udp_connect()
100 try:
101 self.tracker.udp_announce(sock, connection_id, self.hash_for("statudp"), left=0)
102 finally:
103 sock.close()
104 self.assertGreater(int(self.tracker.stats("udp4").splitlines()[0]), before)
105
106 def test_the_scrape_counter_follows_scrapes(self):
107 before = int(self.tracker.stats("scrp").splitlines()[0])
108 self.tracker.scrape(self.hash_for("statscrape"))
109 self.assertEqual(int(self.tracker.stats("scrp").splitlines()[0]), before + 1)
110
111
112class StatsTop(TrackerTestCase):
113 def test_the_top_list_ranks_by_peer_count(self):
114 busy = self.hash_for("topbusy")
115 quiet = self.hash_for("topquiet")
116 for port in range(6001, 6006):
117 self.tracker.announce(busy, port=port, left=0)
118 self.tracker.announce(quiet, port=6100, left=0)
119
120 body = self.tracker.stats("top10")
121 self.assertIn(b"Top 10 torrents by peers", body)
122 ranked = re.findall(rb"\t(\d+)\t([0-9A-F]{40})", body.split(b"by seeds")[0])
123 self.assertEqual(ranked[0][1], busy.hex().upper().encode(), "the busy torrent ranks first")
124 self.assertEqual(int(ranked[0][0]), 5)
125
126 def test_the_top_list_has_three_sections(self):
127 self.tracker.announce(self.hash_for("topsections"), port=6001, left=0)
128 body = self.tracker.stats("top100")
129 for heading in (b"by peers", b"by seeds", b"by leechers"):
130 self.assertIn(heading, body)
131
132
133class StatsNetworks(TrackerTestCase):
134 def test_the_slash24_report_names_the_loopback_network(self):
135 self.tracker.announce(self.hash_for("s24"), port=6001, left=0)
136 body = self.tracker.stats("s24s")
137 self.assertIn(b"Networks, limit /28:", body)
138 self.assertIn(b"Networks, limit /24:", body)
139 # The prefix is masked out of a v4 peer's four address bytes and then
140 # printed through the v6 formatter, so 127.0.0.0/24 reads as "7f00::"
141 # rather than as an address anyone would recognise. Pinned as it is.
142 self.assertIn(b"7f00::", body)
143
144
145if __name__ == "__main__":
146 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()