diff options
Diffstat (limited to 'tests/integration/test_livesync.py')
| -rw-r--r-- | tests/integration/test_livesync.py | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/tests/integration/test_livesync.py b/tests/integration/test_livesync.py new file mode 100644 index 0000000..8dd754b --- /dev/null +++ b/tests/integration/test_livesync.py | |||
| @@ -0,0 +1,121 @@ | |||
| 1 | """Live sync, driven over the multicast group a cluster would use. | ||
| 2 | |||
| 3 | Live sync sits behind WANT_SYNC_LIVE, so these only run against a binary | ||
| 4 | built with it: make check-livesync. Against any other build they skip, which | ||
| 5 | is what the feature list in -v is for. | ||
| 6 | """ | ||
| 7 | |||
| 8 | import socket | ||
| 9 | import struct | ||
| 10 | import subprocess | ||
| 11 | import unittest | ||
| 12 | |||
| 13 | from harness import TRACKER, TrackerTestCase | ||
| 14 | |||
| 15 | # enum { OT_SYNC_PEER4, OT_SYNC_PEER6 } in src/ot_livesync.c | ||
| 16 | SYNC_PEER4 = 0 | ||
| 17 | SYNC_PEER6 = 1 | ||
| 18 | |||
| 19 | # src/trackerlogic.h | ||
| 20 | FLAG_SEEDING = 0x80 | ||
| 21 | FLAG_COMPLETED = 0x40 | ||
| 22 | FLAG_STOPPED = 0x20 | ||
| 23 | |||
| 24 | |||
| 25 | def has_livesync(): | ||
| 26 | done = subprocess.run([TRACKER, "-v"], capture_output=True, timeout=30) | ||
| 27 | return b"WANT_SYNC_LIVE" in done.stderr | ||
| 28 | |||
| 29 | |||
| 30 | def peer4(address, port, flags): | ||
| 31 | """Four bytes of address, two of port, one of flags, one the tracker | ||
| 32 | overwrites with its own idea of the time.""" | ||
| 33 | return socket.inet_aton(address) + struct.pack("!H", port) + bytes([flags, 0]) | ||
| 34 | |||
| 35 | |||
| 36 | def peer6(address, port, flags): | ||
| 37 | return socket.inet_pton(socket.AF_INET6, address) + struct.pack("!H", port) + bytes([flags, 0]) | ||
| 38 | |||
| 39 | |||
| 40 | def packet(kind, entries, tracker_id=0xDEADBEEF): | ||
| 41 | body = b"".join(info_hash + peer for info_hash, peer in entries) | ||
| 42 | return struct.pack("!I", tracker_id) + struct.pack("!I", kind) + body | ||
| 43 | |||
| 44 | |||
| 45 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | ||
| 46 | class LiveSyncIn(TrackerTestCase): | ||
| 47 | config = "livesync.cluster.listen 127.0.0.1:{livesync_port}\nlivesync.cluster.node_ip 127.0.0.1\n" | ||
| 48 | |||
| 49 | def counts(self, info_hash): | ||
| 50 | """Seeders and leechers a scrape reports for one torrent.""" | ||
| 51 | files = self.tracker.scrape(info_hash)[b"files"] | ||
| 52 | if info_hash not in files: | ||
| 53 | return None | ||
| 54 | entry = files[info_hash] | ||
| 55 | return entry[b"complete"], entry[b"incomplete"] | ||
| 56 | |||
| 57 | def test_a_synced_seeder_arrives(self): | ||
| 58 | info_hash = self.hash_for("sync-seed") | ||
| 59 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.3", 6881, FLAG_SEEDING))])) | ||
| 60 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)), | ||
| 61 | "scrape says %r" % (self.counts(info_hash),)) | ||
| 62 | |||
| 63 | def test_a_synced_leecher_arrives(self): | ||
| 64 | info_hash = self.hash_for("sync-leech") | ||
| 65 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.4", 6881, 0))])) | ||
| 66 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (0, 1)), | ||
| 67 | "scrape says %r" % (self.counts(info_hash),)) | ||
| 68 | |||
| 69 | def test_a_v6_peer_arrives(self): | ||
| 70 | info_hash = self.hash_for("sync-v6") | ||
| 71 | self.tracker.livesync_send(packet(SYNC_PEER6, [(info_hash, peer6("2001:db8::1", 6881, FLAG_SEEDING))])) | ||
| 72 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)), | ||
| 73 | "scrape says %r" % (self.counts(info_hash),)) | ||
| 74 | |||
| 75 | def test_one_packet_carries_several_torrents(self): | ||
| 76 | hashes = [self.hash_for("sync-many-%d" % i) for i in range(4)] | ||
| 77 | self.tracker.livesync_send(packet(SYNC_PEER4, [(h, peer4("10.1.2.%d" % (5 + i), 6881, FLAG_SEEDING)) | ||
| 78 | for i, h in enumerate(hashes)])) | ||
| 79 | self.assertTrue(self.tracker.wait_until(lambda: all(self.counts(h) == (1, 0) for h in hashes))) | ||
| 80 | |||
| 81 | def test_a_stopped_peer_is_taken_away_again(self): | ||
| 82 | info_hash = self.hash_for("sync-stop") | ||
| 83 | peer = peer4("10.1.2.9", 6881, FLAG_SEEDING) | ||
| 84 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer)])) | ||
| 85 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0))) | ||
| 86 | |||
| 87 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.9", 6881, FLAG_STOPPED))])) | ||
| 88 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) in (None, (0, 0))), | ||
| 89 | "scrape still says %r" % (self.counts(info_hash),)) | ||
| 90 | |||
| 91 | def test_a_truncated_entry_is_dropped(self): | ||
| 92 | """The loop takes whole hash and peer pairs and leaves a short tail | ||
| 93 | where it is, so the complete entry before it still lands.""" | ||
| 94 | good, short = self.hash_for("sync-good"), self.hash_for("sync-short") | ||
| 95 | payload = packet(SYNC_PEER4, [(good, peer4("10.1.2.10", 6881, FLAG_SEEDING))]) + short + b"\x0a\x01" | ||
| 96 | self.tracker.livesync_send(payload) | ||
| 97 | self.assertTrue(self.tracker.wait_until(lambda: self.counts(good) == (1, 0))) | ||
| 98 | self.assertIsNone(self.counts(short)) | ||
| 99 | |||
| 100 | def test_an_unknown_packet_type_is_ignored(self): | ||
| 101 | info_hash = self.hash_for("sync-unknown") | ||
| 102 | self.tracker.livesync_send(packet(99, [(info_hash, peer4("10.1.2.11", 6881, FLAG_SEEDING))])) | ||
| 103 | self.assertFalse(self.tracker.wait_until(lambda: self.counts(info_hash) is not None, timeout=1)) | ||
| 104 | |||
| 105 | def test_a_packet_with_nothing_but_a_header_is_ignored(self): | ||
| 106 | before = self.tracker.torrent_count() | ||
| 107 | self.tracker.livesync_send(struct.pack("!I", 0xDEADBEEF) + struct.pack("!I", SYNC_PEER4)) | ||
| 108 | self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != before, timeout=1)) | ||
| 109 | |||
| 110 | |||
| 111 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | ||
| 112 | class LiveSyncAccess(TrackerTestCase): | ||
| 113 | """Nothing blesses 127.0.0.1 here, so the tracker should take nothing | ||
| 114 | from it. Source address checking is all live sync has.""" | ||
| 115 | |||
| 116 | config = "livesync.cluster.listen 127.0.0.1:{livesync_port}\nlivesync.cluster.node_ip 10.0.0.1\n" | ||
| 117 | |||
| 118 | def test_an_unblessed_sender_is_ignored(self): | ||
| 119 | info_hash = self.hash_for("sync-unblessed") | ||
| 120 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.12", 6881, FLAG_SEEDING))])) | ||
| 121 | self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != 0, timeout=1)) | ||
