summaryrefslogtreecommitdiff
path: root/tests/integration/test_fullscrape.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration/test_fullscrape.py')
-rw-r--r--tests/integration/test_fullscrape.py114
1 files changed, 114 insertions, 0 deletions
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()