diff options
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_root_eject.py | 128 |
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() |