From 2bf79d0411fbc4a93de0f1f51c6ee2ebb07e371c Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 6 Oct 2026 15:08:20 -0700 Subject: [PATCH] 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 --- tests/connection_torture.py | 38 +++++++++++++++++++++++++------- tests/test_connection_torture.py | 8 ++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/tests/connection_torture.py b/tests/connection_torture.py index eaa6e5ca..a5380f90 100644 --- a/tests/connection_torture.py +++ b/tests/connection_torture.py @@ -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() diff --git a/tests/test_connection_torture.py b/tests/test_connection_torture.py index f6a06569..67623c7f 100644 --- a/tests/test_connection_torture.py +++ b/tests/test_connection_torture.py @@ -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"] )