Skip to content

Commit edaa750

Browse files
committed
ControlModeEngine(test[live]): exercise flags/flow/subscriptions
why: ensure control-mode helpers behave against real tmux clients. what: - add live control-mode tests for set_client_flags, set_pane_flow, and subscribe - run control engine inside sandbox sessions to verify client flags and pane flow usability - drop redundant stub-based cases from unit suite
1 parent 6fc1ca7 commit edaa750

File tree

2 files changed

+148
-211
lines changed

2 files changed

+148
-211
lines changed

tests/test_control_mode_engine.py

Lines changed: 0 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -323,231 +323,20 @@ def test_iter_notifications_survives_overflow(
323323
assert first.kind.name == "SESSIONS_CHANGED"
324324

325325

326-
class SetClientFlagsCase(t.NamedTuple):
327-
"""Fixture for refresh-client flag construction."""
328-
329-
test_id: str
330-
kwargs: dict[str, t.Any]
331-
expected_flags: set[str]
332-
expect_run: bool
333-
334-
335-
@pytest.mark.parametrize(
336-
"case",
337-
[
338-
SetClientFlagsCase(
339-
test_id="enable_no_output_with_pause",
340-
kwargs={"no_output": True, "pause_after": 1},
341-
expected_flags={"no-output", "pause-after=1"},
342-
expect_run=True,
343-
),
344-
SetClientFlagsCase(
345-
test_id="disable_no_output_clear_pause",
346-
kwargs={"no_output": False, "pause_after": 0},
347-
expected_flags={"!no-output", "!pause-after"},
348-
expect_run=True,
349-
),
350-
SetClientFlagsCase(
351-
test_id="wait_exit_and_read_only",
352-
kwargs={"wait_exit": True, "read_only": True},
353-
expected_flags={"wait-exit", "read-only"},
354-
expect_run=True,
355-
),
356-
SetClientFlagsCase(
357-
test_id="clear_wait_exit",
358-
kwargs={"wait_exit": False},
359-
expected_flags={"!wait-exit"},
360-
expect_run=True,
361-
),
362-
SetClientFlagsCase(
363-
test_id="toggle_misc_flags",
364-
kwargs={"ignore_size": True, "active_pane": False},
365-
expected_flags={"ignore-size", "!active-pane"},
366-
expect_run=True,
367-
),
368-
SetClientFlagsCase(
369-
test_id="noop_when_no_flags",
370-
kwargs={},
371-
expected_flags=set(),
372-
expect_run=False,
373-
),
374-
],
375-
ids=lambda c: c.test_id,
376-
)
377-
def test_set_client_flags_builds_refresh_client(case: SetClientFlagsCase) -> None:
378-
"""set_client_flags should call refresh-client with correct flag string."""
379-
engine = ControlModeEngine(start_threads=False)
380-
calls: list[tuple[str, tuple[str, ...], tuple[str, ...]]] = []
381-
382-
class DummyCmd:
383-
stdout: t.ClassVar[list[str]] = []
384-
stderr: t.ClassVar[list[str]] = []
385-
returncode: t.ClassVar[int] = 0
386-
387-
def fake_run(
388-
cmd: str,
389-
cmd_args: t.Sequence[str | int] | None = None,
390-
server_args: t.Sequence[str | int] | None = None,
391-
timeout: float | None = None,
392-
) -> DummyCmd:
393-
cmd_args_tuple = tuple(str(a) for a in (cmd_args or ()))
394-
server_args_tuple = tuple(str(a) for a in (server_args or ()))
395-
calls.append((cmd, cmd_args_tuple, server_args_tuple))
396-
return DummyCmd()
397-
398-
engine.run = fake_run # type: ignore[assignment]
399-
400-
engine.set_client_flags(**case.kwargs)
401-
402-
if not case.expect_run:
403-
assert calls == []
404-
return
405-
406-
assert len(calls) == 1
407-
cmd, cmd_args, server_args = calls[0]
408-
assert cmd == "refresh-client"
409-
assert cmd_args and cmd_args[0] == "-f"
410-
flags_str = cmd_args[1] if len(cmd_args) > 1 else ""
411-
for flag in case.expected_flags:
412-
assert flag in flags_str
413-
assert server_args == ()
414-
415-
416326
def test_set_client_flags_rejects_negative_pause() -> None:
417327
"""pause_after must be non-negative."""
418328
engine = ControlModeEngine(start_threads=False)
419329
with pytest.raises(ValueError):
420330
engine.set_client_flags(pause_after=-1)
421331

422332

423-
class PaneFlowCase(t.NamedTuple):
424-
"""Fixture for refresh-client -A flow control."""
425-
426-
test_id: str
427-
pane_id: str | int
428-
state: str
429-
expected_arg: str
430-
431-
432-
@pytest.mark.parametrize(
433-
"case",
434-
[
435-
PaneFlowCase(
436-
test_id="resume_default",
437-
pane_id="%1",
438-
state="continue",
439-
expected_arg="%1:continue",
440-
),
441-
PaneFlowCase(
442-
test_id="pause_pane",
443-
pane_id=3,
444-
state="pause",
445-
expected_arg="3:pause",
446-
),
447-
],
448-
ids=lambda c: c.test_id,
449-
)
450-
def test_set_pane_flow_builds_refresh_client(case: PaneFlowCase) -> None:
451-
"""set_pane_flow should build refresh-client -A args."""
452-
engine = ControlModeEngine(start_threads=False)
453-
calls: list[tuple[str, tuple[str, ...], tuple[str, ...]]] = []
454-
455-
class DummyCmd:
456-
stdout: t.ClassVar[list[str]] = []
457-
stderr: t.ClassVar[list[str]] = []
458-
returncode: t.ClassVar[int] = 0
459-
460-
def fake_run(
461-
cmd: str,
462-
cmd_args: t.Sequence[str | int] | None = None,
463-
server_args: t.Sequence[str | int] | None = None,
464-
timeout: float | None = None,
465-
) -> DummyCmd:
466-
cmd_args_tuple = tuple(str(a) for a in (cmd_args or ()))
467-
server_args_tuple = tuple(str(a) for a in (server_args or ()))
468-
calls.append((cmd, cmd_args_tuple, server_args_tuple))
469-
return DummyCmd()
470-
471-
engine.run = fake_run # type: ignore[assignment]
472-
473-
engine.set_pane_flow(case.pane_id, state=case.state)
474-
475-
assert calls
476-
cmd, cmd_args, server_args = calls[0]
477-
assert cmd == "refresh-client"
478-
assert cmd_args == ("-A", case.expected_arg)
479-
assert server_args == ()
480-
481-
482333
def test_set_pane_flow_validates_state() -> None:
483334
"""Invalid flow state should raise."""
484335
engine = ControlModeEngine(start_threads=False)
485336
with pytest.raises(ValueError):
486337
engine.set_pane_flow("%1", state="bad")
487338

488339

489-
class SubscribeCase(t.NamedTuple):
490-
"""Fixture for refresh-client -B subscription arguments."""
491-
492-
test_id: str
493-
name: str
494-
what: str | None
495-
format: str | None
496-
expected_args: tuple[str, ...]
497-
498-
499-
@pytest.mark.parametrize(
500-
"case",
501-
[
502-
SubscribeCase(
503-
test_id="add_subscription",
504-
name="focus",
505-
what="%1",
506-
format="#{pane_active}",
507-
expected_args=("-B", "focus:%1:#{pane_active}"),
508-
),
509-
SubscribeCase(
510-
test_id="remove_subscription",
511-
name="focus",
512-
what=None,
513-
format=None,
514-
expected_args=("-B", "focus"),
515-
),
516-
],
517-
ids=lambda c: c.test_id,
518-
)
519-
def test_subscribe_builds_refresh_client(case: SubscribeCase) -> None:
520-
"""Subscribe should wrap refresh-client -B calls."""
521-
engine = ControlModeEngine(start_threads=False)
522-
calls: list[tuple[str, tuple[str, ...], tuple[str, ...]]] = []
523-
524-
class DummyCmd:
525-
stdout: t.ClassVar[list[str]] = []
526-
stderr: t.ClassVar[list[str]] = []
527-
returncode: t.ClassVar[int] = 0
528-
529-
def fake_run(
530-
cmd: str,
531-
cmd_args: t.Sequence[str | int] | None = None,
532-
server_args: t.Sequence[str | int] | None = None,
533-
timeout: float | None = None,
534-
) -> DummyCmd:
535-
cmd_args_tuple = tuple(str(a) for a in (cmd_args or ()))
536-
server_args_tuple = tuple(str(a) for a in (server_args or ()))
537-
calls.append((cmd, cmd_args_tuple, server_args_tuple))
538-
return DummyCmd()
539-
540-
engine.run = fake_run # type: ignore[assignment]
541-
542-
engine.subscribe(case.name, what=case.what, fmt=case.format)
543-
544-
assert calls
545-
cmd, cmd_args, server_args = calls[0]
546-
assert cmd == "refresh-client"
547-
assert cmd_args == case.expected_args
548-
assert server_args == ()
549-
550-
551340
class ScriptedStdin:
552341
"""Fake stdin that can optionally raise BrokenPipeError on write."""
553342

tests/test_control_mode_live.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Live control-mode functional tests (no fakes)."""
2+
3+
from __future__ import annotations
4+
5+
import typing as t
6+
from typing import NamedTuple
7+
8+
import pytest
9+
10+
from libtmux._internal.engines.control_mode import ControlModeEngine
11+
from libtmux.server import Server
12+
from tests.helpers import wait_for_line
13+
14+
15+
class ClientFlagCase(NamedTuple):
16+
"""Fixture for exercising set_client_flags against real tmux."""
17+
18+
test_id: str
19+
kwargs: dict[str, t.Any]
20+
present: set[str]
21+
absent: frozenset[str] = frozenset()
22+
23+
24+
CLIENT_FLAG_CASES: list[ClientFlagCase] = [
25+
ClientFlagCase(
26+
test_id="enable_no_output_pause",
27+
kwargs={"no_output": True, "pause_after": 1},
28+
present={"no-output", "pause-after=1"},
29+
),
30+
ClientFlagCase(
31+
test_id="clear_no_output_pause",
32+
kwargs={"no_output": False, "pause_after": 0, "wait_exit": False},
33+
present=set(),
34+
absent=frozenset({"no-output", "pause-after", "pause-after=1", "wait-exit"}),
35+
),
36+
ClientFlagCase(
37+
test_id="enable_wait_exit",
38+
kwargs={"wait_exit": True},
39+
present={"wait-exit"},
40+
),
41+
ClientFlagCase(
42+
test_id="enable_active_pane",
43+
kwargs={"active_pane": True},
44+
present={"active-pane"},
45+
),
46+
]
47+
CLIENT_FLAG_IDS = [case.test_id for case in CLIENT_FLAG_CASES]
48+
49+
50+
@pytest.mark.engines(["control"])
51+
@pytest.mark.parametrize("case", CLIENT_FLAG_CASES, ids=CLIENT_FLAG_IDS)
52+
def test_set_client_flags_live(
53+
case: ClientFlagCase,
54+
control_sandbox: t.ContextManager[Server],
55+
) -> None:
56+
"""set_client_flags should actually toggle tmux client flags."""
57+
with control_sandbox as server:
58+
engine = t.cast(ControlModeEngine, server.engine)
59+
engine.set_client_flags(**case.kwargs)
60+
61+
flags_line = server.cmd("list-clients", "-F", "#{client_flags}").stdout
62+
assert flags_line
63+
flags = set(flags_line[0].split(","))
64+
65+
for flag in case.present:
66+
assert flag in flags
67+
for flag in case.absent:
68+
assert flag not in flags
69+
70+
71+
class PaneFlowLiveCase(NamedTuple):
72+
"""Fixture for exercising set_pane_flow against real tmux."""
73+
74+
test_id: str
75+
state: str
76+
77+
78+
PANE_FLOW_CASES = [
79+
PaneFlowLiveCase(test_id="pause", state="pause"),
80+
PaneFlowLiveCase(test_id="continue", state="continue"),
81+
]
82+
PANE_FLOW_IDS = [case.test_id for case in PANE_FLOW_CASES]
83+
84+
85+
@pytest.mark.engines(["control"])
86+
@pytest.mark.parametrize("case", PANE_FLOW_CASES, ids=PANE_FLOW_IDS)
87+
def test_set_pane_flow_live(
88+
case: PaneFlowLiveCase,
89+
control_sandbox: t.ContextManager[Server],
90+
) -> None:
91+
"""set_pane_flow should succeed and leave the client usable."""
92+
with control_sandbox as server:
93+
session = server.new_session(
94+
session_name="flow_case",
95+
attach=True,
96+
kill_session=True,
97+
)
98+
pane = session.active_pane
99+
assert pane is not None
100+
pane_id = t.cast(str, pane.pane_id)
101+
102+
engine = t.cast(ControlModeEngine, server.engine)
103+
engine.set_pane_flow(pane_id, state=case.state)
104+
105+
pane.send_keys('printf "flow-test"\\n', literal=True, suppress_history=False)
106+
lines = wait_for_line(pane, lambda line: "flow-test" in line)
107+
assert any("flow-test" in line for line in lines)
108+
109+
110+
class SubscribeLiveCase(NamedTuple):
111+
"""Fixture for exercising subscribe/unsubscribe against real tmux."""
112+
113+
test_id: str
114+
what_fmt: tuple[str, str]
115+
116+
117+
SUBSCRIBE_CASES = [
118+
SubscribeLiveCase(
119+
test_id="active_pane_subscription",
120+
what_fmt=("%1", "#{pane_active}"),
121+
),
122+
]
123+
SUBSCRIBE_IDS = [case.test_id for case in SUBSCRIBE_CASES]
124+
125+
126+
@pytest.mark.engines(["control"])
127+
@pytest.mark.parametrize("case", SUBSCRIBE_CASES, ids=SUBSCRIBE_IDS)
128+
def test_subscribe_roundtrip_live(
129+
case: SubscribeLiveCase,
130+
control_sandbox: t.ContextManager[Server],
131+
) -> None:
132+
"""subscribe/unsubscribe should succeed without breaking control client."""
133+
with control_sandbox as server:
134+
engine = t.cast(ControlModeEngine, server.engine)
135+
session = server.new_session(
136+
session_name="sub_case",
137+
attach=True,
138+
kill_session=True,
139+
)
140+
pane = session.active_pane
141+
assert pane is not None
142+
143+
target = case.what_fmt[0] if case.what_fmt[0] != "%1" else pane.pane_id
144+
engine.subscribe("focus_test", what=target, fmt=case.what_fmt[1])
145+
assert server.cmd("display-message", "-p", "ok").stdout == ["ok"]
146+
147+
engine.subscribe("focus_test", fmt=None)
148+
assert server.cmd("display-message", "-p", "ok").stdout == ["ok"]

0 commit comments

Comments
 (0)