# 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 # 4200ae60: a011 j 4200ae64 <----------- 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'