Skip to content

Commit beac1ee

Browse files
evakhoniclaude
andcommitted
test: add console hotkey tests using PTY-simulated stdin
- Ctrl-B x3 exits cleanly - Ctrl-] x3 triggers on_power_cycle and console stays alive - Ctrl-] x3 without a power client does nothing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c720171 commit beac1ee

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import threading
2+
from unittest.mock import MagicMock
3+
4+
from .driver import PySerial
5+
from jumpstarter.common.utils import serve
6+
7+
8+
def test_find_power_client_no_root():
9+
with serve(PySerial(url="loop://")) as client:
10+
assert client._find_power_client() is None
11+
12+
13+
def test_find_power_client_with_cycle():
14+
power = MagicMock(spec=["cycle", "children"])
15+
power.children = {}
16+
root = MagicMock(spec=["children"])
17+
root.children = {"power": power}
18+
19+
with serve(PySerial(url="loop://")) as client:
20+
object.__setattr__(client, "root", root)
21+
assert client._find_power_client() is power
22+
23+
24+
def test_make_power_cycle_calls_cycle():
25+
called = threading.Event()
26+
power = MagicMock()
27+
power.cycle = MagicMock(side_effect=lambda: called.set())
28+
29+
with serve(PySerial(url="loop://")) as client:
30+
cycle_fn = client._make_power_cycle(power)
31+
client.portal.call(cycle_fn)
32+
assert called.is_set()
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import os
2+
import threading
3+
import time
4+
from unittest.mock import MagicMock, patch
5+
6+
from .console import Console
7+
from .driver import PySerial
8+
from jumpstarter.common.utils import serve
9+
10+
11+
def _start_console(client, on_power_cycle=None):
12+
"""Run Console.run() in a thread with a PTY substituted for stdin.
13+
14+
Returns (master_fd, thread, result_dict). Write keypresses to master_fd;
15+
the result dict gets an 'exc' key if the console thread raises.
16+
"""
17+
master_fd, slave_fd = os.openpty()
18+
slave_file = os.fdopen(slave_fd, "rb", buffering=0)
19+
20+
mock_stdin = MagicMock()
21+
mock_stdin.fileno.return_value = slave_fd
22+
mock_stdin.buffer = slave_file
23+
24+
result = {}
25+
26+
def _run():
27+
with patch("sys.stdin", mock_stdin):
28+
console = Console(serial_client=client, on_power_cycle=on_power_cycle)
29+
try:
30+
console.run()
31+
except Exception as e:
32+
result["exc"] = e
33+
slave_file.close()
34+
35+
t = threading.Thread(target=_run, daemon=True)
36+
t.start()
37+
return master_fd, t, result
38+
39+
40+
def test_ctrl_b_exits():
41+
with serve(PySerial(url="loop://")) as client:
42+
master_fd, t, result = _start_console(client)
43+
try:
44+
time.sleep(0.1)
45+
os.write(master_fd, b"a")
46+
os.write(master_fd, b"\x02\x02\x02")
47+
t.join(timeout=5)
48+
finally:
49+
os.close(master_fd)
50+
51+
assert not t.is_alive(), "console did not exit after Ctrl-B x3"
52+
assert "exc" not in result
53+
54+
55+
def test_ctrl_bracket_triggers_power_cycle():
56+
power_cycled = threading.Event()
57+
58+
async def on_power_cycle():
59+
power_cycled.set()
60+
61+
with serve(PySerial(url="loop://")) as client:
62+
master_fd, t, result = _start_console(client, on_power_cycle=on_power_cycle)
63+
try:
64+
time.sleep(0.1)
65+
os.write(master_fd, b"\x1d\x1d\x1d")
66+
assert power_cycled.wait(timeout=5), "power cycle was not triggered"
67+
assert t.is_alive(), "console exited after power cycle"
68+
os.write(master_fd, b"\x02\x02\x02")
69+
t.join(timeout=5)
70+
finally:
71+
os.close(master_fd)
72+
73+
assert not t.is_alive()
74+
assert "exc" not in result
75+
76+
77+
def test_ctrl_bracket_without_power_client():
78+
with serve(PySerial(url="loop://")) as client:
79+
master_fd, t, result = _start_console(client, on_power_cycle=None)
80+
try:
81+
time.sleep(0.1)
82+
os.write(master_fd, b"\x1d\x1d\x1d")
83+
time.sleep(0.1)
84+
assert t.is_alive(), "console exited unexpectedly on Ctrl-] without power client"
85+
os.write(master_fd, b"\x02\x02\x02")
86+
t.join(timeout=5)
87+
finally:
88+
os.close(master_fd)
89+
90+
assert not t.is_alive()
91+
assert "exc" not in result

0 commit comments

Comments
 (0)