diff --git a/.gitlab/ci/build.yml b/.gitlab/ci/build.yml index 014c00889d..d6d2061346 100644 --- a/.gitlab/ci/build.yml +++ b/.gitlab/ci/build.yml @@ -129,9 +129,52 @@ --parallel-count ${CI_NODE_TOTAL:-1} --parallel-index ${CI_NODE_INDEX:-1} +.build_pytest_no_jtag_template: + extends: .build_pytest_template + script: + # CI specific options start from "--collect-size-info xxx". could ignore when running locally + - run_cmd python tools/ci/ci_build_apps.py $TEST_DIR -v + -t $IDF_TARGET + -m \"not host_test and not jtag\" + --pytest-apps + --collect-size-info size_info.txt + --parallel-count ${CI_NODE_TOTAL:-1} + --parallel-index ${CI_NODE_INDEX:-1} + +.build_pytest_jtag_template: + extends: + - .build_cmake_template + - .before_script_build_jobs + artifacts: + paths: + - "**/build*/size.json" + - "**/build*/build_log.txt" + - "**/build*/*.bin" + # upload to s3 server to save the artifacts size + # - "**/build*/*.map" + - "**/build*/*.elf" # need elf for gdb + - "**/build*/flasher_args.json" + - "**/build*/flash_project_args" + - "**/build*/config/sdkconfig.json" + - "**/build*/bootloader/*.bin" + - "**/build*/partition_table/*.bin" + - list_job_*.json + - size_info.txt + when: always + expire_in: 4 days + script: + # CI specific options start from "--collect-size-info xxx". could ignore when running locally + - run_cmd python tools/ci/ci_build_apps.py $TEST_DIR -v + -t $IDF_TARGET + -m \"not host_test and jtag\" + --pytest-apps + --collect-size-info size_info.txt + --parallel-count ${CI_NODE_TOTAL:-1} + --parallel-index ${CI_NODE_INDEX:-1} + build_pytest_examples_esp32: extends: - - .build_pytest_template + - .build_pytest_no_jtag_template - .rules:build:example_test-esp32 parallel: 4 variables: @@ -140,7 +183,7 @@ build_pytest_examples_esp32: build_pytest_examples_esp32s2: extends: - - .build_pytest_template + - .build_pytest_no_jtag_template - .rules:build:example_test-esp32s2 parallel: 3 variables: @@ -149,7 +192,7 @@ build_pytest_examples_esp32s2: build_pytest_examples_esp32s3: extends: - - .build_pytest_template + - .build_pytest_no_jtag_template - .rules:build:example_test-esp32s3 parallel: 4 variables: @@ -158,7 +201,7 @@ build_pytest_examples_esp32s3: build_pytest_examples_esp32c3: extends: - - .build_pytest_template + - .build_pytest_no_jtag_template - .rules:build:example_test-esp32c3 parallel: 3 variables: @@ -167,7 +210,7 @@ build_pytest_examples_esp32c3: build_pytest_examples_esp32c2: extends: - - .build_pytest_template + - .build_pytest_no_jtag_template - .rules:build:example_test-esp32c2 variables: IDF_TARGET: esp32c2 @@ -175,12 +218,20 @@ build_pytest_examples_esp32c2: build_pytest_examples_esp32h4: extends: - - .build_pytest_template + - .build_pytest_no_jtag_template - .rules:build:example_test-esp32h4 variables: IDF_TARGET: esp32h4 TEST_DIR: examples +build_pytest_examples_jtag: # for all targets + extends: + - .build_pytest_jtag_template + - .rules:build:example_test-esp32 + variables: + IDF_TARGET: all + TEST_DIR: examples + build_pytest_components_esp32h4: extends: - .build_pytest_template diff --git a/.gitlab/ci/target-test.yml b/.gitlab/ci/target-test.yml index 2aa2650e6e..a1921bd906 100644 --- a/.gitlab/ci/target-test.yml +++ b/.gitlab/ci/target-test.yml @@ -38,6 +38,7 @@ --known-failure-cases-file known_failure_cases/known_failure_cases.txt --parallel-count ${CI_NODE_TOTAL:-1} --parallel-index ${CI_NODE_INDEX:-1} + ${PYTEST_EXTRA_FLAGS} .pytest_examples_dir_template: extends: .pytest_template @@ -76,6 +77,17 @@ example_test_pytest_esp32_twai_network: - build_pytest_examples_esp32 tags: [ esp32, twai_network ] +example_test_pytest_esp32_jtag: + extends: + - .pytest_examples_dir_template + - .rules:test:example_test-esp32 + needs: + - build_pytest_examples_jtag + tags: [ esp32, jtag ] + variables: + SETUP_TOOLS: "1" # need gdb openocd + PYTEST_EXTRA_FLAGS: "--log-cli-level DEBUG" + example_test_pytest_esp32s2_generic: extends: - .pytest_examples_dir_template @@ -677,9 +689,10 @@ test_app_test_pytest_esp32_jtag: - .rules:test:custom_test-esp32 needs: - build_pytest_test_apps_esp32 - tags: [ esp32, test_jtag_arm] + tags: [ esp32, jtag] variables: SETUP_TOOLS: "1" # need gdb + PYTEST_EXTRA_FLAGS: "--log-cli-level DEBUG" test_app_test_pytest_esp32s2_generic: extends: @@ -908,15 +921,6 @@ example_test_007: - ESP32 - Example_I2C_CCS811_SENSOR -example_test_009: - extends: .example_test_esp32_template - tags: - - ESP32 - - test_jtag_arm - variables: - SETUP_TOOLS: "1" - PYTHON_VER: 3 - example_test_011: extends: .example_test_esp32_template tags: @@ -992,14 +996,6 @@ example_test_C6_GENERIC: - .test_app_template - .rules:test:custom_test-esp32s3 -test_app_test_001: - extends: .test_app_esp32_template - tags: - - ESP32 - - test_jtag_arm - variables: - SETUP_TOOLS: "1" - test_app_test_eth: extends: .test_app_esp32_template tags: diff --git a/conftest.py b/conftest.py index 5d771ef237..be7d5309a8 100644 --- a/conftest.py +++ b/conftest.py @@ -109,7 +109,7 @@ ENV_MARKERS = { 'MSPI_F8R8': 'runner with Octal Flash and Octal PSRAM', 'MSPI_F4R8': 'runner with Quad Flash and Octal PSRAM', 'MSPI_F4R4': 'runner with Quad Flash and Quad PSRAM', - 'test_jtag_arm': 'runner where the chip is accessible through JTAG as well', + 'jtag': 'runner where the chip is accessible through JTAG as well', 'adc': 'ADC related tests should run on adc runners', 'xtal32k': 'Runner with external 32k crystal connected', 'no32kXtal': 'Runner with no external 32k crystal connected', @@ -203,6 +203,11 @@ def get_target_marker_from_expr(markexpr: str) -> str: ############ # Fixtures # ############ +@pytest.fixture(scope='session') +def idf_path() -> str: + return os.path.dirname(__file__) + + @pytest.fixture(scope='session', autouse=True) def session_tempdir() -> str: _tmpdir = os.path.join( diff --git a/examples/storage/semihost_vfs/pytest_semihost_vfs.py b/examples/storage/semihost_vfs/pytest_semihost_vfs.py new file mode 100644 index 0000000000..ba5e5478d9 --- /dev/null +++ b/examples/storage/semihost_vfs/pytest_semihost_vfs.py @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 + +import os +import shutil +import tempfile +import typing as t +from itertools import zip_longest + +import pytest +from pytest_embedded_idf import IdfDut + +TEMP_DIR = tempfile.mkdtemp() +HOST_FILE_NAME = 'host_file.txt' +HOST_FILE_PATH = os.path.join(os.path.dirname(__file__), 'data', HOST_FILE_NAME) + + +@pytest.fixture(autouse=True) +def prepare() -> t.Generator[None, None, None]: + shutil.copyfile(HOST_FILE_PATH, os.path.join(TEMP_DIR, HOST_FILE_NAME)) + + yield + + shutil.rmtree(TEMP_DIR, ignore_errors=True) + + +@pytest.mark.esp32 +@pytest.mark.jtag +@pytest.mark.parametrize( + 'embedded_services, no_gdb, openocd_cli_args', + [ + pytest.param( + 'esp,idf,jtag', + 'y', + f'-c \'set ESP_SEMIHOST_BASEDIR "{TEMP_DIR}"\' -f board/esp32-wrover-kit-3.3v.cfg', + marks=[pytest.mark.esp32], + ), + ], + indirect=True, +) +def test_semihost_vfs(dut: IdfDut) -> None: + dut.expect_exact('example: Switch to semihosted stdout') + dut.expect_exact('example: Switched back to UART stdout') + dut.expect_exact('example: Wrote 2798 bytes') + dut.expect_exact('====================== HOST DATA START =========================') + + with open(HOST_FILE_PATH) as f: + for line in f: + if line.strip(): + dut.expect_exact(line.strip()) + + dut.expect_exact('====================== HOST DATA END =========================') + dut.expect_exact('example: Read 6121 bytes') + + with open(os.path.join(TEMP_DIR, 'esp32_stdout.txt')) as f: + + def expected_content() -> t.Iterator[str]: + yield 'example: Switched to semihosted stdout' + for i in range(100): + yield 'Semihosted stdout write {}'.format(i) + yield 'example: Switch to UART stdout' + + for actual, expected in zip_longest(f, expected_content(), fillvalue='-'): + if expected not in actual: # "in" used because of the printed ASCII color codes + raise RuntimeError('"{}" != "{}"'.format(expected, actual.strip())) diff --git a/examples/storage/semihost_vfs/semihost_vfs_example_test.py b/examples/storage/semihost_vfs/semihost_vfs_example_test.py deleted file mode 100644 index e0cb41a4d6..0000000000 --- a/examples/storage/semihost_vfs/semihost_vfs_example_test.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import shutil -import tempfile -from io import open - -import ttfw_idf - -try: - from itertools import izip_longest as zip_longest -except ImportError: - # Python 3 - from itertools import zip_longest - - -@ttfw_idf.idf_example_test(env_tag='test_jtag_arm') -def test_examples_semihost_vfs(env, extra_data): - - rel_project_path = os.path.join('examples', 'storage', 'semihost_vfs') - dut = env.get_dut('semihost_vfs', rel_project_path) - idf_path = dut.app.get_sdk_path() - proj_path = os.path.join(idf_path, rel_project_path) - host_file_name = 'host_file.txt' - - try: - temp_dir = tempfile.mkdtemp() - host_file_path = os.path.join(proj_path, 'data', host_file_name) - shutil.copyfile(host_file_path, os.path.join(temp_dir, host_file_name)) - cfg_cmds = ['set ESP_SEMIHOST_BASEDIR "{}"'.format(temp_dir)] - - with ttfw_idf.OCDBackend(os.path.join(proj_path, 'openocd.log'), dut.app.target, cfg_cmds=cfg_cmds): - dut.start_app() - dut.expect_all('example: Switch to semihosted stdout', - 'example: Switched back to UART stdout', - 'example: Wrote 2798 bytes', - '====================== HOST DATA START =========================', - timeout=20) - with open(host_file_path) as f: - file_content = [line.strip() for line in f] - dut.expect_all(*file_content, timeout=20) - dut.expect_all('====================== HOST DATA END =========================', - 'example: Read 6121 bytes', - timeout=5) - - with open(os.path.join(temp_dir, 'esp32_stdout.txt')) as f: - def expected_content(): - yield 'example: Switched to semihosted stdout' - for i in range(100): - yield 'Semihosted stdout write {}'.format(i) - yield 'example: Switch to UART stdout' - - for actual, expected in zip_longest(f, expected_content(), fillvalue='-'): - if expected not in actual: # "in" used because of the printed ASCII color codes - raise RuntimeError('"{}" != "{}"'.format(expected, actual.strip())) - finally: - shutil.rmtree(temp_dir, ignore_errors=True) - - -if __name__ == '__main__': - test_examples_semihost_vfs() diff --git a/examples/system/app_trace_to_host/example_test.py b/examples/system/app_trace_to_host/example_test.py deleted file mode 100644 index 5857ed69df..0000000000 --- a/examples/system/app_trace_to_host/example_test.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import unicode_literals - -import os -import re - -import ttfw_idf - - -@ttfw_idf.idf_example_test(env_tag='test_jtag_arm') -def test_examples_app_trace_to_host(env, extra_data): - rel_project_path = os.path.join('examples', 'system', 'app_trace_to_host') - dut = env.get_dut('app_trace_to_host', rel_project_path) - idf_path = dut.app.get_sdk_path() - proj_path = os.path.join(idf_path, rel_project_path) - oocd_log_path = os.path.join(proj_path, 'openocd.log') - - with ttfw_idf.OCDBackend(oocd_log_path, dut.app.target) as ocd: - dut.start_app() - dut.expect_all('example: Enabling ADC1 on channel 6 / GPIO34.', - 'example: Enabling CW generator on DAC channel 0 / GPIO25', - 'example: Sampling ADC and sending data to the host...', - re.compile(r'example: Collected \d+ samples in 20 ms.'), - 'example: Sampling ADC and sending data to the UART...', - re.compile(r'example: Sample:\d, Value:\d+'), - re.compile(r'example: Collected \d+ samples in 20 ms.'), - timeout=20) - - ocd.apptrace_start('file://adc.log 0 9000 5 0 0') - ocd.apptrace_wait_stop(tmo=30) - - with open(oocd_log_path) as oocd_log: - cores = 1 if dut.app.get_sdkconfig().get('CONFIG_FREERTOS_UNICORE', '').replace('"','') == 'y' else 2 - params_str = 'App trace params: from {} cores'.format(cores) - for line in oocd_log: - if params_str in line: - break - else: - raise RuntimeError('"{}" could not be found in {}'.format(params_str, oocd_log_path)) - - with ttfw_idf.CustomProcess(' '.join([os.path.join(idf_path, 'tools/esp_app_trace/logtrace_proc.py'), - 'adc.log', - os.path.join(dut.app.binary_path, 'app_trace_to_host.elf')]), - logfile='logtrace_proc.log') as logtrace: - logtrace.pexpect_proc.expect_exact('Parse trace file') - logtrace.pexpect_proc.expect_exact('Parsing completed.') - logtrace.pexpect_proc.expect_exact('====================================================================') - logtrace.pexpect_proc.expect(re.compile(r'example: Sample:\d+, Value:\d+')) - logtrace.pexpect_proc.expect_exact('====================================================================') - logtrace.pexpect_proc.expect(re.compile(r'Log records count: \d+')) - - -if __name__ == '__main__': - test_examples_app_trace_to_host() diff --git a/examples/system/app_trace_to_host/pytest_app_trace_to_host.py b/examples/system/app_trace_to_host/pytest_app_trace_to_host.py new file mode 100644 index 0000000000..3536f45939 --- /dev/null +++ b/examples/system/app_trace_to_host/pytest_app_trace_to_host.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import os +import re +import time + +import pexpect +import pytest +from pytest_embedded.log import DuplicateStdoutPopen, MessageQueue +from pytest_embedded_idf import IdfDut +from pytest_embedded_jtag import OpenOcd + + +def apptrace_wait_stop(openocd: OpenOcd, timeout: int = 30) -> None: + stopped = False + end_before = time.time() + timeout + while not stopped: + cmd_out = openocd.write('esp apptrace status') + for line in cmd_out.splitlines(): + if line.startswith('Tracing is STOPPED.'): + stopped = True + break + if not stopped and time.time() > end_before: + raise pexpect.TIMEOUT('Failed to wait for apptrace stop!') + + time.sleep(1) + + +@pytest.mark.esp32 +@pytest.mark.jtag +@pytest.mark.parametrize( + 'embedded_services, no_gdb', + [ + ('esp,idf,jtag', 'y'), + ], + indirect=True, +) +def test_examples_app_trace_to_host(msg_queue: MessageQueue, dut: IdfDut, openocd: OpenOcd, idf_path: str) -> None: + dut.expect_exact('example: Enabling ADC1 on channel 6 / GPIO34.') + dut.expect_exact('example: Enabling CW generator on DAC channel 0 / GPIO25') + dut.expect_exact('example: Sampling ADC and sending data to the host...') + dut.expect(r'example: Collected \d+ samples in 20 ms.') + dut.expect_exact('example: Sampling ADC and sending data to the UART...') + dut.expect(r'example: Sample:\d, Value:\d+') + dut.expect(r'example: Collected \d+ samples in 20 ms.') + + assert 'Targets connected.' in dut.openocd.write('esp apptrace start file://adc.log 0 9000 5 0 0') + apptrace_wait_stop(dut.openocd) + + with open(openocd._logfile) as oocd_log: # pylint: disable=protected-access + cores = 1 if dut.app.sdkconfig.get('CONFIG_FREERTOS_UNICORE') == 'y' else 2 + params_str = 'App trace params: from {} cores'.format(cores) + for line in oocd_log: + if params_str in line: + break + else: + raise RuntimeError( + '"{}" could not be found in {}'.format(params_str, openocd._logfile) # pylint: disable=protected-access + ) + + DuplicateStdoutPopen( + msg_queue, + [ + os.path.join(idf_path, 'tools', 'esp_app_trace', 'logtrace_proc.py'), + 'adc.log', + os.path.join(dut.app.elf_file), + ], + ) + + dut.expect_exact('Parse trace file') + dut.expect_exact('Parsing completed.') + dut.expect_exact('====================================================================') + dut.expect(re.compile(rb'example: Sample:\d+, Value:\d+')) + dut.expect_exact('====================================================================') + dut.expect(re.compile(rb'Log records count: \d+')) diff --git a/examples/system/gcov/example_test.py b/examples/system/gcov/example_test.py deleted file mode 100644 index 4331ba2129..0000000000 --- a/examples/system/gcov/example_test.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import unicode_literals - -import os -import time - -import ttfw_idf -from ttfw_idf import Utility - - -@ttfw_idf.idf_example_test(env_tag='test_jtag_arm') -def test_examples_gcov(env, extra_data): - - rel_project_path = os.path.join('examples', 'system', 'gcov') - dut = env.get_dut('gcov', rel_project_path) - idf_path = dut.app.get_sdk_path() - proj_path = os.path.join(idf_path, rel_project_path) - openocd_cmd_log = os.path.join(proj_path, 'openocd_cmd.log') - - with ttfw_idf.OCDBackend(os.path.join(proj_path, 'openocd.log'), dut.app.target) as oocd: - dut.start_app() - - def expect_counter_output(loop, timeout=10): - dut.expect_all('blink_dummy_func: Counter = {}'.format(loop), - 'some_dummy_func: Counter = {}'.format(loop * 2), - timeout=timeout) - - expect_counter_output(0, timeout=20) - dut.expect('Ready to dump GCOV data...', timeout=5) - - def dump_coverage(cmd): - try: - response = oocd.cmd_exec(cmd) - with open(openocd_cmd_log, 'a') as f: - f.write(response) - - assert all(x in response for x in ['Targets connected.', - 'gcov_example_main.c.gcda', - 'gcov_example_func.c.gcda', - 'some_funcs.c.gcda', - 'Targets disconnected.', - ]) - - except AssertionError: - # Print what is happening with DUT. Limit the size if it is in loop and generating output. - Utility.console_log(dut.read(size=1000)) - raise - - # Test two hard-coded dumps - dump_coverage('esp gcov dump') - dut.expect('GCOV data have been dumped.', timeout=5) - expect_counter_output(1) - dut.expect('Ready to dump GCOV data...', timeout=5) - dump_coverage('esp gcov dump') - dut.expect('GCOV data have been dumped.', timeout=5) - - for i in range(2, 6): - expect_counter_output(i) - - for _ in range(3): - time.sleep(1) - # Test instant run-time dump - dump_coverage('esp gcov') - - -if __name__ == '__main__': - test_examples_gcov() diff --git a/examples/system/gcov/pytest_gcov.py b/examples/system/gcov/pytest_gcov.py new file mode 100644 index 0000000000..634b20ac95 --- /dev/null +++ b/examples/system/gcov/pytest_gcov.py @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import os.path +import time + +import pytest +from pytest_embedded_idf import IdfDut +from pytest_embedded_jtag import OpenOcd + + +@pytest.mark.esp32 +@pytest.mark.jtag +@pytest.mark.parametrize( + 'embedded_services, no_gdb', + [ + ('esp,idf,jtag', 'y'), + ], + indirect=True, +) +def test_gcov(dut: IdfDut, openocd: OpenOcd) -> None: + # create the generated .gcda folder, otherwise would have error: failed to open file. + # normally this folder would be created via `idf.py build`. but in CI the non-related files would not be preserved + os.makedirs(os.path.join(dut.app.binary_path, 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir'), exist_ok=True) + os.makedirs(os.path.join(dut.app.binary_path, 'esp-idf', 'sample', 'CMakeFiles', '__idf_sample.dir'), exist_ok=True) + + def expect_counter_output(loop: int, timeout: int = 10) -> None: + dut.expect_exact( + [f'blink_dummy_func: Counter = {loop}', f'some_dummy_func: Counter = {loop * 2}'], + expect_all=True, + timeout=timeout, + ) + + expect_counter_output(0) + dut.expect('Ready to dump GCOV data...', timeout=5) + + def dump_coverage(cmd: str) -> None: + response = openocd.write(cmd) + + expect_lines = [ + 'Targets connected.', + 'gcov_example_main.c.gcda', + 'gcov_example_func.c.gcda', + 'some_funcs.c.gcda', + 'Targets disconnected.', + ] + + for line in response.splitlines(): + for expect in expect_lines[:]: + if expect in line: + if expect.endswith('.gcda'): # check file exists + file_path = line.split()[-1].strip("'") + assert os.path.isfile(file_path) + + expect_lines.remove(expect) + + assert len(expect_lines) == 0 + + # Test two hard-coded dumps + dump_coverage('esp gcov dump') + dut.expect('GCOV data have been dumped.', timeout=5) + expect_counter_output(1) + dut.expect('Ready to dump GCOV data...', timeout=5) + dump_coverage('esp gcov dump') + dut.expect('GCOV data have been dumped.', timeout=5) + + for i in range(2, 6): + expect_counter_output(i) + + for _ in range(3): + time.sleep(1) + # Test instant run-time dump + dump_coverage('esp gcov') diff --git a/examples/system/sysview_tracing/example_test.py b/examples/system/sysview_tracing/example_test.py deleted file mode 100644 index 0e1cea85a7..0000000000 --- a/examples/system/sysview_tracing/example_test.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import unicode_literals - -import os -import re -import tempfile -import time -from io import open - -import debug_backend -import ttfw_idf - - -@ttfw_idf.idf_example_test(env_tag='test_jtag_arm') -def test_examples_sysview_tracing(env, extra_data): - - rel_project_path = os.path.join('examples', 'system', 'sysview_tracing') - dut = env.get_dut('sysview_tracing', rel_project_path) - proj_path = os.path.join(dut.app.idf_path, rel_project_path) - elf_path = os.path.join(dut.app.binary_path, 'sysview_tracing.elf') - - def get_temp_file(): - with tempfile.NamedTemporaryFile(delete=False) as f: - return f.name - - try: - tempfiles = [get_temp_file(), get_temp_file()] - - with open(os.path.join(proj_path, 'gdbinit')) as f_in, open(tempfiles[0], 'w') as f_out: - new_content = f_in.read() - # localhost connection issue occurs in docker unless: - new_content = new_content.replace(':3333', '127.0.0.1:3333', 1) - new_content = new_content.replace('file:///tmp/sysview_example.svdat', 'file://{}'.format(tempfiles[1]), 1) - f_out.write(new_content) - - with ttfw_idf.OCDBackend(os.path.join(proj_path, 'openocd.log'), dut.app.target) as oocd: - dut.start_app() - - def dut_expect_task_event(): - dut.expect(re.compile(r'example: Task\[0x3[0-9A-Fa-f]+\]: received event \d+'), timeout=30) - - dut_expect_task_event() - - gdb_log = os.path.join(proj_path, 'gdb.log') - gdb_workdir = os.path.join(proj_path, 'main') - with ttfw_idf.GDBBackend(gdb_log, elf_path, dut.app.target, tempfiles[0], gdb_workdir) as p: - p.gdb.wait_target_state(debug_backend.TARGET_STATE_RUNNING) - stop_reason = p.gdb.wait_target_state(debug_backend.TARGET_STATE_STOPPED) - assert stop_reason == debug_backend.TARGET_STOP_REASON_BP, 'STOP reason: {}'.format(stop_reason) - - dut.expect('example: Created task') # dut has been restarted by gdb since the last dut.expect() - dut_expect_task_event() - - # Do a sleep while sysview samples are captured. - time.sleep(3) - # GDBMI isn't responding now to any commands, therefore, the following command is issued to openocd - oocd.cmd_exec('esp sysview stop') - finally: - for x in tempfiles: - try: - os.unlink(x) - except Exception: - pass - - -if __name__ == '__main__': - test_examples_sysview_tracing() diff --git a/examples/system/sysview_tracing/pytest_sysview_tracing.py b/examples/system/sysview_tracing/pytest_sysview_tracing.py new file mode 100644 index 0000000000..92e561b992 --- /dev/null +++ b/examples/system/sysview_tracing/pytest_sysview_tracing.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import re +import time + +import pexpect.fdpexpect +import pytest +from pytest_embedded_idf import IdfDut + + +@pytest.mark.esp32 +@pytest.mark.jtag +@pytest.mark.parametrize( + 'embedded_services', + [ + 'esp,idf,jtag', + ], + indirect=True, +) +def test_examples_sysview_tracing(dut: IdfDut) -> None: + def dut_expect_task_event() -> None: + dut.expect(re.compile(rb'example: Task\[0x3[0-9A-Fa-f]+\]: received event \d+'), timeout=30) + + dut.gdb.write('mon reset halt') + dut.gdb.write('flushregs') + dut.gdb.write('b app_main') + + dut.gdb.write('commands', non_blocking=True) + dut.gdb.write( + 'mon esp sysview start file:///tmp/sysview_example0.svdat file:///tmp/sysview_example1.svdat', non_blocking=True + ) + dut.gdb.write('c', non_blocking=True) + dut.gdb.write('end') + + dut.gdb.write('c', non_blocking=True) + time.sleep(1) # to avoid EOF file error + with open(dut.gdb._logfile) as fr: # pylint: disable=protected-access + gdb_pexpect_proc = pexpect.fdpexpect.fdspawn(fr.fileno()) + gdb_pexpect_proc.expect('Thread 2 "main" hit Breakpoint 1, app_main ()') + + dut.expect('example: Created task') # dut has been restarted by gdb since the last dut.expect() + dut_expect_task_event() + + # Do a sleep while sysview samples are captured. + time.sleep(3) + # GDB isn't responding now to any commands, therefore, the following command is issued to openocd + dut.openocd.write('esp sysview stop') diff --git a/examples/system/sysview_tracing_heap_log/example_test.py b/examples/system/sysview_tracing_heap_log/example_test.py deleted file mode 100644 index cea4e334ec..0000000000 --- a/examples/system/sysview_tracing_heap_log/example_test.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import unicode_literals - -import os -import re -import tempfile -from io import open - -import debug_backend -import ttfw_idf - - -@ttfw_idf.idf_example_test(env_tag='test_jtag_arm') -def test_examples_sysview_tracing_heap_log(env, extra_data): - - rel_project_path = os.path.join('examples', 'system', 'sysview_tracing_heap_log') - dut = env.get_dut('sysview_tracing_heap_log', rel_project_path) - proj_path = os.path.join(dut.app.idf_path, rel_project_path) - elf_path = os.path.join(dut.app.binary_path, 'sysview_tracing_heap_log.elf') - - def get_temp_file(): - with tempfile.NamedTemporaryFile(delete=False) as f: - return f.name - - try: - tempfiles = [get_temp_file(), get_temp_file()] - - with open(os.path.join(proj_path, 'gdbinit')) as f_in, open(tempfiles[0], 'w') as f_out: - new_content = f_in.read() - # localhost connection issue occurs in docker unless: - new_content = new_content.replace(':3333', '127.0.0.1:3333', 1) - new_content = new_content.replace('file:///tmp/heap_log.svdat', 'file://{}'.format(tempfiles[1]), 1) - f_out.write(new_content) - - with ttfw_idf.OCDBackend(os.path.join(proj_path, 'openocd.log'), dut.app.target): - dut.start_app() - dut.expect('esp_apptrace: Initialized TRAX on CPU0') - - gdb_log = os.path.join(proj_path, 'gdb.log') - gdb_workdir = os.path.join(proj_path, 'main') - with ttfw_idf.GDBBackend(gdb_log, elf_path, dut.app.target, tempfiles[0], gdb_workdir) as p: - for _ in range(2): # There are two breakpoints - p.gdb.wait_target_state(debug_backend.TARGET_STATE_RUNNING) - stop_reason = p.gdb.wait_target_state(debug_backend.TARGET_STATE_STOPPED) - assert stop_reason == debug_backend.TARGET_STOP_REASON_BP, 'STOP reason: {}'.format(stop_reason) - - # dut has been restarted by gdb since the last dut.expect() - dut.expect('esp_apptrace: Initialized TRAX on CPU0') - - with ttfw_idf.CustomProcess(' '.join([os.path.join(dut.app.idf_path, 'tools/esp_app_trace/sysviewtrace_proc.py'), - '-p', - '-b', elf_path, - tempfiles[1]]), - logfile='sysviewtrace_proc.log') as sysviewtrace: - sysviewtrace.pexpect_proc.expect(re.compile(r'Found \d+ leaked bytes in \d+ blocks.'), timeout=120) - finally: - for x in tempfiles: - try: - os.unlink(x) - except Exception: - pass - - -if __name__ == '__main__': - test_examples_sysview_tracing_heap_log() diff --git a/examples/system/sysview_tracing_heap_log/pytest_sysview_tracing_heap_log.py b/examples/system/sysview_tracing_heap_log/pytest_sysview_tracing_heap_log.py new file mode 100644 index 0000000000..0e9eff9f45 --- /dev/null +++ b/examples/system/sysview_tracing_heap_log/pytest_sysview_tracing_heap_log.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import os.path +import time + +import pexpect.fdpexpect +import pytest +from pytest_embedded_idf import IdfDut + +TEMP_FILE = os.path.join(os.path.dirname(__file__), 'heap_log.svdat') + + +@pytest.mark.esp32 +@pytest.mark.jtag +@pytest.mark.parametrize('embedded_services', [ + 'esp,idf,jtag', +], indirect=True) +def test_examples_sysview_tracing_heap_log(idf_path: str, dut: IdfDut) -> None: + dut.gdb.write('mon reset halt') + dut.gdb.write('flushregs') + + dut.gdb.write('tb heap_trace_start') + dut.gdb.write('commands', non_blocking=True) + dut.gdb.write(f'mon esp sysview start file://{TEMP_FILE}', non_blocking=True) + dut.gdb.write('c', non_blocking=True) + dut.gdb.write('end') + + dut.gdb.write('tb heap_trace_stop') + dut.gdb.write('commands', non_blocking=True) + dut.gdb.write('mon esp sysview stop', non_blocking=True) + dut.gdb.write('end') + + dut.gdb.write('c', non_blocking=True) + dut.expect('esp_apptrace: Initialized TRAX on CPU0') + + time.sleep(1) # make sure that the sysview file has been generated + with pexpect.spawn(' '.join([os.path.join(idf_path, 'tools', 'esp_app_trace', 'sysviewtrace_proc.py'), + '-p', + '-b', dut.app.elf_file, + TEMP_FILE])) as sysviewtrace: + sysviewtrace.expect(r'Found \d+ leaked bytes in \d+ blocks.', timeout=120) + + with open(dut.gdb._logfile) as fr: # pylint: disable=protected-access + gdb_pexpect_proc = pexpect.fdpexpect.fdspawn(fr.fileno()) + gdb_pexpect_proc.expect_exact( + 'Thread 2 "main" hit Temporary breakpoint 1, heap_trace_start (mode_param=HEAP_TRACE_ALL)', timeout=10) + gdb_pexpect_proc.expect_exact('Thread 2 "main" hit Temporary breakpoint 2, heap_trace_stop ()', timeout=10) diff --git a/tools/requirements/requirements.pytest.txt b/tools/requirements/requirements.pytest.txt index a13ce3edfe..fdd66462dc 100644 --- a/tools/requirements/requirements.pytest.txt +++ b/tools/requirements/requirements.pytest.txt @@ -3,6 +3,7 @@ pytest-embedded-serial-esp pytest-embedded-idf +pytest-embedded-jtag pytest-embedded-qemu pytest-rerunfailures pytest-timeout diff --git a/tools/test_apps/system/gdb/pytest_gdb.py b/tools/test_apps/system/gdb/pytest_gdb.py index 4bad6de523..73242b1320 100644 --- a/tools/test_apps/system/gdb/pytest_gdb.py +++ b/tools/test_apps/system/gdb/pytest_gdb.py @@ -1,14 +1,14 @@ # SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 +import logging import os import re -import signal import subprocess import sys import pexpect import pytest -from pytest_embedded import Dut +from pytest_embedded_idf import IdfDut try: from idf_py_actions.debug_ext import get_openocd_arguments @@ -18,36 +18,38 @@ except ModuleNotFoundError: @pytest.mark.supported_targets -@pytest.mark.test_jtag_arm -def test_idf_gdb(dut: Dut) -> None: +@pytest.mark.jtag +def test_idf_gdb(dut: IdfDut) -> None: # Need to wait a moment to connect via OpenOCD after the hard reset happened. # Along with this check that app runs ok dut.expect('Hello world!') - # Don't need to have output from UART any more + # Don't need to have output from UART anymore dut.serial.stop_redirect_thread() with open(os.path.join(dut.logdir, 'ocd.txt'), 'w') as ocd_log: - ocd = subprocess.Popen(f'openocd {get_openocd_arguments(dut.target)}', stdout=ocd_log, stderr=ocd_log, shell=True) + cmd = ['openocd', *get_openocd_arguments(dut.target).split()] + openocd_scripts = os.getenv('OPENOCD_SCRIPTS') + if openocd_scripts: + cmd.extend(['-s', openocd_scripts]) + + logging.info('Running %s', cmd) + ocd = subprocess.Popen(cmd, stdout=ocd_log, stderr=ocd_log) try: gdb_env = os.environ.copy() gdb_env['ESP_IDF_GDB_TESTING'] = '1' with open(os.path.join(dut.logdir, 'gdb.txt'), 'w') as gdb_log, \ - pexpect.spawn(f'idf.py -B {dut.app.binary_path} gdb --batch', - env=gdb_env, - timeout=60, - logfile=gdb_log, - encoding='utf-8', - codec_errors='ignore') as p: + pexpect.spawn(f'idf.py -B {dut.app.binary_path} gdb --batch', + env=gdb_env, + timeout=60, + logfile=gdb_log, + encoding='utf-8', + codec_errors='ignore') as p: p.expect(re.compile(r'add symbol table from file.*bootloader.elf')) p.expect(re.compile(r'add symbol table from file.*rom.elf')) p.expect_exact('hit Temporary breakpoint 1, app_main ()') finally: - try: - ocd.send_signal(signal.SIGINT) - ocd.communicate(timeout=15) - except subprocess.TimeoutExpired: - ocd.kill() - ocd.communicate() + ocd.terminate() + ocd.kill() diff --git a/tools/test_apps/system/gdb_loadable_elf/app_test.py b/tools/test_apps/system/gdb_loadable_elf/app_test.py deleted file mode 100644 index f740980220..0000000000 --- a/tools/test_apps/system/gdb_loadable_elf/app_test.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import unicode_literals - -import os -import threading -import time - -import debug_backend -import pexpect -import serial -import ttfw_idf -from tiny_test_fw import Utility - - -class SerialThread(object): - def run(self, log_path, exit_event): - with serial.Serial(os.getenv('ESPPORT', '/dev/ttyUSB1'), 115200) as ser, open(log_path, 'wb') as f: - while True: - f.write(ser.read(ser.in_waiting)) - if exit_event.is_set(): - break - time.sleep(1) - - def __init__(self, log_path): - self.exit_event = threading.Event() - self.t = threading.Thread(target=self.run, args=(log_path, self.exit_event,)) - self.t.start() - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.exit_event.set() - self.t.join(60) - if self.t.is_alive(): - Utility.console_log('The pyserial thread is still alive', 'O') - - -@ttfw_idf.idf_custom_test(env_tag='test_jtag_arm', group='test-apps') -def test_app_loadable_elf(env, extra_data): - rel_project_path = os.path.join('tools', 'test_apps', 'system', 'gdb_loadable_elf') - app_files = ['gdb_loadable_elf.elf'] - target = 'esp32' - app = ttfw_idf.LoadableElfTestApp(rel_project_path, app_files, target=target) - idf_path = app.get_sdk_path() - proj_path = os.path.join(idf_path, rel_project_path) - elf_path = os.path.join(app.binary_path, 'gdb_loadable_elf.elf') - esp_log_path = os.path.join(proj_path, 'esp.txt') - - with SerialThread(esp_log_path): - openocd_log = os.path.join(proj_path, 'openocd.txt') - gdb_log = os.path.join(proj_path, 'gdb.txt') - gdb_init = os.path.join(proj_path, 'gdbinit_' + target) - gdb_dir = os.path.join(proj_path, 'main') - - with ttfw_idf.OCDBackend(openocd_log, app.target): - with ttfw_idf.GDBBackend(gdb_log, elf_path, app.target, gdb_init, gdb_dir) as p: - def wait_for_breakpoint(): - p.gdb.wait_target_state(debug_backend.TARGET_STATE_RUNNING) - stop_reason = p.gdb.wait_target_state(debug_backend.TARGET_STATE_STOPPED) - assert stop_reason == debug_backend.TARGET_STOP_REASON_BP, 'STOP reason: {}'.format(stop_reason) - - wait_for_breakpoint() - - p.gdb.add_bp('esp_restart') - p.gdb.exec_continue() - - wait_for_breakpoint() - - if pexpect.run('grep "Restarting now." {}'.format(esp_log_path), withexitstatus=True)[1]: - raise RuntimeError('Expected output from ESP was not received') - - -if __name__ == '__main__': - test_app_loadable_elf() diff --git a/tools/test_apps/system/gdb_loadable_elf/pytest_gdb_loadable_elf.py b/tools/test_apps/system/gdb_loadable_elf/pytest_gdb_loadable_elf.py new file mode 100644 index 0000000000..18ef8b800a --- /dev/null +++ b/tools/test_apps/system/gdb_loadable_elf/pytest_gdb_loadable_elf.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 + +import pytest +from pytest_embedded_idf import IdfDut + + +@pytest.mark.parametrize('offset', [ + pytest.param('0x40007d54', marks=[pytest.mark.esp32]), + # pytest.param('0x4000f6e2', marks=[pytest.mark.esp32s2]), + # pytest.param('0x40047654', marks=[pytest.mark.esp32c3]), +]) +@pytest.mark.parametrize('embedded_services, skip_autoflash, erase_all', [ + ('esp,idf,jtag', 'y', 'y'), +], indirect=True) +@pytest.mark.jtag +def test_loadable_elf(dut: IdfDut, offset: str) -> None: + dut.gdb.write('mon reset halt') + dut.gdb.write(f'thb *{offset}') + assert 'Temporary breakpoint 1, 0x40007d54' in dut.gdb.write('c') + dut.gdb.write('load') + dut.gdb.write('tb app_main') + assert 'Thread 2 "main" hit Temporary breakpoint 2, app_main ()' in dut.gdb.write('c') + dut.gdb.write('tb esp_restart') + assert 'Thread 2 "main" hit Temporary breakpoint 3, esp_restart ()' in dut.gdb.write('c') + dut.expect_exact('Restarting now.')