ci: apply idf-ci 1.x

(cherry picked from commit 21d772a24da646bfb672418cf056199cad9b17e4)
This commit is contained in:
Fu Hanxi
2026-07-24 15:51:47 +02:00
parent 6a46ffa617
commit d359f42f6d
44 changed files with 614 additions and 352 deletions

View File

@@ -2,7 +2,7 @@
#
# Checks all public headers in IDF in the ci
#
# SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import argparse
@@ -309,6 +309,10 @@ class PublicHeaderChecker:
idf_path = os.getenv('IDF_PATH')
if idf_path is None:
raise RuntimeError("Environment variable 'IDF_PATH' wasn't set.")
idf_tools_path = os.getenv('IDF_TOOLS_PATH') or os.path.expanduser(os.path.join('~', '.espressif'))
idf_root_dep_path = os.path.join(idf_tools_path, 'root_managed_components')
project_dir = os.path.join(idf_path, 'examples', 'get-started', 'blink')
sdkconfig = os.path.join(self.build_dir, 'sdkconfig')
if self.libc_type == 'picolibc':
@@ -371,7 +375,11 @@ class PublicHeaderChecker:
if os.path.relpath(d, idf_path).startswith(tuple(ignore_dirs)):
self.log('{} - directory ignored'.format(d))
continue
for root, dirnames, filenames in os.walk(d):
for root, _, filenames in os.walk(d):
if root.startswith(idf_root_dep_path):
self.log(f'{root} - directory ignored (inside IDF_TOOLS_PATH/root_managed_components)')
continue
for filename in fnmatch.filter(filenames, '*.h'):
all_include_files.append(os.path.join(root, filename))
self.main_c = main_c

View File

@@ -52,7 +52,7 @@
# set while generating the pipeline
nodes: ""
INSTALL_EXTRA_TOOLS: "xtensa-esp-elf-gdb riscv32-esp-elf-gdb openocd-esp32 esp-rom-elfs"
PYTEST_EXTRA_FLAGS: "--dev-passwd ${ETHERNET_TEST_PASSWORD} --dev-user ${ETHERNET_TEST_USER} --capture=fd --verbosity=0 --unity-test-report-mode merge"
PYTEST_EXTRA_FLAGS: "--capture=fd --verbosity=0 --unity-test-report-mode merge"
needs:
- pipeline: $PARENT_PIPELINE_ID
job: pipeline_variables
@@ -77,7 +77,6 @@
- run_cmd python $CHECKOUT_REF_SCRIPT ci-test-runner-configs ci-test-runner-configs
# CI specific options start from "--known-failure-cases-file xxx". could ignore when running locally
- run_cmd pytest $nodes
--pipeline-id $PARENT_PIPELINE_ID
--junitxml=XUNIT_RESULT_${CI_JOB_ID}.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
--parallel-count ${CI_NODE_TOTAL:-1}

View File

@@ -4,17 +4,16 @@ import os
import subprocess
import sys
import typing as t
from pathlib import Path
from dynamic_pipelines.constants import BINARY_SIZE_METRIC_NAME
from idf_build_apps import App
from idf_build_apps import CMakeApp
from idf_build_apps.constants import BuildStatus
from idf_build_apps.utils import rmdir
from idf_ci_utils import APP_EXTRA_S3_ARTIFACT_TYPE
from idf_ci_utils import idf_relpath
from metrics.size_metrics import collect_build_metrics
if t.TYPE_CHECKING:
pass
_SIZE_METRICS_CONFIG_PATH = Path(__file__).parent.parent / 'metrics' / 'size_metrics' / 'size_metrics_config.yml'
class IdfCMakeApp(CMakeApp):
@@ -37,6 +36,8 @@ class IdfCMakeApp(CMakeApp):
'gitlab',
'upload-artifacts',
self.app_dir,
'--build-dir',
self.build_dir,
],
[
'idf-ci',
@@ -63,103 +64,3 @@ class IdfCMakeApp(CMakeApp):
self.build_path,
exclude_file_patterns=['build_log.txt', 'size*.json'],
)
class Metrics:
"""
Represents a metric and its values for source, target, and the differences.
"""
def __init__(
self,
source_value: t.Optional[float] = None,
target_value: t.Optional[float] = None,
difference: t.Optional[float] = None,
difference_percentage: t.Optional[float] = None,
) -> None:
self.source_value = source_value or 0.0
self.target_value = target_value or 0.0
self.difference = difference or 0.0
self.difference_percentage = difference_percentage or 0.0
def to_dict(self) -> t.Dict[str, t.Any]:
"""
Converts the Metrics object to a dictionary.
"""
return {
'source_value': self.source_value,
'target_value': self.target_value,
'difference': self.difference,
'difference_percentage': self.difference_percentage,
}
class AppWithMetricsInfo(IdfCMakeApp):
metrics: t.Dict[str, Metrics]
is_new_app: bool
def __init__(self, **kwargs: t.Any) -> None:
super().__init__(**kwargs)
self.metrics = {metric_name: metric_data for metric_name, metric_data in kwargs.get('metrics', {}).items()}
self.is_new_app = kwargs.get('is_new_app', False)
class Config:
arbitrary_types_allowed = True
def enrich_apps_with_metrics_info(
app_metrics_info_map: t.Dict[str, t.Dict[str, t.Any]], apps: t.List[App]
) -> t.List[AppWithMetricsInfo]:
def _get_full_attributes(obj: App) -> t.Dict[str, t.Any]:
"""
Retrieves all attributes of an object, including properties and computed fields.
"""
attributes: t.Dict[str, t.Any] = obj.__dict__.copy()
for attr in dir(obj):
if not attr.startswith('_'): # Skip private/internal attributes
try:
value = getattr(obj, attr)
# Include only if it's not already in __dict__
if attr not in attributes:
attributes[attr] = value
except Exception:
# Skip attributes that raise exceptions (e.g., methods needing args)
pass
return attributes
default_metrics_structure = {
BINARY_SIZE_METRIC_NAME: Metrics(
source_value=0,
target_value=0,
difference=0,
difference_percentage=0.0,
),
}
apps_with_metrics_info = []
for app in apps:
app.app_dir = idf_relpath(app.app_dir)
key = f'{app.app_dir}_{app.config_name}_{app.target}'
app_attributes = _get_full_attributes(app)
metrics = {metric_name: default_metric for metric_name, default_metric in default_metrics_structure.items()}
is_new_app = False
if key in app_metrics_info_map:
info = app_metrics_info_map[key]
for metric_name, metric_data in info.get('metrics', {}).items():
metrics[metric_name] = Metrics(
source_value=metric_data.get('source_value', 0),
target_value=metric_data.get('target_value', 0),
difference=metric_data.get('difference', 0),
difference_percentage=metric_data.get('difference_percentage', 0.0),
)
is_new_app = info.get('is_new_app', False)
app_attributes.update({'metrics': metrics, 'is_new_app': is_new_app})
apps_with_metrics_info.append(AppWithMetricsInfo(**app_attributes))
return apps_with_metrics_info

View File

@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
import typing as t
@@ -9,6 +9,7 @@ import pytest
import yaml
from _pytest.config import Config
from _pytest.python import Function
from _pytest.python import Metafunc
from _pytest.runner import CallInfo
from dynamic_pipelines.constants import KNOWN_GENERATE_TEST_CHILD_PIPELINE_WARNINGS_FILEPATH
from idf_ci import IdfPytestPlugin
@@ -114,6 +115,55 @@ class IdfLocalPlugin:
return item.callspec.params.get(key, default) or default
@staticmethod
def _has_parametrized_arg(metafunc: Metafunc, arg_name: str) -> bool:
for marker in metafunc.definition.iter_markers(name='parametrize'):
if not marker.args:
continue
argnames = marker.args[0]
if isinstance(argnames, str):
names = [name.strip() for name in argnames.split(',')]
else:
names = list(argnames)
if arg_name in names:
return True
for callspec in getattr(metafunc, '_calls', []):
if arg_name in callspec.params:
return True
return False
@staticmethod
def _is_linux_target_run(config: Config) -> bool:
target = config.getoption('target')
if not target:
return False
if isinstance(target, str):
targets = [_t.strip() for _t in target.split(',')]
else:
targets = [str(_t).strip() for _t in target]
return 'linux' in targets
@pytest.hookimpl(trylast=True)
def pytest_generate_tests(self, metafunc: Metafunc) -> None:
if 'embedded_services' not in metafunc.fixturenames:
return
if self._has_parametrized_arg(metafunc, 'embedded_services'):
return
if metafunc.definition.get_closest_marker('qemu') is not None:
metafunc.parametrize('embedded_services', ['idf,qemu'], indirect=True)
return
if self._is_linux_target_run(metafunc.config):
metafunc.parametrize('embedded_services', ['idf'], indirect=True)
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(self, config: Config, items: t.List[Function]) -> t.Generator[None, None, None]:
yield # throw it back to idf-ci

View File

@@ -661,6 +661,21 @@ macro(idf_build_process target)
endif()
endif()
idf_build_get_property(prefix __PREFIX)
file(GLOB root_dep_component_dirs
${IDF_TOOLS_PATH}/root_managed_components/idf${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}.${IDF_VERSION_PATCH}/*)
list(SORT root_dep_component_dirs)
foreach(component_dir ${root_dep_component_dirs})
# A potential component must be a directory
if(IS_DIRECTORY ${component_dir})
__component_dir_quick_check(is_component ${component_dir})
if(is_component)
__component_add(${component_dir} ${prefix} "idf_managed_components")
endif()
endif()
endforeach()
# Perform early expansion of component CMakeLists.txt in CMake scripting mode.
# It is here we retrieve the public and private requirements of each component.
# It is also here we add the common component requirements to each component's

View File

@@ -52,6 +52,13 @@ if(NOT __idf_env_set)
include(prefix_map)
include(openocd)
# ESP-IDF extra dependencies defined in tools/idf_extra_components.yml
if(WIN32)
set_default(IDF_TOOLS_PATH "$ENV{USERPROFILE}/.espressif")
else()
set_default(IDF_TOOLS_PATH "$ENV{HOME}/.espressif")
endif()
__build_init("${idf_path}")
# Check if IDF_ENV_FPGA environment is set

View File

@@ -131,6 +131,8 @@ foreach(__component_target ${__component_targets})
if("${__component_source}" STREQUAL "idf_components")
list(APPEND __TARGETS_IDF_COMPONENTS ${__component_target})
elseif("${__component_source}" STREQUAL "idf_managed_components")
list(APPEND __TARGETS_IDF_MANAGED_COMPONENTS ${__component_target})
elseif("${__component_source}" STREQUAL "project_managed_components")
list(APPEND __TARGETS_PROJECT_MANAGED_COMPONENTS ${__component_target})
elseif("${__component_source}" STREQUAL "project_extra_components")
@@ -147,6 +149,7 @@ set(__sorted_component_targets "")
foreach(__target IN LISTS __TARGETS_PROJECT_COMPONENTS
__TARGETS_PROJECT_EXTRA_COMPONENTS
__TARGETS_PROJECT_MANAGED_COMPONENTS
__TARGETS_IDF_MANAGED_COMPONENTS
__TARGETS_IDF_COMPONENTS)
__component_get_property(__component_name ${__target} COMPONENT_NAME)
list(APPEND __sorted_component_targets ${__target})

View File

@@ -0,0 +1,11 @@
# This file defines extra dependencies for ESP-IDF
# the dependencies defined here will be downloaded to
# $IDF_TOOLS_PATH/root_managed_components
# Each major.minor version of ESP-IDF can have its own subdirectory
# For example, for ESP-IDF v6.0, the dependencies will be installed to
# $IDF_TOOLS_PATH/root_managed_components/idf6.0
# The syntax is defined in:
# https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/manifest_file.html#dependencies
#dependencies:

View File

@@ -6,7 +6,7 @@
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/tools/idf-tools.html
# ci
idf-ci>=0.3,<1
idf-ci
coverage
jsonschema

View File

@@ -166,7 +166,6 @@ def test_examples_security_secure_boot_ecdsa(dut: Dut) -> None:
# Test secure boot flow.
# Correctly signed bootloader + correctly signed app should work
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'qemu_extra_args',

View File

@@ -6,7 +6,6 @@ from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize('config', ['secure_update_with_fe'], indirect=True)
@idf_parametrize('target', ['esp32c3'], indirect=['target'])

View File

@@ -6,7 +6,6 @@ from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.qemu
@pytest.mark.host_test
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_std_filesystem(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=200)

View File

@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import os
@@ -10,7 +10,6 @@ PROMPT = 'test_intr_dump>'
@pytest.mark.qemu
@pytest.mark.host_test
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_esp_intr_dump_nonshared(dut: Dut) -> None:
dut.expect_exact(PROMPT, timeout=30)
@@ -24,7 +23,6 @@ def test_esp_intr_dump_nonshared(dut: Dut) -> None:
@pytest.mark.qemu
@pytest.mark.host_test
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_esp_intr_dump_shared(dut: Dut) -> None:
dut.expect_exact(PROMPT, timeout=30)
@@ -54,5 +52,5 @@ def test_esp_intr_dump_expected_output(dut: Dut) -> None:
dut.expect_exact(PROMPT, timeout=30)
dut.write('intr_dump\n')
exp_out_file = os.path.join(os.path.dirname(__file__), 'expected_output', f'{dut.target}.txt')
for line in open(exp_out_file, 'r').readlines():
for line in open(exp_out_file).readlines():
dut.expect_exact(line.strip())

View File

@@ -3,7 +3,6 @@
import os
import pytest
from idf_ci_utils import IDF_PATH
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@@ -16,12 +15,12 @@ def test_app_mmu_page_size_32k_and_bootloader_mmu_page_size_64k(dut: Dut, app_do
assert '32K' in config
app_config = config.replace('32K', '64K')
build_dir = f'build_{dut.target}_{app_config}'
path_to_mmu_page_size_64k_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
if app_downloader:
app_downloader.download_app(os.path.relpath(path_to_mmu_page_size_64k_build, IDF_PATH), 'flash')
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
dut.serial.bootloader_flash(path_to_mmu_page_size_64k_build)
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
dut.expect('MMU page size mismatch')
dut.expect('App is running')
dut.expect('Partition test done')
@@ -36,12 +35,12 @@ def test_app_mmu_page_size_64k_and_bootloader_mmu_page_size_32k(dut: Dut, app_do
assert '64K' in config
app_config = config.replace('64K', '32K')
build_dir = f'build_{dut.target}_{app_config}'
path_to_mmu_page_size_32k_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
if app_downloader:
app_downloader.download_app(os.path.relpath(path_to_mmu_page_size_32k_build, IDF_PATH), 'flash')
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
dut.serial.bootloader_flash(path_to_mmu_page_size_32k_build)
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
dut.expect('MMU page size mismatch')
dut.expect('App is running')
dut.expect('Partition test done')

View File

@@ -4,7 +4,6 @@ import os
import re
import pytest
from idf_ci_utils import IDF_PATH
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@@ -19,12 +18,12 @@ def test_multicore_app_and_unicore_bootloader(dut: Dut, app_downloader, config)
assert 'multicore' in config
app_config = config.replace('multicore', 'unicore')
build_dir = f'build_{dut.target}_{app_config}'
path_to_unicore_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
if app_downloader:
app_downloader.download_app(os.path.relpath(path_to_unicore_build, IDF_PATH), 'flash')
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
dut.serial.bootloader_flash(path_to_unicore_build)
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
dut.expect('Unicore bootloader')
dut.expect('Multicore app')
if 'psram' in config:
@@ -43,12 +42,12 @@ def test_unicore_app_and_multicore_bootloader(dut: Dut, app_downloader, config)
assert 'unicore' in config
app_config = config.replace('unicore', 'multicore')
build_dir = f'build_{dut.target}_{app_config}'
path_to_multicore_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
if app_downloader:
app_downloader.download_app(os.path.relpath(path_to_multicore_build, IDF_PATH), 'flash')
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
dut.serial.bootloader_flash(path_to_multicore_build)
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
dut.expect('Multicore bootloader')
dut.expect('Unicore app')
if 'psram' in config:

View File

@@ -14,6 +14,9 @@ junit_family = xunit1
junit_logging = stdout
junit_log_passing_tests = False
filterwarnings =
ignore::pytest.PytestExperimentalApiWarning
## !! When adding new markers, don't forget to update also the tools\test_build_system\README.md !!
markers =
test_app_copy: specify relative path of the app to copy, and the prefix of the destination directory name

View File

@@ -1,9 +1,12 @@
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import json
import os.path
import textwrap
from pathlib import Path
import pytest
from test_build_system_helpers import EXT_IDF_PATH
from test_build_system_helpers import IdfPyFunc
from test_build_system_helpers import replace_in_file
@@ -192,3 +195,84 @@ class TestOptionalDependencyWithKconfig:
data = json.load(open(test_app_copy / 'build' / 'project_description.json'))
assert ['example__cmp'] == data['build_component_info']['foo']['priv_reqs']
assert ['espressif__mdns'] == data['build_component_info']['foo']['reqs']
@pytest.mark.revert_later(['tools/idf_extra_components.yml'])
class TestIdfRootDependency:
def test_basic_build(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None:
with open(os.path.join(EXT_IDF_PATH, 'tools', 'idf_extra_components.yml'), 'w') as fw:
fw.write(
textwrap.dedent("""
dependencies:
espressif/mdns: "*"
""")
)
replace_in_file(
(test_app_copy / 'main' / 'build_test_app.c'),
'// placeholder_before_main',
'#include "mdns.h"',
)
replace_in_file(
(test_app_copy / 'main' / 'CMakeLists.txt'),
'# placeholder_inside_idf_component_register',
'REQUIRES mdns',
)
idf_py('build')
def test_build_only_when_required(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None:
with open(os.path.join(EXT_IDF_PATH, 'tools', 'idf_extra_components.yml'), 'w') as fw:
fw.write(
textwrap.dedent("""
dependencies:
espressif/mdns: "*"
example/cmp: "*"
""")
)
idf_py('reconfigure')
data = json.load(open(test_app_copy / 'build' / 'project_description.json'))
assert 'espressif__mdns' not in data['build_components']
assert 'example__cmp' not in data['build_components']
replace_in_file(
(test_app_copy / 'main' / 'CMakeLists.txt'),
'# placeholder_inside_idf_component_register',
'REQUIRES mdns',
)
idf_py('reconfigure')
data = json.load(open(test_app_copy / 'build' / 'project_description.json'))
assert 'espressif__mdns' in data['build_components']
assert 'example__cmp' not in data['build_components']
def test_cleanup_unused(self, idf_py: IdfPyFunc, test_app_copy: Path) -> None:
with open(os.path.join(EXT_IDF_PATH, 'tools', 'idf_extra_components.yml'), 'w') as fw:
fw.write(
textwrap.dedent("""
dependencies:
espressif/mdns: "*"
""")
)
idf_py('reconfigure')
data = json.load(open(test_app_copy / 'build' / 'project_description.json'))
assert 'espressif__mdns' in data['all_component_info']
with open(os.path.join(EXT_IDF_PATH, 'tools', 'idf_extra_components.yml'), 'w') as fw:
fw.write(
textwrap.dedent("""
dependencies:
espressif/led_strip: "*"
example/cmp: "*"
""")
)
idf_py('reconfigure')
data = json.load(open(test_app_copy / 'build' / 'project_description.json'))
assert 'espressif__led_strip' in data['all_component_info']
assert 'example__cmp' in data['all_component_info']
assert 'espressif__mdns' not in data['all_component_info']

View File

@@ -27,9 +27,9 @@ except ImportError:
import idf
current_dir = os.path.dirname(os.path.realpath(__file__))
idf_py_path = os.path.join(current_dir, '..', 'idf.py')
idf_py_path = os.path.normpath(os.path.join(current_dir, '..', 'idf.py'))
extension_path = os.path.join(current_dir, 'test_idf_extensions', 'test_ext')
py_actions_path = os.path.join(current_dir, '..', 'idf_py_actions')
py_actions_path = os.path.normpath(os.path.join(current_dir, '..', 'idf_py_actions'))
link_path = os.path.join(py_actions_path, 'test_ext')