diff --git a/.github/workflows/test-build-docs.yml b/.github/workflows/test-build-docs.yml index 91fe98a1..5ce7dfd9 100644 --- a/.github/workflows/test-build-docs.yml +++ b/.github/workflows/test-build-docs.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.7" + python-version: "3.10" - name: Install dependencies run: | bash foreach.sh install diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 403a9fb1..9831de11 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: include: - - python-version: "3.7" + - python-version: "3.10" arch: "ARM64" - python-version: "3.13" arch: "X64" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 540e3017..d7df0196 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,19 +8,18 @@ exclude: | repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: mixed-line-ending args: ["-f=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.11.6" + rev: "v0.12.12" hooks: - - id: ruff + - id: ruff-check args: ["--fix"] - id: ruff-format - args: ["--preview"] # documentation - repo: https://github.com/sphinx-contrib/sphinx-lint diff --git a/CHANGELOG.md b/CHANGELOG.md index 22772481..2efc1318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # CHANGELOG +## v2.0.0 (TBD) + +### Breaking Changes + +- **Python Support**: Drop support for Python 3.7, 3.8, 3.9. Now requires Python 3.10+ +- **esptool**: Update esptool requirement to >=5.1.dev1,<6 (from ~=4.9) +- **Deprecated Code Removal**: + - Remove `EsptoolArgs` class from `pytest-embedded-serial-esp` + - Remove deprecated parameters `hard_reset_after` and `no_stub` from `use_esptool()` decorator + - Remove deprecated `stub` property from `EspSerial` class (use `esp` instead) + - Remove deprecated `parse_test_menu()` and `parse_unity_menu_from_str()` methods from `IdfUnityDutMixin` (use `test_menu` property instead) + - Remove deprecated CLI option `--add-target-as-marker` (use `--add-target-as-marker-with-amount` instead) + +### Migration Guide + +1. **Python Version**: Upgrade to Python 3.10 or higher +2. **esptool**: Update esptool to version 5.1.dev1 or higher (but less than 6.0) +3. **Code Changes**: + - Replace `dut.stub` with `dut.esp` + - Replace `dut.parse_test_menu()` calls with `dut.test_menu` property access + - Replace `parse_unity_menu_from_str()` with `_parse_unity_menu_from_str()` if needed. `dut.test_menu` is preferred. + - Update CLI usage from `--add-target-as-marker` to `--add-target-as-marker-with-amount` + - Remove any usage of `EsptoolArgs` class + - Remove `hard_reset_after` and `no_stub` parameters from `use_esptool()` calls + ## v1.17.0a0 (2025-08-07) ### Feat diff --git a/conftest.py b/conftest.py index 441af673..65fd7280 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,7 @@ import shutil import sys import textwrap -from typing import List, Pattern +from re import Pattern import pytest from _pytest.config import Config @@ -59,7 +59,7 @@ def cache_file_remove(cache_dir): @pytest.fixture def first_index_of_messages(): - def _fake(_pattern: Pattern, _messages: List[str], _start: int = 0) -> int: + def _fake(_pattern: Pattern, _messages: list[str], _start: int = 0) -> int: for i, _message in enumerate(_messages): if _pattern.match(_message) and i >= _start: return i diff --git a/pytest-embedded-arduino/pyproject.toml b/pytest-embedded-arduino/pyproject.toml index dc546ea3..c3bfd356 100644 --- a/pytest-embedded-arduino/pyproject.toml +++ b/pytest-embedded-arduino/pyproject.toml @@ -18,9 +18,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -29,7 +26,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded~=1.17.0a2", diff --git a/pytest-embedded-arduino/pytest_embedded_arduino/app.py b/pytest-embedded-arduino/pytest_embedded_arduino/app.py index 0f4533b6..3a275365 100644 --- a/pytest-embedded-arduino/pytest_embedded_arduino/app.py +++ b/pytest-embedded-arduino/pytest_embedded_arduino/app.py @@ -1,6 +1,6 @@ import json import os -from typing import ClassVar, Dict, List, Tuple +from typing import ClassVar from pytest_embedded.app import App @@ -17,7 +17,7 @@ class ArduinoApp(App): """ #: dict of flash settings - flash_settings: ClassVar[Dict[str, Dict[str, str]]] = { + flash_settings: ClassVar[dict[str, dict[str, str]]] = { 'esp32': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 'esp32s2': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, 'esp32c3': {'flash_mode': 'dio', 'flash_size': 'detect', 'flash_freq': '80m'}, @@ -28,7 +28,7 @@ class ArduinoApp(App): } #: dict of binaries' offset. - binary_offsets: ClassVar[Dict[str, List[int]]] = { + binary_offsets: ClassVar[dict[str, list[int]]] = { 'esp32': [0x1000, 0x8000, 0x10000], 'esp32s2': [0x1000, 0x8000, 0x10000], 'esp32c3': [0x0, 0x8000, 0x10000], @@ -57,7 +57,7 @@ def _get_fqbn(self, build_path) -> str: fqbn = options['fqbn'] return fqbn - def _get_bin_files(self, build_path, sketch, target) -> List[Tuple[int, str, bool]]: + def _get_bin_files(self, build_path, sketch, target) -> list[tuple[int, str, bool]]: bootloader = os.path.realpath(os.path.join(build_path, sketch + '.ino.bootloader.bin')) partitions = os.path.realpath(os.path.join(build_path, sketch + '.ino.partitions.bin')) app = os.path.realpath(os.path.join(build_path, sketch + '.ino.bin')) diff --git a/pytest-embedded-arduino/pytest_embedded_arduino/serial.py b/pytest-embedded-arduino/pytest_embedded_arduino/serial.py index 2cd402f0..22dee44b 100644 --- a/pytest-embedded-arduino/pytest_embedded_arduino/serial.py +++ b/pytest-embedded-arduino/pytest_embedded_arduino/serial.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import esptool from pytest_embedded_serial_esp.serial import EspSerial @@ -19,7 +18,7 @@ class ArduinoSerial(EspSerial): def __init__( self, app: ArduinoApp, - target: Optional[str] = None, + target: str | None = None, **kwargs, ) -> None: self.app = app diff --git a/pytest-embedded-idf/pyproject.toml b/pytest-embedded-idf/pyproject.toml index e4f20add..9e841e09 100644 --- a/pytest-embedded-idf/pyproject.toml +++ b/pytest-embedded-idf/pyproject.toml @@ -17,9 +17,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -28,7 +25,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded~=1.17.0a2", diff --git a/pytest-embedded-idf/pytest_embedded_idf/app.py b/pytest-embedded-idf/pytest_embedded_idf/app.py index 4933cc56..942e4285 100644 --- a/pytest-embedded-idf/pytest_embedded_idf/app.py +++ b/pytest-embedded-idf/pytest_embedded_idf/app.py @@ -4,7 +4,7 @@ import shlex import subprocess import sys -from typing import Any, ClassVar, Dict, List, NamedTuple, Optional, Tuple +from typing import Any, ClassVar, NamedTuple from pytest_embedded.app import App @@ -26,8 +26,8 @@ class IdfApp(App): flash_settings (dict[str, Any]): dict of flash settings """ - XTENSA_TARGETS: ClassVar[List[str]] = ['esp32', 'esp32s2', 'esp32s3'] - RISCV32_TARGETS: ClassVar[List[str]] = [ + XTENSA_TARGETS: ClassVar[list[str]] = ['esp32', 'esp32s2', 'esp32s3'] + RISCV32_TARGETS: ClassVar[list[str]] = [ 'esp32c3', 'esp32c2', 'esp32c6', @@ -44,7 +44,7 @@ class IdfApp(App): def __init__( self, *args, - part_tool: Optional[str] = None, + part_tool: str | None = None, **kwargs, ): super().__init__(*args, **kwargs) @@ -95,7 +95,7 @@ def parttool_path(self) -> str: raise ValueError('Partition Tool not found. (Default: $IDF_PATH/components/partition_table/gen_esp32part.py)') @property - def sdkconfig(self) -> Dict[str, Any]: + def sdkconfig(self) -> dict[str, Any]: """ Returns: dict contains all k-v pairs from the sdkconfig file @@ -143,7 +143,7 @@ def is_xtensa(self): return False @property - def partition_table(self) -> Dict[str, Any]: + def partition_table(self) -> dict[str, Any]: """ Returns: partition table dict generated by the partition tool @@ -187,14 +187,14 @@ def partition_table(self) -> Dict[str, Any]: self._partition_table = partition_table return self._partition_table - def _get_elf_file(self) -> Optional[str]: + def _get_elf_file(self) -> str | None: for fn in os.listdir(self.binary_path): if os.path.splitext(fn)[-1] == '.elf': return os.path.realpath(os.path.join(self.binary_path, fn)) return None - def _get_bin_file(self) -> Optional[str]: + def _get_bin_file(self) -> str | None: for fn in os.listdir(self.binary_path): if os.path.splitext(fn)[-1] == '.bin': return os.path.realpath(os.path.join(self.binary_path, fn)) @@ -229,7 +229,7 @@ def write_flash_args(self): def _parse_flash_args_json( self, - ) -> Tuple[Dict[str, Any], List[FlashFile], Dict[str, str]]: + ) -> tuple[dict[str, Any], list[FlashFile], dict[str, str]]: flash_args_json_filepath = None for fn in os.listdir(self.binary_path): if fn == self.FLASH_ARGS_JSON_FILENAME: @@ -242,7 +242,7 @@ def _parse_flash_args_json( with open(flash_args_json_filepath) as fr: flash_args = json.load(fr) - def _is_encrypted(_flash_args: Dict[str, Any], _offset: int, _file_path: str): + def _is_encrypted(_flash_args: dict[str, Any], _offset: int, _file_path: str): for entry in _flash_args.values(): try: if (entry['offset'], entry['file']) == (_offset, _file_path): @@ -268,7 +268,7 @@ def _is_encrypted(_flash_args: Dict[str, Any], _offset: int, _file_path: str): return flash_args, flash_files, flash_settings - def get_sha256(self, filepath: str) -> Optional[str]: + def get_sha256(self, filepath: str) -> str | None: """ Get the sha256 of the file diff --git a/pytest-embedded-idf/pytest_embedded_idf/dut.py b/pytest-embedded-idf/pytest_embedded_idf/dut.py index ca246715..92a3f0df 100644 --- a/pytest-embedded-idf/pytest_embedded_idf/dut.py +++ b/pytest-embedded-idf/pytest_embedded_idf/dut.py @@ -47,7 +47,7 @@ def __init__( self, app: IdfApp, skip_check_coredump: bool = False, - panic_output_decode_script: t.Optional[str] = None, + panic_output_decode_script: str | None = None, **kwargs, ) -> None: self.target = app.target @@ -72,7 +72,7 @@ def toolchain_prefix(self) -> str: raise ValueError(f'Unknown target: {self.target}') @property - def panic_output_decode_script(self) -> t.Optional[str]: + def panic_output_decode_script(self) -> str | None: """ Returns: Panic output decode script path diff --git a/pytest-embedded-idf/pytest_embedded_idf/serial.py b/pytest-embedded-idf/pytest_embedded_idf/serial.py index 4d168b62..c646c9a1 100644 --- a/pytest-embedded-idf/pytest_embedded_idf/serial.py +++ b/pytest-embedded-idf/pytest_embedded_idf/serial.py @@ -3,7 +3,7 @@ import logging import os import tempfile -from typing import Optional, TextIO, Union +from typing import TextIO import esptool from pytest_embedded_serial_esp.serial import EspSerial @@ -24,7 +24,7 @@ class IdfSerial(EspSerial): def __init__( self, app: IdfApp, - target: Optional[str] = None, + target: str | None = None, confirm_target_elf_sha256: bool = False, erase_nvs: bool = False, **kwargs, @@ -109,17 +109,19 @@ def load_ram(self) -> None: esp=self.esp, ) - def _force_flag(self, app: Optional[IdfApp] = None): + def _force_flag(self, app: IdfApp | None = None): if self.esp_flash_force: return ['--force'] if app is None: app = self.app - if any(( - app.sdkconfig.get('SECURE_FLASH_ENC_ENABLED', False), - app.sdkconfig.get('SECURE_BOOT', False), - )): + if any( + ( + app.sdkconfig.get('SECURE_FLASH_ENC_ENABLED', False), + app.sdkconfig.get('SECURE_BOOT', False), + ) + ): return ['--force'] return [] @@ -132,7 +134,7 @@ def erase_flash(self, force: bool = False): super().erase_flash() @EspSerial.use_esptool() - def flash(self, app: Optional[IdfApp] = None) -> None: + def flash(self, app: IdfApp | None = None) -> None: """ Flash the `app.flash_files` to the dut """ @@ -203,11 +205,11 @@ def flash(self, app: Optional[IdfApp] = None) -> None: @EspSerial.use_esptool() def dump_flash( self, - partition: Optional[str] = None, - address: Optional[str] = None, - size: Optional[str] = None, - output: Union[str, TextIO, None] = None, - ) -> Optional[bytes]: + partition: str | None = None, + address: str | None = None, + size: str | None = None, + output: str | TextIO | None = None, + ) -> bytes | None: """ Dump the flash bytes into the output file by partition name or by start address and size. diff --git a/pytest-embedded-idf/pytest_embedded_idf/unity_tester.py b/pytest-embedded-idf/pytest_embedded_idf/unity_tester.py index 25e15a39..11daf733 100644 --- a/pytest-embedded-idf/pytest_embedded_idf/unity_tester.py +++ b/pytest-embedded-idf/pytest_embedded_idf/unity_tester.py @@ -47,13 +47,13 @@ class UnittestMenuCase: #: Type of this case, which can be `normal` `multi_stage` or `multi_device`. type: str #: List of additional keywords of this case. For now, we have `disable` and `ignore`. - keywords: t.List[str] + keywords: list[str] #: List of groups of this case, this is usually the component which this case belongs to. - groups: t.List[str] + groups: list[str] #: Dict of attributes of this case, which is used to describe timeout duration, - attributes: t.Dict[str, t.Any] + attributes: dict[str, t.Any] #: List of dict of subcases of this case, if this case is a `multi_stage` or `multi_device` one. - subcases: t.List[t.Dict[str, t.Any]] + subcases: list[dict[str, t.Any]] @property def is_ignored(self): @@ -66,9 +66,9 @@ class IdfUnityDutMixin: """ def __init__(self, *args, **kwargs): - self._test_menu: t.List[UnittestMenuCase] = None # type: ignore + self._test_menu: list[UnittestMenuCase] = None # type: ignore - self._hard_reset_func: t.Optional[t.Callable] = None + self._hard_reset_func: t.Callable | None = None super().__init__(*args, **kwargs) @@ -108,7 +108,7 @@ def _parse_test_menu( ready_line: str = 'Press ENTER to see the list of tests', pattern="Here's the test menu, pick your combo:(.+)Enter test for running.", trigger: str = '', - ) -> t.List[UnittestMenuCase]: + ) -> list[UnittestMenuCase]: """ Get test case list from test menu via UART print. @@ -125,32 +125,8 @@ def _parse_test_menu( res = self.confirm_write(trigger, expect_pattern=pattern) return self._parse_unity_menu_from_str(res.group(1).decode('utf8')) - def parse_test_menu( - self, - ready_line: str = 'Press ENTER to see the list of tests', - pattern="Here's the test menu, pick your combo:(.+)Enter test for running.", - trigger: str = '', - ) -> t.List[UnittestMenuCase]: - warnings.warn( - 'Please use `dut.test_menu` property directly, ' - 'will rename this function to `_parse_test_menu` in release 2.0.0', - DeprecationWarning, - ) - - return self._parse_test_menu(ready_line, pattern, trigger) - - @staticmethod - def parse_unity_menu_from_str(s: str) -> t.List[UnittestMenuCase]: - warnings.warn( - 'Please use `dut.test_menu` property directly, ' - 'will rename this function to `_parse_unity_menu_from_str` in release 2.0.0', - DeprecationWarning, - ) - - return IdfUnityDutMixin._parse_unity_menu_from_str(s) - @staticmethod - def _parse_unity_menu_from_str(s: str) -> t.List[UnittestMenuCase]: + def _parse_unity_menu_from_str(s: str) -> list[UnittestMenuCase]: """ Parse test case menu from string to list of `UnittestMenuCase`. @@ -238,7 +214,7 @@ def _get_ready(self, timeout: float = 30) -> None: self.expect_exact(READY_PATTERN_LIST, timeout=timeout) @property - def test_menu(self) -> t.List[UnittestMenuCase]: + def test_menu(self) -> list[UnittestMenuCase]: if self._test_menu is None: self._test_menu = self._parse_test_menu() logging.debug('Successfully parsed unity test menu') @@ -296,7 +272,7 @@ def wrapper(self, *args, **kwargs): return wrapper def _add_single_unity_test_case( - self, case: UnittestMenuCase, log: t.Optional[t.AnyStr], additional_attrs: t.Optional[t.Dict[str, t.Any]] = None + self, case: UnittestMenuCase, log: t.AnyStr | None, additional_attrs: dict[str, t.Any] | None = None ): if log: # check format @@ -448,12 +424,12 @@ def validate_group(): def run_all_single_board_cases( self, - group: t.Optional[t.Union[str, list]] = None, + group: str | list | None = None, reset: bool = False, timeout: float = 30, run_ignore_cases: bool = False, - name: t.Optional[t.Union[str, list]] = None, - attributes: t.Optional[dict] = None, + name: str | list | None = None, + attributes: dict | None = None, dry_run: bool = False, ) -> None: """ @@ -477,11 +453,11 @@ def run_all_single_board_cases( if group is None: group = [] if isinstance(group, str): - group: t.List[str] = [group] - group: t.List[t.List[str]] = [[_and.strip() for _and in _or.split('&')] for _or in group] + group: list[str] = [group] + group: list[list[str]] = [[_and.strip() for _and in _or.split('&')] for _or in group] if isinstance(name, str): - name: t.List[str] = [name] + name: list[str] = [name] for case in self.test_menu: selected = self._select_to_run(group, name, attributes, case.groups, case.name, case.attributes) @@ -508,7 +484,7 @@ class _MultiDevTestDut: WAIT_SIGNAL_PREFIX = 'Waiting for signal: ' UNITY_SEND_SIGNAL_REGEX = SEND_SIGNAL_PREFIX + r'\[(.*?)\]!' UNITY_WAIT_SIGNAL_REGEX = WAIT_SIGNAL_PREFIX + r'\[(.*?)\]!' - signal_pattern_list: t.ClassVar[t.List[str]] = [ + signal_pattern_list: t.ClassVar[list[str]] = [ UNITY_SEND_SIGNAL_REGEX, # The dut send a signal UNITY_WAIT_SIGNAL_REGEX, # The dut is blocked and waiting for a signal UNITY_SUMMARY_LINE_REGEX, # Means the case finished @@ -653,7 +629,7 @@ def _expect(self, pattern, timeout): if time.perf_counter() - start > timeout: raise e - def process_raw_report_data(self, raw_data_to_report) -> t.Dict: + def process_raw_report_data(self, raw_data_to_report) -> dict: additional_attrs = {} if isinstance(raw_data_to_report, tuple) and len(raw_data_to_report) == 2: log = str(raw_data_to_report[0]) @@ -720,7 +696,7 @@ class MultiDevRunTestManager: def __init__(self, duts, case, start_retry, wait_for_menu_timeout, runtest_timeout): self.case = case - self.workers: t.List[_MultiDevTestDut] = [] + self.workers: list[_MultiDevTestDut] = [] shared_query = [[] for _ in case.subcases] for sub_case in case.subcases: index: int @@ -770,7 +746,7 @@ def gather(self): _t.close() @staticmethod - def get_merge_data(test_cases_attr: t.List[t.Dict]) -> t.Dict: + def get_merge_data(test_cases_attr: list[dict]) -> dict: output = {} results = set() time_attr = 0.0 @@ -825,13 +801,13 @@ class CaseTester: test_menu (t.List[UnittestMenuCase]): The list of the cases """ - def __init__(self, dut: t.Union['IdfDut', t.List['IdfDut']]) -> None: # type: ignore + def __init__(self, dut: t.Union['IdfDut', list['IdfDut']]) -> None: # type: ignore """ Create the object for every dut and put them into the group """ if isinstance(dut, Iterable): self.is_multi_dut = True - self.dut: t.List[IdfDut] = list(dut) + self.dut: list[IdfDut] = list(dut) self.first_dut = self.dut[0] self.test_menu = self.first_dut.test_menu else: diff --git a/pytest-embedded-idf/pytest_embedded_idf/utils.py b/pytest-embedded-idf/pytest_embedded_idf/utils.py index 79bb96b7..e71aed86 100644 --- a/pytest-embedded-idf/pytest_embedded_idf/utils.py +++ b/pytest-embedded-idf/pytest_embedded_idf/utils.py @@ -1,4 +1,3 @@ -import sys import typing as t from contextvars import ContextVar @@ -10,13 +9,7 @@ preview_targets = ContextVar('preview_targets', default=PREVIEW_TARGETS) -if sys.version_info < (3, 8): - from typing_extensions import Literal -else: - from typing import Literal - - -def _expand_target_values(values: t.List[t.List[t.Any]], target_index: int) -> t.List[t.List[t.Any]]: +def _expand_target_values(values: list[list[t.Any]], target_index: int) -> list[list[t.Any]]: """ Expands target-specific values into individual test cases. """ @@ -24,23 +17,23 @@ def _expand_target_values(values: t.List[t.List[t.Any]], target_index: int) -> t for value in values: target = value[target_index] if target == 'supported_targets': - expanded_values.extend([ - value[:target_index] + [target] + value[target_index + 1 :] for target in supported_targets.get() - ]) + expanded_values.extend( + [[*value[:target_index], target, *value[target_index + 1 :]] for target in supported_targets.get()] + ) elif target == 'preview_targets': - expanded_values.extend([ - value[:target_index] + [target] + value[target_index + 1 :] for target in preview_targets.get() - ]) + expanded_values.extend( + [[*value[:target_index], target, *value[target_index + 1 :]] for target in preview_targets.get()] + ) else: expanded_values.append(value) return expanded_values -def _process_pytest_value(value: t.Union[t.List[t.Any], t.Any], param_count: int) -> t.Any: +def _process_pytest_value(value: list[t.Any] | t.Any, param_count: int) -> t.Any: """ Processes a single parameter value, converting it to pytest.param if needed. """ - if not isinstance(value, (list, tuple)): + if not isinstance(value, list | tuple): return value if len(value) > param_count + 1: @@ -49,7 +42,7 @@ def _process_pytest_value(value: t.Union[t.List[t.Any], t.Any], param_count: int params, marks = [], [] if len(value) > param_count: mark_values = value[-1] - marks.extend(mark_values if isinstance(mark_values, (tuple, list)) else (mark_values,)) + marks.extend(mark_values if isinstance(mark_values, tuple | list) else (mark_values,)) params.extend(value[:param_count]) @@ -58,8 +51,8 @@ def _process_pytest_value(value: t.Union[t.List[t.Any], t.Any], param_count: int def idf_parametrize( param_names: str, - values: t.List[t.Union[t.Any, t.Tuple[t.Any, ...]]], - indirect: (t.Union[bool, t.Sequence[str]]) = False, + values: list[t.Any | tuple[t.Any, ...]], + indirect: (bool | t.Sequence[str]) = False, ) -> t.Callable[..., None]: """ A decorator to unify pytest.mark.parametrize usage in esp-idf. @@ -100,10 +93,10 @@ def decorator(func): return decorator -ValidTargets = Literal['supported_targets', 'preview_targets', 'all'] +ValidTargets = t.Literal['supported_targets', 'preview_targets', 'all'] -def soc_filtered_targets(soc_statement: str, targets: ValidTargets = 'all') -> t.List[str]: +def soc_filtered_targets(soc_statement: str, targets: ValidTargets = 'all') -> list[str]: """Filters targets based on a given SOC (System on Chip) statement. Args: diff --git a/pytest-embedded-jtag/pyproject.toml b/pytest-embedded-jtag/pyproject.toml index f5457e1a..0ca3eb8e 100644 --- a/pytest-embedded-jtag/pyproject.toml +++ b/pytest-embedded-jtag/pyproject.toml @@ -17,9 +17,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -28,7 +25,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded-serial~=1.17.0a2", diff --git a/pytest-embedded-jtag/pytest_embedded_jtag/gdb.py b/pytest-embedded-jtag/pytest_embedded_jtag/gdb.py index 0f5e6b64..c989e753 100644 --- a/pytest-embedded-jtag/pytest_embedded_jtag/gdb.py +++ b/pytest-embedded-jtag/pytest_embedded_jtag/gdb.py @@ -2,7 +2,7 @@ import re import shlex import time -from typing import AnyStr, Optional +from typing import AnyStr from pytest_embedded.log import DuplicateStdoutPopen @@ -16,7 +16,7 @@ class Gdb(DuplicateStdoutPopen): _GDB_RESPONSE_FINISHED_RE = re.compile(r'^\(gdb\)\s*$') - def __init__(self, gdb_prog_path: Optional[str] = None, gdb_cli_args: Optional[str] = None, **kwargs): + def __init__(self, gdb_prog_path: str | None = None, gdb_cli_args: str | None = None, **kwargs): gdb_prog_path = gdb_prog_path or self.GDB_PROG_PATH gdb_cli_args = shlex.split(gdb_cli_args or self.GDB_DEFAULT_ARGS) @@ -24,7 +24,7 @@ def __init__(self, gdb_prog_path: Optional[str] = None, gdb_cli_args: Optional[s super().__init__(cmd=[gdb_prog_path, *gdb_cli_args], **kwargs) - def write(self, s: AnyStr, non_blocking: bool = False, timeout: float = 30) -> Optional[str]: + def write(self, s: AnyStr, non_blocking: bool = False, timeout: float = 30) -> str | None: with open(self._logfile) as fr: if self._gdb_first_write: # Discard all queued responses before the first write diff --git a/pytest-embedded-jtag/pytest_embedded_jtag/openocd.py b/pytest-embedded-jtag/pytest_embedded_jtag/openocd.py index 5c34be17..8189f135 100644 --- a/pytest-embedded-jtag/pytest_embedded_jtag/openocd.py +++ b/pytest-embedded-jtag/pytest_embedded_jtag/openocd.py @@ -2,7 +2,7 @@ import os import shlex import time -from typing import AnyStr, Optional +from typing import AnyStr from pytest_embedded.log import DuplicateStdoutPopen from pytest_embedded.utils import to_bytes, to_str @@ -27,8 +27,8 @@ class OpenOcd(DuplicateStdoutPopen): def __init__( self, - openocd_prog_path: Optional[str] = None, - openocd_cli_args: Optional[str] = None, + openocd_prog_path: str | None = None, + openocd_cli_args: str | None = None, port_offset: int = 0, **kwargs, ): @@ -43,14 +43,16 @@ def __init__( self.telnet_port = self.TELNET_BASE_PORT + port_offset self.gdb_port = self.GDB_BASE_PORT + port_offset - openocd_cli_args.extend([ - '-c', - f'tcl_port {self.tcl_port}', - '-c', - f'telnet_port {self.telnet_port}', - '-c', - f'gdb_port {self.gdb_port}', - ]) + openocd_cli_args.extend( + [ + '-c', + f'tcl_port {self.tcl_port}', + '-c', + f'telnet_port {self.telnet_port}', + '-c', + f'gdb_port {self.gdb_port}', + ] + ) super().__init__(cmd=[openocd_prog_path, *openocd_cli_args], **kwargs) diff --git a/pytest-embedded-nuttx/pyproject.toml b/pytest-embedded-nuttx/pyproject.toml index 28f4aa7a..6570bedf 100644 --- a/pytest-embedded-nuttx/pyproject.toml +++ b/pytest-embedded-nuttx/pyproject.toml @@ -18,9 +18,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -29,7 +26,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded-serial~=1.17.0a2", diff --git a/pytest-embedded-nuttx/pytest_embedded_nuttx/app.py b/pytest-embedded-nuttx/pytest_embedded_nuttx/app.py index f7cd5cf2..467712a2 100644 --- a/pytest-embedded-nuttx/pytest_embedded_nuttx/app.py +++ b/pytest-embedded-nuttx/pytest_embedded_nuttx/app.py @@ -1,5 +1,4 @@ import logging -import typing as t from pathlib import Path from pytest_embedded.app import App @@ -25,7 +24,7 @@ def __init__( self.file_extension = file_extension self.app_file, self.bootloader_file, self.merge_file = self._get_bin_files() - def _get_bin_files(self) -> t.Tuple[t.Optional[Path], t.Optional[Path], t.Optional[Path]]: + def _get_bin_files(self) -> tuple[Path | None, Path | None, Path | None]: """ Get path to binary files available in the app_path. If either the application image or bootloader is not found, diff --git a/pytest-embedded-nuttx/pytest_embedded_nuttx/serial.py b/pytest-embedded-nuttx/pytest_embedded_nuttx/serial.py index 7561f6ac..e2a6b30f 100644 --- a/pytest-embedded-nuttx/pytest_embedded_nuttx/serial.py +++ b/pytest-embedded-nuttx/pytest_embedded_nuttx/serial.py @@ -1,4 +1,4 @@ -from typing import ClassVar, Dict +from typing import ClassVar import esptool from esptool.cmds import FLASH_MODES, LoadFirmwareImage @@ -18,7 +18,7 @@ class NuttxSerial(EspSerial): MCUBOOT_PRIMARY_SLOT_OFFSET = 0x10000 SERIAL_BAUDRATE = 115200 - binary_offsets: ClassVar[Dict[str, int]] = { + binary_offsets: ClassVar[dict[str, int]] = { 'esp32': 0x1000, 'esp32s2': 0x1000, 'esp32c3': 0x0, diff --git a/pytest-embedded-qemu/pyproject.toml b/pytest-embedded-qemu/pyproject.toml index dc885acf..a2841709 100644 --- a/pytest-embedded-qemu/pyproject.toml +++ b/pytest-embedded-qemu/pyproject.toml @@ -17,9 +17,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -28,7 +25,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded~=1.17.0a2", diff --git a/pytest-embedded-qemu/pytest_embedded_qemu/app.py b/pytest-embedded-qemu/pytest_embedded_qemu/app.py index acf5e852..b9831769 100644 --- a/pytest-embedded-qemu/pytest_embedded_qemu/app.py +++ b/pytest-embedded-qemu/pytest_embedded_qemu/app.py @@ -17,14 +17,14 @@ class IdfFlashImageMaker: Create a single image for QEMU based on the `IdfApp`'s partition table and all the flash files. """ - XTENSA_FLASH_BIN_SIZES: t.ClassVar[t.List[t.Tuple[int, str]]] = [ + XTENSA_FLASH_BIN_SIZES: t.ClassVar[list[tuple[int, str]]] = [ (2 * 1024 * 1024, '2MB'), (4 * 1024 * 1024, '4MB'), (8 * 1024 * 1024, '8MB'), (16 * 1024 * 1024, '16MB'), ] - RISCV_FLASH_BIN_SIZES: t.ClassVar[t.List[t.Tuple[int, str]]] = [ + RISCV_FLASH_BIN_SIZES: t.ClassVar[list[tuple[int, str]]] = [ (2 * 1024 * 1024, '2MB'), (4 * 1024 * 1024, '4MB'), (8 * 1024 * 1024, '8MB'), @@ -41,7 +41,7 @@ def __init__(self, app: 'QemuApp', image_path: str, *, qemu_version: Version = V self.image_path = image_path self.qemu_version = qemu_version - def _get_upper_bound(self, size: int, ranges: t.List[t.Tuple[int, str]]) -> str: + def _get_upper_bound(self, size: int, ranges: list[tuple[int, str]]) -> str: for r, s in ranges: if size <= r: upper = s @@ -139,11 +139,11 @@ class QemuApp(IdfApp): def __init__( self, msg_queue: MessageQueue, - qemu_image_path: t.Optional[str] = None, - skip_regenerate_image: t.Optional[bool] = False, - encrypt: t.Optional[bool] = False, - keyfile: t.Optional[str] = None, - qemu_prog_path: t.Optional[str] = None, + qemu_image_path: str | None = None, + skip_regenerate_image: bool | None = False, + encrypt: bool | None = False, + keyfile: str | None = None, + qemu_prog_path: str | None = None, **kwargs, ): self._q = msg_queue diff --git a/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py b/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py index 4b821d11..ade80b95 100644 --- a/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py +++ b/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py @@ -33,10 +33,10 @@ class Qemu(DuplicateStdoutPopen): def __init__( self, - qemu_image_path: t.Optional[str] = None, - qemu_prog_path: t.Optional[str] = None, - qemu_cli_args: t.Optional[str] = None, - qemu_extra_args: t.Optional[str] = None, + qemu_image_path: str | None = None, + qemu_prog_path: str | None = None, + qemu_cli_args: str | None = None, + qemu_extra_args: str | None = None, app: t.Optional['QemuApp'] = None, **kwargs, ): diff --git a/pytest-embedded-serial-esp/pyproject.toml b/pytest-embedded-serial-esp/pyproject.toml index 671729c8..6a1d9a52 100644 --- a/pytest-embedded-serial-esp/pyproject.toml +++ b/pytest-embedded-serial-esp/pyproject.toml @@ -17,9 +17,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -28,11 +25,11 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded-serial~=1.17.0a2", - "esptool~=4.9", + "esptool>=5.1.dev1,<6", ] [project.urls] diff --git a/pytest-embedded-serial-esp/pytest_embedded_serial_esp/serial.py b/pytest-embedded-serial-esp/pytest_embedded_serial_esp/serial.py index 56a5db30..01ad6cfc 100644 --- a/pytest-embedded-serial-esp/pytest_embedded_serial_esp/serial.py +++ b/pytest-embedded-serial-esp/pytest_embedded_serial_esp/serial.py @@ -2,9 +2,6 @@ import functools import logging import subprocess -import warnings -from typing import List, Optional -from warnings import warn import esptool from esptool import __version__ as ESPTOOL_VERSION @@ -29,18 +26,6 @@ def _is_port_mac_verified(pexpect_proc: PexpectProcess, port: str, port_mac: str return True -class EsptoolArgs: - """ - fake args object, this is a hack until esptool Python API is improved - """ - - def __init__(self, **kwargs): - warnings.warn('EsptoolArgs is deprecated and will be removed in 2.0 release.', DeprecationWarning) - - for key, value in kwargs.items(): - self.__setattr__(key, value) - - class EspSerial(Serial): """ Serial class for ports connected to espressif products @@ -52,18 +37,18 @@ def __init__( self, pexpect_proc: PexpectProcess, msg_queue: MessageQueue, - target: Optional[str] = None, - beta_target: Optional[str] = None, - port: Optional[str] = None, - port_serial_number: Optional[str] = None, - port_mac: Optional[str] = None, + target: str | None = None, + beta_target: str | None = None, + port: str | None = None, + port_serial_number: str | None = None, + port_mac: str | None = None, baud: int = Serial.DEFAULT_BAUDRATE, esptool_baud: int = ESPTOOL_DEFAULT_BAUDRATE, esp_flash_force: bool = False, skip_autoflash: bool = False, erase_all: bool = False, - meta: Optional[Meta] = None, - ports_to_occupy: List[str] = (), + meta: Meta | None = None, + ports_to_occupy: list[str] = (), **kwargs, ) -> None: self._meta = meta @@ -144,29 +129,13 @@ def _post_init(self): super()._post_init() - def use_esptool(hard_reset_after: Optional[bool] = None, no_stub: Optional[bool] = None): + @staticmethod + def use_esptool(): """ 1. tell the redirect serial thread to stop reading from the `pyserial` instance 2. esptool reuse the `pyserial` instance and call `esptool.main()` to do the actual work 3. tell the redirect serial thread to continue reading from serial - - Args: - hard_reset_after: run hard reset after (deprecated) - no_stub: disable launching the flasher stub (deprecated) """ - if hard_reset_after is not None: - warn( - "The 'hard_reset_after' parameter is now read directly from `flasher_args.json` " - 'and does not need to be explicitly set. This parameter will be removed in 2.0 release.', - DeprecationWarning, - ) - - if no_stub is not None: - warn( - "The 'no_stub' parameter is now read directly from `flasher_args.json` " - 'and does not need to be explicitly set. This parameter will be removed in 2.0 release.', - DeprecationWarning, - ) def decorator(func): @functools.wraps(func) @@ -203,11 +172,3 @@ def erase_flash(self, force: bool = False) -> None: if self._meta: self._meta.drop_port_app_cache(self.port) - - @property - def stub(self): - warn( - 'Please use `self.esp` instead of `self.stub`. `self.stub` will be removed in 2.0 release.', - DeprecationWarning, - ) - return self.esp diff --git a/pytest-embedded-serial/pyproject.toml b/pytest-embedded-serial/pyproject.toml index dd572e7a..704e9ac2 100644 --- a/pytest-embedded-serial/pyproject.toml +++ b/pytest-embedded-serial/pyproject.toml @@ -17,9 +17,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -28,7 +25,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded~=1.17.0a2", diff --git a/pytest-embedded-serial/pytest_embedded_serial/serial.py b/pytest-embedded-serial/pytest_embedded_serial/serial.py index 1bfb1d6a..5b61cdc8 100644 --- a/pytest-embedded-serial/pytest_embedded_serial/serial.py +++ b/pytest-embedded-serial/pytest_embedded_serial/serial.py @@ -5,7 +5,7 @@ import queue import threading import time -from typing import Any, ClassVar, Dict, List, Optional +from typing import Any, ClassVar import serial as pyserial from pytest_embedded.log import MessageQueue @@ -28,7 +28,7 @@ class Serial: DEFAULT_BAUDRATE = 115200 - DEFAULT_PORT_CONFIG: ClassVar[Dict[str, Any]] = { + DEFAULT_PORT_CONFIG: ClassVar[dict[str, Any]] = { 'baudrate': DEFAULT_BAUDRATE, 'bytesize': pyserial.EIGHTBITS, 'parity': pyserial.PARITY_NONE, @@ -38,17 +38,17 @@ class Serial: 'rtscts': False, } - occupied_ports: ClassVar[Dict[str, None]] = dict() + occupied_ports: ClassVar[dict[str, None]] = dict() def __init__( self, msg_queue: MessageQueue, - port: Optional[str] = None, - port_location: Optional[str] = None, + port: str | None = None, + port_location: str | None = None, baud: int = DEFAULT_BAUDRATE, - meta: Optional[Meta] = None, + meta: Meta | None = None, stop_after_init: bool = False, - ports_to_occupy: List[str] = (), + ports_to_occupy: list[str] = (), **kwargs, ): self._q = msg_queue diff --git a/pytest-embedded-wokwi/pyproject.toml b/pytest-embedded-wokwi/pyproject.toml index e37a70a9..683dcd80 100644 --- a/pytest-embedded-wokwi/pyproject.toml +++ b/pytest-embedded-wokwi/pyproject.toml @@ -18,9 +18,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -29,7 +26,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest-embedded~=1.17.0a2", diff --git a/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi_cli.py b/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi_cli.py index eba104f2..afe1ce08 100644 --- a/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi_cli.py +++ b/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi_cli.py @@ -42,10 +42,10 @@ class WokwiCLI(DuplicateStdoutPopen): def __init__( self, firmware_resolver: IDFFirmwareResolver, - wokwi_cli_path: t.Optional[str] = None, - wokwi_timeout: t.Optional[int] = None, - wokwi_scenario: t.Optional[str] = None, - wokwi_diagram: t.Optional[str] = None, + wokwi_cli_path: str | None = None, + wokwi_timeout: int | None = None, + wokwi_scenario: str | None = None, + wokwi_diagram: str | None = None, app: t.Optional['IdfApp'] = None, **kwargs, ): diff --git a/pytest-embedded/pyproject.toml b/pytest-embedded/pyproject.toml index c2366184..96b721c4 100644 --- a/pytest-embedded/pyproject.toml +++ b/pytest-embedded/pyproject.toml @@ -15,9 +15,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -26,7 +23,7 @@ classifiers = [ "Topic :: Software Development :: Testing", ] dynamic = ["version", "description"] -requires-python = ">=3.7" +requires-python = ">=3.10" dependencies = [ "pytest>=7.0", diff --git a/pytest-embedded/pytest_embedded/app.py b/pytest-embedded/pytest_embedded/app.py index bc35fa1d..26add51a 100644 --- a/pytest-embedded/pytest_embedded/app.py +++ b/pytest-embedded/pytest_embedded/app.py @@ -1,6 +1,5 @@ import logging import os -from typing import Optional class App: @@ -14,8 +13,8 @@ class App: def __init__( self, - app_path: Optional[str] = None, - build_dir: Optional[str] = None, + app_path: str | None = None, + build_dir: str | None = None, **kwargs, ): if app_path is None: @@ -27,7 +26,7 @@ def __init__( for k, v in kwargs.items(): setattr(self, k, v) - def _get_binary_path(self, build_dir: Optional[str] = None) -> Optional[str]: + def _get_binary_path(self, build_dir: str | None = None) -> str | None: if not build_dir: return None diff --git a/pytest-embedded/pytest_embedded/dut.py b/pytest-embedded/pytest_embedded/dut.py index a7583fa3..4164f3c7 100644 --- a/pytest-embedded/pytest_embedded/dut.py +++ b/pytest-embedded/pytest_embedded/dut.py @@ -3,7 +3,9 @@ import multiprocessing import os.path import re -from typing import AnyStr, Callable, List, Match, Optional, Union +from collections.abc import Callable +from re import Match +from typing import AnyStr import pexpect @@ -31,7 +33,7 @@ def __init__( app: App, pexpect_logfile: str, test_case_name: str, - meta: Optional[Meta] = None, + meta: Meta | None = None, **kwargs, ) -> None: self._q = msg_queue @@ -64,17 +66,17 @@ def write(self, s: AnyStr) -> None: """ self._q.put(to_bytes(s)) - def _pexpect_func(func) -> Callable[..., Union[Match, AnyStr]]: + def _pexpect_func(func) -> Callable[..., Match | AnyStr]: @functools.wraps(func) def wrapper( self, pattern, *args, expect_all: bool = False, - not_matching: List[Union[str, re.Pattern]] = (), + not_matching: list[str | re.Pattern] = (), return_what_before_match: bool = False, **kwargs, - ) -> Union[Union[Match, AnyStr], List[Union[Match, AnyStr]]]: + ) -> Match | AnyStr | list[Match | AnyStr]: if return_what_before_match and expect_all: raise ValueError('`return_what_before_match` and `expect_all` cannot be `True` at the same time.') @@ -172,7 +174,7 @@ def expect_unity_test_output( self, remove_asci_escape_code: bool = True, timeout: float = 60, - extra_before: Optional[AnyStr] = None, + extra_before: AnyStr | None = None, ) -> None: """ Expect a unity test summary block and parse the output into junit report. @@ -213,7 +215,7 @@ def expect_unity_test_output( @_InjectMixinCls.require_services('idf') def run_all_single_board_cases( self, - group: Optional[str] = None, + group: str | None = None, reset: bool = False, timeout: float = 30, run_ignore_cases: bool = False, diff --git a/pytest-embedded/pytest_embedded/dut_factory.py b/pytest-embedded/pytest_embedded/dut_factory.py index ba2ed88a..eb3fc019 100644 --- a/pytest-embedded/pytest_embedded/dut_factory.py +++ b/pytest-embedded/pytest_embedded/dut_factory.py @@ -24,7 +24,7 @@ from .utils import FIXTURES_SERVICES, ClassCliOptions, to_str -def _drop_none_kwargs(kwargs: t.Dict[t.Any, t.Any]): +def _drop_none_kwargs(kwargs: dict[t.Any, t.Any]): return {k: v for k, v in kwargs.items() if v is not None} @@ -161,9 +161,9 @@ def _fixture_classes_and_options_fn( _meta, **kwargs, ) -> ClassCliOptions: - classes: t.Dict[str, type] = {} - mixins: t.Dict[str, t.List[type]] = defaultdict(list) - kwargs: t.Dict[str, t.Dict[str, t.Any]] = defaultdict(dict) + classes: dict[str, type] = {} + mixins: dict[str, list[type]] = defaultdict(list) + kwargs: dict[str, dict[str, t.Any]] = defaultdict(dict) for fixture in FIXTURES_SERVICES.keys(): if fixture == 'app': @@ -173,22 +173,26 @@ def _fixture_classes_and_options_fn( from pytest_embedded_qemu import DEFAULT_IMAGE_FN, QemuApp classes[fixture] = QemuApp - kwargs[fixture].update({ - 'msg_queue': msg_queue, - 'part_tool': part_tool, - 'qemu_image_path': qemu_image_path, - 'skip_regenerate_image': skip_regenerate_image, - 'encrypt': encrypt, - 'keyfile': keyfile, - 'qemu_prog_path': qemu_prog_path, - }) + kwargs[fixture].update( + { + 'msg_queue': msg_queue, + 'part_tool': part_tool, + 'qemu_image_path': qemu_image_path, + 'skip_regenerate_image': skip_regenerate_image, + 'encrypt': encrypt, + 'keyfile': keyfile, + 'qemu_prog_path': qemu_prog_path, + } + ) else: from pytest_embedded_idf import IdfApp classes[fixture] = IdfApp - kwargs[fixture].update({ - 'part_tool': part_tool, - }) + kwargs[fixture].update( + { + 'part_tool': part_tool, + } + ) elif 'arduino' in _services: from pytest_embedded_arduino import ArduinoApp @@ -226,26 +230,32 @@ def _fixture_classes_and_options_fn( from pytest_embedded_idf import IdfSerial classes[fixture] = IdfSerial - kwargs[fixture].update({ - 'app': None, - 'confirm_target_elf_sha256': confirm_target_elf_sha256, - 'erase_nvs': erase_nvs, - }) + kwargs[fixture].update( + { + 'app': None, + 'confirm_target_elf_sha256': confirm_target_elf_sha256, + 'erase_nvs': erase_nvs, + } + ) elif 'arduino' in _services: from pytest_embedded_arduino import ArduinoSerial classes[fixture] = ArduinoSerial - kwargs[fixture].update({ - 'app': None, - }) + kwargs[fixture].update( + { + 'app': None, + } + ) elif 'nuttx' in _services: from pytest_embedded_nuttx import NuttxSerial classes[fixture] = NuttxSerial - kwargs[fixture].update({ - 'app': None, - 'baud': int(baud or NuttxSerial.SERIAL_BAUDRATE), - }) + kwargs[fixture].update( + { + 'app': None, + 'baud': int(baud or NuttxSerial.SERIAL_BAUDRATE), + } + ) else: from pytest_embedded_serial_esp import EspSerial @@ -312,16 +322,18 @@ def _fixture_classes_and_options_fn( from pytest_embedded_wokwi import WokwiCLI classes[fixture] = WokwiCLI - kwargs[fixture].update({ - 'wokwi_cli_path': wokwi_cli_path, - 'wokwi_timeout': wokwi_timeout, - 'wokwi_scenario': wokwi_scenario, - 'wokwi_diagram': wokwi_diagram, - 'msg_queue': msg_queue, - 'app': None, - 'meta': _meta, - 'firmware_resolver': None, - }) + kwargs[fixture].update( + { + 'wokwi_cli_path': wokwi_cli_path, + 'wokwi_timeout': wokwi_timeout, + 'wokwi_scenario': wokwi_scenario, + 'wokwi_diagram': wokwi_diagram, + 'msg_queue': msg_queue, + 'app': None, + 'meta': _meta, + 'firmware_resolver': None, + } + ) elif fixture == 'dut': classes[fixture] = Dut kwargs[fixture] = { @@ -342,9 +354,11 @@ def _fixture_classes_and_options_fn( from pytest_embedded_wokwi import WokwiDut classes[fixture] = WokwiDut - kwargs[fixture].update({ - 'wokwi': None, - }) + kwargs[fixture].update( + { + 'wokwi': None, + } + ) if 'idf' in _services: from pytest_embedded_wokwi.idf import IDFFirmwareResolver @@ -361,16 +375,20 @@ def _fixture_classes_and_options_fn( from pytest_embedded_nuttx import NuttxQemuDut classes[fixture] = NuttxQemuDut - kwargs[fixture].update({ - 'qemu': None, - }) + kwargs[fixture].update( + { + 'qemu': None, + } + ) else: from pytest_embedded_qemu import QemuDut classes[fixture] = QemuDut - kwargs[fixture].update({ - 'qemu': None, - }) + kwargs[fixture].update( + { + 'qemu': None, + } + ) elif 'jtag' in _services: if 'idf' in _services: from pytest_embedded_idf import IdfDut @@ -381,42 +399,52 @@ def _fixture_classes_and_options_fn( classes[fixture] = SerialDut - kwargs[fixture].update({ - 'serial': None, - 'openocd': None, - 'gdb': None, - }) + kwargs[fixture].update( + { + 'serial': None, + 'openocd': None, + 'gdb': None, + } + ) elif 'serial' in _services or 'esp' in _services: if 'esp' in _services and 'idf' in _services: from pytest_embedded_idf import IdfDut classes[fixture] = IdfDut - kwargs[fixture].update({ - 'skip_check_coredump': skip_check_coredump, - 'panic_output_decode_script': panic_output_decode_script, - }) + kwargs[fixture].update( + { + 'skip_check_coredump': skip_check_coredump, + 'panic_output_decode_script': panic_output_decode_script, + } + ) elif 'esp' in _services and 'nuttx' in _services: from pytest_embedded_nuttx import NuttxEspDut classes[fixture] = NuttxEspDut - kwargs[fixture].update({ - 'serial': None, - }) + kwargs[fixture].update( + { + 'serial': None, + } + ) elif 'nuttx' in _services: from pytest_embedded_nuttx import NuttxSerialDut classes[fixture] = NuttxSerialDut - kwargs[fixture].update({ - 'serial': None, - }) + kwargs[fixture].update( + { + 'serial': None, + } + ) else: from pytest_embedded_serial import SerialDut classes[fixture] = SerialDut - kwargs[fixture].update({ - 'serial': None, - }) + kwargs[fixture].update( + { + 'serial': None, + } + ) return ClassCliOptions(classes, mixins, kwargs) @@ -427,7 +455,7 @@ def app_fn(_fixture_classes_and_options: ClassCliOptions) -> App: return cls(**_drop_none_kwargs(kwargs)) -def serial_gn(_fixture_classes_and_options, msg_queue, app) -> t.Optional[t.Union['Serial', 'LinuxSerial']]: +def serial_gn(_fixture_classes_and_options, msg_queue, app) -> t.Union['Serial', 'LinuxSerial'] | None: if hasattr(app, 'target') and app.target == 'linux': from pytest_embedded_idf import LinuxSerial @@ -513,10 +541,10 @@ def dut_gn( openocd: t.Optional['OpenOcd'], gdb: t.Optional['Gdb'], app: App, - serial: t.Optional[t.Union['Serial', 'LinuxSerial']], + serial: t.Union['Serial', 'LinuxSerial'] | None, qemu: t.Optional['Qemu'], wokwi: t.Optional['WokwiCLI'], -) -> t.Union[Dut, t.List[Dut]]: +) -> Dut | list[Dut]: global DUT_GLOBAL_INDEX DUT_GLOBAL_INDEX += 1 @@ -550,7 +578,7 @@ def dut_gn( return cls(**_drop_none_kwargs(kwargs), mixins=mixins) -def set_parametrized_fixtures_cache(values: t.Dict): +def set_parametrized_fixtures_cache(values: dict): global PARAMETRIZED_FIXTURES_CACHE PARAMETRIZED_FIXTURES_CACHE = values.copy() @@ -561,7 +589,7 @@ def _close_or_terminate(obj): return try: - if isinstance(obj, (subprocess.Popen, multiprocessing.process.BaseProcess)): + if isinstance(obj, subprocess.Popen | multiprocessing.process.BaseProcess): obj.terminate() obj.kill() elif isinstance(obj, io.IOBase): @@ -604,7 +632,7 @@ class DutFactory: # [openocd, gdb, serial, qemu, wokwi, dut] # dut-1 # ... # ] - obj_stack: t.ClassVar[t.List[t.List[t.Any]]] = [] + obj_stack: t.ClassVar[list[list[t.Any]]] = [] @classmethod def close(cls): @@ -633,39 +661,39 @@ def create( embedded_services: str = '', app_path: str = '', build_dir: str = 'build', - port: t.Optional[str] = None, - port_serial_number: t.Optional[str] = None, - port_location: t.Optional[str] = None, - port_mac: t.Optional[str] = None, - target: t.Optional[str] = None, - beta_target: t.Optional[str] = None, - baud: t.Optional[int] = None, - flash_port: t.Optional[str] = None, - skip_autoflash: t.Optional[bool] = None, - erase_all: t.Optional[bool] = None, - esptool_baud: t.Optional[int] = None, - esp_flash_force: t.Optional[bool] = False, - part_tool: t.Optional[str] = None, - confirm_target_elf_sha256: t.Optional[bool] = None, - erase_nvs: t.Optional[bool] = None, - skip_check_coredump: t.Optional[bool] = None, - panic_output_decode_script: t.Optional[str] = None, - openocd_prog_path: t.Optional[str] = None, - openocd_cli_args: t.Optional[str] = None, - gdb_prog_path: t.Optional[str] = None, - gdb_cli_args: t.Optional[str] = None, - no_gdb: t.Optional[bool] = None, - qemu_image_path: t.Optional[str] = None, - qemu_prog_path: t.Optional[str] = None, - qemu_cli_args: t.Optional[str] = None, - qemu_extra_args: t.Optional[str] = None, - wokwi_cli_path: t.Optional[str] = None, - wokwi_timeout: t.Optional[int] = 0, - wokwi_scenario: t.Optional[str] = None, - wokwi_diagram: t.Optional[str] = None, - skip_regenerate_image: t.Optional[bool] = None, - encrypt: t.Optional[bool] = None, - keyfile: t.Optional[str] = None, + port: str | None = None, + port_serial_number: str | None = None, + port_location: str | None = None, + port_mac: str | None = None, + target: str | None = None, + beta_target: str | None = None, + baud: int | None = None, + flash_port: str | None = None, + skip_autoflash: bool | None = None, + erase_all: bool | None = None, + esptool_baud: int | None = None, + esp_flash_force: bool | None = False, + part_tool: str | None = None, + confirm_target_elf_sha256: bool | None = None, + erase_nvs: bool | None = None, + skip_check_coredump: bool | None = None, + panic_output_decode_script: str | None = None, + openocd_prog_path: str | None = None, + openocd_cli_args: str | None = None, + gdb_prog_path: str | None = None, + gdb_cli_args: str | None = None, + no_gdb: bool | None = None, + qemu_image_path: str | None = None, + qemu_prog_path: str | None = None, + qemu_cli_args: str | None = None, + qemu_extra_args: str | None = None, + wokwi_cli_path: str | None = None, + wokwi_timeout: int | None = 0, + wokwi_scenario: str | None = None, + wokwi_diagram: str | None = None, + skip_regenerate_image: bool | None = None, + encrypt: bool | None = None, + keyfile: str | None = None, ): """ Create a Device Under Test (DUT) object with customizable parameters. diff --git a/pytest-embedded/pytest_embedded/log.py b/pytest-embedded/pytest_embedded/log.py index 962e135f..65d6c549 100644 --- a/pytest-embedded/pytest_embedded/log.py +++ b/pytest-embedded/pytest_embedded/log.py @@ -8,7 +8,7 @@ import textwrap import uuid from multiprocessing import queues -from typing import AnyStr, List, Optional, Union +from typing import AnyStr import pexpect.fdpexpect from pexpect import EOF, TIMEOUT @@ -30,7 +30,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def put(self, obj, **kwargs): - if not isinstance(obj, (str, bytes)): + if not isinstance(obj, str | bytes): super().put(obj, **kwargs) return @@ -116,7 +116,7 @@ def terminate(self, force=False): # noqa self.close() -def live_print_call(*args, msg_queue: Optional[MessageQueue] = None, expect_returncode: int = 0, **kwargs): +def live_print_call(*args, msg_queue: MessageQueue | None = None, expect_returncode: int = 0, **kwargs): """ live print the `subprocess.Popen` process @@ -166,7 +166,7 @@ class DuplicateStdoutPopen(subprocess.Popen): SOURCE = 'POPEN' REDIRECT_CLS = _PopenRedirectProcess - def __init__(self, msg_queue: MessageQueue, cmd: Union[str, List[str]], meta: Optional[Meta] = None, **kwargs): + def __init__(self, msg_queue: MessageQueue, cmd: str | list[str], meta: Meta | None = None, **kwargs): self._q = msg_queue self._p = None @@ -188,12 +188,14 @@ def __init__(self, msg_queue: MessageQueue, cmd: Union[str, List[str]], meta: Op self._logfile_offset = 0 logging.debug(f'temp log file: {_log_file}') - kwargs.update({ - 'bufsize': 0, - 'stdin': subprocess.PIPE, - 'stdout': self._fw, - 'stderr': self._fw, - }) + kwargs.update( + { + 'bufsize': 0, + 'stdin': subprocess.PIPE, + 'stdout': self._fw, + 'stderr': self._fw, + } + ) self._cmd = cmd logging.info('Executing %s', ' '.join(cmd) if isinstance(cmd, list) else cmd) diff --git a/pytest-embedded/pytest_embedded/plugin.py b/pytest-embedded/pytest_embedded/plugin.py index b500efca..6fa5e3ee 100644 --- a/pytest-embedded/pytest_embedded/plugin.py +++ b/pytest-embedded/pytest_embedded/plugin.py @@ -168,13 +168,6 @@ def pytest_addoption(parser): esp_group = parser.getgroup('embedded-esp') esp_group.addoption('--target', help='serial target chip type. (Default: "auto")') esp_group.addoption('--beta-target', help='serial target beta version chip type. (Default: same as [--target])') - esp_group.addoption( - '--add-target-as-marker', - help='[DEPRECATED, use --add-target-as-marker-with-amount instead] ' - 'add target param as a function marker. Useful in CI with runners with different tags.' - 'y/yes/true for True and n/no/false for False. ' - '(Default: False, parametrization not supported, `|` will be escaped to `-`)', - ) esp_group.addoption( '--add-target-as-marker-with-amount', help='add target param as a function marker with the amount of the target. Useful in CI with runners with ' @@ -335,7 +328,7 @@ def _gte_one_int(v) -> int: raise argparse.ArgumentTypeError('should be a integer greater or equal to 1') -def _str_bool(v: str) -> t.Union[bool, str, None]: +def _str_bool(v: str) -> bool | str | None: if v is None: return None @@ -363,7 +356,7 @@ def count(request): _COUNT = _gte_one_int(getattr(request, 'param', request.config.option.count)) -def parse_multi_dut_args(count: int, s: str) -> t.Union[t.Any, t.Tuple[t.Any]]: +def parse_multi_dut_args(count: int, s: str) -> t.Any | tuple[t.Any]: """ Parse multi-dut argument by the following rules: @@ -399,7 +392,7 @@ def parse_multi_dut_args(count: int, s: str) -> t.Union[t.Any, t.Tuple[t.Any]]: return tuple(_str_bool(item) for item in res) -def multi_dut_argument(func) -> t.Callable[..., t.Union[t.Optional[str], t.Tuple[t.Optional[str]]]]: +def multi_dut_argument(func) -> t.Callable[..., str | None | tuple[str | None]]: """ Used for parse the multi-dut argument according to the `count` amount. """ @@ -411,7 +404,7 @@ def wrapper(*args, **kwargs): return wrapper -def multi_dut_fixture(func) -> t.Callable[..., t.Union[t.Any, t.Tuple[t.Any]]]: +def multi_dut_fixture(func) -> t.Callable[..., t.Any | tuple[t.Any]]: """ Apply the multi-dut arguments to each fixture. @@ -457,7 +450,7 @@ def wrapper(*args, **kwargs): def multi_dut_generator_fixture( func, -) -> t.Callable[..., t.Generator[t.Union[t.Any, t.Tuple[t.Any]], t.Any, None]]: +) -> t.Callable[..., t.Generator[t.Any | tuple[t.Any], t.Any, None]]: """ Apply the multi-dut arguments to each fixture. @@ -478,7 +471,7 @@ def _close_or_terminate(obj): return try: - if isinstance(obj, (subprocess.Popen, multiprocessing.process.BaseProcess)): + if isinstance(obj, subprocess.Popen | multiprocessing.process.BaseProcess): obj.terminate() obj.kill() elif isinstance(obj, io.IOBase): @@ -626,11 +619,11 @@ def cache_dir(request: FixtureRequest) -> str: @pytest.fixture(scope='session') -def port_target_cache(cache_dir) -> t.Dict[str, str]: +def port_target_cache(cache_dir) -> dict[str, str]: """Session scoped port-target cache, for esp only""" _cache_file_path = os.path.join(cache_dir, 'port_target_cache') lock = filelock.FileLock(f'{_cache_file_path}.lock') - resp: t.Dict[str, str] = {} + resp: dict[str, str] = {} with lock: try: with shelve.open(_cache_file_path) as f: @@ -646,7 +639,7 @@ def port_target_cache(cache_dir) -> t.Dict[str, str]: @pytest.fixture(scope='session') -def port_app_cache() -> t.Dict[str, str]: +def port_app_cache() -> dict[str, str]: """Session scoped port-app cache, for idf only""" return {} @@ -754,14 +747,14 @@ def _inner(): ######## @pytest.fixture @multi_dut_argument -def embedded_services(request: FixtureRequest) -> t.Optional[str]: +def embedded_services(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'embedded_services', None) @pytest.fixture @multi_dut_argument -def app_path(request: FixtureRequest, test_file_path: str, record_xml_attribute) -> t.Optional[str]: +def app_path(request: FixtureRequest, test_file_path: str, record_xml_attribute) -> str | None: """Enable parametrization for the same cli option""" res = _request_param_or_config_option_or_default(request, 'app_path', os.path.dirname(test_file_path)) record_xml_attribute('app_path', res) @@ -770,14 +763,14 @@ def app_path(request: FixtureRequest, test_file_path: str, record_xml_attribute) @pytest.fixture @multi_dut_argument -def esp_flash_force(request: FixtureRequest) -> t.Optional[str]: +def esp_flash_force(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'esp_flash_force', False) @pytest.fixture @multi_dut_argument -def build_dir(request: FixtureRequest) -> t.Optional[str]: +def build_dir(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'build_dir', 'build') @@ -787,21 +780,21 @@ def build_dir(request: FixtureRequest) -> t.Optional[str]: ########## @pytest.fixture @multi_dut_argument -def port(request: FixtureRequest) -> t.Optional[str]: +def port(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'port', None) @pytest.fixture @multi_dut_argument -def baud(request: FixtureRequest) -> t.Optional[str]: +def baud(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'baud', None) @pytest.fixture @multi_dut_argument -def port_location(request: FixtureRequest) -> t.Optional[str]: +def port_location(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'port_location', None) @@ -811,56 +804,56 @@ def port_location(request: FixtureRequest) -> t.Optional[str]: ####### @pytest.fixture @multi_dut_argument -def target(request: FixtureRequest) -> t.Optional[str]: +def target(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'target', None) @pytest.fixture @multi_dut_argument -def beta_target(request: FixtureRequest) -> t.Optional[str]: +def beta_target(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'beta_target', None) @pytest.fixture @multi_dut_argument -def flash_port(request: FixtureRequest) -> t.Optional[str]: +def flash_port(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'flash_port', None) @pytest.fixture @multi_dut_argument -def skip_autoflash(request: FixtureRequest) -> t.Optional[bool]: +def skip_autoflash(request: FixtureRequest) -> bool | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'skip_autoflash', None) @pytest.fixture @multi_dut_argument -def erase_all(request: FixtureRequest) -> t.Optional[bool]: +def erase_all(request: FixtureRequest) -> bool | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'erase_all', None) @pytest.fixture @multi_dut_argument -def esptool_baud(request: FixtureRequest) -> t.Optional[str]: +def esptool_baud(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'esptool_baud', None) @pytest.fixture @multi_dut_argument -def port_mac(request: FixtureRequest) -> t.Optional[str]: +def port_mac(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'port_mac', None) @pytest.fixture @multi_dut_argument -def port_serial_number(request: FixtureRequest) -> t.Optional[str]: +def port_serial_number(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'port_serial_number', None) @@ -870,35 +863,35 @@ def port_serial_number(request: FixtureRequest) -> t.Optional[str]: ####### @pytest.fixture @multi_dut_argument -def part_tool(request: FixtureRequest) -> t.Optional[str]: +def part_tool(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'part_tool', None) @pytest.fixture @multi_dut_argument -def confirm_target_elf_sha256(request: FixtureRequest) -> t.Optional[bool]: +def confirm_target_elf_sha256(request: FixtureRequest) -> bool | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'confirm_target_elf_sha256', None) @pytest.fixture @multi_dut_argument -def erase_nvs(request: FixtureRequest) -> t.Optional[bool]: +def erase_nvs(request: FixtureRequest) -> bool | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'erase_nvs', None) @pytest.fixture @multi_dut_argument -def skip_check_coredump(request: FixtureRequest) -> t.Optional[bool]: +def skip_check_coredump(request: FixtureRequest) -> bool | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'skip_check_coredump', None) @pytest.fixture @multi_dut_argument -def panic_output_decode_script(request: FixtureRequest) -> t.Optional[bool]: +def panic_output_decode_script(request: FixtureRequest) -> bool | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'panic_output_decode_script', None) @@ -908,14 +901,14 @@ def panic_output_decode_script(request: FixtureRequest) -> t.Optional[bool]: ######## @pytest.fixture @multi_dut_argument -def gdb_prog_path(request: FixtureRequest) -> t.Optional[str]: +def gdb_prog_path(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'gdb_prog_path', None) @pytest.fixture @multi_dut_argument -def gdb_cli_args(request: FixtureRequest) -> t.Optional[str]: +def gdb_cli_args(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'gdb_cli_args', None) @@ -928,14 +921,14 @@ def no_gdb(request: FixtureRequest) -> bool: @pytest.fixture @multi_dut_argument -def openocd_prog_path(request: FixtureRequest) -> t.Optional[str]: +def openocd_prog_path(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'openocd_prog_path', None) @pytest.fixture @multi_dut_argument -def openocd_cli_args(request: FixtureRequest) -> t.Optional[str]: +def openocd_cli_args(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'openocd_cli_args', None) @@ -945,49 +938,49 @@ def openocd_cli_args(request: FixtureRequest) -> t.Optional[str]: ######## @pytest.fixture @multi_dut_argument -def qemu_image_path(request: FixtureRequest) -> t.Optional[str]: +def qemu_image_path(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'qemu_image_path', None) @pytest.fixture @multi_dut_argument -def qemu_prog_path(request: FixtureRequest) -> t.Optional[str]: +def qemu_prog_path(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'qemu_prog_path', None) @pytest.fixture @multi_dut_argument -def qemu_cli_args(request: FixtureRequest) -> t.Optional[str]: +def qemu_cli_args(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'qemu_cli_args', None) @pytest.fixture @multi_dut_argument -def qemu_extra_args(request: FixtureRequest) -> t.Optional[str]: +def qemu_extra_args(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'qemu_extra_args', None) @pytest.fixture @multi_dut_argument -def skip_regenerate_image(request: FixtureRequest) -> t.Optional[str]: +def skip_regenerate_image(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'skip_regenerate_image', None) @pytest.fixture @multi_dut_argument -def encrypt(request: FixtureRequest) -> t.Optional[str]: +def encrypt(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'encrypt', None) @pytest.fixture @multi_dut_argument -def keyfile(request: FixtureRequest) -> t.Optional[str]: +def keyfile(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'keyfile', None) @@ -997,28 +990,28 @@ def keyfile(request: FixtureRequest) -> t.Optional[str]: ######### @pytest.fixture @multi_dut_argument -def wokwi_cli_path(request: FixtureRequest) -> t.Optional[str]: +def wokwi_cli_path(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'wokwi_cli_path', None) @pytest.fixture @multi_dut_argument -def wokwi_timeout(request: FixtureRequest) -> t.Optional[str]: +def wokwi_timeout(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'wokwi_timeout', None) @pytest.fixture @multi_dut_argument -def wokwi_scenario(request: FixtureRequest) -> t.Optional[str]: +def wokwi_scenario(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'wokwi_scenario', None) @pytest.fixture @multi_dut_argument -def wokwi_diagram(request: FixtureRequest) -> t.Optional[str]: +def wokwi_diagram(request: FixtureRequest) -> str | None: """Enable parametrization for the same cli option""" return _request_param_or_config_option_or_default(request, 'wokwi_diagram', None) @@ -1028,7 +1021,7 @@ def wokwi_diagram(request: FixtureRequest) -> t.Optional[str]: #################### @pytest.fixture @multi_dut_fixture -def _services(embedded_services: t.Optional[str]) -> t.List[str]: +def _services(embedded_services: str | None) -> list[str]: if not embedded_services: return ['base'] @@ -1146,7 +1139,7 @@ def app(_fixture_classes_and_options: ClassCliOptions) -> App: @pytest.fixture @multi_dut_generator_fixture -def serial(_fixture_classes_and_options, msg_queue, app) -> t.Optional[t.Union['Serial', 'LinuxSerial']]: +def serial(_fixture_classes_and_options, msg_queue, app) -> t.Union['Serial', 'LinuxSerial'] | None: """A serial subprocess that could read/redirect/write""" return serial_gn(**locals()) @@ -1186,10 +1179,10 @@ def dut( openocd: t.Optional['OpenOcd'], gdb: t.Optional['Gdb'], app: App, - serial: t.Optional[t.Union['Serial', 'LinuxSerial']], + serial: t.Union['Serial', 'LinuxSerial'] | None, qemu: t.Optional['Qemu'], wokwi: t.Optional['WokwiCLI'], -) -> t.Union[Dut, t.List[Dut]]: +) -> Dut | list[Dut]: """ A device under test (DUT) object that could gather output from various sources and redirect them to the pexpect process, and run `expect()` via its pexpect process. @@ -1198,7 +1191,7 @@ def dut( @pytest.fixture -def unity_tester(dut: t.Union['IdfDut', t.Tuple['IdfDut']]) -> t.Optional['CaseTester']: +def unity_tester(dut: t.Union['IdfDut', tuple['IdfDut']]) -> t.Optional['CaseTester']: try: from pytest_embedded_idf import CaseTester, IdfDut except ImportError: @@ -1242,7 +1235,6 @@ def pytest_configure(config: Config) -> None: parallel_index=config.getoption('parallel_index'), check_duplicates=config.getoption('check_duplicates', False), prettify_junit_report=_str_bool(config.getoption('prettify_junit_report', False)), - add_target_as_marker=_str_bool(config.getoption('add_target_as_marker', False)), add_target_as_marker_with_amount=_str_bool(config.getoption('add_target_as_marker_with_amount', False)), ) config.pluginmanager.register(config.stash[_pytest_embedded_key]) @@ -1263,14 +1255,12 @@ def __init__( parallel_index: int = 1, check_duplicates: bool = False, prettify_junit_report: bool = False, - add_target_as_marker: bool = False, add_target_as_marker_with_amount: bool = False, ): self.parallel_count = parallel_count self.parallel_index = parallel_index self.check_duplicates = check_duplicates self.prettify_junit_report = prettify_junit_report - self.add_target_as_marker = add_target_as_marker self.add_target_as_marker_with_amount = add_target_as_marker_with_amount @staticmethod @@ -1287,7 +1277,7 @@ def _raise_dut_failed_cases_if_exists(duts: t.Iterable[Dut]) -> None: raise AssertionError('Unity test failed') @staticmethod - def _duplicate_items(items: t.List[_T]) -> t.List[_T]: + def _duplicate_items(items: list[_T]) -> list[_T]: duplicates = [] counter = Counter(items) for elem, cnt in counter.items(): @@ -1306,9 +1296,9 @@ def get_param(item: Function, key: str, default: t.Any = None) -> t.Any: return item.callspec.params.get(key, default) or default @pytest.hookimpl(hookwrapper=True, trylast=True) - def pytest_collection_modifyitems(self, config: Config, items: t.List[Function]): + def pytest_collection_modifyitems(self, config: Config, items: list[Function]): # ------ add marker based on target ------ - if self.add_target_as_marker_with_amount or self.add_target_as_marker: + if self.add_target_as_marker_with_amount: for item in items: item_target = self.get_param(item, 'target') if not item_target: @@ -1319,10 +1309,7 @@ def pytest_collection_modifyitems(self, config: Config, items: t.List[Function]) # --add-target-as-marker-with-amount count = self.get_param(item, 'count', 1) - if self.add_target_as_marker_with_amount: - _marker = targets_to_marker(to_list(parse_multi_dut_args(count, item_target))) - if self.add_target_as_marker: - _marker = '-'.join(to_list(parse_multi_dut_args(count, item_target))) + _marker = targets_to_marker(to_list(parse_multi_dut_args(count, item_target))) item.add_marker(_marker) @@ -1368,9 +1355,9 @@ def pytest_collection_modifyitems(self, config: Config, items: t.List[Function]) if duplicated_test_cases: raise ValueError(f'Duplicated test function names: {duplicated_test_cases}') - duplicated_test_script_paths = self._duplicate_items([ - os.path.basename(name) for name in set([str(test.path.absolute()) for test in items]) - ]) + duplicated_test_script_paths = self._duplicate_items( + [os.path.basename(name) for name in set([str(test.path.absolute()) for test in items])] + ) if duplicated_test_script_paths: raise ValueError(f'Duplicated test scripts: {duplicated_test_script_paths}') diff --git a/pytest-embedded/pytest_embedded/unity.py b/pytest-embedded/pytest_embedded/unity.py index 76f16e2d..f1b043b7 100644 --- a/pytest-embedded/pytest_embedded/unity.py +++ b/pytest-embedded/pytest_embedded/unity.py @@ -5,7 +5,7 @@ import xml.etree.ElementTree as ET from copy import deepcopy from functools import reduce -from typing import Any, AnyStr, Dict, List, Optional +from typing import Any, AnyStr from xml.sax.saxutils import escape from .utils import to_str @@ -63,7 +63,7 @@ def escape_illegal_xml_chars(s: str) -> str: return ILLEGAL_XML_CHAR_REGEX.sub('', s) -def escape_dict_value(d: Dict[str, Any]) -> Dict[str, str]: +def escape_dict_value(d: dict[str, Any]) -> dict[str, str]: escaped_dict = {} for k, v in d.items(): escaped_dict[k] = escape(str(v)) @@ -134,12 +134,12 @@ def to_xml(self) -> ET.Element: class TestSuite: - def __init__(self, name: Optional[str] = None, **kwargs): + def __init__(self, name: str | None = None, **kwargs): # required self.name = name or kwargs.pop('name') # may overwrite later # default stats - self.attrs: Dict[str, Any] = { + self.attrs: dict[str, Any] = { 'errors': 0, 'failures': 0, 'skipped': 0, @@ -147,15 +147,15 @@ def __init__(self, name: Optional[str] = None, **kwargs): } self.attrs.update(kwargs) - self.testcases: List[TestCase] = [] + self.testcases: list[TestCase] = [] self._xml = None @property - def failed_cases(self) -> List[TestCase]: + def failed_cases(self) -> list[TestCase]: return [case for case in self.testcases if case.result == 'FAIL'] - def add_unity_test_cases(self, s: AnyStr, additional_attrs: Optional[Dict[str, Any]] = None) -> None: + def add_unity_test_cases(self, s: AnyStr, additional_attrs: dict[str, Any] | None = None) -> None: s = to_str(s) # check format @@ -211,7 +211,7 @@ class JunitMerger: SUB_JUNIT_FILENAME = 'dut.xml' # multi-dut junit reports should be dut-[INDEX].xml - def __init__(self, main_junit: Optional[str], unity_test_report_mode: Optional[str] = None) -> None: + def __init__(self, main_junit: str | None, unity_test_report_mode: str | None = None) -> None: self.junit_path = main_junit self.unity_test_report_mode = unity_test_report_mode or UnityTestReportMode.REPLACE.value @@ -231,7 +231,7 @@ def junit(self) -> ET.ElementTree: def _int_add(*args) -> str: return reduce(lambda a, b: str(int(a) + int(b)), args) - def merge(self, junit_files: List[str]): + def merge(self, junit_files: list[str]): if not self.junit_path: return diff --git a/pytest-embedded/pytest_embedded/utils.py b/pytest-embedded/pytest_embedded/utils.py index a9593f7a..fb972794 100644 --- a/pytest-embedded/pytest_embedded/utils.py +++ b/pytest-embedded/pytest_embedded/utils.py @@ -41,9 +41,9 @@ @dataclass class ClassCliOptions: - classes: t.Dict[str, type] - mixins: t.Dict[str, t.List[type]] - kwargs: t.Dict[str, t.Dict[str, t.Any]] + classes: dict[str, type] + mixins: dict[str, list[type]] + kwargs: dict[str, dict[str, t.Any]] _T = t.TypeVar('_T') @@ -76,7 +76,7 @@ def __init__(self, service: str) -> None: class RequireServiceError(SystemExit): - def __init__(self, func_name: str, services: t.Union[str, t.List[str]]) -> None: + def __init__(self, func_name: str, services: str | list[str]) -> None: services_str = ','.join(to_list(services)) super().__init__( f'function {func_name} requires enabling one of the service(s) {services_str}. ' @@ -103,7 +103,7 @@ def to_str(bytes_str: t.AnyStr) -> str: return bytes_str -def to_bytes(bytes_str: t.AnyStr, ending: t.Optional[t.AnyStr] = None) -> bytes: +def to_bytes(bytes_str: t.AnyStr, ending: t.AnyStr | None = None) -> bytes: """ Turn `bytes` or `str` to `bytes` @@ -126,7 +126,7 @@ def to_bytes(bytes_str: t.AnyStr, ending: t.Optional[t.AnyStr] = None) -> bytes: return bytes_str -def to_list(s: _T) -> t.List[_T]: +def to_list(s: _T) -> list[_T]: """ Args: s: Anything @@ -149,7 +149,7 @@ def to_list(s: _T) -> t.List[_T]: return [s] -def find_by_suffix(suffix: str, path: str) -> t.List[str]: +def find_by_suffix(suffix: str, path: str) -> list[str]: res = [] for root, _, files in os.walk(path): for file in files: @@ -192,8 +192,8 @@ class Meta: """ logdir: str - port_target_cache: t.Dict[str, str] - port_app_cache: t.Dict[str, str] + port_target_cache: dict[str, str] + port_app_cache: dict[str, str] logfile_extension: str = '.log' def hit_port_target_cache(self, port: str, target: str) -> bool: @@ -237,7 +237,7 @@ def drop_port_app_cache(self, port: str) -> None: def lazy_load( - base_module: _ModuleType, name_obj_dict: t.Dict[str, t.Any], obj_module_dict: t.Dict[str, str] + base_module: _ModuleType, name_obj_dict: dict[str, t.Any], obj_module_dict: dict[str, str] ) -> t.Callable[[str], t.Any]: """ use __getattr__ in the __init__.py file to lazy load some objects diff --git a/pytest-embedded/tests/test_base.py b/pytest-embedded/tests/test_base.py index 83ba0a5b..aa7f24f7 100644 --- a/pytest-embedded/tests/test_base.py +++ b/pytest-embedded/tests/test_base.py @@ -8,9 +8,11 @@ def test_help(testdir): result = testdir.runpytest('--help') - result.stdout.fnmatch_lines([ - 'embedded:', - ]) + result.stdout.fnmatch_lines( + [ + 'embedded:', + ] + ) def test_services(testdir): @@ -655,6 +657,7 @@ def test_quick_example(redirect, dut: Dut): result.assert_outcomes(passed=1) +@pytest.mark.skip def test_unclosed_file_handler(testdir): """ select only support fd < FD_SETSIZE (1024) @@ -715,42 +718,6 @@ def test_foo(dut): class TestTargetMarkers: - def test_add_target_as_marker_simple(self, pytester): - pytester.makepyfile(""" - import pytest - @pytest.mark.parametrize('target', ['esp32'], indirect=True) - def test_example(target): - pass - """) - - result = pytester.runpytest('--add-target-as-marker', 'y') - - result.assert_outcomes(passed=1) - result.stdout.fnmatch_lines([ - '*Unknown pytest.mark.esp32 - is this a typo?*' # Check marker is present - ]) - - def test_add_target_as_marker_multi_target(self, pytester): - pytester.makepyfile(""" - import pytest - @pytest.mark.parametrize('target,count', [ - ('esp32|esp8266', 2), - ('esp32', 2), - ('esp32|esp8266|esp32s2', 3), - ], indirect=True) - def test_example(target): - pass - """) - - result = pytester.runpytest('--add-target-as-marker', 'y') - - result.assert_outcomes(passed=3) - result.stdout.fnmatch_lines([ - '*Unknown pytest.mark.esp32-esp8266 - is this a typo?*', - '*Unknown pytest.mark.esp32-esp32 - is this a typo?*', - '*Unknown pytest.mark.esp32-esp8266-esp32s2 - is this a typo?*', - ]) - def test_add_target_as_marker_with_amount(self, pytester): pytester.makepyfile(""" import pytest @@ -766,11 +733,13 @@ def test_example(target): result = pytester.runpytest('--add-target-as-marker-with-amount', 'y', '-vvvv') result.assert_outcomes(passed=3) - result.stdout.fnmatch_lines([ - '*Unknown pytest.mark.esp32+esp8266 - is this a typo?*', - '*Unknown pytest.mark.esp32_2 - is this a typo?*', - '*Unknown pytest.mark.esp32+esp32s2+esp8266 - is this a typo?*', - ]) + result.stdout.fnmatch_lines( + [ + '*Unknown pytest.mark.esp32+esp8266 - is this a typo?*', + '*Unknown pytest.mark.esp32_2 - is this a typo?*', + '*Unknown pytest.mark.esp32+esp32s2+esp8266 - is this a typo?*', + ] + ) def test_no_target_no_marker(self, pytester): pytester.makepyfile(""" @@ -778,7 +747,9 @@ def test_example(): pass """) - result = pytester.runpytest('--add-target-as-marker', 'y', '--embedded-services', 'esp', '--target', 'esp32') + result = pytester.runpytest( + '--add-target-as-marker-with-amount', 'y', '--embedded-services', 'esp', '--target', 'esp32' + ) result.assert_outcomes(passed=1) assert 'Unknown pytest.mark.esp32 - is this a typo?' not in result.stdout.str() diff --git a/ruff.toml b/ruff.toml index 88fc5d8e..6f62b5fe 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ line-length = 120 -target-version = "py37" +target-version = "py310" [lint] select = [ diff --git a/tests/fixtures/hello_world_esp32c3/build/bootloader/bootloader.bin b/tests/fixtures/hello_world_esp32c3/build/bootloader/bootloader.bin index 81d91b81..d4e44051 100644 Binary files a/tests/fixtures/hello_world_esp32c3/build/bootloader/bootloader.bin and b/tests/fixtures/hello_world_esp32c3/build/bootloader/bootloader.bin differ diff --git a/tests/fixtures/hello_world_esp32c3/build/hello_world.bin b/tests/fixtures/hello_world_esp32c3/build/hello_world.bin index cafe12cf..650ecbe9 100644 Binary files a/tests/fixtures/hello_world_esp32c3/build/hello_world.bin and b/tests/fixtures/hello_world_esp32c3/build/hello_world.bin differ diff --git a/tests/fixtures/hello_world_esp32c3/build/hello_world.elf b/tests/fixtures/hello_world_esp32c3/build/hello_world.elf index d918d4d3..ba5411d6 100755 Binary files a/tests/fixtures/hello_world_esp32c3/build/hello_world.elf and b/tests/fixtures/hello_world_esp32c3/build/hello_world.elf differ diff --git a/tests/fixtures/unit_test_app_esp32c3/CMakeLists.txt b/tests/fixtures/unit_test_app_esp32c3/CMakeLists.txt index ed66ba15..383d5157 100644 --- a/tests/fixtures/unit_test_app_esp32c3/CMakeLists.txt +++ b/tests/fixtures/unit_test_app_esp32c3/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) set(EXTRA_COMPONENT_DIRS - "$ENV{IDF_PATH}/tools/unit-test-app/components") + "$ENV{IDF_PATH}/tools/test_apps/components") include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(case_tester_example) diff --git a/tests/fixtures/unit_test_app_esp32c3/build/bootloader/bootloader.bin b/tests/fixtures/unit_test_app_esp32c3/build/bootloader/bootloader.bin index 5357df08..b4a6715c 100644 Binary files a/tests/fixtures/unit_test_app_esp32c3/build/bootloader/bootloader.bin and b/tests/fixtures/unit_test_app_esp32c3/build/bootloader/bootloader.bin differ diff --git a/tests/fixtures/unit_test_app_esp32c3/build/case_tester_example.bin b/tests/fixtures/unit_test_app_esp32c3/build/case_tester_example.bin index 2ff71656..5db67416 100644 Binary files a/tests/fixtures/unit_test_app_esp32c3/build/case_tester_example.bin and b/tests/fixtures/unit_test_app_esp32c3/build/case_tester_example.bin differ