From d620e4c3ea42b0df6eb5e8dab92d7a8f031fa617 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Thu, 5 Nov 2026 13:59:14 +0100 Subject: Take the clock off the wall --- tests/integration/harness.py | 5 ++-- tests/integration/test_fullscrape.py | 12 ++++---- tests/integration/test_statefile.py | 53 ++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 tests/integration/test_statefile.py (limited to 'tests') diff --git a/tests/integration/harness.py b/tests/integration/harness.py index 10aa6de..e5ec621 100644 --- a/tests/integration/harness.py +++ b/tests/integration/harness.py @@ -77,7 +77,7 @@ def free_port(attempts=50): class Tracker: """One tracker process, listening on loopback.""" - def __init__(self, extra_config="", files=None): + def __init__(self, extra_config="", files=None, extra_args=()): self.port = free_port() # Only used by a config that asks for live sync, but allocated either # way so {livesync_port} always resolves. @@ -99,7 +99,8 @@ class Tracker: # where LeakSanitizer exists and off where it does not. Asking for it # by name makes the tracker refuse to start on macOS and FreeBSD. environment = dict(os.environ, ASAN_OPTIONS="abort_on_error=0") - self.process = subprocess.Popen([TRACKER, "-f", self.config], stdout=self.log, stderr=self.log, env=environment) + argv = [TRACKER, "-f", self.config] + [str(a) for a in extra_args] + self.process = subprocess.Popen(argv, stdout=self.log, stderr=self.log, env=environment) self._wait_for_port() except BaseException: self._cleanup() diff --git a/tests/integration/test_fullscrape.py b/tests/integration/test_fullscrape.py index be7b502..3150612 100644 --- a/tests/integration/test_fullscrape.py +++ b/tests/integration/test_fullscrape.py @@ -78,13 +78,15 @@ class FullScrape(TrackerTestCase): escaped = line.rsplit(b":", 2)[0] self.assertIn(b"%", escaped, "these hashes are mostly unprintable bytes") - def test_the_state_dump_carries_a_base_time_rather_than_a_peer_count(self): - """What a restart reads back, so its second column is the torrent's - own clock and not a count of anybody.""" + def test_the_state_dump_carries_an_age_rather_than_a_peer_count(self): + """What a restart reads back. The second column is how many minutes + ago the torrent was last touched, not a stamp and not a count of + anybody: the clock counts from boot, so an absolute time would mean + nothing to the process that reads it.""" entries = self.entries_from_lines(self.scrape_all("/stats?mode=statedump")) self.assertIn(self.seeding.hex().upper().encode(), entries) - base, completed = entries[self.seeding.hex().upper().encode()] - self.assertGreater(int(base), 0) + age, completed = entries[self.seeding.hex().upper().encode()] + self.assertEqual(int(age), 0, "announced a moment ago") self.assertEqual(int(completed), 0) # ------------------------------------------------------------ shape diff --git a/tests/integration/test_statefile.py b/tests/integration/test_statefile.py new file mode 100644 index 0000000..2a2d147 --- /dev/null +++ b/tests/integration/test_statefile.py @@ -0,0 +1,53 @@ +"""The state dump and the -l switch that reads it back. + +Nothing exercised the load path before, and the dump's second column is now +how long ago a torrent was last touched rather than when, because the clock +counts from boot and an absolute stamp would mean nothing to the process +that reads the file. +""" + +import os +import unittest + +from harness import Tracker, TrackerTestCase + + +class StateFileRoundTrip(TrackerTestCase): + def dump(self): + return self.tracker.get("/stats?mode=statedump") + + def test_a_dumped_torrent_comes_back_with_its_completed_count(self): + info_hash = self.hash_for("state-rt") + self.tracker.announce(info_hash, port=6001, left=0, event="completed") + self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"downloaded"], 1) + + path = os.path.join(self.tracker.dir, "state") + with open(path, "wb") as handle: + handle.write(self.dump()) + + restarted = Tracker(extra_args=["-l", path]) + try: + files = restarted.scrape(info_hash)[b"files"] + self.assertIn(info_hash, files, "the torrent did not survive the restart") + entry = files[info_hash] + self.assertEqual(entry[b"complete"], 0, "no peers are carried over, only the torrent") + self.assertEqual(entry[b"incomplete"], 0) + complaints = restarted.stop() + self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) + finally: + restarted._cleanup() + + def test_a_torrent_older_than_the_tracker_would_keep_is_not_loaded(self): + """An age past the torrent timeout would be cleaned away on the first + sweep, so it is never built in the first place.""" + info_hash = self.hash_for("state-stale") + path = os.path.join(self.tracker.dir, "stale") + with open(path, "w") as handle: + handle.write("%s:%d:7\n" % (info_hash.hex(), 24 * 60 + 1)) + + restarted = Tracker(extra_args=["-l", path]) + try: + self.assertNotIn(info_hash, restarted.scrape(info_hash)[b"files"]) + restarted.stop() + finally: + restarted._cleanup() -- cgit v1.3