summaryrefslogtreecommitdiff
path: root/tests/integration
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration')
-rw-r--r--tests/integration/harness.py5
-rw-r--r--tests/integration/test_accesslist.py4
-rw-r--r--tests/integration/test_dynamic_accesslist.py18
-rw-r--r--tests/integration/test_fullscrape.py14
-rw-r--r--tests/integration/test_keepalive.py16
-rw-r--r--tests/integration/test_livesync.py13
-rw-r--r--tests/integration/test_statefile.py41
7 files changed, 84 insertions, 27 deletions
diff --git a/tests/integration/harness.py b/tests/integration/harness.py
index e5ec621..f3cd0fa 100644
--- a/tests/integration/harness.py
+++ b/tests/integration/harness.py
@@ -135,6 +135,11 @@ class Tracker:
135 self.log.seek(0) 135 self.log.seek(0)
136 return self.log.read().decode("utf-8", "replace") 136 return self.log.read().decode("utf-8", "replace")
137 137
138 def reload(self):
139 """SIGHUP makes the accesslist reader re-read its files, which also
140 empties the dynamic add/delete changesets."""
141 os.kill(self.process.pid, signal.SIGHUP)
142
138 def stop(self): 143 def stop(self):
139 """Returns anything the tracker complained about on the way out. 144 """Returns anything the tracker complained about on the way out.
140 145
diff --git a/tests/integration/test_accesslist.py b/tests/integration/test_accesslist.py
index 7d01264..3ed8ddf 100644
--- a/tests/integration/test_accesslist.py
+++ b/tests/integration/test_accesslist.py
@@ -7,6 +7,7 @@ produces: an empty file, and a last line with no newline after it.
7 7
8import os 8import os
9import struct 9import struct
10import time
10import unittest 11import unittest
11 12
12from harness import Tracker, TrackerTestCase 13from harness import Tracker, TrackerTestCase
@@ -109,9 +110,10 @@ class ListIsInPlaceBeforeTheStateFile(TrackerTestCase):
109 files = {"blacklist": listing(BLOCKED)} 110 files = {"blacklist": listing(BLOCKED)}
110 111
111 def test_a_blacklisted_torrent_in_a_state_file_is_not_restored(self): 112 def test_a_blacklisted_torrent_in_a_state_file_is_not_restored(self):
113 touched = int(time.time()) // 60 - 5
112 path = os.path.join(self.tracker.dir, "state") 114 path = os.path.join(self.tracker.dir, "state")
113 with open(path, "w") as handle: 115 with open(path, "w") as handle:
114 handle.write("%s:5:7\n%s:5:7\n" % (BLOCKED.hex(), ALLOWED.hex())) 116 handle.write("%s:%d:7\n%s:%d:7\n" % (BLOCKED.hex(), touched, ALLOWED.hex(), touched))
115 117
116 restarted = Tracker(self.config, self.files, extra_args=["-l", path]) 118 restarted = Tracker(self.config, self.files, extra_args=["-l", path])
117 try: 119 try:
diff --git a/tests/integration/test_dynamic_accesslist.py b/tests/integration/test_dynamic_accesslist.py
index 7fa5fde..2072f5e 100644
--- a/tests/integration/test_dynamic_accesslist.py
+++ b/tests/integration/test_dynamic_accesslist.py
@@ -40,6 +40,24 @@ class DynamicAccesslist(TrackerTestCase):
40 time.sleep(0.05) 40 time.sleep(0.05)
41 return False 41 return False
42 42
43 def test_a_hash_added_after_a_reload_is_blocked(self):
44 """A reload leaves the add changeset an empty list rather than an
45 absent one. Adding against it walked a null array; the hash must be
46 blocked and the tracker must stay up."""
47 OTHER = bytes(range(21, 41))
48 self.feed("add.fifo", BLOCKED)
49 self.assertTrue(self.wait_for(BLOCKED, blocked=True), "the first hash was never blocked")
50
51 # The reload empties the add changeset, which unblocks the first hash.
52 # Wait for that before adding the second, so it lands on the empty list
53 # the bug walked, not on the changeset before the reload has cleared it.
54 self.tracker.reload()
55 self.assertTrue(self.wait_for(BLOCKED, blocked=False), "the reload never emptied the changeset")
56
57 self.feed("add.fifo", OTHER)
58 self.assertTrue(self.wait_for(OTHER, blocked=True), "the hash added after the reload was never blocked")
59 # tearDown's stop() surfaces any sanitizer complaint on the way out.
60
43 def test_a_hash_can_be_blocked_and_released_while_running(self): 61 def test_a_hash_can_be_blocked_and_released_while_running(self):
44 self.assertNotIn(b"failure reason", self.tracker.announce(BLOCKED)) 62 self.assertNotIn(b"failure reason", self.tracker.announce(BLOCKED))
45 63
diff --git a/tests/integration/test_fullscrape.py b/tests/integration/test_fullscrape.py
index 3150612..45b3ffb 100644
--- a/tests/integration/test_fullscrape.py
+++ b/tests/integration/test_fullscrape.py
@@ -8,6 +8,7 @@ so these also exercise the chunked transfer the ordinary replies never take.
8 8
9import gzip 9import gzip
10import struct 10import struct
11import time
11import unittest 12import unittest
12 13
13from harness import TrackerTestCase, bdecode 14from harness import TrackerTestCase, bdecode
@@ -78,15 +79,14 @@ class FullScrape(TrackerTestCase):
78 escaped = line.rsplit(b":", 2)[0] 79 escaped = line.rsplit(b":", 2)[0]
79 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") 80 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes")
80 81
81 def test_the_state_dump_carries_an_age_rather_than_a_peer_count(self): 82 def test_the_state_dump_carries_a_stamp_rather_than_a_peer_count(self):
82 """What a restart reads back. The second column is how many minutes 83 """What a restart reads back. The second column is the minute the
83 ago the torrent was last touched, not a stamp and not a count of 84 torrent was last touched and not a count of anybody, by the wall
84 anybody: the clock counts from boot, so an absolute time would mean 85 clock, which is the only one two runs of the tracker agree on."""
85 nothing to the process that reads it."""
86 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) 86 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump"))
87 self.assertIn(self.seeding.hex().upper().encode(), entries) 87 self.assertIn(self.seeding.hex().upper().encode(), entries)
88 age, completed = entries[self.seeding.hex().upper().encode()] 88 stamp, completed = entries[self.seeding.hex().upper().encode()]
89 self.assertEqual(int(age), 0, "announced a moment ago") 89 self.assertAlmostEqual(int(stamp), int(time.time()) // 60, delta=1, msg="announced a moment ago")
90 self.assertEqual(int(completed), 0) 90 self.assertEqual(int(completed), 0)
91 91
92 # ------------------------------------------------------------ shape 92 # ------------------------------------------------------------ shape
diff --git a/tests/integration/test_keepalive.py b/tests/integration/test_keepalive.py
index 2c724fc..f9aa468 100644
--- a/tests/integration/test_keepalive.py
+++ b/tests/integration/test_keepalive.py
@@ -107,6 +107,22 @@ class KeepAlive(TrackerTestCase):
107 finally: 107 finally:
108 c.close() 108 c.close()
109 109
110 def test_a_tiny_split_request_does_not_overread(self):
111 """A request that arrives in pieces and completes as a buffer shorter
112 than the dispatch literals (\"GET /\" etc.) used to read past the
113 exactly sized buffer. It must be rejected without a sanitizer hit."""
114 c = self.tracker.connect()
115 try:
116 c.sock.sendall(b"\r\n")
117 time.sleep(0.05)
118 c.sock.sendall(b"\r\n") # completes as a 4-byte buffer
119 time.sleep(0.1)
120 finally:
121 c.close()
122 # the tracker must still answer; tearDown's stop() surfaces any sanitizer abort
123 answer = self.tracker.announce(self.hash_for("after-tiny"), port=6881, left=0)
124 self.assertNotIn(b"failure reason", answer, "the tracker stopped serving after a tiny request")
125
110 def test_a_split_request_answered_by_a_worker_does_not_spin(self): 126 def test_a_split_request_answered_by_a_worker_does_not_spin(self):
111 """A reply that a worker produces does not go through the send path, 127 """A reply that a worker produces does not go through the send path,
112 so the request stays in the buffer the read loop is reading. The loop 128 so the request stays in the buffer the read loop is reading. The loop
diff --git a/tests/integration/test_livesync.py b/tests/integration/test_livesync.py
index f4cb2db..e146d93 100644
--- a/tests/integration/test_livesync.py
+++ b/tests/integration/test_livesync.py
@@ -72,6 +72,19 @@ class LiveSyncIn(TrackerTestCase):
72 self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)), 72 self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)),
73 "scrape says %r" % (self.counts(info_hash),)) 73 "scrape says %r" % (self.counts(info_hash),))
74 74
75 def test_many_v6_announces_do_not_overflow_the_send_buffer(self):
76 """The outgoing peer buffer is filled by announces to a clustered
77 tracker. The flush watermark used sizeof(ot_peer) (== 1), so a v6
78 record could be appended past the 1480-byte buffer onto the adjacent
79 fill field, which then drove an oversized stack copy. Well past the
80 old overflow point (~37 v6 peers) the tracker must still be alive."""
81 last = None
82 for i in range(80):
83 last = self.tracker.announce(self.hash_for("flood-%d" % i),
84 ip="2001:db8::%x" % (i + 1), port=7000 + i, left=1)
85 self.assertIn(b"complete", last, "the tracker stopped answering mid-flood")
86 # tearDown's stop() surfaces any sanitizer complaint on the way out.
87
75 def test_one_packet_carries_several_torrents(self): 88 def test_one_packet_carries_several_torrents(self):
76 hashes = [self.hash_for("sync-many-%d" % i) for i in range(4)] 89 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)) 90 self.tracker.livesync_send(packet(SYNC_PEER4, [(h, peer4("10.1.2.%d" % (5 + i), 6881, FLAG_SEEDING))
diff --git a/tests/integration/test_statefile.py b/tests/integration/test_statefile.py
index 21eefae..a3c8d59 100644
--- a/tests/integration/test_statefile.py
+++ b/tests/integration/test_statefile.py
@@ -1,12 +1,12 @@
1"""The state dump and the -l switch that reads it back. 1"""The state dump and the -l switch that reads it back.
2 2
3Nothing exercised the load path before, and the dump's second column is now 3The second column is the minute a torrent was last touched, by the wall
4how long ago a torrent was last touched rather than when, because the clock 4clock, which is what the file has recorded for years and the only clock two
5counts from boot and an absolute stamp would mean nothing to the process 5runs of the tracker agree on.
6that reads the file.
7""" 6"""
8 7
9import os 8import os
9import time
10import unittest 10import unittest
11 11
12from harness import Tracker, TrackerTestCase 12from harness import Tracker, TrackerTestCase
@@ -54,23 +54,26 @@ class StateFileRoundTrip(TrackerTestCase):
54 restarted._cleanup() 54 restarted._cleanup()
55 55
56 56
57class StateFileWithNonsenseAges(TrackerTestCase): 57class StateFileWithNonsenseStamps(TrackerTestCase):
58 """The age column is read as unsigned and handed on as a signed ot_time. 58 """The column is read as unsigned and handed on as a signed ot_time. A
59 A value at or above 2^63 arrived negative, cleared the timeout check, and 59 value at or above 2^63 arrived negative, cleared the timeout check, and
60 put the torrent's base in the future, where the cleaner never reaches it. 60 put the torrent's base in the future, where the cleaner never reaches it.
61 At exactly 2^63 the subtraction itself overflowed.""" 61 At exactly 2^63 the subtraction itself overflowed."""
62 62
63 AGES = { 63 def stamps(self):
64 "overflow": 2**63, 64 now = int(time.time()) // 60
65 "wrapped": 2**63 + 60 * 24, 65 return {
66 "minus-one": 2**64 - 1, 66 "overflow": 2**63,
67 "int64-max": 2**63 - 1, 67 "minus-one": 2**64 - 1,
68 "past-timeout": 60 * 24 + 1, 68 "int64-max": 2**63 - 1,
69 } 69 "in-the-future": now + 60,
70 "past-timeout": now - (24 * 60 + 1),
71 }
70 72
71 def test_no_age_outside_the_timeout_builds_a_torrent(self): 73 def test_no_stamp_outside_the_timeout_builds_a_torrent(self):
72 lines = ["%s:%d:7" % (self.hash_for(name).hex(), age) for name, age in self.AGES.items()] 74 stamps = self.stamps()
73 lines.append("%s:5:7" % self.hash_for("honest").hex()) 75 lines = ["%s:%d:7" % (self.hash_for(name).hex(), stamp) for name, stamp in stamps.items()]
76 lines.append("%s:%d:7" % (self.hash_for("honest").hex(), int(time.time()) // 60 - 5))
74 77
75 path = os.path.join(self.tracker.dir, "nonsense") 78 path = os.path.join(self.tracker.dir, "nonsense")
76 with open(path, "w") as handle: 79 with open(path, "w") as handle:
@@ -78,9 +81,9 @@ class StateFileWithNonsenseAges(TrackerTestCase):
78 81
79 restarted = Tracker(extra_args=["-l", path]) 82 restarted = Tracker(extra_args=["-l", path])
80 try: 83 try:
81 for name in self.AGES: 84 for name, stamp in stamps.items():
82 self.assertNotIn(self.hash_for(name), restarted.scrape(self.hash_for(name))[b"files"], 85 self.assertNotIn(self.hash_for(name), restarted.scrape(self.hash_for(name))[b"files"],
83 "the line with age %d was loaded" % self.AGES[name]) 86 "the line stamped %d was loaded" % stamp)
84 self.assertIn(self.hash_for("honest"), restarted.scrape(self.hash_for("honest"))[b"files"], 87 self.assertIn(self.hash_for("honest"), restarted.scrape(self.hash_for("honest"))[b"files"],
85 "the state file was not read at all") 88 "the state file was not read at all")
86 self.assertEqual(restarted.torrent_count(), 1) 89 self.assertEqual(restarted.torrent_count(), 1)