Skip to content

Commit db47652

Browse files
authored
feat(waiter): Add terminal content waiting utility for testing (#582)
why: Improve the reliability and expressiveness of tests that interact with terminal output by providing a robust API for waiting on specific content to appear in tmux panes. what: - Added new `waiter.py` module with fluent, Playwright-inspired API for terminal content waiting - Implemented multiple match types: exact, contains, regex, and custom predicates - Added composable waiting functions for complex conditions (any/all) - Created comprehensive test suite with examples and edge cases - Extended retry functionality with improved error handling - Added detailed documentation with usage examples - Updated mypy configuration for test examples - Added timeout handling with configurable behavior This feature enables more reliable testing of terminal applications by providing tools to synchronize test steps with terminal content changes, reducing flaky tests and making assertions more predictable. Closes #579, Resolves #373
2 parents 76326f3 + 50081b5 commit db47652

23 files changed

+4556
-0
lines changed

CHANGES

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ $ pip install --user --upgrade --pre libtmux
1515

1616
- _Future release notes will be placed here_
1717

18+
### New features
19+
20+
#### Waiting (#582)
21+
22+
Added experimental `waiter.py` module for polling for terminal content in tmux panes:
23+
24+
- Fluent API inspired by Playwright for better readability and chainable options
25+
- Support for multiple pattern types (exact text, contains, regex, custom predicates)
26+
- Composable waiting conditions with `wait_for_any_content` and `wait_for_all_content`
27+
- Enhanced error handling with detailed timeouts and match information
28+
- Robust shell prompt detection
29+
1830
## libtmux 0.46.0 (2025-02-25)
1931

2032
### Breaking

docs/internals/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ If you need an internal API stabilized please [file an issue](https://github.com
1111
```{toctree}
1212
dataclasses
1313
query_list
14+
waiter
1415
```
1516

1617
## Environmental variables

docs/internals/waiter.md

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
(waiter)=
2+
3+
# Waiters - `libtmux._internal.waiter`
4+
5+
The waiter module provides utilities for waiting on specific content to appear in tmux panes, making it easier to write reliable tests that interact with terminal output.
6+
7+
## Key Features
8+
9+
- **Fluent API**: Playwright-inspired chainable API for expressive, readable test code
10+
- **Multiple Match Types**: Wait for exact matches, substring matches, regex patterns, or custom predicate functions
11+
- **Composable Waiting**: Wait for any of multiple conditions or all conditions to be met
12+
- **Flexible Timeout Handling**: Configure timeout behavior and error handling to suit your needs
13+
- **Shell Prompt Detection**: Easily wait for shell readiness with built-in prompt detection
14+
- **Robust Error Handling**: Improved exception handling and result reporting
15+
- **Clean Code**: Well-formatted, linted code with proper type annotations
16+
17+
## Basic Concepts
18+
19+
When writing tests that interact with tmux sessions and panes, it's often necessary to wait for specific content to appear before proceeding with the next step. The waiter module provides a set of functions to help with this.
20+
21+
There are multiple ways to match content:
22+
- **Exact match**: The content exactly matches the specified string
23+
- **Contains**: The content contains the specified string
24+
- **Regex**: The content matches the specified regular expression
25+
- **Predicate**: A custom function that takes the pane content and returns a boolean
26+
27+
## Quick Start Examples
28+
29+
### Simple Waiting
30+
31+
Wait for specific text to appear in a pane:
32+
33+
```{literalinclude} ../../tests/examples/_internal/waiter/test_wait_for_text.py
34+
:language: python
35+
```
36+
37+
### Advanced Matching
38+
39+
Use regex patterns or custom predicates for more complex matching:
40+
41+
```{literalinclude} ../../tests/examples/_internal/waiter/test_wait_for_regex.py
42+
:language: python
43+
```
44+
45+
```{literalinclude} ../../tests/examples/_internal/waiter/test_custom_predicate.py
46+
:language: python
47+
```
48+
49+
### Timeout Handling
50+
51+
Control how long to wait and what happens when a timeout occurs:
52+
53+
```{literalinclude} ../../tests/examples/_internal/waiter/test_timeout_handling.py
54+
:language: python
55+
```
56+
57+
### Waiting for Shell Readiness
58+
59+
A common use case is waiting for a shell prompt to appear, indicating the command has completed. The example below uses a regular expression to match common shell prompt characters (`$`, `%`, `>`, `#`):
60+
61+
```{literalinclude} ../../tests/examples/_internal/waiter/test_wait_until_ready.py
62+
:language: python
63+
```
64+
65+
> Note: This test is skipped in CI environments due to timing issues but works well for local development.
66+
67+
## Fluent API (Playwright-inspired)
68+
69+
For a more expressive and chainable API, you can use the fluent interface provided by the `PaneContentWaiter` class:
70+
71+
```{literalinclude} ../../tests/examples/_internal/waiter/test_fluent_basic.py
72+
:language: python
73+
```
74+
75+
```{literalinclude} ../../tests/examples/_internal/waiter/test_fluent_chaining.py
76+
:language: python
77+
```
78+
79+
## Multiple Conditions
80+
81+
The waiter module also supports waiting for multiple conditions at once:
82+
83+
```{literalinclude} ../../tests/examples/_internal/waiter/test_wait_for_any_content.py
84+
:language: python
85+
```
86+
87+
```{literalinclude} ../../tests/examples/_internal/waiter/test_wait_for_all_content.py
88+
:language: python
89+
```
90+
91+
```{literalinclude} ../../tests/examples/_internal/waiter/test_mixed_pattern_types.py
92+
:language: python
93+
```
94+
95+
## Implementation Notes
96+
97+
### Error Handling
98+
99+
The waiting functions are designed to be robust and handle timing and error conditions gracefully:
100+
101+
- All wait functions properly calculate elapsed time for performance tracking
102+
- Functions handle exceptions consistently and provide clear error messages
103+
- Proper handling of return values ensures consistent behavior whether or not raises=True
104+
105+
### Type Safety
106+
107+
The waiter module is fully type-annotated to ensure compatibility with static type checkers:
108+
109+
- All functions include proper type hints for parameters and return values
110+
- The ContentMatchType enum ensures that only valid match types are used
111+
- Combined with runtime checks, this prevents type-related errors during testing
112+
113+
### Example Usage in Documentation
114+
115+
All examples in this documentation are actual test files from the libtmux test suite. The examples are included using `literalinclude` directives, ensuring that the documentation remains synchronized with the actual code.
116+
117+
## API Reference
118+
119+
```{eval-rst}
120+
.. automodule:: libtmux._internal.waiter
121+
:members:
122+
:undoc-members:
123+
:show-inheritance:
124+
:member-order: bysource
125+
```
126+
127+
## Extended Retry Functionality
128+
129+
```{eval-rst}
130+
.. automodule:: libtmux.test.retry_extended
131+
:members:
132+
:undoc-members:
133+
:show-inheritance:
134+
:member-order: bysource
135+
```

pyproject.toml

+5
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ files = [
128128
"tests",
129129
]
130130

131+
[[tool.mypy.overrides]]
132+
module = "tests.examples.*"
133+
disallow_untyped_defs = false
134+
disallow_incomplete_defs = false
135+
131136
[tool.coverage.run]
132137
branch = true
133138
parallel = true
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Extended retry functionality for libtmux."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
import time
7+
import typing as t
8+
9+
from libtmux.exc import WaitTimeout
10+
from libtmux.test.constants import (
11+
RETRY_INTERVAL_SECONDS,
12+
RETRY_TIMEOUT_SECONDS,
13+
)
14+
15+
logger = logging.getLogger(__name__)
16+
17+
if t.TYPE_CHECKING:
18+
from collections.abc import Callable
19+
20+
21+
def retry_until_extended(
22+
fun: Callable[[], bool],
23+
seconds: float = RETRY_TIMEOUT_SECONDS,
24+
*,
25+
interval: float = RETRY_INTERVAL_SECONDS,
26+
raises: bool | None = True,
27+
) -> tuple[bool, Exception | None]:
28+
"""
29+
Retry a function until a condition meets or the specified time passes.
30+
31+
Extended version that returns both success state and exception.
32+
33+
Parameters
34+
----------
35+
fun : callable
36+
A function that will be called repeatedly until it returns ``True`` or
37+
the specified time passes.
38+
seconds : float
39+
Seconds to retry. Defaults to ``8``, which is configurable via
40+
``RETRY_TIMEOUT_SECONDS`` environment variables.
41+
interval : float
42+
Time in seconds to wait between calls. Defaults to ``0.05`` and is
43+
configurable via ``RETRY_INTERVAL_SECONDS`` environment variable.
44+
raises : bool
45+
Whether or not to raise an exception on timeout. Defaults to ``True``.
46+
47+
Returns
48+
-------
49+
tuple[bool, Exception | None]
50+
Tuple containing (success, exception). If successful, the exception will
51+
be None.
52+
"""
53+
ini = time.time()
54+
exception = None
55+
56+
while not fun():
57+
end = time.time()
58+
if end - ini >= seconds:
59+
timeout_msg = f"Timed out after {seconds} seconds"
60+
exception = WaitTimeout(timeout_msg)
61+
if raises:
62+
raise exception
63+
return False, exception
64+
time.sleep(interval)
65+
return True, None

0 commit comments

Comments
 (0)