test(conftest): reap leaked libtmux_test* sockets after session#23
Closed
test(conftest): reap leaked libtmux_test* sockets after session#23
Conversation
Upstream libtmux's pytest plugin (tmux-python/libtmux#660) creates per-test tmux servers on `libtmux_test<N>` sockets but does not reliably kill the daemons or unlink the socket files on teardown. Over repeated runs, `/tmp/tmux-<uid>/` accumulates stale socket entries indefinitely (observed: 10k+ on a dev machine after normal activity, surfaced by the new `list_servers` tool). Add a session-scoped autouse fixture that, after every pytest run, globs `/tmp/tmux-<uid>/libtmux_test*`, kills live daemons, and unlinks stale socket files. Prefix match keeps the reaper away from the developer's real `default` socket. Errors are suppressed — the cleanup is advisory and any failure is a race with a concurrent xdist worker or a vanished socket, neither actionable. Verified locally: `ls /tmp/tmux-$(id -u) | grep -c libtmux_test` dropped from 10693 to 0 after a single `uv run pytest`. Closes #20
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #23 +/- ##
==========================================
- Coverage 87.01% 86.89% -0.12%
==========================================
Files 38 38
Lines 1710 1725 +15
Branches 201 205 +4
==========================================
+ Hits 1488 1499 +11
- Misses 164 166 +2
- Partials 58 60 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This was referenced Apr 19, 2026
tony
added a commit
to tmux-python/libtmux
that referenced
this pull request
Apr 19, 2026
why: 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 using these fixtures left behind stale `libtmux_test*` entries that accumulated indefinitely — 10k+ were observed on a long-lived dev machine, and downstream consumers (libtmux-mcp, tmuxp) inherited the leak. what: - **Add `_reap_test_server(socket_name)`** helper in `src/libtmux/pytest_plugin.py`. Kills the daemon when alive, then unconditionally unlinks the computed socket path (`$TMUX_TMPDIR/tmux-<uid>/<name>`). Needed because `Server(socket_name=...)` leaves `socket_path` unset, so the helper recomputes the resolution the same way tmux does. - **Delegate both fixture finalizers to the helper.** `server` passes `server.socket_name`; `TestServer` iterates the sockets it tracked in `on_init`. Kill-then-unlink applies uniformly. - **Suppress cleanup-time errors** via `contextlib.suppress` on both the kill and the unlink. A finalizer that raised would replace the real test failure with a cleanup error; and any failure (socket already gone, permissions changed, race with a pytest-xdist worker) is not actionable. - **Regression coverage** in `tests/test_pytest_plugin.py`: boot a real daemon, confirm the socket file exists, reap, assert it is gone; plus no-op paths for a missing socket and for a `None` socket name. Fixes #660 Downstream mitigation tmux-python/libtmux-mcp#23.
tony
added a commit
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.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add a session-scoped autouse fixture in
tests/conftest.pythat reaps leakedlibtmux_test*tmux daemons and socket files under/tmp/tmux-<uid>/after every test run.Upstream libtmux's pytest plugin (tmux-python/libtmux#660) does not reliably kill the per-test tmux daemons or unlink the socket files on teardown. Over repeated runs,
/tmp/tmux-<uid>/accumulates stale entries indefinitely — 10,693 were observed on a dev machine before this fixture ran.Root-cause fix filed upstream at tmux-python/libtmux#661. Once that lands and the downstream
libtmuxdependency is bumped, this local mitigation can be reverted — the upstream fixture finalizers will do the reaping at the source.Until then this provides a local safety net so CI and dev machines stay clean regardless of which
libtmuxversion the working checkout pulls in.Safe properties:
libtmux_test*only — never touches the developer's realdefaultsocket or any non-test socket.session+autouse— runs exactly once after everypytestinvocation without opt-in.xdist(each worker is its own session;kill()andunlink(missing_ok=True)are safe on already-gone sockets).hasattr(os, 'geteuid'); errors suppressed because any failure is a race with a concurrent worker or a vanished socket — neither actionable.Test plan
uv run ruff check . --fix --show-fixesuv run ruff format .uv run mypyuv run py.test --reruns 0 -vvv(363 passed)just build-docsEnd-to-end verification:
ls /tmp/tmux-\$(id -u) | grep -c libtmux_testdropped 10693 → 0 after a singleuv run pyteston this branch.Companion PRs
Part of a batch of post-0.1.0a2 smoke-test fixes. Each PR is independent and can land in any order:
asyncio.to_thread#21 — event-loop block in channel waits (closes wait_for_channel and signal_channel block the FastMCP event loop (sync subprocess.run) #18)is_callersocket-blind false positive (closes is_caller annotation gives false positives across tmux sockets #19)libtmux_test*sockets (this PR, closes list_servers exposes dozens of leaked libtmux_test* tmux daemons after test runs #20)Closes #20