Files
esp-idf/examples/system/function_tracing/pytest_function_tracing.py

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)