test: add esp-idf-configdep integration tests

Add build-level tests verifying configdep selective rebuild behavior:
- test_configdep.py: selective rebuild (only affected .obj rebuilt) and
  ELF string verification after Kconfig toggle
- test_rebuild_configdep.py: full rebuild graph tests with configdep enabled,
  including the two-build "settle" pattern for .cdep stub timestamps

Place the configdep test app in tools/test_build_system/configdep_test_app/
alongside the build system test infrastructure. Split configdep-specific
rebuild coverage from pure CMake/Ninja rebuild tests (test_rebuild.py runs
with configdep disabled via a module-local idf_py fixture).
This commit is contained in:
Jan Beran
2026-06-02 01:26:10 +08:00
committed by BOT
parent 9522af1d8f
commit 3abb4c095b
11 changed files with 312 additions and 34 deletions
@@ -0,0 +1,9 @@
cmake_minimum_required(VERSION 3.22)
# placeholder_before_include_project_cmake
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# placeholder_after_include_project_cmake
project(configdep_test_app)
@@ -0,0 +1,6 @@
| 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 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# Configdep test application
Minimal ESP-IDF project used by ``tools/test_build_system/test_configdep.py`` to exercise ``esp-idf-configdep`` selective rebuilds and to assert that the built firmware reflects Kconfig toggles (via strings embedded in the ELF).
@@ -0,0 +1,2 @@
idf_component_register(SRCS "configdep_main.c" "unrelated.c"
INCLUDE_DIRS "")
@@ -0,0 +1,5 @@
menu "Configdep Test"
config TEST_CONFIGDEP_OPTION
bool "Test configdep option"
default y
endmenu
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
extern void unrelated_function(void);
void app_main(void)
{
#if CONFIG_TEST_CONFIGDEP_OPTION
printf("CONFIGDEP_OPTION: ENABLED\n");
#else
printf("CONFIGDEP_OPTION: DISABLED\n");
#endif
unrelated_function();
vTaskDelay(pdMS_TO_TICKS(1000));
}
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Second TU in ``main``: only uses CONFIG_IDF_TARGET from sdkconfig—no conditional
* on the optional symbol defined for configdep_main.c (see ``Kconfig.projbuild``).
* Used by tests to snapshot this ``.obj`` alongside ``configdep_main``.
*/
#include <stdio.h>
#include "sdkconfig.h"
void unrelated_function(void)
{
printf("IDF target: %s\n", CONFIG_IDF_TARGET);
}
@@ -20,6 +20,8 @@ from .idf_utils import get_idf_build_env
from .idf_utils import run_cmake
from .idf_utils import run_cmake_and_build
from .idf_utils import run_idf_py
from .rebuild_utils import BUILD_SNAPSHOT_EXCLUDE
from .rebuild_utils import rebuild_and_check
from .snapshot import Snapshot
from .snapshot import get_snapshot
@@ -46,4 +48,6 @@ __all__ = [
'file_contains',
'bin_file_contains',
'bin_files_differ',
'BUILD_SNAPSHOT_EXCLUDE',
'rebuild_and_check',
]
@@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""Shared helpers and constants for rebuild tests (test_rebuild*.py)."""
from .idf_utils import IdfPyFunc
from .snapshot import get_snapshot
# Paths excluded from mtime snapshots (volatile or external-project stamps).
BUILD_SNAPSHOT_EXCLUDE = [
'build/log/*',
'build/CMakeFiles/bootloader-complete',
'build/bootloader-prefix/src/bootloader-stamp/bootloader-done',
]
def rebuild_and_check(
idf_py: IdfPyFunc, should_be_rebuilt: str | list[str], should_not_be_rebuilt: str | list[str]
) -> None:
"""Assert that files matching *should_be_rebuilt* change and *should_not_be_rebuilt* stay the same after a build."""
snapshot_should_be_rebuilt = get_snapshot(should_be_rebuilt)
snapshot_should_not_be_rebuilt = get_snapshot(should_not_be_rebuilt)
idf_py('build')
snapshot_should_be_rebuilt.assert_different(get_snapshot(should_be_rebuilt))
snapshot_should_not_be_rebuilt.assert_same(get_snapshot(should_not_be_rebuilt))
+80
View File
@@ -0,0 +1,80 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import logging
import os
from pathlib import Path
import pytest
from test_build_system_helpers import IdfPyFunc
from test_build_system_helpers import append_to_file
from test_build_system_helpers import bin_file_contains
from test_build_system_helpers import get_snapshot
from test_build_system_helpers import replace_in_file
CONFIGDEP_TEST_APP = 'tools/test_build_system/configdep_test_app'
_MAIN_OBJ_DIR = ('build', 'esp-idf', 'main', 'CMakeFiles', '__idf_main.dir')
MAIN_OBJ = os.path.join(*_MAIN_OBJ_DIR, 'configdep_main.c.obj')
UNRELATED_OBJ = os.path.join(*_MAIN_OBJ_DIR, 'unrelated.c.obj')
CONFIGDEP_ELF = Path(os.path.join('build', 'configdep_test_app.elf'))
@pytest.mark.test_app_copy(CONFIGDEP_TEST_APP)
@pytest.mark.usefixtures('test_app_copy')
def test_configdep_selective_rebuild(idf_py: IdfPyFunc) -> None:
"""Verify selective rebuild when a Kconfig option toggles.
``esp-idf-configdep`` scans each translation unit's source for ``CONFIG_*``
tokens and appends matching ``.cdep`` stubs to the compiler depfile. Avoid
spelling projbuild-only symbols (even in comments) in ``unrelated.c``, or the
scanner adds a spurious ``option.cdep`` edge and Ninja rebuilds that ``.obj``.
"""
logging.info('initial build with configdep enabled (default)')
idf_py('build')
logging.info('toggle CONFIG_TEST_CONFIGDEP_OPTION from y to n')
replace_in_file('sdkconfig', 'CONFIG_TEST_CONFIGDEP_OPTION=y', '# CONFIG_TEST_CONFIGDEP_OPTION is not set')
snapshot_main = get_snapshot(MAIN_OBJ)
snapshot_unrelated = get_snapshot(UNRELATED_OBJ)
logging.info('rebuild after config change')
idf_py('build')
logging.info('configdep_main.c.obj must be rebuilt (references changed option)')
snapshot_main.assert_different(get_snapshot(MAIN_OBJ))
logging.info('unrelated.c.obj must not be rebuilt (no CONFIG_* token for that option in source)')
snapshot_unrelated.assert_same(get_snapshot(UNRELATED_OBJ))
@pytest.mark.test_app_copy(CONFIGDEP_TEST_APP)
@pytest.mark.usefixtures('test_app_copy')
def test_configdep_no_rebuild_without_change(idf_py: IdfPyFunc) -> None:
logging.info('initial build with configdep enabled (default)')
idf_py('build')
snapshot_main = get_snapshot(MAIN_OBJ)
snapshot_unrelated = get_snapshot(UNRELATED_OBJ)
logging.info('rebuild with no config changes')
idf_py('build')
logging.info('neither object file should be rebuilt')
snapshot_main.assert_same(get_snapshot(MAIN_OBJ))
snapshot_unrelated.assert_same(get_snapshot(UNRELATED_OBJ))
@pytest.mark.test_app_copy(CONFIGDEP_TEST_APP)
@pytest.mark.usefixtures('test_app_copy')
def test_configdep_elf_strings_track_sdkconfig_toggle(idf_py: IdfPyFunc) -> None:
"""After rebuild, the firmware string matches the final Kconfig (host-side ELF check)."""
idf_py('build')
assert CONFIGDEP_ELF.is_file()
assert bin_file_contains(CONFIGDEP_ELF, b'CONFIGDEP_OPTION: ENABLED')
assert not bin_file_contains(CONFIGDEP_ELF, b'CONFIGDEP_OPTION: DISABLED')
append_to_file('sdkconfig', '\n# CONFIG_TEST_CONFIGDEP_OPTION is not set\n')
idf_py('build')
assert bin_file_contains(CONFIGDEP_ELF, b'CONFIGDEP_OPTION: DISABLED')
assert not bin_file_contains(CONFIGDEP_ELF, b'CONFIGDEP_OPTION: ENABLED')
+35 -34
View File
@@ -1,20 +1,51 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
# These tests check whether the build system rebuilds some files or not
# depending on the changes to the project.
# depending on the changes to the project. Configdep is disabled here so
# mtime assertions reflect CMake/Ninja only; see test_rebuild_configdep.py for
# the same checks with configdep enabled (after settle builds).
import logging
import os
import subprocess
from pathlib import Path
from typing import cast
import pytest
from test_build_system_helpers import ALL_ARTIFACTS_BUILDV1
from test_build_system_helpers import ALL_ARTIFACTS_BUILDV2
from test_build_system_helpers import APP_BINS
from test_build_system_helpers import BOOTLOADER_BINS
from test_build_system_helpers import BUILD_SNAPSHOT_EXCLUDE
from test_build_system_helpers import PARTITION_BIN
from test_build_system_helpers import IdfPyFunc
from test_build_system_helpers import get_idf_build_env
from test_build_system_helpers import get_snapshot
from test_build_system_helpers import rebuild_and_check
from test_build_system_helpers import replace_in_file
from test_build_system_helpers import run_idf_py
@pytest.fixture
def idf_py(monkeypatch: pytest.MonkeyPatch) -> IdfPyFunc:
"""Use ``idf.py`` with esp-idf-configdep disabled for this module.
These tests assert Ninja rebuild granularity via file mtimes. The
``esp-idf-configdep`` wrapper can create per-option ``.cdep`` stubs after the
real compiler has written ``.obj`` files, so on the next Ninja run inputs can
look newer than outputs and trigger spurious rebuilds. That behavior is
covered separately in ``test_rebuild_configdep.py`` with a settle-build
pattern. Here we exercise the core CMake/Ninja dependency graph only.
"""
monkeypatch.setenv('IDF_CONFIGDEP_ENABLE', '0')
env = get_idf_build_env(os.environ['IDF_PATH'])
def result(*args: str, check: bool = True, input_str: str | None = None) -> subprocess.CompletedProcess[str]:
return cast(
subprocess.CompletedProcess[str],
run_idf_py(*args, env=env, workdir=os.getcwd(), check=check, input_str=input_str),
)
return result
@pytest.mark.usefixtures('test_app_copy')
@@ -23,14 +54,7 @@ def test_rebuild_no_changes(idf_py: IdfPyFunc, request: pytest.FixtureRequest) -
idf_py('build')
logging.info('get the first snapshot')
# excluding the 'log' subdirectory here since it changes after every build
all_build_files = get_snapshot(
'build/**/*',
exclude_patterns=[
'build/log/*',
'build/CMakeFiles/bootloader-complete',
'build/bootloader-prefix/src/bootloader-stamp/bootloader-done',
],
)
all_build_files = get_snapshot('build/**/*', exclude_patterns=BUILD_SNAPSHOT_EXCLUDE)
logging.info('check that all build artifacts were generated')
all_artifacts = ALL_ARTIFACTS_BUILDV2 if request.config.getoption('buildv2', False) else ALL_ARTIFACTS_BUILDV1
@@ -40,33 +64,10 @@ def test_rebuild_no_changes(idf_py: IdfPyFunc, request: pytest.FixtureRequest) -
logging.info('build again with no changes')
idf_py('build')
# if there are no changes, nothing gets rebuilt
all_build_files_after_rebuild = get_snapshot(
'build/**/*',
exclude_patterns=[
'build/log/*',
'build/CMakeFiles/bootloader-complete',
'build/bootloader-prefix/src/bootloader-stamp/bootloader-done',
],
)
all_build_files_after_rebuild = get_snapshot('build/**/*', exclude_patterns=BUILD_SNAPSHOT_EXCLUDE)
all_build_files_after_rebuild.assert_same(all_build_files)
def rebuild_and_check(
idf_py: IdfPyFunc, should_be_rebuilt: str | list[str], should_not_be_rebuilt: str | list[str]
) -> None:
"""
Helper function for the test cases below.
Asserts that the files matching 'should_be_rebuilt' patterns are rebuilt
and files matching 'should_not_be_rebuilt' patterns aren't, after
touching (updating the mtime) of the given 'file_to_touch' and rebuilding.
"""
snapshot_should_be_rebuilt = get_snapshot(should_be_rebuilt)
snapshot_should_not_be_rebuilt = get_snapshot(should_not_be_rebuilt)
idf_py('build')
snapshot_should_be_rebuilt.assert_different(get_snapshot(should_be_rebuilt))
snapshot_should_not_be_rebuilt.assert_same(get_snapshot(should_not_be_rebuilt))
# For this and the following test function, there are actually multiple logical
# tests in one test function. It would be better to have every check in a separate
# test case, but that would mean doing a full clean build each time. Having a few
@@ -104,7 +105,7 @@ def test_rebuild_source_files(idf_py: IdfPyFunc) -> None:
# pick on .c, .cpp, .S file which includes sdkconfig.h:
obj_files = [
'build/esp-idf/esp_libc/CMakeFiles/__idf_esp_libc.dir/newlib_init.c.obj',
'build/esp-idf/nvs_flash/CMakeFiles/__idf_nvs_flash.dir/src/nvs_api.cpp.obj'
'build/esp-idf/nvs_flash/CMakeFiles/__idf_nvs_flash.dir/src/nvs_api.cpp.obj',
'build/esp-idf/esp_system/CMakeFiles/__idf_esp_system.dir/port/arch/xtensa/panic_handler_asm.S.obj',
]
sdkconfig_files = ['build/config/sdkconfig.h', 'build/config/sdkconfig.json']
@@ -0,0 +1,108 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""Rebuild tests with esp-idf-configdep enabled (default idf.py behavior).
esp-idf-configdep rewrites compiler .d files to depend on per-option
build/config/.../*.cdep stubs instead of sdkconfig.h. When a referenced
.cdep file does not exist yet, the wrapper creates it **after** the real
compiler has written the .obj. Those stubs then have a newer mtime than the
object. On the **next** Ninja run, those .cdep paths are prerequisites in the
merged dependency graph, so Ninja correctly treats the object as out of date and
rebuilds it—even though nothing in the source tree or sdkconfig changed.
That one-time “settling” wave is not a CMake bug; it is an artifact of how
configdep materializes stubs relative to object timestamps. The tests in
test_rebuild.py therefore disable configdep to exercise pure CMake/Ninja
incremental behavior. Here we keep configdep **on** and run **two** full
idf.py build invocations before any mtime baseline so stub/object mtimes are
consistent; then we apply the same coarse-grained checks as the classic rebuild
tests (plus relaxed expectations where configdep legitimately skips compiling
TUs that do not reference changed CONFIG_* symbols).
"""
import logging
import os
from pathlib import Path
import pytest
from test_build_system_helpers import ALL_ARTIFACTS_BUILDV1
from test_build_system_helpers import ALL_ARTIFACTS_BUILDV2
from test_build_system_helpers import APP_BINS
from test_build_system_helpers import BOOTLOADER_BINS
from test_build_system_helpers import BUILD_SNAPSHOT_EXCLUDE
from test_build_system_helpers import PARTITION_BIN
from test_build_system_helpers import IdfPyFunc
from test_build_system_helpers import get_snapshot
from test_build_system_helpers import rebuild_and_check
from test_build_system_helpers import replace_in_file
def _settle_configdep_graph(idf_py: IdfPyFunc) -> None:
"""Two full builds so ``.cdep`` stubs created by configdep are not newer than ``.obj`` for the next graph walk."""
logging.info('settle (configdep): first full build')
idf_py('build')
logging.info('settle (configdep): second full build reconciles obj vs .cdep mtimes')
idf_py('build')
@pytest.mark.usefixtures('test_app_copy')
def test_rebuild_no_changes_with_configdep(idf_py: IdfPyFunc, request: pytest.FixtureRequest) -> None:
"""No-op rebuild: with configdep on, baseline snapshot only after two settle builds (see module docstring)."""
_settle_configdep_graph(idf_py)
logging.info('baseline snapshot after settle')
all_build_files = get_snapshot('build/**/*', exclude_patterns=BUILD_SNAPSHOT_EXCLUDE)
logging.info('check that all build artifacts were generated')
all_artifacts = ALL_ARTIFACTS_BUILDV2 if request.config.getoption('buildv2', False) else ALL_ARTIFACTS_BUILDV1
for artifact in all_artifacts:
assert Path(artifact).exists()
logging.info('noop build with configdep; mtimes under baseline should match')
idf_py('build')
all_build_files_after = get_snapshot('build/**/*', exclude_patterns=BUILD_SNAPSHOT_EXCLUDE)
all_build_files_after.assert_same(all_build_files)
@pytest.mark.usefixtures('test_app_copy')
def test_rebuild_source_files_with_configdep(idf_py: IdfPyFunc) -> None:
"""Same selective-rebuild checks as ``test_rebuild_source_files``, after configdep settle builds."""
idf_path = Path(os.environ['IDF_PATH'])
_settle_configdep_graph(idf_py)
logging.info('updating a component source file rebuilds only that component')
component_files_patterns = [
'build/esp-idf/esp_system/libesp_system.a',
'build/esp-idf/esp_system/CMakeFiles/__idf_esp_system.dir/port/cpu_start.c.obj',
]
other_files_patterns = [
'build/esp-idf/lwip/liblwip.a',
'build/esp-idf/freertos/libfreertos.a',
*BOOTLOADER_BINS,
*PARTITION_BIN,
]
(idf_path / 'components/esp_system/port/cpu_start.c').touch()
rebuild_and_check(idf_py, component_files_patterns, other_files_patterns)
logging.info('changing a bootloader source file rebuilds the bootloader')
(idf_path / 'components/bootloader/subproject/main/bootloader_start.c').touch()
rebuild_and_check(idf_py, BOOTLOADER_BINS, APP_BINS + PARTITION_BIN)
logging.info('changing the partitions CSV file rebuilds only the partition table')
(idf_path / 'components/partition_table/partitions_singleapp.csv').touch()
rebuild_and_check(idf_py, PARTITION_BIN, APP_BINS + BOOTLOADER_BINS)
logging.info(
'sdkconfig update: binaries and generated headers must refresh (objects are selective under configdep)'
)
sdkconfig_files = ['build/config/sdkconfig.h', 'build/config/sdkconfig.json']
replace_in_file('sdkconfig', '# CONFIG_FREERTOS_UNICORE is not set', 'CONFIG_FREERTOS_UNICORE=y')
rebuild_and_check(idf_py, APP_BINS + BOOTLOADER_BINS + sdkconfig_files, PARTITION_BIN)
logging.info('Updating project CMakeLists.txt triggers app re-link at minimum')
replace_in_file(
'CMakeLists.txt',
'# placeholder_after_include_project_cmake',
'add_compile_options("-DUSELESS_MACRO_DOES_NOTHING=1")',
)
rebuild_and_check(idf_py, APP_BINS, PARTITION_BIN + BOOTLOADER_BINS + sdkconfig_files)