Merge branch 'ci/enable_gcov_test_v5.5' into 'release/v5.5'

ci: enable gcov example for all chips (v5.5)

See merge request espressif/esp-idf!39162
This commit is contained in:
Alexey Gerenkov
2025-05-20 01:58:20 +08:00
8 changed files with 179 additions and 35 deletions

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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})

View File

@ -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)

View File

@ -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;

View File

@ -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)

View File

@ -1 +0,0 @@
CONFIG_FREERTOS_UNICORE=y