diff options
Diffstat (limited to 'tests/integration/test_fullscrape.py')
| -rw-r--r-- | tests/integration/test_fullscrape.py | 114 |
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 | |||
| 3 | Each format is a separate writer, and they differ in more than cosmetics: | ||
| 4 | one is binary, one is bencoded, three are line based and one dumps the | ||
| 5 | tracker's own state for a restart. A worker thread produces them in chunks, | ||
| 6 | so these also exercise the chunked transfer the ordinary replies never take. | ||
| 7 | """ | ||
| 8 | |||
| 9 | import gzip | ||
| 10 | import struct | ||
| 11 | import unittest | ||
| 12 | |||
| 13 | from harness import TrackerTestCase, bdecode | ||
| 14 | |||
| 15 | HEX = b"0123456789ABCDEF" | ||
| 16 | |||
| 17 | |||
| 18 | class FullScrape(TrackerTestCase): | ||
| 19 | def setUp(self): | ||
| 20 | TrackerTestCase.setUp(self) | ||
| 21 | self.seeding = self.hash_for("fsA") | ||
| 22 | self.leeching = self.hash_for("fsB") | ||
| 23 | self.tracker.announce(self.seeding, port=6001, left=0) | ||
| 24 | self.tracker.announce(self.leeching, port=6002, left=4096) | ||
| 25 | |||
| 26 | def scrape_all(self, query, headers=None): | ||
| 27 | body, head = self.tracker.get_with_headers(query, headers) | ||
| 28 | if head.get("Content-Encoding") == "gzip": | ||
| 29 | body = gzip.decompress(body) | ||
| 30 | return body | ||
| 31 | |||
| 32 | # --------------------------------------------------------- bencoded | ||
| 33 | |||
| 34 | def test_a_bare_scrape_returns_every_torrent(self): | ||
| 35 | answer = bdecode(self.scrape_all("/scrape")) | ||
| 36 | self.assertIn(self.seeding, answer[b"files"]) | ||
| 37 | self.assertIn(self.leeching, answer[b"files"]) | ||
| 38 | self.assertEqual(answer[b"files"][self.seeding][b"complete"], 1) | ||
| 39 | self.assertEqual(answer[b"files"][self.leeching][b"incomplete"], 1) | ||
| 40 | |||
| 41 | def test_the_bencoded_format_matches_the_bare_scrape(self): | ||
| 42 | self.assertEqual(bdecode(self.scrape_all("/stats?mode=tpbs&format=ben")), bdecode(self.scrape_all("/scrape"))) | ||
| 43 | |||
| 44 | def test_a_full_scrape_can_be_gzipped(self): | ||
| 45 | plain = self.scrape_all("/scrape") | ||
| 46 | body, head = self.tracker.get_with_headers("/scrape", {"Accept-Encoding": "gzip"}) | ||
| 47 | self.assertEqual(head.get("Content-Encoding"), "gzip") | ||
| 48 | self.assertEqual(gzip.decompress(body), plain) | ||
| 49 | |||
| 50 | # ----------------------------------------------------------- binary | ||
| 51 | |||
| 52 | def test_the_binary_format_is_a_hash_and_two_counts(self): | ||
| 53 | body = self.scrape_all("/stats?mode=tpbs&format=bin") | ||
| 54 | self.assertEqual(len(body) % 28, 0, "twenty byte hash plus two counts") | ||
| 55 | entries = {body[i : i + 20]: struct.unpack(">II", body[i + 20 : i + 28]) for i in range(0, len(body), 28)} | ||
| 56 | self.assertEqual(entries[self.seeding], (1, 0), "one seeder, no leechers") | ||
| 57 | self.assertEqual(entries[self.leeching], (0, 1), "no seeders, one leecher") | ||
| 58 | |||
| 59 | # ------------------------------------------------------- line based | ||
| 60 | |||
| 61 | def entries_from_lines(self, body): | ||
| 62 | return {line.split(b":")[0]: line.split(b":")[1:] for line in body.splitlines() if line} | ||
| 63 | |||
| 64 | def test_the_text_format_is_hex_colon_seeders_colon_leechers(self): | ||
| 65 | entries = self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txt")) | ||
| 66 | self.assertEqual(entries[self.seeding.hex().upper().encode()], [b"1", b"0"]) | ||
| 67 | self.assertEqual(entries[self.leeching.hex().upper().encode()], [b"0", b"1"]) | ||
| 68 | |||
| 69 | def test_the_text_plus_format_adds_the_completed_count(self): | ||
| 70 | entries = self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txtp")) | ||
| 71 | self.assertEqual(entries[self.seeding.hex().upper().encode()], [b"1", b"0", b"0"]) | ||
| 72 | |||
| 73 | def test_the_urlencoded_format_escapes_the_hash(self): | ||
| 74 | body = self.scrape_all("/stats?mode=tpbs&format=url") | ||
| 75 | lines = [line for line in body.splitlines() if line] | ||
| 76 | self.assertEqual(len(lines), 2) | ||
| 77 | for line in lines: | ||
| 78 | escaped = line.rsplit(b":", 2)[0] | ||
| 79 | self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") | ||
| 80 | |||
| 81 | def test_the_state_dump_carries_a_base_time_rather_than_a_peer_count(self): | ||
| 82 | """What a restart reads back, so its second column is the torrent's | ||
| 83 | own clock and not a count of anybody.""" | ||
| 84 | entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) | ||
| 85 | self.assertIn(self.seeding.hex().upper().encode(), entries) | ||
| 86 | base, completed = entries[self.seeding.hex().upper().encode()] | ||
| 87 | self.assertGreater(int(base), 0) | ||
| 88 | self.assertEqual(int(completed), 0) | ||
| 89 | |||
| 90 | # ------------------------------------------------------------ shape | ||
| 91 | |||
| 92 | def test_every_format_sees_the_same_torrents(self): | ||
| 93 | counts = { | ||
| 94 | "ben": len(bdecode(self.scrape_all("/stats?mode=tpbs&format=ben"))[b"files"]), | ||
| 95 | "bin": len(self.scrape_all("/stats?mode=tpbs&format=bin")) // 28, | ||
| 96 | "txt": len(self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txt"))), | ||
| 97 | "txtp": len(self.entries_from_lines(self.scrape_all("/stats?mode=tpbs&format=txtp"))), | ||
| 98 | "url": len([l for l in self.scrape_all("/stats?mode=tpbs&format=url").splitlines() if l]), | ||
| 99 | } | ||
| 100 | self.assertEqual(set(counts.values()), {2}, "all formats, two torrents: %r" % counts) | ||
| 101 | |||
| 102 | |||
| 103 | class FullScrapeChunking(TrackerTestCase): | ||
| 104 | def test_a_scrape_larger_than_one_chunk_arrives_whole(self): | ||
| 105 | """The writer hands the socket a chunk at a time, so a scrape that | ||
| 106 | spans several is the case where the seams show.""" | ||
| 107 | for i in range(400): | ||
| 108 | self.tracker.announce(struct.pack(">I", i) + b"\0" * 16, port=7000 + i, left=0) | ||
| 109 | answer = bdecode(self.tracker.get("/scrape")) | ||
| 110 | self.assertEqual(len(answer[b"files"]), 400) | ||
| 111 | |||
| 112 | |||
| 113 | if __name__ == "__main__": | ||
| 114 | unittest.main() | ||
