scripts: twister: Support multiple ports in pytest-twister-harness

Add support for multiple connections (e.g. UART) from one device,
enabling communication with second core UARTs. Feature implemented
by extracting connection logic from DeviceAdapter to new
DeviceConnection class with specialized implementations:
- SerialConnection for hardware UART ports
- ProcessConnection for native simulation
- FifoConnection for QEMU communication
Each connection maintains separate log files (handler.log,
handler_1.log, etc.) and can be accessed via connection_index
parameter in device methods like readline() and write().
This enables testing multi-core applications where different
cores communicate through separate UART interfaces.

Signed-off-by: Grzegorz Chwierut <grzegorz.chwierut@nordicsemi.no>
This commit is contained in:
Grzegorz Chwierut
2025-11-24 17:13:25 +01:00
committed by Benjamin Cabé
parent 0d448cc2c1
commit 1b7810f0cb
10 changed files with 716 additions and 521 deletions

View File

@@ -310,32 +310,14 @@
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports "I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
"UP015", # https://docs.astral.sh/ruff/rules/redundant-open-modes "UP015", # https://docs.astral.sh/ruff/rules/redundant-open-modes
] ]
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/binary_adapter.py" = [
"E501", # https://docs.astral.sh/ruff/rules/line-too-long
"SIM103", # https://docs.astral.sh/ruff/rules/needless-bool
]
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/device_adapter.py" = [
"E501", # https://docs.astral.sh/ruff/rules/line-too-long
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
]
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/factory.py" = [ "./scripts/pylib/pytest-twister-harness/src/twister_harness/device/factory.py" = [
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports "I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
"UP006", # https://docs.astral.sh/ruff/rules/non-pep585-annotation "UP006", # https://docs.astral.sh/ruff/rules/non-pep585-annotation
"UP035", # https://docs.astral.sh/ruff/rules/deprecated-import "UP035", # https://docs.astral.sh/ruff/rules/deprecated-import
] ]
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/fifo_handler.py" = [ "./scripts/pylib/pytest-twister-harness/src/twister_harness/device/fifo_handler.py" = [
"E501", # https://docs.astral.sh/ruff/rules/line-too-long
"SIM115", # https://docs.astral.sh/ruff/rules/open-file-with-context-handler "SIM115", # https://docs.astral.sh/ruff/rules/open-file-with-context-handler
] ]
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/hardware_adapter.py" = [
"E501", # https://docs.astral.sh/ruff/rules/line-too-long
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
"UP024", # https://docs.astral.sh/ruff/rules/os-error-alias
]
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/qemu_adapter.py" = [
"E501", # https://docs.astral.sh/ruff/rules/line-too-long
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
]
"./scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py" = [ "./scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py" = [
"E501", # https://docs.astral.sh/ruff/rules/line-too-long "E501", # https://docs.astral.sh/ruff/rules/line-too-long
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports "I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
@@ -956,11 +938,6 @@ exclude = [
"./scripts/net/enumerate_http_status.py", "./scripts/net/enumerate_http_status.py",
"./scripts/profiling/stackcollapse.py", "./scripts/profiling/stackcollapse.py",
"./scripts/pylib/build_helpers/domains.py", "./scripts/pylib/build_helpers/domains.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/binary_adapter.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/device_adapter.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/fifo_handler.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/hardware_adapter.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/device/qemu_adapter.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py", "./scripts/pylib/pytest-twister-harness/src/twister_harness/fixtures.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/helpers/mcumgr.py", "./scripts/pylib/pytest-twister-harness/src/twister_harness/helpers/mcumgr.py",
"./scripts/pylib/pytest-twister-harness/src/twister_harness/plugin.py", "./scripts/pylib/pytest-twister-harness/src/twister_harness/plugin.py",

View File

@@ -35,8 +35,9 @@ class BinaryAdapterBase(DeviceAdapter, abc.ABC):
def generate_command(self) -> None: def generate_command(self) -> None:
"""Generate and set command which will be used during running device.""" """Generate and set command which will be used during running device."""
def _flash_and_run(self) -> None: def _device_launch(self) -> None:
self._run_subprocess() self._run_subprocess()
self.connect()
def _run_subprocess(self) -> None: def _run_subprocess(self) -> None:
if not self.command: if not self.command:
@@ -46,6 +47,9 @@ class BinaryAdapterBase(DeviceAdapter, abc.ABC):
log_command(logger, 'Running command', self.command, level=logging.DEBUG) log_command(logger, 'Running command', self.command, level=logging.DEBUG)
try: try:
self._process = subprocess.Popen(self.command, **self.process_kwargs) self._process = subprocess.Popen(self.command, **self.process_kwargs)
# Update all ProcessConnection instances with the new process
for conn in self.connections:
conn.update(process=self._process)
except subprocess.SubprocessError as exc: except subprocess.SubprocessError as exc:
msg = f'Running subprocess failed due to SubprocessError {exc}' msg = f'Running subprocess failed due to SubprocessError {exc}'
logger.error(msg) logger.error(msg)
@@ -59,22 +63,6 @@ class BinaryAdapterBase(DeviceAdapter, abc.ABC):
logger.error(msg) logger.error(msg)
raise TwisterHarnessException(msg) from exc raise TwisterHarnessException(msg) from exc
def _connect_device(self) -> None:
"""
This method was implemented only to imitate standard connect behavior
like in Serial class.
"""
def _disconnect_device(self) -> None:
"""
This method was implemented only to imitate standard disconnect behavior
like in serial connection.
"""
def _close_device(self) -> None:
"""Terminate subprocess"""
self._stop_subprocess()
def _stop_subprocess(self) -> None: def _stop_subprocess(self) -> None:
if self._process is None: if self._process is None:
# subprocess already stopped # subprocess already stopped
@@ -84,30 +72,14 @@ class BinaryAdapterBase(DeviceAdapter, abc.ABC):
terminate_process(self._process, self.base_timeout) terminate_process(self._process, self.base_timeout)
return_code = self._process.wait(self.base_timeout) return_code = self._process.wait(self.base_timeout)
self._process = None self._process = None
for conn in self.connections:
if hasattr(conn, '_process'):
conn._process = None
logger.debug('Running subprocess finished with return code %s', return_code) logger.debug('Running subprocess finished with return code %s', return_code)
def _read_device_output(self) -> bytes: def _close_device(self) -> None:
return self._process.stdout.readline() """Terminate subprocess"""
self._stop_subprocess()
def _write_to_device(self, data: bytes) -> None:
self._process.stdin.write(data)
self._process.stdin.flush()
def _flush_device_output(self) -> None:
if self.is_device_running():
self._process.stdout.flush()
def is_device_running(self) -> bool:
return self._device_run.is_set() and self._is_binary_running()
def _is_binary_running(self) -> bool:
if self._process is None or self._process.poll() is not None:
return False
return True
def is_device_connected(self) -> bool:
"""Return true if device is connected."""
return self.is_device_running() and self._device_connected.is_set()
def _clear_internal_resources(self) -> None: def _clear_internal_resources(self) -> None:
super()._clear_internal_resources() super()._clear_internal_resources()
@@ -131,6 +103,32 @@ class UnitSimulatorAdapter(BinaryAdapterBase):
class CustomSimulatorAdapter(BinaryAdapterBase): class CustomSimulatorAdapter(BinaryAdapterBase):
"""Simulator adapter to run custom simulator"""
def generate_command(self) -> None: def generate_command(self) -> None:
"""Set command to run.""" """Set command to run."""
self.command = [self.west, 'build', '-d', str(self.device_config.app_build_dir), '-t', 'run'] self.command = [
self.west,
'build',
'-d',
str(self.device_config.app_build_dir),
'-t',
'run',
]
class QemuAdapter(BinaryAdapterBase):
"""Simulator adapter to run QEMU"""
def generate_command(self) -> None:
"""Set command to run."""
self.command = [
self.west,
'build',
'-d',
str(self.device_config.app_build_dir),
'-t',
'run',
]
if 'stdin' in self.process_kwargs:
self.process_kwargs.pop('stdin')

View File

@@ -7,19 +7,13 @@ from __future__ import annotations
import abc import abc
import logging import logging
import os import os
import queue
import re
import shutil import shutil
import threading import threading
import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from serial import SerialException
from twister_harness.exceptions import ( from twister_harness.device.device_connection import DeviceConnection, create_device_connections
TwisterHarnessException, from twister_harness.exceptions import TwisterHarnessException
TwisterHarnessTimeoutException,
)
from twister_harness.twister_harness_config import DeviceConfig from twister_harness.twister_harness_config import DeviceConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -32,24 +26,16 @@ class DeviceAdapter(abc.ABC):
it. it.
""" """
_west: str | None = None
def __init__(self, device_config: DeviceConfig) -> None: def __init__(self, device_config: DeviceConfig) -> None:
"""
:param device_config: device configuration
"""
self.device_config: DeviceConfig = device_config self.device_config: DeviceConfig = device_config
self.base_timeout: float = device_config.base_timeout self.base_timeout: float = device_config.base_timeout
self._device_read_queue: queue.Queue = queue.Queue() self._reader_started: threading.Event = threading.Event()
self._reader_thread: threading.Thread | None = None
self._device_run: threading.Event = threading.Event()
self._device_connected: threading.Event = threading.Event()
self.command: list[str] = [] self.command: list[str] = []
self._west: str | None = None
self.handler_log_path: Path = device_config.build_dir / 'handler.log' self.connections: list[DeviceConnection] = create_device_connections(device_config)
self._log_files: list[Path] = [self.handler_log_path] self._log_files: list[Path] = [conn.log_path for conn in self.connections]
def __repr__(self) -> str:
return f'{self.__class__.__name__}()'
@property @property
def env(self) -> dict[str, str]: def env(self) -> dict[str, str]:
@@ -57,171 +43,117 @@ class DeviceAdapter(abc.ABC):
return env return env
def launch(self) -> None: def launch(self) -> None:
""" """Launch the test application on the target device.
Start by closing previously running application (no effect if not
needed). Then, flash and run test application. Finally, start an This method performs the complete device initialization sequence:
internal reader thread capturing an output from a device.
- Close any previously running application (cleanup)
- Generate the execution command if not already set
- Add any extra test arguments to the command
- Start reader threads to capture device output
- Launch the application (flash for hardware, execute for simulators)
The launch process varies by device type:
- Hardware devices: Flash the application and establish serial communication.
May connect before or after flashing depending on device configuration.
- QEMU/simulators: Start subprocess and establish FIFO/pipe communication
- Native simulators: Execute binary and connect via process pipes
""" """
self.close() self.close()
self._clear_internal_resources()
if not self.command: if not self.command:
self.generate_command() self.generate_command()
if self.device_config.extra_test_args: if self.device_config.extra_test_args:
self.command.extend(self.device_config.extra_test_args.split()) self.command.extend(self.device_config.extra_test_args.split())
if self.device_config.type != 'hardware': self.start_reader()
self._flash_and_run() self._device_launch()
self._device_run.set()
self._start_reader_thread()
self.connect()
return
self._device_run.set()
self._start_reader_thread()
if self.device_config.flash_before:
# For hardware devices with shared USB or software USB, connect after flashing.
# Retry for up to 10 seconds for USB-CDC based devices to enumerate.
self._flash_and_run()
self.connect(retry_s = 10)
else:
# On hardware, flash after connecting to COM port, otherwise some messages
# from target can be lost.
self.connect()
self._flash_and_run()
def close(self) -> None: def close(self) -> None:
"""Disconnect, close device and close reader thread.""" """Disconnect, close device and close reader threads."""
if not self._device_run.is_set():
# device already closed
return
self.disconnect() self.disconnect()
self._close_device() self._close_device()
self._device_run.clear() self.stop_reader()
self._join_reader_thread() self._clear_internal_resources()
def connect(self, retry_s: int = 0) -> None: def connect(self, retry_s: int = 0) -> None:
"""Connect to device - allow for output gathering.""" """Connect to device - allow for output gathering."""
if self.is_device_connected(): for connection in self.connections:
logger.debug('Device already connected') connection.connect()
return
if not self.is_device_running():
msg = 'Cannot connect to not working device'
logger.error(msg)
raise TwisterHarnessException(msg)
if retry_s > 0:
retry_cycles = retry_s * 10
for i in range(retry_cycles):
try:
self._connect_device()
break
except SerialException:
if i == retry_cycles - 1:
raise
time.sleep(0.1)
else:
self._connect_device()
self._device_connected.set()
def disconnect(self) -> None: def disconnect(self) -> None:
"""Disconnect device - block output gathering.""" """Disconnect device - block output gathering."""
if not self.is_device_connected(): for connection in self.connections:
logger.debug("Device already disconnected") connection.disconnect()
return
self._disconnect_device()
self._device_connected.clear()
def readline(self, timeout: float | None = None, print_output: bool = True) -> str: def check_connection(self, connection_index: int = 0) -> None:
""" """Validate that the specified connection index exists."""
Read line from device output. If timeout is not provided, then use if connection_index >= len(self.connections):
base_timeout. msg = f'Connection index {connection_index} is out of range.'
"""
timeout = timeout or self.base_timeout
if self.is_device_connected() or not self._device_read_queue.empty():
data = self._read_from_queue(timeout)
else:
msg = 'No connection to the device and no more data to read.'
logger.error(msg)
raise TwisterHarnessException('No connection to the device and no more data to read.')
if print_output:
logger.debug('#: %s', data)
return data
def readlines_until(
self,
regex: str | None = None,
num_of_lines: int | None = None,
timeout: float | None = None,
print_output: bool = True,
) -> list[str]:
"""
Read available output lines produced by device from internal buffer
until following conditions:
1. If regex is provided - read until regex regex is found in read
line (or until timeout).
2. If num_of_lines is provided - read until number of read lines is
equal to num_of_lines (or until timeout).
3. If none of above is provided - return immediately lines collected so
far in internal buffer.
If timeout is not provided, then use base_timeout.
"""
__tracebackhide__ = True # pylint: disable=unused-variable
timeout = timeout or self.base_timeout
if regex:
regex_compiled = re.compile(regex)
lines: list[str] = []
if regex or num_of_lines:
timeout_time: float = time.time() + timeout
while time.time() < timeout_time:
try:
line = self.readline(0.1, print_output)
except TwisterHarnessTimeoutException:
continue
lines.append(line)
if regex and regex_compiled.search(line):
break
if num_of_lines and len(lines) == num_of_lines:
break
else:
if regex is not None:
msg = f'Did not find line "{regex}" within {timeout} seconds'
else:
msg = f'Did not find expected number of lines within {timeout} seconds'
logger.error(msg)
raise AssertionError(msg)
else:
lines = self.readlines(print_output)
return lines
def readlines(self, print_output: bool = True) -> list[str]:
"""
Read all available output lines produced by device from internal buffer.
"""
lines: list[str] = []
while not self._device_read_queue.empty():
line = self.readline(0.1, print_output)
lines.append(line)
return lines
def clear_buffer(self) -> None:
"""
Remove all available output produced by device from internal buffer
(queue).
"""
self.readlines(print_output=False)
def write(self, data: bytes) -> None:
"""Write data bytes to device."""
if not self.is_device_connected():
msg = 'No connection to the device'
logger.error(msg) logger.error(msg)
raise TwisterHarnessException(msg) raise TwisterHarnessException(msg)
self._write_to_device(data)
def readline(self, connection_index: int = 0, **kwargs) -> str:
"""Read a single line from device output.
:param connection_index: Connection port to read from (0=main UART, 1=second core, etc.)
:param kwargs: Additional keyword arguments (timeout, print_output)
:returns: Single line of text without trailing newlines
"""
self.check_connection(connection_index)
if kwargs.get('timeout') is None:
kwargs['timeout'] = self.base_timeout
return self.connections[connection_index].readline(**kwargs)
def readlines(self, connection_index: int = 0, **kwargs) -> list[str]:
"""Read all available output lines produced by device from internal buffer."""
self.check_connection(connection_index)
return self.connections[connection_index].readlines(**kwargs)
def readlines_until(self, connection_index: int = 0, **kwargs) -> list[str]:
"""Read lines from device output until a specific condition is met.
This method provides flexible ways to collect device output: wait for a specific
pattern, collect a fixed number of lines, or get all currently available lines.
:param connection_index: Index of the connection/port to read from (default: 0).
For hardware devices: 0 = main UART, 1 = second core UART, etc.
For QEMU and native: always use 0 (only one connection available)
:param kwargs: Keyword arguments passed to underlying connection including:
- regex - Regular expression pattern to search for
- num_of_lines - Exact number of lines to read
- timeout - Maximum time in seconds to wait (uses base_timeout if not provided)
- print_output - Whether to log each line as it's read (default: True)
:returns: List of output lines without trailing newlines
:raises AssertionError: If timeout expires before condition is met
"""
self.check_connection(connection_index)
return self.connections[connection_index].readlines_until(**kwargs)
def clear_buffer(self) -> None:
"""Remove all available output produced by device from internal buffer."""
for connection in self.connections:
connection.clear_buffer()
def write(self, data: bytes, connection_index: int = 0) -> None:
"""Write data bytes to the target device.
Sends raw bytes to the device through the appropriate communication channel.
The underlying transport mechanism varies by device type: Hardware devices
write to serial/UART port, QEMU devices write to FIFO queue, and Native
simulators write to process stdin pipes.
:param data: Raw bytes to send to the device
:param connection_index: Index of the connection/port to write to (default: 0)
"""
self.check_connection(connection_index)
if not self.connections[connection_index].is_device_connected():
msg = f'Cannot write to not connected device on connection index {connection_index}.'
logger.error(msg)
raise TwisterHarnessException(msg)
self.connections[connection_index]._write_to_device(data)
def initialize_log_files(self, test_name: str = '') -> None: def initialize_log_files(self, test_name: str = '') -> None:
""" """
@@ -232,46 +164,27 @@ class DeviceAdapter(abc.ABC):
with open(log_file_path, 'a+') as log_file: with open(log_file_path, 'a+') as log_file:
log_file.write(f'\n==== Test {test_name} started at {datetime.now()} ====\n') log_file.write(f'\n==== Test {test_name} started at {datetime.now()} ====\n')
def _start_reader_thread(self) -> None: def start_reader(self) -> None:
self._reader_thread = threading.Thread(target=self._handle_device_output, daemon=True) """Start internal reader threads for all connections."""
self._reader_thread.start() if self._reader_started.is_set():
# reader already started
return
self._reader_started.set()
for connection in self.connections:
connection.start_reader_thread(self._reader_started)
def _handle_device_output(self) -> None: def stop_reader(self) -> None:
""" """Stop internal reader threads for all connections."""
This method is dedicated to run it in separate thread to read output if not self._reader_started.is_set():
from device and put them into internal queue and save to log file. # reader already stopped
""" return
with open(self.handler_log_path, 'a+') as log_file: self._reader_started.clear()
while self.is_device_running(): for connection in self.connections:
if self.is_device_connected(): connection.join_reader_thread(self.base_timeout)
output = self._read_device_output().decode(errors='replace').rstrip("\r\n")
if output:
self._device_read_queue.put(output)
log_file.write(f'{output}\n')
log_file.flush()
else:
# ignore output from device
self._flush_device_output()
time.sleep(0.1)
def _read_from_queue(self, timeout: float) -> str:
"""Read data from internal queue"""
try:
data: str | object = self._device_read_queue.get(timeout=timeout)
except queue.Empty as exc:
raise TwisterHarnessTimeoutException(f'Read from device timeout occurred ({timeout}s)') from exc
return data
def _join_reader_thread(self) -> None:
if self._reader_thread is not None:
self._reader_thread.join(self.base_timeout)
self._reader_thread = None
def _clear_internal_resources(self) -> None: def _clear_internal_resources(self) -> None:
self._reader_thread = None for connection in self.connections:
self._device_read_queue = queue.Queue() connection._clear_internal_resources()
self._device_run.clear()
self._device_connected.clear()
@property @property
def west(self) -> str: def west(self) -> str:
@@ -296,42 +209,17 @@ class DeviceAdapter(abc.ABC):
""" """
@abc.abstractmethod @abc.abstractmethod
def _flash_and_run(self) -> None: def _device_launch(self) -> None:
"""Flash and run application on a device.""" """Launch the application on the target device."""
@abc.abstractmethod
def _connect_device(self) -> None:
"""Connect with the device (e.g. via serial port)."""
@abc.abstractmethod
def _disconnect_device(self) -> None:
"""Disconnect from the device (e.g. from serial port)."""
@abc.abstractmethod @abc.abstractmethod
def _close_device(self) -> None: def _close_device(self) -> None:
"""Stop application""" """Stop application on the target device."""
@abc.abstractmethod
def _read_device_output(self) -> bytes:
"""
Read device output directly through serial, subprocess, FIFO, etc.
Even if device is not connected, this method has to return something
(e.g. empty bytes string). This assumption is made to maintain
compatibility between various adapters and their reading technique.
"""
@abc.abstractmethod
def _write_to_device(self, data: bytes) -> None:
"""Write to device directly through serial, subprocess, FIFO, etc."""
@abc.abstractmethod
def _flush_device_output(self) -> None:
"""Flush device connection (serial, subprocess output, FIFO, etc.)"""
@abc.abstractmethod
def is_device_running(self) -> bool:
"""Return true if application is running on device."""
@abc.abstractmethod
def is_device_connected(self) -> bool: def is_device_connected(self) -> bool:
"""Return true if device is connected.""" """
Check if the primary device connection is active.
Added to keep backward compatibility as it is used in fixtures.
"""
return self.connections and self.connections[0].is_device_connected()

View File

@@ -0,0 +1,476 @@
# Copyright (c) 2025 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
import abc
import logging
import os
import re
if os.name != 'nt':
import pty
import queue
import subprocess
import threading
import time
from pathlib import Path
import serial
from twister_harness.device.fifo_handler import FifoHandler
from twister_harness.exceptions import TwisterHarnessException, TwisterHarnessTimeoutException
from twister_harness.twister_harness_config import DeviceConfig, DeviceSerialConfig
logger = logging.getLogger(__name__)
class DeviceConnection(abc.ABC):
"""
Interface for device communication transport layer.
Handles the actual connection mechanism (Serial/UART, FIFO, process, etc.)
"""
def __init__(self, log_path: Path, timeout: float) -> None:
"""Initialize the device connection"""
self.log_path = log_path
self.timeout = timeout
self._device_read_queue: queue.Queue = queue.Queue()
self._reader_thread: threading.Thread | None = None
# Prefix for log messages to differentiate between multiple connections
self.log_prefix: str = ''
def start_reader_thread(self, reader_started: threading.Event) -> None:
"""Start the internal reader thread for the device connection."""
if self._reader_thread is None or not self._reader_thread.is_alive():
self._reader_thread = threading.Thread(
target=self._handle_device_output, args=(reader_started,), daemon=True
)
self._reader_thread.start()
def join_reader_thread(self, timeout: float) -> None:
"""Join the internal reader thread for the device connection."""
if self._reader_thread is not None:
self._reader_thread.join(timeout)
if self._reader_thread.is_alive():
logger.warning("Reader thread did not terminate within timeout")
self._reader_thread = None
def update(self, **kwargs) -> None: # noqa: B027
"""Update connection parameters based on provided keyword arguments."""
def _clear_internal_resources(self) -> None:
self._reader_thread = None
self._device_read_queue = queue.Queue()
def _handle_device_output(self, reader_started: threading.Event) -> None:
"""
This method is dedicated to run it in separate thread to read output
from device and put them into internal queue and save to log file.
"""
with open(self.log_path, 'a+', encoding='utf-8', errors='replace') as log_file:
while reader_started.is_set():
if self.is_device_connected():
output = self._read_device_output().decode(errors='replace').rstrip("\r\n")
if output:
self._device_read_queue.put(output)
log_file.write(f'{output}\n')
log_file.flush()
else:
# ignore output from device
self._flush_device_output()
time.sleep(0.1)
def _read_from_queue(self, timeout: float) -> str:
"""Read data from internal queue"""
try:
data: str | object = self._device_read_queue.get(timeout=timeout)
except queue.Empty as exc:
raise TwisterHarnessTimeoutException(
f'Read from device timeout occurred ({timeout}s)'
) from exc
return data
def readline(self, timeout: float | None = None, print_output: bool = True) -> str:
"""
Read line from device output. If timeout is not provided, then use
base_timeout.
"""
timeout = timeout or self.timeout
if self.is_device_connected() or not self._device_read_queue.empty():
data = self._read_from_queue(timeout)
else:
msg = 'No connection to the device and no more data to read.'
logger.error(msg)
raise TwisterHarnessException('No connection to the device and no more data to read.')
if print_output:
logger.debug('%s#: %s', self.log_prefix, data)
return data
def readlines_until(
self,
regex: str | None = None,
num_of_lines: int | None = None,
timeout: float | None = None,
print_output: bool = True,
) -> list[str]:
"""
Read lines from device output until a specific condition is met.
This method provides flexible ways to collect device output from your test device:
- Wait for a specific pattern/message to appear in the output
- Collect a fixed number of lines
- Get all currently available lines immediately
"""
__tracebackhide__ = True # pylint: disable=unused-variable
timeout = timeout or self.timeout
if regex:
regex_compiled = re.compile(regex)
lines: list[str] = []
if regex or num_of_lines:
timeout_time: float = time.time() + timeout
while time.time() < timeout_time:
try:
line = self.readline(0.1, print_output)
except TwisterHarnessTimeoutException:
continue
lines.append(line)
if regex and regex_compiled.search(line):
break
if num_of_lines and len(lines) == num_of_lines:
break
else:
if regex is not None:
msg = f'Did not find line "{regex}" within {timeout} seconds'
else:
msg = f'Did not find expected number of lines within {timeout} seconds'
logger.error(msg)
raise AssertionError(msg)
else:
lines = self.readlines(print_output)
return lines
def readlines(self, print_output: bool = True) -> list[str]:
"""
Read all available output lines produced by device from internal buffer.
"""
lines: list[str] = []
while not self._device_read_queue.empty():
line = self.readline(0.1, print_output)
lines.append(line)
return lines
def clear_buffer(self) -> None:
"""
Remove all available output produced by device from internal buffer.
"""
self.readlines(print_output=False)
def connect(self) -> None:
"""Connect to device - allow for output gathering."""
if self.is_device_connected():
# Device already connected
return
self._connect_device()
def disconnect(self) -> None:
"""Disconnect device - block output gathering."""
if not self.is_device_connected():
# Device already disconnected
return
self._disconnect_device()
@abc.abstractmethod
def _connect_device(self) -> None:
"""Connect with the device (e.g. via serial port)."""
@abc.abstractmethod
def _disconnect_device(self) -> None:
"""Disconnect from the device (e.g. from serial port)."""
@abc.abstractmethod
def _read_device_output(self) -> bytes:
"""
Read device output directly through serial, subprocess, FIFO, etc.
Even if device is not connected, this method has to return something
(e.g. empty bytes string). This assumption is made to maintain
compatibility between various adapters and their reading technique.
"""
@abc.abstractmethod
def is_device_connected(self) -> bool:
"""Return true if device is connected."""
@abc.abstractmethod
def _write_to_device(self, data: bytes) -> None:
"""Write to device directly through serial, subprocess, FIFO, etc."""
@abc.abstractmethod
def _flush_device_output(self) -> None:
"""Flush device connection (serial, subprocess output, FIFO, etc.)"""
class SerialConnection(DeviceConnection):
"""Serial/UART connection implementation for hardware devices"""
def __init__(self, log_path: Path, timeout: float, serial_config: DeviceSerialConfig) -> None:
"""
Initialize serial connection.
"""
super().__init__(log_path, timeout)
self.serial_config: DeviceSerialConfig = serial_config
self._serial_connection: serial.Serial | None = None
self._serial_pty_proc: subprocess.Popen | None = None
self._serial_buffer: bytearray = bytearray()
def _connect_device(self) -> None:
if self.is_device_connected():
# Device already connected
return
serial_name = self._open_serial_pty() or self.serial_config.port
logger.debug('Opening serial connection for %s', serial_name)
try:
self._serial_connection = serial.Serial(
serial_name,
baudrate=self.serial_config.baud,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=self.timeout,
)
except serial.SerialException as exc:
logger.exception('Cannot open connection: %s', exc)
self._close_serial_pty()
raise
self._serial_connection.flush()
self._serial_connection.reset_input_buffer()
self._serial_connection.reset_output_buffer()
def is_device_connected(self) -> bool:
return self._serial_connection and self._serial_connection.is_open
def _open_serial_pty(self) -> str | None:
"""Open a pty pair, run process and return tty name"""
if not self.serial_config.serial_pty:
return None
try:
master, slave = pty.openpty()
except NameError as exc:
logger.exception('PTY module is not available.')
raise exc
try:
self._serial_pty_proc = subprocess.Popen(
re.split('[, ]', self.serial_config.serial_pty),
stdout=master,
stdin=master,
stderr=master,
)
except subprocess.CalledProcessError as exc:
logger.exception('Failed to run subprocess, error %s', str(exc))
raise
return os.ttyname(slave)
def _disconnect_device(self) -> None:
if self._serial_connection:
serial_name = self._serial_connection.port
self._serial_connection.close()
self._serial_connection = None
logger.debug('Closed serial connection for %s', serial_name)
self._close_serial_pty()
def _close_serial_pty(self) -> None:
"""Terminate the process opened for serial pty script"""
if self._serial_pty_proc:
self._serial_pty_proc.terminate()
self._serial_pty_proc.communicate(timeout=self.timeout)
logger.debug('Process %s terminated', self.serial_config.serial_pty)
self._serial_pty_proc = None
def _read_device_output(self) -> bytes:
try:
output = self._readline_serial()
except (OSError, TypeError):
# serial was probably disconnected
output = b''
return output
def _readline_serial(self) -> bytes:
"""
This method was created to avoid using PySerial built-in readline
method which cause blocking reader thread even if there is no data to
read. Instead for this, following implementation try to read data only
if they are available. Inspiration for this code was taken from this
comment:
https://github.com/pyserial/pyserial/issues/216#issuecomment-369414522
"""
line = self._readline_from_serial_buffer()
if line is not None:
return line
while True:
if self._serial_connection is None or not self._serial_connection.is_open:
return b''
elif self._serial_connection.in_waiting == 0:
time.sleep(0.05)
else:
bytes_to_read = max(1, min(2048, self._serial_connection.in_waiting))
output = self._serial_connection.read(bytes_to_read)
self._serial_buffer.extend(output)
line = self._readline_from_serial_buffer()
if line is not None:
return line
def _readline_from_serial_buffer(self) -> bytes | None:
idx = self._serial_buffer.find(b"\n")
if idx >= 0:
line = self._serial_buffer[: idx + 1]
self._serial_buffer = self._serial_buffer[idx + 1 :]
return bytes(line)
else:
return None
def _write_to_device(self, data: bytes) -> None:
self._serial_connection.write(data)
def _flush_device_output(self) -> None:
if self.is_device_connected():
self._serial_connection.flush()
self._serial_connection.reset_input_buffer()
def _clear_internal_resources(self) -> None:
super()._clear_internal_resources()
self._serial_connection = None
self._serial_pty_proc = None
self._serial_buffer.clear()
class ProcessConnection(DeviceConnection):
"""Process pipe connection implementation for native simulation"""
def __init__(self, log_path: Path, timeout: float) -> None:
"""
Initialize serial connection.
"""
super().__init__(log_path, timeout)
self._process: subprocess.Popen | None = None
def update(self, **kwargs) -> None:
"""
The process instance is set after connection initialization and must be
provided by the binary adapter via this method.
"""
if 'process' in kwargs:
self._process = kwargs['process']
def _read_device_output(self) -> bytes:
return self._process.stdout.readline()
def _connect_device(self) -> None:
"""Connect with the device. Left empty for native simulation."""
def _disconnect_device(self) -> None:
"""Disconnect from the device. Left empty for native simulation."""
def is_device_connected(self) -> bool:
return self._is_binary_running()
def _is_binary_running(self) -> bool:
return self._process is not None and self._process.poll() is None
def _write_to_device(self, data: bytes) -> None:
if self.is_device_connected():
self._process.stdin.write(data)
self._process.stdin.flush()
def _flush_device_output(self) -> None:
if self.is_device_connected():
self._process.stdout.flush()
class FifoConnection(ProcessConnection):
"""FIFO queue connection implementation for QEMU"""
def __init__(self, log_path: Path, timeout: float, fifo_file: Path) -> None:
"""
Initialize serial connection.
"""
super().__init__(log_path, timeout)
self._fifo_connection: FifoHandler = FifoHandler(fifo_file, timeout)
def _connect_device(self) -> None:
"""Create fifo connection"""
if self.is_device_connected():
# Device already connected
return
if not self._is_binary_running():
msg = 'Cannot connect to not working device.'
logger.error(msg)
raise TwisterHarnessException(msg)
self._fifo_connection.initiate_connection()
timeout_time: float = time.time() + self.timeout
while time.time() < timeout_time and self._is_binary_running():
if self._fifo_connection.is_open:
# to flush after reconnection
self._write_to_device(b'\n')
return
time.sleep(0.1)
msg = 'Cannot establish communication with QEMU device.'
logger.error(msg)
raise TwisterHarnessException(msg)
def _disconnect_device(self) -> None:
"""Disconnect fifo connection"""
if self._fifo_connection.is_open:
self._fifo_connection.disconnect()
def _read_device_output(self) -> bytes:
try:
output = self._fifo_connection.readline()
except (OSError, ValueError):
# emulation was probably finished and thus fifo file was closed too
output = b''
return output
def is_device_connected(self) -> bool:
return super().is_device_connected() and self._fifo_connection.is_open
def _write_to_device(self, data: bytes) -> None:
if self.is_device_connected():
self._fifo_connection.write(data)
self._fifo_connection.flush_write()
def _flush_device_output(self) -> None:
if self.is_device_connected():
self._fifo_connection.flush_read()
def _clear_internal_resources(self) -> None:
super()._clear_internal_resources()
self._fifo_connection.cleanup()
def create_device_connections(device_config: DeviceConfig) -> list[DeviceConnection]:
"""Factory method to create device connections based on device configuration."""
connections: list[DeviceConnection] = []
log_path: Path = device_config.build_dir / 'handler.log'
timeout = device_config.base_timeout
if device_config.type == "hardware":
for core, serial_config in enumerate(device_config.serial_configs):
if core > 0:
log_path = device_config.build_dir / f'handler_{core}.log'
connection = SerialConnection(log_path, timeout, serial_config)
connections.append(connection)
elif device_config.type == "qemu":
fifo_file = device_config.build_dir / 'qemu-fifo'
connection = FifoConnection(log_path, timeout, fifo_file)
connections.append(connection)
else: # native
connection = ProcessConnection(log_path, timeout)
connections.append(connection)
# Update log prefixes only for extra connections (not for the first one)
for index, _ in enumerate(connections[1:], start=1):
connections[index].log_prefix = f"[{index}]"
return connections

View File

@@ -9,11 +9,11 @@ from typing import Type
from twister_harness.device.device_adapter import DeviceAdapter from twister_harness.device.device_adapter import DeviceAdapter
from twister_harness.device.hardware_adapter import HardwareAdapter from twister_harness.device.hardware_adapter import HardwareAdapter
from twister_harness.device.qemu_adapter import QemuAdapter
from twister_harness.device.binary_adapter import ( from twister_harness.device.binary_adapter import (
CustomSimulatorAdapter, CustomSimulatorAdapter,
NativeSimulatorAdapter, NativeSimulatorAdapter,
UnitSimulatorAdapter, UnitSimulatorAdapter,
QemuAdapter,
) )
from twister_harness.exceptions import TwisterHarnessException from twister_harness.exceptions import TwisterHarnessException

View File

@@ -44,13 +44,17 @@ class FifoHandler:
thread when timeout will expire. thread when timeout will expire.
""" """
self._stop_waiting_for_opening.clear() self._stop_waiting_for_opening.clear()
self._fifo_opened.clear()
self._make_fifo_file(self._fifo_out_path) self._make_fifo_file(self._fifo_out_path)
self._make_fifo_file(self._fifo_in_path) self._make_fifo_file(self._fifo_in_path)
if self._open_fifo_thread is None: # Only create new threads if they don't exist or are not alive
if self._open_fifo_thread is None or not self._open_fifo_thread.is_alive():
self._open_fifo_thread = threading.Thread(target=self._open_fifo, daemon=True) self._open_fifo_thread = threading.Thread(target=self._open_fifo, daemon=True)
self._open_fifo_thread.start() self._open_fifo_thread.start()
if self._opening_monitor_thread is None: if self._opening_monitor_thread is None or not self._opening_monitor_thread.is_alive():
self._opening_monitor_thread = threading.Thread(target=self._opening_monitor, daemon=True) self._opening_monitor_thread = threading.Thread(
target=self._opening_monitor, daemon=True
)
self._opening_monitor_thread.start() self._opening_monitor_thread.start()
@staticmethod @staticmethod
@@ -90,6 +94,7 @@ class FifoHandler:
def disconnect(self) -> None: def disconnect(self) -> None:
self._stop_waiting_for_opening.set() self._stop_waiting_for_opening.set()
if self._open_fifo_thread and self._open_fifo_thread.is_alive(): if self._open_fifo_thread and self._open_fifo_thread.is_alive():
self._open_fifo_thread.join(timeout=1) self._open_fifo_thread.join(timeout=1)
self._open_fifo_thread = None self._open_fifo_thread = None
@@ -100,9 +105,13 @@ class FifoHandler:
if self._fifo_out_file: if self._fifo_out_file:
self._fifo_out_file.close() self._fifo_out_file.close()
self._fifo_out_file = None
if self._fifo_in_file: if self._fifo_in_file:
self._fifo_in_file.close() self._fifo_in_file.close()
self._fifo_in_file = None
def cleanup(self):
"""Clean up FIFO files when object is destroyed"""
if os.path.exists(self._fifo_out_path): if os.path.exists(self._fifo_out_path):
os.unlink(self._fifo_out_path) os.unlink(self._fifo_out_path)
if os.path.exists(self._fifo_in_path): if os.path.exists(self._fifo_in_path):
@@ -113,8 +122,10 @@ class FifoHandler:
try: try:
return bool( return bool(
self._fifo_opened.is_set() self._fifo_opened.is_set()
and self._fifo_in_file is not None and self._fifo_out_file is not None and self._fifo_in_file is not None
and self._fifo_in_file.fileno() and self._fifo_out_file.fileno() and self._fifo_out_file is not None
and self._fifo_in_file.fileno()
and self._fifo_out_file.fileno()
) )
except ValueError: except ValueError:
return False return False

View File

@@ -5,21 +5,18 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
if os.name != 'nt':
import pty
import re
import subprocess import subprocess
import time import time
from pathlib import Path from pathlib import Path
import serial from serial import SerialException
from twister_harness.device.device_adapter import DeviceAdapter from twister_harness.device.device_adapter import DeviceAdapter
from twister_harness.device.utils import log_command, terminate_process
from twister_harness.exceptions import ( from twister_harness.exceptions import (
TwisterHarnessException, TwisterHarnessException,
TwisterHarnessTimeoutException, TwisterHarnessTimeoutException,
) )
from twister_harness.device.utils import log_command, terminate_process
from twister_harness.twister_harness_config import DeviceConfig from twister_harness.twister_harness_config import DeviceConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -31,9 +28,6 @@ class HardwareAdapter(DeviceAdapter):
def __init__(self, device_config: DeviceConfig) -> None: def __init__(self, device_config: DeviceConfig) -> None:
super().__init__(device_config) super().__init__(device_config)
self._flashing_timeout: float = device_config.flash_timeout self._flashing_timeout: float = device_config.flash_timeout
self._serial_connection: serial.Serial | None = None
self._serial_pty_proc: subprocess.Popen | None = None
self._serial_buffer: bytearray = bytearray()
self.device_log_path: Path = device_config.build_dir / 'device.log' self.device_log_path: Path = device_config.build_dir / 'device.log'
self._log_files.append(self.device_log_path) self._log_files.append(self.device_log_path)
@@ -59,7 +53,8 @@ class HardwareAdapter(DeviceAdapter):
self.west, self.west,
'flash', 'flash',
'--no-rebuild', '--no-rebuild',
'--build-dir', str(self.device_config.build_dir), '--build-dir',
str(self.device_config.build_dir),
] ]
command_extra_args = [] command_extra_args = []
@@ -94,7 +89,10 @@ class HardwareAdapter(DeviceAdapter):
elif runner in ('nrfjprog', 'nrfutil', 'nrfutil_next'): elif runner in ('nrfjprog', 'nrfutil', 'nrfutil_next'):
extra_args.append('--dev-id') extra_args.append('--dev-id')
extra_args.append(board_id) extra_args.append(board_id)
elif runner == 'openocd' and self.device_config.product in ['STM32 STLink', 'STLINK-V3']: elif runner == 'openocd' and self.device_config.product in [
'STM32 STLink',
'STLINK-V3',
]:
extra_args.append('--cmd-pre-init') extra_args.append('--cmd-pre-init')
extra_args.append(f'hla_serial {board_id}') extra_args.append(f'hla_serial {board_id}')
elif runner == 'openocd' and self.device_config.product == 'EDBG CMSIS-DAP': elif runner == 'openocd' and self.device_config.product == 'EDBG CMSIS-DAP':
@@ -103,13 +101,38 @@ class HardwareAdapter(DeviceAdapter):
elif runner == "openocd" and self.device_config.product == "LPC-LINK2 CMSIS-DAP": elif runner == "openocd" and self.device_config.product == "LPC-LINK2 CMSIS-DAP":
extra_args.append("--cmd-pre-init") extra_args.append("--cmd-pre-init")
extra_args.append(f'adapter serial {board_id}') extra_args.append(f'adapter serial {board_id}')
elif runner == 'jlink' or (runner == 'stm32cubeprogrammer' and self.device_config.product != "BOOT-SERIAL"): elif runner == 'jlink' or (
runner == 'stm32cubeprogrammer' and self.device_config.product != "BOOT-SERIAL"
):
base_args.append('--dev-id') base_args.append('--dev-id')
base_args.append(board_id) base_args.append(board_id)
elif runner == 'linkserver': elif runner == 'linkserver':
base_args.append(f'--probe={board_id}') base_args.append(f'--probe={board_id}')
return base_args, extra_args return base_args, extra_args
def _device_launch(self) -> None:
"""Flash and run application on a device and connect with serial port."""
if self.device_config.flash_before:
# For hardware devices with shared USB or software USB, connect after flashing.
# Retry for up to 10 seconds for USB-CDC based devices to enumerate.
self._flash_and_run()
attempt = 0
while True:
try:
self.connect()
break
except SerialException:
if attempt < 100:
attempt += 1
time.sleep(0.1)
else:
raise
else:
# On hardware, flash after connecting to COM port, otherwise some messages
# from target can be lost.
self.connect()
self._flash_and_run()
def _flash_and_run(self) -> None: def _flash_and_run(self) -> None:
"""Flash application on a device.""" """Flash application on a device."""
if not self.command: if not self.command:
@@ -126,7 +149,9 @@ class HardwareAdapter(DeviceAdapter):
process = stdout = None process = stdout = None
try: try:
process = subprocess.Popen(self.command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.env) process = subprocess.Popen(
self.command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.env
)
stdout, _ = process.communicate(timeout=self._flashing_timeout) stdout, _ = process.communicate(timeout=self._flashing_timeout)
except subprocess.TimeoutExpired as exc: except subprocess.TimeoutExpired as exc:
process.kill() process.kill()
@@ -151,141 +176,15 @@ class HardwareAdapter(DeviceAdapter):
logger.error(msg) logger.error(msg)
raise TwisterHarnessException(msg) raise TwisterHarnessException(msg)
def _connect_device(self) -> None:
serial_name = self._open_serial_pty() or self.device_config.serial
logger.debug('Opening serial connection for %s', serial_name)
try:
self._serial_connection = serial.Serial(
serial_name,
baudrate=self.device_config.baud,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=self.base_timeout,
)
except serial.SerialException as exc:
logger.exception('Cannot open connection: %s', exc)
self._close_serial_pty()
raise
self._serial_connection.flush()
self._serial_connection.reset_input_buffer()
self._serial_connection.reset_output_buffer()
def _open_serial_pty(self) -> str | None:
"""Open a pty pair, run process and return tty name"""
if not self.device_config.serial_pty:
return None
try:
master, slave = pty.openpty()
except NameError as exc:
logger.exception('PTY module is not available.')
raise exc
try:
self._serial_pty_proc = subprocess.Popen(
re.split(',| ', self.device_config.serial_pty),
stdout=master,
stdin=master,
stderr=master
)
except subprocess.CalledProcessError as exc:
logger.exception('Failed to run subprocess %s, error %s', self.device_config.serial_pty, str(exc))
raise
return os.ttyname(slave)
def _disconnect_device(self) -> None:
if self._serial_connection:
serial_name = self._serial_connection.port
self._serial_connection.close()
# self._serial_connection = None
logger.debug('Closed serial connection for %s', serial_name)
self._close_serial_pty()
def _close_serial_pty(self) -> None:
"""Terminate the process opened for serial pty script"""
if self._serial_pty_proc:
self._serial_pty_proc.terminate()
self._serial_pty_proc.communicate(timeout=self.base_timeout)
logger.debug('Process %s terminated', self.device_config.serial_pty)
self._serial_pty_proc = None
def _close_device(self) -> None: def _close_device(self) -> None:
if self.device_config.post_script: if self.device_config.post_script:
self._run_custom_script(self.device_config.post_script, self.base_timeout) self._run_custom_script(self.device_config.post_script, self.base_timeout)
def is_device_running(self) -> bool:
return self._device_run.is_set()
def is_device_connected(self) -> bool:
return bool(
self.is_device_running()
and self._device_connected.is_set()
and self._serial_connection
and self._serial_connection.is_open
)
def _read_device_output(self) -> bytes:
try:
output = self._readline_serial()
except (serial.SerialException, TypeError, IOError):
# serial was probably disconnected
output = b''
return output
def _readline_serial(self) -> bytes:
"""
This method was created to avoid using PySerial built-in readline
method which cause blocking reader thread even if there is no data to
read. Instead for this, following implementation try to read data only
if they are available. Inspiration for this code was taken from this
comment:
https://github.com/pyserial/pyserial/issues/216#issuecomment-369414522
"""
line = self._readline_from_serial_buffer()
if line is not None:
return line
while True:
if self._serial_connection is None or not self._serial_connection.is_open:
return b''
elif self._serial_connection.in_waiting == 0:
time.sleep(0.05)
continue
else:
bytes_to_read = max(1, min(2048, self._serial_connection.in_waiting))
output = self._serial_connection.read(bytes_to_read)
self._serial_buffer.extend(output)
line = self._readline_from_serial_buffer()
if line is not None:
return line
def _readline_from_serial_buffer(self) -> bytes | None:
idx = self._serial_buffer.find(b"\n")
if idx >= 0:
line = self._serial_buffer[:idx+1]
self._serial_buffer = self._serial_buffer[idx+1:]
return bytes(line)
else:
return None
def _write_to_device(self, data: bytes) -> None:
self._serial_connection.write(data)
def _flush_device_output(self) -> None:
if self.is_device_connected():
self._serial_connection.flush()
self._serial_connection.reset_input_buffer()
def _clear_internal_resources(self) -> None:
super()._clear_internal_resources()
self._serial_connection = None
self._serial_pty_proc = None
self._serial_buffer.clear()
@staticmethod @staticmethod
def _run_custom_script(script_path: str | Path, timeout: float) -> None: def _run_custom_script(script_path: str | Path, timeout: float) -> None:
with subprocess.Popen(str(script_path), stderr=subprocess.PIPE, stdout=subprocess.PIPE) as proc: with subprocess.Popen(
str(script_path), stderr=subprocess.PIPE, stdout=subprocess.PIPE
) as proc:
try: try:
stdout, stderr = proc.communicate(timeout=timeout) stdout, stderr = proc.communicate(timeout=timeout)
logger.debug(stdout.decode()) logger.debug(stdout.decode())

View File

@@ -1,67 +0,0 @@
# Copyright (c) 2023 Nordic Semiconductor ASA
#
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import logging
import time
from twister_harness.device.fifo_handler import FifoHandler
from twister_harness.device.binary_adapter import BinaryAdapterBase
from twister_harness.exceptions import TwisterHarnessException
from twister_harness.twister_harness_config import DeviceConfig
logger = logging.getLogger(__name__)
class QemuAdapter(BinaryAdapterBase):
def __init__(self, device_config: DeviceConfig) -> None:
super().__init__(device_config)
qemu_fifo_file_path = self.device_config.build_dir / 'qemu-fifo'
self._fifo_connection: FifoHandler = FifoHandler(qemu_fifo_file_path, self.base_timeout)
def generate_command(self) -> None:
"""Set command to run."""
self.command = [self.west, 'build', '-d', str(self.device_config.app_build_dir), '-t', 'run']
if 'stdin' in self.process_kwargs:
self.process_kwargs.pop('stdin')
def _flash_and_run(self) -> None:
super()._flash_and_run()
self._create_fifo_connection()
def _create_fifo_connection(self) -> None:
self._fifo_connection.initiate_connection()
timeout_time: float = time.time() + self.base_timeout
while time.time() < timeout_time and self._is_binary_running():
if self._fifo_connection.is_open:
return
time.sleep(0.1)
msg = 'Cannot establish communication with QEMU device.'
logger.error(msg)
raise TwisterHarnessException(msg)
def _stop_subprocess(self) -> None:
super()._stop_subprocess()
self._fifo_connection.disconnect()
def _read_device_output(self) -> bytes:
try:
output = self._fifo_connection.readline()
except (OSError, ValueError):
# emulation was probably finished and thus fifo file was closed too
output = b''
return output
def _write_to_device(self, data: bytes) -> None:
self._fifo_connection.write(data)
self._fifo_connection.flush_write()
def _flush_device_output(self) -> None:
if self.is_device_running():
self._fifo_connection.flush_read()
def is_device_connected(self) -> bool:
"""Return true if device is connected."""
return bool(super().is_device_connected() and self._fifo_connection.is_open)

View File

@@ -97,7 +97,7 @@ def mcumgr(device_object: DeviceAdapter) -> Generator[MCUmgr, None, None]:
"""Fixture to create an MCUmgr instance for serial connection.""" """Fixture to create an MCUmgr instance for serial connection."""
if not MCUmgr.is_available(): if not MCUmgr.is_available():
pytest.skip('mcumgr not available') pytest.skip('mcumgr not available')
yield MCUmgr.create_for_serial(device_object.device_config.serial) yield MCUmgr.create_for_serial(device_object.device_config.serial_configs[0].port)
@pytest.fixture() @pytest.fixture()

View File

@@ -15,6 +15,13 @@ import pytest
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@dataclass
class DeviceSerialConfig:
port: str
baud: int = 115200
serial_pty: str = ''
@dataclass @dataclass
class DeviceConfig: class DeviceConfig:
type: str type: str
@@ -22,13 +29,11 @@ class DeviceConfig:
base_timeout: float = 60.0 # [s] base_timeout: float = 60.0 # [s]
flash_timeout: float = 60.0 # [s] flash_timeout: float = 60.0 # [s]
platform: str = '' platform: str = ''
serial: str = '' serial_configs: list[DeviceSerialConfig] = field(default_factory=list)
baud: int = 115200
runner: str = '' runner: str = ''
runner_params: list[str] = field(default_factory=list, repr=False) runner_params: list[str] = field(default_factory=list, repr=False)
id: str = '' id: str = ''
product: str = '' product: str = ''
serial_pty: str = ''
flash_before: bool = False flash_before: bool = False
west_flash_extra_args: list[str] = field(default_factory=list, repr=False) west_flash_extra_args: list[str] = field(default_factory=list, repr=False)
flash_command: str = '' flash_command: str = ''
@@ -68,19 +73,27 @@ class TwisterHarnessConfig:
runner_params: list[str] = [] runner_params: list[str] = []
if config.option.runner_params: if config.option.runner_params:
runner_params = [w.strip() for w in config.option.runner_params] runner_params = [w.strip() for w in config.option.runner_params]
serial_configs: list[DeviceSerialConfig] = []
if config.option.device_serial:
for serial_port in config.option.device_serial:
serial_configs.append(
DeviceSerialConfig(
port=serial_port,
baud=config.option.device_serial_baud,
serial_pty=config.option.device_serial_pty
)
)
device_from_cli = DeviceConfig( device_from_cli = DeviceConfig(
type=config.option.device_type, type=config.option.device_type,
build_dir=_cast_to_path(config.option.build_dir), build_dir=_cast_to_path(config.option.build_dir),
base_timeout=config.option.base_timeout, base_timeout=config.option.base_timeout,
flash_timeout=config.option.flash_timeout, flash_timeout=config.option.flash_timeout,
platform=config.option.platform, platform=config.option.platform,
serial=config.option.device_serial[0] if config.option.device_serial else '', serial_configs=serial_configs,
baud=config.option.device_serial_baud,
runner=config.option.runner, runner=config.option.runner,
runner_params=runner_params, runner_params=runner_params,
id=config.option.device_id, id=config.option.device_id,
product=config.option.device_product, product=config.option.device_product,
serial_pty=config.option.device_serial_pty,
flash_before=bool(config.option.flash_before), flash_before=bool(config.option.flash_before),
west_flash_extra_args=west_flash_extra_args, west_flash_extra_args=west_flash_extra_args,
flash_command=flash_command, flash_command=flash_command,