summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/integration/test_dynamic_accesslist.py50
-rw-r--r--tests/integration/test_tcpworkers.py67
-rw-r--r--tests/runner.c2
-rw-r--r--tests/stubs.c33
-rw-r--r--tests/t_mutex.c171
-rw-r--r--tests/t_trackerlogic.c243
-rw-r--r--tests/t_workqueue.c107
-rw-r--r--tests/test.h2
8 files changed, 602 insertions, 73 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
3The two FIFOs are read by threads of their own, which is also what makes
4this the test that drives those threads through a shutdown.
5"""
6
7import os
8import time
9import unittest
10
11from harness import TrackerTestCase
12
13BLOCKED = bytes(range(1, 21))
14
15
16def as_hex(info_hash):
17 return "".join("%02x" % b for b in info_hash)
18
19
20class 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
3With workers configured, a connection is accepted and answered on a worker's
4own stack. Anything it cannot finish there is copied to the heap and handed
5to the event loop, which is the one place where a worker and the main thread
6touch the same structures, so these drive all three outcomes: answered
7inline, held open, and parked waiting for another thread entirely.
8"""
9
10import threading
11import unittest
12import urllib.parse
13
14from harness import TrackerTestCase, bdecode
15
16WORKERS = 4
17
18
19class 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)
diff --git a/tests/runner.c b/tests/runner.c
index 71a5e2a..97c6d51 100644
--- a/tests/runner.c
+++ b/tests/runner.c
@@ -28,6 +28,8 @@ static const struct module modules[] = {{"ot_urlencoded", test_urlencoded},
28 {"ot_ip", test_ip}, 28 {"ot_ip", test_ip},
29 {"ot_socket", test_socket}, 29 {"ot_socket", test_socket},
30 {"ot_mutex", test_mutex}, 30 {"ot_mutex", test_mutex},
31 {"trackerlogic", test_trackerlogic},
32 {"ot_workqueue", test_workqueue},
31 {"ot_batch", test_batch}, 33 {"ot_batch", test_batch},
32 {"ot_fdstate", test_fdstate}, 34 {"ot_fdstate", test_fdstate},
33 {"ot_event", test_event}, 35 {"ot_event", test_event},
diff --git a/tests/stubs.c b/tests/stubs.c
index a8e6962..5999e2d 100644
--- a/tests/stubs.c
+++ b/tests/stubs.c
@@ -8,11 +8,9 @@
8 behaviour should be supplied here rather than the test weakened. */ 8 behaviour should be supplied here rather than the test weakened. */
9 9
10/* Opentracker */ 10/* Opentracker */
11#include "ot_stats.h"
11#include "trackerlogic.h" 12#include "trackerlogic.h"
12 13
13/* ot_vector.c calls this from vector_clean_list, which no test drives yet. */
14void free_peerlist(ot_peerlist *peer_list) { (void)peer_list; }
15
16/* ot_accesslist.c is built with WANT_ACCESSLIST_BLACK so that the hash parser 14/* ot_accesslist.c is built with WANT_ACCESSLIST_BLACK so that the hash parser
17 is reachable. These two belong to accesslist_readfile and the reload worker, 15 is reachable. These two belong to accesslist_readfile and the reload worker,
18 neither of which any test drives. */ 16 neither of which any test drives. */
@@ -26,3 +24,32 @@ long io_trywrite(long d, const char *buf, long len) {
26 (void)buf; 24 (void)buf;
27 return len; 25 return len;
28} 26}
27
28/* trackerlogic.c is under test, and starting it up starts every module it
29 leans on. None of them is driven by these tests, so they are answered here
30 rather than linked in and left to run threads of their own. */
31void clean_init(void) {}
32void clean_deinit(void) {}
33int clean_single_torrent(ot_torrent *torrent) {
34 (void)torrent;
35 return 0;
36}
37void fullscrape_init(void) {}
38void fullscrape_deinit(void) {}
39void livesync_init(void) {}
40void livesync_deinit(void) {}
41void livesync_tell(struct ot_workstruct *ws) { (void)ws; }
42void stats_init(void) {}
43void stats_deinit(void) {}
44void stats_issue_event(struct ot_workstruct *ws, ot_status_event event, PROTO_FLAG proto, uintptr_t event_data) {
45 (void)ws;
46 (void)event;
47 (void)proto;
48 (void)event_data;
49}
50void stats_counters_register(struct ot_workstruct *ws) { (void)ws; }
51void stats_counters_retire(struct ot_workstruct *ws) { (void)ws; }
52
53const char *g_stats_path;
54ssize_t g_stats_path_len;
55uint32_t g_tracker_id;
diff --git a/tests/t_mutex.c b/tests/t_mutex.c
index 3307d70..7c1463b 100644
--- a/tests/t_mutex.c
+++ b/tests/t_mutex.c
@@ -4,104 +4,135 @@
4 $id$ */ 4 $id$ */
5 5
6/* System */ 6/* System */
7#include <stdlib.h> 7#include <pthread.h>
8#include <stdatomic.h>
8#include <string.h> 9#include <string.h>
10#include <unistd.h>
9 11
10/* Opentracker */ 12/* Opentracker */
11#include "ot_mutex.h" 13#include "ot_mutex.h"
12#include "test.h" 14#include "test.h"
13 15
14/* A worker asks for a class of task and blocks until one turns up, so a task 16/* Every announce passes through here, and what it has to get right is which
15 whose class matches no worker is never claimed and whoever is waiting for 17 bucket a hash belongs to and that two threads never hold one bucket at
16 its answer waits forever. These cases pin the matching. */ 18 once. Both are checked against real mutexes rather than a stand-in. */
17 19
18static void case_task_goes_to_its_own_class(void) { 20/* A hash that lands in a chosen bucket: the bucket is the top
19 ot_tasktype asked; 21 OT_BUCKET_COUNT_BITS of the first four bytes, read in network order */
20 ot_taskid taskid; 22static void hash_for_bucket(ot_hash hash, int bucket) {
23 memset(hash, 0, sizeof(ot_hash));
24 hash[0] = (uint8_t)(bucket >> (OT_BUCKET_COUNT_BITS - 8));
25 hash[1] = (uint8_t)((bucket << (16 - OT_BUCKET_COUNT_BITS)) & 0xff);
26}
27
28static void case_a_hash_names_its_bucket(void) {
29 static const int buckets[] = {0, 1, 2, 17, 511, 512, OT_BUCKET_COUNT - 1};
30 size_t i;
31
32 test_case("a hash is served by the bucket its leading bits name");
33 for (i = 0; i < sizeof(buckets) / sizeof(buckets[0]); ++i) {
34 ot_hash hash;
35 ot_vector *by_hash, *by_number;
36
37 hash_for_bucket(hash, buckets[i]);
38 by_hash = mutex_bucket_lock_by_hash(hash);
39 mutex_bucket_unlock_by_hash(hash, 0);
21 40
22 test_case("a worker is handed the task of the class it asked for"); 41 by_number = mutex_bucket_lock(buckets[i]);
23 CHECK_INT(mutex_workqueue_pushtask(41, TASK_FULLSCRAPE_TPB_ASCII), 0); 42 mutex_bucket_unlock(buckets[i], 0);
24 asked = TASK_FULLSCRAPE;
25 taskid = mutex_workqueue_poptask(&asked);
26 CHECK(taskid != 0);
27 43
28 test_case("and the full task type, not just its class"); 44 CHECK(by_hash == by_number);
29 CHECK_INT(asked, TASK_FULLSCRAPE_TPB_ASCII); 45 }
30 mutex_workqueue_canceltask(41);
31} 46}
32 47
33static void case_flags_survive_the_queue(void) { 48static void case_every_bucket_is_its_own(void) {
34 ot_tasktype asked; 49 ot_vector *first, *last, *middle;
35 50
36 test_case("flags outside the class mask are carried through"); 51 test_case("no two buckets are the same one");
37 CHECK_INT(mutex_workqueue_pushtask(42, TASK_FULLSCRAPE_TPB_URLENCODED | TASK_FLAG_GZIP), 0); 52 first = mutex_bucket_lock(0);
38 asked = TASK_FULLSCRAPE; 53 mutex_bucket_unlock(0, 0);
39 CHECK(mutex_workqueue_poptask(&asked) != 0); 54 middle = mutex_bucket_lock(OT_BUCKET_COUNT / 2);
40 CHECK_INT(asked, TASK_FULLSCRAPE_TPB_URLENCODED | TASK_FLAG_GZIP); 55 mutex_bucket_unlock(OT_BUCKET_COUNT / 2, 0);
41 mutex_workqueue_canceltask(42); 56 last = mutex_bucket_lock(OT_BUCKET_COUNT - 1);
57 mutex_bucket_unlock(OT_BUCKET_COUNT - 1, 0);
58
59 CHECK(first != middle);
60 CHECK(middle != last);
61 CHECK(first != last);
42} 62}
43 63
44static void case_classes_do_not_cross(void) { 64static void case_the_count_follows_what_unlock_is_told(void) {
45 ot_tasktype asked; 65 size_t before = mutex_get_torrent_count();
46 ot_taskid taskid; 66
67 test_case("the torrent count moves by what unlocking reports, and not otherwise");
68 mutex_bucket_lock(3);
69 mutex_bucket_unlock(3, 0);
70 CHECK_INT((int)(mutex_get_torrent_count() - before), 0);
47 71
48 test_case("a stats worker passes over a fullscrape task"); 72 mutex_bucket_lock(3);
49 CHECK_INT(mutex_workqueue_pushtask(43, TASK_FULLSCRAPE_TPB_BINARY), 0); 73 mutex_bucket_unlock(3, 5);
50 CHECK_INT(mutex_workqueue_pushtask(44, TASK_STATS_PEERS), 0); 74 CHECK_INT((int)(mutex_get_torrent_count() - before), 5);
51 75
52 asked = TASK_STATS; 76 mutex_bucket_lock(3);
53 taskid = mutex_workqueue_poptask(&asked); 77 mutex_bucket_unlock(3, -2);
54 CHECK(taskid != 0); 78 CHECK_INT((int)(mutex_get_torrent_count() - before), 3);
55 CHECK_INT(asked, TASK_STATS_PEERS); 79}
56 80
57 test_case("and leaves it for the one that wants it"); 81static _Atomic int g_other_got_in;
58 asked = TASK_FULLSCRAPE;
59 taskid = mutex_workqueue_poptask(&asked);
60 CHECK(taskid != 0);
61 CHECK_INT(asked, TASK_FULLSCRAPE_TPB_BINARY);
62 82
63 mutex_workqueue_canceltask(43); 83static void *take_a_bucket(void *args) {
64 mutex_workqueue_canceltask(44); 84 int bucket = (int)(intptr_t)args;
85
86 mutex_bucket_lock(bucket);
87 atomic_store(&g_other_got_in, 1);
88 mutex_bucket_unlock(bucket, 0);
89 return NULL;
90}
91
92static int waited_for_entry(int patience_ms) {
93 int waited;
94
95 for (waited = 0; waited < patience_ms && !atomic_load(&g_other_got_in); ++waited)
96 usleep(1000);
97 return atomic_load(&g_other_got_in);
65} 98}
66 99
67static void case_every_task_gets_its_own_id(void) { 100static void case_a_held_bucket_keeps_others_out(void) {
68 ot_tasktype asked; 101 pthread_t other;
69 ot_taskid first, second;
70 102
71 test_case("two queued tasks are handed out under different ids"); 103 test_case("a second thread waits for the bucket this one holds");
72 CHECK_INT(mutex_workqueue_pushtask(45, TASK_FULLSCRAPE), 0); 104 atomic_store(&g_other_got_in, 0);
73 CHECK_INT(mutex_workqueue_pushtask(46, TASK_FULLSCRAPE), 0); 105 mutex_bucket_lock(9);
106 CHECK_INT(pthread_create(&other, NULL, take_a_bucket, (void *)(intptr_t)9), 0);
74 107
75 asked = TASK_FULLSCRAPE; 108 CHECK_INT(waited_for_entry(200), 0);
76 first = mutex_workqueue_poptask(&asked);
77 asked = TASK_FULLSCRAPE;
78 second = mutex_workqueue_poptask(&asked);
79 CHECK(first != 0);
80 CHECK(second != 0);
81 CHECK(first != second);
82 109
83 mutex_workqueue_canceltask(45); 110 mutex_bucket_unlock(9, 0);
84 mutex_workqueue_canceltask(46); 111 CHECK_INT(waited_for_entry(2000), 1);
112 pthread_join(other, NULL);
85} 113}
86 114
87static void case_cancelled_task_is_not_handed_out(void) { 115static void case_another_bucket_is_free_meanwhile(void) {
88 ot_tasktype asked; 116 pthread_t other;
89 117
90 test_case("a cancelled task is gone before any worker sees it"); 118 test_case("and a different bucket is not held up by it");
91 CHECK_INT(mutex_workqueue_pushtask(47, TASK_FULLSCRAPE_TPB_ASCII_PLUS), 0); 119 atomic_store(&g_other_got_in, 0);
92 CHECK_INT(mutex_workqueue_pushtask(48, TASK_FULLSCRAPE), 0); 120 mutex_bucket_lock(9);
93 mutex_workqueue_canceltask(47); 121 CHECK_INT(pthread_create(&other, NULL, take_a_bucket, (void *)(intptr_t)10), 0);
94 122
95 asked = TASK_FULLSCRAPE; 123 CHECK_INT(waited_for_entry(2000), 1);
96 CHECK(mutex_workqueue_poptask(&asked) != 0); 124 pthread_join(other, NULL);
97 CHECK_INT(asked, TASK_FULLSCRAPE); 125 mutex_bucket_unlock(9, 0);
98 mutex_workqueue_canceltask(48);
99} 126}
100 127
101void test_mutex(void) { 128void test_mutex(void) {
102 case_task_goes_to_its_own_class(); 129 mutex_init();
103 case_flags_survive_the_queue(); 130
104 case_classes_do_not_cross(); 131 case_a_hash_names_its_bucket();
105 case_every_task_gets_its_own_id(); 132 case_every_bucket_is_its_own();
106 case_cancelled_task_is_not_handed_out(); 133 case_the_count_follows_what_unlock_is_told();
134 case_a_held_bucket_keeps_others_out();
135 case_another_bucket_is_free_meanwhile();
136
137 mutex_deinit();
107} 138}
diff --git a/tests/t_trackerlogic.c b/tests/t_trackerlogic.c
new file mode 100644
index 0000000..d3d8755
--- /dev/null
+++ b/tests/t_trackerlogic.c
@@ -0,0 +1,243 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <arpa/inet.h>
8#include <signal.h>
9#include <pthread.h>
10#include <string.h>
11
12/* Opentracker */
13#include "ot_mutex.h"
14#include "trackerlogic.h"
15#include "test.h"
16
17/* The swarm itself: what an announce does to a torrent, and what a scrape
18 says about it afterwards. The counts are read back through the udp scrape
19 because that is the one reply the tracker builds as plain numbers. */
20
21static struct ot_workstruct ws;
22
23static void hash_named(ot_hash hash, const char *name) {
24 memset(hash, 0, sizeof(ot_hash));
25 memcpy(hash, name, strlen(name) < sizeof(ot_hash) ? strlen(name) : sizeof(ot_hash));
26}
27
28/* An IPv4 peer arrives as the v4 mapped form the tracker stores */
29static void peer_v4(ot_peer6 peer, uint8_t last, uint16_t port) {
30 ot_ip6 ip = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 10, 0, 0, 0};
31 uint16_t net_port = htons(port);
32
33 ip[15] = last;
34 memset(peer, 0, sizeof(ot_peer6));
35 OT_SETIP(peer, ip);
36 OT_SETPORT(peer, &net_port);
37 OT_PEERFLAG(peer) = 0;
38}
39
40static void peer_v6(ot_peer6 peer, uint8_t last, uint16_t port) {
41 ot_ip6 ip = {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
42 uint16_t net_port = htons(port);
43
44 ip[15] = last;
45 memset(peer, 0, sizeof(ot_peer6));
46 OT_SETIP(peer, ip);
47 OT_SETPORT(peer, &net_port);
48 OT_PEERFLAG(peer) = 0;
49}
50
51static void announce(ot_hash *hash, ot_peer6 peer) {
52 ws.hash = hash;
53 memcpy(ws.peer, peer, sizeof(ot_peer6));
54 ws.reply = ws.outbuf;
55 add_peer_to_torrent_and_return_peers(FLAG_TCP, &ws, 50);
56}
57
58/* seeders, downloads completed, leechers */
59static void scrape(ot_hash hash, uint32_t counts[3]) {
60 char reply[12];
61
62 return_udp_scrape_for_torrent(hash, reply);
63 counts[0] = ntohl(((uint32_t *)reply)[0]);
64 counts[1] = ntohl(((uint32_t *)reply)[1]);
65 counts[2] = ntohl(((uint32_t *)reply)[2]);
66}
67
68static void case_an_announce_makes_a_torrent(void) {
69 ot_hash hash;
70 ot_peer6 peer;
71 uint32_t counts[3];
72 size_t before = mutex_get_torrent_count();
73
74 test_case("the first announce for a hash creates its torrent");
75 hash_named(hash, "first-announce");
76 peer_v4(peer, 1, 6881);
77 announce(&hash, peer);
78
79 CHECK_INT((int)(mutex_get_torrent_count() - before), 1);
80 scrape(hash, counts);
81 CHECK_INT((int)counts[2], 1); /* one leecher */
82 CHECK_INT((int)counts[0], 0);
83}
84
85static void case_the_same_peer_twice_is_one_peer(void) {
86 ot_hash hash;
87 ot_peer6 peer;
88 uint32_t counts[3];
89
90 test_case("announcing twice renews a peer rather than adding another");
91 hash_named(hash, "renewing");
92 peer_v4(peer, 2, 6881);
93 announce(&hash, peer);
94 announce(&hash, peer);
95
96 scrape(hash, counts);
97 CHECK_INT((int)counts[2], 1);
98}
99
100static void case_a_port_makes_a_different_peer(void) {
101 ot_hash hash;
102 ot_peer6 peer;
103 uint32_t counts[3];
104
105 test_case("the same address on another port is another peer");
106 hash_named(hash, "two-ports");
107 peer_v4(peer, 3, 6881);
108 announce(&hash, peer);
109 peer_v4(peer, 3, 6882);
110 announce(&hash, peer);
111
112 scrape(hash, counts);
113 CHECK_INT((int)counts[2], 2);
114}
115
116static void case_seeding_is_counted_apart(void) {
117 ot_hash hash;
118 ot_peer6 peer;
119 uint32_t counts[3];
120
121 test_case("a seeder counts as complete, a leecher as incomplete");
122 hash_named(hash, "seed-and-leech");
123 peer_v4(peer, 4, 6881);
124 OT_PEERFLAG(peer) |= PEER_FLAG_SEEDING;
125 announce(&hash, peer);
126 peer_v4(peer, 5, 6881);
127 announce(&hash, peer);
128
129 scrape(hash, counts);
130 CHECK_INT((int)counts[0], 1);
131 CHECK_INT((int)counts[2], 1);
132}
133
134static void case_completion_without_seeding_is_not_believed(void) {
135 ot_hash hash;
136 ot_peer6 peer;
137 uint32_t counts[3];
138
139 test_case("a peer that claims to have completed but is still leeching is not counted");
140 hash_named(hash, "claiming");
141 peer_v4(peer, 6, 6881);
142 OT_PEERFLAG(peer) |= PEER_FLAG_COMPLETED;
143 announce(&hash, peer);
144
145 scrape(hash, counts);
146 CHECK_INT((int)counts[1], 0);
147 CHECK_INT((int)counts[2], 1); /* still a leecher */
148}
149
150static void case_completing_is_counted_once(void) {
151 ot_hash hash;
152 ot_peer6 peer;
153 uint32_t counts[3];
154
155 test_case("a seeder reporting completion is counted once, however often it says so");
156 hash_named(hash, "completing");
157 peer_v4(peer, 7, 6881);
158 OT_PEERFLAG(peer) |= PEER_FLAG_COMPLETED | PEER_FLAG_SEEDING;
159 announce(&hash, peer);
160 announce(&hash, peer);
161 announce(&hash, peer);
162
163 scrape(hash, counts);
164 CHECK_INT((int)counts[1], 1);
165 CHECK_INT((int)counts[0], 1);
166}
167
168static void case_a_peer_can_leave(void) {
169 ot_hash hash;
170 ot_peer6 peer;
171 uint32_t counts[3];
172
173 test_case("a peer that stops is gone from the swarm");
174 hash_named(hash, "leaving");
175 peer_v4(peer, 7, 6881);
176 announce(&hash, peer);
177 peer_v4(peer, 8, 6881);
178 announce(&hash, peer);
179 scrape(hash, counts);
180 CHECK_INT((int)counts[2], 2);
181
182 ws.hash = &hash;
183 memcpy(ws.peer, peer, sizeof(ot_peer6));
184 remove_peer_from_torrent(FLAG_TCP, &ws);
185
186 scrape(hash, counts);
187 CHECK_INT((int)counts[2], 1);
188}
189
190static void case_the_families_are_kept_apart(void) {
191 ot_hash hash;
192 ot_peer6 peer;
193 uint32_t counts[3];
194
195 test_case("a v4 and a v6 peer of one swarm are both counted");
196 hash_named(hash, "dual-stack");
197 peer_v4(peer, 9, 6881);
198 announce(&hash, peer);
199 peer_v6(peer, 9, 6881);
200 announce(&hash, peer);
201
202 scrape(hash, counts);
203 CHECK_INT((int)counts[2], 2);
204}
205
206static void case_a_hash_nobody_announced(void) {
207 ot_hash hash;
208 uint32_t counts[3];
209
210 test_case("a hash nobody has announced scrapes as nothing at all");
211 hash_named(hash, "never-seen");
212 scrape(hash, counts);
213 CHECK_INT((int)counts[0], 0);
214 CHECK_INT((int)counts[1], 0);
215 CHECK_INT((int)counts[2], 0);
216}
217
218void test_trackerlogic(void) {
219 sigset_t signal_mask;
220
221 /* accesslist_init starts a thread that waits for SIGHUP and is stopped by
222 being sent one, so the signal has to be blocked here as the tracker
223 blocks it, or stopping the tracker logic ends this process instead */
224 sigemptyset(&signal_mask);
225 sigaddset(&signal_mask, SIGHUP);
226 pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);
227
228 trackerlogic_init();
229 CHECK_INT(workstruct_init(&ws), 0);
230
231 case_an_announce_makes_a_torrent();
232 case_the_same_peer_twice_is_one_peer();
233 case_a_port_makes_a_different_peer();
234 case_seeding_is_counted_apart();
235 case_completion_without_seeding_is_not_believed();
236 case_completing_is_counted_once();
237 case_a_peer_can_leave();
238 case_the_families_are_kept_apart();
239 case_a_hash_nobody_announced();
240
241 workstruct_deinit(&ws);
242 trackerlogic_deinit();
243}
diff --git a/tests/t_workqueue.c b/tests/t_workqueue.c
new file mode 100644
index 0000000..13e0b2e
--- /dev/null
+++ b/tests/t_workqueue.c
@@ -0,0 +1,107 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <stdlib.h>
8#include <string.h>
9
10/* Opentracker */
11#include "ot_workqueue.h"
12#include "test.h"
13
14/* A worker asks for a class of task and blocks until one turns up, so a task
15 whose class matches no worker is never claimed and whoever is waiting for
16 its answer waits forever. These cases pin the matching. */
17
18static void case_task_goes_to_its_own_class(void) {
19 ot_tasktype asked;
20 ot_taskid taskid;
21
22 test_case("a worker is handed the task of the class it asked for");
23 CHECK_INT(workqueue_pushtask(41, TASK_FULLSCRAPE_TPB_ASCII), 0);
24 asked = TASK_FULLSCRAPE;
25 taskid = workqueue_poptask(&asked);
26 CHECK(taskid != 0);
27
28 test_case("and the full task type, not just its class");
29 CHECK_INT(asked, TASK_FULLSCRAPE_TPB_ASCII);
30 workqueue_canceltask(41);
31}
32
33static void case_flags_survive_the_queue(void) {
34 ot_tasktype asked;
35
36 test_case("flags outside the class mask are carried through");
37 CHECK_INT(workqueue_pushtask(42, TASK_FULLSCRAPE_TPB_URLENCODED | TASK_FLAG_GZIP), 0);
38 asked = TASK_FULLSCRAPE;
39 CHECK(workqueue_poptask(&asked) != 0);
40 CHECK_INT(asked, TASK_FULLSCRAPE_TPB_URLENCODED | TASK_FLAG_GZIP);
41 workqueue_canceltask(42);
42}
43
44static void case_classes_do_not_cross(void) {
45 ot_tasktype asked;
46 ot_taskid taskid;
47
48 test_case("a stats worker passes over a fullscrape task");
49 CHECK_INT(workqueue_pushtask(43, TASK_FULLSCRAPE_TPB_BINARY), 0);
50 CHECK_INT(workqueue_pushtask(44, TASK_STATS_PEERS), 0);
51
52 asked = TASK_STATS;
53 taskid = workqueue_poptask(&asked);
54 CHECK(taskid != 0);
55 CHECK_INT(asked, TASK_STATS_PEERS);
56
57 test_case("and leaves it for the one that wants it");
58 asked = TASK_FULLSCRAPE;
59 taskid = workqueue_poptask(&asked);
60 CHECK(taskid != 0);
61 CHECK_INT(asked, TASK_FULLSCRAPE_TPB_BINARY);
62
63 workqueue_canceltask(43);
64 workqueue_canceltask(44);
65}
66
67static void case_every_task_gets_its_own_id(void) {
68 ot_tasktype asked;
69 ot_taskid first, second;
70
71 test_case("two queued tasks are handed out under different ids");
72 CHECK_INT(workqueue_pushtask(45, TASK_FULLSCRAPE), 0);
73 CHECK_INT(workqueue_pushtask(46, TASK_FULLSCRAPE), 0);
74
75 asked = TASK_FULLSCRAPE;
76 first = workqueue_poptask(&asked);
77 asked = TASK_FULLSCRAPE;
78 second = workqueue_poptask(&asked);
79 CHECK(first != 0);
80 CHECK(second != 0);
81 CHECK(first != second);
82
83 workqueue_canceltask(45);
84 workqueue_canceltask(46);
85}
86
87static void case_cancelled_task_is_not_handed_out(void) {
88 ot_tasktype asked;
89
90 test_case("a cancelled task is gone before any worker sees it");
91 CHECK_INT(workqueue_pushtask(47, TASK_FULLSCRAPE_TPB_ASCII_PLUS), 0);
92 CHECK_INT(workqueue_pushtask(48, TASK_FULLSCRAPE), 0);
93 workqueue_canceltask(47);
94
95 asked = TASK_FULLSCRAPE;
96 CHECK(workqueue_poptask(&asked) != 0);
97 CHECK_INT(asked, TASK_FULLSCRAPE);
98 workqueue_canceltask(48);
99}
100
101void test_workqueue(void) {
102 case_task_goes_to_its_own_class();
103 case_flags_survive_the_queue();
104 case_classes_do_not_cross();
105 case_every_task_gets_its_own_id();
106 case_cancelled_task_is_not_handed_out();
107}
diff --git a/tests/test.h b/tests/test.h
index 0eddc82..d7296cc 100644
--- a/tests/test.h
+++ b/tests/test.h
@@ -87,6 +87,8 @@ void test_buf(void);
87void test_ip(void); 87void test_ip(void);
88void test_socket(void); 88void test_socket(void);
89void test_mutex(void); 89void test_mutex(void);
90void test_trackerlogic(void);
91void test_workqueue(void);
90void test_batch(void); 92void test_batch(void);
91void test_fdstate(void); 93void test_fdstate(void);
92void test_event(void); 94void test_event(void);