klab-pytest-toolkit-embedded 1.1.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.
- klab_pytest_toolkit_embedded-1.1.0/.gitignore +178 -0
- klab_pytest_toolkit_embedded-1.1.0/PKG-INFO +217 -0
- klab_pytest_toolkit_embedded-1.1.0/README.md +198 -0
- klab_pytest_toolkit_embedded-1.1.0/pyproject.toml +46 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/__init__.py +4 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/board.py +107 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/communicators/__init__.py +4 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/communicators/interface.py +22 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/communicators/serial.py +113 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/debug_probes/__init__.py +4 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/debug_probes/esp.py +48 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/debug_probes/interface.py +26 -0
- klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/plugin.py +6 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Created by https://www.toptal.com/developers/gitignore/api/python
|
|
2
|
+
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
|
3
|
+
|
|
4
|
+
### Custom ###
|
|
5
|
+
pytest.xml
|
|
6
|
+
pytest-coverage.txt
|
|
7
|
+
tmp/
|
|
8
|
+
|
|
9
|
+
### Python ###
|
|
10
|
+
# Byte-compiled / optimized / DLL files
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
|
|
15
|
+
# Distribution / packaging
|
|
16
|
+
.Python
|
|
17
|
+
build/
|
|
18
|
+
develop-eggs/
|
|
19
|
+
dist/
|
|
20
|
+
downloads/
|
|
21
|
+
eggs/
|
|
22
|
+
.eggs/
|
|
23
|
+
lib/
|
|
24
|
+
lib64/
|
|
25
|
+
parts/
|
|
26
|
+
sdist/
|
|
27
|
+
var/
|
|
28
|
+
wheels/
|
|
29
|
+
share/python-wheels/
|
|
30
|
+
*.egg-info/
|
|
31
|
+
.installed.cfg
|
|
32
|
+
*.egg
|
|
33
|
+
MANIFEST
|
|
34
|
+
|
|
35
|
+
# PyInstaller
|
|
36
|
+
# Usually these files are written by a python script from a template
|
|
37
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
38
|
+
*.manifest
|
|
39
|
+
*.spec
|
|
40
|
+
|
|
41
|
+
# Installer logs
|
|
42
|
+
pip-log.txt
|
|
43
|
+
pip-delete-this-directory.txt
|
|
44
|
+
|
|
45
|
+
# Unit test / coverage reports
|
|
46
|
+
htmlcov/
|
|
47
|
+
.tox/
|
|
48
|
+
.nox/
|
|
49
|
+
.coverage
|
|
50
|
+
.coverage.*
|
|
51
|
+
.cache
|
|
52
|
+
nosetests.xml
|
|
53
|
+
coverage.xml
|
|
54
|
+
*.cover
|
|
55
|
+
*.py,cover
|
|
56
|
+
.hypothesis/
|
|
57
|
+
.pytest_cache/
|
|
58
|
+
cover/
|
|
59
|
+
|
|
60
|
+
# Translations
|
|
61
|
+
*.mo
|
|
62
|
+
*.pot
|
|
63
|
+
|
|
64
|
+
# Django stuff:
|
|
65
|
+
*.log
|
|
66
|
+
local_settings.py
|
|
67
|
+
db.sqlite3
|
|
68
|
+
db.sqlite3-journal
|
|
69
|
+
|
|
70
|
+
# Flask stuff:
|
|
71
|
+
instance/
|
|
72
|
+
.webassets-cache
|
|
73
|
+
|
|
74
|
+
# Scrapy stuff:
|
|
75
|
+
.scrapy
|
|
76
|
+
|
|
77
|
+
# Sphinx documentation
|
|
78
|
+
docs/_build/
|
|
79
|
+
|
|
80
|
+
# PyBuilder
|
|
81
|
+
.pybuilder/
|
|
82
|
+
target/
|
|
83
|
+
|
|
84
|
+
# Jupyter Notebook
|
|
85
|
+
.ipynb_checkpoints
|
|
86
|
+
|
|
87
|
+
# IPython
|
|
88
|
+
profile_default/
|
|
89
|
+
ipython_config.py
|
|
90
|
+
|
|
91
|
+
# pyenv
|
|
92
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
93
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
94
|
+
# .python-version
|
|
95
|
+
|
|
96
|
+
# pipenv
|
|
97
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
98
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
99
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
100
|
+
# install all needed dependencies.
|
|
101
|
+
#Pipfile.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
116
|
+
.pdm.toml
|
|
117
|
+
|
|
118
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
119
|
+
__pypackages__/
|
|
120
|
+
|
|
121
|
+
# Celery stuff
|
|
122
|
+
celerybeat-schedule
|
|
123
|
+
celerybeat.pid
|
|
124
|
+
|
|
125
|
+
# SageMath parsed files
|
|
126
|
+
*.sage.py
|
|
127
|
+
|
|
128
|
+
# Environments
|
|
129
|
+
.env
|
|
130
|
+
.venv
|
|
131
|
+
env/
|
|
132
|
+
venv/
|
|
133
|
+
ENV/
|
|
134
|
+
env.bak/
|
|
135
|
+
venv.bak/
|
|
136
|
+
|
|
137
|
+
# Spyder project settings
|
|
138
|
+
.spyderproject
|
|
139
|
+
.spyproject
|
|
140
|
+
|
|
141
|
+
# Rope project settings
|
|
142
|
+
.ropeproject
|
|
143
|
+
|
|
144
|
+
# mkdocs documentation
|
|
145
|
+
/site
|
|
146
|
+
|
|
147
|
+
# mypy
|
|
148
|
+
.mypy_cache/
|
|
149
|
+
.dmypy.json
|
|
150
|
+
dmypy.json
|
|
151
|
+
|
|
152
|
+
# Pyre type checker
|
|
153
|
+
.pyre/
|
|
154
|
+
|
|
155
|
+
# pytype static type analyzer
|
|
156
|
+
.pytype/
|
|
157
|
+
|
|
158
|
+
# Cython debug symbols
|
|
159
|
+
cython_debug/
|
|
160
|
+
|
|
161
|
+
# PyCharm
|
|
162
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
163
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
164
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
165
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
166
|
+
#.idea/
|
|
167
|
+
|
|
168
|
+
### Python Patch ###
|
|
169
|
+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
|
170
|
+
poetry.toml
|
|
171
|
+
|
|
172
|
+
# ruff
|
|
173
|
+
.ruff_cache/
|
|
174
|
+
|
|
175
|
+
# LSP config files
|
|
176
|
+
pyrightconfig.json
|
|
177
|
+
|
|
178
|
+
.ms-playwright
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: klab-pytest-toolkit-embedded
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Pytest embedded testing fixtures for the Klab Pytest Toolkit
|
|
5
|
+
Project-URL: Changelog, https://github.com/klab365/klab-pytest-toolkit/blob/main/CHANGELOG.md
|
|
6
|
+
Project-URL: Repository, https://github.com/klab365/klab-pytest-toolkit
|
|
7
|
+
Project-URL: Issues, https://github.com/klab365/klab-pytest-toolkit/issues
|
|
8
|
+
Author-email: Burak Kizilkaya <burak.kizilkaya@outlook.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: <4,>=3.11
|
|
15
|
+
Requires-Dist: esptool>=5.1.0
|
|
16
|
+
Requires-Dist: pyserial>=3.5
|
|
17
|
+
Requires-Dist: pytest>=8.3.5
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Klab Pytest Toolkit - Embedded
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/klab-pytest-toolkit-embedded/)
|
|
23
|
+
[](https://pypi.org/project/klab-pytest-toolkit-embedded/)
|
|
24
|
+
[](../../LICENSE)
|
|
25
|
+
|
|
26
|
+
Custom pytest fixtures for embedded systems testing.
|
|
27
|
+
The goal is to allow testers to easily test embedded devices with reusable components for programming, resetting, and communicating with boards.
|
|
28
|
+
|
|
29
|
+
At the moment the package provides the following components:
|
|
30
|
+
|
|
31
|
+
- `Board`: Main orchestration class for managing board operations, including programming, resetting, and communication.
|
|
32
|
+
- Debug Probes:
|
|
33
|
+
- `EspTool`: Debug probe implementation for ESP32 devices using `esptool`.
|
|
34
|
+
- Communicators:
|
|
35
|
+
- `SerialCommunicator`: Serial port communication interface for UART/USB connections.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install klab-pytest-toolkit-embedded
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Board Class
|
|
46
|
+
|
|
47
|
+
The `Board` class orchestrates board operations by combining a debug probe (for programming and resetting) with a communicator (for sending and receiving data).
|
|
48
|
+
|
|
49
|
+
**Create a fixture**
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import pytest
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Generator
|
|
55
|
+
from klab_pytest_toolkit_embedded.board import Board
|
|
56
|
+
from klab_pytest_toolkit_embedded.debug_probes import EspTool
|
|
57
|
+
from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def dut() -> Generator[Board]:
|
|
61
|
+
"""Fixture to provide a Board instance for Device Under Test (DUT)."""
|
|
62
|
+
PORT = "/dev/ttyUSB0"
|
|
63
|
+
|
|
64
|
+
communicator = SerialCommunicator(port=PORT, baudrate=115200)
|
|
65
|
+
debug_probe = EspTool(port=PORT, baudrate=1500000, address="0x0")
|
|
66
|
+
|
|
67
|
+
with Board(debug_probe=debug_probe, communicator=communicator) as board:
|
|
68
|
+
yield board
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Programming and Reset**
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
def test_program_firmware(dut: Board):
|
|
75
|
+
"""Test programming firmware to the board."""
|
|
76
|
+
firmware_file = "path/to/firmware.bin"
|
|
77
|
+
dut.program(firmware_file)
|
|
78
|
+
# Firmware is now flashed to the device
|
|
79
|
+
|
|
80
|
+
def test_reset_board(dut: Board):
|
|
81
|
+
"""Test resetting the board."""
|
|
82
|
+
dut.reset()
|
|
83
|
+
# Board has been reset
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Communication**
|
|
87
|
+
|
|
88
|
+
The `Board` class provides methods for sending and receiving data:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
def test_send_data(dut: Board):
|
|
92
|
+
"""Test sending data to the board."""
|
|
93
|
+
dut.send(b"Hello Device!\n")
|
|
94
|
+
|
|
95
|
+
def test_receive_data(dut: Board):
|
|
96
|
+
"""Test receiving data from the board."""
|
|
97
|
+
data = dut.receive_some(num_bytes=1024)
|
|
98
|
+
print(data.decode('utf-8', errors='ignore'))
|
|
99
|
+
|
|
100
|
+
def test_wait_for_boot_message(dut: Board):
|
|
101
|
+
"""Test waiting for a specific message during boot."""
|
|
102
|
+
firmware_file = "path/to/firmware.bin"
|
|
103
|
+
dut.program(firmware_file)
|
|
104
|
+
|
|
105
|
+
# Wait for boot message with regex
|
|
106
|
+
boot_message = b"Firmware Ready!"
|
|
107
|
+
assert dut.wait_for_regex_in_line(boot_message, timeout_s=10, log=True)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Serial Communicator
|
|
111
|
+
|
|
112
|
+
The `SerialCommunicator` provides serial communication functionality with configurable parameters:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
|
|
116
|
+
|
|
117
|
+
# Create a serial communicator
|
|
118
|
+
communicator = SerialCommunicator(
|
|
119
|
+
port="/dev/ttyUSB0",
|
|
120
|
+
baudrate=115200,
|
|
121
|
+
timeout=1.0
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Send data
|
|
125
|
+
communicator.send(b"AT\r\n")
|
|
126
|
+
|
|
127
|
+
# Receive data
|
|
128
|
+
data = communicator.receive(num_bytes=100)
|
|
129
|
+
|
|
130
|
+
# Check available bytes
|
|
131
|
+
available = communicator.bytes_available()
|
|
132
|
+
|
|
133
|
+
# Flush buffers
|
|
134
|
+
communicator.flush_input()
|
|
135
|
+
communicator.flush_output()
|
|
136
|
+
|
|
137
|
+
# Close when done
|
|
138
|
+
communicator.close()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### ESP Debug Probe
|
|
142
|
+
|
|
143
|
+
The `EspTool` class provides programming and reset functionality for ESP32 devices:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from klab_pytest_toolkit_embedded.debug_probes import EspTool
|
|
147
|
+
|
|
148
|
+
# Create ESP debug probe
|
|
149
|
+
esp_probe = EspTool(
|
|
150
|
+
port="/dev/ttyUSB0",
|
|
151
|
+
baudrate=1500000,
|
|
152
|
+
address="0x0"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Program firmware
|
|
156
|
+
esp_probe.program("path/to/firmware.bin")
|
|
157
|
+
|
|
158
|
+
# Reset the device
|
|
159
|
+
esp_probe.reset()
|
|
160
|
+
|
|
161
|
+
# Close (no persistent connection for esptool)
|
|
162
|
+
esp_probe.close()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Examples
|
|
166
|
+
|
|
167
|
+
See the test files for comprehensive examples.
|
|
168
|
+
|
|
169
|
+
## Best Practices
|
|
170
|
+
|
|
171
|
+
### Use Context Managers
|
|
172
|
+
|
|
173
|
+
The `Board` class supports context managers to ensure proper cleanup of resources:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
with Board(debug_probe=debug_probe, communicator=communicator) as board:
|
|
177
|
+
board.program(firmware_file)
|
|
178
|
+
board.wait_for_regex_in_line(b"Ready", timeout_s=10)
|
|
179
|
+
# Resources are automatically closed when exiting the context
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Timeout Configuration
|
|
183
|
+
|
|
184
|
+
When waiting for messages from the device, always specify appropriate timeouts to prevent tests from hanging:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# Wait with custom timeout
|
|
188
|
+
dut.wait_for_regex_in_line(
|
|
189
|
+
regex=b"Boot complete",
|
|
190
|
+
timeout_s=30,
|
|
191
|
+
log=True # Enable logging to see device output
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Hardware Availability
|
|
196
|
+
|
|
197
|
+
For tests that require physical hardware, use `pytest.mark.skipif` to conditionally skip tests when hardware is not available:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
@pytest.mark.skipif(
|
|
201
|
+
not hardware_available(),
|
|
202
|
+
reason="This test requires a physical ESP32 device connected."
|
|
203
|
+
)
|
|
204
|
+
def test_with_hardware(dut: Board):
|
|
205
|
+
# Test code here
|
|
206
|
+
pass
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Links
|
|
210
|
+
|
|
211
|
+
- [Source code](https://github.com/klab365/klab-pytest-toolkit/tree/main/packages/klab-pytest-toolkit-embedded)
|
|
212
|
+
- [PyPI](https://pypi.org/project/klab-pytest-toolkit-embedded/)
|
|
213
|
+
- [Issue tracker](https://github.com/klab365/klab-pytest-toolkit/issues)
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Klab Pytest Toolkit - Embedded
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/klab-pytest-toolkit-embedded/)
|
|
4
|
+
[](https://pypi.org/project/klab-pytest-toolkit-embedded/)
|
|
5
|
+
[](../../LICENSE)
|
|
6
|
+
|
|
7
|
+
Custom pytest fixtures for embedded systems testing.
|
|
8
|
+
The goal is to allow testers to easily test embedded devices with reusable components for programming, resetting, and communicating with boards.
|
|
9
|
+
|
|
10
|
+
At the moment the package provides the following components:
|
|
11
|
+
|
|
12
|
+
- `Board`: Main orchestration class for managing board operations, including programming, resetting, and communication.
|
|
13
|
+
- Debug Probes:
|
|
14
|
+
- `EspTool`: Debug probe implementation for ESP32 devices using `esptool`.
|
|
15
|
+
- Communicators:
|
|
16
|
+
- `SerialCommunicator`: Serial port communication interface for UART/USB connections.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install klab-pytest-toolkit-embedded
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Board Class
|
|
27
|
+
|
|
28
|
+
The `Board` class orchestrates board operations by combining a debug probe (for programming and resetting) with a communicator (for sending and receiving data).
|
|
29
|
+
|
|
30
|
+
**Create a fixture**
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import pytest
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Generator
|
|
36
|
+
from klab_pytest_toolkit_embedded.board import Board
|
|
37
|
+
from klab_pytest_toolkit_embedded.debug_probes import EspTool
|
|
38
|
+
from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def dut() -> Generator[Board]:
|
|
42
|
+
"""Fixture to provide a Board instance for Device Under Test (DUT)."""
|
|
43
|
+
PORT = "/dev/ttyUSB0"
|
|
44
|
+
|
|
45
|
+
communicator = SerialCommunicator(port=PORT, baudrate=115200)
|
|
46
|
+
debug_probe = EspTool(port=PORT, baudrate=1500000, address="0x0")
|
|
47
|
+
|
|
48
|
+
with Board(debug_probe=debug_probe, communicator=communicator) as board:
|
|
49
|
+
yield board
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Programming and Reset**
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
def test_program_firmware(dut: Board):
|
|
56
|
+
"""Test programming firmware to the board."""
|
|
57
|
+
firmware_file = "path/to/firmware.bin"
|
|
58
|
+
dut.program(firmware_file)
|
|
59
|
+
# Firmware is now flashed to the device
|
|
60
|
+
|
|
61
|
+
def test_reset_board(dut: Board):
|
|
62
|
+
"""Test resetting the board."""
|
|
63
|
+
dut.reset()
|
|
64
|
+
# Board has been reset
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Communication**
|
|
68
|
+
|
|
69
|
+
The `Board` class provides methods for sending and receiving data:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
def test_send_data(dut: Board):
|
|
73
|
+
"""Test sending data to the board."""
|
|
74
|
+
dut.send(b"Hello Device!\n")
|
|
75
|
+
|
|
76
|
+
def test_receive_data(dut: Board):
|
|
77
|
+
"""Test receiving data from the board."""
|
|
78
|
+
data = dut.receive_some(num_bytes=1024)
|
|
79
|
+
print(data.decode('utf-8', errors='ignore'))
|
|
80
|
+
|
|
81
|
+
def test_wait_for_boot_message(dut: Board):
|
|
82
|
+
"""Test waiting for a specific message during boot."""
|
|
83
|
+
firmware_file = "path/to/firmware.bin"
|
|
84
|
+
dut.program(firmware_file)
|
|
85
|
+
|
|
86
|
+
# Wait for boot message with regex
|
|
87
|
+
boot_message = b"Firmware Ready!"
|
|
88
|
+
assert dut.wait_for_regex_in_line(boot_message, timeout_s=10, log=True)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Serial Communicator
|
|
92
|
+
|
|
93
|
+
The `SerialCommunicator` provides serial communication functionality with configurable parameters:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
|
|
97
|
+
|
|
98
|
+
# Create a serial communicator
|
|
99
|
+
communicator = SerialCommunicator(
|
|
100
|
+
port="/dev/ttyUSB0",
|
|
101
|
+
baudrate=115200,
|
|
102
|
+
timeout=1.0
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Send data
|
|
106
|
+
communicator.send(b"AT\r\n")
|
|
107
|
+
|
|
108
|
+
# Receive data
|
|
109
|
+
data = communicator.receive(num_bytes=100)
|
|
110
|
+
|
|
111
|
+
# Check available bytes
|
|
112
|
+
available = communicator.bytes_available()
|
|
113
|
+
|
|
114
|
+
# Flush buffers
|
|
115
|
+
communicator.flush_input()
|
|
116
|
+
communicator.flush_output()
|
|
117
|
+
|
|
118
|
+
# Close when done
|
|
119
|
+
communicator.close()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### ESP Debug Probe
|
|
123
|
+
|
|
124
|
+
The `EspTool` class provides programming and reset functionality for ESP32 devices:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from klab_pytest_toolkit_embedded.debug_probes import EspTool
|
|
128
|
+
|
|
129
|
+
# Create ESP debug probe
|
|
130
|
+
esp_probe = EspTool(
|
|
131
|
+
port="/dev/ttyUSB0",
|
|
132
|
+
baudrate=1500000,
|
|
133
|
+
address="0x0"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Program firmware
|
|
137
|
+
esp_probe.program("path/to/firmware.bin")
|
|
138
|
+
|
|
139
|
+
# Reset the device
|
|
140
|
+
esp_probe.reset()
|
|
141
|
+
|
|
142
|
+
# Close (no persistent connection for esptool)
|
|
143
|
+
esp_probe.close()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Examples
|
|
147
|
+
|
|
148
|
+
See the test files for comprehensive examples.
|
|
149
|
+
|
|
150
|
+
## Best Practices
|
|
151
|
+
|
|
152
|
+
### Use Context Managers
|
|
153
|
+
|
|
154
|
+
The `Board` class supports context managers to ensure proper cleanup of resources:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
with Board(debug_probe=debug_probe, communicator=communicator) as board:
|
|
158
|
+
board.program(firmware_file)
|
|
159
|
+
board.wait_for_regex_in_line(b"Ready", timeout_s=10)
|
|
160
|
+
# Resources are automatically closed when exiting the context
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Timeout Configuration
|
|
164
|
+
|
|
165
|
+
When waiting for messages from the device, always specify appropriate timeouts to prevent tests from hanging:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Wait with custom timeout
|
|
169
|
+
dut.wait_for_regex_in_line(
|
|
170
|
+
regex=b"Boot complete",
|
|
171
|
+
timeout_s=30,
|
|
172
|
+
log=True # Enable logging to see device output
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Hardware Availability
|
|
177
|
+
|
|
178
|
+
For tests that require physical hardware, use `pytest.mark.skipif` to conditionally skip tests when hardware is not available:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
@pytest.mark.skipif(
|
|
182
|
+
not hardware_available(),
|
|
183
|
+
reason="This test requires a physical ESP32 device connected."
|
|
184
|
+
)
|
|
185
|
+
def test_with_hardware(dut: Board):
|
|
186
|
+
# Test code here
|
|
187
|
+
pass
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Links
|
|
191
|
+
|
|
192
|
+
- [Source code](https://github.com/klab365/klab-pytest-toolkit/tree/main/packages/klab-pytest-toolkit-embedded)
|
|
193
|
+
- [PyPI](https://pypi.org/project/klab-pytest-toolkit-embedded/)
|
|
194
|
+
- [Issue tracker](https://github.com/klab365/klab-pytest-toolkit/issues)
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "klab-pytest-toolkit-embedded"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Pytest embedded testing fixtures for the Klab Pytest Toolkit"
|
|
9
|
+
authors = [{ name = "Burak Kizilkaya", email = "burak.kizilkaya@outlook.com" }]
|
|
10
|
+
requires-python = ">=3.11,<4"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"esptool>=5.1.0",
|
|
21
|
+
"pyserial>=3.5",
|
|
22
|
+
"pytest>=8.3.5",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Changelog = "https://github.com/klab365/klab-pytest-toolkit/blob/main/CHANGELOG.md"
|
|
27
|
+
Repository = "https://github.com/klab365/klab-pytest-toolkit"
|
|
28
|
+
Issues = "https://github.com/klab365/klab-pytest-toolkit/issues"
|
|
29
|
+
|
|
30
|
+
[project.entry-points.pytest11]
|
|
31
|
+
klab_pytest_toolkit_embedded = "klab_pytest_toolkit_embedded.plugin"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
34
|
+
path = "src/klab_pytest_toolkit_embedded/__init__.py"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
exclude = [
|
|
38
|
+
"tests",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
asyncio_mode = "strict"
|
|
43
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
44
|
+
pythonpath = [
|
|
45
|
+
"."
|
|
46
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from klab_pytest_toolkit_embedded.communicators import CommunicatorInterface
|
|
5
|
+
from klab_pytest_toolkit_embedded.debug_probes import DebugProbe
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Board:
|
|
9
|
+
"""Main class for the orchestration of board operations.
|
|
10
|
+
|
|
11
|
+
This class uses a debug probe to program and reset the board,
|
|
12
|
+
and a communication interface to send and receive data.
|
|
13
|
+
|
|
14
|
+
It could be, that both functionalities are provided by the same physical device,
|
|
15
|
+
but this is abstracted away by using separate interfaces.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
debug_probe: DebugProbe,
|
|
21
|
+
communicator: CommunicatorInterface,
|
|
22
|
+
):
|
|
23
|
+
"""Initialize the board.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
debug_probe: Debug probe instance (e.g., EspTool)
|
|
27
|
+
communication_interface: Communication interface instance (e.g., SerialCommunicator)
|
|
28
|
+
"""
|
|
29
|
+
self._debug_probe = debug_probe
|
|
30
|
+
self._communicator = communicator
|
|
31
|
+
|
|
32
|
+
def program(self, fw_image: str) -> None:
|
|
33
|
+
"""Flash the firmware image to the board.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
fw_image (str): Path to the firmware image.
|
|
37
|
+
"""
|
|
38
|
+
self._debug_probe.program(fw_image)
|
|
39
|
+
|
|
40
|
+
def reset(self) -> None:
|
|
41
|
+
"""Reset the board."""
|
|
42
|
+
self._debug_probe.reset()
|
|
43
|
+
|
|
44
|
+
def receive_some(self, num_bytes: int = 1024) -> bytes:
|
|
45
|
+
"""Receive some data from the communication interface.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
num_bytes (int, optional): Number of bytes to receive. Defaults to 1024.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
bytes: Received data.
|
|
52
|
+
"""
|
|
53
|
+
return self._communicator.receive(num_bytes)
|
|
54
|
+
|
|
55
|
+
def send(self, data: bytes) -> None:
|
|
56
|
+
"""Send data to the device through the communication interface.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
data: Bytes to send
|
|
60
|
+
"""
|
|
61
|
+
self._communicator.send(data)
|
|
62
|
+
|
|
63
|
+
def wait_for_regex_in_line(self, regex, timeout_s=20, log=True) -> bool:
|
|
64
|
+
"""Wait for a line matching the regex from the communication interface.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
regex: Regular expression to match.
|
|
68
|
+
timeout_s (int, optional): Timeout in seconds. Defaults to 20.
|
|
69
|
+
log (bool, optional): Whether to log the output. Defaults to True.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
bool: True if a matching line is found, False otherwise.
|
|
73
|
+
"""
|
|
74
|
+
received = b"" # Start with bytes, not string
|
|
75
|
+
start_time = time.time()
|
|
76
|
+
while True:
|
|
77
|
+
# Check for timeout
|
|
78
|
+
if time.time() - start_time > timeout_s:
|
|
79
|
+
raise TimeoutError(f"Timeout waiting for regex: {regex}")
|
|
80
|
+
|
|
81
|
+
# Check in already received data
|
|
82
|
+
lines = received.splitlines(keepends=True)
|
|
83
|
+
|
|
84
|
+
for _, line in enumerate(lines):
|
|
85
|
+
regex_search = re.search(
|
|
86
|
+
regex,
|
|
87
|
+
line.replace(b"\r", b"").replace(b"\n", b"").decode("utf-8", errors="ignore"),
|
|
88
|
+
)
|
|
89
|
+
if regex_search:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
# Receive more data
|
|
93
|
+
chunk = self.receive_some()
|
|
94
|
+
|
|
95
|
+
if log:
|
|
96
|
+
print(chunk.replace(b"\r", b"").decode("utf-8", errors="ignore"), end="")
|
|
97
|
+
|
|
98
|
+
received = received + chunk
|
|
99
|
+
|
|
100
|
+
def __enter__(self):
|
|
101
|
+
"""Enter context manager."""
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
105
|
+
"""Exit context manager."""
|
|
106
|
+
self._communicator.close()
|
|
107
|
+
self._debug_probe.close()
|
klab_pytest_toolkit_embedded-1.1.0/src/klab_pytest_toolkit_embedded/communicators/interface.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Communication interface definition for embedded boards."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CommunicatorInterface(abc.ABC):
|
|
7
|
+
"""Abstract interface for MCU communication."""
|
|
8
|
+
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
def send(self, data: bytes) -> None:
|
|
11
|
+
"""Send data to the device."""
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def receive(self, num_bytes: int) -> bytes:
|
|
16
|
+
"""Receive data from the device."""
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def close(self) -> None:
|
|
21
|
+
"""Close the communication channel."""
|
|
22
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Serial communication implementation for embedded boards."""
|
|
2
|
+
|
|
3
|
+
import serial
|
|
4
|
+
|
|
5
|
+
from klab_pytest_toolkit_embedded.communicators.interface import CommunicatorInterface
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SerialCommunicator(CommunicatorInterface):
|
|
9
|
+
"""Serial port communication interface for UART/USB connections."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
port: str,
|
|
14
|
+
baudrate: int = 115200,
|
|
15
|
+
timeout: float = 1.0,
|
|
16
|
+
bytesize: int = serial.EIGHTBITS,
|
|
17
|
+
parity: str = serial.PARITY_NONE,
|
|
18
|
+
stopbits: float = serial.STOPBITS_ONE,
|
|
19
|
+
):
|
|
20
|
+
"""Initialize serial communication.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
port: Serial port path (e.g., '/dev/ttyUSB0', 'COM3')
|
|
24
|
+
baudrate: Communication speed in bits per second (default: 115200)
|
|
25
|
+
timeout: Read timeout in seconds (default: 1.0)
|
|
26
|
+
bytesize: Number of data bits (default: 8)
|
|
27
|
+
parity: Parity checking mode (default: None)
|
|
28
|
+
stopbits: Number of stop bits (default: 1)
|
|
29
|
+
"""
|
|
30
|
+
self.port = port
|
|
31
|
+
self.baudrate = baudrate
|
|
32
|
+
self._serial: serial.Serial | None = None
|
|
33
|
+
|
|
34
|
+
# Open the serial connection
|
|
35
|
+
self._serial = serial.Serial(
|
|
36
|
+
port=port,
|
|
37
|
+
baudrate=baudrate,
|
|
38
|
+
timeout=timeout,
|
|
39
|
+
bytesize=bytesize,
|
|
40
|
+
parity=parity,
|
|
41
|
+
stopbits=stopbits,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def send(self, data: bytes) -> None:
|
|
45
|
+
"""Send data to the device.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data: Bytes to send
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
RuntimeError: If serial port is not open
|
|
52
|
+
"""
|
|
53
|
+
if not self._serial or not self._serial.is_open:
|
|
54
|
+
raise RuntimeError("Serial port is not open")
|
|
55
|
+
|
|
56
|
+
self._serial.write(data)
|
|
57
|
+
self._serial.flush()
|
|
58
|
+
|
|
59
|
+
def receive(self, num_bytes: int) -> bytes:
|
|
60
|
+
"""Receive data from the device.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
num_bytes: Maximum number of bytes to receive
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Received bytes (may be less than num_bytes if timeout occurs)
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
RuntimeError: If serial port is not open
|
|
70
|
+
"""
|
|
71
|
+
if not self._serial or not self._serial.is_open:
|
|
72
|
+
raise RuntimeError("Serial port is not open")
|
|
73
|
+
|
|
74
|
+
return self._serial.read(num_bytes)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
"""Close the serial connection."""
|
|
78
|
+
if self._serial and self._serial.is_open:
|
|
79
|
+
self._serial.close()
|
|
80
|
+
|
|
81
|
+
def flush_input(self) -> None:
|
|
82
|
+
"""Flush input buffer, discarding all pending data."""
|
|
83
|
+
if self._serial and self._serial.is_open:
|
|
84
|
+
self._serial.reset_input_buffer()
|
|
85
|
+
|
|
86
|
+
def flush_output(self) -> None:
|
|
87
|
+
"""Flush output buffer, waiting for all data to be transmitted."""
|
|
88
|
+
if self._serial and self._serial.is_open:
|
|
89
|
+
self._serial.reset_output_buffer()
|
|
90
|
+
|
|
91
|
+
def bytes_available(self) -> int:
|
|
92
|
+
"""Get number of bytes available in the input buffer.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Number of bytes available to read
|
|
96
|
+
"""
|
|
97
|
+
if not self._serial or not self._serial.is_open:
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
return self._serial.in_waiting
|
|
101
|
+
|
|
102
|
+
def __enter__(self) -> "SerialCommunicator":
|
|
103
|
+
"""Context manager entry."""
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
107
|
+
"""Context manager exit - ensures cleanup."""
|
|
108
|
+
self.close()
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
"""String representation."""
|
|
112
|
+
status = "open" if (self._serial and self._serial.is_open) else "closed"
|
|
113
|
+
return f"<SerialCommunicator(port='{self.port}', baudrate={self.baudrate}, {status})>"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from klab_pytest_toolkit_embedded.debug_probes.interface import DebugProbe
|
|
2
|
+
|
|
3
|
+
import esptool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EspTool(DebugProbe):
|
|
7
|
+
"""ESP debug probe using esptool.py."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, port: str, baudrate: int = 1500000, address: str = "0x0"):
|
|
10
|
+
self._port = port
|
|
11
|
+
self._baudrate = baudrate
|
|
12
|
+
self._address = address
|
|
13
|
+
|
|
14
|
+
def program(self, fw_image: str) -> None:
|
|
15
|
+
cmd = [
|
|
16
|
+
"--chip",
|
|
17
|
+
"esp32",
|
|
18
|
+
"--port",
|
|
19
|
+
self._port,
|
|
20
|
+
"--baud",
|
|
21
|
+
str(self._baudrate),
|
|
22
|
+
"--after",
|
|
23
|
+
"hard_reset",
|
|
24
|
+
"write_flash",
|
|
25
|
+
"-e",
|
|
26
|
+
self._address,
|
|
27
|
+
fw_image,
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
esptool.main(cmd)
|
|
31
|
+
|
|
32
|
+
def reset(self) -> None:
|
|
33
|
+
cmd = [
|
|
34
|
+
"--chip",
|
|
35
|
+
"esp32",
|
|
36
|
+
"--port",
|
|
37
|
+
self._port,
|
|
38
|
+
"--baud",
|
|
39
|
+
str(self._baudrate),
|
|
40
|
+
"reset",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
esptool.main(cmd)
|
|
44
|
+
|
|
45
|
+
def close(self) -> None:
|
|
46
|
+
"""Close the debug probe connection."""
|
|
47
|
+
# esptool does not maintain a persistent connection, so nothing to close.
|
|
48
|
+
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Debug probe interface definition for embedded boards."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DebugProbe(abc.ABC):
|
|
7
|
+
"""Abstract base class for debug probes."""
|
|
8
|
+
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
def program(self, fw_image: str) -> None:
|
|
11
|
+
"""Flash the firmware image to the target device.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
fw_image (str): Path to the firmware image.
|
|
15
|
+
"""
|
|
16
|
+
raise NotImplementedError("program method must be implemented by subclasses")
|
|
17
|
+
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
def reset(self) -> None:
|
|
20
|
+
"""Reset the target device."""
|
|
21
|
+
raise NotImplementedError("reset method must be implemented by subclasses")
|
|
22
|
+
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def close(self) -> None:
|
|
25
|
+
"""Close the debug probe connection."""
|
|
26
|
+
raise NotImplementedError("close method must be implemented by subclasses")
|