mirror of
https://github.com/simonw/datasette.git
synced 2026-10-19 07:47:00 +02:00
Torture harness: check for leaked file descriptors on macOS too
check_resources() listed /proc/self/fd, which only exists on Linux, so on macOS every scenario crashed at the end with FileNotFoundError. macOS now lists /dev/fd and resolves each descriptor with fcntl F_GETPATH, which, like /proc, still reports files that have been deleted (psutil's open_files() leaves those out). The temp directory is compared via realpath(), since macOS reports /var/folders as /private/var/folders. The test now fails with the harness's stderr when it exits without a summary line, instead of an IndexError that hid the real traceback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
74244c9f41
commit
2bf79d0411
2 changed files with 37 additions and 9 deletions
|
|
@ -295,6 +295,29 @@ class Hang(Exception):
|
|||
pass
|
||||
|
||||
|
||||
def open_file_paths():
|
||||
# Paths of every file this process has open, including files that have
|
||||
# since been deleted (psutil's open_files() leaves those out)
|
||||
paths = []
|
||||
if os.path.isdir("/proc/self/fd"):
|
||||
for fd in os.listdir("/proc/self/fd"):
|
||||
try:
|
||||
paths.append(os.readlink(f"/proc/self/fd/{fd}"))
|
||||
except OSError:
|
||||
continue
|
||||
return paths
|
||||
# macOS: no /proc, but F_GETPATH works on any open descriptor
|
||||
import fcntl
|
||||
|
||||
for fd in os.listdir("/dev/fd"):
|
||||
try:
|
||||
path = fcntl.fcntl(int(fd), fcntl.F_GETPATH, bytes(1024))
|
||||
except OSError:
|
||||
continue
|
||||
paths.append(os.fsdecode(path.rstrip(b"\0")))
|
||||
return paths
|
||||
|
||||
|
||||
def make_file(path, wal=False, extra_sql=""):
|
||||
conn = sqlite3.connect(path)
|
||||
if wal:
|
||||
|
|
@ -1190,16 +1213,15 @@ class Torture:
|
|||
|
||||
def check_resources(self, baseline_threads):
|
||||
deadline = time.monotonic() + 15
|
||||
# On macOS the temp directory is reported via its /private symlink target
|
||||
tmp = os.path.realpath(self.tmp)
|
||||
while True:
|
||||
gc.collect()
|
||||
leaked_fds = []
|
||||
for fd in os.listdir("/proc/self/fd"):
|
||||
try:
|
||||
target = os.readlink(f"/proc/self/fd/{fd}")
|
||||
except OSError:
|
||||
continue
|
||||
if target.startswith(self.tmp) or "datasette_temp_" in target:
|
||||
leaked_fds.append(target)
|
||||
leaked_fds = [
|
||||
target
|
||||
for target in open_file_paths()
|
||||
if target.startswith(tmp) or "datasette_temp_" in target
|
||||
]
|
||||
threads = [
|
||||
t.name
|
||||
for t in threading.enumerate()
|
||||
|
|
|
|||
|
|
@ -58,7 +58,13 @@ def test_connection_torture(scenario, seed):
|
|||
f"{scenario} crashed with {signal.Signals(-proc.returncode).name}:\n"
|
||||
f"{proc.stderr[-4000:]}"
|
||||
)
|
||||
summary = json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
lines = proc.stdout.strip().splitlines()
|
||||
if not lines:
|
||||
pytest.fail(
|
||||
f"{scenario} exited {proc.returncode} without a summary:\n"
|
||||
f"{proc.stderr[-4000:]}"
|
||||
)
|
||||
summary = json.loads(lines[-1])
|
||||
problems = "\n".join(
|
||||
f"[{p['kind']}] {p['message'][:600]}" for p in summary["problems"]
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue