Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
155 changes: 151 additions & 4 deletions pytest-embedded-qemu/pytest_embedded_qemu/qemu.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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__(
Expand All @@ -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,
):
Expand All @@ -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

Expand All @@ -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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When launching QEMU and expecting that it should communicate with another application, it is better to use -daemonize flag — as recommended by QEMU documentation. Otherwise there may be a race condition between QEMU getting ready to accept TCP connections and the launch of the other application. It seems child.expect('qemu') aims to work around that, but I am not sure this is reliable enough.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, now that I'm thinking of this... don't you end up with two QEMU processes running at the same time, and accessing the same eFuse file?.. One QEMU process is already launched when dut is created, another one is launched here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When launching QEMU and expecting that it should communicate with another application, it is better to use -daemonize flag — as recommended by QEMU documentation. Otherwise there may be a race condition between QEMU getting ready to accept TCP connections and the launch of the other application. It seems child.expect('qemu') aims to work around that, but I am not sure this is reliable enough.

I think solution using pexpect and nowait flag is reliable enough. Once qemu appears in the output, it indicates that QEMU is ready to accept connections. Another solution using -daemonize also works:

run_qemu_command = [
    '-daemonize',
    '-display',
    'none',
    '-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:
    proc = subprocess.run(
        [self.current_qemu_executable_path, *run_qemu_command],
        check=True
    )
    res = shlex.split(command)

    ...

But I suggest to keep the current implementation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, now that I'm thinking of this... don't you end up with two QEMU processes running at the same time, and accessing the same eFuse file?.. One QEMU process is already launched when dut is created, another one is launched here.

Yes, but the first process never writes to the eFuse file. All write operations are performed via TCP using espefuse. So it should be ok.

]
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:
Expand Down
47 changes: 47 additions & 0 deletions pytest-embedded-qemu/tests/test_qemu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
Expand Down
6 changes: 3 additions & 3 deletions pytest-embedded-wokwi/pytest_embedded_wokwi/wokwi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions pytest-embedded/pytest_embedded/dut_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions pytest-embedded/pytest_embedded/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. '
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
Loading