Skip to content

Commit 31a089c

Browse files
committed
Merge branch 'ci/enable_gcov_test' into 'master'
ci: enable gcov example for all chips See merge request espressif/esp-idf!39114
2 parents a8128ad + bee2ed2 commit 31a089c

File tree

7 files changed

+177
-35
lines changed

7 files changed

+177
-35
lines changed

examples/system/.build-test-rules.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22

33
examples/system/app_trace_basic:
44
disable_test:
5-
- if: IDF_TARGET in ["esp32p4", "esp32h21"]
5+
- if: IDF_TARGET == "esp32p4"
66
temporary: true
77
reason: lack of runners.
8+
- if: IDF_TARGET == "esp32h21"
9+
temporary: true
10+
reason: not supported yet #TODO: OCD-1081
11+
- if: IDF_TARGET == "esp32h4"
12+
temporary: true
13+
reason: not supported yet #TODO: OCD-1137
814

915
examples/system/base_mac_address:
1016
depends_components:
@@ -86,9 +92,18 @@ examples/system/freertos/real_time_stats:
8692

8793
examples/system/gcov:
8894
disable_test:
89-
- if: IDF_TARGET != "esp32"
95+
- if: IDF_TARGET == "esp32p4"
9096
temporary: true
9197
reason: lack of runners
98+
- if: IDF_TARGET == "esp32s3"
99+
temporary: true
100+
reason: unstable, known data corruption issue #TODO: OCD-1048
101+
- if: IDF_TARGET == "esp32h21"
102+
temporary: true
103+
reason: not supported yet #TODO: OCD-1079
104+
- if: IDF_TARGET == "esp32h4"
105+
temporary: true
106+
reason: not supported yet #TODO: OCD-1138
92107

93108
examples/system/gdbstub:
94109
disable:

examples/system/app_trace_basic/pytest_app_trace_basic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ def _test_examples_app_trace_basic(dut: IdfDut) -> None:
131131
with open(openocd.log_file, encoding='utf-8') as oocd_log: # pylint: disable=protected-access
132132
cores = 1 if dut.app.sdkconfig.get('ESP_SYSTEM_SINGLE_CORE_MODE') is True else 2
133133
search_strings.append('App trace params: from {} cores,'.format(cores))
134-
found = False
135134
for search_str in search_strings:
135+
found = False
136136
oocd_log.seek(0)
137137
for line in oocd_log:
138138
if search_str in line:

examples/system/gcov/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
# in this exact order for cmake to work correctly
33
cmake_minimum_required(VERSION 3.16)
44

5+
# keep this string to detect as project file in CI:
6+
#include($ENV{IDF_PATH}/tools/cmake/project.cmake)
7+
58
file(TO_NATIVE_PATH "$ENV{IDF_PATH}/tools/cmake/project.cmake" _project_path)
69

710
include(${_project_path})

examples/system/gcov/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 |
2-
| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
1+
| 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 |
2+
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- |
33

44
# Blink Example With Coverage Info (Gcov)
55

examples/system/gcov/main/gcov_example_main.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,22 @@
1212
#include "driver/gpio.h"
1313
#include "esp_app_trace.h"
1414
#include "sdkconfig.h"
15+
#include "esp_log.h"
1516

1617
/* Can use project configuration menu (idf.py menuconfig) to choose the GPIO
1718
to blink, or you can edit the following line and set a number here.
1819
*/
1920
#define BLINK_GPIO CONFIG_BLINK_GPIO
2021

22+
static const char *TAG = "example";
23+
2124
void blink_dummy_func(void);
2225
void some_dummy_func(void);
2326

2427
static void blink_task(void *pvParameter)
2528
{
29+
ESP_LOGI(TAG, "Ready for OpenOCD connection");
30+
2631
// The first two iterations GCOV data are dumped using call to esp_gcov_dump() and OOCD's "esp32 gcov dump" command.
2732
// After that they can be dumped using OOCD's "esp32 gcov" command only.
2833
int dump_gcov_after = -2;
Lines changed: 149 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,136 @@
11
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
22
# SPDX-License-Identifier: Unlicense OR CC0-1.0
3+
import json
4+
import logging
35
import os.path
6+
import signal
47
import time
8+
from telnetlib import Telnet
9+
from typing import Any
10+
from typing import Optional
511

12+
import pexpect
613
import pytest
14+
from pytest_embedded.utils import to_bytes
15+
from pytest_embedded.utils import to_str
716
from pytest_embedded_idf import IdfDut
817
from pytest_embedded_idf.utils import idf_parametrize
9-
from pytest_embedded_jtag import OpenOcd
1018

19+
MAX_RETRIES = 3
20+
RETRY_DELAY = 1
21+
TELNET_PORT = 4444
1122

12-
@pytest.mark.jtag
13-
@pytest.mark.parametrize(
14-
'embedded_services, no_gdb',
15-
[
16-
('esp,idf,jtag', 'y'),
17-
],
18-
indirect=True,
19-
)
20-
@idf_parametrize('target', ['esp32'], indirect=['target'])
21-
def test_gcov(dut: IdfDut, openocd: OpenOcd) -> None:
23+
24+
class OpenOCD:
25+
def __init__(self, dut: 'IdfDut'):
26+
self.dut = dut
27+
self.telnet: Optional[Telnet] = None
28+
self.log_file = os.path.join(self.dut.logdir, 'ocd.txt')
29+
self.proc: Optional[pexpect.spawn] = None
30+
31+
def run(self) -> Optional['OpenOCD']:
32+
desc_path = os.path.join(self.dut.app.binary_path, 'project_description.json')
33+
34+
try:
35+
with open(desc_path, 'r') as f:
36+
project_desc = json.load(f)
37+
except FileNotFoundError:
38+
logging.error('Project description file not found at %s', desc_path)
39+
return None
40+
41+
openocd_scripts = os.getenv('OPENOCD_SCRIPTS')
42+
if not openocd_scripts:
43+
logging.error('OPENOCD_SCRIPTS environment variable is not set.')
44+
return None
45+
46+
debug_args = project_desc.get('debug_arguments_openocd')
47+
if not debug_args:
48+
logging.error("'debug_arguments_openocd' key is missing in project_description.json")
49+
return None
50+
51+
# For debug purposes, make the value '4'
52+
ocd_env = os.environ.copy()
53+
ocd_env['LIBUSB_DEBUG'] = '1'
54+
55+
for _ in range(1, MAX_RETRIES + 1):
56+
try:
57+
self.proc = pexpect.spawn(
58+
command='openocd',
59+
args=['-s', openocd_scripts] + debug_args.split(),
60+
timeout=5,
61+
encoding='utf-8',
62+
codec_errors='ignore',
63+
env=ocd_env,
64+
)
65+
if self.proc and self.proc.isalive():
66+
self.proc.expect_exact('Info : Listening on port 3333 for gdb connections', timeout=5)
67+
return self
68+
except (pexpect.exceptions.EOF, pexpect.exceptions.TIMEOUT) as e:
69+
logging.error('Error running OpenOCD: %s', str(e))
70+
if self.proc and self.proc.isalive():
71+
self.proc.terminate()
72+
time.sleep(RETRY_DELAY)
73+
74+
logging.error('Failed to run OpenOCD after %d attempts.', MAX_RETRIES)
75+
return None
76+
77+
def connect_telnet(self) -> None:
78+
for attempt in range(1, MAX_RETRIES + 1):
79+
try:
80+
self.telnet = Telnet('127.0.0.1', TELNET_PORT, 5)
81+
break
82+
except ConnectionRefusedError as e:
83+
logging.error('Error telnet connection: %s in attempt:%d', e, attempt)
84+
time.sleep(1)
85+
else:
86+
raise ConnectionRefusedError
87+
88+
def write(self, s: str) -> Any:
89+
if self.telnet is None:
90+
logging.error('Telnet connection is not established.')
91+
return ''
92+
resp = self.telnet.read_very_eager()
93+
self.telnet.write(to_bytes(s, '\n'))
94+
resp += self.telnet.read_until(b'>')
95+
return to_str(resp)
96+
97+
def apptrace_wait_stop(self, timeout: int = 30) -> None:
98+
stopped = False
99+
end_before = time.time() + timeout
100+
while not stopped:
101+
cmd_out = self.write('esp apptrace status')
102+
for line in cmd_out.splitlines():
103+
if line.startswith('Tracing is STOPPED.'):
104+
stopped = True
105+
break
106+
if not stopped and time.time() > end_before:
107+
raise pexpect.TIMEOUT('Failed to wait for apptrace stop!')
108+
time.sleep(1)
109+
110+
def kill(self) -> None:
111+
# Check if the process is still running
112+
if self.proc and self.proc.isalive():
113+
self.proc.terminate()
114+
self.proc.kill(signal.SIGKILL)
115+
116+
117+
def _test_gcov(dut: IdfDut) -> None:
22118
# create the generated .gcda folder, otherwise would have error: failed to open file.
23119
# normally this folder would be created via `idf.py build`. but in CI the non-related files would not be preserved
24120
os.makedirs(os.path.join(dut.app.binary_path, 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir'), exist_ok=True)
25121
os.makedirs(os.path.join(dut.app.binary_path, 'esp-idf', 'sample', 'CMakeFiles', '__idf_sample.dir'), exist_ok=True)
26122

123+
dut.expect_exact('example: Ready for OpenOCD connection', timeout=5)
124+
openocd = OpenOCD(dut).run()
125+
assert openocd
126+
27127
def expect_counter_output(loop: int, timeout: int = 10) -> None:
28128
dut.expect_exact(
29129
[f'blink_dummy_func: Counter = {loop}', f'some_dummy_func: Counter = {loop * 2}'],
30130
expect_all=True,
31131
timeout=timeout,
32132
)
33133

34-
expect_counter_output(0)
35-
dut.expect('Ready to dump GCOV data...', timeout=5)
36-
37134
def dump_coverage(cmd: str) -> None:
38135
response = openocd.write(cmd)
39136

@@ -56,18 +153,41 @@ def dump_coverage(cmd: str) -> None:
56153

57154
assert len(expect_lines) == 0
58155

59-
# Test two hard-coded dumps
60-
dump_coverage('esp gcov dump')
61-
dut.expect('GCOV data have been dumped.', timeout=5)
62-
expect_counter_output(1)
63-
dut.expect('Ready to dump GCOV data...', timeout=5)
64-
dump_coverage('esp gcov dump')
65-
dut.expect('GCOV data have been dumped.', timeout=5)
66-
67-
for i in range(2, 6):
68-
expect_counter_output(i)
69-
70-
for _ in range(3):
71-
time.sleep(1)
72-
# Test instant run-time dump
73-
dump_coverage('esp gcov')
156+
try:
157+
openocd.connect_telnet()
158+
openocd.write('log_output {}'.format(openocd.log_file))
159+
openocd.write('reset run')
160+
dut.expect_exact('example: Ready for OpenOCD connection', timeout=5)
161+
162+
expect_counter_output(0)
163+
dut.expect('Ready to dump GCOV data...', timeout=5)
164+
165+
# Test two hard-coded dumps
166+
dump_coverage('esp gcov dump')
167+
dut.expect('GCOV data have been dumped.', timeout=5)
168+
expect_counter_output(1)
169+
dut.expect('Ready to dump GCOV data...', timeout=5)
170+
dump_coverage('esp gcov dump')
171+
dut.expect('GCOV data have been dumped.', timeout=5)
172+
173+
for i in range(2, 6):
174+
expect_counter_output(i)
175+
176+
for _ in range(3):
177+
time.sleep(1)
178+
# Test instant run-time dump
179+
dump_coverage('esp gcov')
180+
finally:
181+
openocd.kill()
182+
183+
184+
@pytest.mark.jtag
185+
@idf_parametrize('target', ['esp32', 'esp32c2', 'esp32s2'], indirect=['target'])
186+
def test_gcov(dut: IdfDut) -> None:
187+
_test_gcov(dut)
188+
189+
190+
@pytest.mark.usb_serial_jtag
191+
@idf_parametrize('target', ['esp32c3', 'esp32c5', 'esp32c6', 'esp32c61', 'esp32h2'], indirect=['target'])
192+
def test_gcov_usj(dut: IdfDut) -> None:
193+
_test_gcov(dut)

examples/system/gcov/sdkconfig.ci

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
CONFIG_FREERTOS_UNICORE=y

0 commit comments

Comments
 (0)