diff --git a/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py b/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py index ade80b95..548ac800 100644 --- a/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py +++ b/pytest-embedded-qemu/pytest_embedded_qemu/qemu.py @@ -1,9 +1,11 @@ import asyncio +import binascii import logging import os import shlex import socket import typing as t +from dataclasses import dataclass from pytest_embedded.log import DuplicateStdoutPopen from qemu.qmp import QMPClient @@ -14,6 +16,87 @@ from .app import QemuApp +@dataclass +class QemuTarget: + strap_mode: str + default_efuse: bytes + + +QEMU_TARGETS: dict[str, QemuTarget] = { + 'esp32': QemuTarget( + strap_mode='0x0F', + default_efuse=binascii.unhexlify( + '00000000000000000000000000800000000000000000100000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000' + ), + ), + 'esp32c3': QemuTarget( + strap_mode='0x02', + default_efuse=binascii.unhexlify( + '00000000000000000000000000000000000000000000000000000000000000000000000000000c00' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '000000000000000000000000000000000000000000000000' + ), + ), + 'esp32s3': QemuTarget( + strap_mode='0x07', + default_efuse=binascii.unhexlify( + '00000000000000000000000000000000000000000000000000000000000000000000000000000c00' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '00000000000000000000000000000000000000000000000000000000000000000000000000000000' + '000000000000000000000000000000000000000000000000' + ), + ), +} + + class Qemu(DuplicateStdoutPopen): """ QEMU class @@ -26,9 +109,8 @@ class Qemu(DuplicateStdoutPopen): QEMU_DEFAULT_ARGS = '-nographic -machine esp32' QEMU_DEFAULT_FMT = '-nographic -machine {}' - QEMU_STRAP_MODE_FMT = '-global driver=esp32.gpio,property=strap_mode,value={}' + QEMU_STRAP_MODE_FMT = 'driver={}.gpio,property=strap_mode,value={}' QEMU_SERIAL_TCP_FMT = '-serial tcp::{},server,nowait' - QEMU_DEFAULT_QMP_FMT = '-qmp tcp:127.0.0.1:{},server,wait=off' def __init__( @@ -37,6 +119,7 @@ def __init__( qemu_prog_path: str | None = None, qemu_cli_args: str | None = None, qemu_extra_args: str | None = None, + qemu_efuse_path: str | None = None, app: t.Optional['QemuApp'] = None, **kwargs, ): @@ -53,13 +136,28 @@ def __init__( if not os.path.exists(image_path): raise ValueError(f"QEMU image path doesn't exist: {image_path}") - qemu_prog_path = qemu_prog_path or self.qemu_prog_name + self.qemu_prog_path = qemu_prog_path or self.qemu_prog_name + self.image_path = image_path + self.efuse_path = qemu_efuse_path if qemu_cli_args: qemu_cli_args = qemu_cli_args.strip('"').strip("'") qemu_cli_args = shlex.split(qemu_cli_args or self.qemu_default_args) qemu_extra_args = shlex.split(qemu_extra_args or '') + if self.efuse_path: + logging.debug('The eFuse file will be saved to: %s', self.efuse_path) + with open(self.efuse_path, 'wb') as f: + f.write(QEMU_TARGETS[self.app.target].default_efuse) + qemu_extra_args += [ + '-global', + self.QEMU_STRAP_MODE_FMT.format(self.app.target, QEMU_TARGETS[self.app.target].strap_mode), + '-drive', + f'file={self.efuse_path},if=none,format=raw,id=efuse', + '-global', + f'driver=nvram.{self.app.target}.efuse,property=drive,value=efuse', + ] + self.qmp_addr = None self.qmp_port = None @@ -83,10 +181,59 @@ def __init__( qemu_cli_args += shlex.split(self.QEMU_DEFAULT_QMP_FMT.format(self.qmp_port)) super().__init__( - cmd=[qemu_prog_path, *qemu_cli_args, *qemu_extra_args, '-drive', f'file={image_path},if=mtd,format=raw'], + cmd=[ + self.qemu_prog_path, + *qemu_cli_args, + *qemu_extra_args, + '-drive', + f'file={image_path},if=mtd,format=raw', + ], **kwargs, ) + def execute_efuse_command(self, command: str): + import espefuse + import pexpect + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('127.0.0.1', 0)) + _, available_port = s.getsockname() + + run_qemu_command = [ + '-nographic', + '-machine', + self.app.target, + '-drive', + f'file={self.image_path},if=mtd,format=raw', + '-global', + self.QEMU_STRAP_MODE_FMT.format(self.app.target, QEMU_TARGETS[self.app.target].strap_mode), + '-drive', + f'file={self.efuse_path},if=none,format=raw,id=efuse', + '-global', + f'driver=nvram.{self.app.target}.efuse,property=drive,value=efuse', + '-serial', + f'tcp::{available_port},server,nowait', + ] + try: + child = pexpect.spawn(self.qemu_prog_path, run_qemu_command) + res = shlex.split(command) + child.expect('qemu') + + res = [r for r in res if r != '--do-not-confirm'] + espefuse.main( + [ + '--port', + f'socket://localhost:{available_port}', + '--before', + 'no-reset', + '--do-not-confirm', + *res, + ] + ) + self._hard_reset() + finally: + child.terminate() + @property def qemu_prog_name(self): if self.app: diff --git a/pytest-embedded-qemu/tests/test_qemu.py b/pytest-embedded-qemu/tests/test_qemu.py index 53d25f7b..b6bf230e 100644 --- a/pytest-embedded-qemu/tests/test_qemu.py +++ b/pytest-embedded-qemu/tests/test_qemu.py @@ -11,6 +11,53 @@ ) +@qemu_bin_required +def test_pexpect_write_efuse(testdir): + testdir.makepyfile(""" + import pexpect + import pytest + + def test_pexpect_by_qemu(dut): + dut.qemu.execute_efuse_command('burn-custom-mac 00:11:22:33:44:55') + dut.expect('') + + with open('/tmp/test.test', 'rb') as f: + content = f.read() + + expected_output = [ + '00000000:00000000000000000000000000800000', + '00000010:00000000000010000000000000000000', + '00000020:00000000000000000000000000000000', + '00000030:00000000000000000000000000000000', + '00000040:00000000000000000000000000000000', + '00000050:000000000000000000000000b8001122', + '00000060:33445500000000000000000000000000', + '00000070:000000010000000000000000' + ] + + lines = [] + for i in range(0, len(content), 16): + line = content[i:i+16] + hex_values = ''.join(f'{byte:02x}' for byte in line) + lines.append(f'{i:08x}:{hex_values}') + assert lines == expected_output + + """) + + result = testdir.runpytest( + '-s', + '--embedded-services', + 'idf,qemu', + '--app-path', + os.path.join(testdir.tmpdir, 'hello_world_esp32'), + '--qemu-cli-args="-machine esp32 -nographic"', + '--qemu-efuse-path', + '/tmp/test.test', + ) + + result.assert_outcomes(passed=1) + + @qemu_bin_required def test_pexpect_by_qemu_xtensa(testdir): testdir.makepyfile(""" diff --git a/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi.py b/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi.py index 1c4f2b97..0ceed139 100644 --- a/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi.py +++ b/pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi.py @@ -47,6 +47,9 @@ def __init__( meta: Meta | None = None, **kwargs, ): + # Initialize parent class + super().__init__(msg_queue=msg_queue, meta=meta, **kwargs) + self.app = app # Get Wokwi API token @@ -71,9 +74,6 @@ def __init__( self.create_diagram_json() wokwi_diagram = os.path.join(self.app.app_path, 'diagram.json') - # Initialize parent class - super().__init__(msg_queue=msg_queue, meta=meta, **kwargs) - # Connect and start simulation try: flasher_args = firmware_resolver.resolve_firmware(app) diff --git a/pytest-embedded/pytest_embedded/dut_factory.py b/pytest-embedded/pytest_embedded/dut_factory.py index d2e03186..dcb9d7ec 100644 --- a/pytest-embedded/pytest_embedded/dut_factory.py +++ b/pytest-embedded/pytest_embedded/dut_factory.py @@ -138,6 +138,7 @@ def _fixture_classes_and_options_fn( qemu_prog_path, qemu_cli_args, qemu_extra_args, + qemu_efuse_path, wokwi_diagram, skip_regenerate_image, encrypt, @@ -172,6 +173,7 @@ def _fixture_classes_and_options_fn( 'encrypt': encrypt, 'keyfile': keyfile, 'qemu_prog_path': qemu_prog_path, + 'qemu_efuse_path': qemu_efuse_path, } ) else: @@ -303,6 +305,7 @@ def _fixture_classes_and_options_fn( 'qemu_prog_path': qemu_prog_path, 'qemu_cli_args': qemu_cli_args, 'qemu_extra_args': qemu_extra_args, + 'qemu_efuse_path': qemu_efuse_path, 'app': None, 'meta': _meta, 'dut_index': dut_index, @@ -674,6 +677,7 @@ def create( qemu_prog_path: str | None = None, qemu_cli_args: str | None = None, qemu_extra_args: str | None = None, + qemu_efuse_path: str | None = None, wokwi_diagram: str | None = None, skip_regenerate_image: bool | None = None, encrypt: bool | None = None, @@ -720,6 +724,7 @@ def create( qemu_prog_path: QEMU program path. qemu_cli_args: QEMU CLI arguments. qemu_extra_args: Additional QEMU arguments. + qemu_efuse_path: Efuse binary path. wokwi_diagram: Wokwi diagram path. skip_regenerate_image: Skip image regeneration flag. encrypt: Encryption flag. @@ -787,6 +792,7 @@ def create( 'qemu_prog_path': qemu_prog_path, 'qemu_cli_args': qemu_cli_args, 'qemu_extra_args': qemu_extra_args, + 'qemu_efuse_path': qemu_efuse_path, 'wokwi_diagram': wokwi_diagram, 'skip_regenerate_image': skip_regenerate_image, 'encrypt': encrypt, diff --git a/pytest-embedded/pytest_embedded/plugin.py b/pytest-embedded/pytest_embedded/plugin.py index aa98d86c..78d60b2d 100644 --- a/pytest-embedded/pytest_embedded/plugin.py +++ b/pytest-embedded/pytest_embedded/plugin.py @@ -274,6 +274,10 @@ def pytest_addoption(parser): '--qemu-extra-args', help='QEMU cli extra arguments, will append to the argument list. (Default: None)', ) + qemu_group.addoption( + '--qemu-efuse-path', + help='This option makes it possible to use efuse in QEMU when it is set up.', + ) qemu_group.addoption( '--skip-regenerate-image', help='y/yes/true for True and n/no/false for False. ' @@ -963,6 +967,13 @@ def qemu_extra_args(request: FixtureRequest) -> str | None: return _request_param_or_config_option_or_default(request, 'qemu_extra_args', None) +@pytest.fixture +@multi_dut_argument +def qemu_efuse_path(request: FixtureRequest) -> str | None: + """Enable parametrization for the same cli option""" + return _request_param_or_config_option_or_default(request, 'qemu_efuse_path', None) + + @pytest.fixture @multi_dut_argument def skip_regenerate_image(request: FixtureRequest) -> str | None: @@ -1050,6 +1061,7 @@ def parametrize_fixtures( qemu_prog_path, qemu_cli_args, qemu_extra_args, + qemu_efuse_path, wokwi_diagram, skip_regenerate_image, encrypt,