182 lines
5.5 KiB
Python
182 lines
5.5 KiB
Python
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from archive_clients.eviction import (
|
|
remove_qb_entry,
|
|
safe_unlink,
|
|
verify_and_snapshot,
|
|
)
|
|
from archive_clients.resources import NormalizedResource
|
|
from archive_clients.state import ClientStore
|
|
from archive_control.v1 import resource_pb2
|
|
|
|
|
|
def normalized(torrent_hash, paths):
|
|
summary = resource_pb2.ResourceSummary(
|
|
qb_torrent_id=torrent_hash,
|
|
display_name="resource",
|
|
total_file_count=len(paths),
|
|
canonical_paths=True,
|
|
)
|
|
summary.resource_id.info_hash_v1_hex = torrent_hash
|
|
files = []
|
|
for index, path in enumerate(paths):
|
|
size = len(path) + 10
|
|
files.append(resource_pb2.TorrentFile(
|
|
file_index=index,
|
|
canonical_path=path,
|
|
logical_bytes=size,
|
|
completed_bytes=size,
|
|
selected=True,
|
|
))
|
|
summary.selected_files.ranges.add(first=index, last=index)
|
|
summary.selected_complete_files.ranges.add(first=index, last=index)
|
|
return NormalizedResource(summary, tuple(files), None)
|
|
|
|
|
|
class FakeQB:
|
|
def __init__(self, evicted, remaining):
|
|
self.evicted = evicted
|
|
self.remaining = remaining
|
|
self.deleted = []
|
|
|
|
def get_resource(self, torrent_hash):
|
|
return (
|
|
self.evicted
|
|
if self.evicted
|
|
and self.evicted.summary.qb_torrent_id == torrent_hash
|
|
else None
|
|
)
|
|
|
|
def delete_entry(self, torrent_hash):
|
|
self.deleted.append(torrent_hash)
|
|
self.evicted = None
|
|
|
|
def list_resources(self):
|
|
return list(self.remaining)
|
|
|
|
|
|
class EvictionTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp.name) / "qb"
|
|
self.root.mkdir()
|
|
self.store = ClientStore(Path(self.temp.name) / "state.db")
|
|
self.store.initialize()
|
|
self.store.save_job(
|
|
"job-1", '{"jobId":"job-1"}', "JOB_STATE_RUNNING", 1, 0, False
|
|
)
|
|
|
|
def tearDown(self):
|
|
self.temp.cleanup()
|
|
|
|
def test_shared_paths_and_unknown_files_survive(self):
|
|
evicted = normalized("a" * 40, ["tree/shared.bin", "tree/owned.bin"])
|
|
other = normalized("b" * 40, ["tree/shared.bin"])
|
|
for item in evicted.files:
|
|
path = self.root / item.canonical_path
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(b"x" * item.logical_bytes)
|
|
unknown = self.root / "tree" / "from-another-app.txt"
|
|
unknown.write_text("keep")
|
|
qb = FakeQB(evicted, [other])
|
|
|
|
verify_and_snapshot(
|
|
job_id="job-1",
|
|
resource=evicted,
|
|
selected_indices=[0, 1],
|
|
qb_root=self.root,
|
|
store=self.store,
|
|
)
|
|
remove_qb_entry(
|
|
job_id="job-1",
|
|
torrent_hash="a" * 40,
|
|
qbittorrent=qb,
|
|
store=self.store,
|
|
)
|
|
result = safe_unlink(
|
|
job_id="job-1",
|
|
qb_root=self.root,
|
|
qbittorrent=qb,
|
|
store=self.store,
|
|
)
|
|
self.assertTrue((self.root / "tree/shared.bin").exists())
|
|
self.assertFalse((self.root / "tree/owned.bin").exists())
|
|
self.assertTrue(unknown.exists())
|
|
self.assertTrue((self.root / "tree").is_dir())
|
|
self.assertEqual(qb.deleted, ["a" * 40])
|
|
self.assertEqual(
|
|
result["retained_files"],
|
|
[{"path": "tree/shared.bin", "reason": "shared"}],
|
|
)
|
|
|
|
def test_changed_inode_is_not_unlinked(self):
|
|
evicted = normalized("a" * 40, ["tree/owned.bin"])
|
|
path = self.root / "tree/owned.bin"
|
|
path.parent.mkdir(parents=True)
|
|
path.write_bytes(b"x" * evicted.files[0].logical_bytes)
|
|
qb = FakeQB(evicted, [])
|
|
verify_and_snapshot(
|
|
job_id="job-1",
|
|
resource=evicted,
|
|
selected_indices=[0],
|
|
qb_root=self.root,
|
|
store=self.store,
|
|
)
|
|
path.unlink()
|
|
path.write_bytes(b"y" * evicted.files[0].logical_bytes)
|
|
remove_qb_entry(
|
|
job_id="job-1",
|
|
torrent_hash="a" * 40,
|
|
qbittorrent=qb,
|
|
store=self.store,
|
|
)
|
|
result = safe_unlink(
|
|
job_id="job-1",
|
|
qb_root=self.root,
|
|
qbittorrent=qb,
|
|
store=self.store,
|
|
)
|
|
self.assertTrue(path.exists())
|
|
self.assertEqual(
|
|
result["retained_files"][0]["reason"], "identity-changed"
|
|
)
|
|
|
|
def test_retry_is_idempotent(self):
|
|
evicted = normalized("a" * 40, ["owned.bin"])
|
|
path = self.root / "owned.bin"
|
|
path.write_bytes(b"x" * evicted.files[0].logical_bytes)
|
|
qb = FakeQB(evicted, [])
|
|
verify_and_snapshot(
|
|
job_id="job-1",
|
|
resource=evicted,
|
|
selected_indices=[0],
|
|
qb_root=self.root,
|
|
store=self.store,
|
|
)
|
|
remove_qb_entry(
|
|
job_id="job-1",
|
|
torrent_hash="a" * 40,
|
|
qbittorrent=qb,
|
|
store=self.store,
|
|
)
|
|
first = safe_unlink(
|
|
job_id="job-1",
|
|
qb_root=self.root,
|
|
qbittorrent=qb,
|
|
store=self.store,
|
|
)
|
|
second = safe_unlink(
|
|
job_id="job-1",
|
|
qb_root=self.root,
|
|
qbittorrent=qb,
|
|
store=self.store,
|
|
)
|
|
self.assertEqual(first, second)
|
|
self.assertEqual(qb.deleted, ["a" * 40])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|