klab-pytest-toolkit-embedded 1.1.0__py3-none-any.whl

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.
@@ -0,0 +1,4 @@
1
+ """Klab Pytest Toolkit - Embedded package."""
2
+
3
+ __version__ = "1.1.0"
4
+ __all__ = []
@@ -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()
@@ -0,0 +1,4 @@
1
+ from klab_pytest_toolkit_embedded.communicators.interface import CommunicatorInterface
2
+ from klab_pytest_toolkit_embedded.communicators.serial import SerialCommunicator
3
+
4
+ __all__ = ["CommunicatorInterface", "SerialCommunicator"]
@@ -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,4 @@
1
+ from klab_pytest_toolkit_embedded.debug_probes.interface import DebugProbe
2
+ from klab_pytest_toolkit_embedded.debug_probes.esp import EspTool
3
+
4
+ __all__ = ["DebugProbe", "EspTool"]
@@ -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")
@@ -0,0 +1,6 @@
1
+ """Pytest plugin for embedded testing."""
2
+
3
+
4
+ def pytest_configure(config):
5
+ """Register custom markers and configuration for klab-pytest-toolkit-embedded."""
6
+ pass
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/klab-pytest-toolkit-embedded)](https://pypi.org/project/klab-pytest-toolkit-embedded/)
23
+ [![Python](https://img.shields.io/pypi/pyversions/klab-pytest-toolkit-embedded)](https://pypi.org/project/klab-pytest-toolkit-embedded/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../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,13 @@
1
+ klab_pytest_toolkit_embedded/__init__.py,sha256=XuKlGM4HmHN1IY-4P6b7v7hUMqnU4QpWZSc8qjds6nc,82
2
+ klab_pytest_toolkit_embedded/board.py,sha256=ygFculAQPWVs0QUoqGqSpJPrsHD7Ok9BLO3VTym7GGA,3423
3
+ klab_pytest_toolkit_embedded/plugin.py,sha256=jcsFRt-xUmol-6vJmHdWxyMkalE3Pkmqg8KKlgYCNS4,169
4
+ klab_pytest_toolkit_embedded/communicators/__init__.py,sha256=aGxQIBkucDVkL2GfkUr7oMke-I8XY79GV-thCNAc1HQ,227
5
+ klab_pytest_toolkit_embedded/communicators/interface.py,sha256=J8LKxoekqkQ2e1vYFg7LzW212QqGxDpWbm0JLF0EXag,591
6
+ klab_pytest_toolkit_embedded/communicators/serial.py,sha256=K-Fzin_ePh4Kh-fp5yGIohJ-rfbZ-Z9SG6deMnYH9aY,3574
7
+ klab_pytest_toolkit_embedded/debug_probes/__init__.py,sha256=Y-24bcddqoXKWKpWyz6tdNYN-cFfrJl2MaCh17ULi8U,178
8
+ klab_pytest_toolkit_embedded/debug_probes/esp.py,sha256=B7UHM3tjqDcmVvU-OpUwC0r5M3iisyYK50QfzfQ5Syo,1147
9
+ klab_pytest_toolkit_embedded/debug_probes/interface.py,sha256=p3h9VPdcnk-BLINOjNmwbbwdjLBCNVYGPtsGAcc95D4,810
10
+ klab_pytest_toolkit_embedded-1.1.0.dist-info/METADATA,sha256=JBd6mW3teoq8lT09xL6ecNHkDv8yw3A8CBsDbb771u0,6267
11
+ klab_pytest_toolkit_embedded-1.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
12
+ klab_pytest_toolkit_embedded-1.1.0.dist-info/entry_points.txt,sha256=kUVjtsOnIuuc3h6iU-rqoUD_yqrV4k2Z-JkewJT6Dbg,78
13
+ klab_pytest_toolkit_embedded-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ klab_pytest_toolkit_embedded = klab_pytest_toolkit_embedded.plugin