diff --git a/tests/test_api_images.py b/tests/test_api_images.py index 0775bcb1..edb3c363 100644 --- a/tests/test_api_images.py +++ b/tests/test_api_images.py @@ -35,6 +35,7 @@ from aiohttp import web # noqa: E402 from aiohttp.test_utils import AioHTTPTestCase # noqa: E402 from PIL import Image # noqa: E402 +from database.models import PromptModel # noqa: E402 from database.operations import PromptDatabase # noqa: E402 import py.api.images as images_module from py.api import gallery_listing # noqa: E402 @@ -92,14 +93,41 @@ class ImageAPITestCase(AioHTTPTestCase): self._anchor_patch.stop() # Windows refuses to unlink a database that still has open handles. self.api.db.close_all() - for path in ( - self._temp_db.name, - self._temp_db.name + "-wal", - self._temp_db.name + "-shm", - ): - if os.path.exists(path): - os.unlink(path) - self._output_tmp.cleanup() + try: + leaked = self._close_leaked_connections() + finally: + for path in ( + self._temp_db.name, + self._temp_db.name + "-wal", + self._temp_db.name + "-shm", + ): + if os.path.exists(path): + os.unlink(path) + self._output_tmp.cleanup() + if leaked: + raise AssertionError( + f"{leaked} connection(s) to the test database were still open at " + "teardown; Windows cannot unlink it (WinError 32). Close every " + "extra PromptDatabase before tearDownAsync, not in a cleanup." + ) + + def _close_leaked_connections(self): + """Close and count connections other models still hold on the temp DB. + + Linux unlinks an open SQLite file, Windows does not; counting here makes + a handle left open past teardown fail on every OS (#475). + """ + key = os.path.normcase(os.path.realpath(self._temp_db.name)) + with PromptModel._instances_lock: + models = list(PromptModel._instances) + leaked = 0 + for model in models: + if model._schema_key() != key: + continue + with model._conn_lock: + leaked += len(model._connections) + model.close_all() + return leaked # -- helpers --------------------------------------------------------- diff --git a/tests/test_api_workspace_restart.py b/tests/test_api_workspace_restart.py index d975c640..d2c80bad 100644 --- a/tests/test_api_workspace_restart.py +++ b/tests/test_api_workspace_restart.py @@ -38,11 +38,25 @@ def _payload(token): class TestRestartKeepsTheOpenPage(ImageAPITestCase): async def get_application(self): + self._restarts = [] app = await super().get_application() for index in range(3): make_png(self.output_dir / f"image_{index}.png") return app + async def tearDownAsync(self): + # Before the base teardown unlinks the database: cleanups run after + # tearDownAsync, and Windows cannot unlink a file still open (#475). + errors = [] + while self._restarts: + try: + await self._restarts.pop()() + except Exception as error: # every closer runs; the first error wins + errors.append(error) + await super().tearDownAsync() + if errors: + raise errors[0] + async def first_page(self, client=None, **params): response = await (client or self.client).get( LISTING, params={"mode": "workspace", "limit": "1", **params} @@ -67,7 +81,7 @@ class TestRestartKeepsTheOpenPage(ImageAPITestCase): api.close_auto_thumbnails() api.db.close_all() - self.addAsyncCleanup(close) + self._restarts.append(close) return api, client async def test_a_restart_keeps_every_held_scope_and_cursor_valid(self): @@ -152,8 +166,13 @@ class TestRestartKeepsTheOpenPage(ImageAPITestCase): other_db = self.comfy_dir / "elsewhere" / "prompts.db" other_db.parent.mkdir() api = PromptManagerAPI(db=PromptDatabase(str(other_db))) - self.addCleanup(api.db.close_all) - self.addCleanup(api.close_auto_thumbnails) + + async def close(): + api.close_auto_thumbnails() + api.db.close_all() + + # Closed before teardown removes the folder that holds its database. + self._restarts.append(close) self.assertNotEqual( api._gallery_root_scope(self.output_dir, 0), page["images"][0]["root_scope"] )