The first push of the branch ran the Tests matrix on all three operating systems. Every Python job failed; the causes were: - numpy missing in CI: the WD14 engine tests import it (requirements-dev) - macOS: temp directories resolve through /private; the admin API tests now use the real path like the gallery roots they compare against - Windows: the startup banner printed an emoji, which raises UnicodeEncodeError on a legacy-code-page console and aborted loading the whole node; the banner is ASCII now and a test loads the package under a cp1252 stdout - Windows: gallery subfolders came back with backslashes, which neither the folder tree nor the search filter understand; they are "/" joined on every platform. A relative DEFAULT_DB_PATH kept its forward slashes after being joined to the extension root; it is normalised now - Windows: sqlite connections opened with `with sqlite3.connect()` are not closed by the context manager and linger until garbage collection, which holds a file lock there. Production sites (vacuum, diagnostics, restore inspection) close explicitly; a restore that swaps the API's database object closes the previous one; PromptModel keeps a registry so a shutdown or a test can close every instance - Windows: served files were still open when their temp directory was removed (tests now read the body), a log download test compared text mode newlines, two tests compared paths with a hard-coded separator tests/open_handles.py makes the Linux run fail the way Windows would: every database test base checks, right before deleting its files, that nothing in the process still holds them open.
44 lines
1.5 KiB
Python
44 lines
1.5 KiB
Python
"""Teardown guard: fail when a file about to be deleted is still open.
|
|
|
|
Windows refuses to delete an open file, so a sqlite connection that was not
|
|
closed explicitly turns into a PermissionError in CI there. Linux would let
|
|
the deletion through silently; this check makes the Linux run catch the same
|
|
mistake. It inspects /proc/self/fd and is a no-op where that does not exist.
|
|
"""
|
|
|
|
import os
|
|
|
|
|
|
def open_handles_under(path):
|
|
"""Basenames of this process's open files at *path* or below it."""
|
|
fd_dir = "/proc/self/fd"
|
|
if not os.path.isdir(fd_dir):
|
|
return []
|
|
real = os.path.realpath(str(path))
|
|
hits = []
|
|
for fd in os.listdir(fd_dir):
|
|
try:
|
|
target = os.readlink(os.path.join(fd_dir, fd))
|
|
except OSError:
|
|
continue
|
|
if not target.startswith(os.sep):
|
|
continue # sockets, pipes, anon inodes
|
|
target = target.replace(" (deleted)", "")
|
|
if (
|
|
target == real
|
|
or target.startswith(real + os.sep)
|
|
or target.startswith(real + "-")
|
|
):
|
|
hits.append(os.path.basename(target))
|
|
return sorted(hits)
|
|
|
|
|
|
def assert_closed(testcase, *paths):
|
|
"""Fail *testcase* if any of *paths* (files or directories) is still open."""
|
|
hits = [hit for path in paths for hit in open_handles_under(path)]
|
|
if hits:
|
|
testcase.fail(
|
|
"Files still open at teardown (Windows could not delete them): "
|
|
+ ", ".join(hits)
|
|
)
|