summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-11-05 13:52:17 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-11-05 13:52:17 +0100
commit47293a4f116e04a88b0797bafad0c98b4393b744 (patch)
treeb71d19aa8637382a6d3b453614a6a52a2c1b3245 /tests
parent9a3a01643fc78bf544a1e6f5ca5c7529e2026b25 (diff)
Clamp every elapsed time span at zero, using a set of helper functions
Diffstat (limited to 'tests')
-rw-r--r--tests/runner.c1
-rw-r--r--tests/stubs.c9
-rw-r--r--tests/t_clean.c140
-rw-r--r--tests/test.h1
4 files changed, 145 insertions, 6 deletions
diff --git a/tests/runner.c b/tests/runner.c
index ae2565b..2398991 100644
--- a/tests/runner.c
+++ b/tests/runner.c
@@ -27,6 +27,7 @@ static const struct module modules[] = {{"ot_urlencoded", test_urlencoded},
27 {"ot_socket", test_socket}, 27 {"ot_socket", test_socket},
28 {"ot_mutex", test_mutex}, 28 {"ot_mutex", test_mutex},
29 {"trackerlogic", test_trackerlogic}, 29 {"trackerlogic", test_trackerlogic},
30 {"clean", test_clean},
30 {"ot_workqueue", test_workqueue}, 31 {"ot_workqueue", test_workqueue},
31 {"ot_batch", test_batch}, 32 {"ot_batch", test_batch},
32 {"ot_fdstate", test_fdstate}, 33 {"ot_fdstate", test_fdstate},
diff --git a/tests/stubs.c b/tests/stubs.c
index f7ac679..fd47a68 100644
--- a/tests/stubs.c
+++ b/tests/stubs.c
@@ -26,14 +26,11 @@ long io_trywrite(long d, const char *buf, long len) {
26/* trackerlogic.c is under test, and starting it up starts every module it 26/* trackerlogic.c is under test, and starting it up starts every module it
27 leans on. None of them is driven by these tests, so they are answered here 27 leans on. None of them is driven by these tests, so they are answered here
28 rather than linked in and left to run threads of their own. */ 28 rather than linked in and left to run threads of their own. */
29void clean_init(void) {} 29/* ot_clean.c is in the unit build for t_clean, so its thread control is the
30void clean_deinit(void) {} 30 real thing and only the stats hook it calls is stubbed here. */
31int clean_single_torrent(ot_torrent *torrent) {
32 (void)torrent;
33 return 0;
34}
35void fullscrape_init(void) {} 31void fullscrape_init(void) {}
36void fullscrape_deinit(void) {} 32void fullscrape_deinit(void) {}
33void stats_cleanup(void) {}
37void livesync_init(void) {} 34void livesync_init(void) {}
38void livesync_deinit(void) {} 35void livesync_deinit(void) {}
39void livesync_tell(struct ot_workstruct *ws) { (void)ws; } 36void livesync_tell(struct ot_workstruct *ws) { (void)ws; }
diff --git a/tests/t_clean.c b/tests/t_clean.c
new file mode 100644
index 0000000..7eca9aa
--- /dev/null
+++ b/tests/t_clean.c
@@ -0,0 +1,140 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever. */
3
4#include <arpa/inet.h>
5#include <signal.h>
6#include <pthread.h>
7#include <string.h>
8
9#include "ot_clean.h"
10#include "ot_mutex.h"
11#include "trackerlogic.h"
12#include "test.h"
13
14/* A clock stepped backwards used to leave peer_list->base in the future, so
15 the span the cleaner works from came out negative. It was then added to a
16 peer's age and stored in the single byte that age lives in, where -1 reads
17 as 255 and the next pass threw the whole swarm away. */
18
19static void announce(struct ot_workstruct *ws, ot_hash *hash, uint8_t last_octet, int seeding) {
20 static const uint8_t v4mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff};
21
22 /* The peer list is written into the reply, which workstruct_init leaves
23 for its caller to point somewhere */
24 ws->reply = ws->outbuf;
25
26 memset(&ws->peer, 0, sizeof(ws->peer));
27 memcpy(&ws->peer, v4mapped, sizeof(v4mapped));
28 ((uint8_t *)&ws->peer)[15] = last_octet;
29 OT_SETPORT(&ws->peer, &(uint16_t){6881});
30 if (seeding)
31 OT_PEERFLAG(ws->peer) |= PEER_FLAG_SEEDING;
32 ws->hash = hash;
33 add_peer_to_torrent_and_return_peers(FLAG_TCP, ws, 0);
34}
35
36static size_t peers_in(ot_hash *hash) {
37 char reply[1024];
38 size_t len = return_udp_scrape_for_torrent(*hash, reply);
39 uint32_t seeders, leechers;
40
41 if (len < 12)
42 return 0;
43 memcpy(&seeders, reply, 4);
44 memcpy(&leechers, reply + 8, 4);
45 return (size_t)ntohl(seeders) + (size_t)ntohl(leechers);
46}
47
48static ot_torrent *torrent_of(ot_hash *hash) {
49 ot_vector *bucket = mutex_bucket_lock_by_hash(*hash);
50 ot_torrent *t = (ot_torrent *)bucket->data;
51 ot_torrent *found = NULL;
52 size_t i;
53
54 for (i = 0; i < bucket->size; ++i)
55 if (!memcmp(t[i].hash, hash, sizeof(ot_hash)))
56 found = t + i;
57 mutex_bucket_unlock_by_hash(*hash, 0);
58 return found;
59}
60
61/* What a clock stepped back by `minutes` leaves behind */
62static void step_clock_back(ot_torrent *torrent, int minutes) {
63 torrent->peer_list6->base += minutes;
64 torrent->peer_list4->base += minutes;
65}
66
67/* What the next minute of a well behaved clock looks like */
68static void age_by_a_minute(ot_torrent *torrent) {
69 torrent->peer_list6->base -= 1;
70 torrent->peer_list4->base -= 1;
71}
72
73static void case_a_clock_that_went_back_keeps_the_swarm(void) {
74 struct ot_workstruct ws;
75 ot_hash hash;
76 ot_torrent *torrent;
77
78 test_case("a swarm survives the clock being stepped backwards");
79 memset(hash, 0x31, sizeof(hash));
80 CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0);
81
82 announce(&ws, &hash, 1, 1);
83 announce(&ws, &hash, 2, 0);
84 CHECK_INT(peers_in(&hash), 2);
85
86 torrent = torrent_of(&hash);
87 CHECK(torrent != NULL);
88 step_clock_back(torrent, 5);
89
90 /* The pass that sees the step keeps everybody either way: unclamped it
91 writes a negative span into each peer's age byte, where it reads as 251
92 rather than as a removal. */
93 CHECK_INT(clean_single_torrent(torrent), 0);
94 CHECK_INT(peers_in(&hash), 2);
95
96 /* A minute later, and this is where a poisoned age byte took the swarm:
97 251 plus one minute is past any timeout. */
98 age_by_a_minute(torrent);
99 CHECK_INT(clean_single_torrent(torrent), 0);
100 CHECK_INT(peers_in(&hash), 2);
101
102 workstruct_deinit(&ws);
103}
104
105static void case_an_idle_swarm_still_ages_out(void) {
106 struct ot_workstruct ws;
107 ot_hash hash;
108 ot_torrent *torrent;
109
110 test_case("a swarm nobody touched is still cleaned away");
111 memset(hash, 0x32, sizeof(hash));
112 CHECK_INT(workstruct_init(&ws, G_INBUF_SIZE), 0);
113
114 announce(&ws, &hash, 3, 1);
115 CHECK_INT(peers_in(&hash), 1);
116
117 torrent = torrent_of(&hash);
118 CHECK(torrent != NULL);
119 step_clock_back(torrent, -OT_PEER_TIMEOUT);
120
121 CHECK_INT(clean_single_torrent(torrent), 0);
122 CHECK_INT(peers_in(&hash), 0);
123
124 workstruct_deinit(&ws);
125}
126
127void test_clean(void) {
128 sigset_t hup;
129
130 /* accesslist_deinit sends SIGHUP to its reader, trackerlogic_init starts
131 the accesslist, and the test process must not take it */
132 sigemptyset(&hup);
133 sigaddset(&hup, SIGHUP);
134 pthread_sigmask(SIG_BLOCK, &hup, NULL);
135
136 trackerlogic_init();
137 case_a_clock_that_went_back_keeps_the_swarm();
138 case_an_idle_swarm_still_ages_out();
139 trackerlogic_deinit();
140}
diff --git a/tests/test.h b/tests/test.h
index 82ec178..9b569c7 100644
--- a/tests/test.h
+++ b/tests/test.h
@@ -86,6 +86,7 @@ void test_ip(void);
86void test_socket(void); 86void test_socket(void);
87void test_mutex(void); 87void test_mutex(void);
88void test_trackerlogic(void); 88void test_trackerlogic(void);
89void test_clean(void);
89void test_workqueue(void); 90void test_workqueue(void);
90void test_batch(void); 91void test_batch(void);
91void test_fdstate(void); 92void test_fdstate(void);