Compare commits

...
Author SHA1 Message Date
Vito Sansevero 4d328b08c9 Merge remote-tracking branch 'origin/main' into fix/475-windows-restart-teardown 2026-10-29 06:56:44 -07:00
Vito Sansevero e3c1322151 fix(tests): close the restarted API before teardown unlinks the database
test_a_restart_keeps_every_held_scope_and_cursor_valid registered the
second PromptManagerAPI's close with addAsyncCleanup, which unittest runs
after tearDownAsync. The base teardown then unlinked the temp database
while that API's SQLite connections were still open: Windows refuses
(WinError 32). The restart test class now closes its extra APIs in
tearDownAsync before the base teardown.

ImageAPITestCase's teardown now counts connections any live model still
holds on the test database, closes them and fails, so a handle left open
past teardown fails on Linux too instead of only on Windows.

Closes #475
2026-10-29 06:20:41 -07:00
2 changed files with 58 additions and 11 deletions
+36 -8
View File
@@ -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 ---------------------------------------------------------
+22 -3
View File
@@ -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"]
)