Skip to content

Commit dcca2a4

Browse files
committed
tests: Add test coverage for console and terminal handling
Add unit tests for the terminal handling in term.py, covering the external console launch (microcom and telnet fallback), the internal console read/write loop, and terminal setup and teardown. Also add tests for the new is_allowed() helper and the new parser argument for the internal console. Use real pipes for stdin rather than mocking os.read(), giving more realistic coverage of the keystroke and txdelay paths. The exit-char deadline test uses a threading.Event to synchronise with the run loop, avoiding brittle fixed sleeps. Fix an UnboundLocalError in term.internal() where log_fd could be referenced in the finally block before being assigned. Also fix a duplicate --logfile append in term.external() when using microcom. Series-changes: 5 - Feed keystrokes through a prompt_toolkit Input, so that raw mode and the event-loop hookup are covered too - Add a test that escape sequences and control characters reach the board untouched Signed-off-by: Simon Glass <sjg@chromium.org> Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b4e11ea commit dcca2a4

3 files changed

Lines changed: 626 additions & 2 deletions

File tree

labgrid/util/term.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ async def external(check_allowed, host, port, resource, logfile, listen_only):
5454
if logfile:
5555
logging.warning("--logfile option not supported by telnet, ignoring")
5656

57-
if logfile:
58-
call.append(f"--logfile={logfile}")
5957
logging.info("connecting to %s calling %s", resource, " ".join(call))
6058
p = await asyncio.create_subprocess_exec(*call)
6159
while p.returncode is None:
@@ -166,6 +164,7 @@ def read_keys():
166164

167165
to_cons += data
168166

167+
# Drain one byte at a time, honouring txdelay between bytes
169168
if to_cons and time.monotonic() > next_cons:
170169
cons._write(to_cons[:1])
171170
to_cons = to_cons[1:]

tests/test_client_unit.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Unit tests for labgrid.remote.client"""
2+
3+
import argparse
4+
from unittest.mock import MagicMock, patch
5+
6+
import pytest
7+
8+
from labgrid.remote.client import ClientSession, UserError, get_parser
9+
10+
11+
# --- is_allowed() tests ---
12+
13+
@pytest.fixture
14+
def session():
15+
"""Create a minimal ClientSession-like object for testing"""
16+
s = object.__new__(ClientSession)
17+
s.args = argparse.Namespace()
18+
return s
19+
20+
21+
@pytest.fixture
22+
def mock_place():
23+
place = MagicMock()
24+
place.name = "testplace"
25+
place.acquired = "myhost/myuser"
26+
place.allowed = {"myhost/myuser"}
27+
return place
28+
29+
30+
class TestIsAllowed:
31+
def test_place_not_acquired(self, session, mock_place):
32+
mock_place.acquired = None
33+
with patch.object(session, "gethostname", return_value="myhost"), \
34+
patch.object(session, "getuser", return_value="myuser"):
35+
result = session.is_allowed(mock_place)
36+
assert "not acquired" in result
37+
38+
def test_place_acquired_by_us(self, session, mock_place):
39+
with patch.object(session, "gethostname", return_value="myhost"), \
40+
patch.object(session, "getuser", return_value="myuser"):
41+
result = session.is_allowed(mock_place)
42+
assert result is None
43+
44+
def test_place_acquired_by_different_user(self, session, mock_place):
45+
mock_place.acquired = "myhost/otheruser"
46+
mock_place.allowed = {"myhost/otheruser"}
47+
with patch.object(session, "gethostname", return_value="myhost"), \
48+
patch.object(session, "getuser", return_value="myuser"):
49+
result = session.is_allowed(mock_place)
50+
assert "not acquired by your user" in result
51+
assert "otheruser" in result
52+
53+
def test_place_acquired_on_different_host(self, session, mock_place):
54+
mock_place.acquired = "otherhost/myuser"
55+
mock_place.allowed = {"otherhost/myuser"}
56+
with patch.object(session, "gethostname", return_value="myhost"), \
57+
patch.object(session, "getuser", return_value="myuser"):
58+
result = session.is_allowed(mock_place)
59+
assert "not acquired on this computer" in result
60+
assert "otherhost" in result
61+
62+
def test_place_acquired_elsewhere_but_allowed(self, session, mock_place):
63+
"""User is in the allowed set even though place was acquired elsewhere"""
64+
mock_place.acquired = "otherhost/otheruser"
65+
mock_place.allowed = {"otherhost/otheruser", "myhost/myuser"}
66+
with patch.object(session, "gethostname", return_value="myhost"), \
67+
patch.object(session, "getuser", return_value="myuser"):
68+
result = session.is_allowed(mock_place)
69+
assert result is None
70+
71+
72+
# --- _check_allowed() tests ---
73+
74+
class TestCheckAllowed:
75+
def test_raises_on_not_allowed(self, session, mock_place):
76+
mock_place.acquired = None
77+
with patch.object(session, "gethostname", return_value="myhost"), \
78+
patch.object(session, "getuser", return_value="myuser"):
79+
with pytest.raises(UserError, match="not acquired"):
80+
session._check_allowed(mock_place)
81+
82+
def test_no_raise_when_allowed(self, session, mock_place):
83+
with patch.object(session, "gethostname", return_value="myhost"), \
84+
patch.object(session, "getuser", return_value="myuser"):
85+
session._check_allowed(mock_place) # should not raise
86+
87+
88+
# --- get_parser() tests ---
89+
90+
class TestGetParser:
91+
def test_console_internal_argument(self):
92+
parser = get_parser()
93+
args = parser.parse_args(["console", "--internal"])
94+
assert args.internal is True

0 commit comments

Comments
 (0)