Files
esp-idf/tools/test_apps/system/gdbstub_runtime/pytest_gdbstub_runtime.py
2025-08-27 14:45:06 +07:00

405 lines
15 KiB
Python

# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import os
import os.path as path
import sys
from collections.abc import Callable
from typing import Any
import pytest
from pytest_embedded_idf.utils import idf_parametrize
sys.path.append(path.expandvars(path.join('$IDF_PATH', 'tools', 'test_apps', 'system', 'panic')))
from test_panic_util import PanicTestDut # noqa: E402
def get_line_number(lookup: str, offset: int = 0) -> int:
src_file = path.join(path.dirname(path.abspath(__file__)), 'main', 'test_app_main.c')
with open(src_file) as f:
for num, line in enumerate(f, 1):
if lookup in line:
return num + offset
return -1
def start_gdb(dut: PanicTestDut) -> None:
dut.expect_exact('waiting start_testing variable to be changed.')
dut.write(b'\x03') # send Ctrl-C
dut.start_gdb_for_gdbstub()
def run_and_break(dut: PanicTestDut, cmd: str) -> dict[Any, Any]:
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
if not dut.find_gdb_response('stopped', 'notify', responses): # have not stopped on breakpoint yet
responses = dut.gdbmi.get_gdb_response(timeout_sec=3)
assert dut.find_gdb_response('stopped', 'notify', responses) is not None
payload = dut.find_gdb_response('stopped', 'notify', responses)['payload']
assert isinstance(payload, dict)
return payload
def dut_set_variable(dut: PanicTestDut, var_name: str, value: int) -> None:
cmd = f'-gdb-set {var_name}={value}'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
def dut_enable_test(dut: PanicTestDut, testcase: str | None = None) -> None:
dut_set_variable(dut, 'start_testing', 1)
# enable specific testcase (otherwise default testcase)
if testcase:
dut_set_variable(dut, f'do_test_{testcase}', 1)
def dut_get_threads(dut: PanicTestDut) -> Any:
cmd = '-thread-info'
responses = dut.gdb_write(cmd)
if not responses[0]['message']:
responses = dut.gdb_write(cmd)
assert responses is not None
return responses[0]['payload']['threads']
@pytest.mark.generic
@idf_parametrize('target', ['esp32p4'], indirect=['target'])
def test_hwloop_jump(dut: PanicTestDut) -> None:
start_gdb(dut)
# enable coprocessors registers testing
dut_enable_test(dut, 'xesppie_loops')
cmd = '-break-insert --source xesppie_loops.S --function test_loop_start'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
# go to the beginning of the loop
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['bkptno'] == '1'
assert payload['frame']['func'] == 'test_xesppie_loops'
assert payload['stopped-threads'] == 'all'
cmd = '-break-delete 1'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
# go through the loop
loop_count = 3
while loop_count:
inst_count = 2
while inst_count:
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'test_xesppie_loops'
assert payload['stopped-threads'] == 'all'
inst_count -= 1
cmd = '-data-list-register-values d 11'
responses = dut.gdb_write(cmd)
response = dut.find_gdb_response('done', 'result', responses)
assert response is not None
payload = response['payload']
assert payload['register-values'][0]['number'] == '11'
assert payload['register-values'][0]['value'] == f'{loop_count}'
loop_count -= 1
# go through the func prologue
remaining_instructions = 3
while remaining_instructions:
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'test_xesppie_loops'
assert payload['stopped-threads'] == 'all'
remaining_instructions -= 1
# Now we stepping back to app_main
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'app_main'
assert payload['stopped-threads'] == 'all'
def check_registers_numbers(dut: PanicTestDut) -> None:
cmd = '-data-list-register-values d'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
registers = responses[0]['payload']['register-values']
assert len(registers) == 83 # 80 registers supported + xesppie misc + pseudo frm, fflags
r_id = 0
for r in registers:
assert int(r['number']) == r_id
if r_id == 4211: # check if value of q0 register is uint128
assert 'uint128' in r['value']
if r_id == 64:
r_id = 68 # fcsr
elif r_id == 68:
r_id = 4211 # q0
else:
r_id += 1
def set_float_registers(dut: PanicTestDut, t_id: int, addition: int) -> None:
cmd = f'-thread-select {t_id}'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
for i in range(32):
cmd = f'-data-write-register-values d {33 + i} {i + addition}'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
# Note that it's a gap between the last floating register number and fcsr register number.
cmd = f'-data-write-register-values d 68 {32 + addition}'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
def set_pie_registers(dut: PanicTestDut, t_id: int, addition: int) -> None:
cmd = f'-thread-select {t_id}'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
def set_gdb_128_bit_register(reg: str, byte: int) -> None:
val64 = f'0x{hex(byte)[2:] * 8}'
value = f'{{{val64}, {val64}}}'
cmd = f'-interpreter-exec console "set ${reg}.v2_int64={value}"'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
for i in range(8):
set_gdb_128_bit_register(f'q{i}', 0x10 + i + addition)
set_gdb_128_bit_register('qacc_l_l', 0x18 + addition)
set_gdb_128_bit_register('qacc_l_h', 0x19 + addition)
set_gdb_128_bit_register('qacc_h_l', 0x1A + addition)
set_gdb_128_bit_register('qacc_h_h', 0x1B + addition)
set_gdb_128_bit_register('ua_state', 0x1C + addition)
xacc_val = ','.join([hex(0x1D + addition)] * 5)
cmd = f'-interpreter-exec console "set $xacc={{{xacc_val}}}"'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
def coproc_registers_test(dut: PanicTestDut, regs_type: str, set_registers: Callable) -> None:
# set start test breakpoint
cmd = f'-break-insert --source coproc_regs.c --function test_{regs_type} --label {regs_type}_start'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
# stop when the second task is stopped
for i in range(2):
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['frame']['func'] == f'test_{regs_type}'
assert payload['stopped-threads'] == 'all'
threads = dut_get_threads(dut)
"""
Set expected values to both testing tasks.
This will test setting register for both:
- Task coproc owner (direct registers write)
- Other tasks (write registers to task's stack)
"""
coproc_tasks = [f'test_{regs_type}_1', f'test_{regs_type}_2']
found_tasks = [False] * len(coproc_tasks)
for t in threads:
for index, test in enumerate(coproc_tasks):
if test in t['details']:
set_registers(dut, t['id'], index + 1)
found_tasks[index] = True
assert all(found_tasks)
dut_set_variable(dut, f'test_{regs_type}_ready', 1)
cmd = '-break-delete'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
cmd = f'-break-insert --source coproc_regs.c --function test_coproc_regs --label {regs_type}_succeed'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['frame']['func'] == 'test_coproc_regs'
assert payload['stopped-threads'] == 'all'
threads = dut_get_threads(dut)
found_tasks = [False] * len(coproc_tasks)
for t in threads:
for index, test in enumerate(coproc_tasks):
if test in t['details']:
found_tasks[index] = True
assert not any(found_tasks)
@pytest.mark.generic
@idf_parametrize('target', ['esp32p4'], indirect=['target'])
def test_coproc_registers(dut: PanicTestDut) -> None:
start_gdb(dut)
# enable coprocessors registers testing
dut_enable_test(dut, 'coproc_regs')
check_registers_numbers(dut)
coproc_registers_test(dut, 'fpu', set_float_registers)
if dut.target == 'esp32p4':
coproc_registers_test(dut, 'pie', set_pie_registers)
@pytest.mark.generic
@idf_parametrize('target', ['supported_targets'], indirect=['target'])
def test_gdbstub_runtime(dut: PanicTestDut) -> None:
start_gdb(dut)
dut_enable_test(dut)
# Test breakpoint
cmd = '-break-insert --source test_app_main.c --function app_main --label label_1'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['bkptno'] == '1'
assert payload['frame']['func'] == 'app_main'
assert payload['frame']['line'] == str(get_line_number('label_1:', 1))
assert payload['stopped-threads'] == 'all'
# Test step command
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'foo'
assert payload['frame']['line'] == str(get_line_number('var_2+=2;'))
assert payload['stopped-threads'] == 'all'
# Test finish command
cmd = '-exec-finish'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'function-finished'
# On riscv we may have situation when returned from a function but stay on exactly the same line
# foo();
# 4200ae5c: f99ff0ef jal ra,4200adf4 <foo>
# 4200ae60: a011 j 4200ae64 <app_main+0x4e> <----------- here after return from foo()
# }
assert payload['frame']['line'] == str(
get_line_number('label_3:', 1) if dut.is_xtensa else get_line_number('foo();', 0)
)
assert payload['frame']['func'] == 'app_main'
assert payload['stopped-threads'] == 'all'
cmd = '-exec-continue'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
assert dut.find_gdb_response('running', 'notify', responses) is not None
# Test next command
cmd = '-exec-next'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['line'] == str(get_line_number('label_3:', 1))
assert payload['frame']['func'] == 'app_main'
assert payload['stopped-threads'] == 'all'
# test delete breakpoint
cmd = '-break-delete 1'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
cmd = '-exec-continue'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('running', 'result', responses) is not None
assert dut.find_gdb_response('running', 'notify', responses) is not None
# test ctrl-c
os.kill(dut.gdbmi.gdb_process.pid, 2)
# responses = dut.gdbmi.send_signal_to_gdb(2) # https://github.com/cs01/pygdbmi/issues/97
# assert dut.find_gdb_response('stopped', 'notify', responses) is not None
# ?? No response? check we stopped
dut.gdb_backtrace()
# test watchpoint
cmd = '-break-watch var_2'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'signal-received'
assert payload['frame']['func'] == 'foo'
assert payload['stopped-threads'] == 'all'
# Uncomment this when implement send reason to gdb: GCC-313
#
# assert payload['reason'] == 'watchpoint-trigger'
# assert int(payload['value']['new']) == int(payload['value']['old']) + 1
# assert payload['frame']['line'] == '14'
cmd = '-break-delete 2'
responses = dut.gdb_write(cmd)
assert dut.find_gdb_response('done', 'result', responses) is not None
# test set variable
dut_set_variable(dut, 'do_panic', 1)
# test panic handling
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'signal-received'
assert payload['signal-name'] == 'SIGSEGV'
assert payload['frame']['func'] == 'app_main'
assert payload['frame']['line'] == str(get_line_number('label_5', 1))
assert payload['stopped-threads'] == 'all'
@pytest.mark.generic
@pytest.mark.temp_skip_ci(targets=['esp32', 'esp32s2', 'esp32s3'], reason='fix IDF-7927')
@idf_parametrize('target', ['esp32', 'esp32s2', 'esp32s3'], indirect=['target'])
def test_gdbstub_runtime_xtensa_stepping_bug(dut: PanicTestDut) -> None:
start_gdb(dut)
dut_enable_test(dut)
# Test breakpoint
cmd = '-break-insert --source test_app_main.c --function app_main --label label_1'
response = dut.find_gdb_response('done', 'result', dut.gdb_write(cmd))
assert response is not None
cmd = '-exec-continue'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'breakpoint-hit'
assert payload['bkptno'] == '1'
assert payload['frame']['func'] == 'app_main'
assert payload['frame']['line'] == str(get_line_number('label_1:', 1))
assert payload['stopped-threads'] == 'all'
# Test step command
cmd = '-exec-step'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['func'] == 'foo'
assert payload['frame']['line'] == str(get_line_number('var_2+=2;'))
assert payload['stopped-threads'] == 'all'
# Test next command
cmd = '-exec-next'
payload = run_and_break(dut, cmd)
assert payload['reason'] == 'end-stepping-range'
assert payload['frame']['line'] == str(get_line_number('var_2--;', 0))
assert payload['frame']['func'] == 'foo'
assert payload['stopped-threads'] == 'all'