summaryrefslogtreecommitdiff
path: root/tests/integration
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-11-06 14:24:04 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-11-06 14:24:04 +0100
commit355c8ae57d582974f4f6a4ba42f635f2253b84a7 (patch)
tree81536ae109b37242217c818ae3a6b2c193fb8ff3 /tests/integration
parent367ff65242ca88fd6435d04a577a43cec5de92fe (diff)
Add a wall clock helper so that statedumps can be written and re-read
Diffstat (limited to 'tests/integration')
-rw-r--r--tests/integration/test_accesslist.py4
-rw-r--r--tests/integration/test_fullscrape.py14
-rw-r--r--tests/integration/test_statefile.py41
3 files changed, 32 insertions, 27 deletions
diff --git a/tests/integration/test_accesslist.py b/tests/integration/test_accesslist.py
index 7d01264..3ed8ddf 100644
--- a/tests/integration/test_accesslist.py
+++ b/tests/integration/test_accesslist.py
@@ -7,6 +7,7 @@ produces: an empty file, and a last line with no newline after it.
7 7
8import os 8import os
9import struct 9import struct
10import time
10import unittest 11import unittest
11 12
12from harness import Tracker, TrackerTestCase 13from harness import Tracker, TrackerTestCase
@@ -109,9 +110,10 @@ class ListIsInPlaceBeforeTheStateFile(TrackerTestCase):
109 files = {"blacklist": listing(BLOCKED)} 110 files = {"blacklist": listing(BLOCKED)}
110 111
111 def test_a_blacklisted_torrent_in_a_state_file_is_not_restored(self): 112 def test_a_blacklisted_torrent_in_a_state_file_is_not_restored(self):
113 touched = int(time.time()) // 60 - 5
112 path = os.path.join(self.tracker.dir, "state") 114 path = os.path.join(self.tracker.dir, "state")
113 with open(path, "w") as handle: 115 with open(path, "w") as handle:
114 handle.write("%s:5:7\n%s:5:7\n" % (BLOCKED.hex(), ALLOWED.hex())) 116 handle.write("%s:%d:7\n%s:%d:7\n" % (BLOCKED.hex(), touched, ALLOWED.hex(), touched))
115 117
116 restarted = Tracker(self.config, self.files, extra_args=["-l", path]) 118 restarted = Tracker(self.config, self.files, extra_args=["-l", path])
117 try: 119 try:
diff --git a/tests/integration/test_fullscrape.py b/tests/integration/test_fullscrape.py
index 3150612..45b3ffb 100644
--- a/tests/integration/test_fullscrape.py
+++ b/tests/integration/test_fullscrape.py
@@ -8,6 +8,7 @@ so these also exercise the chunked transfer the ordinary replies never take.
8 8
9import gzip 9import gzip
10import struct 10import struct
11import time
11import unittest 12import unittest
12 13
13from harness import TrackerTestCase, bdecode 14from harness import TrackerTestCase, bdecode
@@ -78,15 +79,14 @@ class FullScrape(TrackerTestCase):
78 escaped = line.rsplit(b":", 2)[0] 79 escaped = line.rsplit(b":", 2)[0]
79 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") 80 self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes")
80 81
81 def test_the_state_dump_carries_an_age_rather_than_a_peer_count(self): 82 def test_the_state_dump_carries_a_stamp_rather_than_a_peer_count(self):
82 """What a restart reads back. The second column is how many minutes 83 """What a restart reads back. The second column is the minute the
83 ago the torrent was last touched, not a stamp and not a count of 84 torrent was last touched and not a count of anybody, by the wall
84 anybody: the clock counts from boot, so an absolute time would mean 85 clock, which is the only one two runs of the tracker agree on."""
85 nothing to the process that reads it."""
86 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) 86 entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump"))
87 self.assertIn(self.seeding.hex().upper().encode(), entries) 87 self.assertIn(self.seeding.hex().upper().encode(), entries)
88 age, completed = entries[self.seeding.hex().upper().encode()] 88 stamp, completed = entries[self.seeding.hex().upper().encode()]
89 self.assertEqual(int(age), 0, "announced a moment ago") 89 self.assertAlmostEqual(int(stamp), int(time.time()) // 60, delta=1, msg="announced a moment ago")
90 self.assertEqual(int(completed), 0) 90 self.assertEqual(int(completed), 0)
91 91
92 # ------------------------------------------------------------ shape 92 # ------------------------------------------------------------ shape
diff --git a/tests/integration/test_statefile.py b/tests/integration/test_statefile.py
index 21eefae..a3c8d59 100644
--- a/tests/integration/test_statefile.py
+++ b/tests/integration/test_statefile.py
@@ -1,12 +1,12 @@
1"""The state dump and the -l switch that reads it back. 1"""The state dump and the -l switch that reads it back.
2 2
3Nothing exercised the load path before, and the dump's second column is now 3The second column is the minute a torrent was last touched, by the wall
4how long ago a torrent was last touched rather than when, because the clock 4clock, which is what the file has recorded for years and the only clock two
5counts from boot and an absolute stamp would mean nothing to the process 5runs of the tracker agree on.
6that reads the file.
7""" 6"""
8 7
9import os 8import os
9import time
10import unittest 10import unittest
11 11
12from harness import Tracker, TrackerTestCase 12from harness import Tracker, TrackerTestCase
@@ -54,23 +54,26 @@ class StateFileRoundTrip(TrackerTestCase):
54 restarted._cleanup() 54 restarted._cleanup()
55 55
56 56
57class StateFileWithNonsenseAges(TrackerTestCase): 57class StateFileWithNonsenseStamps(TrackerTestCase):
58 """The age column is read as unsigned and handed on as a signed ot_time. 58 """The column is read as unsigned and handed on as a signed ot_time. A
59 A value at or above 2^63 arrived negative, cleared the timeout check, and 59 value at or above 2^63 arrived negative, cleared the timeout check, and
60 put the torrent's base in the future, where the cleaner never reaches it. 60 put the torrent's base in the future, where the cleaner never reaches it.
61 At exactly 2^63 the subtraction itself overflowed.""" 61 At exactly 2^63 the subtraction itself overflowed."""
62 62
63 AGES = { 63 def stamps(self):
64 "overflow": 2**63, 64 now = int(time.time()) // 60
65 "wrapped": 2**63 + 60 * 24, 65 return {
66 "minus-one": 2**64 - 1, 66 "overflow": 2**63,
67 "int64-max": 2**63 - 1, 67 "minus-one": 2**64 - 1,
68 "past-timeout": 60 * 24 + 1, 68 "int64-max": 2**63 - 1,
69 } 69 "in-the-future": now + 60,
70 "past-timeout": now - (24 * 60 + 1),
71 }
70 72
71 def test_no_age_outside_the_timeout_builds_a_torrent(self): 73 def test_no_stamp_outside_the_timeout_builds_a_torrent(self):
72 lines = ["%s:%d:7" % (self.hash_for(name).hex(), age) for name, age in self.AGES.items()] 74 stamps = self.stamps()
73 lines.append("%s:5:7" % self.hash_for("honest").hex()) 75 lines = ["%s:%d:7" % (self.hash_for(name).hex(), stamp) for name, stamp in stamps.items()]
76 lines.append("%s:%d:7" % (self.hash_for("honest").hex(), int(time.time()) // 60 - 5))
74 77
75 path = os.path.join(self.tracker.dir, "nonsense") 78 path = os.path.join(self.tracker.dir, "nonsense")
76 with open(path, "w") as handle: 79 with open(path, "w") as handle:
@@ -78,9 +81,9 @@ class StateFileWithNonsenseAges(TrackerTestCase):
78 81
79 restarted = Tracker(extra_args=["-l", path]) 82 restarted = Tracker(extra_args=["-l", path])
80 try: 83 try:
81 for name in self.AGES: 84 for name, stamp in stamps.items():
82 self.assertNotIn(self.hash_for(name), restarted.scrape(self.hash_for(name))[b"files"], 85 self.assertNotIn(self.hash_for(name), restarted.scrape(self.hash_for(name))[b"files"],
83 "the line with age %d was loaded" % self.AGES[name]) 86 "the line stamped %d was loaded" % stamp)
84 self.assertIn(self.hash_for("honest"), restarted.scrape(self.hash_for("honest"))[b"files"], 87 self.assertIn(self.hash_for("honest"), restarted.scrape(self.hash_for("honest"))[b"files"],
85 "the state file was not read at all") 88 "the state file was not read at all")
86 self.assertEqual(restarted.torrent_count(), 1) 89 self.assertEqual(restarted.torrent_count(), 1)