scripts: pytest: improve plugin options

Add better description of available options and add actions which
should help to prevent from passing invalid options to plugin.

Signed-off-by: Piotr Golyzniak <piotr.golyzniak@nordicsemi.no>
This commit is contained in:
Piotr Golyzniak 2023-08-04 17:33:39 +02:00 committed by Anas Nashif
commit eb672ddd0a
11 changed files with 102 additions and 109 deletions

View file

@ -7,7 +7,6 @@ from __future__ import annotations
import abc
import logging
import subprocess
from pathlib import Path
from twister_harness.device.device_adapter import DeviceAdapter
from twister_harness.device.utils import log_command, terminate_process
@ -119,7 +118,7 @@ class NativeSimulatorAdapter(BinaryAdapterBase):
def generate_command(self) -> None:
"""Set command to run."""
self.command = [str(Path(self.device_config.build_dir) / 'zephyr' / 'zephyr.exe')]
self.command = [str(self.device_config.build_dir / 'zephyr' / 'zephyr.exe')]
class UnitSimulatorAdapter(BinaryAdapterBase):
@ -127,7 +126,7 @@ class UnitSimulatorAdapter(BinaryAdapterBase):
def generate_command(self) -> None:
"""Set command to run."""
self.command = [str(Path(self.device_config.build_dir) / 'testbinary')]
self.command = [str(self.device_config.build_dir / 'testbinary')]
class CustomSimulatorAdapter(BinaryAdapterBase):

View file

@ -40,7 +40,7 @@ class DeviceAdapter(abc.ABC):
self.command: list[str] = []
self._west: str | None = None
self.handler_log_path: Path = Path(device_config.build_dir) / 'handler.log'
self.handler_log_path: Path = device_config.build_dir / 'handler.log'
self._log_files: list[Path] = [self.handler_log_path]
def __repr__(self) -> str:

View file

@ -37,9 +37,9 @@ class DeviceFactory:
logger.debug('Get device type "%s"', name)
try:
return cls._devices[name]
except KeyError as e:
except KeyError as exc:
logger.error('There is no device with name "%s"', name)
raise TwisterHarnessException(f'There is no device with name "{name}"') from e
raise TwisterHarnessException(f'There is no device with name "{name}"') from exc
DeviceFactory.register_device_class('custom', CustomSimulatorAdapter)

View file

@ -34,7 +34,7 @@ class HardwareAdapter(DeviceAdapter):
self._serial_pty_proc: subprocess.Popen | None = None
self._serial_buffer: bytearray = bytearray()
self.device_log_path: 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)
def generate_command(self) -> None:

View file

@ -6,7 +6,6 @@ from __future__ import annotations
import logging
import time
from pathlib import Path
from twister_harness.device.fifo_handler import FifoHandler
from twister_harness.device.binary_adapter import BinaryAdapterBase
@ -19,7 +18,7 @@ logger = logging.getLogger(__name__)
class QemuAdapter(BinaryAdapterBase):
def __init__(self, device_config: DeviceConfig) -> None:
super().__init__(device_config)
qemu_fifo_file_path = Path(self.device_config.build_dir) / 'qemu-fifo'
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:

View file

@ -2,6 +2,7 @@
#
# SPDX-License-Identifier: Apache-2.0
class TwisterHarnessException(Exception):
"""General Twister harness exception."""

View file

@ -6,7 +6,6 @@ from __future__ import annotations
import logging
import os
from pathlib import Path
import pytest
@ -26,65 +25,13 @@ def pytest_addoption(parser: pytest.Parser):
'--twister-harness',
action='store_true',
default=False,
help='Activate Twister harness plugin'
help='Activate Twister harness plugin.'
)
parser.addini(
'twister_harness',
'Activate Twister harness plugin',
type='bool'
)
twister_harness_group.addoption(
'-O',
'--outdir',
metavar='PATH',
dest='output_dir',
help='Output directory for logs. If not provided then use '
'--build-dir path as default.'
)
twister_harness_group.addoption(
'--platform',
help='Choose specific platform'
)
twister_harness_group.addoption(
'--device-type',
choices=('native', 'qemu', 'hardware', 'unit', 'custom'),
help='Choose type of device (hardware, qemu, etc.)'
)
twister_harness_group.addoption(
'--device-serial',
help='Serial device for accessing the board '
'(e.g., /dev/ttyACM0)'
)
twister_harness_group.addoption(
'--device-serial-baud',
type=int,
default=115200,
help='Serial device baud rate (default 115200)'
)
twister_harness_group.addoption(
'--runner',
help='use the specified west runner (pyocd, nrfjprog, etc)'
)
twister_harness_group.addoption(
'--device-id',
help='ID of connected hardware device (for example 000682459367)'
)
twister_harness_group.addoption(
'--device-product',
help='Product name of connected hardware device (for example "STM32 STLink")'
)
twister_harness_group.addoption(
'--device-serial-pty',
metavar='PATH',
help='Script for controlling pseudoterminal. '
'E.g --device-testing --device-serial-pty=<script>'
)
twister_harness_group.addoption(
'--west-flash-extra-args',
help='Extend parameters for west flash. '
'E.g. --west-flash-extra-args="--board-id=foobar,--erase" '
'will translate to "west flash -- --board-id=foobar --erase"'
)
twister_harness_group.addoption(
'--base-timeout',
type=float,
@ -95,26 +42,64 @@ def pytest_addoption(parser: pytest.Parser):
)
twister_harness_group.addoption(
'--build-dir',
dest='build_dir',
metavar='PATH',
help='Directory with built application.'
)
twister_harness_group.addoption(
'--binary-file',
metavar='PATH',
help='Path to file which should be flashed.'
'--device-type',
choices=('native', 'qemu', 'hardware', 'unit', 'custom'),
help='Choose type of device (hardware, qemu, etc.).'
)
twister_harness_group.addoption(
'--platform',
help='Name of used platform (qemu_x86, nrf52840dk_nrf52840, etc.).'
)
twister_harness_group.addoption(
'--device-serial',
help='Serial device for accessing the board (e.g., /dev/ttyACM0).'
)
twister_harness_group.addoption(
'--device-serial-baud',
type=int,
default=115200,
help='Serial device baud rate (default 115200).'
)
twister_harness_group.addoption(
'--runner',
help='Use the specified west runner (pyocd, nrfjprog, etc.).'
)
twister_harness_group.addoption(
'--device-id',
help='ID of connected hardware device (for example 000682459367).'
)
twister_harness_group.addoption(
'--device-product',
help='Product name of connected hardware device (e.g. "STM32 STLink").'
)
twister_harness_group.addoption(
'--device-serial-pty',
help='Script for controlling pseudoterminal.'
)
twister_harness_group.addoption(
'--west-flash-extra-args',
help='Extend parameters for west flash. '
'E.g. --west-flash-extra-args="--board-id=foobar,--erase" '
'will translate to "west flash -- --board-id=foobar --erase".'
)
twister_harness_group.addoption(
'--pre-script',
metavar='PATH'
)
twister_harness_group.addoption(
'--post-script',
metavar='PATH'
metavar='PATH',
help='Script executed before flashing and connecting to serial.'
)
twister_harness_group.addoption(
'--post-flash-script',
metavar='PATH'
metavar='PATH',
help='Script executed after flashing.'
)
twister_harness_group.addoption(
'--post-script',
metavar='PATH',
help='Script executed after closing serial connection.'
)
@ -125,24 +110,31 @@ def pytest_configure(config: pytest.Config):
if not (config.getoption('twister_harness') or config.getini('twister_harness')):
return
validate_options(config)
if config.option.output_dir is None:
config.option.output_dir = config.option.build_dir
config.option.output_dir = _normalize_path(config.option.output_dir)
# create output directory if not exists
os.makedirs(config.option.output_dir, exist_ok=True)
_normalize_paths(config)
_validate_options(config)
config.twister_harness_config = TwisterHarnessConfig.create(config) # type: ignore
def validate_options(config: pytest.Config) -> None:
"""Verify if user provided proper options"""
# TBD
def _validate_options(config: pytest.Config) -> None:
if not config.option.build_dir:
raise Exception('--build-dir has to be provided')
if not os.path.isdir(config.option.build_dir):
raise Exception(f'Provided --build-dir does not exist: {config.option.build_dir}')
if not config.option.device_type:
raise Exception('--device-type has to be provided')
def _normalize_path(path: str | Path) -> str:
path = os.path.expanduser(os.path.expandvars(path))
path = os.path.normpath(os.path.abspath(path))
def _normalize_paths(config: pytest.Config) -> None:
"""Normalize paths provided by user via CLI"""
config.option.build_dir = _normalize_path(config.option.build_dir)
config.option.pre_script = _normalize_path(config.option.pre_script)
config.option.post_script = _normalize_path(config.option.post_script)
config.option.post_flash_script = _normalize_path(config.option.post_flash_script)
def _normalize_path(path: str | None) -> str:
if path is not None:
path = os.path.expanduser(os.path.expandvars(path))
path = os.path.normpath(os.path.abspath(path))
return path

View file

@ -15,8 +15,10 @@ logger = logging.getLogger(__name__)
@dataclass
class DeviceConfig:
type: str
build_dir: Path
base_timeout: float = 60.0 # [s]
platform: str = ''
type: str = ''
serial: str = ''
baud: int = 115200
runner: str = ''
@ -24,25 +26,20 @@ class DeviceConfig:
product: str = ''
serial_pty: str = ''
west_flash_extra_args: list[str] = field(default_factory=list, repr=False)
base_timeout: float = 60.0 # [s]
build_dir: Path | str = ''
binary_file: Path | str = ''
name: str = ''
pre_script: str = ''
post_script: str = ''
post_flash_script: str = ''
pre_script: Path | None = None
post_script: Path | None = None
post_flash_script: Path | None = None
@dataclass
class TwisterHarnessConfig:
"""Store Twister harness configuration to have easy access in test."""
output_dir: Path = Path('twister_harness_out')
devices: list[DeviceConfig] = field(default_factory=list, repr=False)
@classmethod
def create(cls, config: pytest.Config) -> TwisterHarnessConfig:
"""Create new instance from pytest.Config."""
output_dir: Path = config.option.output_dir
devices = []
@ -50,8 +47,10 @@ class TwisterHarnessConfig:
if config.option.west_flash_extra_args:
west_flash_extra_args = [w.strip() for w in config.option.west_flash_extra_args.split(',')]
device_from_cli = DeviceConfig(
platform=config.option.platform,
type=config.option.device_type,
build_dir=_cast_to_path(config.option.build_dir),
base_timeout=config.option.base_timeout,
platform=config.option.platform,
serial=config.option.device_serial,
baud=config.option.device_serial_baud,
runner=config.option.runner,
@ -59,17 +58,19 @@ class TwisterHarnessConfig:
product=config.option.device_product,
serial_pty=config.option.device_serial_pty,
west_flash_extra_args=west_flash_extra_args,
base_timeout=config.option.base_timeout,
build_dir=config.option.build_dir,
binary_file=config.option.binary_file,
pre_script=config.option.pre_script,
post_script=config.option.post_script,
post_flash_script=config.option.post_flash_script,
pre_script=_cast_to_path(config.option.pre_script),
post_script=_cast_to_path(config.option.post_script),
post_flash_script=_cast_to_path(config.option.post_flash_script),
)
devices.append(device_from_cli)
return cls(
output_dir=output_dir,
devices=devices
)
def _cast_to_path(path: str | None) -> Path | None:
if path is None:
return None
return Path(path)

View file

@ -20,7 +20,7 @@ from twister_harness.twister_harness_config import DeviceConfig
@pytest.fixture(name='device')
def fixture_adapter(tmp_path) -> NativeSimulatorAdapter:
return NativeSimulatorAdapter(DeviceConfig(build_dir=tmp_path))
return NativeSimulatorAdapter(DeviceConfig(build_dir=tmp_path, type='native'))
def test_if_simulator_adapter_runs_without_errors(
@ -106,7 +106,7 @@ def test_if_native_simulator_adapter_get_command_returns_proper_string(
@mock.patch('shutil.which', return_value='west')
def test_if_custom_simulator_adapter_get_command_returns_proper_string(patched_which, tmp_path: Path) -> None:
device = CustomSimulatorAdapter(DeviceConfig(build_dir=tmp_path))
device = CustomSimulatorAdapter(DeviceConfig(build_dir=tmp_path, type='custom'))
device.generate_command()
assert isinstance(device.command, list)
assert device.command == ['west', 'build', '-d', str(tmp_path), '-t', 'run']
@ -114,13 +114,13 @@ def test_if_custom_simulator_adapter_get_command_returns_proper_string(patched_w
@mock.patch('shutil.which', return_value=None)
def test_if_custom_simulator_adapter_raise_exception_when_west_not_found(patched_which, tmp_path: Path) -> None:
device = CustomSimulatorAdapter(DeviceConfig(build_dir=tmp_path))
device = CustomSimulatorAdapter(DeviceConfig(build_dir=tmp_path, type='custom'))
with pytest.raises(TwisterHarnessException, match='west not found'):
device.generate_command()
def test_if_unit_simulator_adapter_get_command_returns_proper_string(tmp_path: Path) -> None:
device = UnitSimulatorAdapter(DeviceConfig(build_dir=tmp_path))
device = UnitSimulatorAdapter(DeviceConfig(build_dir=tmp_path, type='unit'))
device.generate_command()
assert isinstance(device.command, list)
assert device.command == [str(tmp_path / 'testbinary')]

View file

@ -18,6 +18,7 @@ def fixture_adapter(tmp_path) -> HardwareAdapter:
build_dir = tmp_path / 'build_dir'
os.mkdir(build_dir)
device_config = DeviceConfig(
type='hardware',
runner='runner',
build_dir=build_dir,
platform='platform',

View file

@ -18,7 +18,7 @@ from twister_harness.twister_harness_config import DeviceConfig
def fixture_device_adapter(tmp_path) -> Generator[QemuAdapter, None, None]:
build_dir = tmp_path / 'build_dir'
os.mkdir(build_dir)
device = QemuAdapter(DeviceConfig(build_dir=build_dir))
device = QemuAdapter(DeviceConfig(build_dir=build_dir, type='qemu'))
try:
yield device
finally: