Skip to content

Commit ba6a803

Browse files
authored
[otci] update to support more commands (openthread#11015)
* otci now supports all commands the Thread Test Harness requires * improve typehinting across the board * fixes some typos
1 parent 21ba5bb commit ba6a803

File tree

10 files changed

+649
-228
lines changed

10 files changed

+649
-228
lines changed

tests/scripts/thread-cert/command.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ def check_tlv_request_tlv(command_msg, check_type, tlv_id):
144144

145145
elif check_type == CheckType.NOT_CONTAIN:
146146
if tlv_request_tlv is not None:
147-
assert (any(tlv_id == tlv for tlv in tlv_request_tlv.tlvs) is
148-
False), "Error: The msg contains TLV Request TLV ID: {}".format(tlv_id)
147+
assert (not any(tlv_id == tlv
148+
for tlv in tlv_request_tlv.tlvs)), f"Error: The msg contains TLV Request TLV ID: {tlv_id}"
149149

150150
elif check_type == CheckType.OPTIONAL:
151151
if tlv_request_tlv is not None:

tools/otci/otci/__init__.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@
3131
from .constants import THREAD_VERSION_1_1, THREAD_VERSION_1_2
3232
from .command_handlers import OTCommandHandler
3333
from .otci import OTCI
34-
from .otci import \
35-
connect_cli_sim, \
36-
connect_cli_serial, \
37-
connect_ncp_sim, \
38-
connect_cmd_handler, \
39-
connect_otbr_ssh, \
40-
connect_otbr_adb_tcp, \
41-
connect_otbr_adb_usb
34+
from .otci import (
35+
connect_cli_sim,
36+
connect_cli_serial,
37+
connect_ncp_sim,
38+
connect_cmd_handler,
39+
connect_otbr_ssh,
40+
connect_otbr_adb_tcp,
41+
connect_otbr_adb_usb,
42+
)
4243

4344
from .types import Rloc16, ChildId, NetifIdentifier
4445

@@ -61,4 +62,6 @@
6162
'NetifIdentifier',
6263
'THREAD_VERSION_1_1',
6364
'THREAD_VERSION_1_2',
65+
'THREAD_VERSION_1_3',
66+
'THREAD_VERSION_1_4',
6467
] + _connectors

tools/otci/otci/command_handlers.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,21 @@ class OtCliCommandRunner(OTCommandHandler):
105105

106106
__ASYNC_COMMANDS = {'scan', 'ping', 'discover'}
107107

108-
def __init__(self, otcli: OtCliHandler, is_spinel_cli=False):
108+
def __init__(self, otcli: OtCliHandler, is_spinel_cli: bool = False):
109109
self.__otcli: OtCliHandler = otcli
110110
self.__is_spinel_cli = is_spinel_cli
111111
self.__expect_command_echoback = not self.__is_spinel_cli
112112
self.__line_read_callback = None
113113

114-
self.__pending_lines = queue.Queue()
114+
self.__pending_lines: queue.Queue[str] = queue.Queue()
115115
self.__should_close = threading.Event()
116116
self.__otcli_reader = threading.Thread(target=self.__otcli_read_routine, daemon=True)
117117
self.__otcli_reader.start()
118118

119119
def __repr__(self):
120120
return repr(self.__otcli)
121121

122-
def execute_command(self, cmd, timeout=10) -> List[str]:
122+
def execute_command(self, cmd: str, timeout: float = 10) -> List[str]:
123123
assert not self.__should_close.is_set(), "OT CLI is already closed."
124124
self.__otcli.writeline(cmd)
125125

@@ -137,13 +137,13 @@ def execute_command(self, cmd, timeout=10) -> List[str]:
137137
asynchronous=cmd.split()[0] in OtCliCommandRunner.__ASYNC_COMMANDS)
138138
return output
139139

140-
def execute_platform_command(self, cmd, timeout=10) -> List[str]:
140+
def execute_platform_command(self, cmd: str, timeout: float = 10) -> List[str]:
141141
raise NotImplementedError(f'Platform command is not supported on {self.__class__.__name__}')
142142

143143
def wait(self, duration: float) -> List[str]:
144144
self.__otcli.wait(duration)
145145

146-
output = []
146+
output: List[str] = []
147147
try:
148148
while True:
149149
line = self.__pending_lines.get_nowait()
@@ -166,8 +166,11 @@ def set_line_read_callback(self, callback: Optional[Callable[[str], Any]]):
166166
# Private methods
167167
#
168168

169-
def __expect_line(self, timeout: float, expect_line: Union[str, Pattern], asynchronous=False) -> List[str]:
170-
output = []
169+
def __expect_line(self,
170+
timeout: float,
171+
expect_line: Union[str, Pattern[str]],
172+
asynchronous: bool = False) -> List[str]:
173+
output: List[str] = []
171174

172175
if not asynchronous:
173176
while True:
@@ -222,12 +225,13 @@ def __otcli_read_routine(self):
222225
logging.debug('%s: %s', self.__otcli, line)
223226

224227
if not OtCliCommandRunner.__PATTERN_LOG_LINE.match(line):
228+
logging.info('%s: %s', self.__otcli, line)
225229
self.__pending_lines.put(line)
226230

227231

228232
class OtbrSshCommandRunner(OTCommandHandler):
229233

230-
def __init__(self, host, port, username, password, sudo):
234+
def __init__(self, host: str, port: int, username: str, password: str, sudo: bool):
231235
import paramiko
232236

233237
self.__host = host
@@ -272,16 +276,16 @@ def execute_command(self, cmd: str, timeout: float) -> List[str]:
272276

273277
return output
274278

275-
def execute_platform_command(self, cmd, timeout=10) -> List[str]:
279+
def execute_platform_command(self, cmd: str, timeout: float = 10) -> List[str]:
276280
if self.__sudo:
277281
cmd = 'sudo ' + cmd
278282

279283
return self.shell(cmd, timeout=timeout)
280284

281285
def shell(self, cmd: str, timeout: float) -> List[str]:
282-
cmd_in, cmd_out, cmd_err = self.__ssh.exec_command(cmd, timeout=int(timeout), bufsize=1024)
283-
errput = [l.rstrip('\r\n') for l in cmd_err.readlines()]
284-
output = [l.rstrip('\r\n') for l in cmd_out.readlines()]
286+
_, cmd_out, cmd_err = self.__ssh.exec_command(cmd, timeout=int(timeout), bufsize=1024)
287+
errput = [line.rstrip('\r\n') for line in cmd_err.readlines()]
288+
output = [line.rstrip('\r\n') for line in cmd_out.readlines()]
285289

286290
if errput:
287291
raise CommandError(cmd, errput)

tools/otci/otci/connectors.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import subprocess
3131
import time
3232
from abc import abstractmethod, ABC
33-
from typing import Optional
33+
from typing import Any, Optional
3434

3535

3636
class OtCliHandler(ABC):
@@ -72,7 +72,7 @@ def go(self, duration: float):
7272
class OtCliPopen(OtCliHandler):
7373
"""Connector for OT CLI process (a Popen instance)."""
7474

75-
def __init__(self, proc: subprocess.Popen, nodeid: int, simulator: Simulator):
75+
def __init__(self, proc: subprocess.Popen[Any], nodeid: int, simulator: Optional[Simulator]):
7676
self.__otcli_proc = proc
7777
self.__nodeid = nodeid
7878
self.__simulator = simulator
@@ -108,7 +108,7 @@ def close(self):
108108
class OtCliSim(OtCliPopen):
109109
"""Connector for OT CLI Simulation instances."""
110110

111-
def __init__(self, executable: str, nodeid: int, simulator: Simulator):
111+
def __init__(self, executable: str, nodeid: int, simulator: Optional[Simulator]):
112112
logging.info('%s: executable=%s', self.__class__.__name__, executable)
113113

114114
proc = subprocess.Popen(args=[executable, str(nodeid)],
@@ -123,7 +123,7 @@ def __init__(self, executable: str, nodeid: int, simulator: Simulator):
123123
class OtNcpSim(OtCliPopen):
124124
"""Connector for OT NCP Simulation instances."""
125125

126-
def __init__(self, executable: str, nodeid: int, simulator: Simulator):
126+
def __init__(self, executable: str, nodeid: int, simulator: Optional[Simulator]):
127127
logging.info('%s: executable=%s', self.__class__.__name__, executable)
128128

129129
proc = subprocess.Popen(args=f'spinel-cli.py -p "{executable}" -n {nodeid} 2>&1',
@@ -144,6 +144,7 @@ def __init__(self, dev: str, baudrate: int):
144144

145145
import serial
146146
self.__serial = serial.Serial(self.__dev, self.__baudrate, timeout=0.1, exclusive=True)
147+
self.writeline('\r\n')
147148
self.__linebuffer = b''
148149

149150
def __repr__(self):
@@ -164,7 +165,7 @@ def readline(self) -> Optional[str]:
164165
return None
165166

166167
def writeline(self, s: str):
167-
self.__serial.write((s + '\n').encode('utf-8'))
168+
self.__serial.write((s + '\r\n').encode('utf-8'))
168169

169170
def wait(self, duration: float):
170171
time.sleep(duration)

tools/otci/otci/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@
3030
# Thread versions
3131
THREAD_VERSION_1_1 = 2
3232
THREAD_VERSION_1_2 = 3
33+
THREAD_VERSION_1_3 = 4
34+
THREAD_VERSION_1_4 = 5

tools/otci/otci/errors.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
2727
# POSSIBILITY OF SUCH DAMAGE.
2828
#
29-
from typing import List
29+
30+
import re
31+
from typing import Collection, List, Pattern, Union
3032

3133

3234
class OTCIError(Exception):
@@ -37,15 +39,26 @@ class OTCIError(Exception):
3739
class ExpectLineTimeoutError(OTCIError):
3840
"""OTCI failed to find an expected line before timeout."""
3941

40-
def __init__(self, line):
42+
def __init__(self, line: Union[str, Pattern[str], Collection[str]]):
4143
super(ExpectLineTimeoutError, self).__init__("Expected line %r, but timed out" % line)
4244

4345

4446
class CommandError(OTCIError):
4547
"""OTCI failed to execute a command."""
4648

49+
__COMMAND_OUTPUT_ERROR_PATTERN = re.compile(r'Error (\d+): (.*)')
50+
4751
def __init__(self, cmd: str, output: List[str]):
4852
self.__output = output
53+
54+
for line in output:
55+
m = self.__COMMAND_OUTPUT_ERROR_PATTERN.match(line)
56+
if not m:
57+
continue
58+
code, msg = m.groups()
59+
self.code, self.msg = int(code), str(msg)
60+
break
61+
4962
super(CommandError, self).__init__("Command error while executing %r:\n%s\n" % (cmd, '\n'.join(output)))
5063

5164
def error(self) -> str:

0 commit comments

Comments
 (0)