1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
"""Tests for ops.index_cache_stats / ops.prune_index_cache."""
import types
import pytest
from meshbay_node import ops
from meshbay_node.indexer.cache import IndexCache
from meshbay_node.roots import Root
@pytest.fixture
async def cache(tmp_path):
c = IndexCache(db_path=tmp_path / "index_cache.db")
await c.open()
yield c
await c.close()
def _indexer(roots):
"""A stand-in for DirectoryIndexer — prune_index_cache only ever reads
`.roots`, so a real one (with its watchdog Observer, executor, etc.)
would be pure overhead here."""
return types.SimpleNamespace(roots=roots)
@pytest.mark.asyncio
async def test_stats_with_no_cache_reports_zero():
assert await ops.index_cache_stats({}) == {"count": 0}
@pytest.mark.asyncio
async def test_stats_reports_row_count(cache):
await cache.put("/lib/a.mkv", size=1, mtime=1.0, hash="a", type="video", added_at=1)
assert await ops.index_cache_stats({"index_cache": cache}) == {"count": 1}
@pytest.mark.asyncio
async def test_prune_without_a_cache_raises():
with pytest.raises(ops.OpError):
await ops.prune_index_cache({})
@pytest.mark.asyncio
async def test_prune_removes_a_path_no_group_claims_any_more(cache):
"""A folder no group's roots reference at all any more — every group
that once shared it was deleted, or the root was removed everywhere."""
await cache.put("/gone/orphan.mkv", size=1, mtime=1.0, hash="a",
type="video", added_at=1)
state = {"index_cache": cache, "indexers": {}}
result = await ops.prune_index_cache(state)
assert result == {"status": "pruned", "removed": 1, "kept": 0}
assert await cache.count() == 0
@pytest.mark.asyncio
async def test_prune_keeps_a_path_whose_file_still_exists_under_an_available_root(
tmp_path, cache):
root_dir = tmp_path / "lib"
root_dir.mkdir()
f = root_dir / "still-here.mkv"
f.write_bytes(b"x")
await cache.put(str(f), size=1, mtime=1.0, hash="a", type="video", added_at=1)
root = Root(name="lib", path=root_dir, available=True)
state = {"index_cache": cache, "indexers": {"g": _indexer([root])}}
result = await ops.prune_index_cache(state)
assert result == {"status": "pruned", "removed": 0, "kept": 1}
assert await cache.count() == 1
@pytest.mark.asyncio
async def test_prune_removes_a_path_deleted_from_an_available_root(tmp_path, cache):
root_dir = tmp_path / "lib"
root_dir.mkdir()
gone = root_dir / "deleted.mkv"
# Never written to disk — simulates a file that was there when cached
# and has since been deleted.
await cache.put(str(gone), size=1, mtime=1.0, hash="a", type="video", added_at=1)
root = Root(name="lib", path=root_dir, available=True)
state = {"index_cache": cache, "indexers": {"g": _indexer([root])}}
result = await ops.prune_index_cache(state)
assert result == {"status": "pruned", "removed": 1, "kept": 0}
assert await cache.count() == 0
@pytest.mark.asyncio
async def test_prune_never_touches_a_path_under_an_unavailable_root(tmp_path, cache):
"""
indexer.py's own rule: a root that goes away freezes, it never empties.
A disconnected drive must not have its cache wiped just because the
files can't be verified right now — that would force a full rehash of
the whole drive the moment it comes back, exactly the cost this cache
exists to avoid. Checked with a root directory that does not even exist
on disk (the strongest form of "can't verify") to prove availability,
not on-disk state, is what decides this.
"""
root_dir = tmp_path / "usb" # never created — simulates "unplugged"
unreachable = root_dir / "movie.mkv"
await cache.put(str(unreachable), size=1, mtime=1.0, hash="a",
type="video", added_at=1)
root = Root(name="usb", path=root_dir, available=False)
state = {"index_cache": cache, "indexers": {"g": _indexer([root])}}
result = await ops.prune_index_cache(state)
assert result == {"status": "pruned", "removed": 0, "kept": 1}
assert await cache.count() == 1
@pytest.mark.asyncio
async def test_prune_across_two_groups_sharing_one_root_path(tmp_path, cache):
"""The scenario this whole cache redesign is for: the same folder is a
root of two groups. A path under it must be kept as long as *either*
group's root still resolves it, available."""
root_dir = tmp_path / "shared"
root_dir.mkdir()
f = root_dir / "track.mp3"
f.write_bytes(b"x")
await cache.put(str(f), size=1, mtime=1.0, hash="a", type="audio", added_at=1)
root_a = Root(name="shared", path=root_dir, available=True)
root_b = Root(name="shared", path=root_dir, available=True)
state = {
"index_cache": cache,
"indexers": {"g1": _indexer([root_a]), "g2": _indexer([root_b])},
}
result = await ops.prune_index_cache(state)
assert result == {"status": "pruned", "removed": 0, "kept": 1}
|