Skip to content
Open
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
26 changes: 20 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,26 @@ venv/
.idea/

# built binaries
tests/fixtures/unit_test_app*/build/
!tests/fixtures/**/bootloader.bin
!tests/fixtures/**/partition-table.bin
!tests/fixtures/**/sdkconfig.json
!tests/fixtures/**/flasher_args.json
!tests/fixtures/unit_test_app_*/build/**/case_tester_example.bin
tests/fixtures/*/build/*
!tests/fixtures/*_coredump_*/build/gdbinit/
tests/fixtures/*_coredump_*/build/gdbinit/*
!tests/fixtures/*_coredump_*/build/gdbinit/prefix_map
!tests/fixtures/*_coredump_*/build/*.elf

!tests/fixtures/*_panic/build/gdbinit/
tests/fixtures/*_panic/build/gdbinit/*
!tests/fixtures/*_panic/build/gdbinit/prefix_map
!tests/fixtures/*_panic/build/*.elf

!tests/fixtures/*/build/bootloader/
tests/fixtures/*/build/bootloader/*
!tests/fixtures/*/build/bootloader/*.bin
!tests/fixtures/*/build/partition_table/
!tests/fixtures/*/build/config/
tests/fixtures/*/build/config/*
!tests/fixtures/*/build/config/sdkconfig.json
!tests/fixtures/*/build/flasher_args.json
!tests/fixtures/*/build/*.bin

sdkconfig
sdkconfig.old
2 changes: 2 additions & 0 deletions pytest-embedded-idf/pytest_embedded_idf/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class IdfApp(App):
'esp32p4',
'esp32h2',
'esp32c61',
'esp32h21',
'esp32h4',
]

FLASH_ARGS_FILENAME = 'flash_args'
Expand Down
99 changes: 42 additions & 57 deletions pytest-embedded-idf/pytest_embedded_idf/dut.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import importlib.util
import logging
import os
import re
Expand Down Expand Up @@ -27,6 +26,7 @@ class IdfDut(IdfUnityDutMixin, SerialDut):
Attributes:
target (str): target chip type
skip_check_coredump (bool): skip check core dumped or not while dut teardown if set to True
skip_decode_panic (bool): skip decode panic output or not while dut teardown if set to True
"""

XTENSA_TARGETS = IdfApp.XTENSA_TARGETS
Expand All @@ -47,13 +47,12 @@ def __init__(
self,
app: IdfApp,
skip_check_coredump: bool = False,
panic_output_decode_script: str | None = None,
skip_decode_panic: bool = False,
**kwargs,
) -> None:
self.target = app.target
self.skip_check_coredump = skip_check_coredump
self._panic_output_decode_script = panic_output_decode_script

self.skip_decode_panic = skip_decode_panic
super().__init__(app=app, **kwargs)

self._hard_reset_func = self.serial.hard_reset
Expand All @@ -71,26 +70,6 @@ def toolchain_prefix(self) -> str:
else:
raise ValueError(f'Unknown target: {self.target}')

@property
def panic_output_decode_script(self) -> str | None:
"""
Returns:
Panic output decode script path
"""

script_filepath = self._panic_output_decode_script
if not script_filepath or not os.path.isfile(script_filepath):
module = importlib.util.find_spec('esp_idf_panic_decoder.gdb_panic_server')
if not module:
raise ValueError(
'Panic output decode script not found. '
'Please use the --panic-output-decode-script flag to provide a script '
'or install esp-idf-panic-decoder using the command: `pip install esp-idf-panic-decoder` .'
)
script_filepath = module.origin

return os.path.realpath(script_filepath)

def _get_prefix_map_path(self) -> str:
primary = os.path.join(self.app.binary_path, 'gdbinit', 'prefix_map')
fallback = os.path.join(self.app.binary_path, 'prefix_map_gdbinit')
Expand All @@ -99,7 +78,11 @@ def _get_prefix_map_path(self) -> str:
return primary
return fallback

def _check_panic_decode_trigger(self): # type: () -> None
def _decode_panic(self): # type: () -> None
if self.target not in self.RISCV32_TARGETS:
logging.debug('panic decode only supported for riscv32 targets')
return

if not self.app.elf_file:
logging.warning('No elf file found. Skipping decode panic output...')
return
Expand All @@ -118,25 +101,27 @@ def _check_panic_decode_trigger(self): # type: () -> None
with tempfile.NamedTemporaryFile(mode='wb', delete=False) as panic_output_file:
panic_output_file.write(panic_output)
panic_output_file.flush()

cmd = [
f'{self.toolchain_prefix}gdb',
'--command',
self._get_prefix_map_path(),
'--batch',
'-n',
self.app.elf_file,
'-ex',
f'target remote | "{sys.executable}" -m esp_idf_panic_decoder --target {self.target} "{panic_output_file.name}"', # noqa: E501
'-ex',
'bt',
]
try:
cmd = [
f'{self.toolchain_prefix}-gdb',
'--command',
self._get_prefix_map_path(),
'--batch',
'-n',
self.app.elf_file,
'-ex',
f'target remote | "{sys.executable}" "{self.panic_output_decode_script}" --target {self.target} "{panic_output_file.name}"', # noqa: E501
'-ex',
'bt',
]
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
logging.info('\n\nBacktrace:\n')
logging.info(output.decode())
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('utf-8')
logging.info(f'Backtrace:\n{output}')
with open(self.logfile.replace('dut', 'panic_decoded', 1), 'w') as fw:
fw.write(output)
logging.info(f'Please check decoded panic output file at: {fw.name}')
except subprocess.CalledProcessError as e:
logging.debug(f'Failed to run gdb_panic_server.py script: {e}\n{e.output}\n\n')
logging.info(panic_output.decode())
logging.error(f'Failed to decode panic output: {e.output}. Command was: \n{" ".join(cmd)}')
finally:
if panic_output_file is not None:
try:
Expand All @@ -160,8 +145,6 @@ def _check_coredump(self) -> None:
Returns:
None
"""
if self.target in self.RISCV32_TARGETS:
self._check_panic_decode_trigger() # need IDF_PATH
if self.app.sdkconfig.get('ESP_COREDUMP_ENABLE_TO_UART', False):
self._dump_b64_coredumps()
elif self.app.sdkconfig.get('ESP_COREDUMP_ENABLE_TO_FLASH', False):
Expand Down Expand Up @@ -189,12 +172,14 @@ def _dump_b64_coredumps(self) -> None:
coredump = CoreDump(
chip=self.target,
core=coredump_file.name,
core_format='b64',
prog=self.app.elf_file,
)
with open(os.path.join(self._meta.logdir, f'coredump_output_{i}'), 'w') as fw:
with open(self.logfile.replace('dut', 'coredump', 1), 'w') as fw:
with redirect_stdout(fw):
coredump.info_corefile()
logging.info(f'Please check coredump output file at: {fw.name}')
except Exception as e:
logging.error(f'Error dumping b64 coredump {i} for target: {self.target}: {e}')
finally:
if coredump_file:
os.remove(coredump_file.name)
Expand All @@ -206,30 +191,30 @@ def _dump_flash_coredump(self) -> None:

from esp_coredump import CoreDump # need IDF_PATH

if self.app.sdkconfig['ESP_COREDUMP_DATA_FORMAT_ELF']:
core_format = 'elf'
elif self.app.sdkconfig['ESP_COREDUMP_DATA_FORMAT_BIN']:
core_format = 'raw'
else:
raise ValueError('Invalid coredump format. Use _parse_b64_coredump for UART')

with self.serial.disable_redirect_thread():
self.serial.close()
with redirect_stdout(self._q):
coredump = CoreDump(
chip=self.target,
core_format=core_format,
port=self.serial.port,
prog=self.app.elf_file,
)
with open(os.path.join(self._meta.logdir, 'coredump_output'), 'w') as fw:
with open(self.logfile.replace('dut', 'coredump', 1), 'w') as fw:
with redirect_stdout(fw):
coredump.info_corefile()
logging.info(f'Please check coredump output file at: {fw.name}')

def close(self) -> None:
if not self.skip_decode_panic:
try:
self._decode_panic()
except Exception as e:
logging.error(f'Error decoding panic output for target: {self.target}: {e}')

if not self.skip_check_coredump:
try:
self._check_coredump()
except Exception as e:
logging.debug(e)
logging.error(f'Error checking coredump for target: {self.target}: {e}')
super().close()

def write(self, data: t.AnyStr) -> None:
Expand Down
47 changes: 39 additions & 8 deletions pytest-embedded-idf/tests/test_idf.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,13 +562,44 @@ def test_unity_tester_with_linux(dut):


@toolchain_required
def test_check_coredump(testdir, caplog, first_index_of_messages):
@pytest.mark.parametrize(
'coredump_type',
['flash', 'uart'],
)
def test_check_coredump(testdir, coredump_type, caplog, first_index_of_messages):
testdir.makepyfile(rf"""
import pexpect
import pytest
def test_check_coredump_{coredump_type}(dut):
dut.expect(pexpect.TIMEOUT, timeout=3)
""")

result = testdir.runpytest(
'-s',
'--embedded-services',
'esp,idf',
'--app-path',
f'{os.path.join(testdir.tmpdir, f"hello_world_esp32_coredump_{coredump_type}")}',
'--target',
'esp32',
'--log-cli-level',
'INFO',
)
first_index_of_messages(
re.compile('Please check coredump output file at:.+', re.MULTILINE),
caplog.messages,
)
result.assert_outcomes(passed=1)


@toolchain_required
def test_check_panic(testdir, caplog, first_index_of_messages):
testdir.makepyfile(r"""
import pexpect
import pytest

def test_check_coredump(dut):
dut.expect(pexpect.TIMEOUT, timeout=10)
def test_check_panic(dut):
dut.expect(pexpect.TIMEOUT, timeout=3)
""")

result = testdir.runpytest(
Expand All @@ -579,13 +610,15 @@ def test_check_coredump(dut):
f'{os.path.join(testdir.tmpdir, "hello_world_esp32c3_panic")}',
'--target',
'esp32c3',
'--panic-output-decode-script',
os.path.join(testdir.tmpdir, 'gdb_panic_server.py'),
'--log-cli-level',
'INFO',
)
first_index_of_messages(
re.compile(r'app_main \(\) at /COMPONENT_MAIN_DIR/hello_world_main.c:17', re.MULTILINE),
re.compile(r'Backtrace:\napp_main \(\) at /COMPONENT_MAIN_DIR/hello_world_main.c:17', re.MULTILINE),
caplog.messages,
)
first_index_of_messages(
re.compile(r'Please check decoded panic output file at:.+', re.MULTILINE),
caplog.messages,
)

Expand All @@ -608,8 +641,6 @@ def test_skip_check_coredump(dut):
'esp,idf',
'--app-path',
f'{os.path.join(testdir.tmpdir, "hello_world_esp32c3_panic")}',
'--panic-output-decode-script',
os.path.join(testdir.tmpdir, 'gdb_panic_server.py'),
'--skip-check-coredump',
'True',
'--log-cli-level',
Expand Down
6 changes: 5 additions & 1 deletion pytest-embedded-serial/pytest_embedded_serial/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,18 +144,22 @@ def close(self):
logging.debug(f'released {port}')

@contextlib.contextmanager
def disable_redirect_thread(self) -> bool:
def disable_redirect_thread(self, kill_port: bool = False) -> bool:
"""
kill the redirect thread, and start a new one after got yield back

Yields:
True if redirect serial thread has been terminated
"""
killed = self.stop_redirect_thread()
if kill_port:
self.proc.close()

yield killed

if killed:
if kill_port:
self._start()
self.start_redirect_thread()


Expand Down
10 changes: 5 additions & 5 deletions pytest-embedded/pytest_embedded/dut_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def _fixture_classes_and_options_fn(
confirm_target_elf_sha256,
erase_nvs,
skip_check_coredump,
panic_output_decode_script,
skip_decode_panic,
openocd_prog_path,
openocd_cli_args,
gdb_prog_path,
Expand Down Expand Up @@ -404,7 +404,7 @@ def _fixture_classes_and_options_fn(
kwargs[fixture].update(
{
'skip_check_coredump': skip_check_coredump,
'panic_output_decode_script': panic_output_decode_script,
'skip_decode_panic': skip_decode_panic,
}
)
elif 'esp' in _services and 'nuttx' in _services:
Expand Down Expand Up @@ -667,7 +667,7 @@ def create(
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,
skip_decode_panic: bool | None = None,
openocd_prog_path: str | None = None,
openocd_cli_args: str | None = None,
gdb_prog_path: str | None = None,
Expand Down Expand Up @@ -714,7 +714,7 @@ def create(
confirm_target_elf_sha256: Confirm target ELF SHA256.
erase_nvs: Erase NVS flag.
skip_check_coredump: Skip coredump check flag.
panic_output_decode_script: Panic output decode script.
skip_decode_panic: TODO
openocd_prog_path: OpenOCD program path.
openocd_cli_args: OpenOCD CLI arguments.
gdb_prog_path: GDB program path.
Expand Down Expand Up @@ -782,7 +782,7 @@ def create(
'confirm_target_elf_sha256': confirm_target_elf_sha256,
'erase_nvs': erase_nvs,
'skip_check_coredump': skip_check_coredump,
'panic_output_decode_script': panic_output_decode_script,
'skip_decode_panic': skip_decode_panic,
'openocd_prog_path': openocd_prog_path,
'openocd_cli_args': openocd_cli_args,
'gdb_prog_path': gdb_prog_path,
Expand Down
Loading
Loading