Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion .devcontainer/cpp/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,60 @@
FROM debian:11.11 AS qemu-builder

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y -q && apt-get install -y -q --no-install-recommends \
build-essential \
git \
libgcrypt-dev \
libglib2.0-dev \
libpixman-1-dev \
libslirp-dev \
ninja-build \
python3-pip \
wget \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*

RUN pip3 install meson==1.7.0 tomli==2.2.1

RUN git clone --depth 1 --branch apptrace \
https://github.com/ltowarek/espressif-qemu.git /tmp/espressif-qemu \
&& cd /tmp/espressif-qemu \
&& ./configure --target-list=xtensa-softmmu \
--prefix=/opt/espressif-qemu \
--without-default-features \
--enable-gcrypt \
--enable-pixman \
--enable-slirp \
&& make -j$(nproc) \
&& make install \
&& rm -rf /tmp/espressif-qemu


FROM espressif/idf:release-v6.0

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \
ccache \
curl \
libgcrypt20 \
libglib2.0-0 \
libpixman-1-0 \
libslirp0 \
locales \
protobuf-compiler

COPY --from=qemu-builder /opt/espressif-qemu/bin/qemu-system-xtensa \
/opt/espressif-qemu/bin/qemu-system-xtensa
COPY --from=qemu-builder /opt/espressif-qemu/share/qemu/ \
/opt/espressif-qemu/share/qemu/

ENV QEMU_PROG_PATH=/opt/espressif-qemu/bin/qemu-system-xtensa

# esp-clang provides an Xtensa-aware clang/clang-tidy/clang-format; vanilla apt
# clang-tidy has no Xtensa backend, and a separate apt clang-format would drift
# in version from this one (car/scripts use esp-clang's clang-format/clang-tidy
# everywhere via export.sh, not whatever's on the system PATH).
RUN python3 $IDF_PATH/tools/idf_tools.py --idf-path $IDF_PATH install esp-clang
RUN python3 $IDF_PATH/tools/idf_tools.py --quiet --idf-path $IDF_PATH install esp-clang

# WA for VSCode issue https://github.com/microsoft/vscode/issues/189924
RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && locale-gen
Expand Down
19 changes: 5 additions & 14 deletions .github/workflows/cpp-car.yml
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,10 @@ jobs:
include:
- name: telemetry
component_path: components/telemetry/test_apps
pytest_pattern: pytest_telemetry_qemu_coverage.py
pytest_pattern: pytest_telemetry_qemu.py
- name: web_server
component_path: components/web_server/test_apps
pytest_pattern: pytest_web_server_qemu_coverage.py
pytest_pattern: pytest_web_server_qemu.py

container:
image: ghcr.io/ltowarek/dust-mite-cpp-devcontainer
Expand Down Expand Up @@ -283,17 +283,10 @@ jobs:
ccache-${{ runner.os }}-idf-coverage-${{ matrix.name }}-
ccache-${{ runner.os }}-idf-coverage-

- name: Build
- name: Run coverage
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
SDKCONFIG_DEFAULTS: sdkconfig.defaults;sdkconfig.defaults.qemu;sdkconfig.defaults.coverage
run: ./scripts/run_build.sh ${{ matrix.component_path }}

- name: Run QEMU coverage tests
run: ./scripts/run_qemu_tests.sh ${{ matrix.component_path }} ${{ matrix.pytest_pattern }}

- name: Generate coverage report
run: ./scripts/run_coverage_report.sh ${{ matrix.component_path }}
run: ./scripts/run_coverage.sh ${{ matrix.component_path }} ${{ matrix.pytest_pattern }}

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
Expand All @@ -303,10 +296,8 @@ jobs:
flags: cpp
fail_ci_if_error: false



ci-status-cpp-car:
needs: [build-docker, build, clang-format, clang-tidy, ubsan]
needs: [build-docker, build, clang-format, clang-tidy, ubsan, coverage]
if: always()
runs-on: ubuntu-latest
steps:
Expand Down
69 changes: 49 additions & 20 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,43 +486,72 @@ app's `main/Kconfig.projbuild`, a matching `sdkconfig.defaults.coverage` overlay
`y`, and guarding the coverage flags in the test app's `CMakeLists.txt`:

```cmake
idf_component_register(SRCS ${srcs} ... PRIV_REQUIRES ... gcov_uart_vfs ...)
if(CONFIG_<COMPONENT>_TEST_COVERAGE)
idf_component_get_property(<component>_lib <component> COMPONENT_LIB)
target_compile_options(${<component>_lib} PRIVATE "--coverage")
target_link_options(${CMAKE_PROJECT_NAME}.elf PRIVATE "--coverage")
endif()
```

`gcov_uart_vfs` is listed unconditionally — ESP-IDF's component resolution ordering does not
reliably support Kconfig-gated `PRIV_REQUIRES` entries. The component is small and its entry point
is never called in non-coverage builds (guarded by `#ifdef` in `main.cpp`).
The test app's `main/idf_component.yml` declares `espressif/esp_gcov` as a dependency so the
component manager fetches it automatically:

The `gcov_uart_vfs` component (in `car/components/gcov_uart_vfs/`) handles everything shared: the
VFS registration, the base64-over-UART encoding, the `GCOV_PREFIX_STRIP_COUNT` computation (applied
at CMake time so paths are correct regardless of checkout location), and the gcov flush sequence.
```yaml
dependencies:
idf: ">=6.0.0"
espressif/esp_gcov: "^1"
```

The test app's `main.cpp` drives the dump after `UNITY_END()` with a single call:
The test app's `main.cpp` calls `esp_gcov_dump()` after `UNITY_END()`. This streams `.gcda` data to
the host via the AppTrace JTAG protocol, which the patched esp-qemu emulates:

```cpp
#ifdef CONFIG_<COMPONENT>_TEST_COVERAGE
gcov_uart_vfs_dump();
extern "C" {
#include "esp_gcov.h"
}
#endif
...
#ifdef CONFIG_<COMPONENT>_TEST_COVERAGE
esp_gcov_dump();
printf("GCOV_DUMP_DONE\n");
#endif
```

`gcov_uart_vfs_dump()` registers the VFS at `/gcov`, sets `GCOV_PREFIX`/`GCOV_PREFIX_STRIP`, calls
`__gcov_dump()`, and prints `GCOV_DUMP_DONE`. The VFS intercepts gcov's file I/O and streams each
`.gcda` file as base64 over UART, framed with `GCOV_FILE_START:`, `GCOV_B64:`, and `GCOV_FILE_END`
sentinels. The matching `pytest_<component>_qemu_coverage.py` script decodes the UART stream and
reconstructs `.gcda` files inside `build/`. This approach avoids semihosting (not available without
an OpenOCD connection) and does not require a separate QEMU invocation.
The `sdkconfig.defaults.coverage` overlay enables AppTrace transport and the `esp_gcov` driver in
addition to the coverage Kconfig flag:

```
CONFIG_<COMPONENT>_TEST_COVERAGE=y
CONFIG_ESP_TRACE_TRANSPORT_APPTRACE=y
CONFIG_APPTRACE_DEST_JTAG=y
CONFIG_ESP_GCOV_ENABLE=y
```

QEMU is launched with `-apptrace file_io`, which causes the patched esp-qemu to emulate the
AppTrace JTAG host: it sets `HOST_CONNECT` so the firmware unblocks, processes each file I/O
command block (open/write/close), and writes `.gcda` files directly to the build host filesystem.
No OpenOCD connection or separate decode step is needed.

`run_coverage.sh` sets `COVERAGE_BUILD=1` when invoking pytest. The existing QEMU pytest files
check this variable and wait for the `GCOV_DUMP_DONE` sentinel printed by `app_main` after
`esp_gcov_dump()` returns, ensuring QEMU is not torn down before AppTrace file I/O completes.

`run_coverage.sh` requires the patched QEMU binary via the `QEMU_PROG_PATH` environment variable.
The devcontainer image builds the patched esp-qemu (`ltowarek/espressif-qemu@apptrace`) and sets
`QEMU_PROG_PATH` automatically, so no manual export is needed inside the devcontainer.

Scoping coverage flags to just the component under test mirrors the UBSan pattern and keeps binary
size manageable. A dedicated `coverage` CI job builds each opted-in test app with
`sdkconfig.defaults;sdkconfig.defaults.qemu;sdkconfig.defaults.coverage`, runs the coverage pytest,
and uploads coverage data to Codecov. The `coverage` CI job is non-blocking (not part of
`ci-status-cpp-car`).
`sdkconfig.defaults;sdkconfig.defaults.qemu;sdkconfig.defaults.coverage`, runs the standard QEMU
pytest with `COVERAGE_BUILD=1` and `-apptrace file_io`, and uploads coverage data to Codecov. The
`coverage` CI job is blocking (part of `ci-status-cpp-car`).

To run locally (from the `car/` directory in the C++ devcontainer):

```bash
./scripts/run_coverage.sh components/telemetry/test_apps pytest_telemetry_qemu_coverage.py
./scripts/run_coverage.sh components/web_server/test_apps pytest_web_server_qemu_coverage.py
./scripts/run_coverage.sh components/telemetry/test_apps pytest_telemetry_qemu.py
./scripts/run_coverage.sh components/web_server/test_apps pytest_web_server_qemu.py
```

### Car test types
Expand Down
13 changes: 0 additions & 13 deletions car/components/gcov_uart_vfs/CMakeLists.txt

This file was deleted.

105 changes: 0 additions & 105 deletions car/components/gcov_uart_vfs/gcov_uart_vfs.c

This file was deleted.

12 changes: 0 additions & 12 deletions car/components/gcov_uart_vfs/gcov_uart_vfs.h

This file was deleted.

4 changes: 1 addition & 3 deletions car/components/telemetry/test_apps/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,5 @@ if(CONFIG_TELEMETRY_TEST_COVERAGE)

# --coverage on the link step lets g++ pick the right libgcov multilib
# variant; -lgcov alone does not resolve the correct esp32s3 multilib path.
# --undefined=__gcov_dump forces the linker to pull the symbol from libgcov;
# without it the weak reference in gcov_uart_vfs.c stays NULL.
target_link_options(${CMAKE_PROJECT_NAME}.elf PRIVATE "--coverage" "-Wl,--undefined=__gcov_dump")
target_link_options(${CMAKE_PROJECT_NAME}.elf PRIVATE "--coverage")
endif()
13 changes: 12 additions & 1 deletion car/components/telemetry/test_apps/dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ dependencies:
registry_url: https://components.espressif.com/
type: service
version: 2.1.6
espressif/esp_gcov:
component_hash: f9fc66382e8bce275bd5bd0ce54e8b169e98b50ee31146dec54aa39615aea65b
dependencies:
- name: idf
require: private
version: '>=6.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.0.5
espressif/esp_jpeg:
component_hash: defb83669293cbf86d0fa86b475ba5517aceed04ed70db435388c151ab37b5d7
dependencies:
Expand All @@ -40,7 +50,8 @@ dependencies:
direct_dependencies:
- espressif/cjson
- espressif/esp32-camera
- espressif/esp_gcov
- idf
manifest_hash: 911421cdce94da1ed2bc6de5b3e378be9793a680401c558085611d3a71cad054
manifest_hash: 828ba29bedec26137b74cfd5a5ea6a0de9a45facb65cd30a01f5b001625f1c4f
target: esp32s3
version: 3.0.0
2 changes: 1 addition & 1 deletion car/components/telemetry/test_apps/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ endif()

idf_component_register(SRCS ${srcs}
INCLUDE_DIRS "."
PRIV_REQUIRES unity telemetry gcov_uart_vfs
PRIV_REQUIRES unity telemetry
WHOLE_ARCHIVE)
11 changes: 5 additions & 6 deletions car/components/telemetry/test_apps/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ config TELEMETRY_TEST_COVERAGE
default n
help
Compiles the telemetry component under test with --coverage. After
the test run, app_main registers a VFS at /gcov that streams gcda
file content as base64 over UART, calls __gcov_dump() to flush
coverage data, then prints GCOV_DUMP_DONE. The coverage pytest
script decodes the UART stream and reconstructs .gcda files on the
host. Opt-in: only enabled by the dedicated coverage CI
job/sdkconfig overlay.
the test run, app_main calls esp_gcov_dump() which streams .gcda
data to the host via the AppTrace JTAG protocol, then prints
GCOV_DUMP_DONE. The patched esp-qemu emulates the AppTrace host
and writes .gcda files directly to the build directory. Opt-in:
only enabled by the dedicated coverage CI job/sdkconfig overlay.

endmenu
3 changes: 3 additions & 0 deletions car/components/telemetry/test_apps/main/idf_component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies:
idf: ">=6.0.0"
espressif/esp_gcov: "^1"
Loading
Loading