pytest-embedded-serial 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_embedded_serial-2.3.0/LICENSE +21 -0
- pytest_embedded_serial-2.3.0/PKG-INFO +37 -0
- pytest_embedded_serial-2.3.0/README.md +8 -0
- pytest_embedded_serial-2.3.0/pyproject.toml +39 -0
- pytest_embedded_serial-2.3.0/pytest_embedded_serial/__init__.py +11 -0
- pytest_embedded_serial-2.3.0/pytest_embedded_serial/dut.py +46 -0
- pytest_embedded_serial-2.3.0/pytest_embedded_serial/serial.py +267 -0
- pytest_embedded_serial-2.3.0/tests/test_serial.py +117 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-embedded-serial
|
|
3
|
+
Version: 2.3.0
|
|
4
|
+
Summary: Make pytest-embedded plugin work with Serial.
|
|
5
|
+
Author-email: Fu Hanxi <fuhanxi@espressif.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Framework :: Pytest
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pytest-embedded~=2.3.0
|
|
23
|
+
Requires-Dist: pyserial~=3.0
|
|
24
|
+
Project-URL: changelog, https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md
|
|
25
|
+
Project-URL: documentation, https://docs.espressif.com/projects/pytest-embedded/en/latest/
|
|
26
|
+
Project-URL: homepage, https://github.com/espressif/pytest-embedded
|
|
27
|
+
Project-URL: repository, https://github.com/espressif/pytest-embedded
|
|
28
|
+
|
|
29
|
+
### pytest-embedded-serial
|
|
30
|
+
|
|
31
|
+
pytest embedded service for testing via serial ports
|
|
32
|
+
|
|
33
|
+
Extra Functionalities:
|
|
34
|
+
|
|
35
|
+
- `serial`: enable the fixture
|
|
36
|
+
- `dut`: duplicate the `serial` output to `pexpect_proc`.
|
|
37
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-embedded-serial"
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Fu Hanxi", email = "fuhanxi@espressif.com"},
|
|
9
|
+
]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"Framework :: Pytest",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python",
|
|
25
|
+
"Topic :: Software Development :: Testing",
|
|
26
|
+
]
|
|
27
|
+
dynamic = ["version", "description"]
|
|
28
|
+
requires-python = ">=3.10"
|
|
29
|
+
|
|
30
|
+
dependencies = [
|
|
31
|
+
"pytest-embedded~=2.3.0",
|
|
32
|
+
"pyserial~=3.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
homepage = "https://github.com/espressif/pytest-embedded"
|
|
37
|
+
repository = "https://github.com/espressif/pytest-embedded"
|
|
38
|
+
documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/"
|
|
39
|
+
changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, AnyStr, Optional
|
|
2
|
+
|
|
3
|
+
from pytest_embedded.dut import Dut
|
|
4
|
+
from pytest_embedded.utils import to_bytes
|
|
5
|
+
|
|
6
|
+
from .serial import Serial
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pytest_embedded_jtag import Gdb, OpenOcd, Telnet
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SerialDut(Dut):
|
|
13
|
+
"""
|
|
14
|
+
Dut class for serial ports
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
serial (Serial): `Serial` instance
|
|
18
|
+
openocd (OpenOcd): `OpenOcd` instance, applied only when `jtag` service is activated
|
|
19
|
+
gdb (Gdb): `Gdb` instance, applied only when `jtag` service is activated
|
|
20
|
+
telnet (Telnet): `Telnet` instance, applied only when `jtag` service is activated
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
serial: Serial,
|
|
26
|
+
openocd: Optional['OpenOcd'] = None,
|
|
27
|
+
gdb: Optional['Gdb'] = None,
|
|
28
|
+
telnet: Optional['Telnet'] = None,
|
|
29
|
+
**kwargs,
|
|
30
|
+
) -> None:
|
|
31
|
+
super().__init__(**kwargs)
|
|
32
|
+
|
|
33
|
+
self.serial = serial
|
|
34
|
+
self.openocd = openocd
|
|
35
|
+
self.gdb = gdb
|
|
36
|
+
self.telnet = telnet
|
|
37
|
+
|
|
38
|
+
self.setup_jtag()
|
|
39
|
+
|
|
40
|
+
def write(self, data: AnyStr) -> None:
|
|
41
|
+
self.serial.proc.write(to_bytes(data, '\n'))
|
|
42
|
+
|
|
43
|
+
def setup_jtag(self):
|
|
44
|
+
if self.gdb:
|
|
45
|
+
self.gdb.write('set remotetimeout 10')
|
|
46
|
+
self.gdb.write(f'target extended-remote :{self.openocd.gdb_port}')
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import copy
|
|
3
|
+
import logging
|
|
4
|
+
import multiprocessing
|
|
5
|
+
import queue
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, ClassVar
|
|
9
|
+
|
|
10
|
+
import serial as pyserial
|
|
11
|
+
from pytest_embedded.log import MessageQueue
|
|
12
|
+
from pytest_embedded.utils import Meta
|
|
13
|
+
from serial.tools import list_ports
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Serial:
|
|
17
|
+
"""
|
|
18
|
+
Custom serial class
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
port (str): port address
|
|
22
|
+
baud (int): baud rate
|
|
23
|
+
proc (pyserial.Serial): process created by `serial.serial_for_url()`
|
|
24
|
+
|
|
25
|
+
Warning:
|
|
26
|
+
- make sure this `Serial.__init__()` run the last in MRO, it would create and start the redirect serial process
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
DEFAULT_BAUDRATE = 115200
|
|
30
|
+
|
|
31
|
+
DEFAULT_PORT_CONFIG: ClassVar[dict[str, Any]] = {
|
|
32
|
+
'baudrate': DEFAULT_BAUDRATE,
|
|
33
|
+
'bytesize': pyserial.EIGHTBITS,
|
|
34
|
+
'parity': pyserial.PARITY_NONE,
|
|
35
|
+
'stopbits': pyserial.STOPBITS_ONE,
|
|
36
|
+
'timeout': 0.05, # read timeout
|
|
37
|
+
'xonxoff': False,
|
|
38
|
+
'rtscts': False,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
occupied_ports: ClassVar[dict[str, None]] = dict()
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
msg_queue: MessageQueue,
|
|
46
|
+
port: str | None = None,
|
|
47
|
+
port_location: str | None = None,
|
|
48
|
+
baud: int = DEFAULT_BAUDRATE,
|
|
49
|
+
meta: Meta | None = None,
|
|
50
|
+
stop_after_init: bool = False,
|
|
51
|
+
ports_to_occupy: list[str] = (),
|
|
52
|
+
**kwargs,
|
|
53
|
+
):
|
|
54
|
+
self._q = msg_queue
|
|
55
|
+
self._meta = meta
|
|
56
|
+
self._redirect_thread: _SerialRedirectThread = None # type: ignore
|
|
57
|
+
|
|
58
|
+
self.baud = baud
|
|
59
|
+
self.ports_to_occupy = ports_to_occupy if ports_to_occupy else []
|
|
60
|
+
|
|
61
|
+
if isinstance(port, pyserial.SerialBase):
|
|
62
|
+
self.proc = port
|
|
63
|
+
self.proc.timeout = self.DEFAULT_PORT_CONFIG['timeout'] # set read timeout
|
|
64
|
+
self.port = self.proc.port
|
|
65
|
+
else:
|
|
66
|
+
# Need to detect or create instance
|
|
67
|
+
if port_location:
|
|
68
|
+
for _port in list_ports.comports():
|
|
69
|
+
if _port.device in self.occupied_ports:
|
|
70
|
+
continue
|
|
71
|
+
if _port.location == port_location:
|
|
72
|
+
if port and _port.device != port:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f'The specified location {port_location} binds with port {_port.device}, not {port}'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.port = _port.device
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(f'The specified location {port_location} cannot be found.')
|
|
81
|
+
elif port:
|
|
82
|
+
self.port = port
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError('Please specify port or provide the port location')
|
|
85
|
+
|
|
86
|
+
self.port = port
|
|
87
|
+
port_config = copy.deepcopy(self.DEFAULT_PORT_CONFIG)
|
|
88
|
+
port_config['baudrate'] = baud
|
|
89
|
+
port_config.update(**kwargs)
|
|
90
|
+
self.proc = pyserial.serial_for_url(self.port, **port_config)
|
|
91
|
+
|
|
92
|
+
self.ports_to_occupy.append(self.port)
|
|
93
|
+
self._post_init()
|
|
94
|
+
try:
|
|
95
|
+
self._start()
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.close()
|
|
98
|
+
raise e
|
|
99
|
+
self._finalize_init()
|
|
100
|
+
if not stop_after_init:
|
|
101
|
+
self.start_redirect_thread()
|
|
102
|
+
else:
|
|
103
|
+
self.close()
|
|
104
|
+
|
|
105
|
+
def start_redirect_thread(self) -> None:
|
|
106
|
+
if self._redirect_thread and self._redirect_thread.is_alive():
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Here the reason why we're still using thread is,
|
|
110
|
+
# the `pyserial` object can't be pickled when using multiprocessing.Process
|
|
111
|
+
self._redirect_thread = _SerialRedirectThread(self._q, self.proc)
|
|
112
|
+
self._redirect_thread.start()
|
|
113
|
+
|
|
114
|
+
def stop_redirect_thread(self) -> bool:
|
|
115
|
+
killed = False
|
|
116
|
+
if self._redirect_thread and self._redirect_thread.is_alive():
|
|
117
|
+
self._redirect_thread.terminate()
|
|
118
|
+
killed = True
|
|
119
|
+
|
|
120
|
+
return killed
|
|
121
|
+
|
|
122
|
+
def _post_init(self):
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def _start(self):
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def _finalize_init(self):
|
|
129
|
+
occupied_ports = []
|
|
130
|
+
for port in self.ports_to_occupy:
|
|
131
|
+
if port not in self.occupied_ports:
|
|
132
|
+
self.occupied_ports[port] = None
|
|
133
|
+
occupied_ports.append(port)
|
|
134
|
+
logging.debug(f'occupied {port}')
|
|
135
|
+
else:
|
|
136
|
+
logging.warning(f'port {port} is already occupied')
|
|
137
|
+
self.ports_to_occupy = occupied_ports
|
|
138
|
+
|
|
139
|
+
def close(self):
|
|
140
|
+
self.stop_redirect_thread()
|
|
141
|
+
self.proc.close()
|
|
142
|
+
for port in self.ports_to_occupy:
|
|
143
|
+
self.occupied_ports.pop(port, None)
|
|
144
|
+
logging.debug(f'released {port}')
|
|
145
|
+
|
|
146
|
+
@contextlib.contextmanager
|
|
147
|
+
def disable_redirect_thread(self) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
kill the redirect thread, and start a new one after got yield back
|
|
150
|
+
|
|
151
|
+
Yields:
|
|
152
|
+
True if redirect serial thread has been terminated
|
|
153
|
+
"""
|
|
154
|
+
killed = self.stop_redirect_thread()
|
|
155
|
+
|
|
156
|
+
yield killed
|
|
157
|
+
|
|
158
|
+
if killed:
|
|
159
|
+
self.start_redirect_thread()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class _SerialRedirectThread(threading.Thread):
|
|
163
|
+
"""
|
|
164
|
+
Redirect serial thread
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, msg_queue: MessageQueue, s: pyserial.Serial):
|
|
168
|
+
self._q = msg_queue
|
|
169
|
+
self._event_q = multiprocessing.Queue()
|
|
170
|
+
self._s = s
|
|
171
|
+
|
|
172
|
+
self._block_reading = False
|
|
173
|
+
|
|
174
|
+
super().__init__(target=self._event_loop, daemon=True) # killed by the main process
|
|
175
|
+
|
|
176
|
+
def _event_loop(self):
|
|
177
|
+
"""
|
|
178
|
+
Since pyserial.Serial instance can't be serialized, we pass the `_serial` as an reference of the object
|
|
179
|
+
defined in _forward_io. The pyserial.Serial methods are mocked to send an event to the queue, and the real
|
|
180
|
+
method is running here.
|
|
181
|
+
"""
|
|
182
|
+
while True:
|
|
183
|
+
try:
|
|
184
|
+
_e = self._event_q.get_nowait()
|
|
185
|
+
except queue.Empty:
|
|
186
|
+
_e = 'read'
|
|
187
|
+
except OSError:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
if _e == 'read':
|
|
191
|
+
if self._block_reading:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
s = self._s.read_all()
|
|
196
|
+
except OSError as e:
|
|
197
|
+
logging.error(f'OSError detected: {e}. Serial connection may be lost.')
|
|
198
|
+
if self._s.closed:
|
|
199
|
+
logging.error('Serial port is already closed. Exiting event loop.')
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
port = self._s.port
|
|
203
|
+
port_config = {
|
|
204
|
+
'baudrate': self._s.baudrate,
|
|
205
|
+
'bytesize': self._s.bytesize,
|
|
206
|
+
'parity': self._s.parity,
|
|
207
|
+
'stopbits': self._s.stopbits,
|
|
208
|
+
'timeout': self._s.timeout,
|
|
209
|
+
'xonxoff': self._s.xonxoff,
|
|
210
|
+
'rtscts': self._s.rtscts,
|
|
211
|
+
}
|
|
212
|
+
for attempt in range(1, 4):
|
|
213
|
+
delay = attempt * 1.5
|
|
214
|
+
logging.warning(
|
|
215
|
+
f'Attempting to reconnect to serial port {port} (try {attempt}/3) after {delay}s...'
|
|
216
|
+
)
|
|
217
|
+
time.sleep(delay)
|
|
218
|
+
try:
|
|
219
|
+
self._s.close()
|
|
220
|
+
self._s = pyserial.serial_for_url(port, **port_config)
|
|
221
|
+
logging.info(f'Successfully reconnected to serial port {port}.')
|
|
222
|
+
break
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logging.warning(f'Reconnection attempt {attempt} failed: {e}')
|
|
225
|
+
else:
|
|
226
|
+
logging.error(
|
|
227
|
+
f'Failed to reconnect to serial port {port} after 3 attempts. Exiting event loop.'
|
|
228
|
+
)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logging.warning(
|
|
235
|
+
'unknown error: %s.\nRecommend to close the serial process by `dut.serial.close()`', str(e)
|
|
236
|
+
)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
self._q.put(s)
|
|
241
|
+
except OSError as e:
|
|
242
|
+
logging.warning(f'OSError. Error msg: {e}')
|
|
243
|
+
return
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logging.warning(
|
|
246
|
+
'unknown error: %s.\nRecommend to close the serial process by `dut.serial.close()`', str(e)
|
|
247
|
+
)
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
elif _e == 'stop':
|
|
251
|
+
self._block_reading = True
|
|
252
|
+
elif _e == 'start':
|
|
253
|
+
self._block_reading = False
|
|
254
|
+
elif _e == 'end':
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
time.sleep(0.05) # set interval
|
|
258
|
+
|
|
259
|
+
def stop_reading(self):
|
|
260
|
+
self._event_q.put('stop')
|
|
261
|
+
|
|
262
|
+
def start_reading(self):
|
|
263
|
+
self._event_q.put('start')
|
|
264
|
+
|
|
265
|
+
def terminate(self):
|
|
266
|
+
self._event_q.put('end')
|
|
267
|
+
self.join()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_custom_serial_device(testdir):
|
|
7
|
+
testdir.makepyfile(r"""
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
def test_serial_mixed(dut):
|
|
11
|
+
from pytest_embedded.dut_factory import DutFactory
|
|
12
|
+
assert len(dut)==2
|
|
13
|
+
another_dut = DutFactory.create()
|
|
14
|
+
st = set(
|
|
15
|
+
(
|
|
16
|
+
dut[0].serial.port,
|
|
17
|
+
dut[1].serial.port,
|
|
18
|
+
another_dut.serial.port
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
assert len(st) == 3
|
|
22
|
+
|
|
23
|
+
def test_custom_dut():
|
|
24
|
+
from pytest_embedded.dut_factory import DutFactory
|
|
25
|
+
another_dut = DutFactory.create(embedded_services='esp,serial')
|
|
26
|
+
""")
|
|
27
|
+
|
|
28
|
+
result = testdir.runpytest(
|
|
29
|
+
'-s',
|
|
30
|
+
'--embedded-services',
|
|
31
|
+
'esp,serial',
|
|
32
|
+
'--count',
|
|
33
|
+
2,
|
|
34
|
+
)
|
|
35
|
+
result.assert_outcomes(passed=2, errors=0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_custom_serial_device_dut_count_1(testdir):
|
|
39
|
+
testdir.makepyfile(r"""
|
|
40
|
+
import pytest
|
|
41
|
+
|
|
42
|
+
def test_serial_device_created_dut_count_1(dut):
|
|
43
|
+
from pytest_embedded.dut_factory import DutFactory
|
|
44
|
+
another_dut = DutFactory.create()
|
|
45
|
+
another_dut2 = DutFactory.create()
|
|
46
|
+
st = set(
|
|
47
|
+
(
|
|
48
|
+
dut.serial.port,
|
|
49
|
+
another_dut.serial.port,
|
|
50
|
+
another_dut2.serial.port
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
assert len(st) == 3
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
""")
|
|
57
|
+
|
|
58
|
+
result = testdir.runpytest(
|
|
59
|
+
'-s',
|
|
60
|
+
'--embedded-services',
|
|
61
|
+
'esp,serial',
|
|
62
|
+
'--count',
|
|
63
|
+
1,
|
|
64
|
+
)
|
|
65
|
+
result.assert_outcomes(passed=1, errors=0)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.skipif(sys.platform == 'win32', reason='No socat support on windows')
|
|
69
|
+
@pytest.mark.flaky(reruns=3, reruns_delay=2)
|
|
70
|
+
def test_serial_port(testdir):
|
|
71
|
+
testdir.makepyfile(r"""
|
|
72
|
+
import pytest
|
|
73
|
+
import subprocess
|
|
74
|
+
|
|
75
|
+
@pytest.fixture(autouse=True)
|
|
76
|
+
def open_tcp_port():
|
|
77
|
+
proc = subprocess.Popen('socat TCP4-LISTEN:9876,fork EXEC:cat', shell=True)
|
|
78
|
+
yield
|
|
79
|
+
proc.terminate()
|
|
80
|
+
|
|
81
|
+
def test_serial_port(dut):
|
|
82
|
+
dut.write(b'hello world\n')
|
|
83
|
+
dut.expect('hello world')
|
|
84
|
+
""")
|
|
85
|
+
|
|
86
|
+
result = testdir.runpytest(
|
|
87
|
+
'-s',
|
|
88
|
+
'--embedded-services',
|
|
89
|
+
'serial',
|
|
90
|
+
'--port',
|
|
91
|
+
'socket://localhost:9876',
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
result.assert_outcomes(passed=1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_teardown_called_for_multi_dut(testdir):
|
|
98
|
+
testdir.makepyfile(r"""
|
|
99
|
+
import pytest
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize('count, embedded_services, port', [
|
|
102
|
+
('3', 'serial', '/dev/ttyUSB0|/dev/ttyUSB1|/dev/ttyUSB100'), # set up failure
|
|
103
|
+
], indirect=True)
|
|
104
|
+
def test_teardown_called_for_multi_dut_fail(dut):
|
|
105
|
+
assert len(dut) == 3
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize('count, embedded_services, port', [
|
|
108
|
+
('3', 'serial', '/dev/ttyUSB0|/dev/ttyUSB1|/dev/ttyUSB2'), # set up succeeded
|
|
109
|
+
], indirect=True)
|
|
110
|
+
def test_teardown_called_for_multi_dut_succeeded(dut):
|
|
111
|
+
assert dut[0].serial.port == '/dev/ttyUSB0'
|
|
112
|
+
assert dut[1].serial.port == '/dev/ttyUSB1'
|
|
113
|
+
assert dut[2].serial.port == '/dev/ttyUSB2'
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
result = testdir.runpytest()
|
|
117
|
+
result.assert_outcomes(passed=1, errors=1)
|