diff options
Diffstat (limited to 'tests/integration')
| -rw-r--r-- | tests/integration/test_dynamic_accesslist.py | 50 | ||||
| -rw-r--r-- | tests/integration/test_tcpworkers.py | 67 |
2 files changed, 117 insertions, 0 deletions
diff --git a/tests/integration/test_dynamic_accesslist.py b/tests/integration/test_dynamic_accesslist.py new file mode 100644 index 0000000..7fa5fde --- /dev/null +++ b/tests/integration/test_dynamic_accesslist.py | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | """Hashes added to and removed from the blacklist while the tracker runs. | ||
| 2 | |||
| 3 | The two FIFOs are read by threads of their own, which is also what makes | ||
| 4 | this the test that drives those threads through a shutdown. | ||
| 5 | """ | ||
| 6 | |||
| 7 | import os | ||
| 8 | import time | ||
| 9 | import unittest | ||
| 10 | |||
| 11 | from harness import TrackerTestCase | ||
| 12 | |||
| 13 | BLOCKED = bytes(range(1, 21)) | ||
| 14 | |||
| 15 | |||
| 16 | def as_hex(info_hash): | ||
| 17 | return "".join("%02x" % b for b in info_hash) | ||
| 18 | |||
| 19 | |||
| 20 | class DynamicAccesslist(TrackerTestCase): | ||
| 21 | config = ("access.blacklist {dir}/blacklist\n" | ||
| 22 | "access.fifo_add {dir}/add.fifo\n" | ||
| 23 | "access.fifo_delete {dir}/delete.fifo\n") | ||
| 24 | files = {"blacklist": b""} | ||
| 25 | |||
| 26 | def feed(self, name, info_hash): | ||
| 27 | """Writing to the FIFO blocks until the reader has it, so this also | ||
| 28 | tells us the thread is alive.""" | ||
| 29 | with open(os.path.join(self.tracker.dir, name), "w") as fifo: | ||
| 30 | fifo.write("%s added by a test\n" % as_hex(info_hash)) | ||
| 31 | |||
| 32 | def wait_for(self, info_hash, blocked, patience=5.0): | ||
| 33 | """The reader thread picks the hash up on its own schedule, so this | ||
| 34 | waits for the answer to change rather than assuming it already has.""" | ||
| 35 | deadline = time.time() + patience | ||
| 36 | while time.time() < deadline: | ||
| 37 | answer = self.tracker.announce(info_hash) | ||
| 38 | if (b"failure reason" in answer) == blocked: | ||
| 39 | return True | ||
| 40 | time.sleep(0.05) | ||
| 41 | return False | ||
| 42 | |||
| 43 | def test_a_hash_can_be_blocked_and_released_while_running(self): | ||
| 44 | self.assertNotIn(b"failure reason", self.tracker.announce(BLOCKED)) | ||
| 45 | |||
| 46 | self.feed("add.fifo", BLOCKED) | ||
| 47 | self.assertTrue(self.wait_for(BLOCKED, blocked=True), "the hash was never blocked") | ||
| 48 | |||
| 49 | self.feed("delete.fifo", BLOCKED) | ||
| 50 | self.assertTrue(self.wait_for(BLOCKED, blocked=False), "the hash was never released") | ||
diff --git a/tests/integration/test_tcpworkers.py b/tests/integration/test_tcpworkers.py new file mode 100644 index 0000000..e43612a --- /dev/null +++ b/tests/integration/test_tcpworkers.py | |||
| @@ -0,0 +1,67 @@ | |||
| 1 | """The tracker with its tcp side handed to worker threads. | ||
| 2 | |||
| 3 | With workers configured, a connection is accepted and answered on a worker's | ||
| 4 | own stack. Anything it cannot finish there is copied to the heap and handed | ||
| 5 | to the event loop, which is the one place where a worker and the main thread | ||
| 6 | touch the same structures, so these drive all three outcomes: answered | ||
| 7 | inline, held open, and parked waiting for another thread entirely. | ||
| 8 | """ | ||
| 9 | |||
| 10 | import threading | ||
| 11 | import unittest | ||
| 12 | import urllib.parse | ||
| 13 | |||
| 14 | from harness import TrackerTestCase, bdecode | ||
| 15 | |||
| 16 | WORKERS = 4 | ||
| 17 | |||
| 18 | |||
| 19 | class TcpWorkers(TrackerTestCase): | ||
| 20 | config = "listen.tcp.workers %d\n" % WORKERS | ||
| 21 | |||
| 22 | def announce_path(self, name, port=6881): | ||
| 23 | return "/announce?info_hash=%s&peer_id=%s&port=%d&left=0" % ( | ||
| 24 | urllib.parse.quote_from_bytes(self.hash_for(name), safe=""), | ||
| 25 | "x" * 20, | ||
| 26 | port, | ||
| 27 | ) | ||
| 28 | |||
| 29 | def test_many_at_once_are_all_answered(self): | ||
| 30 | """Every worker gets something to do, and the swarm adds up.""" | ||
| 31 | info_hash = self.hash_for("workers-swarm") | ||
| 32 | errors = [] | ||
| 33 | |||
| 34 | def announce(index): | ||
| 35 | try: | ||
| 36 | self.tracker.announce(info_hash, peer_id=b"-OT0001-%012d" % index, port=7000 + index, left=0) | ||
| 37 | except Exception as problem: # noqa: BLE001 - reported, not swallowed | ||
| 38 | errors.append("%d: %r" % (index, problem)) | ||
| 39 | |||
| 40 | threads = [threading.Thread(target=announce, args=(i,)) for i in range(32)] | ||
| 41 | for thread in threads: | ||
| 42 | thread.start() | ||
| 43 | for thread in threads: | ||
| 44 | thread.join() | ||
| 45 | |||
| 46 | self.assertEqual(errors, [], "\n".join(errors)) | ||
| 47 | self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"complete"], 32) | ||
| 48 | |||
| 49 | def test_a_kept_connection_outlives_the_worker_that_took_it(self): | ||
| 50 | """Keep-alive is the plain case of a connection a worker hands on.""" | ||
| 51 | connection = self.tracker.connect() | ||
| 52 | try: | ||
| 53 | for i in range(4): | ||
| 54 | connection.send(self.announce_path("workers-kept")) | ||
| 55 | status, _, body = connection.reply() | ||
| 56 | self.assertIn(b"200", status) | ||
| 57 | self.assertIn(b"complete", bdecode(body)) | ||
| 58 | finally: | ||
| 59 | connection.close() | ||
| 60 | |||
| 61 | def test_a_full_scrape_asked_for_by_a_worker_still_arrives(self): | ||
| 62 | """The reply is built by another thread, so the worker parks the | ||
| 63 | connection waiting for nothing at all before handing it over.""" | ||
| 64 | for i in range(20): | ||
| 65 | self.tracker.announce(bytes([i]) + b"\0" * 19, port=7100 + i, left=0) | ||
| 66 | body = self.tracker.get("/scrape") | ||
| 67 | self.assertEqual(len(bdecode(body)[b"files"]), 20) | ||
