datasette/tests/test_connection_torture.py
Simon Willison 2bf79d0411 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>
2026-10-06 15:08:20 -07:00

75 lines
2.4 KiB
Python

"""
A fast, fixed-seed subset of the connection torture harness
(tests/connection_torture.py). Each scenario runs in its own process, so a
segfault or a hang fails one test instead of killing the test run.
Several event loops (threads) drive one Datasette with extreme settings
(smallest pool, 1ms idle timeout, 10ms schema polling) through reads,
writes of every kind, DDL, isolated functions, analyze_sql, cancellation,
add/remove_database, scratch databases, external schema changes, files
replaced and deleted, Database.close() and Datasette.close() mid-flight,
and non-threaded mode. See the harness docstring for what is checked.
"""
import json
import os
import signal
import subprocess
import sys
import pytest
HARNESS = os.path.join(os.path.dirname(__file__), "connection_torture.py")
@pytest.mark.parametrize(
"scenario,seed",
[
("mixed", 101),
("lifecycle", 102),
("close", 103),
("cancel", 104),
("pool", 105),
("nothreads", 106),
("writes", 107),
],
)
def test_connection_torture(scenario, seed):
try:
proc = subprocess.run(
[
sys.executable,
HARNESS,
scenario,
"--seed",
str(seed),
"--duration",
"1.5",
],
capture_output=True,
text=True,
timeout=150,
check=False,
)
except subprocess.TimeoutExpired as e: # pragma: no cover
pytest.fail(f"{scenario} hung:\n{(e.stderr or '')[-4000:]}")
if proc.returncode < 0: # pragma: no cover
pytest.fail(
f"{scenario} crashed with {signal.Signals(-proc.returncode).name}:\n"
f"{proc.stderr[-4000:]}"
)
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"]
)
assert proc.returncode == 0, f"{scenario} seed {seed}:\n{problems}"
assert summary["ok"]
# It did something (the slow-query scenarios do a few dozen operations
# in 1.5s on a busy machine, the others hundreds)
assert sum(summary["counts"].values()) > 10