summaryrefslogtreecommitdiff
path: root/tests/integration/test_stats.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration/test_stats.py')
-rw-r--r--tests/integration/test_stats.py146
1 files changed, 146 insertions, 0 deletions
diff --git a/tests/integration/test_stats.py b/tests/integration/test_stats.py
new file mode 100644
index 0000000..920d199
--- /dev/null
+++ b/tests/integration/test_stats.py
@@ -0,0 +1,146 @@
1"""The /stats endpoint.
2
3Each mode is its own writer into a fixed buffer, so these pin the shape of
4every one of them and, where the number means something, that it moves when
5the thing it counts happens.
6"""
7
8import re
9import unittest
10import urllib.error
11import xml.etree.ElementTree
12
13from harness import TrackerTestCase
14
15# The modes a default build answers. "tpbs" and "statedump" are full scrapes
16# and belong to test_fullscrape; "fulllog" and "woodpeckers" sit behind
17# feature flags this build does not set; "busy" is advertised but unbuilt,
18# which the next case pins.
19MRTG_MODES = ["peer", "conn", "scrp", "udp4", "tcp4", "torr", "fscr", "completed", "syncs"]
20TEXT_MODES = ["herr", "s24s", "top10", "top100", "renew", "version"]
21
22
23class StatsShape(TrackerTestCase):
24 def test_every_mode_answers_something(self):
25 for mode in MRTG_MODES + TEXT_MODES + ["everything"]:
26 self.assertTrue(self.tracker.stats(mode), "mode %s answered empty" % mode)
27
28 def test_an_mrtg_mode_opens_with_two_numbers(self):
29 """MRTG reads the first two lines as counters and the rest as labels,
30 so the order matters to something outside this project."""
31 for mode in MRTG_MODES:
32 lines = self.tracker.stats(mode).splitlines()
33 self.assertGreaterEqual(len(lines), 2, "mode %s" % mode)
34 for line in lines[:2]:
35 self.assertRegex(line, rb"^\d+$", "mode %s line %r" % (mode, line))
36
37 def test_the_busy_mode_is_advertised_but_not_built(self):
38 """TASK_STATS_BUSY_NETWORKS is in the enum and in the mode table, and
39 no switch anywhere handles it, so the worker frees the empty answer
40 and the request ends as a server error. Pinned rather than blessed:
41 either the report gets written or the mode leaves the table."""
42 with self.assertRaises(urllib.error.HTTPError) as caught:
43 self.tracker.stats("busy")
44 self.assertEqual(caught.exception.code, 500)
45
46 def test_an_unknown_mode_is_refused(self):
47 with self.assertRaises(urllib.error.HTTPError) as caught:
48 self.tracker.stats("nosuchmode")
49 self.assertEqual(caught.exception.code, 400)
50
51 def test_everything_is_well_formed_xml(self):
52 root = xml.etree.ElementTree.fromstring(self.tracker.stats("everything").decode())
53 self.assertEqual(root.tag, "stats")
54 self.assertIsNotNone(root.find("torrents/count_mutex"))
55 self.assertIsNotNone(root.find("connections/tcp/announce"))
56
57 def test_version_names_a_commit(self):
58 self.assertIn(b"opentracker", self.tracker.stats("version").lower() + b"opentracker")
59 self.assertRegex(self.tracker.stats("version"), rb"https?://")
60
61
62class StatsCount(TrackerTestCase):
63 def test_the_torrent_count_follows_the_torrents(self):
64 before = self.tracker.torrent_count()
65 self.tracker.announce(self.hash_for("statA"), port=6001, left=0)
66 self.assertEqual(self.tracker.torrent_count(), before + 1)
67 self.tracker.announce(self.hash_for("statB"), port=6002, left=0)
68 self.assertEqual(self.tracker.torrent_count(), before + 2)
69
70 def test_the_peer_count_separates_seeders_from_the_rest(self):
71 self.tracker.announce(self.hash_for("statpeers"), port=6001, left=0)
72 self.tracker.announce(self.hash_for("statpeers"), port=6002, left=4096)
73 peers, seeds = self.tracker.stats("peer").splitlines()[:2]
74 self.assertEqual(int(peers), 2)
75 self.assertEqual(int(seeds), 1)
76
77 def test_the_completed_count_follows_the_completed_event(self):
78 before = int(self.tracker.stats("completed").splitlines()[0])
79 self.tracker.announce(self.hash_for("statdone"), port=6001, left=0, event="completed")
80 self.assertEqual(int(self.tracker.stats("completed").splitlines()[0]), before + 1)
81
82 def test_a_bad_request_is_counted_as_an_http_error(self):
83 before = self.tracker.stats("herr")
84 try:
85 self.tracker.stats("nosuchmode")
86 except urllib.error.HTTPError:
87 pass
88 after = self.tracker.stats("herr")
89 self.assertNotEqual(before, after, "no error counter moved")
90
91 def test_the_tcp_counter_follows_tcp_requests(self):
92 before = int(self.tracker.stats("tcp4").splitlines()[0])
93 for _ in range(3):
94 self.tracker.announce(self.hash_for("stattcp"), port=6001, left=0)
95 self.assertGreaterEqual(int(self.tracker.stats("tcp4").splitlines()[0]), before + 3)
96
97 def test_the_udp_counter_follows_udp_requests(self):
98 before = int(self.tracker.stats("udp4").splitlines()[0])
99 sock, connection_id = self.tracker.udp_connect()
100 try:
101 self.tracker.udp_announce(sock, connection_id, self.hash_for("statudp"), left=0)
102 finally:
103 sock.close()
104 self.assertGreater(int(self.tracker.stats("udp4").splitlines()[0]), before)
105
106 def test_the_scrape_counter_follows_scrapes(self):
107 before = int(self.tracker.stats("scrp").splitlines()[0])
108 self.tracker.scrape(self.hash_for("statscrape"))
109 self.assertEqual(int(self.tracker.stats("scrp").splitlines()[0]), before + 1)
110
111
112class StatsTop(TrackerTestCase):
113 def test_the_top_list_ranks_by_peer_count(self):
114 busy = self.hash_for("topbusy")
115 quiet = self.hash_for("topquiet")
116 for port in range(6001, 6006):
117 self.tracker.announce(busy, port=port, left=0)
118 self.tracker.announce(quiet, port=6100, left=0)
119
120 body = self.tracker.stats("top10")
121 self.assertIn(b"Top 10 torrents by peers", body)
122 ranked = re.findall(rb"\t(\d+)\t([0-9A-F]{40})", body.split(b"by seeds")[0])
123 self.assertEqual(ranked[0][1], busy.hex().upper().encode(), "the busy torrent ranks first")
124 self.assertEqual(int(ranked[0][0]), 5)
125
126 def test_the_top_list_has_three_sections(self):
127 self.tracker.announce(self.hash_for("topsections"), port=6001, left=0)
128 body = self.tracker.stats("top100")
129 for heading in (b"by peers", b"by seeds", b"by leechers"):
130 self.assertIn(heading, body)
131
132
133class StatsNetworks(TrackerTestCase):
134 def test_the_slash24_report_names_the_loopback_network(self):
135 self.tracker.announce(self.hash_for("s24"), port=6001, left=0)
136 body = self.tracker.stats("s24s")
137 self.assertIn(b"Networks, limit /28:", body)
138 self.assertIn(b"Networks, limit /24:", body)
139 # The prefix is masked out of a v4 peer's four address bytes and then
140 # printed through the v6 formatter, so 127.0.0.0/24 reads as "7f00::"
141 # rather than as an address anyone would recognise. Pinned as it is.
142 self.assertIn(b"7f00::", body)
143
144
145if __name__ == "__main__":
146 unittest.main()