Skip to content

Commit 4414c51

Browse files
committed
Initial import of MultiRIOTCtrl
1 parent 9148884 commit 4414c51

File tree

7 files changed

+507
-4
lines changed

7 files changed

+507
-4
lines changed

riotctrl/ctrl.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,22 @@ def start_term(self, **spawnkwargs):
131131
The function is blocking until it is ready.
132132
It waits some time until the terminal is ready and resets the ctrl.
133133
"""
134+
self.start_term_wo_sleep(**spawnkwargs)
135+
# on many platforms, the termprog needs a short while to be ready
136+
time.sleep(self.TERM_STARTED_DELAY)
137+
138+
def start_term_wo_sleep(self, **spawnkwargs):
139+
"""Start the terminal.
140+
141+
The function is blocking until it is ready.
142+
It does not wait until the terminal is ready and resets the ctrl.
143+
"""
134144
self.stop_term()
135145

136146
term_cmd = self.make_command(['term'])
137147
self.term = self.TERM_SPAWN_CLASS(term_cmd[0], args=term_cmd[1:],
138148
env=self.env, **spawnkwargs)
139149

140-
# on many platforms, the termprog needs a short while to be ready
141-
time.sleep(self.TERM_STARTED_DELAY)
142-
143150
def _term_pid(self):
144151
"""Terminal pid or None."""
145152
return getattr(self.term, 'pid', None)

riotctrl/multictrl/__init__.py

Whitespace-only changes.

riotctrl/multictrl/ctrl.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Abstraction for multiple RIOTCtrl objects
2+
3+
Defines a class to abstract access to multiple RIOTCtrls.
4+
"""
5+
6+
import riotctrl.ctrl
7+
8+
from riotctrl.multictrl.utils import MultiKeyDict
9+
10+
11+
# Intentionally do not inherit from TermSpawn so we do not have to rewrite
12+
# all of `pexpect` ;-)
13+
# pylint: disable=too-many-ancestors
14+
class MultiTermSpawn(MultiKeyDict):
15+
"""Allows for access and control of multiple RIOTCtrl objects
16+
"""
17+
def expect(self, pattern, *args, **kwargs):
18+
"""
19+
mirroring riotctrl.ctrl.TermSpawn.expect()
20+
"""
21+
return {k: v.expect(pattern, *args, **kwargs)
22+
for k, v in self.items()}
23+
24+
def expect_exact(self, pattern, *args, **kwargs):
25+
"""
26+
mirroring riotctrl.ctrl.TermSpawn.expect()
27+
"""
28+
return {k: v.expect_exact(pattern, *args, **kwargs)
29+
for k, v in self.items()}
30+
31+
def sendline(self, *args, **kwargs):
32+
"""
33+
mirroring riotctrl.ctrl.TermSpawn.expect()
34+
"""
35+
return {k: v.sendline(*args, **kwargs)
36+
for k, v in self.items()}
37+
38+
39+
# pylint: disable=too-many-ancestors
40+
class MultiRIOTCtrl(MultiKeyDict, riotctrl.ctrl.RIOTCtrl):
41+
"""Allows for access and control of multiple RIOTCtrl objects
42+
43+
>>> ctrl = MultiRIOTCtrl({'a': riotctrl.ctrl.RIOTCtrl(env={'BOARD': 'A'}),
44+
... 'b': riotctrl.ctrl.RIOTCtrl(env={'BOARD': 'B'})})
45+
>>> ctrl.board()
46+
{'a': 'A', 'b': 'B'}
47+
>>> ctrl['a','b'].board()
48+
{'a': 'A', 'b': 'B'}
49+
>>> ctrl['a'].board()
50+
'A'
51+
"""
52+
TERM_SPAWN_CLASS = MultiTermSpawn
53+
54+
def __init__(self, ctrls=None):
55+
super().__init__(ctrls)
56+
self.term = None # type: MultiRIOTCtrl.TERM_SPAWN_CLASS
57+
58+
@property
59+
def application_directory(self):
60+
"""Absolute path to the containing RIOTCtrls current directory as
61+
dictionary."""
62+
return {k: ctrl.application_directory for k, ctrl in self.items()}
63+
64+
def board(self):
65+
"""Return board type of containing RIOTCtrls as dictionary."""
66+
return {k: ctrl.board() for k, ctrl in self.items()}
67+
68+
def start_term_wo_sleep(self, **spawnkwargs):
69+
"""Start the terminal (without waiting) for containing ctrls.
70+
"""
71+
res = {}
72+
for k, ctrl in self.items():
73+
ctrl.start_term_wo_sleep(**spawnkwargs)
74+
res[k] = ctrl.term
75+
self.term = self.TERM_SPAWN_CLASS(res)
76+
77+
def make_run(self, targets, *runargs, **runkwargs):
78+
"""Call make `targets` for containing RIOTctrl contexts.
79+
80+
It is using `subprocess.run` internally.
81+
82+
:param targets: make targets
83+
:param *runargs: args passed to subprocess.run
84+
:param *runkwargs: kwargs passed to subprocess.run
85+
:return: subprocess.CompletedProcess object
86+
"""
87+
return {k: ctrl.make_run(targets, *runargs, **runkwargs)
88+
for k, ctrl in self.items()}
89+
90+
def make_command(self, targets):
91+
"""Dictionary of make command for context of containing RIOTctrls.
92+
93+
:return: list of command arguments (for example for subprocess)
94+
"""
95+
return {k: ctrl.make_command(targets) for k, ctrl in self.items()}

riotctrl/multictrl/shell.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Shell interaction extenison for riotctrl.multictrl
3+
4+
Defines classes to abstract interactions with RIOT shell commands using
5+
riotctrl.multictrl.MultiRIOTCtrl objects
6+
"""
7+
8+
import pexpect
9+
10+
import riotctrl.shell
11+
import riotctrl.multictrl.ctrl
12+
13+
from riotctrl.multictrl.ctrl import MultiKeyDict
14+
15+
16+
class MultiShellInteractionMixin(riotctrl.shell.ShellInteraction):
17+
"""
18+
Mixin class for shell interactions of riotctrl.multictrl.ctrl.MultiRIOTCtrl
19+
20+
:param ctrl: a MultiRIOTCtrl object
21+
"""
22+
# pylint: disable=super-init-not-called
23+
# intentionally do not call super-init, to not cause TypeError
24+
def __init__(self, ctrl):
25+
# used in __del__, so initialize before raising exception
26+
self.term_was_started = False
27+
if not isinstance(ctrl, riotctrl.multictrl.ctrl.MultiRIOTCtrl):
28+
raise TypeError(
29+
"{} not compatible with non multi-RIOTCtrl {}. Use {} instead."
30+
.format(type(self), type(ctrl),
31+
riotctrl.shell.ShellInteraction)
32+
)
33+
self.riotctrl = ctrl
34+
self.replwrap = MultiKeyDict()
35+
36+
def _start_replwrap(self):
37+
if not self.replwrap or \
38+
(any(key not in self.replwrap for key in self.riotctrl) and
39+
any(self.replwrap[key].child != self.riotctrl[key].term
40+
for key in self.riotctrl)):
41+
for key, ctrl in self.riotctrl.items():
42+
# consume potentially shown prompt to be on the same ground as
43+
# if it is not shown
44+
ctrl.term.expect_exact(["> ", pexpect.TIMEOUT], timeout=.1)
45+
# enforce prompt to be shown by sending newline
46+
ctrl.term.sendline("")
47+
self.replwrap[key] = pexpect.replwrap.REPLWrapper(
48+
ctrl.term, orig_prompt="> ", prompt_change=None,
49+
)
50+
51+
# pylint: disable=arguments-differ
52+
def cmd(self, cmd, timeout=-1, async_=False, ctrls=None):
53+
"""
54+
Sends a command via the MultiShellInteractionMixin `riotctrl`s
55+
56+
:param cmd: A shell command as string.
57+
:param ctrls: ctrls to run command on
58+
59+
:return: Output of the command as a string
60+
"""
61+
self._start_replwrap()
62+
if ctrls is None:
63+
ctrls = self.riotctrl.keys()
64+
elif not isinstance(ctrls, (tuple, list)):
65+
ctrls = (ctrls,)
66+
return {k: rw.run_command(cmd, timeout=timeout, async_=async_)
67+
for k, rw in self.replwrap.items() if k in ctrls}

riotctrl/multictrl/utils.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Helper utilities.
2+
"""
3+
4+
import collections.abc
5+
6+
7+
class MultiKeyDict(collections.abc.MutableMapping):
8+
"""Works like a dict, but returns another dict, when used with tuples
9+
as a key:
10+
11+
>>> a = MultiKeyDict({0: 'zero', 1: 'one', 2: 'two'})
12+
>>> len(a)
13+
3
14+
>>> a[0]
15+
'zero'
16+
>>> a[1]
17+
'one'
18+
>>> a[2]
19+
'two'
20+
>>> a[0,1]
21+
{0: 'zero', 1: 'one'}
22+
>>> a[3] = 'three'
23+
>>> a[0,1] = 'foobar'
24+
>>> a
25+
{0: 'foobar', 1: 'foobar', 2: 'two', 3: 'three'}
26+
>>> del a[1,2]
27+
>>> a
28+
{0: 'foobar', 3: 'three'}
29+
>>> del a[0]
30+
>>> a
31+
{3: 'three'}
32+
"""
33+
def __init__(self, dictionary=None):
34+
if dictionary is None:
35+
self._dict = {}
36+
else:
37+
self._dict = dict(dictionary)
38+
39+
def __str__(self):
40+
return str(self._dict)
41+
42+
def __repr__(self):
43+
return str(self)
44+
45+
def __getitem__(self, key):
46+
if isinstance(key, tuple):
47+
return type(self)(
48+
{k: self._dict[k] for k in key}
49+
)
50+
return self._dict[key]
51+
52+
def __setitem__(self, key, value):
53+
if isinstance(key, tuple):
54+
for k in key:
55+
self._dict[k] = value
56+
else:
57+
self._dict[key] = value
58+
59+
def __delitem__(self, key):
60+
if isinstance(key, tuple):
61+
for k in key:
62+
del self._dict[k]
63+
else:
64+
del self._dict[key]
65+
66+
def __iter__(self):
67+
return iter(self._dict)
68+
69+
def __len__(self):
70+
return len(self._dict)

riotctrl/shell/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import pexpect
1010
import pexpect.replwrap
1111

12+
import riotctrl.multictrl.ctrl as multictrl
13+
1214

1315
# pylint: disable=R0903
1416
class ShellInteractionParser(abc.ABC):
@@ -30,9 +32,16 @@ class ShellInteraction():
3032
:param riotctrl: a RIOTCtrl object
3133
"""
3234
def __init__(self, riotctrl):
35+
# used in __del__, so initialize before raising exception
36+
self.term_was_started = False
37+
if isinstance(riotctrl, multictrl.MultiRIOTCtrl):
38+
raise TypeError(
39+
"{} not compatible with multi-RIOTCtrl {}. Use {} instead."
40+
.format(type(self), type(riotctrl),
41+
'riotctrl.multictrl.shell.MultiShellInteractionMixin')
42+
)
3343
self.riotctrl = riotctrl
3444
self.replwrap = None
35-
self.term_was_started = False
3645

3746
def __del__(self):
3847
if self.term_was_started:

0 commit comments

Comments
 (0)