Files
Sudeep Mohanty 2ebd9c19b7 test(buildv2): enable test_cmake.py for cmakev2
Add buildv2 path and option handling for idf_as_lib, import_lib, and
related tests. Use cmakev2 examples when running with buildv2.
2026-03-20 08:13:27 +01:00

440 lines
18 KiB
Python

# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import json
import logging
import os
import re
import shutil
import sys
from pathlib import Path
import pytest
from test_build_system_helpers import EnvDict
from test_build_system_helpers import IdfPyFunc
from test_build_system_helpers import append_to_file
from test_build_system_helpers import file_contains
from test_build_system_helpers import replace_in_file
from test_build_system_helpers import run_cmake
from test_build_system_helpers import run_cmake_and_build
from test_build_system_helpers import run_idf_py
# This test checks multiple targets in one test function. It would be better to have each target
# tested in a isolated test case, but that would mean doing idf_copy each time, and copying takes most of the time
@pytest.mark.usefixtures('idf_copy')
def test_build_custom_cmake_project(test_app_copy: Path, request: pytest.FixtureRequest) -> None:
# Test is compatible with any target. Random targets in the list are selected for performance reasons
idf_path = Path(os.environ['IDF_PATH'])
is_buildv2 = request.config.getoption('buildv2', False)
if is_buildv2:
idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'idf_as_lib'
base_cmake_args = ['-G', 'Ninja', '-DESP_PLATFORM=1']
target_var = 'IDF_TARGET'
else:
idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib'
base_cmake_args = ['-G', 'Ninja']
target_var = 'TARGET'
for target in ['esp32', 'esp32c2', 'esp32c3', 'esp32c6', 'esp32h2', 'esp32p4', 'esp32s2', 'esp32s3']:
logging.info(f'Test build ESP-IDF as a library to a custom CMake projects for {target}')
run_cmake_and_build(
str(idf_as_lib_path),
*base_cmake_args,
'-DCMAKE_TOOLCHAIN_FILE={}'.format(idf_path / 'tools' / 'cmake' / f'toolchain-{target}.cmake'),
f'-D{target_var}={target}',
)
assert file_contains((test_app_copy / 'build' / 'compile_commands.json'), '"command"')
shutil.rmtree(test_app_copy / 'build')
sdkconfig_path = idf_as_lib_path / 'sdkconfig'
if sdkconfig_path.exists():
os.remove(sdkconfig_path)
@pytest.mark.skipif(
sys.platform == 'win32',
reason='On Win project is not buildable without system compiler on the host machine. (Win CI runners)',
)
@pytest.mark.usefixtures('idf_copy')
@pytest.mark.usefixtures('test_app_copy')
def test_build_custom_cmake_project_host(request: pytest.FixtureRequest) -> None:
logging.info('Test build ESP-IDF as a library to a custom CMake projects for host')
idf_path = Path(os.environ['IDF_PATH'])
is_buildv2 = request.config.getoption('buildv2', False)
if is_buildv2:
idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'idf_as_lib'
else:
idf_as_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib'
run_cmake_and_build(str(idf_as_lib_path), '-G', 'Ninja')
def test_build_cmake_library_with_toolchain_flags(test_app_copy: Path, request: pytest.FixtureRequest) -> None:
logging.info('Building a project with CMake library imported with modified toolchain flags')
idf_path = Path(os.environ['IDF_PATH'])
is_buildv2 = request.config.getoption('buildv2', False)
if is_buildv2:
import_lib_path = idf_path / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'import_lib'
else:
import_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'import_lib'
# Enable Picolibc to verify that all flags are passed correctly to the external project.
# In case something is missing, the build will fail on linking stage.
# Note: To enable Picolibc, IDF_EXPERIMENTAL_FEATURES must also be set for now.
(test_app_copy / 'sdkconfig.defaults').write_text(
'\n'.join(['CONFIG_IDF_EXPERIMENTAL_FEATURES=y', 'CONFIG_LIBC_PICOLIBC=y'])
)
run_cmake_and_build(
str(import_lib_path),
'-G',
'Ninja',
f'-DSDKCONFIG_DEFAULTS={import_lib_path / "sdkconfig.defaults"};{test_app_copy / "sdkconfig.defaults"}',
)
def check_flag_in_compile_commands(build_dir: Path, flag_to_find: str) -> None:
with open(build_dir / 'build' / 'compile_commands.json', encoding='utf-8') as f:
compile_commands = json.load(f)
# check if compile_commands is an array
if not isinstance(compile_commands, list):
assert False, f'compile_commands is not a list: {compile_commands}'
assert len(compile_commands) != 0, 'compile_commands is empty'
for entry in compile_commands:
command = entry['command']
assert isinstance(command, str), f'command is not a string: {command}'
flag_is_found = flag_to_find in command
if flag_is_found:
continue # Flag found in command, no need to check response files
# check if command contains response file paths starts with @
response_file_paths = re.findall(r'@([^\s]+)', command)
for response_file_path in response_file_paths:
# check if the flag file contains flag_to_find
try:
# Strip surrounding quotes and normalize the path
response_file_path = response_file_path.strip('"\'\\')
response_file_path = Path(response_file_path).resolve()
with open(response_file_path, encoding='utf-8') as f:
flags = f.read()
if flag_to_find in flags:
flag_is_found = True
break
except FileNotFoundError:
assert False, f'{response_file_path} does not exist'
if not flag_is_found:
assert False, f'{flag_to_find} not found in {command}'
def test_build_cmake_library_psram_workaround(test_app_copy: Path, request: pytest.FixtureRequest) -> None:
logging.info(
'Building a project with CMake library imported and PSRAM workaround, all files compile with workaround'
)
idf_path = Path(os.environ['IDF_PATH'])
is_buildv2 = request.config.getoption('buildv2', False)
(test_app_copy / 'sdkconfig.defaults').write_text(
'\n'.join(['CONFIG_SPIRAM=y', 'CONFIG_SPIRAM_CACHE_WORKAROUND=y'])
)
if is_buildv2:
# Use buildv2_test_app and add esp_psram via CMakeLists placeholder
replace_in_file(
test_app_copy / 'main' / 'CMakeLists.txt',
'# placeholder_inside_idf_component_register',
'REQUIRES esp_psram',
)
run_cmake(
'..',
'-G',
'Ninja',
'-DSDKCONFIG_DEFAULTS={}'.format(test_app_copy / 'sdkconfig.defaults'),
)
else:
import_lib_path = idf_path / 'examples' / 'build_system' / 'cmake' / 'import_lib'
run_cmake(
'-G',
'Ninja',
'-DCOMPONENTS=main;esp_psram',
'-DSDKCONFIG_DEFAULTS={}'.format(test_app_copy / 'sdkconfig.defaults'),
str(import_lib_path),
)
check_flag_in_compile_commands(test_app_copy, '-mfix-esp32-psram-cache-issue')
def test_build_cmake_library_psram_strategies(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
# Add explicit REQUIRES for cmakev2 compatibility
# Adding a public dependency on esp_psram should propagate the compile options publicly.
replace_in_file(
test_app_copy / 'main' / 'CMakeLists.txt',
'# placeholder_inside_idf_component_register',
'REQUIRES esp_psram',
)
for strategy in ['DUPLDST', 'NOPS', 'MEMW']:
logging.info(f'Test for external libraries in custom CMake projects with PSRAM strategy {strategy}')
(test_app_copy / 'sdkconfig.defaults').write_text(
'\n'.join(
[
'CONFIG_SPIRAM=y',
f'CONFIG_SPIRAM_CACHE_WORKAROUND_STRATEGY_{strategy}=y',
'CONFIG_SPIRAM_CACHE_WORKAROUND=y',
]
)
)
idf_py('reconfigure')
check_flag_in_compile_commands(test_app_copy, f'-mfix-esp32-psram-cache-strategy={strategy.lower()}')
(test_app_copy / 'sdkconfig').unlink()
def test_defaults_unspecified_build_args(idf_copy: Path, request: pytest.FixtureRequest) -> None:
is_buildv2 = request.config.getoption('buildv2', False)
if is_buildv2:
logging.info('PROJECT_DIR set correctly by build system v2')
idf_as_lib_path = idf_copy / 'examples' / 'build_system' / 'cmakev2' / 'features' / 'idf_as_lib'
cmake_args = [
'..',
'-G',
'Ninja',
'-DCMAKE_TOOLCHAIN_FILE={}'.format(str(idf_copy / 'tools' / 'cmake' / 'toolchain-esp32.cmake')),
'-DESP_PLATFORM=1',
'-DIDF_TARGET=esp32',
]
else:
logging.info('Defaults set properly for unspecified idf_build_process args')
idf_as_lib_path = idf_copy / 'examples' / 'build_system' / 'cmake' / 'idf_as_lib'
cmake_args = [
'..',
'-G',
'Ninja',
'-DCMAKE_TOOLCHAIN_FILE={}'.format(str(idf_copy / 'tools' / 'cmake' / 'toolchain-esp32.cmake')),
'-DTARGET=esp32',
]
append_to_file(
idf_as_lib_path / 'CMakeLists.txt',
'\n'.join(['idf_build_get_property(project_dir PROJECT_DIR)', 'message("Project directory: ${project_dir}")']),
)
ret = run_cmake(*cmake_args, workdir=idf_as_lib_path)
assert f'Project directory: {str(idf_as_lib_path.as_posix())}' in ret.stderr
def test_build_example_on_host(default_idf_env: EnvDict, request: pytest.FixtureRequest) -> None:
logging.info('Test if it can build the example to run on host')
idf_path = Path(default_idf_env.get('IDF_PATH'))
is_buildv2 = request.config.getoption('buildv2', False)
if is_buildv2:
idf_as_lib_path = Path(idf_path, 'examples', 'build_system', 'cmakev2', 'features', 'idf_as_lib')
cmake_configure_args = [
'..',
f'-DCMAKE_TOOLCHAIN_FILE={idf_path}/tools/cmake/toolchain-esp32.cmake',
'-DESP_PLATFORM=1',
'-DIDF_TARGET=esp32',
'-GNinja',
]
else:
idf_as_lib_path = Path(idf_path, 'examples', 'build_system', 'cmake', 'idf_as_lib')
cmake_configure_args = [
'..',
f'-DCMAKE_TOOLCHAIN_FILE={idf_path}/tools/cmake/toolchain-esp32.cmake',
'-DTARGET=esp32',
'-GNinja',
]
try:
run_cmake(*cmake_configure_args, workdir=idf_as_lib_path)
run_cmake('--build', '.', workdir=idf_as_lib_path)
finally:
shutil.rmtree(idf_as_lib_path / 'build', ignore_errors=True)
def test_cmake_preset_basic_functionality(test_app_copy: Path, default_idf_env: EnvDict) -> None:
logging.info('Test basic CMake preset functionality with multiple presets')
# Create a CMakePresets.json file with multiple configurations
presets_content = {
'version': 3,
'configurePresets': [
{
'name': 'default',
'binaryDir': 'build/default',
'displayName': 'Default Configuration',
'cacheVariables': {'SDKCONFIG': './build/default/sdkconfig'},
},
{
'name': 'prod1',
'binaryDir': 'build/prod1',
'displayName': 'Production 1',
'cacheVariables': {
'SDKCONFIG_DEFAULTS': 'sdkconfig.prod_common;sdkconfig.prod1',
'SDKCONFIG': './build/prod1/sdkconfig',
},
},
{
'name': 'prod2',
'binaryDir': 'build/prod2',
'displayName': 'Production 2',
'cacheVariables': {
'SDKCONFIG_DEFAULTS': 'sdkconfig.prod_common;sdkconfig.prod2',
'SDKCONFIG': './build/prod2/sdkconfig',
},
},
],
}
# Write the presets file
presets_file = test_app_copy / 'CMakePresets.json'
with open(presets_file, 'w') as f:
json.dump(presets_content, f, indent=4)
(test_app_copy / 'sdkconfig.prod_common').write_text('CONFIG_LWIP_IPV6=y\n')
(test_app_copy / 'sdkconfig.prod1').write_text('CONFIG_ESP_TASK_WDT_TIMEOUT_S=10\n')
(test_app_copy / 'sdkconfig.prod2').write_text('CONFIG_ESP_TASK_WDT_TIMEOUT_S=20\n')
# Test default preset auto-selection
ret = run_idf_py('reconfigure')
assert "CMake presets file found but no preset name given; using 'default' preset" in ret.stderr
assert (test_app_copy / 'build' / 'default').is_dir()
assert (test_app_copy / 'build' / 'default' / 'sdkconfig').is_file()
# Verify that sdkconfig is NOT in the project root, even when no preset is specified but auto-selected
assert not (test_app_copy / 'sdkconfig').is_file(), (
'sdkconfig should not be in project root when preset specifies custom location'
)
# Test explicit preset selection
ret = run_idf_py('--preset', 'prod1', 'reconfigure')
assert (test_app_copy / 'build' / 'prod1').is_dir()
assert (test_app_copy / 'build' / 'prod1' / 'sdkconfig').is_file()
# Test env variable preset selection
env_with_preset = default_idf_env.copy()
env_with_preset['IDF_PRESET'] = 'prod2'
ret = run_idf_py('reconfigure', env=env_with_preset)
assert (test_app_copy / 'build' / 'prod2').is_dir()
assert (test_app_copy / 'build' / 'prod2' / 'sdkconfig').is_file()
def test_cmake_preset_build_directory_precedence(test_app_copy: Path) -> None:
logging.info('Test CMake preset build directory precedence - command line overrides preset')
presets_content = {
'version': 3,
'configurePresets': [{'name': 'test_preset', 'binaryDir': 'build/preset_dir', 'displayName': 'Test Preset'}],
}
presets_file = test_app_copy / 'CMakePresets.json'
with open(presets_file, 'w') as f:
json.dump(presets_content, f, indent=4)
ret = run_idf_py('--preset', 'test_preset', '-B', 'custom_build', 'reconfigure')
assert 'custom_build' in ret.stdout
assert 'build/preset_dir' not in ret.stdout
def test_cmake_preset_without_binary_dir(test_app_copy: Path) -> None:
logging.info('Test CMake preset without binaryDir specification')
presets_content = {
'version': 3,
'configurePresets': [{'name': 'no_binary_dir', 'displayName': 'Preset without binaryDir'}],
}
presets_file = test_app_copy / 'CMakePresets.json'
with open(presets_file, 'w') as f:
json.dump(presets_content, f, indent=4)
ret = run_idf_py('--preset', 'no_binary_dir', 'reconfigure', check=False)
assert 'does not specify the build directory ("binaryDir")' in ret.stderr
def test_cmake_preset_error_cases(test_app_copy: Path) -> None:
logging.info('Test CMake preset error cases and invalid inputs')
# Test with invalid JSON
presets_file = test_app_copy / 'CMakePresets.json'
presets_file.write_text('{ invalid json content')
ret = run_idf_py('--preset', 'nonexistent', 'reconfigure', check=False)
assert ret.returncode != 0
# Clean up and test with valid JSON but nonexistent preset
presets_content = {
'version': 3,
'configurePresets': [
{'name': 'existing_preset', 'binaryDir': 'build/existing', 'displayName': 'Existing Preset'}
],
}
with open(presets_file, 'w') as f:
json.dump(presets_content, f, indent=4)
ret = run_idf_py('--preset', 'nonexistent_preset', 'reconfigure', check=False)
assert ret.returncode != 0
assert "No preset 'nonexistent_preset' found" in ret.stderr
presets_content_no_name = {
'version': 3,
'configurePresets': [
{'binaryDir': 'build/no_name', 'displayName': 'Preset without name'},
{'name': 'valid_preset', 'binaryDir': 'build/valid', 'displayName': 'Valid Preset'},
],
}
with open(presets_file, 'w') as f:
json.dump(presets_content_no_name, f, indent=4)
ret = run_idf_py('reconfigure', check=False)
assert 'Found CMake preset without a name' in ret.stderr
assert ret.returncode != 0
presets_content_no_version = {
'configurePresets': [
{'name': 'no_version', 'binaryDir': 'build/no_version', 'displayName': 'Preset without version'},
],
}
with open(presets_file, 'w') as f:
json.dump(presets_content_no_version, f, indent=4)
ret = run_idf_py('reconfigure', check=False)
assert 'Found CMake preset file without a version' in ret.stderr
assert ret.returncode != 0
def test_cmake_preset_sdkconfig_defaults_integration(test_app_copy: Path) -> None:
logging.info('Test CMake preset integration with SDKCONFIG_DEFAULTS')
presets_content = {
'version': 3,
'configurePresets': [
{
'name': 'with_defaults',
'binaryDir': 'build/with_defaults',
'displayName': 'Preset with SDKCONFIG_DEFAULTS',
'cacheVariables': {
'SDKCONFIG_DEFAULTS': 'sdkconfig.preset1;sdkconfig.preset2',
'SDKCONFIG': './build/with_defaults/sdkconfig',
},
},
{
'name': 'prod1',
'binaryDir': 'build/prod1',
'displayName': 'Production 1',
'cacheVariables': {
'SDKCONFIG_DEFAULTS': 'sdkconfig.prod_common;sdkconfig.prod1',
'SDKCONFIG': './build/prod1/sdkconfig',
},
},
],
}
presets_file = test_app_copy / 'CMakePresets.json'
with open(presets_file, 'w') as f:
json.dump(presets_content, f, indent=4)
(test_app_copy / 'sdkconfig.preset1').write_text('CONFIG_LWIP_IPV6=y\n')
(test_app_copy / 'sdkconfig.preset2').write_text('CONFIG_ESP_TASK_WDT_TIMEOUT_S=15\n')
run_idf_py('--preset', 'with_defaults', 'reconfigure')
assert (test_app_copy / 'build' / 'with_defaults').is_dir()
sdkconfig_path = test_app_copy / 'build' / 'with_defaults' / 'sdkconfig'
assert sdkconfig_path.is_file()
sdkconfig_content = sdkconfig_path.read_text()
assert 'CONFIG_LWIP_IPV6=y' in sdkconfig_content
assert 'CONFIG_ESP_TASK_WDT_TIMEOUT_S=15' in sdkconfig_content