mirror of
https://github.com/espressif/esp-idf.git
synced 2026-08-15 21:25:17 +03:00
157 lines
6.4 KiB
Python
157 lines
6.4 KiB
Python
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
|
# SPDX-License-Identifier: Unlicense OR CC0-1.0
|
|
import json
|
|
import os.path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import typing
|
|
|
|
import pexpect
|
|
import pytest
|
|
from pytest_embedded_idf import IdfDut
|
|
from pytest_embedded_idf.utils import idf_parametrize
|
|
from pytest_embedded_idf.utils import soc_filtered_targets
|
|
|
|
if typing.TYPE_CHECKING:
|
|
from conftest import OpenOCD
|
|
|
|
# Function-trace events are a SystemView module. The first (only) registered module
|
|
# gets EventOffset 512, so enter uses ID 512 and exit uses ID 513.
|
|
FT_EVENT_ENTER = 512
|
|
FT_EVENT_EXIT = 513
|
|
|
|
# Demo source compiled with instrumentation. Its functions must be traced.
|
|
INSTRUMENTED_SOURCE = 'ft_demo.c'
|
|
# Demo sources excluded by file. Their functions must never be traced.
|
|
EXCLUDED_SOURCES = ['ft_demo_hot.c', 'ft_demo_quiet.c']
|
|
|
|
|
|
def _encode_event_id(event_id: int) -> bytes:
|
|
"""Encode a SystemView event ID the way SEGGER_SYSVIEW does (base-128, LSB first)."""
|
|
out = bytearray()
|
|
while True:
|
|
b = event_id & 0x7F
|
|
event_id >>= 7
|
|
out.append(b | 0x80 if event_id else b)
|
|
if not event_id:
|
|
return bytes(out)
|
|
|
|
|
|
def _toolchain_prefix(binary_path: str) -> str:
|
|
with open(os.path.join(binary_path, 'project_description.json')) as f:
|
|
return str(json.load(f)['monitor_toolprefix'])
|
|
|
|
|
|
def _validate_function_trace_manual(trace_log: str) -> None:
|
|
"""Fallback validation when no toolchain is available to decode the capture.
|
|
|
|
The module description string is not in a JTAG capture (it is recorded before
|
|
recording is enabled and never re-sent), so check for the enter/exit event IDs.
|
|
"""
|
|
with open(trace_log, 'rb') as f:
|
|
content = f.read()
|
|
enter = content.count(_encode_event_id(FT_EVENT_ENTER))
|
|
exit_ = content.count(_encode_event_id(FT_EVENT_EXIT))
|
|
assert enter > 0 and exit_ > 0, f'no function enter/exit events in {trace_log} (enter={enter}, exit={exit_})'
|
|
|
|
|
|
def _validate_function_trace(trace_log: str, idf_path: str, elf_file: str, binary_path: str) -> None:
|
|
"""Decode function-trace events with sysviewtrace_proc.py and check instrumented
|
|
functions are traced while excluded sources are not. Fall back to counting
|
|
enter/exit packets when the target toolchain is not available."""
|
|
toolchain = _toolchain_prefix(binary_path)
|
|
if shutil.which(f'{toolchain}addr2line') is None:
|
|
print(f'addr2line not found for {toolchain}, using manual validation')
|
|
_validate_function_trace_manual(trace_log)
|
|
return
|
|
|
|
proc_script = os.path.join(idf_path, 'tools', 'esp_app_trace', 'sysviewtrace_proc.py')
|
|
result = subprocess.run(
|
|
[sys.executable, proc_script, '-b', elf_file, '-t', toolchain, '-i', 'func', f'file://{trace_log}'],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
report = result.stdout + result.stderr
|
|
assert result.returncode == 0, f'sysviewtrace_proc.py failed:\n{report}'
|
|
|
|
print(f'{report}')
|
|
|
|
m = re.search(r'Processed (\d+) function trace events\.', report)
|
|
assert m and int(m.group(1)) > 0, f'no function trace events decoded:\n{report}'
|
|
assert INSTRUMENTED_SOURCE in report, f'instrumented functions ({INSTRUMENTED_SOURCE}) not traced:\n{report}'
|
|
for excluded in EXCLUDED_SOURCES:
|
|
assert excluded not in report, f'excluded source {excluded} was traced:\n{report}'
|
|
|
|
|
|
def _validate_trace_data(trace_log: str, target: str, dual_core: bool) -> None:
|
|
"""Validate the multi-core capture contains SystemView trace data for each core."""
|
|
with open(trace_log, 'rb') as f:
|
|
content = f.read()
|
|
for idx in range(2 if dual_core else 1):
|
|
search_str = f'N=FreeRTOS Application,D={target},C=core{idx},O=FreeRTOS'.encode()
|
|
assert search_str in content, f'SysView core{idx} trace data not found in {trace_log}'
|
|
|
|
|
|
def _test_function_tracing_jtag(openocd_dut: 'OpenOCD', idf_path: str, dut: IdfDut) -> None:
|
|
# Single multi-core capture file (esp sysview_mcore).
|
|
trace_log = os.path.join(dut.logdir, 'function_tracing.svdat')
|
|
dual_core = not dut.app.sdkconfig.get('ESP_SYSTEM_SINGLE_CORE_MODE') or dut.target == 'esp32s3'
|
|
|
|
# Prepare gdbinit file pointing at this run's capture file
|
|
gdb_logfile = os.path.join(dut.logdir, 'gdb.txt')
|
|
gdbinit_orig = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'gdbinit')
|
|
gdbinit = os.path.join(dut.logdir, 'gdbinit')
|
|
with open(gdbinit_orig) as f_r, open(gdbinit, 'w') as f_w:
|
|
for line in f_r:
|
|
if line.startswith('mon esp sysview_mcore start'):
|
|
f_w.write(f'mon esp sysview_mcore start file://{trace_log}\n')
|
|
else:
|
|
f_w.write(line)
|
|
|
|
time.sleep(1) # Wait for the USJ port to be ready
|
|
dut.expect_exact('function-tracing: Hello from function_tracing example!', timeout=5)
|
|
with (
|
|
openocd_dut.run() as openocd,
|
|
open(gdb_logfile, 'w') as gdb_log,
|
|
pexpect.spawn(
|
|
f'idf.py -B {dut.app.binary_path} gdb --batch -x {gdbinit}',
|
|
timeout=60,
|
|
logfile=gdb_log,
|
|
encoding='utf-8',
|
|
codec_errors='ignore',
|
|
) as p,
|
|
):
|
|
p.expect_exact('hit Breakpoint 1, app_main ()')
|
|
# dut has been restarted by gdb since the last dut.expect()
|
|
dut.expect(re.compile(rb'function-tracing: workload iteration \d+'), timeout=30)
|
|
|
|
# Let function-trace samples accumulate while recording.
|
|
time.sleep(1)
|
|
openocd.write('esp sysview_mcore stop')
|
|
openocd.apptrace_wait_stop()
|
|
|
|
_validate_trace_data(trace_log, dut.target, dual_core)
|
|
_validate_function_trace(trace_log, idf_path, dut.app.elf_file, dut.app.binary_path)
|
|
|
|
|
|
@pytest.mark.jtag
|
|
@idf_parametrize('config', ['jtag'], indirect=['config'])
|
|
@idf_parametrize('target', ['esp32', 'esp32c2', 'esp32s2'], indirect=['target'])
|
|
def test_function_tracing_jtag(openocd_dut: 'OpenOCD', idf_path: str, dut: IdfDut) -> None:
|
|
_test_function_tracing_jtag(openocd_dut, idf_path, dut)
|
|
|
|
|
|
@pytest.mark.usb_serial_jtag
|
|
@idf_parametrize('config', ['jtag'], indirect=['config'])
|
|
@idf_parametrize(
|
|
'target',
|
|
soc_filtered_targets('SOC_USB_SERIAL_JTAG_SUPPORTED == 1'),
|
|
indirect=['target'],
|
|
)
|
|
@idf_parametrize('port', ['/dev/serial_ports/ttyUSB-esp32'], indirect=['port'])
|
|
def test_function_tracing_usj(openocd_dut: 'OpenOCD', idf_path: str, dut: IdfDut) -> None:
|
|
_test_function_tracing_jtag(openocd_dut, idf_path, dut)
|