diff --git a/tests/browser/fast_reset.py b/tests/browser/fast_reset.py index 7baa7789..7374675f 100644 --- a/tests/browser/fast_reset.py +++ b/tests/browser/fast_reset.py @@ -36,6 +36,11 @@ SEED_ON_LIVE_ENV = "PM_TEST_SEED_ON_LIVE" SCRATCH_SCHEMA = "reset_seed" # Linux's shared-memory tmpfs (present in the Playwright image and on runners). MEMORY_ROOT = Path("/dev/shm") +# The largest fixtures' scratch copy is about 8 MB (database plus a WAL that +# autocheckpoints near 4 MB). Docker's default /dev/shm is 64 MB, shared with +# the browsers and the other workers; with less free than this, the scratch +# copy goes on disk instead. +MEMORY_FREE_BYTES = 32 * 1024 * 1024 def _quote(name: str) -> str: @@ -225,7 +230,12 @@ class ScratchSeeder: kernel is writing back the pages they rewrite. """ root = self._memory_root - if root is not None and root.is_dir() and os.access(root, os.W_OK): + if ( + root is not None + and root.is_dir() + and os.access(root, os.W_OK) + and shutil.disk_usage(root).free >= MEMORY_FREE_BYTES + ): return Path(tempfile.mkdtemp(prefix="pm-reset-", dir=root)) self._fallback.mkdir(parents=True, exist_ok=True) return self._fallback diff --git a/tests/browser/folder_trash.py b/tests/browser/folder_trash.py index e1456e05..b1af2282 100644 --- a/tests/browser/folder_trash.py +++ b/tests/browser/folder_trash.py @@ -46,6 +46,9 @@ class _Trash: _trashes: dict[Path, _Trash] = {} +# Trash folders whose server has cleaned up: a reset still running then +# deletes in place instead of starting a thread nobody would stop. +_closed: set[Path] = set() _lock = threading.Lock() @@ -56,15 +59,20 @@ def replace_folder(folder: Path) -> None: directory = folder.parent / TRASH_NAME with _lock: trash = _trashes.get(directory) - if trash is None: + if trash is None and directory not in _closed: trash = _trashes[directory] = _Trash(directory) - trash.discard(folder) + if trash is not None: + trash.discard(folder) + if folder.exists(): + shutil.rmtree(folder) folder.mkdir() def close_trash(parent: Path) -> None: """Stop deleting under ``parent``'s trash and remove it (blocking).""" + directory = Path(parent) / TRASH_NAME with _lock: - trash = _trashes.pop(Path(parent) / TRASH_NAME, None) + _closed.add(directory) + trash = _trashes.pop(directory, None) if trash is not None: trash.close() diff --git a/tests/test_browser_fast_reset.py b/tests/test_browser_fast_reset.py index d773a0f3..36b8ca54 100644 --- a/tests/test_browser_fast_reset.py +++ b/tests/test_browser_fast_reset.py @@ -255,6 +255,21 @@ class TestScratchSeeder(unittest.TestCase): seeder.close() self.assertEqual(list(memory.iterdir()), []) + def test_a_full_memory_root_puts_the_scratch_on_disk(self): + memory = self.root / "memory" + memory.mkdir() + seeder = ScratchSeeder(self.api, self.root / "fallback", memory_root=memory) + self.addCleanup(seeder.close) + full = mock.patch.object( + fast_reset.shutil, + "disk_usage", + return_value=types.SimpleNamespace(free=fast_reset.MEMORY_FREE_BYTES - 1), + ) + with full: + seeder.run(reseed) + self.assertEqual(list(memory.iterdir()), []) + self.assertTrue((self.root / "fallback" / "seed.db").exists()) + def test_close_releases_the_scratch_files(self): self.seeder.run(reseed) self.seeder.close() diff --git a/tests/test_browser_folder_trash.py b/tests/test_browser_folder_trash.py index 5550a635..a8a91a69 100644 --- a/tests/test_browser_folder_trash.py +++ b/tests/test_browser_folder_trash.py @@ -48,5 +48,20 @@ class TestFolderTrash(unittest.TestCase): close_trash(self.root) # idempotent +class TestFolderTrashAfterClose(unittest.TestCase): + def test_a_replacement_after_close_deletes_in_place_without_a_thread(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + close_trash(root) + folder = root / "output" + folder.mkdir() + (folder / "late.png").write_bytes(b"png") + replace_folder(folder) + self.assertEqual(list(folder.iterdir()), []) + self.assertFalse((root / TRASH_NAME).exists()) + names = [t.name for t in threading.enumerate()] + self.assertNotIn("pm-reset-trash", names) + + if __name__ == "__main__": unittest.main()