ci: apply idf-ci 1.x

(cherry picked from commit 21d772a24da646bfb672418cf056199cad9b17e4)
This commit is contained in:
Fu Hanxi
2026-07-27 10:52:35 +02:00
parent 6a46ffa617
commit d359f42f6d
44 changed files with 614 additions and 352 deletions
+45 -1
View File
@@ -270,7 +270,11 @@ variables:
- source tools/ci/utils.sh
- source tools/ci/configure_ci_environment.sh
- *show_ccache_statistics
- *upload_failed_job_log_artifacts
- run_cmd idf-ci gitlab upload-artifacts --type junit
- |
if [ $CI_JOB_STATUS = "failed" ]; then
run_cmd idf-ci gitlab upload-artifacts --type log
fi
.brew-macos-settings:
variables:
@@ -283,6 +287,46 @@ variables:
after_script: [] # ccache now is disabled for macos brew runners
timeout: 30m
.brew-windows-settings:
image: windows_11_idf_v6_0
tags: [windows-vm, brew]
variables:
GIT_STRATEGY: fetch
LC_ALL: C.UTF-8
# git clean at the final cleanup stage stuck on Windows VM
# a new VM for each job anyway, so we can skip cleaning the workspace
GIT_CLEAN_FLAGS: none
# IDF_CCACHE_ENABLE: "1"
# CCACHE_DIR: "C:/cache/idf_ccache"
before_script:
- if ($env:IDF_DONT_USE_MIRRORS) { $env:IDF_MIRROR_PREFIX_MAP = '' }
- ./install.ps1 --enable-ci
- ./export.ps1
- $env:PYTHONPATH = "$env:PYTHONPATH;$env:IDF_PATH\tools;$env:IDF_PATH\tools\esp_app_trace;$env:IDF_PATH\components\partition_table;$env:IDF_PATH\tools\ci\python_packages"
- python "${SUBMODULE_FETCH_TOOL}" -s "${SUBMODULES_TO_FETCH}"
# Sync VM clock. net start/stop exit 2 when the service is already in that state; do not fail CI.
- |
function Invoke-NetService($Action, $Name) {
$output = net $Action $Name 2>&1
if ($LASTEXITCODE -eq 2) {
Write-Host "net $Action $Name already in requested state: $output"
$global:LASTEXITCODE = 0
return
}
if ($LASTEXITCODE -ne 0) {
Write-Host $output
exit $LASTEXITCODE
}
}
Invoke-NetService start w32time
w32tm /config /manualpeerlist:"time.google.com,0x9 pool.ntp.org,0x9" /syncfromflags:manual /reliable:yes /update
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Invoke-NetService stop w32time
Invoke-NetService start w32time
w32tm /resync
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
after_script: []
#############
# `default` #
#############
+1 -2
View File
@@ -373,7 +373,6 @@ test_pytest_qemu:
- run_cmd pytest
--target $IDF_TARGET
-m qemu
--embedded-services idf,qemu
--junitxml=XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
--qemu-extra-args \"-global driver=timer.$IDF_TARGET.timg,property=wdt_disable,value=true\"
@@ -398,7 +397,7 @@ test_pytest_linux:
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- run_cmd pytest
--target linux
--embedded-services idf
-m \"not macos\"
--junitxml=XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
+2
View File
@@ -68,6 +68,7 @@
- "tools/ci/check_esp_memory_utils_headers.sh"
- "tools/ci/check_blobs.sh"
- "tools/ci/check_public_headers.py"
- "tools/ci/check_public_headers_exceptions.txt"
- "tools/ci/check_register_rw_half_word.cmake"
- "tools/ci/check_register_rw_half_word.py"
- "examples/build_system/**/*"
@@ -102,6 +103,7 @@
- "tools/idf_tools.py"
- "tools/test_idf_tools/**/*"
- "tools/install_util.py"
- "tools/idf_extra_components.yml"
- "tools/export_utils/utils.py"
- "tools/export_utils/shell_types.py"
+23 -67
View File
@@ -1,9 +1,8 @@
# Host tests
.host_test_win_template:
extends: .rules:test:host_test
extends:
- .brew-windows-settings
- .rules:test:host_test
stage: host_test
image: $ESP_ENV_IMAGE
tags: [windows-build, brew]
dependencies: # set dependencies to null to avoid missing artifacts issue
# run host_test jobs immediately, only after upload cache
needs:
@@ -13,13 +12,8 @@
- job: upload-submodules-cache
optional: true
artifacts: false
variables:
GIT_STRATEGY: fetch # use brew local mirror first
before_script:
- if ($env:IDF_DONT_USE_MIRRORS) {
$env:IDF_MIRROR_PREFIX_MAP = ""
}
after_script: []
- job: manual_gate
optional: true
test_cli_installer_win:
rules:
@@ -34,15 +28,12 @@ test_cli_installer_win:
- tools/tools.new.json
- tools/test_idf_tools/test_python_env_logs.txt
expire_in: 1 week
variables:
IDF_PATH: "$CI_PROJECT_DIR"
timeout: 3h
script:
# Tools must be downloaded for testing
# cmake@version that is supported
- python ${IDF_PATH}\tools\idf_tools.py download required qemu-riscv32 qemu-xtensa cmake cmake@3.22.1
- cd ${IDF_PATH}\tools\test_idf_tools
- python -m pip install jsonschema
- python tools\idf_tools.py download required qemu-riscv32 qemu-xtensa cmake cmake@3.22.1
- cd tools\test_idf_tools
- python .\test_idf_tools.py
- python .\test_idf_tools_python_env.py
@@ -53,81 +44,47 @@ test_tools_win:
parallel: 4
artifacts:
paths:
- ${IDF_PATH}/*.out
- ${IDF_PATH}/XUNIT_*.xml
- "*.out"
- "XUNIT_*.xml"
reports:
junit: ${IDF_PATH}/XUNIT_*.xml
junit: "XUNIT_*.xml"
expire_in: 1 week
when: always
variables:
LC_ALL: C.UTF-8
PYTHONPATH: "$PYTHONPATH;$IDF_PATH\\tools;$IDF_PATH\\tools\\esp_app_trace;$IDF_PATH\\components\\partition_table;$IDF_PATH\\tools\\ci\\python_packages"
script:
- python -m pip install jsonschema
- .\install.ps1 --enable-ci
- .\export.ps1
- python "${SUBMODULE_FETCH_TOOL}" -s "all"
- cd ${IDF_PATH}/tools/test_idf_py
- cd tools\test_idf_py
- idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml=${IDF_PATH}/XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml ${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
# Build tests
.test_build_system_template_win:
stage: host_test
variables:
# Enable ccache for all build jobs. See configure_ci_environment.sh for more ccache related settings.
IDF_CCACHE_ENABLE: "1"
PYTHONPATH: "$PYTHONPATH;$IDF_PATH\\tools;$IDF_PATH\\tools\\esp_app_trace;$IDF_PATH\\components\\partition_table;$IDF_PATH\\tools\\ci\\python_packages"
before_script: []
after_script: []
extends: .host_test_win_template
timeout: 4 hours
artifacts:
paths:
- XUNIT_RESULT.xml
- test_build_system
expire_in: 2 days
when: always
reports:
junit: XUNIT_RESULT.xml
script:
- .\install.ps1 --enable-ci
- . .\export.ps1
- python "${SUBMODULE_FETCH_TOOL}" -s "all"
- cd ${IDF_PATH}\tools\test_build_system
- cd tools\test_build_system
- idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml ${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
pytest_build_system_win:
extends:
- .test_build_system_template_win
- .rules:labels:windows_pytest_build_system
parallel: 2
needs:
- job: manual_gate
optional: true
tags: [windows-build, brew]
artifacts:
paths:
- XUNIT_RESULT.xml
- test_build_system
expire_in: 2 days
reports:
junit: XUNIT_RESULT.xml
when: always
pytest_build_system_win_minimal_cmake:
extends:
- .test_build_system_template_win
- .rules:labels:windows_pytest_build_system
needs:
- job: manual_gate
optional: true
tags: [windows-build, brew]
artifacts:
paths:
- XUNIT_RESULT.xml
- test_build_system
expire_in: 2 days
reports:
junit: XUNIT_RESULT.xml
when: always
variables:
MINIMAL_CMAKE_VERSION: "3.16.3"
script:
- .\install.ps1 --enable-ci
- . .\export.ps1
- python ${IDF_PATH}\tools\idf_tools.py install cmake@${MINIMAL_CMAKE_VERSION}
- $Env:PATH = "$Env:USERPROFILE\.espressif\tools\cmake\${MINIMAL_CMAKE_VERSION}\bin;$Env:PATH"
- |
@@ -136,7 +93,6 @@ pytest_build_system_win_minimal_cmake:
Write-Error "ERROR: Wrong CMake version! Detected: $actualVersion, but expected: $Env:MINIMAL_CMAKE_VERSION"
exit 1
}
- python "${SUBMODULE_FETCH_TOOL}" -s "all"
- cd ${IDF_PATH}\tools\test_build_system
- idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- pytest -k cmake --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
+2 -2
View File
@@ -31,8 +31,8 @@ additional_build_targets = [
]
# collect
collect_app_info_filename = "app_info_${CI_JOB_NAME_SLUG}.txt"
junitxml = "build_summary_${CI_JOB_NAME_SLUG}.xml"
collect_app_info_filename = "app_info_${CI_JOB_ID}.txt"
junitxml = "build_summary_${CI_JOB_ID}.xml"
# manifest
check_manifest_rules = true
+33 -24
View File
@@ -1,4 +1,6 @@
preserve_non_test_related_apps = false
filter_non_test_related_apps_by_modified_files = true
filter_apps_by_component_target = true
exclude_dirs = [
'tools/test_mkdfu',
'tools/test_idf_size',
@@ -20,7 +22,7 @@ IDF_CI_BUILD = "1"
[gitlab.build_pipeline]
workflow_name = "build_child_pipeline"
presigned_json_job_name = 'generate_pytest_build_report'
runs_per_job = 30
job_tags = ['build', 'shiny']
job_template_name = '.dynamic_build_template'
@@ -51,37 +53,44 @@ include:
- tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
"""
[gitlab.artifacts.s3.debug]
[gitlab.artifacts.s3]
enable = true
[gitlab.artifacts.s3.configs.debug]
bucket = "idf-artifacts"
zip_first = true
build_dir_pattern = "**/build*/"
patterns = [
'**/build*/bootloader/*.map',
'**/build*/bootloader/*.elf',
'**/build*/*.map',
'**/build*/*.elf',
'bootloader/*.map',
'bootloader/*.elf',
'*.map',
'*.elf',
# customized
'**/build*/esp_tee/*.map',
'**/build*/esp_tee/*.elf',
'**/build*/gdbinit/*',
'esp_tee/*.map',
'esp_tee/*.elf',
'gdbinit/*',
]
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
[gitlab.artifacts.s3.flash]
[gitlab.artifacts.s3.configs.flash]
bucket = "idf-artifacts"
zip_first = true
build_dir_pattern = "**/build*/"
patterns = [
'**/build*/bootloader/*.bin',
'**/build*/*.bin',
'**/build*/partition_table/*.bin',
'**/build*/flasher_args.json',
'**/build*/flash_project_args',
'**/build*/config/sdkconfig.json',
'**/build*/sdkconfig',
'**/build*/project_description.json',
'bootloader/*.bin',
'*.bin',
'partition_table/*.bin',
'flasher_args.json',
'flash_project_args',
'config/sdkconfig.json',
'sdkconfig',
'project_description.json',
# customized
'**/build*/esp_tee/*.bin',
'esp_tee/*.bin',
]
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
[gitlab.artifacts.s3.app_extra]
[gitlab.artifacts.s3.configs.app_extra]
bucket = "idf-artifacts"
# App-dir files needed at target-test time but outside build_* (uploaded without --build-dir).
# Add patterns here as needed, e.g. coverage info produced during build.
@@ -90,27 +99,27 @@ patterns = [
]
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
[gitlab.artifacts.s3.log]
[gitlab.artifacts.s3.configs.log]
bucket = "idf-artifacts"
patterns = [
'**/build*/build_log.txt',
'**/build*/size*.json',
]
[gitlab.artifacts.s3.junit]
[gitlab.artifacts.s3.configs.junit]
bucket = "idf-artifacts"
patterns = [
'**/XUNIT_RESULT_*.xml',
'**/build_summary_*.xml',
]
[gitlab.artifacts.s3.env]
[gitlab.artifacts.s3.configs.env]
bucket = "idf-artifacts"
patterns = [
'**/pipeline.env',
]
[gitlab.artifacts.s3.longterm]
[gitlab.artifacts.s3.configs.longterm]
bucket = "longterm"
if_clause = 'CI_COMMIT_REF_NAME == "master"'
patterns = [
+1 -1
View File
@@ -229,7 +229,7 @@ repos:
- id: check-kconfig-files
- id: check-deprecated-kconfig-options
- repo: https://github.com/espressif/idf-ci
rev: v1.0.0b4
rev: v1.2.1
hooks:
- id: check-tests-missing-config
files: 'pytest_.*\.py$|sdkconfig(\..*)?$'
@@ -11,14 +11,12 @@ def test_bootloader_support(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.host_test
@pytest.mark.qemu
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_bootloader_support_qemu_esp32(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.host_test
@pytest.mark.qemu
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
def test_bootloader_support_qemu_esp32c3(dut: Dut) -> None:
@@ -12,6 +12,11 @@ import sys
import unittest
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union
# Add scripts/ to sys.path so test modules can import the production code
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / 'scripts'
@@ -61,11 +66,11 @@ def make_compressor() -> Any:
def write_yaml_config(
tmp_dir: str,
tags: list[str],
script_path: str | Path,
tags: List[str],
script_path: Union[str, Path],
module_name: str = 'BLE_HOST',
log_index_file: str = 'test_log_index.h',
tags_with_preserve: list[str] | None = None,
tags_with_preserve: Optional[List[str]] = None,
) -> Path:
"""Write a module_info.yml and return its path."""
import yaml
@@ -140,9 +145,8 @@ class PipelineContext:
shutil.copy2(src, dst)
return str(rel_path)
def run_compression(self, src_list: list[str]) -> dict[str, list[tuple[int, str]]]:
"""Run mirror + prepare + compress + header generation. Returns generated macros."""
self.compressor.mirror_local_includes()
def run_compression(self, src_list: List[str]) -> Dict[str, List[Tuple[int, str]]]:
"""Run prepare + compress + header generation. Returns generated macros."""
self.compressor.prepare_source_files(src_list)
files_to_process = []
@@ -150,7 +154,7 @@ class PipelineContext:
files_to_process.extend([(module, path) for path in info['files_to_process']])
files_to_process.sort(key=lambda x: x[1])
all_macros: dict[str, list[tuple[int, str]]] = {}
all_macros: Dict[str, List[Tuple[int, str]]] = {}
for file_info in files_to_process:
file_macros = self.compressor.compress_file(file_info)
for module, log_id, macro in file_macros:
@@ -186,8 +190,8 @@ class PipelineContext:
def assert_header_matches_golden(
test_case: unittest.TestCase,
generated_path: str | Path,
golden_path: str | Path,
generated_path: Union[str, Path],
golden_path: Union[str, Path],
) -> None:
"""Compare generated header to golden file, normalizing copyright year."""
gen_text = Path(generated_path).read_text()
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded import Dut
@@ -50,63 +50,59 @@ def do_test_help_quit(dut: Dut) -> None:
dut.expect(r'quit\s+Quit REPL environment\s+esp>')
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console(dut: Dut, test_on: str) -> None:
def test_console(dut: Dut) -> None:
dut.run_all_single_board_cases(group='!ignore', timeout=120)
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_repl(dut: Dut, test_on: str) -> None:
def test_console_repl(dut: Dut) -> None:
do_test_quit(dut)
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_sorted_registration(dut: Dut, test_on: str) -> None:
def test_console_help_sorted_registration(dut: Dut) -> None:
do_test_help_generic(dut, 'sorted')
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_reverse_registration(dut: Dut, test_on: str) -> None:
def test_console_help_reverse_registration(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console help command - reverse registration"', expect_str='esp>')
@@ -131,45 +127,41 @@ def test_console_help_reverse_registration(dut: Dut, test_on: str) -> None:
@idf_parametrize('config', ['sorted'], indirect=['config'])
@idf_parametrize('target', ['linux'], indirect=['target'])
@idf_parametrize('test_on,markers', [('host', (pytest.mark.host_test,))])
def test_console_sorted_help_sorted_registration(dut: Dut, test_on: str) -> None:
def test_console_sorted_help_sorted_registration(dut: Dut) -> None:
do_test_help_generic(dut, 'sorted')
@idf_parametrize('config', ['sorted'], indirect=['config'])
@idf_parametrize('target', ['linux'], indirect=['target'])
@idf_parametrize('test_on,markers', [('host', (pytest.mark.host_test,))])
def test_console_sorted_help_reverse_registration(dut: Dut, test_on: str) -> None:
def test_console_sorted_help_reverse_registration(dut: Dut) -> None:
do_test_help_generic(dut, 'reverse')
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_quit(dut: Dut, test_on: str) -> None:
def test_console_help_quit(dut: Dut) -> None:
do_test_help_quit(dut)
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_verbose_level_0(dut: Dut, test_on: str) -> None:
def test_console_help_verbose_level_0(dut: Dut) -> None:
help_verbose_info = 'Print the summary of all registered commands if no arguments are given,'
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console help command - set verbose level = 0"', expect_str='esp>')
@@ -180,18 +172,17 @@ def test_console_help_verbose_level_0(dut: Dut, test_on: str) -> None:
dut.expect_exact('help', not_matching=help_verbose_info)
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_verbose_level_1(dut: Dut, test_on: str) -> None:
def test_console_help_verbose_level_1(dut: Dut) -> None:
help_verbose_info = 'Print the summary of all registered commands if no arguments are given,'
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console help command - set verbose level = 1"', expect_str='esp>')
@@ -201,18 +192,17 @@ def test_console_help_verbose_level_1(dut: Dut, test_on: str) -> None:
dut.expect_exact(help_verbose_info)
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_verbose_subcommand(dut: Dut, test_on: str) -> None:
def test_console_help_verbose_subcommand(dut: Dut) -> None:
help_verbose_info = 'Print the summary of all registered commands if no arguments are given,'
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console help command - --verbose sub command"', expect_str='esp>')
@@ -227,18 +217,17 @@ def test_console_help_verbose_subcommand(dut: Dut, test_on: str) -> None:
dut.expect_exact(help_verbose_info)
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_deregister(dut: Dut, test_on: str) -> None:
def test_console_help_deregister(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console deregister commands"', expect_str='esp>')
@@ -250,18 +239,17 @@ def test_console_help_deregister(dut: Dut, test_on: str) -> None:
dut.expect_exact(cmd_z_description, not_matching=cmd_a_description)
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
'target,config,embedded_services,markers',
[
('linux', 'host', (pytest.mark.host_test,)),
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target'],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_help_re_register(dut: Dut, test_on: str) -> None:
def test_console_help_re_register(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console re-register commands"', expect_str='esp>')
@@ -16,7 +16,6 @@ def test_efuse(dut: Dut) -> None:
@pytest.mark.qemu
@pytest.mark.host_test
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_efuse_qemu(dut: Dut) -> None:
dut.run_all_single_board_cases()
@@ -19,7 +19,6 @@ def test_esp_event(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.xfail('config.getvalue("target") == "esp32c3"', reason='Unstable on QEMU, needs investigation')
@pytest.mark.parametrize(
@@ -24,7 +24,6 @@ def test_esp_ringbuf(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'target',
@@ -15,7 +15,6 @@ def test_esp_rom(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.host_test
@pytest.mark.qemu
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
def test_esp_rom_qemu(dut: Dut) -> None:
@@ -20,7 +20,6 @@ def test_heap_poisoning(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'config, embedded_services',
@@ -46,7 +46,6 @@ def test_pthread_single_core_tls(dut: Dut) -> None:
dut.run_all_single_board_cases(group='thread-specific', timeout=300)
@pytest.mark.host_test
@pytest.mark.qemu
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_pthread_qemu(dut: Dut) -> None:
+24 -11
View File
@@ -44,6 +44,7 @@ from idf_pytest.plugin import IDF_LOCAL_PLUGIN_KEY
from idf_pytest.plugin import IdfLocalPlugin
from idf_pytest.plugin import requires_elf_or_map
from idf_pytest.utils import format_case_id
from pytest_embedded.plugin import _request_param_or_config_option_or_default
from pytest_embedded.plugin import multi_dut_fixture
from pytest_embedded.utils import to_bytes
from pytest_embedded.utils import to_str
@@ -127,7 +128,7 @@ class AppDownloader:
self.commit_sha = commit_sha
self.pipeline_id = pipeline_id
def download_app(self, app_build_path: str, artifact_type: t.Optional[str] = None) -> None:
def download_app(self, app_dir: str, build_dir: str, artifact_type: t.Optional[str] = None) -> None:
args = [
'idf-ci',
'gitlab',
@@ -137,15 +138,26 @@ class AppDownloader:
]
if artifact_type:
args.extend(['--type', artifact_type])
if self.pipeline_id:
args.extend(['--pipeline-id', self.pipeline_id])
args.append(app_build_path)
subprocess.run(
args,
stdout=sys.stdout,
stderr=sys.stderr,
args.extend(
[
app_dir,
'--build-dir',
build_dir,
]
)
result = subprocess.run(
args,
capture_output=True,
text=True,
cwd=IDF_PATH,
)
logging.info(result.stdout)
if result.stderr:
logging.info(result.stderr)
def download_app_extra(self, app_dir: str) -> None:
"""Download app-dir artifacts defined under app_extra in .idf_ci.toml."""
@@ -348,12 +360,13 @@ def build_dir(
downloader = app_downloader
if downloader:
app_dir = idf_relpath(app_path)
build_dir = f'build_{target}_{config}'
# somehow hardcoded...
app_build_path = os.path.join(idf_relpath(app_path), f'build_{target}_{config}')
if requires_elf_or_map(case):
downloader.download_app(app_build_path)
downloader.download_app(app_dir, build_dir)
else:
downloader.download_app(app_build_path, 'flash')
downloader.download_app(app_dir, build_dir, 'flash')
check_dirs = [f'build_{target}_{config}']
else:
check_dirs = []
@@ -537,12 +550,12 @@ def log_minimum_free_heap_size(dut: IdfDut, config: str, idf_path: str) -> t.Cal
@pytest.fixture(scope='session')
def dev_password(request: FixtureRequest) -> str:
return request.config.getoption('dev_passwd') or ''
return _request_param_or_config_option_or_default(request, 'dev_password', '') # type: ignore
@pytest.fixture(scope='session')
def dev_user(request: FixtureRequest) -> str:
return request.config.getoption('dev_user') or ''
return _request_param_or_config_option_or_default(request, 'dev_user', '') # type: ignore
##################
+2
View File
@@ -45,6 +45,8 @@ Inside the ``IDF_TOOLS_PATH`` directory, the tools installation scripts create t
- ``python_env`` — not related to the tools; virtual Python environments are installed in the sub-directories. Note that the Python environment directory can be placed elsewhere by setting the ``IDF_PYTHON_ENV_PATH`` environment variable.
- ``idf_version.txt`` — located within each specific Python environment sub-directory under ``python_env``, this file records the ESP-IDF version corresponding to that environment. The version is stored in a format like ``5.3`` to represent ESP-IDF version ``v5.3``.
- ``root_managed_components`` — directory managed by ``idf-component-manager`` for components installed globally.
- ``espidf.constraints.*.txt`` — one constraint file for each ESP-IDF release containing Python package version requirements.
GitHub Assets Mirror
@@ -120,6 +120,52 @@ Next, we have the environment marker:
Finally, we have the test function. With a ``dut`` fixture. In single-dut test cases, the ``dut`` fixture is an instance of ``IdfDut`` class, for multi-dut test cases, it is a tuple of ``IdfDut`` instances. For more details regarding the ``IdfDut`` class, please refer to `pytest-embedded IdfDut API reference <https://docs.espressif.com/projects/pytest-embedded/en/latest/api.html#pytest_embedded_idf.dut.IdfDut>`__.
Running Tests on Linux
^^^^^^^^^^^^^^^^^^^^^^
To execute a pytest case on the Linux host, set ``target`` to ``linux``.
.. code-block:: python
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_hello_world_linux(dut) -> None:
dut.expect('Hello world!')
This is the simplest way to run the same test flow on the Linux host instead of on physical hardware.
For a simple Linux-only test, setting ``target`` to ``linux`` is enough and the ``idf`` embedded services will be selected automatically. The ``pytest.mark.host_test`` marker is not required.
For a mixed environment matrix, specify ``embedded_services`` manually for each case. See :ref:`Same App With Different Running Environments <pytest-same-app-different-running-environments>` for a more complex example.
.. only:: TARGET_SUPPORT_QEMU
Running Tests in QEMU
^^^^^^^^^^^^^^^^^^^^^
To execute a pytest case in QEMU, add the ``@pytest.mark.qemu`` marker to the test function.
.. code-block:: python
@pytest.mark.qemu
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_hello_world_qemu(dut) -> None:
dut.expect('Hello world!')
This is the simplest way to run the same test flow in QEMU instead of on physical hardware.
For a simple QEMU-only test, adding ``pytest.mark.qemu`` is enough and the ``idf,qemu`` embedded services will be selected automatically.
For a mixed environment matrix, specify ``embedded_services`` manually for each case. See the later section in this guide for a more complex example.
For QEMU installation and setup, refer to page :doc:`/api-guides/tools/qemu`.
Deprecation of ``pytest.mark.host_test``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``pytest.mark.host_test`` is no longer needed and should not be added to new test cases.
For Linux target test cases and QEMU test cases, the required behavior is handled dynamically by the test framework. In particular, the embedded services are selected automatically for simple Linux-only and QEMU-only cases.
Same App With Different sdkconfig Files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -208,6 +254,49 @@ Now this test function would be replicated to 2 test cases (represented as test
* ``esp32.foo.test_foo_bar``
* ``esp32s2.bar.test_foo_bar``
.. _pytest-same-app-different-running-environments:
Same App With Different Running Environments
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sometimes the same app should be validated in different running environments, for example on the host with the Linux target, on real hardware, or in QEMU. If a single ``@pytest.mark.qemu`` test is not enough, combine ``target``, ``config``, and ``embedded_services`` in a single ``idf_parametrize`` decorator, and attach the required marker for each case.
The following example is adapted from :idf_file:`components/console/test_apps/console/pytest_console.py`:
.. code-block:: python
@idf_parametrize(
'target,config,embedded_services,markers',
[
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_repl(dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
This creates four test cases for the same app:
* Linux host execution with the ``idf`` service
* ESP32 hardware execution with the ``esp,idf`` services
* ESP32-C3 hardware execution with the ``esp,idf`` services
* ESP32 execution in QEMU with the ``idf,qemu`` services
When running locally, you can select only the environment you want:
.. code-block:: shell
$ pytest --target linux
$ pytest -m qemu
$ pytest -m qemu --target esp32
``pytest --target linux`` selects Linux target cases only. ``pytest -m qemu`` selects all QEMU-marked cases. ``pytest -m qemu --target esp32`` further limits the selection to QEMU cases for the ESP32 target.
Use this pattern when the test logic is the same but the execution environment changes.
Testing Serial Output (Expecting)
---------------------------------
@@ -120,6 +120,52 @@ ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自
关于测试函数,使用了一个 ``dut`` fixture。在单一 DUT 测试用例中,``dut`` fixture 是 ``IdfDut`` 类的一个实例,对于多个 DUT 测试用例,它是 ``IdfDut`` 实例的一个元组。有关 ``IdfDut`` 类的更多详细信息,请参阅 `pytest-embedded IdfDut API 参考 <https://docs.espressif.com/projects/pytest-embedded/en/latest/api.html#pytest_embedded_idf.dut.IdfDut>`__。
在 Linux 上运行测试
^^^^^^^^^^^^^^^^^^^^
要在 Linux 主机上执行 pytest 测试用例,请将 ``target`` 设置为 ``linux``。
.. code-block:: python
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_hello_world_linux(dut) -> None:
dut.expect('Hello world!')
这是在 Linux 主机上运行与物理硬件相同测试流程的最简单方式。
对于简单的纯 Linux 测试,只需将 ``target`` 设置为 ``linux``,系统会自动选择 ``idf`` 对应的 embedded services。``pytest.mark.host_test`` marker 不再需要。
对于混合运行环境矩阵,则需要为每个用例手动指定 ``embedded_services``。更复杂的示例请参阅 :ref:`在不同运行环境中运行相同的应用程序 <pytest-same-app-different-running-environments>` 小节。
.. only:: TARGET_SUPPORT_QEMU
在 QEMU 中运行测试
^^^^^^^^^^^^^^^^^^^^^
要在 QEMU 中执行 pytest 测试用例,请将 ``@pytest.mark.qemu`` 添加到测试函数上。
.. code-block:: python
@pytest.mark.qemu
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_hello_world_qemu(dut) -> None:
dut.expect('Hello world!')
这是在 QEMU 中运行与物理硬件相同测试流程的最简单方式。
对于简单的纯 QEMU 测试,只需添加 ``pytest.mark.qemu``,系统会自动选择 ``idf,qemu`` 对应的 embedded services。
对于混合运行环境矩阵,则需要为每个用例手动指定 ``embedded_services``。更复杂的示例请参阅本指南后面的对应小节。
有关 QEMU 的安装和配置,请参阅页面 :doc:`../api-guides/tools/qemu`。
``pytest.mark.host_test`` 的弃用说明
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``pytest.mark.host_test`` 已不再需要,也不应再添加到新的测试用例中。
对于 Linux target 测试用例和 QEMU 测试用例,相关行为会由测试框架动态处理。尤其是在简单的纯 Linux 或纯 QEMU 场景下,embedded services 会被自动选择。
使用不同的 sdkconfig 文件运行相同的应用程序
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -208,6 +254,49 @@ ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自
* ``esp32.foo.test_foo_bar``
* ``esp32s2.bar.test_foo_bar``
.. _pytest-same-app-different-running-environments:
在不同运行环境中运行相同的应用程序
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
有时,同一个应用程序需要在不同的运行环境中进行验证,例如在 Linux target 的主机上、真实硬件上,或在 QEMU 中运行。如果单独使用 ``@pytest.mark.qemu`` 测试还不够,可以在一个 ``idf_parametrize`` 装饰器中组合 ``target``、``config`` 和 ``embedded_services``,并为每种情况附加所需的 marker。
下面的示例改编自 :idf_file:`components/console/test_apps/console/pytest_console.py`:
.. code-block:: python
@idf_parametrize(
'target,config,embedded_services,markers',
[
('linux', 'defaults', 'idf', ()),
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
],
indirect=['target', 'config', 'embedded_services'],
)
def test_console_repl(dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
这会为同一个应用程序生成 4 个测试用例:
* 在 Linux 主机上使用 ``idf`` service 运行
* 在 ESP32 硬件上使用 ``esp,idf`` services 运行
* 在 ESP32-C3 硬件上使用 ``esp,idf`` services 运行
* 在 QEMU 中以 ESP32 为目标,使用 ``idf,qemu`` services 运行
在本地运行时,可以按需只选择某一种运行环境:
.. code-block:: shell
$ pytest --target linux
$ pytest -m qemu
$ pytest -m qemu --target esp32
``pytest --target linux`` 只选择 Linux target 的测试用例。``pytest -m qemu`` 选择所有带有 QEMU marker 的测试用例。``pytest -m qemu --target esp32`` 会进一步把范围限制为目标芯片为 ESP32 的 QEMU 测试用例。
当测试逻辑相同,但执行环境不同的时候,可使用此模式。
测试串行输出
^^^^^^^^^^^^^^^^
@@ -5,7 +5,6 @@ from pytest_embedded_idf.utils import idf_parametrize
from pytest_embedded_qemu.dut import QemuDut
@pytest.mark.host_test
@pytest.mark.qemu
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_pytest_host(dut: QemuDut) -> None:
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: CC0-1.0
import hashlib
import logging
from typing import Callable
from collections.abc import Callable
import pytest
from pytest_embedded_idf.dut import IdfDut
@@ -45,7 +45,6 @@ def verify_elf_sha256_embedding(app: QemuApp, sha256_reported: str) -> None:
raise ValueError('ELF file SHA256 mismatch')
@pytest.mark.host_test
@pytest.mark.qemu
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_hello_world_host(app: QemuApp, dut: QemuDut) -> None:
@@ -29,6 +29,8 @@ from pytest_embedded_idf.utils import idf_parametrize
# Testbed configuration
ETHVM_ENDNODE_USER = 'ci.ethvm'
ETHERNET_TEST_USER = os.getenv('ETHERNET_TEST_USER')
ETHERNET_TEST_PASSWORD = os.getenv('ETHERNET_TEST_PASSWORD')
BR_PORTS_NUM = 2
IPERF_BW_LIM = 6
@@ -636,5 +638,10 @@ def setup_test_environment() -> Generator[None, None, None]:
indirect=True,
)
@idf_parametrize('target', ['esp32'], indirect=['target'])
@pytest.mark.parametrize(
'dev_user, dev_password',
[(ETHERNET_TEST_USER, ETHERNET_TEST_PASSWORD)],
indirect=True,
)
def test_esp_eth_bridge(dut: Dut, dev_user: str, dev_password: str) -> None:
eth_bridge_test(dut, dev_user, dev_password)
@@ -1,4 +1,4 @@
# 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 logging
import os
@@ -52,7 +52,6 @@ def test_examples_protocol_https_x509_bundle_dynamic_buffer(dut: Dut) -> None:
@pytest.mark.qemu
@pytest.mark.host_test
@pytest.mark.parametrize(
'config',
[
@@ -900,7 +900,6 @@ def test_examples_efuse_with_virt_sb_v2_and_fe(dut: Dut) -> None:
dut.expect('example: Done')
@pytest.mark.host_test
@pytest.mark.qemu
@pytest.mark.parametrize(
'config',
@@ -1110,7 +1110,6 @@ def test_examples_protocol_advanced_https_ota_example_openssl_aligned_bin(dut: D
@pytest.mark.qemu
@pytest.mark.nightly_run
@pytest.mark.host_test
@pytest.mark.parametrize(
'qemu_extra_args',
[
@@ -1166,7 +1165,6 @@ def test_examples_protocol_advanced_https_ota_example_verify_min_chip_revision(d
@pytest.mark.qemu
@pytest.mark.nightly_run
@pytest.mark.host_test
@pytest.mark.parametrize(
'qemu_extra_args',
[
+10 -2
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
@@ -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}
+5 -104
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
+51 -1
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
+15
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
+7
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
@@ -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})
+11
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:
+1 -1
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
@@ -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',
@@ -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'])
@@ -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)
@@ -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())
@@ -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')
@@ -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:
+3
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
@@ -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']
+2 -2
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')