diff --git a/examples/system/.build-test-rules.yml b/examples/system/.build-test-rules.yml index a8ff663a16..e76b552a0c 100644 --- a/examples/system/.build-test-rules.yml +++ b/examples/system/.build-test-rules.yml @@ -1,8 +1,15 @@ # Documentation: .gitlab/ci/README.md#manifest-file-to-control-the-buildtest-apps examples/system/app_trace_basic: + disable: + - if: IDF_TARGET == "esp32h21" + temporary: true + reason: not supported yet #TODO: OCD-1081 + - if: IDF_TARGET == "esp32h4" + temporary: true + reason: not supported yet #TODO: OCD-1137 disable_test: - - if: IDF_TARGET in ["esp32p4", "esp32h21"] + - if: IDF_TARGET == "esp32p4" temporary: true reason: lack of runners. @@ -85,10 +92,20 @@ examples/system/freertos/real_time_stats: - freertos examples/system/gcov: + disable: + - if: IDF_TARGET == "esp32h21" + temporary: true + reason: not supported yet #TODO: OCD-1079 + - if: IDF_TARGET == "esp32h4" + temporary: true + reason: not supported yet #TODO: OCD-1138 disable_test: - - if: IDF_TARGET != "esp32" + - if: IDF_TARGET == "esp32p4" temporary: true reason: lack of runners + - if: IDF_TARGET == "esp32s3" + temporary: true + reason: unstable, known data corruption issue #TODO: OCD-1048 examples/system/gdbstub: disable: diff --git a/examples/system/app_trace_basic/README.md b/examples/system/app_trace_basic/README.md index 81adb079a5..151e992d1a 100644 --- a/examples/system/app_trace_basic/README.md +++ b/examples/system/app_trace_basic/README.md @@ -1,5 +1,5 @@ -| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | -| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- | # Application Level Tracing Example (Basic) diff --git a/examples/system/app_trace_basic/pytest_app_trace_basic.py b/examples/system/app_trace_basic/pytest_app_trace_basic.py index c53644f024..004e07632b 100644 --- a/examples/system/app_trace_basic/pytest_app_trace_basic.py +++ b/examples/system/app_trace_basic/pytest_app_trace_basic.py @@ -131,8 +131,8 @@ def _test_examples_app_trace_basic(dut: IdfDut) -> None: with open(openocd.log_file, encoding='utf-8') as oocd_log: # pylint: disable=protected-access cores = 1 if dut.app.sdkconfig.get('ESP_SYSTEM_SINGLE_CORE_MODE') is True else 2 search_strings.append('App trace params: from {} cores,'.format(cores)) - found = False for search_str in search_strings: + found = False oocd_log.seek(0) for line in oocd_log: if search_str in line: diff --git a/examples/system/gcov/CMakeLists.txt b/examples/system/gcov/CMakeLists.txt index cd5ed13608..9f13094314 100644 --- a/examples/system/gcov/CMakeLists.txt +++ b/examples/system/gcov/CMakeLists.txt @@ -2,6 +2,9 @@ # in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) +# keep this string to detect as project file in CI: +#include($ENV{IDF_PATH}/tools/cmake/project.cmake) + file(TO_NATIVE_PATH "$ENV{IDF_PATH}/tools/cmake/project.cmake" _project_path) include(${_project_path}) diff --git a/examples/system/gcov/README.md b/examples/system/gcov/README.md index 8ff0cb4ff6..736c19052f 100644 --- a/examples/system/gcov/README.md +++ b/examples/system/gcov/README.md @@ -1,5 +1,5 @@ -| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | -| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- | -------- | -------- | # Blink Example With Coverage Info (Gcov) diff --git a/examples/system/gcov/main/gcov_example_main.c b/examples/system/gcov/main/gcov_example_main.c index b3a4370750..bd275aea9c 100644 --- a/examples/system/gcov/main/gcov_example_main.c +++ b/examples/system/gcov/main/gcov_example_main.c @@ -12,17 +12,22 @@ #include "driver/gpio.h" #include "esp_app_trace.h" #include "sdkconfig.h" +#include "esp_log.h" /* Can use project configuration menu (idf.py menuconfig) to choose the GPIO to blink, or you can edit the following line and set a number here. */ #define BLINK_GPIO CONFIG_BLINK_GPIO +static const char *TAG = "example"; + void blink_dummy_func(void); void some_dummy_func(void); static void blink_task(void *pvParameter) { + ESP_LOGI(TAG, "Ready for OpenOCD connection"); + // The first two iterations GCOV data are dumped using call to esp_gcov_dump() and OOCD's "esp32 gcov dump" command. // After that they can be dumped using OOCD's "esp32 gcov" command only. int dump_gcov_after = -2; diff --git a/examples/system/gcov/pytest_gcov.py b/examples/system/gcov/pytest_gcov.py index 5ecd772825..59c4763579 100644 --- a/examples/system/gcov/pytest_gcov.py +++ b/examples/system/gcov/pytest_gcov.py @@ -1,29 +1,129 @@ # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 +import json +import logging import os.path +import signal import time +from telnetlib import Telnet +from typing import Any +from typing import Optional +import pexpect import pytest +from pytest_embedded.utils import to_bytes +from pytest_embedded.utils import to_str from pytest_embedded_idf import IdfDut from pytest_embedded_idf.utils import idf_parametrize -from pytest_embedded_jtag import OpenOcd + +MAX_RETRIES = 3 +RETRY_DELAY = 1 +TELNET_PORT = 4444 -@pytest.mark.jtag -@pytest.mark.parametrize( - 'embedded_services, no_gdb', - [ - ('esp,idf,jtag', 'y'), - ], - indirect=True, -) -@idf_parametrize('target', ['esp32'], indirect=['target']) -def test_gcov(dut: IdfDut, openocd: OpenOcd) -> None: +class OpenOCD: + def __init__(self, dut: 'IdfDut'): + self.dut = dut + self.telnet: Optional[Telnet] = None + self.log_file = os.path.join(self.dut.logdir, 'ocd.txt') + self.proc: Optional[pexpect.spawn] = None + + def run(self) -> Optional['OpenOCD']: + desc_path = os.path.join(self.dut.app.binary_path, 'project_description.json') + + try: + with open(desc_path, 'r') as f: + project_desc = json.load(f) + except FileNotFoundError: + logging.error('Project description file not found at %s', desc_path) + return None + + openocd_scripts = os.getenv('OPENOCD_SCRIPTS') + if not openocd_scripts: + logging.error('OPENOCD_SCRIPTS environment variable is not set.') + return None + + debug_args = project_desc.get('debug_arguments_openocd') + if not debug_args: + logging.error("'debug_arguments_openocd' key is missing in project_description.json") + return None + + # For debug purposes, make the value '4' + ocd_env = os.environ.copy() + ocd_env['LIBUSB_DEBUG'] = '1' + + for _ in range(1, MAX_RETRIES + 1): + try: + self.proc = pexpect.spawn( + command='openocd', + args=['-s', openocd_scripts] + debug_args.split(), + timeout=5, + encoding='utf-8', + codec_errors='ignore', + env=ocd_env, + ) + if self.proc and self.proc.isalive(): + self.proc.expect_exact('Info : Listening on port 3333 for gdb connections', timeout=5) + return self + except (pexpect.exceptions.EOF, pexpect.exceptions.TIMEOUT) as e: + logging.error('Error running OpenOCD: %s', str(e)) + if self.proc and self.proc.isalive(): + self.proc.terminate() + time.sleep(RETRY_DELAY) + + logging.error('Failed to run OpenOCD after %d attempts.', MAX_RETRIES) + return None + + def connect_telnet(self) -> None: + for attempt in range(1, MAX_RETRIES + 1): + try: + self.telnet = Telnet('127.0.0.1', TELNET_PORT, 5) + break + except ConnectionRefusedError as e: + logging.error('Error telnet connection: %s in attempt:%d', e, attempt) + time.sleep(1) + else: + raise ConnectionRefusedError + + def write(self, s: str) -> Any: + if self.telnet is None: + logging.error('Telnet connection is not established.') + return '' + resp = self.telnet.read_very_eager() + self.telnet.write(to_bytes(s, '\n')) + resp += self.telnet.read_until(b'>') + return to_str(resp) + + def apptrace_wait_stop(self, timeout: int = 30) -> None: + stopped = False + end_before = time.time() + timeout + while not stopped: + cmd_out = self.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) + + def kill(self) -> None: + # Check if the process is still running + if self.proc and self.proc.isalive(): + self.proc.terminate() + self.proc.kill(signal.SIGKILL) + + +def _test_gcov(dut: IdfDut) -> 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) + dut.expect_exact('example: Ready for OpenOCD connection', timeout=5) + openocd = OpenOCD(dut).run() + assert openocd + 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}'], @@ -31,9 +131,6 @@ def test_gcov(dut: IdfDut, openocd: OpenOcd) -> None: 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) @@ -56,18 +153,41 @@ def test_gcov(dut: IdfDut, openocd: OpenOcd) -> None: 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) + try: + openocd.connect_telnet() + openocd.write('log_output {}'.format(openocd.log_file)) + openocd.write('reset run') + dut.expect_exact('example: Ready for OpenOCD connection', timeout=5) - for i in range(2, 6): - expect_counter_output(i) + expect_counter_output(0) + dut.expect('Ready to dump GCOV data...', timeout=5) - for _ in range(3): - time.sleep(1) - # Test instant run-time dump - dump_coverage('esp gcov') + # 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') + finally: + openocd.kill() + + +@pytest.mark.jtag +@idf_parametrize('target', ['esp32', 'esp32c2', 'esp32s2'], indirect=['target']) +def test_gcov(dut: IdfDut) -> None: + _test_gcov(dut) + + +@pytest.mark.usb_serial_jtag +@idf_parametrize('target', ['esp32c3', 'esp32c5', 'esp32c6', 'esp32c61', 'esp32h2'], indirect=['target']) +def test_gcov_usj(dut: IdfDut) -> None: + _test_gcov(dut) diff --git a/examples/system/gcov/sdkconfig.ci b/examples/system/gcov/sdkconfig.ci index f0b0b5e03d..e69de29bb2 100644 --- a/examples/system/gcov/sdkconfig.ci +++ b/examples/system/gcov/sdkconfig.ci @@ -1 +0,0 @@ -CONFIG_FREERTOS_UNICORE=y