Skip to content

fix(pytest_plugin): unlink socket file on fixture teardown#661

Merged
tony merged 2 commits intomasterfrom
fix/pytest-plugin-reap-sockets
Apr 19, 2026
Merged

fix(pytest_plugin): unlink socket file on fixture teardown#661
tony merged 2 commits intomasterfrom
fix/pytest-plugin-reap-sockets

Conversation

@tony
Copy link
Copy Markdown
Member

@tony tony commented Apr 19, 2026

Summary

The server and TestServer fixtures in libtmux.pytest_plugin called server.kill() on teardown but never unlinked the socket file under /tmp/tmux-<uid>/. tmux does not reliably unlink(2) its own socket on non-graceful exit, so every test run that used these fixtures left behind stale socket entries that accumulated indefinitely (10k+ observed on a long-lived dev machine; surfaced by libtmux-mcp's list_servers tool as 43 live leaked servers alongside the filesystem cruft).

Introduce _reap_test_server(socket_name) helper that kills the daemon (if alive) and unconditionally unlinks the computed socket path. Server(socket_name=...) does not populate socket_path, so the helper recomputes $TMUX_TMPDIR/tmux-<uid>/<name> the same way tmux does. Both fixture finalizers delegate to it.

Cleanup errors are suppressed via contextlib.suppress so a finalizer failure can never mask the real test failure, and races with pytest-xdist workers stay benign. Kills ride through LibTmuxException / OSError; unlinks use missing_ok=True and swallow OSError.

Test plan

  • uv run ruff check . --fix --show-fixes
  • uv run ruff format .
  • uv run mypy
  • uv run py.test --reruns 0 -vvv (885 passed, 1 skipped)
  • just build-docs

End-to-end verification on this machine: before the fix ls /tmp/tmux-$(id -u) | grep -c libtmux_test was 10693. After the fix, running the full libtmux suite from a clean /tmp/tmux-<uid>/ leaves 0 stale sockets.

New regression tests in tests/test_pytest_plugin.py

  • test_reap_test_server_unlinks_socket_file — boots a real daemon, confirms the socket file exists, calls _reap_test_server, asserts the file is gone.
  • test_reap_test_server_is_noop_when_socket_missing — the finalizer path when a fixture failed before the daemon started.
  • test_reap_test_server_tolerates_none — nullable Server.socket_name argument doesn't crash the reaper.

Downstream

Surfaced by tmux-python/libtmux-mcp#20. That repo has a local mitigation PR that can be reverted once this lands.

Fixes #660

The `server` and `TestServer` fixtures called `server.kill()` on
teardown but never unlinked the socket file under `/tmp/tmux-<uid>/`.
tmux does not reliably `unlink(2)` its own socket on non-graceful exit
— so every test run that used these fixtures left behind stale socket
entries that accumulated indefinitely (10k+ observed on long-lived
dev machines).

Introduce `_reap_test_server(socket_name)` helper that kills the
daemon (if alive) *and* unconditionally unlinks the computed socket
path. `Server(socket_name=...)` does not populate `socket_path`, so
the helper recomputes `$TMUX_TMPDIR/tmux-<uid>/<name>` the same way
tmux does. Both fixture finalizers delegate to it.

Cleanup errors are suppressed via `contextlib.suppress` so a
finalizer failure never masks the real test failure, and races with
pytest-xdist workers (concurrent access to the same directory) stay
benign. Kills ride through `LibTmuxException`/`OSError`; unlinks use
`missing_ok=True` and swallow `OSError`.

Added regression tests:
- `_reap_test_server` unlinks a real socket file after killing a live
  daemon.
- Reaping a never-created socket is a silent no-op.
- Reaping `None` is tolerated for symmetry with `Server.socket_name`'s
  nullable type.

Fixes #660
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 19, 2026

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 46.58%. Comparing base (027c919) to head (0900857).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
src/libtmux/pytest_plugin.py 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #661      +/-   ##
==========================================
+ Coverage   46.55%   46.58%   +0.03%     
==========================================
  Files          22       22              
  Lines        2363     2372       +9     
  Branches      389      390       +1     
==========================================
+ Hits         1100     1105       +5     
- Misses       1094     1098       +4     
  Partials      169      169              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony merged commit 76e938c into master Apr 19, 2026
13 checks passed
@tony tony deleted the fix/pytest-plugin-reap-sockets branch April 19, 2026 18:21
tony added a commit to tmux-python/libtmux-mcp that referenced this pull request Apr 19, 2026
libtmux 0.55.1 ships the pytest-plugin socket-reaper fix
(tmux-python/libtmux#661) so the `server` / `TestServer` fixtures now
kill the tmux daemon AND unlink the socket file under
`/tmp/tmux-<uid>/` on teardown. This bumps the runtime floor and
refreshes uv.lock so CI and fresh installs pick up the fix.

Verified locally: after the bump, `uv run pytest` leaves a single
residual socket per run (down from thousands accumulating across
runs pre-fix). The remaining straggler originates outside the
standard fixture teardown path and is a separate, minor issue —
not a regression.

Supersedes the local conftest reaper proposed in
#23; that PR can now be closed without merge.
tony added a commit to tmux-python/libtmux-mcp that referenced this pull request Apr 19, 2026
why: libtmux 0.55.1 ships the pytest-plugin socket-reaper fix
(tmux-python/libtmux#661). The `server` and `TestServer` fixtures now
kill the tmux daemon *and* unlink the socket file under
`/tmp/tmux-<uid>/` on teardown, eliminating the `libtmux_test*`
accumulation that downstream consumers like this package inherited.
Bumping the floor makes the fix part of the install contract; the
lockfile refresh guarantees CI and fresh installs pick it up.

what:
- **`pyproject.toml`** — dependency specifier `libtmux>=0.55.0,<1.0`
  -> `libtmux>=0.55.1,<1.0`.
- **`uv.lock`** — regenerated; resolved `libtmux==0.55.0` ->
  `libtmux==0.55.1`.
- **`CHANGES`** — unreleased Dependencies section notes the floor
  bump and points at the upstream fix.

Downstream effect: obsoletes #23 (local conftest socket reaper),
which was a workaround for the same leak. Close #23 without merge.

Release notes: https://libtmux.git-pull.com/history.html (libtmux
0.55.1 section).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pytest plugin leaks tmux daemons and socket files (libtmux_test* servers not reaped)

1 participant