aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_root_eject.py128
1 files changed, 128 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_root_eject.py b/packages/meshbay-node/tests/test_root_eject.py
index d73e71c..b6b50aa 100644
--- a/packages/meshbay-node/tests/test_root_eject.py
+++ b/packages/meshbay-node/tests/test_root_eject.py
@@ -264,3 +264,131 @@ async def test_the_ejected_key_is_case_folded(tmp_path):
assert Roster.root_ejected_key("Films") == Roster.root_ejected_key("FILMS")
finally:
await roster.close()
+
+
+# ── The safety net's eject undoes itself; the operator's does not ───────────
+
+def _vanish(films: Path) -> None:
+ for f in films.iterdir():
+ f.unlink()
+ films.rmdir()
+
+
+async def test_an_auto_ejected_root_comes_back_with_its_own_files(tmp_path):
+ """
+ The drive that was not mounted yet when the node started (found on a node
+ started with the session, its USB drives mounted a minute later): the
+ safety net ejected it, and once the same files are readable at the same
+ place it is plugged back without anyone having to.
+ """
+ films = tmp_path / "Films"
+ films.mkdir()
+ (films / "a.mkv").write_bytes(b"a")
+ seen: list[tuple[str, bool]] = []
+
+ async def record(name: str, ejected: bool) -> None:
+ seen.append((name, ejected))
+
+ roots = _roots(films)
+ idx = await _indexer(roots, on_root_ejected=record)
+ hidden = tmp_path / "unmounted"
+ films.rename(hidden)
+ await idx.reconcile()
+ assert roots.roots[0].ejected is True
+
+ hidden.rename(films)
+ await idx.reconcile()
+ assert roots.roots[0].ejected is False
+ assert roots.roots[0].available is True
+ assert _names(idx) == {"a.mkv"}
+ assert seen == [("Films", True), ("Films", False)]
+
+
+@pytest.mark.parametrize("what_came_back", ["empty", "another drive"])
+async def test_an_auto_ejected_root_stays_out_when_its_files_are_not_there(
+ tmp_path, what_came_back):
+ """
+ What the safety net exists for: an empty mount point, or another drive
+ mounted at the same place, is not the library. Plugging it back would
+ rescan it, and the rescan would read the library as erased.
+ """
+ films = tmp_path / "Films"
+ films.mkdir()
+ (films / "a.mkv").write_bytes(b"a")
+ roots = _roots(films)
+ idx = await _indexer(roots)
+ _vanish(films)
+ await idx.reconcile()
+
+ films.mkdir()
+ if what_came_back == "another drive":
+ (films / "other.mkv").write_bytes(b"something else")
+ await idx.reconcile()
+ assert roots.roots[0].ejected is True
+ assert _names(idx) == {"a.mkv"}, "the library was treated as erased"
+
+
+async def test_an_operator_eject_is_never_undone_automatically(tmp_path):
+ films = tmp_path / "Films"
+ films.mkdir()
+ (films / "a.mkv").write_bytes(b"a")
+ roots = _roots(films)
+ idx = await _indexer(roots)
+
+ idx.eject_root("Films")
+ await idx.reconcile()
+ assert roots.roots[0].ejected is True
+
+
+async def test_after_a_restart_the_cache_recognises_the_drive(tmp_path):
+ """
+ A restarted node has no index entry for an ejected root: the index is
+ rebuilt by scanning, and an ejected root is not scanned. What it does have is
+ the hash cache, with the size and mtime of every file it read there.
+ """
+ import os
+
+ from meshbay_node.indexer.cache import IndexCache
+
+ films = tmp_path / "Films"
+ films.mkdir()
+ movie = films / "a.mkv"
+ movie.write_bytes(b"a")
+ st = movie.stat()
+
+ def restarted_ejected() -> RootSet:
+ return RootSet.build([{"path": str(films), "removable": True,
+ "ejected": True, "ejected_auto": True}])
+
+ async with IndexCache(tmp_path / "cache.db") as cache:
+ await _indexer(_roots(films), cache=cache)
+
+ # Another drive at the same place, with a file of the same name.
+ movie.write_bytes(b"another drive")
+ roots = restarted_ejected()
+ await _indexer(roots, cache=cache)
+ assert roots.roots[0].ejected is True
+
+ # The drive itself.
+ movie.write_bytes(b"a")
+ os.utime(movie, ns=(st.st_atime_ns, st.st_mtime_ns))
+ roots = restarted_ejected()
+ idx = await _indexer(roots, cache=cache)
+ assert roots.roots[0].ejected is False
+ assert _names(idx) == {"a.mkv"}
+
+
+async def test_the_roster_keeps_an_auto_eject_apart(tmp_path):
+ roster = Roster(db_path=tmp_path / "roster.db")
+ await roster.open()
+ try:
+ await roster.set_root_ejected("g1", "Films", True, set_by="op", auto=True)
+ await roster.set_root_ejected("g1", "Music", True, set_by="op")
+ assert await roster.ejected_roots("g1") == {"films", "music"}
+ assert await roster.auto_ejected_roots("g1") == {"films"}
+
+ # An operator eject of the same root replaces the safety net's.
+ await roster.set_root_ejected("g1", "Films", True, set_by="op")
+ assert await roster.auto_ejected_roots("g1") == set()
+ finally:
+ await roster.close()