Skip to content

Commit 0e9b1cb

Browse files
committed
control-mode: stabilize capture_pane and env assertions
1 parent 066923f commit 0e9b1cb

File tree

7 files changed

+71
-23
lines changed

7 files changed

+71
-23
lines changed

src/libtmux/pane.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import dataclasses
1111
import logging
1212
import pathlib
13+
import time
1314
import typing as t
1415
import warnings
1516

@@ -350,9 +351,25 @@ def capture_pane(
350351
if end is not None:
351352
cmd.extend(["-E", str(end)])
352353
output = self.cmd(*cmd).stdout
353-
# Control mode (and tmux in general) can include viewport-sized blank lines.
354-
while output and output[-1].strip() == "":
355-
output.pop()
354+
355+
def _trim(lines: list[str]) -> list[str]:
356+
trimmed = list(lines)
357+
while trimmed and trimmed[-1].strip() == "":
358+
trimmed.pop()
359+
return trimmed
360+
361+
output = _trim(output)
362+
363+
# In control mode, capture-pane can race the shell: the first capture
364+
# right after send-keys may return only the echoed command. Retry
365+
# briefly to allow the prompt/output to land.
366+
engine_name = self.server.engine.__class__.__name__
367+
if engine_name == "ControlModeEngine" and not output:
368+
deadline = time.monotonic() + 0.35
369+
while not output and time.monotonic() < deadline:
370+
time.sleep(0.05)
371+
output = _trim(self.cmd(*cmd).stdout)
372+
356373
return output
357374

358375
def send_keys(

tests/helpers.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Test helpers for control-mode flakiness handling."""
2+
3+
from __future__ import annotations
4+
5+
import time
6+
import typing as t
7+
8+
from libtmux.pane import Pane
9+
10+
11+
def wait_for_line(
12+
pane: Pane,
13+
predicate: t.Callable[[str], bool],
14+
*,
15+
timeout: float = 1.0,
16+
interval: float = 0.05,
17+
) -> list[str]:
18+
"""Poll capture_pane until a line satisfies ``predicate``.
19+
20+
Returns the final capture buffer (may be empty if timeout elapses).
21+
"""
22+
deadline = time.monotonic() + timeout
23+
last: list[str] = []
24+
while time.monotonic() < deadline:
25+
captured = pane.capture_pane()
26+
last = [captured] if isinstance(captured, str) else list(captured)
27+
if any(predicate(line) for line in last):
28+
break
29+
time.sleep(interval)
30+
return last

tests/legacy_api/test_pane.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import shutil
77
import typing as t
88

9-
import pytest
10-
119
if t.TYPE_CHECKING:
1210
from libtmux.session import Session
1311

@@ -77,9 +75,6 @@ def test_set_width(session: Session) -> None:
7775

7876
def test_capture_pane(session: Session) -> None:
7977
"""Verify Pane.capture_pane()."""
80-
if session.server.engine.__class__.__name__ == "ControlModeEngine":
81-
pytest.xfail("control-mode capture-pane normalization pending")
82-
8378
env = shutil.which("env")
8479
assert env is not None, "Cannot find usable `env` in PATH."
8580

tests/legacy_api/test_window.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import logging
66
import shutil
7-
import time
87
import typing as t
98

109
import pytest
@@ -14,6 +13,7 @@
1413
from libtmux.pane import Pane
1514
from libtmux.server import Server
1615
from libtmux.window import Window
16+
from tests.helpers import wait_for_line
1717

1818
if t.TYPE_CHECKING:
1919
from libtmux.session import Session
@@ -417,11 +417,14 @@ def test_split_window_with_environment(
417417
environment=environment,
418418
)
419419
assert pane is not None
420-
# wait a bit for the prompt to be ready as the test gets flaky otherwise
421-
time.sleep(0.05)
422420
for k, v in environment.items():
423421
pane.send_keys(f"echo ${k}")
424-
assert pane.capture_pane()[-2] == v
422+
423+
def _match(line: str, expected: str = v) -> bool:
424+
return line.strip() == expected
425+
426+
lines = wait_for_line(pane, _match)
427+
assert any(_match(line) for line in lines)
425428

426429

427430
@pytest.mark.skipif(

tests/test_pane.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ def test_set_width(session: Session) -> None:
6767

6868
def test_capture_pane(session: Session) -> None:
6969
"""Verify Pane.capture_pane()."""
70-
if session.server.engine.__class__.__name__ == "ControlModeEngine":
71-
pytest.xfail("control-mode capture-pane trailing prompt normalization pending")
72-
7370
env = shutil.which("env")
7471
assert env is not None, "Cannot find usable `env` in PATH."
7572

tests/test_session.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from libtmux.test.constants import TEST_SESSION_PREFIX
1818
from libtmux.test.random import namer
1919
from libtmux.window import Window
20+
from tests.helpers import wait_for_line
2021

2122
if t.TYPE_CHECKING:
2223
from libtmux._internal.types import StrPath
@@ -330,9 +331,6 @@ def test_new_window_with_environment(
330331
environment: dict[str, str],
331332
) -> None:
332333
"""Verify new window with environment vars."""
333-
if session.server.engine.__class__.__name__ == "ControlModeEngine":
334-
pytest.xfail("control-mode -e propagation still pending")
335-
336334
env = shutil.which("env")
337335
assert env is not None, "Cannot find usable `env` in PATH."
338336

@@ -346,7 +344,12 @@ def test_new_window_with_environment(
346344
assert pane is not None
347345
for k, v in environment.items():
348346
pane.send_keys(f"echo ${k}")
349-
assert pane.capture_pane()[-2] == v
347+
348+
def _match(line: str, expected: str = v) -> bool:
349+
return line.strip() == expected
350+
351+
lines = wait_for_line(pane, _match)
352+
assert any(_match(line) for line in lines)
350353

351354

352355
@pytest.mark.skipif(

tests/test_window.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import logging
66
import pathlib
77
import shutil
8-
import time
98
import typing as t
109

1110
import pytest
@@ -21,6 +20,7 @@
2120
from libtmux.pane import Pane
2221
from libtmux.server import Server
2322
from libtmux.window import Window
23+
from tests.helpers import wait_for_line
2424

2525
if t.TYPE_CHECKING:
2626
from libtmux._internal.types import StrPath
@@ -456,11 +456,14 @@ def test_split_with_environment(
456456
environment=environment,
457457
)
458458
assert pane is not None
459-
# wait a bit for the prompt to be ready as the test gets flaky otherwise
460-
time.sleep(0.05)
461459
for k, v in environment.items():
462460
pane.send_keys(f"echo ${k}")
463-
assert pane.capture_pane()[-2] == v
461+
462+
def _match(line: str, expected: str = v) -> bool:
463+
return line.strip() == expected
464+
465+
lines = wait_for_line(pane, _match)
466+
assert any(_match(line) for line in lines)
464467

465468

466469
@pytest.mark.skipif(

0 commit comments

Comments
 (0)