mirror of
https://github.com/espressif/esp-idf.git
synced 2026-08-18 06:35:35 +03:00
feat(examples): dump coverage over uart
This commit is contained in:
@@ -24,6 +24,20 @@ static const char *TAG = "example";
|
||||
void blink_dummy_func(void);
|
||||
void some_dummy_func(void);
|
||||
|
||||
#if !CONFIG_APPTRACE_DEST_JTAG
|
||||
#include "soc/uart_pins.h"
|
||||
#include "esp_app_trace.h"
|
||||
/* Override default uart config to use console pins as a uart channel */
|
||||
esp_apptrace_config_t esp_apptrace_get_user_params(void)
|
||||
{
|
||||
esp_apptrace_config_t config = APPTRACE_UART_CONFIG_DEFAULT();
|
||||
config.dest_cfg.uart.uart_num = 0;
|
||||
config.dest_cfg.uart.tx_pin_num = U0TXD_GPIO_NUM;
|
||||
config.dest_cfg.uart.rx_pin_num = U0RXD_GPIO_NUM;
|
||||
return config;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void blink_task(void *pvParameter)
|
||||
{
|
||||
ESP_LOGI(TAG, "Ready for OpenOCD connection");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
|
||||
@@ -11,13 +12,58 @@ from pytest_embedded_idf.utils import idf_parametrize
|
||||
if typing.TYPE_CHECKING:
|
||||
from conftest import OpenOCD
|
||||
|
||||
try:
|
||||
from gcov_capture import UartGcovCapture
|
||||
from gcov_capture import get_coverage_data
|
||||
except ImportError:
|
||||
idf_path = os.getenv('IDF_PATH')
|
||||
if not idf_path:
|
||||
raise RuntimeError('IDF_PATH not found. Please run `source $IDF_PATH/export.sh`')
|
||||
esp_app_trace_dir = os.path.join(idf_path, 'tools', 'esp_app_trace')
|
||||
sys.path.insert(0, esp_app_trace_dir)
|
||||
from gcov_capture import UartGcovCapture
|
||||
from gcov_capture import get_coverage_data
|
||||
|
||||
def _test_gcov(openocd_dut: 'OpenOCD', 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
|
||||
|
||||
def get_expected_gcda_paths(dut: IdfDut) -> list:
|
||||
"""Get list of expected .gcda file paths for this example."""
|
||||
return [
|
||||
os.path.join(
|
||||
dut.app.binary_path, 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir', 'gcov_example_main.c.gcda'
|
||||
),
|
||||
os.path.join(
|
||||
dut.app.binary_path, 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir', 'gcov_example_func.c.gcda'
|
||||
),
|
||||
os.path.join(dut.app.binary_path, 'esp-idf', 'sample', 'CMakeFiles', '__idf_sample.dir', 'some_funcs.c.gcda'),
|
||||
]
|
||||
|
||||
|
||||
def prepare_test(dut: IdfDut) -> list:
|
||||
"""Prepare test environment: create directories and clean up old .gcda files.
|
||||
|
||||
Returns list of expected .gcda file paths.
|
||||
"""
|
||||
# Create the generated .gcda folders
|
||||
# Normally created via `idf.py build`, but in CI non-related files aren't 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)
|
||||
|
||||
# Get expected paths and clean up old files
|
||||
expected_gcda_paths = get_expected_gcda_paths(dut)
|
||||
for gcda_path in expected_gcda_paths:
|
||||
if os.path.isfile(gcda_path):
|
||||
try:
|
||||
os.remove(gcda_path)
|
||||
print(f'Removed old .gcda file: {os.path.basename(gcda_path)}')
|
||||
except OSError as e:
|
||||
print(f'Warning: Could not remove {gcda_path}: {e}')
|
||||
|
||||
return expected_gcda_paths
|
||||
|
||||
|
||||
def _test_gcov(openocd_dut: 'OpenOCD', dut: IdfDut) -> None:
|
||||
expected_gcda_paths = prepare_test(dut)
|
||||
|
||||
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}'],
|
||||
@@ -25,8 +71,8 @@ def _test_gcov(openocd_dut: 'OpenOCD', dut: IdfDut) -> None:
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
def dump_coverage(cmd: str) -> None:
|
||||
response = openocd.write(cmd)
|
||||
def dump_coverage(on_the_fly: bool, expected_counts: dict | None = None) -> None:
|
||||
response = openocd.gcov_dump(on_the_fly=on_the_fly)
|
||||
|
||||
expect_lines = [
|
||||
'Targets connected.',
|
||||
@@ -47,6 +93,21 @@ def _test_gcov(openocd_dut: 'OpenOCD', dut: IdfDut) -> None:
|
||||
|
||||
assert len(expect_lines) == 0
|
||||
|
||||
# Verify execution counts if expected and coverage data is available
|
||||
if expected_counts:
|
||||
coverage = get_coverage_data(dut.app.binary_path, expected_gcda_paths)
|
||||
if coverage: # Only verify if detailed data is available
|
||||
print(f'Coverage data: {coverage}')
|
||||
for func, expected_count in expected_counts.items():
|
||||
actual_count = coverage.get(func)
|
||||
assert actual_count == expected_count, f'Expected {func}={expected_count}, got {actual_count}'
|
||||
else:
|
||||
# Backup verification: ensure .gcda files exist
|
||||
print('Coverage data not available (gcov tool not found)')
|
||||
for gcda_path in expected_gcda_paths:
|
||||
assert os.path.isfile(gcda_path), f'Expected .gcda file not found: {gcda_path}'
|
||||
print('Basic verification passed (all .gcda files exist)')
|
||||
|
||||
dut.expect_exact('example: Ready for OpenOCD connection', timeout=5)
|
||||
with openocd_dut.run() as openocd:
|
||||
openocd.write('reset run')
|
||||
@@ -55,32 +116,86 @@ def _test_gcov(openocd_dut: 'OpenOCD', dut: IdfDut) -> None:
|
||||
expect_counter_output(0)
|
||||
dut.expect('Ready to dump GCOV data...', timeout=5)
|
||||
|
||||
# Test two hard-coded dumps
|
||||
dump_coverage('esp gcov dump')
|
||||
# Test two hard-coded dumps with verification
|
||||
dump_coverage(False, {'gcov_example_func.c:blink_dummy_func': 1, 'some_funcs.c:some_dummy_func': 1})
|
||||
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')
|
||||
|
||||
dump_coverage(False, {'gcov_example_func.c:blink_dummy_func': 2, 'some_funcs.c:some_dummy_func': 2})
|
||||
dut.expect('GCOV data have been dumped.', timeout=5)
|
||||
|
||||
for i in range(2, 6):
|
||||
expect_counter_output(i)
|
||||
|
||||
for _ in range(3):
|
||||
# Test instant run-time dumps with verification
|
||||
expected_runtime_counts = [
|
||||
{'gcov_example_func.c:blink_dummy_func': 7, 'some_funcs.c:some_dummy_func': 7},
|
||||
{'gcov_example_func.c:blink_dummy_func': 8, 'some_funcs.c:some_dummy_func': 8},
|
||||
{'gcov_example_func.c:blink_dummy_func': 10, 'some_funcs.c:some_dummy_func': 10},
|
||||
]
|
||||
|
||||
for expected in expected_runtime_counts:
|
||||
time.sleep(1)
|
||||
# Test instant run-time dump
|
||||
dump_coverage('esp gcov')
|
||||
dump_coverage(True, expected)
|
||||
|
||||
|
||||
@pytest.mark.jtag
|
||||
@idf_parametrize('config', ['gcov_jtag'], indirect=['config'])
|
||||
@idf_parametrize('target', ['esp32', 'esp32c2', 'esp32s2'], indirect=['target'])
|
||||
def test_gcov(openocd_dut: 'OpenOCD', dut: IdfDut) -> None:
|
||||
_test_gcov(openocd_dut, dut)
|
||||
|
||||
|
||||
@pytest.mark.usb_serial_jtag
|
||||
@idf_parametrize('config', ['gcov_jtag'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target', ['esp32s3', 'esp32c3', 'esp32c5', 'esp32c6', 'esp32c61', 'esp32h2', 'esp32p4'], indirect=['target']
|
||||
)
|
||||
def test_gcov_usj(openocd_dut: 'OpenOCD', dut: IdfDut) -> None:
|
||||
_test_gcov(openocd_dut, dut)
|
||||
|
||||
|
||||
def _test_gcov_uart(dut: IdfDut) -> None:
|
||||
# Close console port to free up the port for the gcov capture
|
||||
dut.serial.close()
|
||||
|
||||
expected_gcda_paths = prepare_test(dut)
|
||||
log_file = os.path.join(dut.logdir, 'gcov_uart.log')
|
||||
uart_port = dut.serial.port
|
||||
baud = dut.app.sdkconfig.get('APPTRACE_UART_BAUDRATE')
|
||||
with UartGcovCapture(port=uart_port, baudrate=baud, log_file=log_file, log_level=1) as uart_capture:
|
||||
uart_capture.run(background=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Expected execution counts
|
||||
expected_counts = [
|
||||
{'gcov_example_func.c:blink_dummy_func': 1, 'some_funcs.c:some_dummy_func': 1}, # First dump
|
||||
{'gcov_example_func.c:blink_dummy_func': 2, 'some_funcs.c:some_dummy_func': 2}, # Second dump
|
||||
]
|
||||
|
||||
# Verify each dump
|
||||
for dump_num, expected in enumerate(expected_counts, start=1):
|
||||
if uart_capture.wait_for_fstop(timeout=10.0):
|
||||
coverage = get_coverage_data(dut.app.binary_path, expected_gcda_paths)
|
||||
|
||||
if coverage: # Only verify details if coverage data is available
|
||||
print(f'Coverage data: {coverage}')
|
||||
for func, expected_count in expected.items():
|
||||
actual_count = coverage.get(func)
|
||||
assert actual_count == expected_count, f'Expected {func}={expected_count}, got {actual_count}'
|
||||
else:
|
||||
# Backup verification: ensure .gcda files exist
|
||||
print('Coverage data not available (gcov tool not found)')
|
||||
for gcda_path in expected_gcda_paths:
|
||||
assert os.path.isfile(gcda_path), f'Expected .gcda file not found: {gcda_path}'
|
||||
print('Basic verification passed (all .gcda files exist)')
|
||||
else:
|
||||
assert False, f'esp_gcov_dump {dump_num} timeout'
|
||||
|
||||
|
||||
@pytest.mark.generic
|
||||
@idf_parametrize('config', ['gcov_uart'], indirect=['config'])
|
||||
@idf_parametrize('target', ['supported_targets'], indirect=['target'])
|
||||
def test_gcov_uart(dut: IdfDut) -> None:
|
||||
_test_gcov_uart(dut)
|
||||
|
||||
4
examples/system/gcov/sdkconfig.ci.gcov_jtag
Normal file
4
examples/system/gcov/sdkconfig.ci.gcov_jtag
Normal file
@@ -0,0 +1,4 @@
|
||||
CONFIG_APPTRACE_DEST_JTAG=y
|
||||
CONFIG_APPTRACE_LOCK_ENABLE=y
|
||||
CONFIG_APPTRACE_ONPANIC_HOST_FLUSH_TMO=-1
|
||||
CONFIG_APPTRACE_POSTMORTEM_FLUSH_THRESH=0
|
||||
5
examples/system/gcov/sdkconfig.ci.gcov_uart
Normal file
5
examples/system/gcov/sdkconfig.ci.gcov_uart
Normal file
@@ -0,0 +1,5 @@
|
||||
CONFIG_ESP_CONSOLE_NONE=y
|
||||
CONFIG_APPTRACE_DEST_UART=y
|
||||
CONFIG_APPTRACE_DEST_UART_NUM=0
|
||||
CONFIG_APPTRACE_UART_BAUDRATE=1000000
|
||||
CONFIG_APPTRACE_UART_TX_MSG_SIZE=256
|
||||
@@ -1,9 +1,4 @@
|
||||
CONFIG_ESP_TRACE_ENABLE=y
|
||||
CONFIG_ESP_TRACE_LIB_NONE=y
|
||||
CONFIG_ESP_TRACE_TRANSPORT_APPTRACE=y
|
||||
CONFIG_APPTRACE_DEST_JTAG=y
|
||||
CONFIG_APPTRACE_LOCK_ENABLE=y
|
||||
CONFIG_APPTRACE_ONPANIC_HOST_FLUSH_TMO=-1
|
||||
CONFIG_APPTRACE_POSTMORTEM_FLUSH_THRESH=0
|
||||
|
||||
CONFIG_ESP_GCOV_ENABLE=y
|
||||
|
||||
Reference in New Issue
Block a user