From 123b65bf406783a7bc997c76cd7796b15c8bdb14 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Sun, 1 Nov 2026 01:51:42 +0100 Subject: Add some tests for the actual trackerlogic, something I put off for too long --- Makefile | 4 +- ot_accesslist.h | 2 + tests/runner.c | 1 + tests/stubs.c | 33 ++++++- tests/t_trackerlogic.c | 243 +++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.h | 1 + 6 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 tests/t_trackerlogic.c diff --git a/Makefile b/Makefile index 0e5e985..6a5a270 100644 --- a/Makefile +++ b/Makefile @@ -96,8 +96,8 @@ proxy.debug: $(SOURCES_proxy) $(HEADERS) # has to. tests/stubs.c supplies the few symbols those modules reference but # the tests never reach. TESTBIN=tests/runner -TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_urlencoded.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_workqueue.c tests/t_batch.c tests/t_fdstate.c tests/t_event.c tests/t_fmt.c -TEST_UNITS=ot_urlencoded.c ot_fmt.c ot_event.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c ot_workqueue.c +TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_urlencoded.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_trackerlogic.c tests/t_workqueue.c tests/t_batch.c tests/t_fdstate.c tests/t_event.c tests/t_fmt.c +TEST_UNITS=ot_urlencoded.c ot_fmt.c ot_event.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c ot_workqueue.c trackerlogic.c TEST_HEADERS=tests/test.h $(HEADERS) # Tests reach code that sits behind feature flags, so the units under test are # built with those features enabled. A test binary is not a production binary. diff --git a/ot_accesslist.h b/ot_accesslist.h index 0a8bf4c..9c39a7a 100644 --- a/ot_accesslist.h +++ b/ot_accesslist.h @@ -14,6 +14,8 @@ #if defined(WANT_ACCESSLIST_BLACK) || defined(WANT_ACCESSLIST_WHITE) #define WANT_ACCESSLIST +/* Starts a thread that rereads the list when sent SIGHUP, and is stopped by + being sent one, so the caller blocks SIGHUP process wide beforehand */ void accesslist_init(void); void accesslist_deinit(void); int accesslist_hashisvalid(const ot_hash hash); diff --git a/tests/runner.c b/tests/runner.c index dcb009c..97c6d51 100644 --- a/tests/runner.c +++ b/tests/runner.c @@ -28,6 +28,7 @@ static const struct module modules[] = {{"ot_urlencoded", test_urlencoded}, {"ot_ip", test_ip}, {"ot_socket", test_socket}, {"ot_mutex", test_mutex}, + {"trackerlogic", test_trackerlogic}, {"ot_workqueue", test_workqueue}, {"ot_batch", test_batch}, {"ot_fdstate", test_fdstate}, 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 @@ behaviour should be supplied here rather than the test weakened. */ /* Opentracker */ +#include "ot_stats.h" #include "trackerlogic.h" -/* ot_vector.c calls this from vector_clean_list, which no test drives yet. */ -void free_peerlist(ot_peerlist *peer_list) { (void)peer_list; } - /* ot_accesslist.c is built with WANT_ACCESSLIST_BLACK so that the hash parser is reachable. These two belong to accesslist_readfile and the reload worker, neither of which any test drives. */ @@ -26,3 +24,32 @@ long io_trywrite(long d, const char *buf, long len) { (void)buf; return len; } + +/* trackerlogic.c is under test, and starting it up starts every module it + leans on. None of them is driven by these tests, so they are answered here + rather than linked in and left to run threads of their own. */ +void clean_init(void) {} +void clean_deinit(void) {} +int clean_single_torrent(ot_torrent *torrent) { + (void)torrent; + return 0; +} +void fullscrape_init(void) {} +void fullscrape_deinit(void) {} +void livesync_init(void) {} +void livesync_deinit(void) {} +void livesync_tell(struct ot_workstruct *ws) { (void)ws; } +void stats_init(void) {} +void stats_deinit(void) {} +void stats_issue_event(struct ot_workstruct *ws, ot_status_event event, PROTO_FLAG proto, uintptr_t event_data) { + (void)ws; + (void)event; + (void)proto; + (void)event_data; +} +void stats_counters_register(struct ot_workstruct *ws) { (void)ws; } +void stats_counters_retire(struct ot_workstruct *ws) { (void)ws; } + +const char *g_stats_path; +ssize_t g_stats_path_len; +uint32_t g_tracker_id; 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 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include +#include +#include + +/* Opentracker */ +#include "ot_mutex.h" +#include "trackerlogic.h" +#include "test.h" + +/* The swarm itself: what an announce does to a torrent, and what a scrape + says about it afterwards. The counts are read back through the udp scrape + because that is the one reply the tracker builds as plain numbers. */ + +static struct ot_workstruct ws; + +static void hash_named(ot_hash hash, const char *name) { + memset(hash, 0, sizeof(ot_hash)); + memcpy(hash, name, strlen(name) < sizeof(ot_hash) ? strlen(name) : sizeof(ot_hash)); +} + +/* An IPv4 peer arrives as the v4 mapped form the tracker stores */ +static void peer_v4(ot_peer6 peer, uint8_t last, uint16_t port) { + ot_ip6 ip = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 10, 0, 0, 0}; + uint16_t net_port = htons(port); + + ip[15] = last; + memset(peer, 0, sizeof(ot_peer6)); + OT_SETIP(peer, ip); + OT_SETPORT(peer, &net_port); + OT_PEERFLAG(peer) = 0; +} + +static void peer_v6(ot_peer6 peer, uint8_t last, uint16_t port) { + ot_ip6 ip = {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + uint16_t net_port = htons(port); + + ip[15] = last; + memset(peer, 0, sizeof(ot_peer6)); + OT_SETIP(peer, ip); + OT_SETPORT(peer, &net_port); + OT_PEERFLAG(peer) = 0; +} + +static void announce(ot_hash *hash, ot_peer6 peer) { + ws.hash = hash; + memcpy(ws.peer, peer, sizeof(ot_peer6)); + ws.reply = ws.outbuf; + add_peer_to_torrent_and_return_peers(FLAG_TCP, &ws, 50); +} + +/* seeders, downloads completed, leechers */ +static void scrape(ot_hash hash, uint32_t counts[3]) { + char reply[12]; + + return_udp_scrape_for_torrent(hash, reply); + counts[0] = ntohl(((uint32_t *)reply)[0]); + counts[1] = ntohl(((uint32_t *)reply)[1]); + counts[2] = ntohl(((uint32_t *)reply)[2]); +} + +static void case_an_announce_makes_a_torrent(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + size_t before = mutex_get_torrent_count(); + + test_case("the first announce for a hash creates its torrent"); + hash_named(hash, "first-announce"); + peer_v4(peer, 1, 6881); + announce(&hash, peer); + + CHECK_INT((int)(mutex_get_torrent_count() - before), 1); + scrape(hash, counts); + CHECK_INT((int)counts[2], 1); /* one leecher */ + CHECK_INT((int)counts[0], 0); +} + +static void case_the_same_peer_twice_is_one_peer(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + + test_case("announcing twice renews a peer rather than adding another"); + hash_named(hash, "renewing"); + peer_v4(peer, 2, 6881); + announce(&hash, peer); + announce(&hash, peer); + + scrape(hash, counts); + CHECK_INT((int)counts[2], 1); +} + +static void case_a_port_makes_a_different_peer(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + + test_case("the same address on another port is another peer"); + hash_named(hash, "two-ports"); + peer_v4(peer, 3, 6881); + announce(&hash, peer); + peer_v4(peer, 3, 6882); + announce(&hash, peer); + + scrape(hash, counts); + CHECK_INT((int)counts[2], 2); +} + +static void case_seeding_is_counted_apart(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + + test_case("a seeder counts as complete, a leecher as incomplete"); + hash_named(hash, "seed-and-leech"); + peer_v4(peer, 4, 6881); + OT_PEERFLAG(peer) |= PEER_FLAG_SEEDING; + announce(&hash, peer); + peer_v4(peer, 5, 6881); + announce(&hash, peer); + + scrape(hash, counts); + CHECK_INT((int)counts[0], 1); + CHECK_INT((int)counts[2], 1); +} + +static void case_completion_without_seeding_is_not_believed(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + + test_case("a peer that claims to have completed but is still leeching is not counted"); + hash_named(hash, "claiming"); + peer_v4(peer, 6, 6881); + OT_PEERFLAG(peer) |= PEER_FLAG_COMPLETED; + announce(&hash, peer); + + scrape(hash, counts); + CHECK_INT((int)counts[1], 0); + CHECK_INT((int)counts[2], 1); /* still a leecher */ +} + +static void case_completing_is_counted_once(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + + test_case("a seeder reporting completion is counted once, however often it says so"); + hash_named(hash, "completing"); + peer_v4(peer, 7, 6881); + OT_PEERFLAG(peer) |= PEER_FLAG_COMPLETED | PEER_FLAG_SEEDING; + announce(&hash, peer); + announce(&hash, peer); + announce(&hash, peer); + + scrape(hash, counts); + CHECK_INT((int)counts[1], 1); + CHECK_INT((int)counts[0], 1); +} + +static void case_a_peer_can_leave(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + + test_case("a peer that stops is gone from the swarm"); + hash_named(hash, "leaving"); + peer_v4(peer, 7, 6881); + announce(&hash, peer); + peer_v4(peer, 8, 6881); + announce(&hash, peer); + scrape(hash, counts); + CHECK_INT((int)counts[2], 2); + + ws.hash = &hash; + memcpy(ws.peer, peer, sizeof(ot_peer6)); + remove_peer_from_torrent(FLAG_TCP, &ws); + + scrape(hash, counts); + CHECK_INT((int)counts[2], 1); +} + +static void case_the_families_are_kept_apart(void) { + ot_hash hash; + ot_peer6 peer; + uint32_t counts[3]; + + test_case("a v4 and a v6 peer of one swarm are both counted"); + hash_named(hash, "dual-stack"); + peer_v4(peer, 9, 6881); + announce(&hash, peer); + peer_v6(peer, 9, 6881); + announce(&hash, peer); + + scrape(hash, counts); + CHECK_INT((int)counts[2], 2); +} + +static void case_a_hash_nobody_announced(void) { + ot_hash hash; + uint32_t counts[3]; + + test_case("a hash nobody has announced scrapes as nothing at all"); + hash_named(hash, "never-seen"); + scrape(hash, counts); + CHECK_INT((int)counts[0], 0); + CHECK_INT((int)counts[1], 0); + CHECK_INT((int)counts[2], 0); +} + +void test_trackerlogic(void) { + sigset_t signal_mask; + + /* accesslist_init starts a thread that waits for SIGHUP and is stopped by + being sent one, so the signal has to be blocked here as the tracker + blocks it, or stopping the tracker logic ends this process instead */ + sigemptyset(&signal_mask); + sigaddset(&signal_mask, SIGHUP); + pthread_sigmask(SIG_BLOCK, &signal_mask, NULL); + + trackerlogic_init(); + CHECK_INT(workstruct_init(&ws), 0); + + case_an_announce_makes_a_torrent(); + case_the_same_peer_twice_is_one_peer(); + case_a_port_makes_a_different_peer(); + case_seeding_is_counted_apart(); + case_completion_without_seeding_is_not_believed(); + case_completing_is_counted_once(); + case_a_peer_can_leave(); + case_the_families_are_kept_apart(); + case_a_hash_nobody_announced(); + + workstruct_deinit(&ws); + trackerlogic_deinit(); +} diff --git a/tests/test.h b/tests/test.h index 7582dab..d7296cc 100644 --- a/tests/test.h +++ b/tests/test.h @@ -87,6 +87,7 @@ void test_buf(void); void test_ip(void); void test_socket(void); void test_mutex(void); +void test_trackerlogic(void); void test_workqueue(void); void test_batch(void); void test_fdstate(void); -- cgit v1.3