datasette/tests/test_fd_leak.py
Simon Willison c0153386ef FD-leak regression test for Datasette.close()
Creates and disposes 50 Datasette instances in a loop and asserts that
the number of open file descriptors and live threads does not grow,
exercising the full close() path end to end.

Refs #2692

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 20:18:05 -07:00

56 lines
1.5 KiB
Python

"""
Regression test for https://github.com/simonw/datasette/issues/2692 —
confirm that creating and closing Datasette instances in a loop does not
leak open file descriptors.
Each Datasette() with is_temp_disk internal DB opens a temp file and a
write thread with its own SQLite connection. Without Datasette.close()
nothing unwinds this state, and a large pytest run exhausts the process
FD limit.
"""
import asyncio
import threading
import pytest
try:
import psutil
except ImportError: # pragma: no cover
psutil = None
from datasette.app import Datasette
def _count_open_files():
return len(psutil.Process().open_files())
def _count_threads():
return threading.active_count()
@pytest.mark.skipif(psutil is None, reason="psutil not installed")
def test_close_releases_file_descriptors():
# Warm-up so Python/library caches don't skew the baseline
ds = Datasette(memory=True)
asyncio.run(ds.invoke_startup())
ds.close()
baseline_fds = _count_open_files()
baseline_threads = _count_threads()
for _ in range(50):
ds = Datasette(memory=True)
asyncio.run(ds.invoke_startup())
ds.close()
after_fds = _count_open_files()
after_threads = _count_threads()
assert (
after_fds - baseline_fds <= 2
), f"Leaked FDs: baseline={baseline_fds}, after=50 iterations={after_fds}"
assert (
after_threads - baseline_threads <= 2
), f"Leaked threads: baseline={baseline_threads}, after={after_threads}"