Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Wokwi simulation support (RDT-551) #230

Merged
merged 10 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions pytest-embedded-wokwi/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1 change: 1 addition & 0 deletions pytest-embedded-wokwi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
### pytest-embedded-wokwi
urish marked this conversation as resolved.
Show resolved Hide resolved
105 changes: 105 additions & 0 deletions pytest-embedded-wokwi/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"

[project]
name = "pytest-embedded-wokwi"
authors = [
{name = "Fu Hanxi", email = "[email protected]"},
urish marked this conversation as resolved.
Show resolved Hide resolved
]
readme = "README.md"
license = {file = "LICENSE"}
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Pytest",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"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",
"Topic :: Software Development :: Testing",
]
dynamic = ["version", "description"]
requires-python = ">=3.7"

dependencies = [
"pytest-embedded~=1.3.5",
]

[project.optional-dependencies]
idf = [
"pytest-embedded-idf~=1.3.5",
]

[project.urls]
homepage = "https://github.com/espressif/pytest-embedded"
repository = "https://github.com/espressif/pytest-embedded"
documentation = "https://docs.espressif.com/projects/pytest-embedded/en/latest/"
changelog = "https://github.com/espressif/pytest-embedded/blob/main/CHANGELOG.md"

[tool.isort]
profile = 'black'

[tool.black]
line-length = 120
target-version = ['py37']
force-exclude = '/tests/'
skip-string-normalization = true

[tool.ruff]
select = [
'F', # Pyflakes
'E', # pycodestyle
'W', # pycodestyle
# 'C90', # mccabe
# 'I', # isort
# 'N', # pep8-naming
# 'D', # pydocstyle
# 'UP', # pyupgrade
# 'YTT', # flake8-2020
# 'ANN', # flake8-annotations
# 'S', # flake8-bandit
# 'BLE', # flake8-blind-except
# 'FBT', # flake8-boolean-trap
# 'B', # flake8-bugbear
# 'A', # flake8-builtins
# 'COM', # flake8-commas
# 'C4', # flake8-comprehensions
# 'DTZ', # flake8-datetimez
# 'T10', # flake8-debugger
# 'DJ', # flake8-django
# 'EM', # flake8-errmsg
# 'EXE', # flake8-executable
# 'ISC', # flake8-implicit-str-concat
# 'ICN', # flake8-import-conventions
# 'G', # flake8-logging-format
# 'INP', # flake8-no-pep420
# 'PIE', # flake8-pie
# 'T20', # flake8-print
# 'PYI', # flake8-pyi
# 'PT', # flake8-pytest-style
# 'Q', # flake8-quotes
# 'RSE', # flake8-raise
# 'RET', # flake8-return
# 'SLF', # flake8-self
# 'SIM', # flake8-simplify
# 'TID', # flake8-tidy-imports
# 'TCH', # flake8-type-checking
# 'ARG', # flake8-unused-arguments
# 'PTH', # flake8-use-pathlib
# 'ERA', # eradicate
# 'PD', # pandas-vet
# 'PGH', # pygrep-hooks
# 'PL', # Pylint
# 'TRY', # tryceratops
# 'NPY', # NumPy-specific rules
# 'RUF', # Ruff-specific rules
]
line-length = 120
target-version = "py37"
11 changes: 11 additions & 0 deletions pytest-embedded-wokwi/pytest_embedded_wokwi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Make pytest-embedded plugin work with the Wokwi CLI."""

from .dut import WokwiDut # noqa
from .wokwi_cli import WokwiCLI # noqa

__all__ = [
'WokwiCLI',
'WokwiDut',
]

__version__ = '1.3.5'
25 changes: 25 additions & 0 deletions pytest-embedded-wokwi/pytest_embedded_wokwi/dut.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import AnyStr

from pytest_embedded.dut import Dut

from .wokwi_cli import WokwiCLI


class WokwiDut(Dut):
"""
Wokwi DUT class
"""

def __init__(
self,
wokwi: WokwiCLI,
**kwargs,
) -> None:
self.wokwi = wokwi

super().__init__(**kwargs)

self._hard_reset_func = self.wokwi._hard_reset

def write(self, s: AnyStr) -> None:
self.wokwi.write(s)
hfudev marked this conversation as resolved.
Show resolved Hide resolved
85 changes: 85 additions & 0 deletions pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import json
import os
import typing as t
from pathlib import Path

from pytest_embedded import __version__
from pytest_embedded.log import DuplicateStdoutPopen

if t.TYPE_CHECKING:
from pytest_embedded_idf.app import IdfApp


target_to_board = {
'esp32': 'board-esp32-devkit-c-v4',
'esp32c3': 'board-esp32-c3-devkitm-1',
'esp32c6': 'board-esp32-c6-devkitc-1',
'esp32h2': 'board-esp32-h2-devkitm-1',
'esp32s2': 'board-esp32-s2-devkitm-1',
'esp32s3': 'board-esp32-s3-devkitc-1',
}


class WokwiCLI(DuplicateStdoutPopen):
"""
WokwiCLI class
"""

SOURCE = 'Wokwi'

WOKWI_CLI_PATH = 'wokwi-cli'

def __init__(
self,
wokwi_cli_path: t.Optional[str] = None,
app: t.Optional['IdfApp'] = None,
**kwargs,
):
"""
Args:
wokwi_cli_path: Wokwi CLI arguments
"""
self.app = app
flasher_args = Path(app.binary_path, 'flasher_args.json')

with open(os.path.join(app.app_path, 'wokwi.toml'), 'wt') as f:
hfudev marked this conversation as resolved.
Show resolved Hide resolved
f.write(
f"""
[wokwi]
version = 1
generatedBy = 'pytest-embedded-wokwi {__version__}'
firmware = '{Path(flasher_args).relative_to(app.app_path).as_posix()}'
elf = '{Path(app.elf_file).relative_to(app.app_path).as_posix()}'
"""
)

# TODO: Check if diagram already exist and update it?
diagram = {
'version': 1,
'author': 'Uri Shaked',
'editor': 'wokwi',
'parts': [{'type': target_to_board[app.target], 'id': 'esp'}],
'connections': [
['esp:TX', '$serialMonitor:RX', '', []],
['esp:RX', '$serialMonitor:TX', '', []],
],
}
with open(os.path.join(app.app_path, 'diagram.json'), 'wt') as f:
f.write(json.dumps(diagram))

wokwi_cli = wokwi_cli_path or self.wokwi_cli_executable

super().__init__(
cmd=[wokwi_cli, app.app_path],
**kwargs,
)

@property
def wokwi_cli_executable(self):
return self.WOKWI_CLI_PATH

def _hard_reset(self):
"""
This is a fake hard_reset. Keep this API to keep the consistency.
"""
raise NotImplementedError
64 changes: 63 additions & 1 deletion pytest-embedded/pytest_embedded/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from pytest_embedded_jtag import Gdb, OpenOcd
from pytest_embedded_qemu import Qemu
from pytest_embedded_serial import Serial
from pytest_embedded_wokwi import WokwiCLI


_T = t.TypeVar('_T')
Expand Down Expand Up @@ -118,6 +119,7 @@ def pytest_addoption(parser):
'- jtag: openocd and gdb\n'
'- qemu: use qemu simulator instead of the real target\n'
'- arduino: auto-detect more app info with arduino specific rules, auto flash-in\n'
'- wokwi: use wokwi simulator instead of the real target\n'
'All the related CLI options are under the groups named by "embedded-<service>"',
)
base_group.addoption('--app-path', help='App path')
Expand Down Expand Up @@ -242,6 +244,12 @@ def pytest_addoption(parser):
help='Flash Encryption (pre-encrypted workflow) key path. (Default: None)',
)

wokwi_group = parser.getgroup('embedded-wokwi')
wokwi_group.addoption(
'--wokwi-cli-path',
help='Path to the wokwi-cli program (Default: "wokwi-cli")',
)


###########
# helpers #
Expand Down Expand Up @@ -949,6 +957,16 @@ def keyfile(request: FixtureRequest) -> t.Optional[str]:
return _request_param_or_config_option_or_default(request, 'keyfile', None)


#########
# Wokwi #
#########
@pytest.fixture
@multi_dut_argument
def wokwi_cli_path(request: FixtureRequest) -> t.Optional[str]:
"""Enable parametrization for the same cli option"""
return _request_param_or_config_option_or_default(request, 'wokwi_cli_path', None)


####################
# Private Fixtures #
####################
Expand Down Expand Up @@ -1009,6 +1027,7 @@ def _fixture_classes_and_options(
qemu_prog_path,
qemu_cli_args,
qemu_extra_args,
wokwi_cli_path,
skip_regenerate_image,
encrypt,
keyfile,
Expand Down Expand Up @@ -1175,6 +1194,17 @@ def _fixture_classes_and_options(
'app': None,
'meta': _meta,
}
elif fixture == 'wokwi':
if 'wokwi' in _services:
from pytest_embedded_wokwi import WokwiCLI

classes[fixture] = WokwiCLI
kwargs[fixture] = {
'wokwi_cli_path': wokwi_cli_path,
'msg_queue': msg_queue,
'app': None,
'meta': _meta,
}
elif fixture == 'dut':
classes[fixture] = Dut
kwargs[fixture] = {
Expand All @@ -1185,6 +1215,20 @@ def _fixture_classes_and_options(
'test_case_name': test_case_name,
'meta': _meta,
}
if 'wokwi' in _services:
from pytest_embedded_wokwi import WokwiDut

classes[fixture] = WokwiDut
kwargs[fixture].update(
{
'wokwi': None,
}
)

if 'idf' in _services:
from pytest_embedded_idf.unity_tester import IdfUnityDutMixin

mixins[fixture].append(IdfUnityDutMixin)
if 'qemu' in _services:
from pytest_embedded_qemu import QemuDut

Expand Down Expand Up @@ -1317,6 +1361,22 @@ def qemu(_fixture_classes_and_options: ClassCliOptions, app) -> t.Optional['Qemu
return cls(**_drop_none_kwargs(kwargs))


@pytest.fixture
@multi_dut_generator_fixture
def wokwi(_fixture_classes_and_options: ClassCliOptions, app) -> t.Optional['WokwiCLI']:
"""A wokwi subprocess that could read/redirect/write"""
if 'wokwi' not in _fixture_classes_and_options.classes:
return None

cls = _fixture_classes_and_options.classes['wokwi']
kwargs = _fixture_classes_and_options.kwargs['wokwi']

if 'app' in kwargs and kwargs['app'] is None:
kwargs['app'] = app

return cls(**_drop_none_kwargs(kwargs))


@pytest.fixture
@multi_dut_generator_fixture
def dut(
Expand All @@ -1326,6 +1386,7 @@ def dut(
app: App,
serial: t.Optional[t.Union['Serial', 'LinuxSerial']],
qemu: t.Optional['Qemu'],
wokwi: t.Optional['WokwiCLI'],
) -> t.Union[Dut, t.List[Dut]]:
"""
A device under test (DUT) object that could gather output from various sources and redirect them to the pexpect
Expand Down Expand Up @@ -1356,7 +1417,8 @@ def dut(
kwargs[k] = gdb
elif k == 'qemu':
kwargs[k] = qemu

elif k == 'wokwi':
kwargs[k] = wokwi
return cls(**_drop_none_kwargs(kwargs), mixins=mixins)


Expand Down
Loading