Skip to content

Commit 7960ace

Browse files
committed
docs(CLAUDE): Document testing architecture - we never mock tmux
- Added comprehensive testing architecture documentation - Emphasized that all tests use real tmux processes (never mocked) - Documented unique server isolation via socket name generation - Explained pytest fixtures hierarchy and test utilities - Added command execution architecture details - Included testing patterns, doctest integration, and CI matrix - Clear guidelines for writing new tests with real tmux
1 parent 0866211 commit 7960ace

File tree

1 file changed

+306
-0
lines changed

1 file changed

+306
-0
lines changed

CLAUDE.md

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
libtmux is a typed Python library providing an ORM-like interface for tmux (terminal multiplexer). It allows programmatic control of tmux servers, sessions, windows, and panes.
8+
9+
## Development Commands
10+
11+
### Testing
12+
- `make test` - Run full test suite
13+
- `make start` - Run tests then start pytest-watcher
14+
- `make watch_test` - Auto-run tests on file changes (requires entr)
15+
- `uv run pytest tests/path/to/specific_test.py` - Run a specific test file
16+
- `uv run pytest -k "test_name"` - Run tests matching pattern
17+
18+
### Code Quality
19+
- `make ruff` - Run linter checks (must pass before committing)
20+
- `make ruff_format` - Auto-format code
21+
- `make mypy` - Run type checking (must pass before committing)
22+
- `make watch_ruff` - Auto-lint on file changes
23+
- `make watch_mypy` - Auto-typecheck on file changes
24+
25+
### Documentation
26+
- `make build_docs` - Build documentation
27+
- `make serve_docs` - Serve docs locally at http://localhost:8009
28+
- `make dev_docs` - Watch and rebuild docs on changes
29+
30+
## Architecture
31+
32+
### Core Objects Hierarchy
33+
```
34+
Server (tmux server process)
35+
└── Session (tmux session)
36+
└── Window (tmux window)
37+
└── Pane (tmux pane)
38+
```
39+
40+
Each object provides methods to:
41+
- Query and manipulate tmux state
42+
- Execute tmux commands
43+
- Access child objects (e.g., server.sessions, session.windows)
44+
45+
### Key Modules
46+
- `server.py` - Server class for managing tmux server
47+
- `session.py` - Session class for tmux sessions
48+
- `window.py` - Window class for tmux windows
49+
- `pane.py` - Pane class for tmux panes
50+
- `common.py` - Shared utilities and base classes
51+
- `formats.py` - tmux format string handling
52+
- `exc.py` - Custom exceptions
53+
54+
### Internal Architecture
55+
- `_internal/dataclasses.py` - Type-safe data structures for tmux objects
56+
- `_internal/query_list.py` - QueryList implementation for filtering collections
57+
- All tmux commands go through `tmux_cmd()` method on objects
58+
- Uses subprocess to communicate with tmux via CLI
59+
60+
### Command Execution Architecture
61+
62+
Commands flow through a hierarchical delegation pattern:
63+
64+
```
65+
User Code → Object Method → .cmd() → Server.cmd() → tmux_cmd → subprocess → tmux binary
66+
```
67+
68+
**Key components**:
69+
- `tmux_cmd` class (`src/libtmux/common.py:193-268`) - Wraps tmux binary via subprocess
70+
- Each object (Server, Session, Window, Pane) has a `.cmd()` method
71+
- Commands built progressively as tuples with conditional flags
72+
- Auto-targeting: objects automatically include their ID (`-t` flag)
73+
74+
**Example flow**:
75+
```python
76+
session.new_window(window_name='my_window', attach=False)
77+
# → Builds: ("-d", "-P", "-F#{window_id}", "-n", "my_window")
78+
# → Delegates: self.cmd("new-window", *args, target=self.session_id)
79+
# → Executes: tmux -Llibtmux_test3k9m7x2q new-window -d -P -F#{window_id} -n my_window -t$1
80+
```
81+
82+
### Testing Architecture
83+
84+
**CRITICAL: We NEVER mock tmux. All tests use real tmux processes.**
85+
86+
#### Core Testing Principles
87+
88+
1. **Real tmux processes** - Every test runs against actual tmux server via subprocess
89+
2. **Unique isolation** - Each test gets its own tmux server with guaranteed unique socket
90+
3. **No mocking** - All tmux commands execute through real tmux CLI
91+
4. **Parallel-safe** - Tests can run concurrently without conflicts
92+
93+
#### Unique Server Isolation
94+
95+
Each test gets a real tmux server with unique socket name:
96+
97+
```python
98+
Server(socket_name=f"libtmux_test{next(namer)}")
99+
# Example: libtmux_test3k9m7x2q
100+
```
101+
102+
**Socket name generation** (`src/libtmux/test/random.py:28-56`):
103+
- Uses `RandomStrSequence` to generate 8-character random suffixes
104+
- Format: `libtmux_test` + 8 random chars (lowercase, digits, underscore)
105+
- Each socket creates independent tmux process in `/tmp/tmux-{uid}/`
106+
- Negligible collision probability
107+
108+
#### Pytest Fixtures
109+
110+
**Fixture hierarchy** (`src/libtmux/pytest_plugin.py`):
111+
112+
```
113+
Session-scoped (shared across test session):
114+
├── home_path (temporary /home/)
115+
├── user_path (temporary user directory)
116+
└── config_file (~/.tmux.conf with base-index=1)
117+
118+
Function-scoped (per test):
119+
├── set_home (auto-use: sets $HOME to isolated directory)
120+
├── clear_env (cleans unnecessary environment variables)
121+
├── server (unique tmux Server with auto-cleanup)
122+
├── session (Session on server with unique name)
123+
└── TestServer (factory for creating multiple servers per test)
124+
```
125+
126+
**Key fixtures**:
127+
128+
- **`server`** - Creates tmux server with unique socket, auto-killed via finalizer
129+
- **`session`** - Creates session on server with unique name (`libtmux_` + random)
130+
- **`TestServer`** - Factory using `functools.partial` to create multiple independent servers
131+
```python
132+
def test_multiple_servers(TestServer):
133+
server1 = TestServer() # libtmux_test3k9m7x2q
134+
server2 = TestServer() # libtmux_testz9w1b4a7
135+
# Both are real, independent tmux processes
136+
```
137+
138+
#### Isolation Mechanisms
139+
140+
**Triple isolation ensures parallel test safety**:
141+
142+
1. **Unique socket names** - 8-char random suffix prevents collisions
143+
2. **Independent processes** - Each server is separate tmux process with unique PID
144+
3. **Isolated $HOME** - Temporary home directory with standard `.tmux.conf`
145+
146+
**Home directory setup**:
147+
- Each test session gets temporary home directory
148+
- Contains `.tmux.conf` with `base-index 1` for consistent window/pane indexing
149+
- `$HOME` environment variable monkeypatched to isolated directory
150+
- No interference from user's actual tmux configuration
151+
152+
#### Test Utilities
153+
154+
**Helper modules** in `src/libtmux/test/`:
155+
156+
- **`temporary.py`** - Context managers for temporary objects:
157+
```python
158+
with temp_session(server) as session:
159+
session.new_window() # Auto-cleaned up after block
160+
161+
with temp_window(session) as window:
162+
window.split_window() # Auto-cleaned up after block
163+
```
164+
165+
- **`random.py`** - Unique name generation:
166+
```python
167+
get_test_session_name(server) # Returns: libtmux_3k9m7x2q (checks for uniqueness)
168+
get_test_window_name(session) # Returns: libtmux_z9w1b4a7 (checks for uniqueness)
169+
```
170+
171+
- **`retry.py`** - Retry logic for tmux operations:
172+
```python
173+
retry_until(lambda: pane.pane_current_path is not None)
174+
# Retries for up to 8 seconds (configurable via RETRY_TIMEOUT_SECONDS)
175+
# 50ms intervals (configurable via RETRY_INTERVAL_SECONDS)
176+
```
177+
178+
- **`constants.py`** - Test configuration:
179+
- `TEST_SESSION_PREFIX = "libtmux_"`
180+
- `RETRY_TIMEOUT_SECONDS = 8` (configurable via env var)
181+
- `RETRY_INTERVAL_SECONDS = 0.05` (configurable via env var)
182+
183+
#### Doctest Integration
184+
185+
**All doctests use real tmux** (`conftest.py:31-49`):
186+
187+
```python
188+
@pytest.fixture(autouse=True)
189+
def add_doctest_fixtures(doctest_namespace):
190+
# Injects Server, Session, Window, Pane classes
191+
# Injects server, session, window, pane instances
192+
# All are real tmux objects with unique sockets
193+
```
194+
195+
Docstrings can include runnable examples:
196+
```python
197+
>>> server.new_session('my_session')
198+
Session($1 my_session)
199+
200+
>>> session.new_window(window_name='my_window')
201+
Window(@3 2:my_window, Session($1 ...))
202+
```
203+
204+
These execute against real tmux during `pytest --doctest-modules`.
205+
206+
#### Parallel Test Execution
207+
208+
**Tests are safe for parallel execution** (`pytest -n auto`):
209+
210+
- Each worker process generates unique socket names
211+
- No shared state between test workers
212+
- Independent home directories prevent race conditions
213+
- Automatic cleanup prevents resource leaks
214+
215+
#### Testing Patterns
216+
217+
**Standard test pattern**:
218+
```python
219+
def test_example(server: Server, session: Session) -> None:
220+
"""Test description."""
221+
# No setup needed - fixtures provide real tmux objects
222+
window = session.new_window(window_name='test')
223+
assert window.window_name == 'test'
224+
# No teardown needed - fixtures auto-cleanup
225+
```
226+
227+
**Multiple server pattern**:
228+
```python
229+
def test_multiple_servers(TestServer: t.Callable[..., Server]) -> None:
230+
"""Test with multiple independent servers."""
231+
server1 = TestServer()
232+
server2 = TestServer()
233+
# Both are real tmux processes with unique sockets
234+
assert server1.socket_name != server2.socket_name
235+
```
236+
237+
**Retry pattern for tmux operations**:
238+
```python
239+
def test_async_operation(session: Session) -> None:
240+
"""Test operation that takes time to complete."""
241+
pane = session.active_window.active_pane
242+
pane.send_keys('cd /tmp', enter=True)
243+
244+
# Wait for tmux to update pane path
245+
retry_until(lambda: pane.pane_current_path == '/tmp')
246+
```
247+
248+
#### CI Testing Matrix
249+
250+
Tests run against:
251+
- **tmux versions**: 2.6, 2.7, 2.8, 3.0, 3.1, 3.2, 3.3, 3.4, master
252+
- **Python versions**: 3.9, 3.10, 3.11, 3.12, 3.13
253+
- All use real tmux processes (never mocked)
254+
255+
## Important Patterns
256+
257+
### Command Building
258+
- Commands built progressively as tuples with conditional flags:
259+
```python
260+
tmux_args: tuple[str, ...] = ()
261+
262+
if not attach:
263+
tmux_args += ("-d",)
264+
265+
tmux_args += ("-P", "-F#{window_id}")
266+
267+
if window_name is not None:
268+
tmux_args += ("-n", window_name)
269+
270+
cmd = self.cmd("new-window", *tmux_args, target=target)
271+
```
272+
- Auto-targeting: objects pass their ID automatically (override with `target=`)
273+
- Version-aware: use `has_gte_version()` / `has_lt_version()` for compatibility
274+
- Format strings: use `FORMAT_SEPARATOR` (default ``) for multi-field parsing
275+
276+
### Type Safety
277+
- All public APIs are fully typed
278+
- Use `from __future__ import annotations` in all modules
279+
- Mypy runs in strict mode - new code must be type-safe
280+
281+
### Error Handling
282+
- Custom exceptions in `exc.py` (e.g., `LibTmuxException`, `TmuxCommandNotFound`)
283+
- tmux command failures raise exceptions with command output
284+
- Check `cmd.stderr` after command execution
285+
286+
### Vendor Dependencies
287+
- Some dependencies are vendored in `_vendor/` to avoid runtime dependencies
288+
- Do not modify vendored code directly
289+
290+
### Writing Tests
291+
292+
**When writing new tests**:
293+
- Use `server` and `session` fixtures - they provide real tmux instances
294+
- Never mock tmux - use `retry_until()` for async operations instead
295+
- Use `temp_session()` / `temp_window()` context managers for temporary objects
296+
- Use `get_test_session_name()` / `get_test_window_name()` for unique names
297+
- Tests must work across all tmux versions (2.6+) and Python versions (3.9-3.13)
298+
- Use version checks (`has_gte_version`, `has_lt_version`) for version-specific features
299+
300+
**For multiple servers per test**:
301+
```python
302+
def test_example(TestServer):
303+
server1 = TestServer()
304+
server2 = TestServer()
305+
# Both are real, independent tmux processes
306+
```

0 commit comments

Comments
 (0)