diff --git a/.gitlab/ci/common.yml b/.gitlab/ci/common.yml index 51982b2d806..8b2717d0422 100644 --- a/.gitlab/ci/common.yml +++ b/.gitlab/ci/common.yml @@ -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` # ############# diff --git a/.gitlab/ci/host-test.yml b/.gitlab/ci/host-test.yml index 3baf3247a1e..7e7044c88b2 100644 --- a/.gitlab/ci/host-test.yml +++ b/.gitlab/ci/host-test.yml @@ -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} diff --git a/.gitlab/ci/rules.yml b/.gitlab/ci/rules.yml index dd5dfa13f5c..e1a2fc88470 100644 --- a/.gitlab/ci/rules.yml +++ b/.gitlab/ci/rules.yml @@ -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" diff --git a/.gitlab/ci/test-win.yml b/.gitlab/ci/test-win.yml index 65185645c0e..f592716cfd9 100644 --- a/.gitlab/ci/test-win.yml +++ b/.gitlab/ci/test-win.yml @@ -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} diff --git a/.idf_build_apps.toml b/.idf_build_apps.toml index 25d8d9d5900..b2ca5d4b340 100644 --- a/.idf_build_apps.toml +++ b/.idf_build_apps.toml @@ -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 diff --git a/.idf_ci.toml b/.idf_ci.toml index f95c0332052..c79ce7dc432 100644 --- a/.idf_ci.toml +++ b/.idf_ci.toml @@ -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 = [ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 592da90bef4..888ebe27160 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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(\..*)?$' diff --git a/components/bootloader_support/test_apps/bootloader_support/pytest_bootloader_support.py b/components/bootloader_support/test_apps/bootloader_support/pytest_bootloader_support.py index 59e3d4cf6a1..7bb24e96908 100644 --- a/components/bootloader_support/test_apps/bootloader_support/pytest_bootloader_support.py +++ b/components/bootloader_support/test_apps/bootloader_support/pytest_bootloader_support.py @@ -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: diff --git a/components/bt/common/ble_log/extension/log_compression/tests/test_utils.py b/components/bt/common/ble_log/extension/log_compression/tests/test_utils.py index a576872fe61..6354bb5c0e6 100644 --- a/components/bt/common/ble_log/extension/log_compression/tests/test_utils.py +++ b/components/bt/common/ble_log/extension/log_compression/tests/test_utils.py @@ -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() diff --git a/components/console/test_apps/console/pytest_console.py b/components/console/test_apps/console/pytest_console.py index df7901ff366..ec6d281a045 100644 --- a/components/console/test_apps/console/pytest_console.py +++ b/components/console/test_apps/console/pytest_console.py @@ -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>') diff --git a/components/efuse/test_apps/pytest_efuse.py b/components/efuse/test_apps/pytest_efuse.py index 7d9c5367359..3fb48bf0b8e 100644 --- a/components/efuse/test_apps/pytest_efuse.py +++ b/components/efuse/test_apps/pytest_efuse.py @@ -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() diff --git a/components/esp_event/test_apps/pytest_esp_event.py b/components/esp_event/test_apps/pytest_esp_event.py index 20d59bb2b9e..5840e9bce33 100644 --- a/components/esp_event/test_apps/pytest_esp_event.py +++ b/components/esp_event/test_apps/pytest_esp_event.py @@ -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( diff --git a/components/esp_ringbuf/test_apps/pytest_esp_ringbuf.py b/components/esp_ringbuf/test_apps/pytest_esp_ringbuf.py index ca44dcfa16c..f27087d9c9d 100644 --- a/components/esp_ringbuf/test_apps/pytest_esp_ringbuf.py +++ b/components/esp_ringbuf/test_apps/pytest_esp_ringbuf.py @@ -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', diff --git a/components/esp_rom/test_apps/rom_tests/pytest_esp_rom.py b/components/esp_rom/test_apps/rom_tests/pytest_esp_rom.py index 0bc7bfef1a0..b3e41e01af8 100644 --- a/components/esp_rom/test_apps/rom_tests/pytest_esp_rom.py +++ b/components/esp_rom/test_apps/rom_tests/pytest_esp_rom.py @@ -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: diff --git a/components/heap/test_apps/heap_tests/pytest_heap.py b/components/heap/test_apps/heap_tests/pytest_heap.py index c4eb959a76d..08225beeacf 100644 --- a/components/heap/test_apps/heap_tests/pytest_heap.py +++ b/components/heap/test_apps/heap_tests/pytest_heap.py @@ -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', diff --git a/components/pthread/test_apps/pthread_unity_tests/pytest_pthread_unity_tests.py b/components/pthread/test_apps/pthread_unity_tests/pytest_pthread_unity_tests.py index 5e6aba09c9f..62d2278b30b 100644 --- a/components/pthread/test_apps/pthread_unity_tests/pytest_pthread_unity_tests.py +++ b/components/pthread/test_apps/pthread_unity_tests/pytest_pthread_unity_tests.py @@ -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: diff --git a/conftest.py b/conftest.py index d58c8b38bf2..307ebdbf198 100644 --- a/conftest.py +++ b/conftest.py @@ -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 ################## diff --git a/docs/en/api-guides/tools/idf-tools.rst b/docs/en/api-guides/tools/idf-tools.rst index 8472393d974..0bfa0e79a2b 100644 --- a/docs/en/api-guides/tools/idf-tools.rst +++ b/docs/en/api-guides/tools/idf-tools.rst @@ -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 diff --git a/docs/en/contribute/esp-idf-tests-with-pytest.rst b/docs/en/contribute/esp-idf-tests-with-pytest.rst index 864900d701a..caf409b5655 100644 --- a/docs/en/contribute/esp-idf-tests-with-pytest.rst +++ b/docs/en/contribute/esp-idf-tests-with-pytest.rst @@ -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 `__. +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 ` 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) --------------------------------- diff --git a/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst b/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst index 82cb65cd3b4..d9a92bb1222 100644 --- a/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst +++ b/docs/zh_CN/contribute/esp-idf-tests-with-pytest.rst @@ -120,6 +120,52 @@ ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自 关于测试函数,使用了一个 ``dut`` fixture。在单一 DUT 测试用例中,``dut`` fixture 是 ``IdfDut`` 类的一个实例,对于多个 DUT 测试用例,它是 ``IdfDut`` 实例的一个元组。有关 ``IdfDut`` 类的更多详细信息,请参阅 `pytest-embedded IdfDut API 参考 `__。 +在 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:`在不同运行环境中运行相同的应用程序 ` 小节。 + +.. 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 测试用例。 + +当测试逻辑相同,但执行环境不同的时候,可使用此模式。 + 测试串行输出 ^^^^^^^^^^^^^^^^ diff --git a/examples/build_system/cmake/import_lib/pytest_import_lib.py b/examples/build_system/cmake/import_lib/pytest_import_lib.py index 95f86b0807f..4bf5203c624 100644 --- a/examples/build_system/cmake/import_lib/pytest_import_lib.py +++ b/examples/build_system/cmake/import_lib/pytest_import_lib.py @@ -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: diff --git a/examples/get-started/hello_world/pytest_hello_world.py b/examples/get-started/hello_world/pytest_hello_world.py index 64f6bb8f5fb..e3ae95b6542 100644 --- a/examples/get-started/hello_world/pytest_hello_world.py +++ b/examples/get-started/hello_world/pytest_hello_world.py @@ -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: diff --git a/examples/network/bridge/pytest_example_bridge.py b/examples/network/bridge/pytest_example_bridge.py index 0e805aeda15..cb223514c73 100644 --- a/examples/network/bridge/pytest_example_bridge.py +++ b/examples/network/bridge/pytest_example_bridge.py @@ -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) diff --git a/examples/protocols/https_x509_bundle/pytest_https_x509_bundle.py b/examples/protocols/https_x509_bundle/pytest_https_x509_bundle.py index 8b4b89ca8f1..ddc6d818426 100644 --- a/examples/protocols/https_x509_bundle/pytest_https_x509_bundle.py +++ b/examples/protocols/https_x509_bundle/pytest_https_x509_bundle.py @@ -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', [ diff --git a/examples/system/efuse/pytest_system_efuse_example.py b/examples/system/efuse/pytest_system_efuse_example.py index 1a0708e6645..bfea11cd0ee 100644 --- a/examples/system/efuse/pytest_system_efuse_example.py +++ b/examples/system/efuse/pytest_system_efuse_example.py @@ -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', diff --git a/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py b/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py index 7ad0897509a..0743975cfe0 100644 --- a/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py +++ b/examples/system/ota/advanced_https_ota/pytest_advanced_ota.py @@ -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', [ diff --git a/tools/ci/check_public_headers.py b/tools/ci/check_public_headers.py index a8c3891c8c6..cba9b1b5768 100644 --- a/tools/ci/check_public_headers.py +++ b/tools/ci/check_public_headers.py @@ -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 diff --git a/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml b/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml index 0e50de03a34..f5fc1cbbea6 100644 --- a/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml +++ b/tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml @@ -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} diff --git a/tools/ci/idf_ci_local/app.py b/tools/ci/idf_ci_local/app.py index 4d1f177e59a..4a6fbd8673b 100644 --- a/tools/ci/idf_ci_local/app.py +++ b/tools/ci/idf_ci_local/app.py @@ -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 diff --git a/tools/ci/idf_pytest/plugin.py b/tools/ci/idf_pytest/plugin.py index d8def228fdf..3d5c3be2965 100644 --- a/tools/ci/idf_pytest/plugin.py +++ b/tools/ci/idf_pytest/plugin.py @@ -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 diff --git a/tools/cmake/build.cmake b/tools/cmake/build.cmake index c323d9eac1d..2dab01822b7 100644 --- a/tools/cmake/build.cmake +++ b/tools/cmake/build.cmake @@ -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 diff --git a/tools/cmake/idf.cmake b/tools/cmake/idf.cmake index 384401cd76c..06600490799 100644 --- a/tools/cmake/idf.cmake +++ b/tools/cmake/idf.cmake @@ -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 diff --git a/tools/cmake/scripts/component_get_requirements.cmake b/tools/cmake/scripts/component_get_requirements.cmake index bc94112e52e..f4f4079c13b 100644 --- a/tools/cmake/scripts/component_get_requirements.cmake +++ b/tools/cmake/scripts/component_get_requirements.cmake @@ -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}) diff --git a/tools/idf_extra_components.yml b/tools/idf_extra_components.yml new file mode 100644 index 00000000000..c851252cb46 --- /dev/null +++ b/tools/idf_extra_components.yml @@ -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: diff --git a/tools/requirements/requirements.ci.txt b/tools/requirements/requirements.ci.txt index 9852754ad66..846ca3a4866 100644 --- a/tools/requirements/requirements.ci.txt +++ b/tools/requirements/requirements.ci.txt @@ -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 diff --git a/tools/test_apps/security/secure_boot/pytest_secure_boot.py b/tools/test_apps/security/secure_boot/pytest_secure_boot.py index e767ee71171..57e96718575 100644 --- a/tools/test_apps/security/secure_boot/pytest_secure_boot.py +++ b/tools/test_apps/security/secure_boot/pytest_secure_boot.py @@ -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', diff --git a/tools/test_apps/security/signed_app_no_secure_boot/pytest_signed_app_no_secure_boot.py b/tools/test_apps/security/signed_app_no_secure_boot/pytest_signed_app_no_secure_boot.py index 9dde2e391a5..101f971a65d 100644 --- a/tools/test_apps/security/signed_app_no_secure_boot/pytest_signed_app_no_secure_boot.py +++ b/tools/test_apps/security/signed_app_no_secure_boot/pytest_signed_app_no_secure_boot.py @@ -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']) diff --git a/tools/test_apps/storage/std_filesystem/pytest_std_filesystem.py b/tools/test_apps/storage/std_filesystem/pytest_std_filesystem.py index 75676116044..86031bc508f 100644 --- a/tools/test_apps/storage/std_filesystem/pytest_std_filesystem.py +++ b/tools/test_apps/storage/std_filesystem/pytest_std_filesystem.py @@ -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) diff --git a/tools/test_apps/system/esp_intr_dump/pytest_esp_intr_dump.py b/tools/test_apps/system/esp_intr_dump/pytest_esp_intr_dump.py index cccfdbedeb9..e03da57cc20 100644 --- a/tools/test_apps/system/esp_intr_dump/pytest_esp_intr_dump.py +++ b/tools/test_apps/system/esp_intr_dump/pytest_esp_intr_dump.py @@ -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()) diff --git a/tools/test_apps/system/mmu_page_size/pytest_mmu_page_size.py b/tools/test_apps/system/mmu_page_size/pytest_mmu_page_size.py index 4213f2fec0f..88492e046f2 100644 --- a/tools/test_apps/system/mmu_page_size/pytest_mmu_page_size.py +++ b/tools/test_apps/system/mmu_page_size/pytest_mmu_page_size.py @@ -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') diff --git a/tools/test_apps/system/unicore_bootloader/pytest_unicore_bootloader.py b/tools/test_apps/system/unicore_bootloader/pytest_unicore_bootloader.py index 6a88260bf6e..f3bdd940b71 100644 --- a/tools/test_apps/system/unicore_bootloader/pytest_unicore_bootloader.py +++ b/tools/test_apps/system/unicore_bootloader/pytest_unicore_bootloader.py @@ -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: diff --git a/tools/test_build_system/pytest.ini b/tools/test_build_system/pytest.ini index eeac16e6d7b..6f08513e182 100644 --- a/tools/test_build_system/pytest.ini +++ b/tools/test_build_system/pytest.ini @@ -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 diff --git a/tools/test_build_system/test_component_manager.py b/tools/test_build_system/test_component_manager.py index 5434bc6745c..d11288fe2c6 100644 --- a/tools/test_build_system/test_component_manager.py +++ b/tools/test_build_system/test_component_manager.py @@ -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'] diff --git a/tools/test_idf_py/test_idf_py.py b/tools/test_idf_py/test_idf_py.py index 60456386c53..f5807029762 100755 --- a/tools/test_idf_py/test_idf_py.py +++ b/tools/test_idf_py/test_idf_py.py @@ -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')