ci: apply common-scripts CI refactor

Squashes these original commits for release/v5.4 backport traceability:
- 9b289dc5ad6 ci: apply idf-ci 1.x
- 381df980d57 ci: remove pip-cache and other unused jobs
- 32375a0a15e ci: apply common-scripts CI refactor
This commit is contained in:
Fu Hanxi
2026-08-17 14:13:15 +02:00
parent 78acf95a61
commit b47f9850c7
156 changed files with 2376 additions and 7642 deletions

View File

@@ -1,44 +1,26 @@
workflow:
rules:
# Disable those non-protected push triggered pipelines
- if: '$CI_COMMIT_REF_NAME != "master" && $CI_COMMIT_BRANCH !~ /^release\/v/ && $CI_COMMIT_TAG !~ /^v\d+\.\d+(\.\d+)?($|-)/ && $CI_COMMIT_TAG !~ /^qa-test/ && $CI_PIPELINE_SOURCE == "push"'
when: never
# when running merged result pipelines, CI_COMMIT_SHA represents the temp commit it created.
# Please use PIPELINE_COMMIT_SHA at all places that require a commit sha of the original commit.
- if: $CI_OPEN_MERGE_REQUESTS != null
variables:
PIPELINE_COMMIT_SHA: $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA
IS_MR_PIPELINE: 1
- if: $CI_OPEN_MERGE_REQUESTS == null
variables:
PIPELINE_COMMIT_SHA: $CI_COMMIT_SHA
IS_MR_PIPELINE: 0
- when: always
# Place the default settings in `.gitlab/ci/common.yml` instead
include:
- project: "ci/actions/common"
ref: "v2"
file:
- "templates/idf/integration-test.yml"
- '.gitlab/ci/danger.yml'
- '.gitlab/ci/common.yml'
- '.gitlab/ci/rules.yml'
- ".gitlab/ci/manual_gate.yml"
- '.gitlab/ci/upload_cache.yml'
- '.gitlab/ci/static-code-analysis.yml'
- '.gitlab/ci/pre_commit.yml'
- '.gitlab/ci/pre_check.yml'
- '.gitlab/ci/build.yml'
- '.gitlab/ci/host-test.yml'
- '.gitlab/ci/deploy.yml'
- '.gitlab/ci/post_deploy.yml'
- '.gitlab/ci/retry_failed_jobs.yml'
- '.gitlab/ci/test-win.yml'
- project: "ci/actions/common"
file:
- "templates/stable/default-workflow.yml"
- "templates/idf/pre_check.yml"
- "templates/idf/build.yml"
- "templates/idf/deploy-github.yml"
- "templates/idf/deploy-docs.yml"
- "templates/idf/integration-test.yml"
- project: "ci/actions/common"
ref: "v2"
file: "templates/idf/sync-files.yml"
inputs:
job_name: "sync-pre-commit-config"
remote_repo_url: "https://__token__:${CI_SYNC_REPO_TOKEN}@${CI_SERVER_FQDN}/ci/images.git"
ref_name: "ci/sync_idf_pre_commit_config"
file_mappings:
- ".pre-commit-config.yaml:pre-commit/idf/.pre-commit-config.v5.4.yaml"
changes:
- ".pre-commit-config.yaml"
- project: "ci/actions/common"
file: "templates/idf/build-docs.yml"
inputs:
@@ -50,3 +32,16 @@ include:
DOCTGT: "esp32"
- DOCLANG: "zh_CN"
DOCTGT: "esp32p4"
- ".gitlab/ci/danger.yml"
- ".gitlab/ci/common.yml" # has to be placed after danger.yml, since danger.yml defined stages.
- ".gitlab/ci/rules.yml"
# by stages
- ".gitlab/ci/manual_gate.yml"
- ".gitlab/ci/upload_cache.yml"
- ".gitlab/ci/pre_check.yml"
- ".gitlab/ci/pre_deploy.yml"
- ".gitlab/ci/static-code-analysis.yml"
- ".gitlab/ci/pre_commit.yml"
- ".gitlab/ci/build.yml"
- ".gitlab/ci/host-test.yml"
- ".gitlab/ci/test-win.yml"

View File

@@ -23,12 +23,7 @@
- [Grammar](#grammar)
- [Special Rules](#special-rules)
- [Upload/Download Artifacts to Internal Minio Server](#uploaddownload-artifacts-to-internal-minio-server)
- [Users Without Access to Minio](#users-without-access-to-minio)
- [Users With Access to Minio](#users-with-access-to-minio)
- [Env Vars for Minio](#env-vars-for-minio)
- [Artifacts Types and File Patterns](#artifacts-types-and-file-patterns)
- [Upload](#upload)
- [Download](#download)
- [Revision Marker Naming Rules](#revision-marker-naming-rules)
## General Workflow
@@ -145,11 +140,11 @@ check if there's a suitable `.if-<if-anchor-you-need>` anchor
1. if there is, create a rule following [`rules` Template Naming Rules](#rules-template-naming-rules).For detail information, please refer to [GitLab Documentation `rules-if`](https://docs.gitlab.com/ee/ci/yaml/README.html#rulesif). Here's an example.
```yaml
.rules:patterns:python-files:
.rules:patterns:clang_tidy:
rules:
- <<: *if-protected
- <<: *if-dev-push
changes: *patterns-python-files
changes: *patterns-c-files
```
2. if there isn't
@@ -250,76 +245,15 @@ We're using the latest version of [idf-build-apps][idf-build-apps]. Please refer
In ESP-IDF CI, there's a few more special rules are additionally supported to disable the check app dependencies feature:
- Add MR labels `BUILD_AND_TEST_ALL_APPS`
- Pipeline variable `IDF_CI_SELECT_ALL_PYTEST_CASES=1`
- Run in protected branches
## Upload/Download Artifacts to Internal Minio Server
### Users Without Access to Minio
Please refer to the documentation [here](https://docs.espressif.com/projects/idf-ci/en/latest/guides/cli.html)
If you don't have access to the internal Minio server, you can still download the artifacts from the shared link in the job log.
## Revision Marker Naming Rules
The log will look like this:
When specifying chip revisions for testing markers, configurations or filenames, strictly follow the format `esp32<series>_rev<number>` (or similarly `<series>_rev<number>` when brief).
```shell
Pipeline ID : 587355
Job name : build_clang_test_apps_esp32
Job ID : 40272275
Created archive file: 40272275.zip, uploading as 587355/build_dir_without_map_and_elf_files/build_clang_test_apps_esp32/40272275.zip
Please download the archive file includes build_dir_without_map_and_elf_files from [INTERNAL_URL]
```
### Users With Access to Minio
#### Env Vars for Minio
Minio takes these env vars to connect to the server:
- `IDF_S3_SERVER`
- `IDF_S3_ACCESS_KEY`
- `IDF_S3_SECRET_KEY`
- `IDF_S3_BUCKET`
#### Artifacts Types and File Patterns
The artifacts types and corresponding file patterns are defined in tools/ci/artifacts_handler.py, inside `ArtifactType` and `TYPE_PATTERNS_DICT`.
#### Upload
```shell
python tools/ci/artifacts_handler.py upload
```
will upload the files that match the file patterns to minio object storage with name:
`<pipeline_id>/<artifact_type>/<job_name>/<job_id>.zip`
For example, job 39043328 will upload these four files:
- `575500/map_and_elf_files/build_pytest_examples_esp32/39043328.zip`
- `575500/build_dir_without_map_and_elf_files/build_pytest_examples_esp32/39043328.zip`
- `575500/logs/build_pytest_examples_esp32/39043328.zip`
- `575500/size_reports/build_pytest_examples_esp32/39043328.zip`
#### Download
You may run
```shell
python tools/ci/artifacts_handler.py download --pipeline_id <pipeline_id>
```
to download all files of the pipeline, or
```shell
python tools/ci/artifacts_handler.py download --pipeline_id <pipeline_id> --job_name <job_name_or_pattern>
```
to download all files with the specified job name or pattern, or
```shell
python tools/ci/artifacts_handler.py download --pipeline_id <pipeline_id> --job_name <job_name_or_pattern> --type <artifact_type> <artifact_type> ...
```
to download all files with the specified job name or pattern and artifact type(s).
You may check all detailed documentation with `python tools/ci/artifacts_handler.py download -h`
> e.g., `esp32_rev3`, `esp32c2_rev2`, `esp32c3_rev1`, `esp32p4_rev1`, `esp32c5_rev1`

View File

@@ -1,55 +1,13 @@
.build_template:
stage: build
extends:
- .after_script:build:ccache-show-stats:upload-failed-job-logs
- .after_script:build
image: $ESP_ENV_IMAGE
tags:
- build
# build only on shiny servers since shiny storage server is at the same location
- shiny
tags: [build, shiny]
variables:
# Enable ccache for all build jobs. See configure_ci_environment.sh for more ccache related settings.
IDF_CCACHE_ENABLE: "1"
dependencies: []
.build_cmake_clang_template:
extends:
- .build_template
- .before_script:build
- .after_script:build:ccache-show-stats
dependencies: # set dependencies to null to avoid missing artifacts issue
needs:
- job: fast_template_app
artifacts: false
- pipeline_variables
artifacts:
paths:
# The other artifacts patterns are defined under tools/ci/artifacts_handler.py
# Now we're uploading/downloading the binary files from our internal storage server
#
# keep the log file to help debug
- "**/build*/build_log.txt"
# keep the size info to help track the binary size
- size_info.txt
- "**/build*/size.json"
expire_in: 1 week
when: always
variables:
IDF_TOOLCHAIN: clang
TEST_BUILD_OPTS_EXTRA: ""
TEST_DIR: tools/test_apps/system/clang_build_test
PYTEST_IGNORE_COLLECT_IMPORT_ERROR: "1"
script:
# CI specific options start from "--parallel-count xxx". could ignore when running locally
- run_cmd python tools/ci/ci_build_apps.py $TEST_DIR -v
-t $IDF_TARGET
--copy-sdkconfig
--parallel-count ${CI_NODE_TOTAL:-1}
--parallel-index ${CI_NODE_INDEX:-1}
--modified-components ${MR_MODIFIED_COMPONENTS}
--modified-files ${MR_MODIFIED_FILES}
$TEST_BUILD_OPTS_EXTRA
- python tools/ci/artifacts_handler.py upload
######################
# build_template_app #
@@ -57,18 +15,11 @@
.build_template_app_template:
extends:
- .build_template
- .before_script:build
variables:
LOG_PATH: "${CI_PROJECT_DIR}/log_template_app"
BUILD_PATH: "${CI_PROJECT_DIR}/build_template_app"
BUILD_DIR: "${BUILD_PATH}/@t/@w"
BUILD_LOG_CMAKE: "${LOG_PATH}/cmake_@t_@w.txt"
BUILD_COMMAND_ARGS: ""
artifacts:
paths:
- log_template_app/*
- size_info.txt
- build_template_app/**/size.json
- build_template_app/**/size*.json
expire_in: 1 week
when: always
script:
@@ -82,224 +33,15 @@
# Only do the default cmake build for each target, remaining part are done in the build_template_app job
- tools/ci/build_template_app.sh ${BUILD_COMMAND_ARGS}
# build-related-pre-check-jobs ------------------------------------------------
# Build at least one project for each target at earliest stage to reduce build cost for obvious failing commits
fast_template_app:
extends:
- .build_template_app_template
- .rules:build:target_test
- .rules:build
stage: pre_check
tags: [fast_run, shiny]
variables:
BUILD_COMMAND_ARGS: "-p"
#------------------------------------------------------------------------------
#######################
# gnu_static_analyzer #
#######################
gcc_static_analyzer:
extends:
- .build_template_app_template
- .rules:build:target_test
stage: pre_check
tags: [build, shiny]
variables:
CI_CCACHE_DISABLE: 1
ANALYZING_APP: "examples/get-started/hello_world"
script:
- echo "CONFIG_COMPILER_STATIC_ANALYZER=y" >> ${ANALYZING_APP}/sdkconfig.defaults
- python -m idf_build_apps build -v -p ${ANALYZING_APP} -t all
########################################
# Clang Build Apps Without Tests Cases #
########################################
build_clang_test_apps_esp32:
extends:
- .build_cmake_clang_template
- .rules:build
variables:
IDF_TARGET: esp32
build_clang_test_apps_esp32s2:
extends:
- .build_cmake_clang_template
- .rules:build
variables:
IDF_TARGET: esp32s2
build_clang_test_apps_esp32s3:
extends:
- .build_cmake_clang_template
- .rules:build
variables:
IDF_TARGET: esp32s3
.build_clang_test_apps_riscv:
extends:
- .build_cmake_clang_template
variables:
# https://reviews.llvm.org/D90108.
# GNU 'as' lets .weak override .globl since binutils-gdb
# https://github.com/bminor/binutils-gdb/commit/5ca547dc2399a0a5d9f20626d4bf5547c3ccfddd (1996)
# while MC lets the last directive win (PR38921).
# For RISCV chips we use integrated assembler by default, so suppress this warning to pass CI pipeline.
TEST_BUILD_OPTS_EXTRA: "--ignore-warning-str 'changed binding to STB_WEAK'"
build_clang_test_apps_esp32c3:
extends:
- .build_clang_test_apps_riscv
- .rules:build
variables:
IDF_TARGET: esp32c3
build_clang_test_apps_esp32c2:
extends:
- .build_clang_test_apps_riscv
- .rules:build
variables:
IDF_TARGET: esp32c2
build_clang_test_apps_esp32c6:
extends:
- .build_clang_test_apps_riscv
- .rules:build
variables:
IDF_TARGET: esp32c6
build_clang_test_apps_esp32c5:
extends:
- .build_clang_test_apps_riscv
- .rules:build
variables:
IDF_TARGET: esp32c5
build_clang_test_apps_esp32h2:
extends:
- .build_clang_test_apps_riscv
- .rules:build
variables:
IDF_TARGET: esp32h2
build_clang_test_apps_esp32p4:
extends:
- .build_clang_test_apps_riscv
- .rules:build
variables:
IDF_TARGET: esp32p4
######################
# Build System Tests #
######################
.test_build_system_template:
stage: host_test
extends:
- .build_template
- .rules:build:check
dependencies: # set dependencies to null to avoid missing artifacts issue
needs:
- job: fast_template_app
artifacts: false
optional: true
artifacts:
reports:
junit: XUNIT_RESULT.xml
paths:
- XUNIT_RESULT.xml
- test_build_system
expire_in: 1 week
when: always
script:
- ${IDF_PATH}/tools/ci/test_configure_ci_environment.sh
- cd ${IDF_PATH}/tools/test_build_system
- python ${IDF_PATH}/tools/ci/get_known_failure_cases_file.py
- pytest
--cleanup-idf-copy
--parallel-count ${CI_NODE_TOTAL:-1}
--parallel-index ${CI_NODE_INDEX:-1}
--work-dir ${CI_PROJECT_DIR}/test_build_system
--junitxml ${CI_PROJECT_DIR}/XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
pytest_build_system:
extends: .test_build_system_template
parallel: 3
pytest_build_system_macos:
extends:
- .test_build_system_template
- .before_script:build:macos
- .after_script:build:macos:upload-failed-job-logs:ccache-show-stats
- .rules:build:macos
tags:
- macos_shell
parallel: 3
variables:
PYENV_VERSION: "3.8"
# CCACHE_DIR: "/cache/idf_ccache". On macOS, you cannot write to this folder due to insufficient permissions.
CCACHE_DIR: "" # ccache will use "$HOME/Library/Caches/ccache".
.test_build_system_minimal_cmake_template:
extends: .test_build_system_template
variables:
INSTALL_EXTRA_TOOLS: cmake@3.16.3
script:
- MINIMAL_SUPPORTED_CMAKE_VERSION=$(echo "${INSTALL_EXTRA_TOOLS}" | sed -n 's/.*cmake@\([0-9.]*\).*/\1/p')
- export PATH=$(echo "$PATH" | sed -E "s|/tools/cmake/[0-9.]+|/tools/cmake/${MINIMAL_SUPPORTED_CMAKE_VERSION}|")
- ACTUAL_CMAKE_VERSION=$(cmake --version | head -n1 | awk '{print $3}')
- |
if [ "${ACTUAL_CMAKE_VERSION}" != "${MINIMAL_SUPPORTED_CMAKE_VERSION}" ]; then
echo "ERROR: Wrong minimal CMake version! Detected: ${ACTUAL_CMAKE_VERSION}, but should be: ${MINIMAL_SUPPORTED_CMAKE_VERSION}"
exit 1
fi
- ${IDF_PATH}/tools/ci/test_configure_ci_environment.sh
- cd ${IDF_PATH}/tools/test_build_system
- python ${IDF_PATH}/tools/ci/get_known_failure_cases_file.py
- pytest
-k cmake
--cleanup-idf-copy
--work-dir ${CI_PROJECT_DIR}/test_build_system
--junitxml ${CI_PROJECT_DIR}/XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
pytest_build_system_minimal_cmake:
extends: .test_build_system_minimal_cmake_template
pytest_build_system_macos_minimal_cmake:
extends:
- .test_build_system_minimal_cmake_template
- .before_script:build:macos
- .after_script:build:macos:upload-failed-job-logs:ccache-show-stats
- .rules:build:macos
tags:
- macos_shell
variables:
PYENV_VERSION: "3.8"
# CCACHE_DIR: "/cache/idf_ccache". On macOS, you cannot write to this folder due to insufficient permissions.
CCACHE_DIR: "" # ccache will use "$HOME/Library/Caches/ccache".
INSTALL_EXTRA_TOOLS: ninja cmake@3.16.3
build_docker:
extends:
- .before_script:minimal
- .rules:build:docker
stage: host_test
needs:
- job: manual_gate
optional: true
image: espressif/docker-builder:1
tags: [shiny, dind]
variables:
DOCKER_TMP_IMAGE_NAME: "idf_tmp_image"
script:
# use different image tag for different concurrent job
- export DOCKER_TMP_IMAGE="${DOCKER_TMP_IMAGE_NAME}:${CI_CONCURRENT_ID:-latest}"
- export DOCKER_BUILD_ARGS="--build-arg IDF_CLONE_URL=${CI_REPOSITORY_URL} --build-arg IDF_CLONE_BRANCH_OR_TAG=${CI_COMMIT_REF_NAME} --build-arg IDF_CHECKOUT_REF=${CI_COMMIT_TAG:-$CI_COMMIT_SHA} --build-arg IDF_CLONE_SHALLOW=1 ${EXTRA_DOCKER_BUILD_ARGS:-} "
- time docker build --tag ${DOCKER_TMP_IMAGE} ${DOCKER_BUILD_ARGS} tools/docker/
# We can't mount $PWD/examples/get-started/blink into the container, see https://gitlab.com/gitlab-org/gitlab-ce/issues/41227.
# The workaround mentioned there works, but leaves around directories which need to be cleaned up manually.
# Therefore, build a copy of the example located inside the container.
- docker run --rm --workdir /opt/esp/idf/examples/get-started/blink ${DOCKER_TMP_IMAGE} idf.py build
# This job builds template app with permutations of targets and optimization levels
build_template_app:
@@ -307,57 +49,67 @@ build_template_app:
- .build_template_app_template
- .rules:build
stage: host_test
dependencies: # set dependencies to null to avoid missing artifacts issue
needs:
- job: fast_template_app
artifacts: false
########################################
# Clang Build Apps Without Tests Cases #
########################################
.build_cmake_clang_template:
extends:
- .build_template
- .rules:build
needs:
- job: fast_template_app
artifacts: false
- pipeline_variables
artifacts:
paths:
# The other artifacts patterns are defined under .idf_ci.toml
# Now we're uploading/downloading the binary files from our internal storage server
#
# keep the log file to help debug
- "**/build*/build_log.txt"
- "**/build*/size*.json"
expire_in: 1 week
when: always
variables:
IDF_TOOLCHAIN: clang
TEST_BUILD_OPTS_EXTRA: ""
script:
# CI specific options start from "--parallel-count xxx". could ignore when running locally
- run_cmd idf-build-apps build
-p tools/test_apps/system/clang_build_test
-t $IDF_TARGET
--parallel-count ${CI_NODE_TOTAL:-1}
--parallel-index ${CI_NODE_INDEX:-1}
--modified-components ${MR_MODIFIED_COMPONENTS}
--modified-files ${MR_MODIFIED_FILES}
$TEST_BUILD_OPTS_EXTRA
build_clang_test_apps_xtensa:
extends: .build_cmake_clang_template
parallel:
matrix:
- IDF_TARGET: [esp32, esp32s2, esp32s3]
build_clang_test_apps_riscv:
extends: .build_cmake_clang_template
variables:
# https://reviews.llvm.org/D90108.
# GNU 'as' lets .weak override .globl since binutils-gdb
# https://github.com/bminor/binutils-gdb/commit/5ca547dc2399a0a5d9f20626d4bf5547c3ccfddd (1996)
# while MC lets the last directive win (PR38921).
# For RISCV chips we use integrated assembler by default, so suppress this warning to pass CI pipeline.
TEST_BUILD_OPTS_EXTRA: "--ignore-warning-str 'changed binding to STB_WEAK'"
parallel:
matrix:
- IDF_TARGET: [esp32c3, esp32c2, esp32c6, esp32c5, esp32h2, esp32p4]
####################
# Dynamic Pipeline #
####################
generate_build_child_pipeline:
extends:
- .build_template
tags: [fast_run, shiny]
dependencies: # set dependencies to null to avoid missing artifacts issue
needs:
- pipeline_variables
- job: baseline_manifest_sha
optional: true
artifacts:
paths:
- build_child_pipeline.yml
- test_related_apps.txt
- non_test_related_apps.txt
expire_in: 1 week
when: always
variables:
PYTEST_IGNORE_COLLECT_IMPORT_ERROR: "1"
script:
# requires basic pytest dependencies
- run_cmd bash install.sh --enable-pytest
- run_cmd python tools/ci/dynamic_pipelines/scripts/generate_build_child_pipeline.py
build_child_pipeline:
stage: build
needs:
- job: fast_template_app
optional: true
artifacts: false
- pipeline_variables
- generate_build_child_pipeline
variables:
IS_MR_PIPELINE: $IS_MR_PIPELINE
MR_MODIFIED_COMPONENTS: $MR_MODIFIED_COMPONENTS
MR_MODIFIED_FILES: $MR_MODIFIED_FILES
PARENT_PIPELINE_ID: $CI_PIPELINE_ID
BUILD_AND_TEST_ALL_APPS: $BUILD_AND_TEST_ALL_APPS
REPORT_EXIT_CODE: $REPORT_EXIT_CODE
# https://gitlab.com/gitlab-org/gitlab/-/issues/214340
inherit:
variables: false
trigger:
include:
- artifact: build_child_pipeline.yml
job: generate_build_child_pipeline
strategy: depend
# !!!
# configurations moved to ci/actions/common/idf/
# !!!

View File

@@ -11,9 +11,18 @@ stages:
- target_test
- host_test
- test_deploy
- pre_deploy
- deploy
- post_deploy
- retry_failed_jobs
include:
- project: "ci/actions/common"
ref: "v2"
file: "templates/idf/common-scripts.yml"
inputs:
ci_python_constraint_file: "espidf.constraints.v5.4.txt"
macos_image: "registry.hub.docker.com/espressif/macos-idf:sonoma-v5.4"
windows_image: "windows_11_idf_v5_4"
variables:
# System environment
@@ -35,16 +44,12 @@ variables:
# - set GIT_STRATEGY: "clone" to shiny runners
# - set GIT_STRATEGY: "fetch" to brew runners
GIT_STRATEGY: clone
# we will download archive for each submodule instead of clone.
# we don't do "recursive" when fetch submodule as they're not used in CI now.
GIT_SUBMODULE_STRATEGY: none
# since we're using merged-result pipelines, the last commit should work for most cases
GIT_DEPTH: 1
# --no-recurse-submodules: we use cache for submodules
GIT_SUBMODULE_STRATEGY: none # here we use cache for submodules, so we don't need to fetch them every time
# since we're using merged-result pipelines, the last commit should work for most cases
# --prune --prune-tags: in case remote branch or tag is force pushed
GIT_FETCH_EXTRA_FLAGS: "--no-recurse-submodules --prune --prune-tags"
# we're using .cache folder for caches
GIT_CLEAN_FLAGS: -ffdx -e .cache/
LATEST_GIT_TAG: v5.4.4
SUBMODULE_FETCH_TOOL: "tools/ci/ci_fetch_submodule.py"
@@ -63,40 +68,36 @@ variables:
ESP_ENV_IMAGE: "${CI_REGISTRY}/ci/images/idf-v5.4:1"
ESP_IDF_DOC_ENV_IMAGE: "${CI_REGISTRY}/ci/images/idf-v5.4-docs:1"
TARGET_TEST_ENV_IMAGE: "${CI_REGISTRY}/ci/images/idf-v5.4-target-test:1"
SONARQUBE_SCANNER_IMAGE: "${CI_DOCKER_REGISTRY}/sonarqube-scanner:5"
PRE_COMMIT_IMAGE: "${CI_DOCKER_REGISTRY}/esp-idf-pre-commit:1"
# cache python dependencies
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
# Set this variable to the branch of idf-constraints repo in order to test a custom Python constraint file. The
# branch name must be without the remote part ("origin/"). Keep the variable empty in order to use the constraint
# file from https://dl.espressif.com/dl/esp-idf.
CI_PYTHON_CONSTRAINT_BRANCH: ""
# CI_PYTHON_CONSTRAINT_BRANCH could be defined as component input `ci_python_constraint_branch` in common-scripts above
# Update the filename for a specific ESP-IDF release. It is used only with CI_PYTHON_CONSTRAINT_BRANCH.
CI_PYTHON_CONSTRAINT_FILE: "espidf.constraints.v5.4.txt"
# CI_PYTHON_CONSTRAINT_FILE is defined as component input `ci_python_constraint_file` in common-scripts above
# Set this variable to repository name of a Python tool you wish to install and test in the context of ESP-IDF CI.
# Keep the variable empty when not used.
CI_PYTHON_TOOL_REPO: ""
# CI_PYTHON_TOOL_REPO could be defined as component input `ci_python_tool_repo` in common-scripts above
# Set this variable to the branch of a Python tool repo specified in CI_PYTHON_TOOL_REPO. The
# branch name must be without the remote part ("origin/"). Keep the variable empty when not used.
# This is used only if CI_PYTHON_TOOL_REPO is not empty.
CI_PYTHON_TOOL_BRANCH: ""
# CI_PYTHON_TOOL_BRANCH could be defined as component input `ci_python_tool_branch` in common-scripts above
# Set this variable to Clang toolchain distro URL to be used.
# NOTE: We have separate toolchains for Xtensa and RISCV, therefore jobs for one arch will fail.
# This is OK as far as we use CI_CLANG_DISTRO_URL for pre-release tests purposes only.
# Keep the variable empty when not used.
CI_CLANG_DISTRO_URL: ""
# CI_CLANG_DISTRO_URL could be defined as component input `ci_clang_distro_url` in common-scripts above
# Set this variable to specify the file name for the known failure cases.
KNOWN_FAILURE_CASES_FILE_NAME: "5.4.txt"
IDF_CI_BUILD: 1
PYTHONWARNINGS: "ignore"
# ccache settings
# some settings need to set in .gitlab-ci.yml as it takes effect while start-up the job
# https://ccache.dev/manual/latest.html#_configuring_ccache
@@ -106,295 +107,23 @@ variables:
CCACHE_MAXSIZE: "50G"
FF_USE_NEW_BASH_EVAL_STRATEGY: "true"
FORCE_COLOR: "1" # rich print with color
################################################
# `before_script` and `after_script` Templates #
################################################
.common_before_scripts: &common-before_scripts |
source tools/ci/utils.sh
is_based_on_commits $REQUIRED_ANCESTOR_COMMITS
if [[ -n "$IDF_DONT_USE_MIRRORS" ]]; then
export IDF_MIRROR_PREFIX_MAP=
fi
if echo "$CI_MERGE_REQUEST_LABELS" | egrep "(^|,)include_nightly_run(,|$)"; then
export INCLUDE_NIGHTLY_RUN="1"
export NIGHTLY_RUN="1"
fi
# configure cmake related flags
source tools/ci/configure_ci_environment.sh
if [[ "$CI_CCACHE_STATS" == 1 ]] && command -v ccache >/dev/null 2>&1 && [[ -n "$CCACHE_STATSLOG" ]]; then
mkdir -p "$(dirname "$CCACHE_STATSLOG")"
rm -f "$CCACHE_STATSLOG"
fi
# add extra python packages
export PYTHONPATH="$IDF_PATH/tools:$IDF_PATH/tools/esp_app_trace:$IDF_PATH/components/partition_table:$IDF_PATH/tools/ci/python_packages:$PYTHONPATH"
.setup_tools_and_idf_python_venv: &setup_tools_and_idf_python_venv |
# must use after setup_tools_except_target_test
# otherwise the export.sh won't work properly
# download constraint file for dev
if [[ -n "$CI_PYTHON_CONSTRAINT_BRANCH" ]]; then
wget -O /tmp/constraint.txt --header="Authorization:Bearer ${ESPCI_TOKEN}" ${GITLAB_HTTP_SERVER}/api/v4/projects/2581/repository/files/${CI_PYTHON_CONSTRAINT_FILE}/raw?ref=${CI_PYTHON_CONSTRAINT_BRANCH}
mkdir -p "${IDF_TOOLS_PATH:-$HOME/.espressif}"
mv /tmp/constraint.txt "${IDF_TOOLS_PATH:-$HOME/.espressif}/${CI_PYTHON_CONSTRAINT_FILE}"
fi
# Mirror
if [[ -n "$IDF_DONT_USE_MIRRORS" ]]; then
export IDF_MIRROR_PREFIX_MAP=
fi
if [[ "${CI_JOB_STAGE}" != "target_test" ]]; then
section_start "running_install_sh" "Running install.sh"
if [[ "${CI_JOB_STAGE}" == "build_doc" || "${CI_JOB_GROUP_NAME}" == *docs* ]]; then
run_cmd bash install.sh --enable-ci --enable-docs
elif [[ "${CI_JOB_STAGE}" == "build" ]]; then
run_cmd bash install.sh --enable-ci
else
if ! echo "${CI_JOB_NAME}" | egrep ".*pytest.*"; then
run_cmd bash install.sh --enable-ci
else
run_cmd bash install.sh --enable-ci --enable-pytest --enable-test-specific
fi
fi
section_end "running_install_sh"
else
section_start "install_python_env" "Install Python environment"
run_cmd python tools/idf_tools.py install-python-env --features ci,pytest,test-specific
section_end "install_python_env"
fi
if [[ ! -z "$INSTALL_EXTRA_TOOLS" ]]; then
section_start "installing_optional_tools" "Install optional tools ${INSTALL_EXTRA_TOOLS}"
$IDF_PATH/tools/idf_tools.py --non-interactive install $INSTALL_EXTRA_TOOLS
section_end "installing_optional_tools"
fi
# Install esp-clang if necessary (esp-clang is separately installed)
if [[ "$IDF_TOOLCHAIN" == "clang" && -z "$CI_CLANG_DISTRO_URL" ]]; then
$IDF_PATH/tools/idf_tools.py --non-interactive install esp-clang
fi
if [[ "${CI_JOB_STAGE}" == "target_test" ]]; then
section_start "IDF_SKIP_TOOLS_CHECK" "Skip required tools check"
export IDF_SKIP_TOOLS_CHECK=1
section_end "IDF_SKIP_TOOLS_CHECK"
fi
section_start "source_export" "Source export.sh"
source ./export.sh
section_end "source_export"
# we need the latest DEV release for esptool to be installed in every job without bumping the minimal requirement in
# the constraint file.
section_start "esptool_upgrade" "Upgrade esptool to the latest development version"
pip install --upgrade --pre esptool -c "${IDF_TOOLS_PATH:-$HOME/.espressif}/${CI_PYTHON_CONSTRAINT_FILE}"
section_end "esptool_upgrade"
# Custom clang toolchain
if [[ "$IDF_TOOLCHAIN" == "clang" && ! -z "$CI_CLANG_DISTRO_URL" ]]; then
echo "Using custom clang from ${CI_CLANG_DISTRO_URL}"
wget $CI_CLANG_DISTRO_URL
ARCH_NAME=$(basename $CI_CLANG_DISTRO_URL)
tar -x -f $ARCH_NAME
export PATH=$PWD/esp-clang/bin:$PATH
fi
# Custom OpenOCD
if [[ "$CI_JOB_STAGE" == "target_test" ]]; then
machine="$(uname -m)"
if [[ "$machine" == "armv7l" ]] ; then
OOCD_DISTRO_URL="$OOCD_DISTRO_URL_ARMHF"
elif [[ "$machine" == "aarch64" ]] ; then
OOCD_DISTRO_URL="$OOCD_DISTRO_URL_ARM64"
fi
if [[ ! -z "$OOCD_DISTRO_URL" ]]; then
echo "Using custom OpenOCD from ${OOCD_DISTRO_URL}"
wget $OOCD_DISTRO_URL
ARCH_NAME=$(basename $OOCD_DISTRO_URL)
tar -x -f $ARCH_NAME
export OPENOCD_SCRIPTS=$PWD/openocd-esp32/share/openocd/scripts
export PATH=$PWD/openocd-esp32/bin:$PATH
fi
fi
if [[ -n "$CI_PYTHON_TOOL_REPO" ]]; then
git clone --quiet --depth=1 -b ${CI_PYTHON_TOOL_BRANCH} https://gitlab-ci-token:${ESPCI_TOKEN}@${GITLAB_HTTPS_HOST}/espressif/${CI_PYTHON_TOOL_REPO}.git
pip install ./${CI_PYTHON_TOOL_REPO}
rm -rf ${CI_PYTHON_TOOL_REPO}
fi
info "setup tools and python venv done"
.show_ccache_statistics: &show_ccache_statistics |
# Show ccache statistics if enabled globally
section_start "ccache_show_stats" "Show ccache statistics"
if [[ "$CI_CCACHE_STATS" == 1 ]] && command -v ccache >/dev/null 2>&1; then
if ccache --help 2>/dev/null | grep -q -- '--show-log-stats'; then
if [[ -n "$CCACHE_STATSLOG" && -f "$CCACHE_STATSLOG" ]]; then
ccache --show-log-stats -vv
else
echo "INFO: No per-job ccache statistics were recorded"
fi
else
ccache --show-stats -vv
fi
fi || true
section_end "ccache_show_stats"
.upload_failed_job_log_artifacts: &upload_failed_job_log_artifacts |
if [ $CI_JOB_STATUS = "failed" ]; then
python tools/ci/artifacts_handler.py upload --type logs
fi
.before_script:minimal:
before_script:
- *common-before_scripts
- !reference [.common_before_scripts, minimal]
.before_script:build:macos:
before_script:
# macos is running shell executor, which means it would use
# the system installed /usr/local/bin/python3 by default.
# Ensure pyenv and PYENV_VERSION installed
- eval "$(pyenv init -)"
- *common-before_scripts
# On macOS, these tools need to be installed
- export IDF_TOOLS_PATH="${HOME}/.espressif_runner_${CI_RUNNER_ID}_${CI_CONCURRENT_ID}"
# remove idf-env.json, since it may contains enabled "features"
- rm -f $IDF_TOOLS_PATH/idf-env.json
# This adds tools (compilers) and the version-specific Python environment to PATH
- *setup_tools_and_idf_python_venv
- fetch_submodules
variables:
INSTALL_EXTRA_TOOLS: cmake ninja
.after_script:build:macos:upload-failed-job-logs:ccache-show-stats:
.after_script:build:
after_script:
# macos is running shell executor, which means it would use
# the system installed /usr/local/bin/python3 by default.
# Ensure pyenv and PYENV_VERSION installed
- eval "$(pyenv init -)"
- *upload_failed_job_log_artifacts
- *show_ccache_statistics
.before_script:build:
before_script:
- *common-before_scripts
- *setup_tools_and_idf_python_venv
- add_gitlab_ssh_keys
- fetch_submodules
- export EXTRA_CFLAGS=${PEDANTIC_CFLAGS}
- export EXTRA_CXXFLAGS=${PEDANTIC_CXXFLAGS}
.after_script:build:ccache-show-stats:
after_script:
- *show_ccache_statistics
.after_script:build:ccache-show-stats:upload-failed-job-logs:
after_script:
- *show_ccache_statistics
- *upload_failed_job_log_artifacts
##############################
# Git Strategy Job Templates #
##############################
.git_init: &git_init |
mkdir -p "${CI_PROJECT_DIR}"
cd "${CI_PROJECT_DIR}"
git init
.git_fetch_from_mirror_url_if_exists: &git_fetch_from_mirror_url_if_exists |
# check if set mirror
if [ -n "${LOCAL_GITLAB_HTTPS_HOST:-}" ] && [ -n "${ESPCI_TOKEN:-}" ]; then
MIRROR_REPO_URL="https://bot:${ESPCI_TOKEN}@${LOCAL_GITLAB_HTTPS_HOST}/${CI_PROJECT_PATH}"
elif [ -n "${LOCAL_GIT_MIRROR:-}" ]; then
MIRROR_REPO_URL="${LOCAL_GIT_MIRROR}/${CI_PROJECT_PATH}"
fi
# fetch from mirror first if set
if [ -n "${MIRROR_REPO_URL:-}" ]; then
if git remote -v | grep origin; then
git remote set-url origin "${MIRROR_REPO_URL}"
else
git remote add origin "${MIRROR_REPO_URL}"
fi
# mirror url may fail with authentication issue
git fetch origin --no-recurse-submodules || true
fi
# set remote url to CI_REPOSITORY_URL
if git remote -v | grep origin; then
git remote set-url origin "${CI_REPOSITORY_URL}"
else
git remote add origin "${CI_REPOSITORY_URL}"
fi
.git_checkout_ci_commit_sha: &git_checkout_ci_commit_sha |
git checkout $CI_COMMIT_SHA
git clean ${GIT_CLEAN_FLAGS}
# git diff requires two commits, with different CI env var
#
# By default, we use git strategy "clone" with depth 1 to speed up the clone process.
# But for jobs requires running `git diff`, we need to fetch more commits to get the correct diffs.
#
# Since there's no way to get the correct git_depth before the job starts,
# we can't set `GIT_DEPTH` in the job definition.
#
# Set git strategy to "none" and fetch manually instead.
.before_script:fetch:git_diff:
variables:
GIT_STRATEGY: none
before_script:
- *git_init
- *git_fetch_from_mirror_url_if_exists
- !reference [.common_after_scripts, ccache]
- run_cmd idf-ci gitlab upload-artifacts --type junit
- |
# merged results pipelines, by default
if [[ -n $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA ]]; then
git fetch origin $CI_MERGE_REQUEST_DIFF_BASE_SHA --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
git fetch origin $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
export GIT_DIFF_OUTPUT=$(git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA)
git fetch origin $CI_COMMIT_SHA --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
# merge request pipelines, when the mr got conflicts
elif [[ -n $CI_MERGE_REQUEST_DIFF_BASE_SHA ]]; then
git fetch origin $CI_MERGE_REQUEST_DIFF_BASE_SHA --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
git fetch origin $CI_COMMIT_SHA --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
export GIT_DIFF_OUTPUT=$(git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA $CI_COMMIT_SHA)
# other pipelines, like the protected branches pipelines
elif [[ "$CI_COMMIT_BEFORE_SHA" != "0000000000000000000000000000000000000000" ]]; then
git fetch origin $CI_COMMIT_BEFORE_SHA --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
git fetch origin $CI_COMMIT_SHA --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
export GIT_DIFF_OUTPUT=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA)
else
# pipeline source could be web, scheduler, etc.
git fetch origin $CI_COMMIT_SHA --depth=2 ${GIT_FETCH_EXTRA_FLAGS}
export GIT_DIFF_OUTPUT=$(git diff --name-only $CI_COMMIT_SHA~1 $CI_COMMIT_SHA)
if [ $CI_JOB_STATUS = "failed" ]; then
run_cmd idf-ci gitlab upload-artifacts --type log
fi
- *git_checkout_ci_commit_sha
- *common-before_scripts
- *setup_tools_and_idf_python_venv
- add_gitlab_ssh_keys
# target test runners may locate in different places
# for runners set git mirror, we fetch from the mirror first, then fetch the HEAD commit
.before_script:fetch:target_test:
variables:
GIT_STRATEGY: none
before_script:
- *git_init
- *git_fetch_from_mirror_url_if_exists
- git fetch origin "${CI_COMMIT_SHA}" --depth=1 ${GIT_FETCH_EXTRA_FLAGS}
- *git_checkout_ci_commit_sha
- *common-before_scripts
- *setup_tools_and_idf_python_venv
- add_gitlab_ssh_keys
# no submodules
#############
# `default` #
@@ -403,12 +132,6 @@ default:
cache:
# pull only for most of the use cases since it's cache dir.
# Only set "push" policy for "upload_cache" stage jobs
- key: pip-cache-${LATEST_GIT_TAG}
fallback_keys:
- pip-cache
paths:
- .cache/pip
policy: pull
- key: submodule-cache-${LATEST_GIT_TAG}
fallback_keys:
- submodule-cache
@@ -416,20 +139,8 @@ default:
- .cache/submodule_archives
policy: pull
before_script:
- *common-before_scripts
- *setup_tools_and_idf_python_venv
- !reference [.common_before_scripts, with_tools]
- add_gitlab_ssh_keys
- fetch_submodules
# gitlab bug, setting them here doesn't work
# - expire_in: https://gitlab.com/gitlab-org/gitlab/-/issues/404563
# - when: https://gitlab.com/gitlab-org/gitlab/-/issues/440672
# artifacts:
# expire_in: 1 week
# when: always
retry:
max: 2
when:
# In case of a runner failure we could hop to another one, or a network error could go away.
- runner_system_failure
# Job execution timeout may be caused by a network issue.
- job_execution_timeout
- export EXTRA_CFLAGS=${PEDANTIC_CFLAGS}
- export EXTRA_CXXFLAGS=${PEDANTIC_CXXFLAGS}

View File

@@ -1,20 +0,0 @@
# this file support two keywords:
# - extra_default_build_targets:
# besides of the SUPPORTED_TARGETS in IDF,
# enable build for the specified targets by default as well.
# - bypass_check_test_targets:
# suppress the check_build_test_rules check-test-script warnings for the specified targets
#
# This file should ONLY be used during bringup. Should be reset to empty after the bringup process
extra_default_build_targets:
bypass_check_test_targets:
- esp32c5
- esp32c61
#
# These lines would
# - enable the README.md check for esp32c6. Don't forget to add the build jobs in .gitlab/ci/build.yml
# - disable the test script check with the manifest file.
#

View File

@@ -1,19 +1,3 @@
.all_targets: &all_targets
- esp32
- esp32s2
- esp32s3
- esp32c3
- esp32c2
- esp32c6
- esp32c5
- esp32h2
- esp32p4
.target_test: &target_test
- example_test
- custom_test
- component_ut
##############
# Build Jobs #
##############
@@ -25,7 +9,6 @@
- build_system
- downloadable-tools
included_in:
- build:target_test
- build:check
# -------------------
@@ -57,8 +40,6 @@
"patterns:template-app":
patterns:
- build_template-app
included_in:
- build:target_test
"patterns:build-check":
patterns:

View File

@@ -1,48 +0,0 @@
.deploy_job_template:
stage: deploy
image: $ESP_ENV_IMAGE
tags: [ deploy ]
# Check this before push_to_github
check_submodule_sync:
extends:
- .deploy_job_template
- .rules:test:submodule
stage: test_deploy
tags: [ brew, github_sync ]
retry: 2
variables:
# for brew runners, we always set GIT_STRATEGY to fetch
GIT_STRATEGY: fetch
SUBMODULES_TO_FETCH: "none"
PUBLIC_IDF_URL: "https://github.com/espressif/esp-idf.git"
dependencies: []
script:
- git submodule deinit --force .
- rm -rf .git/modules # remove all the cached metadata
# setting the default remote URL to the public one, to resolve relative location URLs
- git config remote.origin.url ${PUBLIC_IDF_URL}
# check if all submodules are correctly synced to public repository
- git submodule init
- git config --get-regexp '^submodule\..*\.url$' || true
- git submodule update --recursive
- echo "IDF was cloned from ${PUBLIC_IDF_URL} completely"
push_to_github:
extends:
- .deploy_job_template
- .before_script:minimal
- .rules:push_to_github
needs:
- check_submodule_sync
tags: [ brew, github_sync ]
variables:
# for brew runners, we always set GIT_STRATEGY to fetch
GIT_STRATEGY: fetch
# github also need full record of commits
GIT_DEPTH: 0
script:
- add_github_ssh_keys
- git remote remove github &>/dev/null || true
- git remote add github git@github.com:espressif/esp-idf.git
- tools/ci/push_to_github.sh

View File

@@ -2,18 +2,17 @@
extends: .rules:test:host_test
stage: host_test
image: $ESP_ENV_IMAGE
tags:
- host_test
tags: [build, shiny]
dependencies: # set dependencies to null to avoid missing artifacts issue
# run host_test jobs immediately, only after upload cache
needs:
- job: upload-pip-cache
optional: true
artifacts: false
- pipeline_variables
- job: upload-submodules-cache
optional: true
artifacts: false
- pipeline_variables
- job: fast_template_app
optional: true
artifacts: false
artifacts:
expire_in: 1 week
when: always
@@ -23,16 +22,28 @@ check_public_headers:
- .host_test_template
- .rules:build:check
script:
- IDF_TARGET=esp32 python tools/ci/check_public_headers.py --jobs 4 --prefix xtensa-esp32-elf-
- IDF_TARGET=esp32s2 python tools/ci/check_public_headers.py --jobs 4 --prefix xtensa-esp32s2-elf-
- IDF_TARGET=esp32s3 python tools/ci/check_public_headers.py --jobs 4 --prefix xtensa-esp32s3-elf-
- IDF_TARGET=esp32c3 python tools/ci/check_public_headers.py --jobs 4 --prefix riscv32-esp-elf-
- IDF_TARGET=esp32c2 python tools/ci/check_public_headers.py --jobs 4 --prefix riscv32-esp-elf-
- IDF_TARGET=esp32c6 python tools/ci/check_public_headers.py --jobs 4 --prefix riscv32-esp-elf-
- IDF_TARGET=esp32c5 python tools/ci/check_public_headers.py --jobs 4 --prefix riscv32-esp-elf-
- IDF_TARGET=esp32h2 python tools/ci/check_public_headers.py --jobs 4 --prefix riscv32-esp-elf-
- IDF_TARGET=esp32p4 python tools/ci/check_public_headers.py --jobs 4 --prefix riscv32-esp-elf-
- IDF_TARGET=esp32c61 python tools/ci/check_public_headers.py --jobs 4 --prefix riscv32-esp-elf-
- |
targets="esp32 \
esp32s2 \
esp32s3 \
esp32c3 \
esp32c2 \
esp32c6 \
esp32c5 \
esp32h2 \
esp32p4 \
esp32c61"
for target in ${targets}; do
case "${target}" in
esp32|esp32s2|esp32s3)
PREFIX="xtensa-${target}-elf-"
;;
*)
PREFIX="riscv32-esp-elf-"
;;
esac
IDF_TARGET=${target} python tools/ci/check_public_headers.py --jobs 4 --prefix ${PREFIX}
done
test_nvs_coverage:
extends:
@@ -51,14 +62,15 @@ test_partition_table_on_host:
extends: .host_test_template
script:
- cd components/partition_table/test_gen_esp32part_host
- ./gen_esp32part_tests.py
- pytest_for_ut ./gen_esp32part_tests.py
test_ldgen_on_host:
extends: .host_test_template
extends:
- .host_test_template
script:
- cd tools/ldgen/test
- export PYTHONPATH=$PYTHONPATH:..
- python -m unittest
- pytest_for_ut .
variables:
LC_ALL: C.UTF-8
@@ -66,15 +78,13 @@ test_spiffs_on_host:
extends: .host_test_template
script:
- cd components/spiffs/test_spiffsgen/
- ./test_spiffsgen.py
- pytest_for_ut ./test_spiffsgen.py
test_fatfsgen_on_host:
extends: .host_test_template
script:
- cd components/fatfs/test_fatfsgen/
- ./test_fatfsgen.py
- ./test_wl_fatfsgen.py
- ./test_fatfsparse.py
- pytest_for_ut ./test_fatfsgen.py ./test_wl_fatfsgen.py ./test_fatfsparse.py
test_multi_heap_on_host:
extends: .host_test_template
@@ -86,7 +96,7 @@ test_certificate_bundle_on_host:
extends: .host_test_template
script:
- cd components/mbedtls/esp_crt_bundle/test_gen_crt_bundle/
- ./test_gen_crt_bundle.py
- pytest_for_ut ./test_gen_crt_bundle.py
test_gdbstub_on_host:
extends: .host_test_template
@@ -115,15 +125,17 @@ test_cli_installer:
- python3 ${IDF_PATH}/tools/idf_tools.py download required qemu-riscv32 qemu-xtensa cmake cmake@3.16.3
- cd ${IDF_PATH}/tools/test_idf_tools
- python3 -m pip install jsonschema --break-system-packages
# Testing with system python3, so don't use any third-party packages
- python3 ./test_idf_tools.py -v
- python3 ./test_idf_tools_python_env.py
# It runs at the end because it modifies dependencies
- IDF_TEST_MAY_BREAK_DEPENDENCIES=1 python3 ./test_idf_tools.py -v TestSystemDependencies.test_commands_when_nodeps
.test_efuse_table_on_host_template:
test_efuse_table_on_host:
extends: .host_test_template
variables:
IDF_TARGET: "esp32"
parallel:
matrix:
- IDF_TARGET: [esp32, esp32s2, esp32c3, esp32s3, esp32c6, esp32h2]
artifacts:
when: on_failure
paths:
@@ -133,35 +145,7 @@ test_cli_installer:
- ./efuse_table_gen.py -t "${IDF_TARGET}" ${IDF_PATH}/components/efuse/${IDF_TARGET}/esp_efuse_table.csv
- git diff --exit-code -- ${IDF_TARGET}/esp_efuse_table.c || { echo 'Differences found for ${IDF_TARGET} target. Please run idf.py efuse-common-table and commit the changes.'; exit 1; }
- cd ${IDF_PATH}/components/efuse/test_efuse_host
- ./efuse_tests.py
test_efuse_table_on_host_esp32:
extends: .test_efuse_table_on_host_template
test_efuse_table_on_host_esp32s2:
extends: .test_efuse_table_on_host_template
variables:
IDF_TARGET: esp32s2
test_efuse_table_on_host_esp32s3:
extends: .test_efuse_table_on_host_template
variables:
IDF_TARGET: esp32s3
test_efuse_table_on_host_esp32c3:
extends: .test_efuse_table_on_host_template
variables:
IDF_TARGET: esp32c3
test_efuse_table_on_host_esp32h2:
extends: .test_efuse_table_on_host_template
variables:
IDF_TARGET: esp32h2
test_efuse_table_on_host_esp32c6:
extends: .test_efuse_table_on_host_template
variables:
IDF_TARGET: esp32c6
- pytest_for_ut ./efuse_tests.py
test_logtrace_proc:
extends: .host_test_template
@@ -199,16 +183,17 @@ test_tools:
INSTALL_EXTRA_TOOLS: "qemu-xtensa qemu-riscv32" # for test_idf_qemu.py
script:
- stat=0
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- cd ${IDF_PATH}/tools/ci/test_autocomplete
- pytest --noconftest test_autocomplete.py --junitxml=${IDF_PATH}/XUNIT_AUTOCOMP.xml || stat=1
- run_cmd pytest --noconftest test_autocomplete.py --junitxml=${IDF_PATH}/XUNIT_AUTOCOMP.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} || stat=1
- cd ${IDF_PATH}/tools/test_idf_py
- pytest --noconftest test_idf_py.py --junitxml=${IDF_PATH}/XUNIT_IDF_PY.xml || stat=1
- pytest --noconftest test_hints.py --junitxml=${IDF_PATH}/XUNIT_HINTS.xml || stat=1
- pytest --noconftest test_idf_qemu.py --junitxml=${IDF_PATH}/XUNIT_IDF_PY_QEMU.xml || stat=1
- run_cmd pytest --noconftest test_idf_py.py --junitxml=${IDF_PATH}/XUNIT_IDF_PY.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} || stat=1
- run_cmd pytest --noconftest test_hints.py --junitxml=${IDF_PATH}/XUNIT_HINTS.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} || stat=1
- run_cmd pytest --noconftest test_idf_qemu.py --junitxml=${IDF_PATH}/XUNIT_IDF_PY_QEMU.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} || stat=1
- cd ${IDF_PATH}/tools/test_mkdfu
- pytest --noconftest test_mkdfu.py --junitxml=${IDF_PATH}/XUNIT_MKDFU.xml || stat=1
- run_cmd pytest --noconftest test_mkdfu.py --junitxml=${IDF_PATH}/XUNIT_MKDFU.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} || stat=1
- cd ${IDF_PATH}/tools/test_idf_size
- pytest --noconftest test_idf_size.py --junitxml=${IDF_PATH}/XUNIT_IDF_SIZE.xml || stat=1
- run_cmd pytest --noconftest test_idf_size.py --junitxml=${IDF_PATH}/XUNIT_IDF_SIZE.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} || stat=1
- cd ${IDF_PATH}
- shellcheck -s sh tools/detect_python.sh || stat=1
- shellcheck -s bash tools/detect_python.sh || stat=1
@@ -223,12 +208,15 @@ test_split_path_by_spaces:
extends: .host_test_template
script:
- cd ${IDF_PATH}/tools
- python -m unittest split_paths_by_spaces.py
- pytest_for_ut ./split_paths_by_spaces.py
test_mqtt_on_host:
extends: .host_test_template
script:
- cd ${IDF_PATH}/components/mqtt/esp-mqtt/host_test
# no -Werror
- export EXTRA_CFLAGS=
- export EXTRA_CXXFLAGS=
- idf.py build
- LSAN_OPTIONS=verbosity=1:log_threads=1 build/host_mqtt_client_test.elf
@@ -269,122 +257,13 @@ test_gen_soc_caps_kconfig:
extends: .host_test_template
script:
- cd ${IDF_PATH}/tools/gen_soc_caps_kconfig/
- ./test/test_gen_soc_caps_kconfig.py
test_pytest_qemu:
extends:
- .host_test_template
- .before_script:build
artifacts:
paths:
- XUNIT_RESULT.xml
- pytest-embedded/
- "**/build*/*.bin"
reports:
junit: XUNIT_RESULT.xml
parallel:
matrix:
- IDF_TARGET: "esp32"
INSTALL_EXTRA_TOOLS: "qemu-xtensa"
- IDF_TARGET: "esp32c3"
INSTALL_EXTRA_TOOLS: "qemu-riscv32"
script:
- run_cmd python tools/ci/ci_build_apps.py . -v
--target $IDF_TARGET
--pytest-apps
-m qemu
--collect-app-info "list_job_${CI_JOB_NAME_SLUG}.txt"
--modified-components ${MR_MODIFIED_COMPONENTS}
--modified-files ${MR_MODIFIED_FILES}
- python tools/ci/get_known_failure_cases_file.py
- run_cmd pytest
--target $IDF_TARGET
--log-cli-level DEBUG
-m qemu
--embedded-services idf,qemu
--junitxml=XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
--app-info-filepattern \"list_job_*.txt\"
--qemu-extra-args \"-global driver=timer.$IDF_TARGET.timg,property=wdt_disable,value=true\"
test_pytest_linux:
extends:
- .host_test_template
- .before_script:build
artifacts:
paths:
- XUNIT_RESULT.xml
- pytest-embedded/
- "**/build*/build_log.txt"
reports:
junit: XUNIT_RESULT.xml
script:
- run_cmd python tools/ci/ci_build_apps.py components examples tools/test_apps -v
--target linux
--pytest-apps
-m host_test
--collect-app-info "list_job_${CI_JOB_NAME_SLUG}.txt"
--modified-components ${MR_MODIFIED_COMPONENTS}
--modified-files ${MR_MODIFIED_FILES}
- python tools/ci/get_known_failure_cases_file.py
- run_cmd pytest
--target linux
-m host_test
--embedded-services idf
--junitxml=XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
--app-info-filepattern \"list_job_*.txt\"
test_pytest_macos:
extends:
- .host_test_template
- .before_script:build:macos
tags:
- macos_shell
artifacts:
paths:
- XUNIT_RESULT.xml
- pytest-embedded/
- "**/build*/build_log.txt"
reports:
junit: XUNIT_RESULT.xml
variables:
PYTEST_IGNORE_COLLECT_IMPORT_ERROR: "1"
script:
- run_cmd python tools/ci/ci_build_apps.py components examples tools/test_apps -v
--target linux
--pytest-apps
-m \"host_test and macos_shell\"
--collect-app-info "list_job_${CI_JOB_NAME_SLUG}.txt"
--modified-components ${MR_MODIFIED_COMPONENTS}
--modified-files ${MR_MODIFIED_FILES}
- python tools/ci/get_known_failure_cases_file.py
- run_cmd pytest
--target linux
-m \"host_test and macos_shell\"
--junitxml=XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
--app-info-filepattern \"list_job_*.txt\"
test_idf_pytest_plugin:
extends:
- .host_test_template
- .rules:patterns:idf-pytest-plugin
variables:
SUBMODULES_TO_FETCH: "none"
artifacts:
reports:
junit: XUNIT_RESULT.xml
script:
- cd ${IDF_PATH}/tools/ci/dynamic_pipelines/tests/test_report_generator
- python -m unittest test_report_generator.py
- cd ${IDF_PATH}/tools/ci/idf_pytest
- pytest --junitxml=${CI_PROJECT_DIR}/XUNIT_RESULT.xml
- pytest_for_ut ./test/test_gen_soc_caps_kconfig.py
test_idf_build_apps_load_soc_caps:
extends: .host_test_template
script:
- python tools/ci/check_soc_headers_load_in_idf_build_apps.py
- cd tools/ci
- pytest_for_ut ./check_soc_headers_load_in_idf_build_apps.py
test_nvs_gen_check:
extends: .host_test_template
@@ -398,7 +277,7 @@ test_nvs_gen_check:
LC_ALL: C.UTF-8
script:
- cd ${IDF_PATH}/components/nvs_flash/nvs_partition_tool
- pytest --noconftest test_nvs_gen_check.py --junitxml=XUNIT_RESULT.xml
- pytest_for_ut test_nvs_gen_check.py
test_esp_rom:
extends: .host_test_template
@@ -409,7 +288,7 @@ test_esp_rom:
junit: XUNIT_RESULT.xml
script:
- cd ${IDF_PATH}/components/esp_rom/
- pytest --noconftest test_esp_rom.py --junitxml=XUNIT_RESULT.xml
- pytest_for_ut test_esp_rom.py
make_sure_soc_caps_compatible_in_idf_build_apps:
extends:
@@ -428,3 +307,191 @@ make_sure_soc_caps_compatible_in_idf_build_apps:
- git checkout $CI_COMMIT_SHA -- tools/ci/idf_build_apps_dump_soc_caps.py
- python tools/ci/idf_build_apps_dump_soc_caps.py base.json
- diff new.json base.json
build_docker:
extends:
- .before_script:minimal
- .rules:build:docker
stage: host_test
needs:
- job: manual_gate
optional: true
image: espressif/docker-builder:1
tags: [shiny, dind]
variables:
DOCKER_TMP_IMAGE_NAME: "idf_tmp_image"
script:
# use different image tag for different concurrent job
- export DOCKER_TMP_IMAGE="${DOCKER_TMP_IMAGE_NAME}:${CI_CONCURRENT_ID:-latest}"
- export DOCKER_BUILD_ARGS="--build-arg IDF_CLONE_URL=${CI_REPOSITORY_URL} --build-arg IDF_CLONE_BRANCH_OR_TAG=${CI_COMMIT_REF_NAME} --build-arg IDF_CHECKOUT_REF=${CI_COMMIT_TAG:-$CI_COMMIT_SHA} --build-arg IDF_CLONE_SHALLOW=1 ${EXTRA_DOCKER_BUILD_ARGS:-} "
- time docker build --tag ${DOCKER_TMP_IMAGE} ${DOCKER_BUILD_ARGS} tools/docker/
# We can't mount $PWD/examples/get-started/blink into the container, see https://gitlab.com/gitlab-org/gitlab-ce/issues/41227.
# The workaround mentioned there works, but leaves around directories which need to be cleaned up manually.
# Therefore, build a copy of the example located inside the container.
- docker run --rm --workdir /opt/esp/idf/examples/get-started/blink ${DOCKER_TMP_IMAGE} idf.py build
############################
# Host test with test apps #
############################
test_pytest_qemu:
extends:
- .host_test_template
artifacts:
paths:
- XUNIT_RESULT.xml
- pytest-embedded/
- "**/build*/*.bin"
reports:
junit: XUNIT_RESULT.xml
parallel:
matrix:
- IDF_TARGET: "esp32"
INSTALL_EXTRA_TOOLS: "qemu-xtensa"
# Skip Clang + Xtensa tests due to bootloader size issue
IDF_TOOLCHAIN: [gcc]
- IDF_TARGET: "esp32c3"
INSTALL_EXTRA_TOOLS: "qemu-riscv32"
IDF_TOOLCHAIN: [gcc, clang]
script:
- run_cmd idf-ci build run
--build-system cmake
--target $IDF_TARGET
-m qemu
--modified-files ${MR_MODIFIED_FILES}
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- run_cmd pytest
--target $IDF_TARGET
-m 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\"
test_pytest_linux:
extends:
- .host_test_template
artifacts:
paths:
- XUNIT_RESULT.xml
- pytest-embedded/
- "**/build*/build_log.txt"
reports:
junit: XUNIT_RESULT.xml
script:
- run_cmd idf-ci build run
--build-system cmake
-p components -p examples -p tools/test_apps
--target linux
--only-test-related
--modified-files ${MR_MODIFIED_FILES}
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- run_cmd pytest
--target linux
-m \"not macos\"
--junitxml=XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
test_pytest_macos:
extends:
- .host_test_template
- .macos-settings
artifacts:
paths:
- XUNIT_RESULT.xml
- pytest-embedded/
- "**/build*/build_log.txt"
reports:
junit: XUNIT_RESULT.xml
script:
- run_cmd idf-ci build run
-p components -p examples -p tools/test_apps
--build-system cmake
--target linux
--only-test-related
-m macos
--modified-files ${MR_MODIFIED_FILES}
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- run_cmd pytest
--target linux
-m macos
--junitxml=XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
######################
# Build System Tests #
######################
.test_build_system_template:
stage: host_test
extends:
- .build_template
- .rules:build:check
dependencies: # set dependencies to null to avoid missing artifacts issue
needs:
- job: fast_template_app
artifacts: false
optional: true
artifacts:
reports:
junit: XUNIT_RESULT.xml
paths:
- XUNIT_RESULT.xml
- test_build_system
expire_in: 1 week
when: always
script:
- ${IDF_PATH}/tools/ci/test_configure_ci_environment.sh
- cd ${IDF_PATH}/tools/test_build_system
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- pytest
--cleanup-idf-copy
--parallel-count ${CI_NODE_TOTAL:-1}
--parallel-index ${CI_NODE_INDEX:-1}
--work-dir ${CI_PROJECT_DIR}/test_build_system
--junitxml ${CI_PROJECT_DIR}/XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
pytest_build_system:
extends: .test_build_system_template
parallel: 3
pytest_build_system_macos:
extends:
- .test_build_system_template
- .macos-settings
- .rules:build:macos
parallel: 3
.test_build_system_minimal_cmake_template:
extends: .test_build_system_template
variables:
INSTALL_EXTRA_TOOLS: cmake@3.16.3
script:
- MINIMAL_SUPPORTED_CMAKE_VERSION=$(echo "${INSTALL_EXTRA_TOOLS}" | sed -n 's/.*cmake@\([0-9.]*\).*/\1/p')
- export PATH=$(echo "$PATH" | sed -E "s|/tools/cmake/[0-9.]+|/tools/cmake/${MINIMAL_SUPPORTED_CMAKE_VERSION}|")
- ACTUAL_CMAKE_VERSION=$(cmake --version | head -n1 | awk '{print $3}')
- |
if [ "${ACTUAL_CMAKE_VERSION}" != "${MINIMAL_SUPPORTED_CMAKE_VERSION}" ]; then
echo "ERROR: Wrong minimal CMake version! Detected: ${ACTUAL_CMAKE_VERSION}, but should be: ${MINIMAL_SUPPORTED_CMAKE_VERSION}"
exit 1
fi
- ${IDF_PATH}/tools/ci/test_configure_ci_environment.sh
- cd ${IDF_PATH}/tools/test_build_system
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
- pytest
-k cmake
--cleanup-idf-copy
--parallel-count ${CI_NODE_TOTAL:-1}
--parallel-index ${CI_NODE_INDEX:-1}
--work-dir ${CI_PROJECT_DIR}/test_build_system
--junitxml ${CI_PROJECT_DIR}/XUNIT_RESULT.xml
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
pytest_build_system_minimal_cmake:
extends: .test_build_system_minimal_cmake_template
pytest_build_system_macos_minimal_cmake:
extends:
- .test_build_system_minimal_cmake_template
- .macos-settings
- .rules:build:macos
variables:
INSTALL_EXTRA_TOOLS: ninja cmake@3.16.3

View File

@@ -1,13 +0,0 @@
generate_failed_jobs_report:
stage: post_deploy
tags: [build, shiny]
image: $ESP_ENV_IMAGE
when: always
dependencies: [] # Do not download artifacts from the previous stages
artifacts:
expire_in: 1 week
when: always
paths:
- job_report.html
script:
- python tools/ci/dynamic_pipelines/scripts/generate_report.py --report-type job

View File

@@ -1,16 +1,9 @@
.pre_check_template:
stage: pre_check
image: $ESP_ENV_IMAGE
tags:
- host_test
dependencies: # set dependencies to null to avoid missing artifacts issue
check_version:
# Don't run this for feature/bugfix branches, so that it is possible to modify
# esp_idf_version.h in a branch before tagging the next version.
extends:
- .pre_check_template
- .rules:protected
- .rules:protected:check
tags: [ brew, github_sync ]
variables:
# need a full clone to get the latest tag
@@ -116,108 +109,23 @@ check_version_tag:
check_test_scripts_build_test_rules:
extends:
- .pre_check_template
- .before_script:build
variables:
PYTEST_IGNORE_COLLECT_IMPORT_ERROR: "1"
script:
# requires basic pytest dependencies
- run_cmd bash install.sh --enable-pytest
- python tools/ci/check_build_test_rules.py check-test-scripts examples/ tools/test_apps components
check_configure_ci_environment_parsing:
check_gitlab_yaml:
extends:
- .pre_check_template
- .before_script:build
- .rules:build
script:
- cd tools/ci
- python -m unittest ci_build_apps.py
- python tools/ci/gitlab_yaml_linter.py
pipeline_variables:
extends:
- .pre_check_template
- .before_script:fetch:git_diff
tags: [fast_run, shiny]
script:
# MODIFIED_FILES is a list of files that changed, could be used everywhere
- MODIFIED_FILES=$(echo "$GIT_DIFF_OUTPUT" | xargs)
- echo "MODIFIED_FILES=$MODIFIED_FILES" >> pipeline.env
- echo "REPORT_EXIT_CODE=0" >> pipeline.env
# MR_MODIFIED_FILES and MR_MODIFIED_COMPONENTS are semicolon separated lists that is used in MR only
# for non MR pipeline, these are empty lists
- |
if [ $IS_MR_PIPELINE == "0" ]; then
echo "MR_MODIFIED_FILES=\"\"" >> pipeline.env
echo "MR_MODIFIED_COMPONENTS=\"\"" >> pipeline.env
else
MR_MODIFIED_FILES=$(echo "$GIT_DIFF_OUTPUT" | tr '\n' ';')
echo "MR_MODIFIED_FILES=\"$MR_MODIFIED_FILES\"" >> pipeline.env
MR_MODIFIED_COMPONENTS=$(run_cmd python tools/ci/ci_get_mr_info.py components --modified-files $MODIFIED_FILES | tr '\n' ';')
echo "MR_MODIFIED_COMPONENTS=\"$MR_MODIFIED_COMPONENTS\"" >> pipeline.env
fi
- |
if echo "$CI_MERGE_REQUEST_LABELS" | egrep "(^|,)BUILD_AND_TEST_ALL_APPS(,|$)"; then
echo "BUILD_AND_TEST_ALL_APPS=1" >> pipeline.env
fi
# run full pipeline if testing constraint branch
- |
if [ -n "$CI_PYTHON_CONSTRAINT_BRANCH" ]; then
echo "BUILD_AND_TEST_ALL_APPS=1" >> pipeline.env
fi
- echo "OOCD_DISTRO_URL_ARMHF=$OOCD_DISTRO_URL_ARMHF" >> pipeline.env
- echo "OOCD_DISTRO_URL_ARM64=$OOCD_DISTRO_URL_ARM64" >> pipeline.env
- python tools/ci/ci_process_description.py
- cat pipeline.env
- python tools/ci/artifacts_handler.py upload --type modified_files_and_components_report
artifacts:
reports:
dotenv: pipeline.env
paths:
- pipeline.env
expire_in: 1 week
when: always
baseline_manifest_sha:
retry_failed_jobs:
extends:
- .pre_check_template
- .rules:dev-push
tags: [fast_run, shiny]
script:
- |
# merged results pipelines, by default
# diff between target-branch-head and merged-result-head
if [ -n "$CI_MERGE_REQUEST_TARGET_BRANCH_SHA" ]; then
git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_SHA --depth=1
git checkout FETCH_HEAD
idf-build-apps dump-manifest-sha \
--manifest-files $(find . -name ".build-test-rules.yml" | xargs) \
--output .manifest_sha
# merge request pipelines, when the mr got conflicts
# diff between diff-base-sha and merge-request-head
elif [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
git fetch origin $CI_MERGE_REQUEST_DIFF_BASE_SHA --depth=1
git checkout FETCH_HEAD
idf-build-apps dump-manifest-sha \
--manifest-files $(find . -name ".build-test-rules.yml" | xargs) \
--output .manifest_sha
# other pipelines, like the protected branches pipelines
# not triggered in this job
fi
artifacts:
paths:
- .manifest_sha
expire_in: 1 week
when: always
redundant_pass_job:
stage: pre_check
tags: [shiny, fast_run]
image: $ESP_ENV_IMAGE
dependencies: null
before_script: []
cache: []
extends: []
allow_failure: true
script:
- echo "This job is redundant to ensure the 'retry_failed_jobs' job can exist and not be skipped"
when: always
- echo "Retrieving and retrying all failed jobs for the pipeline..."
- python tools/ci/python_packages/gitlab_api.py retry_failed_jobs $CI_MERGE_REQUEST_PROJECT_ID --pipeline_id $CI_PIPELINE_ID
when: manual

View File

@@ -2,33 +2,20 @@
extends:
- .before_script:minimal
stage: pre_check
image: $PRE_COMMIT_IMAGE
tags: [cache, shiny]
image: "${CI_REGISTRY}/ci/images/pre-commit-idf-v5.4:1"
tags: [pre-commit]
variables:
# Both shiny and brew runners can pick this job
GIT_STRATEGY: fetch
GIT_DEPTH: 1
SUBMODULES_TO_FETCH: "all"
needs:
- pipeline_variables
variables:
# cache pre_commit
PRE_COMMIT_HOME: "$CI_PROJECT_DIR/.cache/pre-commit"
script:
- fetch_submodules
- pre-commit run --files $MODIFIED_FILES
- pre-commit run --hook-stage post-commit validate-sbom-manifest
check_pre_commit_upload_cache:
extends:
- .check_pre_commit_template
rules:
- if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_BRANCH =~ /^release\/v/) && $CI_PIPELINE_SOURCE == "push"'
cache:
- key: pre_commit-cache-${LATEST_GIT_TAG}
paths:
- .cache/pre-commit
policy: pull-push
- key: submodule-cache-${LATEST_GIT_TAG}
paths:
- .cache/submodule_archives
policy: pull
check_pre_commit:
extends:
- .check_pre_commit_template
@@ -37,10 +24,6 @@ check_pre_commit:
when: never
- when: on_success
cache:
- key: pre_commit-cache-${LATEST_GIT_TAG}
paths:
- .cache/pre-commit
policy: pull
- key: submodule-cache-${LATEST_GIT_TAG}
paths:
- .cache/submodule_archives
@@ -56,25 +39,20 @@ check_powershell:
tags:
- dind
- amd64
- brew # faster "apk add"
needs:
- pipeline_variables
variables:
# cache pre_commit
PRE_COMMIT_HOME: "$CI_PROJECT_DIR/.cache/pre-commit"
# brew runners always use fetch
GIT_STRATEGY: fetch
GIT_DEPTH: 1
SUBMODULES_TO_FETCH: "none"
rules:
- changes:
- "*.ps1"
- ".gitlab/ci/pre_commit.yml"
script:
- apk add python3
- apk add py3-pip
- pip install pre-commit --break-system-packages
- pre-commit run --hook-stage manual check-powershell-scripts --files $MODIFIED_FILES
cache:
- key: pre_commit-cache-${LATEST_GIT_TAG}
paths:
- .cache/pre-commit
policy: pull
- key: submodule-cache-${LATEST_GIT_TAG}
paths:
- .cache/submodule_archives
policy: pull

24
.gitlab/ci/pre_deploy.yml Normal file
View File

@@ -0,0 +1,24 @@
.pre_deploy_template:
stage: pre_deploy
image: $ESP_ENV_IMAGE
check_submodule_sync:
extends:
- .pre_deploy_template
- .rules:test:submodule
tags: [ brew, github_sync ]
retry: 2
variables:
GIT_STRATEGY: fetch # use brew local mirror first
SUBMODULES_TO_FETCH: "none"
PUBLIC_IDF_URL: "https://github.com/espressif/esp-idf.git"
script:
- git submodule deinit --force .
- rm -rf .git/modules # remove all the cached metadata
# setting the default remote URL to the public one, to resolve relative location URLs
- git config remote.origin.url ${PUBLIC_IDF_URL}
# check if all submodules are correctly synced to public repository
- git submodule init
- git config --get-regexp '^submodule\..*\.url$' || true
- git submodule update --recursive
- echo "IDF was cloned from ${PUBLIC_IDF_URL} completely"

View File

@@ -1,15 +0,0 @@
retry_failed_jobs:
stage: retry_failed_jobs
tags: [shiny, fast_run]
allow_failure: true
image: $ESP_ENV_IMAGE
dependencies: null
before_script: []
cache: []
extends: []
script:
- echo "Retrieving and retrying all failed jobs for the pipeline..."
- python tools/ci/python_packages/gitlab_api.py retry_failed_jobs $CI_MERGE_REQUEST_PROJECT_ID --pipeline_id $CI_PIPELINE_ID
when: manual
needs:
- redundant_pass_job

View File

@@ -9,19 +9,10 @@
- "**/*.{c,C,cpp}"
- "**/*.{h,H,hpp}"
- "Kconfig"
- "components/**/Kconfig"
- "components/**/CMakeList.txt"
- "components/**/CMakeLists.txt"
.patterns-python-cache: &patterns-python-cache
- "tools/requirements.json"
- "tools/requirements/requirements.*.txt"
.patterns-python-files: &patterns-python-files
- ".gitlab/ci/static-code-analysis.yml"
- "**/*.py"
.patterns-sonarqube-files: &patterns-sonarqube-files
- "tools/ci/sonar_exclude_list.txt"
.patterns-build_components: &patterns-build_components
# components files except "test*/" "host*/" folders
@@ -35,6 +26,7 @@
# Add folders excluded by "???[!t]" and "??[!s]?"
# pre-commit: tools/ci/check_rules_components_patterns.py
- "components/bt/host/**/*"
- "components/esp_psram/system_layer/*"
.patterns-downloadable-tools: &patterns-downloadable-tools
- "tools/idf_tools.py"
@@ -66,8 +58,10 @@
- "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/**/*"
.patterns-host_test: &patterns-host_test
- ".gitlab/ci/host-test.yml"
@@ -97,6 +91,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"
@@ -160,34 +155,22 @@
##############
# if anchors #
##############
.if-ref-master: &if-ref-master
if: '$CI_COMMIT_REF_NAME == "master"'
.if-master-push: &if-master-push
if: '$CI_COMMIT_REF_NAME == "master" && $CI_PIPELINE_SOURCE == "push"'
.if-tag-release: &if-tag-release
.if-release-tag: &if-release-tag
if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+(\.\d+)?($|-)/'
.if-protected: &if-protected
.if-protected-check: &if-protected-check
if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_BRANCH =~ /^release\/v/ || $CI_COMMIT_TAG =~ /^v\d+\.\d+(\.\d+)?($|-)/) || $CI_COMMIT_TAG =~ /^qa-test/'
.if-protected-deploy: &if-protected-deploy
if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_BRANCH =~ /^release\/v/ || $CI_COMMIT_TAG =~ /^v\d+\.\d+(\.\d+)?($|-)/) && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "api")'
.if-protected-no_label: &if-protected-no_label
if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_BRANCH =~ /^release\/v/ || $CI_COMMIT_TAG =~ /^v\d+\.\d+(\.\d+)?($|-)/) && $BOT_TRIGGER_WITH_LABEL == null'
.if-protected-ref-push: &if-protected-ref-push
.if-protected-branch-push: &if-protected-branch-push
# rules:changes always evaluates to true for new branch pipelines or when there is no Git push event
if: '($CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_BRANCH =~ /^release\/v/) && $CI_PIPELINE_SOURCE == "push"'
.if-qa-test-tag: &if-qa-test-tag
if: '$CI_COMMIT_TAG =~ /^qa-test/'
.if-dev-push: &if-dev-push
if: '$CI_COMMIT_REF_NAME != "master" && $CI_COMMIT_BRANCH !~ /^release\/v/ && $CI_COMMIT_TAG !~ /^v\d+\.\d+(\.\d+)?($|-)/ && $CI_COMMIT_TAG !~ /^qa-test/ && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event")'
.if-schedule: &if-schedule
if: '$CI_PIPELINE_SOURCE == "schedule"'
.if-schedule-nightly: &if-schedule-nightly
if: '$CI_PIPELINE_SOURCE == "schedule" && $INCLUDE_NIGHTLY_RUN == "1"'
@@ -207,39 +190,26 @@
# Rules #
#########
### Branches ###
.rules:protected:
.rules:protected:check:
rules:
- <<: *if-protected
- <<: *if-protected-check
.rules:push_to_github:
.rules:master:push:
rules:
- <<: *if-qa-test-tag
when: never
- <<: *if-protected-no_label
- <<: *if-master-push
.rules:tag:release:
rules:
- <<: *if-tag-release
- <<: *if-release-tag
.rules:dev-push:
rules:
- <<: *if-dev-push
# Do not upload caches on dev branches by default
.rules:upload-python-cache:
rules:
- <<: *if-tag-release
- <<: *if-schedule-nightly
- <<: *if-protected-ref-push
changes: *patterns-python-cache
- <<: *if-label-upload_cache
when: manual
.rules:upload-submodule-cache:
rules:
# Needn't upload submodule cache in schedule pipeline
- <<: *if-tag-release
- <<: *if-protected-ref-push
- <<: *if-release-tag
- <<: *if-protected-branch-push
changes: *patterns-submodule
- <<: *if-label-upload_cache
when: manual
@@ -247,16 +217,10 @@
### Patterns ###
.rules:patterns:clang_tidy:
rules:
- <<: *if-protected
- <<: *if-protected-check
- <<: *if-dev-push
changes: *patterns-c-files
.rules:patterns:python-files:
rules:
- <<: *if-protected
- <<: *if-dev-push
changes: *patterns-python-files
#.rules:patterns:static-code-analysis-preview:
# rules:
# - <<: *if-dev-push
@@ -266,12 +230,6 @@
# - <<: *if-dev-push
# changes: *patterns-sonarqube-files
.rules:patterns:idf-pytest-plugin:
rules:
- <<: *if-protected
- <<: *if-dev-push
changes: *patterns-idf-pytest-plugin
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# DO NOT place comments or maintain any code from this line
#
@@ -312,7 +270,7 @@
rules:
- <<: *if-revert-branch
when: never
- <<: *if-protected
- <<: *if-protected-check
- <<: *if-label-build
- <<: *if-dev-push
changes: *patterns-build_components
@@ -325,7 +283,7 @@
rules:
- <<: *if-revert-branch
when: never
- <<: *if-protected
- <<: *if-protected-check
- <<: *if-label-build
- <<: *if-dev-push
changes: *patterns-build_check
@@ -340,7 +298,7 @@
rules:
- <<: *if-revert-branch
when: never
- <<: *if-protected
- <<: *if-protected-check
- <<: *if-label-build
- <<: *if-label-docker
- <<: *if-dev-push
@@ -356,7 +314,7 @@
rules:
- <<: *if-revert-branch
when: never
- <<: *if-protected
- <<: *if-protected-check
- <<: *if-label-build
- <<: *if-label-macos
- <<: *if-label-macos_test
@@ -367,21 +325,6 @@
- <<: *if-dev-push
changes: *patterns-downloadable-tools
.rules:build:target_test:
rules:
- <<: *if-revert-branch
when: never
- <<: *if-protected
- <<: *if-label-build
- <<: *if-dev-push
changes: *patterns-build_components
- <<: *if-dev-push
changes: *patterns-build_system
- <<: *if-dev-push
changes: *patterns-build_template-app
- <<: *if-dev-push
changes: *patterns-downloadable-tools
.rules:labels:nvs_coverage:
rules:
- <<: *if-revert-branch
@@ -399,7 +342,7 @@
rules:
- <<: *if-revert-branch
when: never
- <<: *if-protected
- <<: *if-protected-check
- <<: *if-label-build-only
when: never
- <<: *if-label-host_test
@@ -410,7 +353,7 @@
rules:
- <<: *if-revert-branch
when: never
- <<: *if-protected
- <<: *if-protected-check
- <<: *if-label-build-only
when: never
- <<: *if-label-submodule

View File

@@ -16,106 +16,17 @@ clang_tidy_check:
--limit-file tools/ci/static-analysis-rules.yml
--xtensa-include-dir
check_pylint:
gcc_static_analyzer:
extends:
- .pre_check_template
- .rules:patterns:python-files
needs:
- pipeline_variables
artifacts:
reports:
codequality: pylint.json
paths:
- pylint.json
expire_in: 1 week
when: always
- .rules:patterns:clang_tidy
variables:
CI_CCACHE_DISABLE: 1
ANALYZING_APP: "examples/get-started/hello_world"
script:
- |
if [ -n "$CI_MERGE_REQUEST_IID" ]; then
export files=$(echo "$GIT_DIFF_OUTPUT" | grep ".py$" | xargs);
else
export files=$(git ls-files "*.py" | xargs);
fi
- if [ -z "$files" ]; then echo "No python files found"; exit 0; fi
- run_cmd pylint --exit-zero --load-plugins=pylint_gitlab --output-format=gitlab-codeclimate:pylint.json $files
#
## build stage
## Sonarqube related jobs put here for this reason:
## Here we have two jobs. code_quality_check and code_quality_report.
##
## code_quality_check will analyze the code changes between your MR and
## code repo stored in sonarqube server. The analysis result is only shown in
## the comments under this MR and won't be transferred to the server.
##
## code_quality_report will analyze and transfer both of the newly added code
## and the analysis result to the server.
##
## Put in the front to ensure that the newly merged code can be stored in
## sonarqube server ASAP, in order to avoid reporting unrelated code issues
#.sonar_scan_template:
# stage: build
# extends: .pre_check_template
# # full clone since this image does not support fetch --shallow-since-cutoff
# # shiny runners are used for full clone
# tags: [build, shiny]
# image: $SONARQUBE_SCANNER_IMAGE
# before_script:
# - source tools/ci/utils.sh
# - export PYTHONPATH="$CI_PROJECT_DIR/tools:$CI_PROJECT_DIR/tools/ci/python_packages:$PYTHONPATH"
# - fetch_submodules
# # Exclude the submodules, all paths ends with /**
# - submodules=$(get_all_submodules)
# # get all exclude paths specified in tools/ci/sonar_exclude_list.txt | ignore lines start with # | xargs | replace all <space> to <comma>
# - custom_excludes=$(cat $CI_PROJECT_DIR/tools/ci/sonar_exclude_list.txt | grep -v '^#' | xargs | sed -e 's/ /,/g')
# # Exclude the report dir as well
# - export EXCLUSIONS="$custom_excludes,$submodules"
# - export SONAR_SCANNER_OPTS="-Xmx2048m"
# variables:
# GIT_DEPTH: 0
# REPORT_PATTERN: clang_tidy_reports/**/*.txt
# artifacts:
# paths:
# - $REPORT_PATTERN
# expire_in: 1 week
# when: always
# dependencies: # Here is not a hard dependency relationship, could be skipped when only python files changed. so we do not use "needs" here.
# - clang_tidy_check
#
#code_quality_check:
# extends:
# - .sonar_scan_template
# - .rules:patterns:static-code-analysis-preview
# allow_failure: true # it's using exit code to indicate the code analysis result,
# # we don't want to block ci when critical issues founded
# script:
# - export CI_MERGE_REQUEST_COMMITS=$(python ${CI_PROJECT_DIR}/tools/ci/ci_get_mr_info.py commits --src-branch ${CI_COMMIT_REF_NAME} | tr '\n' ',')
# # test if this branch have merge request, if not, exit 0
# - test -n "$CI_MERGE_REQUEST_IID" || exit 0
# - test -n "$CI_MERGE_REQUEST_COMMITS" || exit 0
# - sonar-scanner
# -Dsonar.analysis.mode=preview
# -Dsonar.branch.name=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
# -Dsonar.cxx.clangtidy.reportPath=$REPORT_PATTERN
# -Dsonar.exclusions=$EXCLUSIONS
# -Dsonar.gitlab.ci_merge_request_iid=$CI_MERGE_REQUEST_IID
# -Dsonar.gitlab.commit_sha=$CI_MERGE_REQUEST_COMMITS
# -Dsonar.gitlab.merge_request_discussion=true
# -Dsonar.gitlab.ref_name=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
# -Dsonar.host.url=$SONAR_HOST_URL
# -Dsonar.login=$SONAR_LOGIN
#
#code_quality_report:
# extends:
# - .sonar_scan_template
# - .rules:protected
# allow_failure: true # it's using exit code to indicate the code analysis result,
# # we don't want to block ci when critical issues founded
# script:
# - sonar-scanner
# -Dsonar.branch.name=$CI_COMMIT_REF_NAME
# -Dsonar.cxx.clangtidy.reportPath=$REPORT_PATTERN
# -Dsonar.exclusions=$EXCLUSIONS
# -Dsonar.gitlab.commit_sha=$PIPELINE_COMMIT_SHA
# -Dsonar.gitlab.ref_name=$CI_COMMIT_REF_NAME
# -Dsonar.host.url=$SONAR_HOST_URL
# -Dsonar.login=$SONAR_LOGIN
- echo "CONFIG_COMPILER_STATIC_ANALYZER=y" >> ${ANALYZING_APP}/sdkconfig.defaults
- sed -i 's/.*MINIMAL_BUILD.*//g' ${ANALYZING_APP}/CMakeLists.txt
# no -Werror
- export EXTRA_CFLAGS=
- export EXTRA_CXXFLAGS=
- idf-build-apps build -p ${ANALYZING_APP} --no-check-warnings

View File

@@ -1,21 +1,16 @@
# Host tests
.host_test_win_template:
extends: .rules:test:host_test
extends:
- .windows-settings
- .rules:test:host_test
stage: host_test
image: $ESP_ENV_IMAGE
tags:
- windows-build
dependencies: # set dependencies to null to avoid missing artifacts issue
# run host_test jobs immediately, only after upload cache
needs:
- job: upload-pip-cache
optional: true
artifacts: false
- job: upload-submodules-cache
optional: true
artifacts: false
before_script: []
after_script: []
- job: manual_gate
optional: true
test_cli_installer_win:
rules:
@@ -30,14 +25,11 @@ 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"
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.16.3
- cd ${IDF_PATH}\tools\test_idf_tools
- python -m pip install jsonschema
- cd tools\test_idf_tools
- python .\test_idf_tools.py
- python .\test_idf_tools_python_env.py
@@ -48,84 +40,48 @@ 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 --enable-pytest
- .\export.ps1
- python "${SUBMODULE_FETCH_TOOL}" -s "all"
- cd ${IDF_PATH}/tools/test_idf_py
- pytest --noconftest test_idf_py.py --junitxml=${IDF_PATH}/XUNIT_IDF_PY.xml
- pytest --noconftest test_hints.py --junitxml=${IDF_PATH}/XUNIT_HINTS.xml
- 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 ${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 --enable-pytest
- . .\export.ps1
- python "${SUBMODULE_FETCH_TOOL}" -s "all"
- cd ${IDF_PATH}\tools\test_build_system
- python ${IDF_PATH}\tools\ci\get_known_failure_cases_file.py
- 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}
- 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_build_system_win:
extends:
- .test_build_system_template_win
- .rules:labels:windows_pytest_build_system
parallel: 2
tags:
- windows-build
needs:
- job: manual_gate
optional: true
artifacts:
paths:
- XUNIT_RESULT.xml
- test_build_system
expire_in: 2 days
reports:
junit: XUNIT_RESULT.xml
when: always
parallel: 6
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
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 --enable-pytest
- . .\export.ps1
- python ${IDF_PATH}\tools\idf_tools.py install cmake@${MINIMAL_CMAKE_VERSION}
- python tools\idf_tools.py install cmake@${MINIMAL_CMAKE_VERSION}
- $Env:PATH = "$Env:USERPROFILE\.espressif\tools\cmake\${MINIMAL_CMAKE_VERSION}\bin;$Env:PATH"
- |
$actualVersion = (& cmake --version).Split()[2]
@@ -133,7 +89,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
- python ${IDF_PATH}\tools\ci\get_known_failure_cases_file.py
- pytest -k cmake --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
- cd 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}

View File

@@ -6,26 +6,6 @@
stage: upload_cache
image: $ESP_ENV_IMAGE
upload-pip-cache:
extends:
- .upload_cache_template
- .before_script:minimal
- .rules:upload-python-cache
tags:
- $GEO
- cache
cache:
key: pip-cache-${LATEST_GIT_TAG}
paths:
- .cache/pip
policy: push
script:
- rm -rf .cache/pip # clear old packages
- bash install.sh --enable-ci --enable-pytest
parallel:
matrix:
- GEO: [ 'shiny', 'brew' ]
upload-submodules-cache:
extends:
- .upload_cache_template

63
.idf_build_apps.toml Normal file
View File

@@ -0,0 +1,63 @@
config_rules = [
'sdkconfig.ci=default',
'sdkconfig.ci.*=',
'=default',
]
extra_pythonpaths = [
'$IDF_PATH/tools/ci/python_packages',
'$IDF_PATH/tools/ci',
'$IDF_PATH/tools',
]
build_system = "idf_ci_local.app:IdfCMakeApp"
recursive = true
check_warnings = true
keep_going = true
copy_sdkconfig = true
ignore_warning_files = [
'$IDF_PATH/tools/ci/ignore_build_warnings.txt',
]
build_dir = "build_@t_@w"
build_log_filename = "build_log.txt"
size_json_filename = "size_${CI_JOB_ID}.json"
verbose = 1 # INFO
# collect
collect_app_info_filename = "app_info_${CI_JOB_ID}.txt"
junitxml = "build_summary_${CI_JOB_ID}.xml"
# manifest
check_manifest_rules = true
manifest_rootpath = "$IDF_PATH"
manifest_filepatterns = [
'**/.build-test-rules.yml',
]
# dependency-driven build
deactivate_dependency_driven_build_by_components = [
'cxx',
'esp_common',
'esp_hw_support',
'esp_rom',
'esp_system',
'esp_timer',
'freertos',
'hal',
'heap',
'log',
'newlib',
'riscv',
'soc',
'xtensa',
]
deactivate_dependency_driven_build_by_filepatterns = [
# tools
'tools/cmake/**/*',
'tools/tools.json',
# ci
'tools/ci/ignore_build_warnings.txt',
]

116
.idf_ci.toml Normal file
View File

@@ -0,0 +1,116 @@
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',
'tools/test_idf_py',
'tools/ci/test_autocomplete',
'tools/test_build_system',
'tools/bt/ble_log_console',
]
[local_runtime_envs]
EXTRA_CFLAGS = "-Werror -Werror=deprecated-declarations -Werror=unused-variable -Werror=unused-but-set-variable -Werror=unused-function -Wstrict-prototypes"
EXTRA_CXXFLAGS = "-Werror -Werror=deprecated-declarations -Werror=unused-variable -Werror=unused-but-set-variable -Werror=unused-function"
LDGEN_CHECK_MAPPING = "1"
IDF_CI_BUILD = "1"
[gitlab]
[gitlab.build_pipeline]
workflow_name = "build_child_pipeline"
runs_per_job = 30
job_tags = ['build', 'shiny']
job_template_name = '.dynamic_build_template'
job_template_jinja = '' # write in tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
pre_yaml_jinja = """
include:
- .gitlab/ci/common.yml
- tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
- tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml
"""
yaml_jinja = """
{{ settings.gitlab.build_pipeline.pre_yaml_jinja }}
workflow:
name: {{ settings.gitlab.build_pipeline.workflow_name }}
rules:
- when: always
{{ jobs }}
""" # simplified since we included the tools/ci/dynamic_pipelines/templates/test_child_pipeline.yml
[gitlab.test_pipeline]
job_template_name = '.dynamic_target_test_template'
job_template_jinja = '' # write in tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
pre_yaml_jinja = """
include:
- .gitlab/ci/common.yml
- tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
"""
[gitlab.artifacts.s3]
enable = true
[gitlab.artifacts.s3.configs.debug]
bucket = "idf-artifacts"
zip_first = true
build_dir_pattern = "**/build*/"
patterns = [
'bootloader/*.map',
'bootloader/*.elf',
'*.map',
'*.elf',
# customized
'esp_tee/*.map',
'esp_tee/*.elf',
'gdbinit/*',
]
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
[gitlab.artifacts.s3.configs.flash]
bucket = "idf-artifacts"
zip_first = true
build_dir_pattern = "**/build*/"
patterns = [
'bootloader/*.bin',
'*.bin',
'partition_table/*.bin',
'flasher_args.json',
'flash_project_args',
'config/sdkconfig.json',
'sdkconfig',
'project_description.json',
# customized
'esp_tee/*.bin',
]
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
[gitlab.artifacts.s3.configs.log]
bucket = "idf-artifacts"
patterns = [
'**/build*/build_log.txt',
'**/build*/size*.json',
]
[gitlab.artifacts.s3.configs.junit]
bucket = "idf-artifacts"
patterns = [
'**/XUNIT_RESULT_*.xml',
'**/build_summary_*.xml',
]
[gitlab.artifacts.s3.configs.env]
bucket = "idf-artifacts"
patterns = [
'**/pipeline.env',
]
[gitlab.artifacts.s3.configs.longterm]
bucket = "longterm"
if_clause = 'CI_COMMIT_REF_NAME == "master"'
patterns = [
'**/build*/size*.json',
]

View File

@@ -1,9 +1,15 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_stages: [commit]
default_stages: [pre-commit]
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.9.7"
hooks:
- id: ruff-format
- id: ruff
args: [ "--fix" ]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
@@ -14,6 +20,7 @@ repos:
# 2 - any file matching *test*/*expected* (for host tests, if possible use this naming pattern always)
# 3 - any directory named 'testdata'
# 4 - protobuf auto-generated files
# 5 - COPYING files
exclude: &whitespace_excludes |
(?x)^(
.+\.(md|rst|map|bin)|
@@ -41,21 +48,6 @@ repos:
- id: no-commit-to-branch
name: Do not use uppercase letters in the branch name
args: ['--pattern', '^[^A-Z]*[A-Z]']
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
args: ['--config=.flake8', '--tee', '--benchmark']
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.12.0
hooks:
- id: reorder-python-imports
name: Reorder Python imports
args: [--py38-plus]
exclude: >
(?x)^(
.*_pb2.py
)$
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
@@ -126,7 +118,7 @@ repos:
name: Check tools dir files patterns
entry: tools/ci/check_tools_files_patterns.py
language: python
files: '^tools/.+'
files: '^(tools/.+|.gitlab/ci/rules\.yml)'
additional_dependencies:
- PyYAML == 5.3.1
pass_filenames: false
@@ -150,11 +142,11 @@ repos:
name: Check if all apps readme files match given .build-test-rules.yml files. Modify the supported target tables
entry: tools/ci/check_build_test_rules.py check-readmes
language: python
files: 'tools/test_apps/.+|examples/.+|components/.+|tools/idf_py_actions/constants.py|tools/ci/check_build_test_rules.py'
files: 'tools/.+|examples/.+|components/.+'
require_serial: true
additional_dependencies:
- PyYAML == 5.3.1
- idf-build-apps~=2.5
- idf-build-apps~=2.16
- id: sort-yaml-files
name: sort yaml files
entry: tools/ci/sort_yaml.py
@@ -169,29 +161,12 @@ repos:
files: 'tools/ci/sort_yaml\.py$'
additional_dependencies:
- ruamel.yaml
- id: check-build-test-rules-path-exists
name: check path in .build-test-rules.yml exists
entry: tools/ci/check_build_test_rules.py check-exist
language: python
additional_dependencies:
- PyYAML == 5.3.1
always_run: true
pass_filenames: false
require_serial: true
- id: cleanup-ignore-lists
name: Remove non-existing patterns from ignore lists
entry: tools/ci/cleanup_ignore_lists.py
language: python
always_run: true
require_serial: true
- id: gitlab-yaml-linter
name: Check gitlab yaml files
entry: tools/ci/gitlab_yaml_linter.py
language: python
files: '\.gitlab-ci\.yml|\.gitlab/ci/.+\.yml|\.gitmodules'
pass_filenames: false
additional_dependencies:
- PyYAML == 5.3.1
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
@@ -245,3 +220,8 @@ repos:
hooks:
- id: check-kconfig-files
- id: check-deprecated-kconfig-options
- repo: https://github.com/espressif/idf-ci
rev: v1.2.1
hooks:
- id: check-tests-missing-config
files: 'pytest_.*\.py$|sdkconfig(\..*)?$'

View File

@@ -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:

View File

@@ -14,7 +14,10 @@ def do_test_quit(dut: Dut) -> None:
def do_test_help_generic(dut: Dut, registration_order: str) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console help command - {} registration"'.format(registration_order), expect_str='esp>')
dut.confirm_write(
f'"esp console help command - {registration_order} registration"',
expect_str='esp>',
)
dut.confirm_write('help', expect_str='aaa')
@@ -45,63 +48,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>')
@@ -126,45 +125,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>')
@@ -175,18 +170,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>')
@@ -196,18 +190,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>')
@@ -222,18 +215,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>')
@@ -241,22 +233,24 @@ def test_console_help_deregister(dut: Dut, test_on: str) -> None:
# in the test sequence, command a is registered before registering command z, then
# command a is deregistered; therefore, console shall not print command a's description
cmd_a_description, cmd_z_description = 'should appear first in help', 'should appear last in help'
cmd_a_description, cmd_z_description = (
'should appear first in help',
'should appear last in help',
)
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>')

View File

@@ -6,7 +6,8 @@ from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.temp_skip_ci(
targets=['esp32s2', 'esp32s3'], reason='eFuse for S2 and S3 is similar to the C3 chip, so testing on C3 is enough'
targets=['esp32s2', 'esp32s3'],
reason='eFuse for S2 and S3 is similar to the C3 chip, so testing on C3 is enough',
)
@pytest.mark.generic
@idf_parametrize('target', ['supported_targets'], indirect=['target'])
@@ -15,13 +16,11 @@ 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()
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_efuse_linux(dut: Dut) -> None:
dut.run_all_single_board_cases(timeout=60)

View File

@@ -11,7 +11,6 @@ def test_esp_app_format(dut: Dut) -> None:
dut.expect_unity_test_output()
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_esp_app_format_linux(dut: Dut) -> None:
dut.expect_unity_test_output()

View File

@@ -6,9 +6,7 @@ from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize(
'target', ['esp32h2', 'esp32c3', 'esp32s2', 'esp32s3', 'esp32c6'], indirect=['target']
)
@idf_parametrize('target', ['esp32h2', 'esp32c3', 'esp32s2', 'esp32s3', 'esp32c6'], indirect=['target'])
def test_external_coex_unit_test(dut: Dut) -> None:
dut.run_all_single_board_cases()
@@ -27,30 +25,30 @@ def test_external_coex_unit_test_esp32c2_xtal26m(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.esp32c2eco4
@pytest.mark.esp32c2_rev2
@pytest.mark.xtal_26mhz
@pytest.mark.generic
@pytest.mark.parametrize(
'config, baud',
[
('esp32c2eco4_xtal26m', '74880'),
('esp32c2_rev2_xtal26m', '74880'),
],
indirect=True,
)
@idf_parametrize('target', ['esp32c2'], indirect=['target'])
def test_external_coex_unit_test_esp32c2eco4_xtal26m(dut: Dut) -> None:
def test_external_coex_unit_test_esp32c2_rev2_xtal26m(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.esp32c3eco7
@pytest.mark.esp32c3_rev1
@pytest.mark.generic
@pytest.mark.parametrize(
'config',
[
'esp32c3eco7',
'esp32c3_rev1',
],
indirect=True,
)
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
def test_external_coex_unit_test_esp32c3eco7(dut: Dut) -> None:
def test_external_coex_unit_test_esp32c3_rev1(dut: Dut) -> None:
dut.run_all_single_board_cases()

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_esp_event_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=5)

View File

@@ -19,9 +19,11 @@ 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.xfail(
'config.getvalue("target") == "esp32c3"',
reason='Unstable on QEMU, needs investigation',
)
@pytest.mark.parametrize(
'config',
[
@@ -36,7 +38,6 @@ def test_esp_event_qemu(dut: Dut) -> None:
dut._run_normal_case(case)
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
@pytest.mark.parametrize(
'config',

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_esp_hw_support_linux(dut: Dut) -> None:
dut.run_all_single_board_cases(group='!ignore', timeout=120)

View File

@@ -1,34 +1,10 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import functools
from typing import Callable
from typing import Dict
from typing import List
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
def target_list(targets: List[str]) -> Callable:
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: List, **kwargs: Dict) -> Callable:
return func(*args, **kwargs) # type: ignore
for target in targets:
wrapper = pytest.mark.__getattr__(target)(wrapper)
return wrapper
return decorator
# SOC_PAU_SUPPORTED == 1
retention_targets = ['esp32c6', 'esp32h2', 'esp32p4', 'esp32c5', 'esp32c61']
@target_list(retention_targets)
@pytest.mark.generic
@idf_parametrize('target', ['esp32c61', 'esp32c5', 'esp32p4', 'esp32h2', 'esp32c6'], indirect=['target'])
def test_sleep_retention(dut: Dut) -> None:

View File

@@ -3,6 +3,7 @@
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
# normal mmu tests
@@ -20,14 +21,6 @@ def test_mmap(dut: Dut) -> None:
# mmu tests with psram enabled
PSRAM_RELEASE_CONFIGS = [
pytest.param('psram_release_esp32', marks=[pytest.mark.esp32]),
pytest.param('psram_release_esp32s2', marks=[pytest.mark.esp32s2]),
pytest.param('psram_release_esp32s3', marks=[pytest.mark.esp32s3]),
pytest.param('psram_release_esp32p4', marks=[pytest.mark.esp32p4]),
]
@pytest.mark.generic
@idf_parametrize(
'config,target',
@@ -44,12 +37,6 @@ def test_mmap_psram(dut: Dut) -> None:
# mmu tests with xip_psram
XIP_CONFIGS = [
pytest.param('xip_psram_esp32s2', marks=[pytest.mark.esp32s2]),
pytest.param('xip_psram_esp32s3', marks=[pytest.mark.esp32s3]),
]
@pytest.mark.generic
@idf_parametrize(
'config,target', [('xip_psram_esp32s2', 'esp32s2'), ('xip_psram_esp32s3', 'esp32s3')], indirect=['config', 'target']

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_esp_partition_linux(dut: Dut) -> None:
dut.expect_unity_test_output(timeout=5)

View File

@@ -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',
@@ -46,7 +45,6 @@ def test_esp_ringbuf_qemu(dut: Dut) -> None:
dut._run_normal_case(case)
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_esp_ringbuf_posix_simulator(dut: Dut) -> None:
dut.run_all_single_board_cases(group='linux')

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_esp_rom_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=5)

View File

@@ -8,14 +8,22 @@ from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.generic
@idf_parametrize(
'target',
['esp32c3', 'esp32s2', 'esp32s3', 'esp32c5', 'esp32c6', 'esp32c61', 'esp32h2', 'esp32p4'],
[
'esp32c3',
'esp32s2',
'esp32s3',
'esp32c5',
'esp32c6',
'esp32c61',
'esp32h2',
'esp32p4',
],
indirect=['target'],
)
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:

View File

@@ -15,15 +15,6 @@ def not_expect(dut: Dut, output_regex: str) -> None:
raise RuntimeError(f'Found not_expect output {output_regex}')
JTAG_SERIAL_MARKS = [
pytest.mark.esp32s3,
pytest.mark.esp32c2,
pytest.mark.esp32c3,
pytest.mark.esp32c6,
pytest.mark.esp32h2,
]
@pytest.mark.generic
@idf_parametrize('config', ['console_none'], indirect=['config'])
@idf_parametrize('target', ['supported_targets'], indirect=['target'])

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_esp_system_linux(dut: Dut) -> None:
dut.run_all_single_board_cases(timeout=60)

View File

@@ -4,19 +4,6 @@ import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
CONFIGS = [
pytest.param('general', marks=[pytest.mark.supported_targets]),
pytest.param('release', marks=[pytest.mark.supported_targets]),
pytest.param('single_core', marks=[pytest.mark.esp32]),
pytest.param('freertos_compliance', marks=[pytest.mark.esp32]),
pytest.param('isr_dispatch_esp32', marks=[pytest.mark.esp32]),
pytest.param('isr_dispatch_esp32c3', marks=[pytest.mark.esp32c3]),
pytest.param('cpu1_esp32', marks=[pytest.mark.esp32]),
pytest.param('any_cpu_esp32', marks=[pytest.mark.esp32]),
pytest.param('cpu1_esp32s3', marks=[pytest.mark.esp32s3]),
pytest.param('any_cpu_esp32s3', marks=[pytest.mark.esp32s3]),
]
@pytest.mark.generic
@idf_parametrize(

View File

@@ -36,28 +36,28 @@ def test_wifi_connect_cases_esp32c2_xtal26m(case_tester: CaseTester) -> None:
case_tester.run_all_cases()
@pytest.mark.esp32c2eco4
@pytest.mark.esp32c2_rev2
@pytest.mark.two_duts
@pytest.mark.xtal_26mhz
@pytest.mark.parametrize(
'count, config, target, baud',
[
(2, 'esp32c2eco4_xtal26m', 'esp32c2', '74880'),
(2, 'esp32c2_rev2_xtal26m', 'esp32c2', '74880'),
],
indirect=True,
)
def test_wifi_connect_cases_esp32c2eco4_xtal26m(case_tester: CaseTester) -> None:
def test_wifi_connect_cases_esp32c2_rev2_xtal26m(case_tester: CaseTester) -> None:
case_tester.run_all_cases()
@pytest.mark.two_duts
@pytest.mark.esp32c3eco7
@pytest.mark.esp32c3_rev1
@pytest.mark.parametrize(
'count, config, target',
[
(2, 'esp32c3eco7', 'esp32c3'),
(2, 'esp32c3_rev1', 'esp32c3'),
],
indirect=True,
)
def test_wifi_connect_cases_esp32c3eco7(case_tester: CaseTester) -> None:
def test_wifi_connect_cases_esp32c3_rev1(case_tester: CaseTester) -> None:
case_tester.run_all_cases()

View File

@@ -3,6 +3,7 @@
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
# @pytest.mark.esp32c2 # esp32c2 are using xtal_26mhz
@@ -28,26 +29,26 @@ def test_wifi_unit_test_esp32c2_xtal26m(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.esp32c2eco4
@pytest.mark.esp32c2_rev2
@pytest.mark.xtal_26mhz
@pytest.mark.generic
@pytest.mark.parametrize(
'config, target, baud',
[
('esp32c2eco4_xtal26m', 'esp32c2', '74880'),
('esp32c2_rev2_xtal26m', 'esp32c2', '74880'),
],
indirect=True,
)
def test_wifi_unit_test_esp32c2eco4_xtal26m(dut: Dut) -> None:
def test_wifi_unit_test_esp32c2_rev2_xtal26m(dut: Dut) -> None:
dut.run_all_single_board_cases()
@pytest.mark.esp32c3eco7
@pytest.mark.esp32c3_rev1
@pytest.mark.generic
@pytest.mark.parametrize(
'config, target',
[('esp32c3eco7', 'esp32c3')],
[('esp32c3_rev1', 'esp32c3')],
indirect=True,
)
def test_wifi_unit_test_esp32c3eco7(dut: Dut) -> None:
def test_wifi_unit_test_esp32c3_rev1(dut: Dut) -> None:
dut.run_all_single_board_cases()

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_fatfs_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=120)

View File

@@ -4,28 +4,6 @@ import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
CONFIGS = [
pytest.param(
'default',
marks=[
pytest.mark.supported_targets,
],
),
pytest.param('freertos_options', marks=[pytest.mark.supported_targets]),
pytest.param('psram', marks=[pytest.mark.esp32, pytest.mark.esp32s3, pytest.mark.esp32p4, pytest.mark.esp32c5]),
pytest.param('single_core', marks=[pytest.mark.esp32, pytest.mark.esp32p4]),
# TODO: [ESP32C5] IDF-10335
# TODO: [ESP32C61] IDF-11146
pytest.param(
'smp',
marks=[
pytest.mark.supported_targets,
pytest.mark.temp_skip_ci(targets=['esp32p4', 'esp32c5', 'esp32c61'], reason='test failed/TBD IDF-8113'),
],
),
pytest.param('tickless_idle', marks=[pytest.mark.supported_targets]),
]
@pytest.mark.generic
@idf_parametrize(
@@ -34,9 +12,6 @@ CONFIGS = [
('default', 'supported_targets'),
('freertos_options', 'supported_targets'),
('psram', 'esp32'),
('psram', 'esp32c5'),
('psram', 'esp32p4'),
('psram', 'esp32s3'),
('single_core', 'esp32'),
('single_core', 'esp32p4'),
(

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_hal_utils(dut: Dut) -> None:
dut.run_all_single_board_cases()

View File

@@ -20,11 +20,14 @@ 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',
[('no_poisoning', 'idf,qemu'), ('light_poisoning', 'idf,qemu'), ('comprehensive_poisoning', 'idf,qemu')],
[
('no_poisoning', 'idf,qemu'),
('light_poisoning', 'idf,qemu'),
('comprehensive_poisoning', 'idf,qemu'),
],
)
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
def test_heap_poisoning_qemu(dut: Dut) -> None:

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_heap_linux(dut: Dut) -> None:
dut.run_all_single_board_cases(timeout=60)

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import pytest
from pytest_embedded_idf.dut import IdfDut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_linux_component(dut: IdfDut) -> None:
dut.expect_exact('Press ENTER to see the list of tests.')

View File

@@ -5,12 +5,12 @@ from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@pytest.mark.parametrize(
'config',
[
'default',
'system_timestamptag_level_linked_list',
'system_timestamp',
'tag_level_linked_list',
'tag_level_linked_list_and_array_cache',
'tag_level_none',
],

View File

@@ -1,11 +0,0 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_nvs_host_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=60)

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_nvs_page_linux(dut: Dut) -> None:
dut.expect_unity_test_output(timeout=120)

View File

@@ -4,11 +4,6 @@ import pytest
from pytest_embedded_idf.dut import IdfDut
from pytest_embedded_idf.utils import idf_parametrize
CONFIGS_NVS_ENCR_FLASH_ENC = [
pytest.param('nvs_encr_flash_enc_esp32', marks=[pytest.mark.esp32]),
pytest.param('nvs_encr_flash_enc_esp32c3', marks=[pytest.mark.esp32c3]),
]
@pytest.mark.generic
@pytest.mark.parametrize('config', ['default'], indirect=True)

View File

@@ -20,7 +20,9 @@ def test_pthread(dut: Dut) -> None:
@pytest.mark.generic
@idf_parametrize(
'config,target', [('single_core_esp32', 'esp32'), ('single_core_esp32s3', 'esp32s3')], indirect=['config', 'target']
'config,target',
[('single_core_esp32', 'esp32'), ('single_core_esp32s3', 'esp32s3')],
indirect=['config', 'target'],
)
def test_pthread_single_core(dut: Dut) -> None:
dut.run_all_single_board_cases(group='!thread-specific', timeout=300)
@@ -46,7 +48,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:
@@ -55,7 +56,6 @@ def test_pthread_qemu(dut: Dut) -> None:
dut._run_normal_case(case, timeout=75)
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_pthread_linux(dut: Dut) -> None:
dut.run_all_single_board_cases(timeout=120)

View File

@@ -5,7 +5,6 @@ from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@pytest.mark.parametrize('config', ['erase_check', 'no_erase_check'])
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_spiffs_linux(dut: Dut) -> None:

View File

@@ -4,11 +4,6 @@ import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
CONFIGS = [
pytest.param('default', marks=[pytest.mark.esp32, pytest.mark.esp32c3]),
pytest.param('psram_esp32', marks=[pytest.mark.esp32]),
]
@pytest.mark.generic
@idf_parametrize(

View File

@@ -4,11 +4,6 @@ import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
CONFIGS = [
pytest.param('default', marks=[pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32p4]),
pytest.param('esp32p4_psram', marks=[pytest.mark.esp32p4]),
]
@pytest.mark.usb_host_flash_disk
@pytest.mark.temp_skip_ci(targets=['esp32s2'], reason='lack of runners with usb_host_flash_disk tag')

View File

@@ -1,11 +1,9 @@
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Unlicense OR CC0-1.0
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_wear_levelling_linux(dut: Dut) -> None:
dut.expect_exact('All tests passed', timeout=120)

View File

@@ -34,40 +34,40 @@ def test_wpa_supplicant_ut_offchan(case_tester: CaseTester) -> None:
@pytest.mark.two_duts
@pytest.mark.esp32c2eco4
@pytest.mark.esp32c2_rev2
@pytest.mark.xtal_26mhz
@pytest.mark.parametrize(
'count, config, target, baud',
[
(
2,
'esp32c2eco4_xtal26m',
'esp32c2_rev2_xtal26m',
'esp32c2',
'74880',
),
],
indirect=True,
)
def test_wpa_supplicant_esp32c2eco4_xtal26mhz(case_tester: CaseTester) -> None:
def test_wpa_supplicant_esp32c2_rev2_xtal26mhz(case_tester: CaseTester) -> None:
for case in case_tester.test_menu:
if case.attributes.get('test_env') == 'two_duts':
case_tester.run_multi_dev_case(case=case, reset=True)
@pytest.mark.two_duts
@pytest.mark.esp32c3eco7
@pytest.mark.esp32c3_rev1
@pytest.mark.parametrize(
'count, config, target',
[
(
2,
'esp32c3eco7',
'esp32c3_rev1',
'esp32c3',
),
],
indirect=True,
)
def test_wpa_supplicant_esp32c3eco7(case_tester: CaseTester) -> None:
def test_wpa_supplicant_esp32c3_rev1(case_tester: CaseTester) -> None:
for case in case_tester.test_menu:
if case.attributes.get('test_env') == 'two_duts':
case_tester.run_multi_dev_case(case=case, reset=True)

View File

@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
# pylint: disable=W0621 # redefined-outer-name
#
@@ -9,6 +9,7 @@
# please report to https://github.com/espressif/pytest-embedded/issues
# or discuss at https://github.com/espressif/pytest-embedded/discussions
import os
import subprocess
import sys
if os.path.join(os.path.dirname(__file__), 'tools', 'ci') not in sys.path:
@@ -17,8 +18,6 @@ if os.path.join(os.path.dirname(__file__), 'tools', 'ci') not in sys.path:
if os.path.join(os.path.dirname(__file__), 'tools', 'ci', 'python_packages') not in sys.path:
sys.path.append(os.path.join(os.path.dirname(__file__), 'tools', 'ci', 'python_packages'))
import glob
import io
import json
import logging
import os
@@ -26,7 +25,6 @@ import re
import signal
import time
import typing as t
import zipfile
from copy import deepcopy
from urllib.parse import quote
@@ -34,20 +32,19 @@ import common_test_methods # noqa: F401
import gitlab_api
import pexpect
import pytest
import requests
import yaml
from _pytest.config import Config
from _pytest.fixtures import FixtureRequest
from artifacts_handler import ArtifactType
from dynamic_pipelines.constants import TEST_RELATED_APPS_DOWNLOAD_URLS_FILENAME
from idf_ci_local.app import import_apps_from_txt
from idf_ci_local.uploader import AppDownloader, AppUploader
from idf_ci_utils import IDF_PATH, idf_relpath
from idf_pytest.constants import DEFAULT_SDKCONFIG, ENV_MARKERS, SPECIAL_MARKERS, TARGET_MARKERS, PytestCase, \
DEFAULT_LOGDIR
from idf_pytest.plugin import IDF_PYTEST_EMBEDDED_KEY, ITEM_PYTEST_CASE_KEY, IdfPytestEmbedded
from idf_ci import PytestCase
from idf_ci.idf_pytest import IDF_CI_PYTEST_CASE_KEY
from idf_ci_utils import IDF_PATH
from idf_ci_utils import idf_relpath
from idf_pytest.constants import DEFAULT_LOGDIR
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 multi_dut_argument, multi_dut_fixture
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
from pytest_embedded_idf.dut import IdfDut
@@ -74,23 +71,6 @@ def case_tester(unity_tester: CaseTester) -> CaseTester:
return unity_tester
@pytest.fixture
@multi_dut_argument
def config(request: FixtureRequest) -> str:
return getattr(request, 'param', None) or DEFAULT_SDKCONFIG # type: ignore
@pytest.fixture
@multi_dut_fixture
def target(request: FixtureRequest, dut_total: int, dut_index: int) -> str:
plugin = request.config.stash[IDF_PYTEST_EMBEDDED_KEY]
if dut_total == 1:
return plugin.target[0] # type: ignore
return plugin.target[dut_index] # type: ignore
@pytest.fixture
def test_func_name(request: FixtureRequest) -> str:
return request.node.function.__name__ # type: ignore
@@ -117,33 +97,69 @@ def pipeline_id(request: FixtureRequest) -> t.Optional[str]:
return request.config.getoption('pipeline_id', None) or os.getenv('PARENT_PIPELINE_ID', None) # type: ignore
class BuildReportDownloader(AppDownloader):
def __init__(self, presigned_url_yaml: str) -> None:
self.app_presigned_urls_dict: t.Dict[str, t.Dict[str, str]] = yaml.safe_load(presigned_url_yaml)
def get_pipeline_commit_sha_by_pipeline_id(pipeline_id: str) -> t.Optional[str]:
gl = gitlab_api.Gitlab(os.getenv('CI_PROJECT_ID', 'espressif/esp-idf'))
pipeline = gl.project.pipelines.get(pipeline_id)
if not pipeline:
return None
def _download_app(self, app_build_path: str, artifact_type: ArtifactType) -> None:
url = self.app_presigned_urls_dict[app_build_path][artifact_type.value]
commit = gl.project.commits.get(pipeline.sha)
if not commit or not commit.parent_ids:
return None
logging.info('Downloading app from %s', url)
with io.BytesIO() as f:
for chunk in requests.get(url).iter_content(chunk_size=1024 * 1024):
if chunk:
f.write(chunk)
if len(commit.parent_ids) == 1:
return commit.parent_ids[0] # type: ignore
f.seek(0)
for parent_id in commit.parent_ids:
parent_commit = gl.project.commits.get(parent_id)
if parent_commit.parent_ids and len(parent_commit.parent_ids) == 1:
return parent_id # type: ignore
with zipfile.ZipFile(f) as zip_ref:
zip_ref.extractall(IDF_PATH)
return None
def download_app(self, app_build_path: str, artifact_type: t.Optional[ArtifactType] = None) -> None:
if app_build_path not in self.app_presigned_urls_dict:
raise ValueError(
f'No presigned url found for {app_build_path}. '
f'Usually this should not happen, please re-trigger a pipeline.'
f'If this happens again, please report this bug to the CI channel.'
)
super().download_app(app_build_path, artifact_type)
class AppDownloader:
def __init__(
self,
commit_sha: str,
pipeline_id: t.Optional[str] = None,
) -> None:
self.commit_sha = commit_sha
self.pipeline_id = pipeline_id
def download_app(self, app_dir: str, build_dir: str, artifact_type: t.Optional[str] = None) -> None:
args = [
'idf-ci',
'gitlab',
'download-artifacts',
'--commit-sha',
self.commit_sha,
]
if artifact_type:
args.extend(['--type', artifact_type])
if self.pipeline_id:
args.extend(['--pipeline-id', self.pipeline_id])
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)
PRESIGNED_JSON = 'presigned.json'
class OpenOCD:
@@ -166,7 +182,7 @@ class OpenOCD:
desc_path = os.path.join(self.dut.app.binary_path, 'project_description.json')
try:
with open(desc_path, 'r') as f:
with open(desc_path) as f:
project_desc = json.load(f)
except FileNotFoundError:
logging.error('Project description file not found at %s', desc_path)
@@ -184,7 +200,7 @@ class OpenOCD:
ocd_env = os.environ.copy()
ocd_env['LIBUSB_DEBUG'] = '1'
for _ in range(1, self.MAX_RETRIES + 1):
for attempt in range(1, self.MAX_RETRIES + 1):
try:
self.proc = pexpect.spawn(
command='openocd',
@@ -197,10 +213,12 @@ class OpenOCD:
if self.proc and self.proc.isalive():
self.proc.expect_exact('Info : Listening on port 3333 for gdb connections', timeout=5)
self.connect_telnet()
self.write('log_output {}'.format(self.log_file))
self.write(f'log_output {self.log_file}')
return self
except (pexpect.exceptions.EOF, pexpect.exceptions.TIMEOUT, ConnectionRefusedError) as e:
logging.error('Error running OpenOCD: %s', str(e))
logging.error(
'OpenOCD connection attempt %d/%d failed. Error: %s', attempt, self.MAX_RETRIES, type(e).__name__
)
self.kill()
time.sleep(self.RETRY_DELAY)
@@ -255,38 +273,22 @@ def openocd_dut(dut: IdfDut) -> OpenOCD:
@pytest.fixture(scope='session')
def app_downloader(pipeline_id: t.Optional[str]) -> t.Optional[AppDownloader]:
if commit_sha := os.getenv('PIPELINE_COMMIT_SHA'):
logging.debug('pipeline commit sha from CI env is %s', commit_sha)
return AppDownloader(commit_sha, None)
if not pipeline_id:
return None
if (
'IDF_S3_BUCKET' in os.environ
and 'IDF_S3_ACCESS_KEY' in os.environ
and 'IDF_S3_SECRET_KEY' in os.environ
and 'IDF_S3_SERVER' in os.environ
and 'IDF_S3_BUCKET' in os.environ
):
return AppUploader(pipeline_id)
commit_sha = get_pipeline_commit_sha_by_pipeline_id(pipeline_id)
if not commit_sha:
raise ValueError(
'commit sha cannot be found for pipeline id %s. Please check the pipeline id. '
'If you think this is a bug, please report it to CI team',
)
logging.debug('pipeline commit sha of pipeline %s is %s', pipeline_id, commit_sha)
logging.info('Downloading build report from the build pipeline %s', pipeline_id)
test_app_presigned_urls_file = None
gl = gitlab_api.Gitlab(os.getenv('CI_PROJECT_ID', 'espressif/esp-idf'))
for child_pipeline in gl.project.pipelines.get(pipeline_id, lazy=True).bridges.list(iterator=True):
if child_pipeline.name == 'build_child_pipeline':
for job in gl.project.pipelines.get(child_pipeline.downstream_pipeline['id'], lazy=True).jobs.list(
iterator=True
):
if job.name == 'generate_pytest_build_report':
test_app_presigned_urls_file = gl.download_artifact(
job.id, [TEST_RELATED_APPS_DOWNLOAD_URLS_FILENAME]
)[0]
break
if test_app_presigned_urls_file:
return BuildReportDownloader(test_app_presigned_urls_file)
return None
return AppDownloader(commit_sha, pipeline_id)
@pytest.fixture
@@ -310,14 +312,21 @@ def build_dir(
valid build directory
"""
# download from minio on CI
case: PytestCase = request._pyfuncitem.stash[ITEM_PYTEST_CASE_KEY]
if app_downloader:
case: PytestCase = request.node.stash[IDF_CI_PYTEST_CASE_KEY]
if 'skip_app_downloader' in case.all_markers:
logging.debug('skip_app_downloader marker found, skip downloading app')
downloader = None
else:
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 case.requires_elf_or_map:
app_downloader.download_app(app_build_path)
if requires_elf_or_map(case):
downloader.download_app(app_dir, build_dir)
else:
app_downloader.download_app(app_build_path, ArtifactType.BUILD_DIR_WITHOUT_MAP_AND_ELF_FILES)
downloader.download_app(app_dir, build_dir, 'flash')
check_dirs = [f'build_{target}_{config}']
else:
check_dirs = []
@@ -438,7 +447,7 @@ def check_performance(idf_path: str) -> t.Callable[[str, float, str], None]:
def _find_perf_item(operator: str, path: str) -> float:
with open(path, encoding='utf-8') as f:
data = f.read()
match = re.search(fr'#define\s+IDF_PERFORMANCE_{operator}_{item.upper()}\s+([\d.]+)', data)
match = re.search(rf'#define\s+IDF_PERFORMANCE_{operator}_{item.upper()}\s+([\d.]+)', data)
return float(match.group(1)) # type: ignore
def _check_perf(operator: str, standard_value: float) -> None:
@@ -477,16 +486,18 @@ def check_performance(idf_path: str) -> t.Callable[[str, float, str], None]:
@pytest.fixture
def log_minimum_free_heap_size(dut: IdfDut, config: str) -> t.Callable[..., None]:
def log_minimum_free_heap_size(dut: IdfDut, config: str, idf_path: str) -> t.Callable[..., None]:
def real_func() -> None:
res = dut.expect(r'Minimum free heap size: (\d+) bytes')
logging.info(
'\n------ heap size info ------\n'
'[app_path] {}\n'
'[app_name] {}\n'
'[config_name] {}\n'
'[target] {}\n'
'[minimum_free_heap_size] {} Bytes\n'
'------ heap size end ------'.format(
dut.app.app_path.replace(idf_path, '').lstrip('/\\'),
os.path.basename(dut.app.app_path),
config,
dut.target,
@@ -499,12 +510,12 @@ def log_minimum_free_heap_size(dut: IdfDut, config: str) -> t.Callable[..., None
@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
##################
@@ -512,10 +523,6 @@ def dev_user(request: FixtureRequest) -> str:
##################
def pytest_addoption(parser: pytest.Parser) -> None:
idf_group = parser.getgroup('idf')
idf_group.addoption(
'--sdkconfig',
help='sdkconfig postfix, like sdkconfig.ci.<config>. (Default: None, which would build all found apps)',
)
idf_group.addoption(
'--dev-user',
help='user name associated with some specific device/service used during the test execution',
@@ -524,80 +531,34 @@ def pytest_addoption(parser: pytest.Parser) -> None:
'--dev-passwd',
help='password associated with some specific device/service used during the test execution',
)
idf_group.addoption(
'--app-info-filepattern',
help='glob pattern to specify the files that include built app info generated by '
'`idf-build-apps --collect-app-info ...`. will not raise ValueError when binary '
'paths not exist in local file system if not listed recorded in the app info.',
)
idf_group.addoption(
'--pipeline-id',
help='main pipeline id, not the child pipeline id. Specify this option to download the artifacts '
'from the minio server for debugging purpose.',
help='For users without s3 access. main pipeline id, not the child pipeline id. '
'Specify this option to download the artifacts from the minio server for debugging purpose.',
)
def pytest_configure(config: Config) -> None:
# cli option "--target"
target = [_t.strip().lower() for _t in (config.getoption('target', '') or '').split(',') if _t.strip()]
from idf_pytest.constants import PREVIEW_TARGETS
from idf_pytest.constants import SUPPORTED_TARGETS
from pytest_embedded_idf.utils import preview_targets
from pytest_embedded_idf.utils import supported_targets
# add markers based on idf_pytest/constants.py
for name, description in {
**TARGET_MARKERS,
**ENV_MARKERS,
**SPECIAL_MARKERS,
}.items():
config.addinivalue_line('markers', f'{name}: {description}')
supported_targets.set(SUPPORTED_TARGETS)
preview_targets.set(PREVIEW_TARGETS)
help_commands = ['--help', '--fixtures', '--markers', '--version']
for cmd in help_commands:
if cmd in config.invocation_params.args:
target = ['unneeded']
break
markexpr = config.getoption('markexpr') or ''
# check marker expr set via "pytest -m"
if not target and markexpr:
# we use `-m "esp32 and generic"` in our CI to filter the test cases
# this doesn't cover all use cases, but fit what we do in CI.
for marker in markexpr.split('and'):
marker = marker.strip()
if marker in TARGET_MARKERS:
target.append(marker)
# "--target" must be set
if not target:
raise SystemExit(
"""Pass `--target TARGET[,TARGET...]` to specify all targets the test cases are using.
- for single DUT, we run with `pytest --target esp32`
- for multi DUT, we run with `pytest --target esp32,esp32,esp32s2` to indicate all DUTs
"""
)
apps = None
app_info_filepattern = config.getoption('app_info_filepattern')
if app_info_filepattern:
apps = []
for f in glob.glob(os.path.join(IDF_PATH, app_info_filepattern)):
apps.extend(import_apps_from_txt(f))
if '--collect-only' not in config.invocation_params.args:
config.stash[IDF_PYTEST_EMBEDDED_KEY] = IdfPytestEmbedded(
config_name=config.getoption('sdkconfig'),
target=target,
apps=apps,
)
config.pluginmanager.register(config.stash[IDF_PYTEST_EMBEDDED_KEY])
config.stash[IDF_LOCAL_PLUGIN_KEY] = IdfLocalPlugin()
config.pluginmanager.register(config.stash[IDF_LOCAL_PLUGIN_KEY])
def pytest_unconfigure(config: Config) -> None:
_pytest_embedded = config.stash.get(IDF_PYTEST_EMBEDDED_KEY, None)
if _pytest_embedded:
del config.stash[IDF_PYTEST_EMBEDDED_KEY]
config.pluginmanager.unregister(_pytest_embedded)
idf_local_plugin = config.stash.get(IDF_LOCAL_PLUGIN_KEY, None)
if idf_local_plugin:
del config.stash[IDF_LOCAL_PLUGIN_KEY]
config.pluginmanager.unregister(idf_local_plugin)
dut_artifacts_url = []
dut_artifacts_url: t.List[str] = []
@pytest.hookimpl(hookwrapper=True)
@@ -620,10 +581,10 @@ def pytest_runtest_makereport(item, call): # type: ignore
if isinstance(_dut, list):
logs_files.extend([template.format(get_path(d.logfile)) for d in _dut])
dut_artifacts_url.append('{}:'.format(_dut[0].test_case_name))
dut_artifacts_url.append(f'{_dut[0].test_case_name}:')
else:
logs_files.append(template.format(get_path(_dut.logfile)))
dut_artifacts_url.append('{}:'.format(_dut.test_case_name))
dut_artifacts_url.append(f'{_dut.test_case_name}:')
for file in logs_files:
dut_artifacts_url.append(' - {}'.format(quote(file, safe=':/')))

View File

@@ -33,13 +33,13 @@ On the host side, ESP-IDF employs the pytest framework (alongside certain pytest
Installation
============
All basic dependencies could be installed by running the ESP-IDF install script with the ``--enable-pytest`` argument:
All basic dependencies could be installed by running the ESP-IDF install script with the ``--enable-ci`` argument:
.. code-block:: bash
$ install.sh --enable-pytest
$ install.sh --enable-ci
Additional test script specific dependencies could be installed separately by running the ESP-IDF install script with the ``--enable-pytest-specific`` argument:
Additional test script specific dependencies could be installed separately by running the ESP-IDF install script with the ``--enable-test-specific`` argument:
.. code-block:: bash
@@ -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)
---------------------------------
@@ -492,7 +581,7 @@ First you need to install ESP-IDF with additional Python requirements:
.. code-block:: shell
$ cd $IDF_PATH
$ bash install.sh --enable-ci --enable-pytest
$ bash install.sh --enable-ci
$ . ./export.sh
Build Directories

View File

@@ -33,13 +33,13 @@ ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自
安装
============
基础依赖项可以通过执行 ESP-IDF 安装脚本 ``--enable-pytest`` 进行安装:
基础依赖项可以通过执行 ESP-IDF 安装脚本 ``--enable-ci`` 进行安装:
.. code-block:: bash
$ install.sh --enable-pytest
$ install.sh --enable-ci
额外的测试脚本依赖项可以通过执行 ESP-IDF 安装脚本 ``--enable-pytest-specific`` 进行安装:
额外的测试脚本依赖项可以通过执行 ESP-IDF 安装脚本 ``--enable-test-specific`` 进行安装:
.. code-block:: bash
@@ -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 测试用例。
当测试逻辑相同,但执行环境不同的时候,可使用此模式。
测试串行输出
^^^^^^^^^^^^^^^^
@@ -492,7 +581,7 @@ CI 的工作流程如下所示:
.. code-block:: shell
$ cd $IDF_PATH
$ bash install.sh --enable-ci --enable-pytest
$ bash install.sh --enable-ci
$ . ./export.sh
编译目录

View File

@@ -11,7 +11,7 @@ https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/esp-ble-m
(See the README.md file in the upper level 'examples' directory for more information about examples.)
This example implements Bluetooth Mesh node that supports On/Off and Level models.
It has suport for both Advertising Bearer and GATT Bearer.
It has support for both Advertising Bearer and GATT Bearer.
For more information on NimBLE MESH, please visit [NimBLE_MESH](https://mynewt.apache.org/latest/network/mesh/index.html#bluetooth-mesh).

View File

@@ -1,22 +1,24 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
import os.path
from pathlib import Path
from typing import Tuple
import pexpect
import pytest
from pytest_embedded_idf.dut import IdfDut
from pytest_embedded_idf.utils import idf_parametrize
CUR_DIR = Path(__file__).parent.resolve()
# Case 1: BLE power save test
@pytest.mark.two_duts
@pytest.mark.parametrize(
'count, app_path',
[
(
2,
f'{os.path.join(os.path.dirname(__file__), "power_save")}|{os.path.join(os.path.dirname(__file__), "blecent")}',
f'{str(CUR_DIR / "power_save")}|{str(CUR_DIR / "blecent")}',
),
],
indirect=True,
@@ -47,7 +49,7 @@ def test_power_save_conn(app_path: str, dut: Tuple[IdfDut, IdfDut]) -> None:
(
'esp32c2_xtal26m',
2,
f'{os.path.join(os.path.dirname(__file__), "power_save")}|{os.path.join(os.path.dirname(__file__), "blecent")}',
f'{str(CUR_DIR / "power_save")}|{str(CUR_DIR / "blecent")}',
'74880',
),
],
@@ -71,21 +73,21 @@ def test_power_save_conn_esp32c2_26mhz(dut: Tuple[IdfDut, IdfDut]) -> None:
# Case 2: BLE power save test for ESP32C2ECO4
@pytest.mark.two_duts
@pytest.mark.xtal_26mhz
@pytest.mark.esp32c2eco4
@pytest.mark.esp32c2_rev2
@pytest.mark.parametrize(
'config, count, target, app_path, baud',
[
(
'esp32c2eco4_xtal26m',
'esp32c2_rev2_xtal26m',
2,
'esp32c2',
f'{os.path.join(os.path.dirname(__file__), "power_save")}|{os.path.join(os.path.dirname(__file__), "blecent")}',
f'{str(CUR_DIR / "power_save")}|{str(CUR_DIR / "blecent")}',
'74880',
),
],
indirect=True,
)
def test_power_save_conn_esp32c2eco4(dut: Tuple[IdfDut, IdfDut]) -> None:
def test_power_save_conn_esp32c2_rev2(dut: Tuple[IdfDut, IdfDut]) -> None:
peripheral = dut[0]
central = dut[1]
@@ -101,20 +103,20 @@ def test_power_save_conn_esp32c2eco4(dut: Tuple[IdfDut, IdfDut]) -> None:
# Case 2: BLE power save test for ESP32C3ECO7
@pytest.mark.two_duts
@pytest.mark.esp32c3eco7
@pytest.mark.esp32c3_rev1
@pytest.mark.parametrize(
'config, count, target, app_path',
[
(
'esp32c3eco7',
'esp32c3_rev1',
2,
'esp32c3',
f'{os.path.join(os.path.dirname(__file__), "power_save")}|{os.path.join(os.path.dirname(__file__), "blecent")}',
f'{str(CUR_DIR / "power_save")}|{str(CUR_DIR / "blecent")}',
),
],
indirect=True,
)
def test_power_save_conn_esp32c3eco7(dut: Tuple[IdfDut, IdfDut]) -> None:
def test_power_save_conn_esp32c3_rev1(dut: Tuple[IdfDut, IdfDut]) -> None:
peripheral = dut[0]
central = dut[1]

View File

@@ -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:

View File

@@ -1,8 +1,10 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
from __future__ import annotations
import hashlib
import logging
from typing import Callable
from collections.abc import Callable
import pytest
from pytest_embedded_idf.dut import IdfDut
@@ -18,14 +20,12 @@ def test_hello_world(dut: IdfDut, log_minimum_free_heap_size: Callable[..., None
log_minimum_free_heap_size()
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_hello_world_linux(dut: IdfDut) -> None:
dut.expect('Hello world!')
@pytest.mark.host_test
@pytest.mark.macos_shell
@pytest.mark.macos
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_hello_world_macos(dut: IdfDut) -> None:
dut.expect('Hello world!')
@@ -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'], indirect=['target'])
def test_hello_world_host(app: QemuApp, dut: QemuDut) -> None:

View File

@@ -1,5 +1,7 @@
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: CC0-1.0
from __future__ import annotations
import base64
import io
import ipaddress
@@ -9,14 +11,10 @@ import re
import socket
import subprocess
import time
from collections.abc import Generator
from concurrent.futures import Future
from concurrent.futures import ThreadPoolExecutor
from typing import Generator
from typing import List
from typing import Match
from typing import Optional
from typing import Tuple
from typing import Union
from re import Match
import netifaces
import paramiko # type: ignore
@@ -25,9 +23,12 @@ from common_test_methods import get_host_ip_by_interface
from netmiko import ConnectHandler
from pytest_embedded import Dut
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
@@ -36,7 +37,7 @@ MIN_TCP_THROUGHPUT = 4
class EndnodeSsh:
def __init__(self, host_ip: str, usr: str, passwd: Optional[str] = None):
def __init__(self, host_ip: str, usr: str, passwd: str | None = None):
key_string = os.getenv('CI_ETHVM_KEY')
key = None
if key_string:
@@ -93,7 +94,7 @@ class SwitchSsh:
}
self.ssh_client = ConnectHandler(**edgeSwitch)
def exec_cmd(self, cmd: Union[str, List[str]]) -> str:
def exec_cmd(self, cmd: str | list[str]) -> str:
if self.type == self.EDGE_SWITCH_5XP:
_, stdout, stderr = self.ssh_client.exec_command(cmd)
@@ -192,7 +193,17 @@ def run_iperf(
if ipaddress.ip_address(server_ip).is_multicast:
# Configure Multicast Server
server_proc = subprocess.Popen(
['iperf', '-u', '-s', '-i', '1', '-t', str(interval), '-B', f'{server_ip}%{server_if}'],
[
'iperf',
'-u',
'-s',
'-i',
'1',
'-t',
str(interval),
'-B',
f'{server_ip}%{server_if}',
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -284,14 +295,14 @@ def send_brcast_msg_endnode_to_host(endnode: EndnodeSsh, host_brcast_ip: str, po
return nc_host_out
def get_legacy_host_name_match() -> Optional[Match[str]]:
def get_legacy_host_name_match() -> Match[str] | None:
host_name = socket.gethostname()
regex = r'ethVM-(\d+)-(\d+)'
host_name_match = re.search(regex, host_name, re.DOTALL)
return host_name_match
def get_host_info() -> Tuple[int, int]:
def get_host_info() -> tuple[int, int]:
# Get switch configuration info from the hostname (legacy runners)
sw_info = get_legacy_host_name_match()
if sw_info is not None:
@@ -635,5 +646,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)

View File

@@ -8,16 +8,19 @@ import random
import re
import secrets
import subprocess
import sys
import threading
import time
from typing import Generator
from typing import Optional
from typing import Tuple
import ot_ci_function as ocf
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
import ot_ci_function as ocf # noqa: E402
import pexpect
import pytest
from pytest_embedded_idf.dut import IdfDut
# This file contains the test scripts for Thread:
# Case 1: Thread network formation and attaching
# A Thread Border Router forms a Thread network, Thread devices attach to it, then test ping

View File

@@ -4,11 +4,6 @@ import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
CONFIGS = [
pytest.param('default', marks=[pytest.mark.esp32s2, pytest.mark.esp32s3, pytest.mark.esp32p4]),
pytest.param('esp32p4_psram', marks=[pytest.mark.esp32p4]),
]
@pytest.mark.usb_host_flash_disk
@pytest.mark.temp_skip_ci(targets=['esp32s2'], reason='lack of runners with usb_host_flash_disk tag')

View File

@@ -18,7 +18,7 @@ def test_examples_protocol_esp_http_client(dut: Dut) -> None:
"""
binary_file = os.path.join(dut.app.binary_path, 'esp_http_client_example.bin')
bin_size = os.path.getsize(binary_file)
logging.info('esp_http_client_bin_size : {}KB'.format(bin_size // 1024))
logging.info(f'esp_http_client_bin_size : {bin_size // 1024}KB')
# start test
dut.expect('Connected to AP, begin http example', timeout=30)
dut.expect(r'HTTP GET Status = 200, content_length = (\d)')
@@ -70,7 +70,7 @@ def test_examples_protocol_esp_http_client_dynamic_buffer(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'esp_http_client_example.bin')
bin_size = os.path.getsize(binary_file)
logging.info('esp_http_client_bin_size : {}KB'.format(bin_size // 1024))
logging.info(f'esp_http_client_bin_size : {bin_size // 1024}KB')
dut.expect('Connected to AP, begin http example', timeout=30)
dut.expect(r'HTTP GET Status = 200, content_length = (\d)')
@@ -106,7 +106,6 @@ def test_examples_protocol_esp_http_client_dynamic_buffer(dut: Dut) -> None:
dut.expect('Finish http example')
@pytest.mark.host_test
# Currently we are just testing the build for esp_http_client on Linux target. So skipping the test run.
# Later we will enable the test run for Linux target as well.
@pytest.mark.skipif('config.getvalue("target") == "linux"', reason='Do not run on Linux')

View File

@@ -20,13 +20,13 @@ def test_examples_protocol_https_x509_bundle(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'https_x509_bundle.bin')
bin_size = os.path.getsize(binary_file)
logging.info('https_x509_bundle_bin_size : {}KB'.format(bin_size // 1024))
logging.info(f'https_x509_bundle_bin_size : {bin_size // 1024}KB')
dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)
# start test
num_URLS = int(dut.expect(r'Connecting to (\d+) URLs', timeout=30)[1].decode())
for _ in range(num_URLS):
dut.expect(r'Connection established to ([\s\S]*)', timeout=30)
dut.expect('Completed {} connections'.format(num_URLS), timeout=60)
dut.expect(f'Completed {num_URLS} connections', timeout=60)
@pytest.mark.ethernet
@@ -43,16 +43,15 @@ def test_examples_protocol_https_x509_bundle_dynamic_buffer(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'https_x509_bundle.bin')
bin_size = os.path.getsize(binary_file)
logging.info('https_x509_bundle_bin_size : {}KB'.format(bin_size // 1024))
logging.info(f'https_x509_bundle_bin_size : {bin_size // 1024}KB')
dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)
# start test
num_URLS = int(dut.expect(r'Connecting to (\d+) URLs', timeout=30)[1].decode())
dut.expect(r'Connection established to ([\s\S]*)', timeout=30)
dut.expect('Completed {} connections'.format(num_URLS), timeout=60)
dut.expect(f'Completed {num_URLS} connections', timeout=60)
@pytest.mark.qemu
@pytest.mark.host_test
@pytest.mark.parametrize(
'config',
[
@@ -61,14 +60,16 @@ def test_examples_protocol_https_x509_bundle_dynamic_buffer(dut: Dut) -> None:
indirect=True,
)
@idf_parametrize('target', ['esp32'], indirect=['target'])
def test_examples_protocol_https_x509_bundle_default_crt_bundle_stress_test(dut: Dut) -> None:
def test_examples_protocol_https_x509_bundle_default_crt_bundle_stress_test(
dut: Dut,
) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'https_x509_bundle.bin')
bin_size = os.path.getsize(binary_file)
logging.info('https_x509_bundle_bin_size : {}KB'.format(bin_size // 1024))
logging.info(f'https_x509_bundle_bin_size : {bin_size // 1024}KB')
dut.expect(r'IPv4 address: (\d+\.\d+\.\d+\.\d+)[^\d]', timeout=30)
# start test
num_URLS = int(dut.expect(r'Connecting to (\d+) URLs', timeout=30)[1].decode())
for _ in range(num_URLS):
dut.expect(r'Connection established to ([\s\S]*)', timeout=60)
dut.expect('Completed {} connections'.format(num_URLS), timeout=180)
dut.expect(f'Completed {num_URLS} connections', timeout=180)

View File

@@ -2,8 +2,8 @@
# BSD Socket API Examples
This directory contains simple examples demonstrating BSD Socket API.
Each example, contains README.md file with mode detailed informations about that particular example.
For more general informations about all examples, see the README.md file in the upper level 'examples' directory.
Each example, contains README.md file with mode detailed information about that particular example.
For more general information about all examples, see the README.md file in the upper level 'examples' directory.
Examples:
* UDP Client - The application creates UDP socket and sends message to the predefined port and IP address. After the server's reply, the application prints received reply as ASCII text, waits for 2 seconds and sends another message.
@@ -56,7 +56,7 @@ They can also be run locally. Ref: [ESP-IDF Tests with Pytest Guide](https://doc
Example:
```bash
$ cd $IDF_PATH
$ bash install.sh --enable-pytest
$ bash install.sh --enable-ci
$ . ./export.sh
$ cd examples/protocols/sockets/tcp_client
$ python $IDF_PATH/tools/ci/ci_build_apps.py . --target esp32 -vv --pytest-apps
@@ -112,7 +112,7 @@ Please make sure that when using the Local Link address, an interface id is incl
* On the host
- Interface name suffix is present when passing the address as a string, for example `fe80::260a:XXX:XXX:XXX%en0`
- The interface id is present when passing the endpoint as tupple, for example `socket.connect(('fd00::260a:XXXX:XXXX:XXXX', 3333, 0, 3))`
- The interface id is present when passing the endpoint as tuple, for example `socket.connect(('fd00::260a:XXXX:XXXX:XXXX', 3333, 0, 3))`
## Hardware Required

View File

@@ -10,26 +10,6 @@ from pytest_embedded_idf.utils import idf_parametrize
touch_wake_up_support = ['esp32', 'esp32s2']
CONFIGS = [
pytest.param('esp32_singlecore', marks=[pytest.mark.esp32]),
pytest.param(
'basic',
marks=[
pytest.mark.esp32,
pytest.mark.esp32s2,
pytest.mark.esp32s3,
pytest.mark.esp32c3,
pytest.mark.esp32c5,
pytest.mark.esp32c6,
pytest.mark.esp32c61,
pytest.mark.esp32h2,
pytest.mark.esp32p4,
pytest.mark.esp32c2,
],
),
]
@pytest.mark.generic
@idf_parametrize(
'config,target', [('esp32_singlecore', 'esp32'), ('basic', 'supported_targets')], indirect=['config', 'target']
@@ -42,11 +22,11 @@ def test_deep_sleep(dut: Dut) -> None:
'esp32s2': [9],
}[dut.target]
logging.info('Expecting to see wakeup configured on pad(s): {}'.format(wake_pads))
logging.info(f'Expecting to see wakeup configured on pad(s): {wake_pads}')
expect_items = ['Enabling timer wakeup, 20s']
for pad in wake_pads:
expect_items += [r'Touch pad #{} average: \d+, wakeup threshold set to \d+.'.format(pad)]
expect_items += [rf'Touch pad #{pad} average: \d+, wakeup threshold set to \d+.']
expect_items += ['Enabling touch pad wakeup']
for exp in expect_items:
@@ -69,7 +49,7 @@ def test_deep_sleep(dut: Dut) -> None:
dut.expect_exact('boot: ESP-IDF') # first output that's the same on all chips
sleep_time = time.time() - start_sleep
logging.info('Host measured sleep time at {:.2f}s'.format(sleep_time))
logging.info(f'Host measured sleep time at {sleep_time:.2f}s')
assert 18 < sleep_time < 22 # note: high tolerance as measuring time on the host may have some timing skew
dut.expect_exact('boot: Fast booting app from partition', timeout=2)

View File

@@ -10,7 +10,10 @@ from pytest_embedded_qemu.dut import QemuDut
def basic_efuse_example(dut: Dut) -> None:
dut.expect(r'example: Coding Scheme (3/4)|(NONE)|(REPEAT)|(RS \(Reed-Solomon coding\))', timeout=20)
dut.expect(
r'example: Coding Scheme (3/4)|(NONE)|(REPEAT)|(RS \(Reed-Solomon coding\))',
timeout=20,
)
dut.expect(
[
'example: read efuse fields',
@@ -43,7 +46,6 @@ def test_examples_efuse(dut: Dut) -> None:
basic_efuse_example(dut)
@pytest.mark.host_test
@idf_parametrize('target', ['linux'], indirect=['target'])
def test_examples_efuse_linux(dut: Dut) -> None:
basic_efuse_example(dut)
@@ -60,14 +62,24 @@ def test_examples_efuse_linux(dut: Dut) -> None:
@pytest.mark.parametrize('skip_autoflash', ['y'], indirect=True)
@idf_parametrize(
'target',
['esp32', 'esp32c2', 'esp32c3', 'esp32c5', 'esp32c61', 'esp32c6', 'esp32h2', 'esp32s2', 'esp32s3'],
[
'esp32',
'esp32c2',
'esp32c3',
'esp32c5',
'esp32c61',
'esp32c6',
'esp32h2',
'esp32s2',
'esp32s3',
],
indirect=['target'],
)
def test_examples_efuse_with_virt_flash_enc(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_flash_enc_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_flash_enc_bin_size: {bin_size // 1024}KB')
print(' - Erase flash')
dut.serial.erase_flash()
@@ -126,7 +138,7 @@ def test_examples_efuse_with_virt_flash_enc_aes_256(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_flash_enc_aes_256_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_flash_enc_aes_256_bin_size: {bin_size // 1024}KB')
print(' - Erase flash')
dut.serial.erase_flash()
@@ -169,7 +181,17 @@ def test_examples_efuse_with_virt_flash_enc_aes_256(dut: Dut) -> None:
@pytest.mark.parametrize('skip_autoflash', ['y'], indirect=True)
@idf_parametrize(
'target',
['esp32', 'esp32c2', 'esp32c3', 'esp32c5', 'esp32c61', 'esp32c6', 'esp32h2', 'esp32s2', 'esp32s3'],
[
'esp32',
'esp32c2',
'esp32c3',
'esp32c5',
'esp32c61',
'esp32c6',
'esp32h2',
'esp32s2',
'esp32s3',
],
indirect=['target'],
)
def test_examples_efuse_with_virt_flash_enc_pre_loaded(dut: Dut) -> None:
@@ -239,14 +261,24 @@ def test_examples_efuse_with_virt_flash_enc_pre_loaded(dut: Dut) -> None:
@pytest.mark.parametrize('skip_autoflash', ['y'], indirect=True)
@idf_parametrize(
'target',
['esp32', 'esp32c2', 'esp32c3', 'esp32c5', 'esp32c61', 'esp32c6', 'esp32h2', 'esp32s2', 'esp32s3'],
[
'esp32',
'esp32c2',
'esp32c3',
'esp32c5',
'esp32c61',
'esp32c6',
'esp32h2',
'esp32s2',
'esp32s3',
],
indirect=['target'],
)
def test_examples_efuse_with_virt_flash_enc_release(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_flash_enc_release_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_flash_enc_release_bin_size: {bin_size // 1024}KB')
dut.serial.erase_flash()
@@ -302,7 +334,7 @@ def test_examples_efuse_with_virt_secure_boot_v1(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_secure_boot_v1_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_secure_boot_v1_bin_size: {bin_size // 1024}KB')
print(' - Erase flash')
dut.serial.erase_flash()
@@ -400,7 +432,7 @@ def test_examples_efuse_with_virt_secure_boot_v1_pre_loaded(dut: Dut) -> None:
dut.expect('example: Done')
@pytest.mark.esp32eco3
@pytest.mark.esp32_rev3
@pytest.mark.parametrize(
'config',
[
@@ -414,7 +446,7 @@ def test_examples_efuse_with_virt_secure_boot_v2(dut: Dut) -> None:
# only for ESP32 ECO3
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_secure_boot_v2_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_secure_boot_v2_bin_size: {bin_size // 1024}KB')
print(' - Erase flash')
dut.serial.erase_flash()
@@ -520,7 +552,7 @@ def test_examples_efuse_with_virt_secure_boot_v2(dut: Dut) -> None:
dut.expect('example: Done')
@pytest.mark.esp32eco3
@pytest.mark.esp32_rev3
@pytest.mark.parametrize(
'config',
[
@@ -593,14 +625,24 @@ def test_examples_efuse_with_virt_secure_boot_v2_pre_loaded(dut: Dut) -> None:
@pytest.mark.parametrize('skip_autoflash', ['y'], indirect=True)
@idf_parametrize(
'target',
['esp32c3', 'esp32c2', 'esp32c5', 'esp32c6', 'esp32c61', 'esp32h2', 'esp32p4', 'esp32s2', 'esp32s3'],
[
'esp32c3',
'esp32c2',
'esp32c5',
'esp32c6',
'esp32c61',
'esp32h2',
'esp32p4',
'esp32s2',
'esp32s3',
],
indirect=['target'],
)
def test_examples_efuse_with_virt_secure_boot_v2_esp32xx(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_secure_boot_v2_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_secure_boot_v2_bin_size: {bin_size // 1024}KB')
print(' - Erase flash')
dut.serial.erase_flash()
@@ -616,13 +658,13 @@ def test_examples_efuse_with_virt_secure_boot_v2_esp32xx(dut: Dut) -> None:
signed_scheme = 'ECDSA'
else:
signed_scheme = 'RSA-PSS'
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: enabling secure boot v2...')
dut.expect('Verifying image signature...')
dut.expect('secure_boot_v2: Secure boot V2 is not enabled yet and eFuse digest keys are not set')
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: Secure boot digests absent, generating..')
dut.expect('secure_boot_v2: Digests successfully calculated, 1 valid signatures')
@@ -651,7 +693,7 @@ def test_examples_efuse_with_virt_secure_boot_v2_esp32xx(dut: Dut) -> None:
dut.serial.hard_reset()
dut.expect('Loading virtual efuse blocks from flash')
dut.expect('Verifying image signature...')
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: enabling secure boot v2...')
dut.expect('secure_boot_v2: secure boot v2 is already enabled, continuing..')
@@ -664,7 +706,17 @@ def test_examples_efuse_with_virt_secure_boot_v2_esp32xx(dut: Dut) -> None:
@pytest.mark.parametrize('skip_autoflash', ['y'], indirect=True)
@idf_parametrize(
'target',
['esp32c3', 'esp32c2', 'esp32c5', 'esp32c6', 'esp32c61', 'esp32h2', 'esp32p4', 'esp32s2', 'esp32s3'],
[
'esp32c3',
'esp32c2',
'esp32c5',
'esp32c6',
'esp32c61',
'esp32h2',
'esp32p4',
'esp32s2',
'esp32s3',
],
indirect=['target'],
)
def test_example_efuse_with_virt_secure_boot_v2_esp32xx_pre_loaded(dut: Dut) -> None:
@@ -685,12 +737,14 @@ def test_example_efuse_with_virt_secure_boot_v2_esp32xx_pre_loaded(dut: Dut) ->
# Resets eFuse, which enables Secure boot feature
# Resets eFuses, which control digest slots
if dut.app.sdkconfig.get('SOC_EFUSE_REVOKE_BOOT_KEY_DIGESTS'):
dut.serial.erase_field_on_emul_efuse_by_name([
'SECURE_BOOT_EN',
'SECURE_BOOT_KEY_REVOKE0',
'SECURE_BOOT_KEY_REVOKE1',
'SECURE_BOOT_KEY_REVOKE2',
])
dut.serial.erase_field_on_emul_efuse_by_name(
[
'SECURE_BOOT_EN',
'SECURE_BOOT_KEY_REVOKE0',
'SECURE_BOOT_KEY_REVOKE1',
'SECURE_BOOT_KEY_REVOKE2',
]
)
else:
dut.serial.erase_field_on_emul_efuse_by_name(['SECURE_BOOT_EN'])
@@ -704,7 +758,7 @@ def test_example_efuse_with_virt_secure_boot_v2_esp32xx_pre_loaded(dut: Dut) ->
else:
signed_scheme = 'RSA-PSS'
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: Secure boot digests already present')
dut.expect('secure_boot_v2: Using pre-loaded public key digest in eFuse')
@@ -727,7 +781,7 @@ def test_example_efuse_with_virt_secure_boot_v2_esp32xx_pre_loaded(dut: Dut) ->
dut.serial.hard_reset()
dut.expect('Loading virtual efuse blocks from flash')
dut.expect('Verifying image signature...')
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: enabling secure boot v2...')
dut.expect('secure_boot_v2: secure boot v2 is already enabled, continuing..')
@@ -749,7 +803,7 @@ def test_examples_efuse_with_virt_sb_v1_and_fe(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_sb_v1_and_fe_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_sb_v1_and_fe_bin_size: {bin_size // 1024}KB')
print(' - Erase flash')
dut.serial.erase_flash()
@@ -806,7 +860,7 @@ def test_examples_efuse_with_virt_sb_v1_and_fe(dut: Dut) -> None:
dut.expect('example: Done')
@pytest.mark.esp32eco3
@pytest.mark.esp32_rev3
@pytest.mark.parametrize(
'config',
[
@@ -820,7 +874,7 @@ def test_examples_efuse_with_virt_sb_v2_and_fe(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_sb_v2_and_fe_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_sb_v2_and_fe_bin_size: {bin_size // 1024}KB')
print(' - Erase flash')
dut.serial.erase_flash()
@@ -894,7 +948,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',
@@ -906,7 +959,8 @@ def test_examples_efuse_with_virt_sb_v2_and_fe(dut: Dut) -> None:
@pytest.mark.parametrize(
'qemu_extra_args',
[
f'-drive file={os.path.join(os.path.dirname(__file__), "test", "esp32eco3_efuses.bin")},if=none,format=raw,id=efuse '
f'-drive file={os.path.join(os.path.dirname(__file__), "test", "esp32_rev3_efuses.bin")},'
'if=none,format=raw,id=efuse '
'-global driver=nvram.esp32.efuse,property=drive,value=efuse '
'-global driver=timer.esp32.timg,property=wdt_disable,value=true',
],
@@ -976,11 +1030,14 @@ def test_examples_efuse_with_virt_sb_v2_and_fe_qemu(dut: QemuDut) -> None:
finally:
# the above example test burns the efuses, and hence the efuses file which the
# qemu uses to emulate the efuses, "test/esp32eco3_efuses.bin", gets modified.
# qemu uses to emulate the efuses, "test/esp32_rev3_efuses.bin", gets modified.
# Thus, restore the efuses file values back to the default ESP32-ECO3 efuses values.
with open(os.path.join(os.path.dirname(__file__), 'test', 'esp32eco3_efuses.bin'), 'wb') as efuse_file:
esp32eco3_efuses = '0' * 26 + '8' + '0' * 17 + '1' + '0' * 203
efuse_file.write(bytearray.fromhex(esp32eco3_efuses))
with open(
os.path.join(os.path.dirname(__file__), 'test', 'esp32_rev3_efuses.bin'),
'wb',
) as efuse_file:
esp32_rev3_efuses = '0' * 26 + '8' + '0' * 17 + '1' + '0' * 203
efuse_file.write(bytearray.fromhex(esp32_rev3_efuses))
@pytest.mark.generic
@@ -988,14 +1045,23 @@ def test_examples_efuse_with_virt_sb_v2_and_fe_qemu(dut: QemuDut) -> None:
@pytest.mark.parametrize('config', ['virt_sb_v2_and_fe'], indirect=True)
@idf_parametrize(
'target',
['esp32c3', 'esp32c2', 'esp32c5', 'esp32c61', 'esp32c6', 'esp32h2', 'esp32s2', 'esp32s3'],
[
'esp32c3',
'esp32c2',
'esp32c5',
'esp32c61',
'esp32c6',
'esp32h2',
'esp32s2',
'esp32s3',
],
indirect=['target'],
)
def test_examples_efuse_with_virt_sb_v2_and_fe_esp32xx(dut: Dut) -> None:
# check and log bin size
binary_file = os.path.join(dut.app.binary_path, 'bootloader', 'bootloader.bin')
bin_size = os.path.getsize(binary_file)
logging.info('{}_bootloader_virt_sb_v2_and_fe_bin_size: {}KB'.format(dut.app.target, bin_size // 1024))
logging.info(f'{dut.app.target}_bootloader_virt_sb_v2_and_fe_bin_size: {bin_size // 1024}KB')
dut.serial.erase_flash()
@@ -1012,13 +1078,13 @@ def test_examples_efuse_with_virt_sb_v2_and_fe_esp32xx(dut: Dut) -> None:
signed_scheme = 'ECDSA'
else:
signed_scheme = 'RSA-PSS'
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: enabling secure boot v2...')
dut.expect('Verifying image signature...')
dut.expect('secure_boot_v2: Secure boot V2 is not enabled yet and eFuse digest keys are not set')
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: Secure boot digests absent, generating..')
dut.expect('secure_boot_v2: Digests successfully calculated, 1 valid signatures')
@@ -1057,20 +1123,20 @@ def test_examples_efuse_with_virt_sb_v2_and_fe_esp32xx(dut: Dut) -> None:
dut.expect('boot: Secure boot permanently enabled')
dut.expect('Verifying image signature...')
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('flash_encrypt: bootloader encrypted successfully')
dut.expect('flash_encrypt: partition table encrypted and loaded successfully')
dut.expect('Verifying image signature...')
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('flash_encrypt: Flash encryption completed', timeout=90)
dut.expect('Resetting with flash encryption enabled...')
dut.expect('Loading virtual efuse blocks from flash')
dut.expect('Verifying image signature...')
dut.expect('secure_boot_v2: Verifying with %s...' % signed_scheme)
dut.expect(f'secure_boot_v2: Verifying with {signed_scheme}...')
dut.expect('secure_boot_v2: Signature verified successfully!')
dut.expect('secure_boot_v2: enabling secure boot v2...')
dut.expect('secure_boot_v2: secure boot v2 is already enabled, continuing..')

View File

@@ -6,6 +6,7 @@ from typing import Tuple
import pytest
from pytest_embedded_idf.dut import IdfDut
from pytest_embedded_idf.utils import idf_parametrize
# @pytest.mark.supported_targets
# This test should support all targets, even between different target types
# For now our CI only support multi dut with esp32
@@ -82,13 +83,13 @@ def test_wifi_getting_started_esp32c2_xtal_26mhz(dut: Tuple[IdfDut, IdfDut]) ->
@pytest.mark.two_duts
@pytest.mark.xtal_26mhz
@pytest.mark.esp32c2eco4
@pytest.mark.esp32c2_rev2
@pytest.mark.parametrize(
'count, config, target, baud, app_path',
[
(
2,
'esp32c2eco4_xtal26m',
'esp32c2_rev2_xtal26m',
'esp32c2',
'74880',
f'{os.path.join(os.path.dirname(__file__), "softAP")}|{os.path.join(os.path.dirname(__file__), "station")}',
@@ -96,7 +97,7 @@ def test_wifi_getting_started_esp32c2_xtal_26mhz(dut: Tuple[IdfDut, IdfDut]) ->
],
indirect=True,
)
def test_wifi_getting_started_esp32c2eco4_xtal_26mhz(dut: Tuple[IdfDut, IdfDut]) -> None:
def test_wifi_getting_started_esp32c2_rev2_xtal_26mhz(dut: Tuple[IdfDut, IdfDut]) -> None:
softap = dut[0]
station = dut[1]
@@ -113,20 +114,20 @@ def test_wifi_getting_started_esp32c2eco4_xtal_26mhz(dut: Tuple[IdfDut, IdfDut])
@pytest.mark.two_duts
@pytest.mark.esp32c3eco7
@pytest.mark.esp32c3_rev1
@pytest.mark.parametrize(
'count, config, target, app_path',
[
(
2,
'esp32c3eco7',
'esp32c3_rev1',
'esp32c3',
f'{os.path.join(os.path.dirname(__file__), "softAP")}|{os.path.join(os.path.dirname(__file__), "station")}',
),
],
indirect=True,
)
def test_wifi_getting_started_esp32c3eco7(dut: Tuple[IdfDut, IdfDut]) -> None:
def test_wifi_getting_started_esp32c3_rev1(dut: Tuple[IdfDut, IdfDut]) -> None:
softap = dut[0]
station = dut[1]

View File

@@ -93,24 +93,24 @@ def test_wifi_power_save_esp32c2_26mhz(dut: Dut) -> None:
@pytest.mark.wifi_ap
@pytest.mark.xtal_26mhz
@pytest.mark.esp32c2eco4
@pytest.mark.esp32c2_rev2
@pytest.mark.parametrize(
'config, baud, target',
[
('c2eco4_xtal26m', '74880', 'esp32c2'),
('esp32c2_rev2_xtal26m', '74880', 'esp32c2'),
],
indirect=True,
)
def test_wifi_power_save_esp32c2eco4_26mhz(dut: Dut) -> None:
def test_wifi_power_save_esp32c2_rev2_26mhz(dut: Dut) -> None:
_run_test(dut)
@pytest.mark.wifi_ap
@pytest.mark.esp32c3eco7
@pytest.mark.esp32c3_rev1
@pytest.mark.parametrize(
'config, target',
[('c3eco7', 'esp32c3')],
[('esp32c3_rev1', 'esp32c3')],
indirect=True,
)
def test_wifi_power_save_esp32c3eco7(dut: Dut) -> None:
def test_wifi_power_save_esp32c3_rev1(dut: Dut) -> None:
_run_test(dut)

View File

@@ -7,7 +7,6 @@ python_files = pytest_*.py
addopts =
-s -vv
--embedded-services esp,idf
-p no:idf-ci
--tb short
--strict-markers
--skip-check-coredump y
@@ -15,12 +14,14 @@ addopts =
--check-duplicates y
--ignore-glob */managed_components/*
--ignore pytest-embedded
--unity-test-report-mode merge
--ignore-no-tests-collected-error
# ignore DeprecationWarning
filterwarnings =
ignore::DeprecationWarning:matplotlib.*:
ignore::DeprecationWarning:google.protobuf.*:
ignore::_pytest.warning_types.PytestExperimentalApiWarning
ignore::FutureWarning
# log related
log_cli = True
@@ -34,3 +35,95 @@ junit_family = xunit1
## log all to `system-out` when case fail
junit_logging = stdout
junit_log_passing_tests = False
markers =
temp_skip_ci: mark test to be skipped in CI
temp_skip: mark test to be skipped in CI and locally
skip_app_downloader: mark test required apps built locally, not downloaded from CI
require_elf: mark test to be skipped if no elf file is found
env_markers =
qemu: build and test using qemu, not real target
macos: tests should be run on macos hosts
generic: tests should be run on generic runners
flash_suspend: support flash suspend feature
eth_ip101: connected via IP101 ethernet transceiver
eth_lan8720: connected via LAN8720 ethernet transceiver
eth_rtl8201: connected via RTL8201 ethernet transceiver
eth_ksz8041: connected via KSZ8041 ethernet transceiver
eth_dp83848: connected via DP83848 ethernet transceiver
eth_w5500: SPI Ethernet module with two W5500
eth_ksz8851snl: SPI Ethernet module with two KSZ8851SNL
eth_dm9051: SPI Ethernet module with two DM9051
quad_psram: runners with quad psram
octal_psram: runners with octal psram
usb_host_flash_disk: usb host runners with USB flash disk attached
usb_device: usb device runners
ethernet_ota: ethernet OTA runners
flash_encryption: Flash Encryption runners
flash_encryption_f4r8: Flash Encryption runners with 4-line flash and 8-line psram
flash_encryption_f8r8: Flash Encryption runners with 8-line flash and 8-line psram
flash_encryption_ota: Flash Encryption runners with ethernet OTA support with 4mb flash size
flash_multi: Multiple flash chips tests
psram: Chip has 4-line psram
ir_transceiver: runners with a pair of IR transmitter and receiver
flash_encryption_wifi_high_traffic: Flash Encryption runners with wifi high traffic support
ethernet: ethernet runner
ethernet_flash_8m: ethernet runner with 8mb flash
ethernet_router: both the runner and dut connect to the same router through ethernet NIC
ethernet_vlan: ethernet runner GARM-32-SH-1-R16S5N3
wifi_ap: a wifi AP in the environment
wifi_router: both the runner and dut connect to the same wifi router
wifi_high_traffic: wifi high traffic runners
wifi_wlan: wifi runner with a wireless NIC
wifi_iperf: the AP and ESP dut were placed in a shielded box - for iperf test
Example_ShieldBox: multiple shielded APs connected to shielded ESP DUT via RF cable with programmable attenuator
xtal_26mhz: runner with 26MHz xtal on board
xtal_40mhz: runner with 40MHz xtal on board
external_flash: external flash memory connected via VSPI (FSPI)
sdcard_sdmode: sdcard running in SD mode, to be removed after test migration
sdcard_spimode: sdcard running in SPI mode
emmc: eMMC card
sdcard: sdcard runner
MSPI_F8R8: runner with Octal Flash and Octal PSRAM
MSPI_F4R8: runner with Quad Flash and Octal PSRAM
MSPI_F4R4: runner with Quad Flash and Quad PSRAM
jtag: runner where the chip is accessible through JTAG as well
usb_serial_jtag: runner where the chip is accessible through builtin JTAG as well
xtal32k: Runner with external 32k crystal connected
no32kXtal: Runner with no external 32k crystal connected
psramv0: Runner with PSRAM version 0
ecdsa_efuse: Runner with test ECDSA private keys programmed in efuse
ccs811: Runner with CCS811 connected
nvs_encr_hmac: Runner with test HMAC key programmed in efuse
i2c_oled: Runner with ssd1306 I2C oled connected
httpbin: runner for tests that need to access the httpbin service
flash_4mb: C2 runners with 4 MB flash
jtag_re_enable: Runner to re-enable jtag which is softly disabled by burning bit SOFT_DIS_JTAG on eFuse
es8311: Development board that carries es8311 codec
multi_dut_modbus_rs485: a pair of runners connected by RS485 bus
ieee802154: ieee802154 related tests should run on ieee802154 runners.
openthread_br: openthread BR basic and multicast cases.
openthread_br_connect: openthread BR attach and connectivity cases.
openthread_br_service: openthread BR service discovery cases.
openthread_br_nat64: openthread BR NAT64 cases without internet.
openthread_bbr: openthread BR cases with internet access.
openthread_sleep: tests should be used for openthread sleepy device.
zigbee_multi_dut: zigbee runner which have multiple duts.
two_duts: the runner has two duts with antenna available.
generic_multi_device: generic multiple devices whose corresponding gpio pins are connected to each other.
twai_network: multiple runners form a TWAI network.
sdio_master_slave: Test sdio multi board, esp32+esp32
sdio_multidev_32_c6: Test sdio multi board, esp32+esp32c6
usj_device: Test usb_serial_jtag and usb_serial_jtag is used as serial only (not console)
twai_adapter: runner with multiple twai_std
lp_i2s: lp_i2s runner tested with hp_i2s
ram_app: ram_app runners
# rev markers
rev_default: Runner with default revision connected
esp32_rev3: Runner with ESP32 rev 3.x connected
esp32c2_rev2: Runner with ESP32-C2 rev 2.x connected
esp32c3_rev1: Runner with ESP32-C3 rev 1.x connected
esp32c5_rev1: Runner with ESP32-C5 rev 1.x connected
esp32p4_rev1: Runner with ESP32-P4 rev 1.x connected

48
ruff.toml Normal file
View File

@@ -0,0 +1,48 @@
line-length = 120
target-version = "py38"
[format]
quote-style = "single"
docstring-code-format = true
[lint]
select = [
"A", # flake8-builtins
"E", # pycodestyle
"F", # pyflakes
"W", # pycodestyle
"I", # reorder-imports
"UP", # pyupgrade
]
ignore = [
"E203", # whitespace before ':'
"E221", # multiple spaces before operator
"E225", # missing whitespace around operator
"E231", # missing whitespace after ',', ';', or ':'
"E241", # multiple spaces after ','
]
# Exclude a variety of commonly ignored directories.
exclude = [
".git",
"__pycache__",
# submodules
"components/bootloader/subproject/components/micro-ecc/micro-ecc",
"components/bt/host/nimble/nimble",
"components/cmock/CMock",
"components/JSON/cJSON",
"components/mbedtls/mbedtls",
"components/openthread/openthread",
"components/unity/unity",
"components/spiffs/spiffs",
# autogenerated scripts
"components/protocomm/python/constants_pb2.py",
"components/protocomm/python/sec0_pb2.py",
"components/protocomm/python/sec1_pb2.py",
"components/protocomm/python/sec2_pb2.py",
"components/protocomm/python/session_pb2.py",
"components/esp_local_ctrl/python/esp_local_ctrl_pb2.py",
]
[lint.isort]
force-single-line = true

View File

@@ -1,24 +0,0 @@
sonar.projectKey=esp-idf
sonar.projectName=Espressif IoT Development Framework
sonar.projectVersion=4.4
sonar.sources=.
sonar.sourceEncoding=UTF-8
sonar.inclusions=\
**/*.c,**/*.cpp,**/*.h,**/*.hpp,\
**/*.py
####################
# sonar-cxx-plugin #
####################
sonar.cxx.includeDirectories=/usr/include
sonar.cxx.file.suffixes=.cxx,.cpp,.cc,.c,.hxx,.hpp,.hh,.h
#######################
# sonar-gitlab-plugin #
#######################
sonar.gitlab.failure_notification_mode=exit-code
sonar.gitlab.project_id=103

View File

@@ -11,6 +11,13 @@ set -euo pipefail
TEMPLATE_APP_PATH="esp-idf-template"
# Set default paths for template app builds
BUILD_PATH="${IDF_PATH}/build_template_app"
BUILD_DIR="${BUILD_PATH}/@t/@w"
LOG_PATH="${IDF_PATH}/log_template_app"
BUILD_LOG_CMAKE="${LOG_PATH}/cmake_@t_@w.txt"
gen_configs() {
# CONFIG_COMPILER_OPTIMIZATION_NONE with flag -O0
echo "CONFIG_COMPILER_OPTIMIZATION_NONE=y" > ${TEMPLATE_APP_PATH}/sdkconfig.ci.O0
@@ -54,30 +61,23 @@ build_stage2() {
# Override EXTRA_CFLAGS and EXTRA_CXXFLAGS in the environment
export EXTRA_CFLAGS=${PEDANTIC_CFLAGS/-Werror=unused-variable -Werror=unused-but-set-variable -Werror=unused-function/}
export EXTRA_CXXFLAGS=${PEDANTIC_CXXFLAGS/-Werror=unused-variable -Werror=unused-but-set-variable -Werror=unused-function/}
python -m idf_build_apps build -v \
idf-build-apps build \
-p ${TEMPLATE_APP_PATH} \
-t all \
${CONFIG_STR} \
--work-dir ${BUILD_PATH}/cmake \
--build-dir ${BUILD_DIR} \
--build-log ${BUILD_LOG_CMAKE} \
--size-file size.json \
--keep-going \
--collect-size-info size_info.txt \
--default-build-targets esp32 esp32s2 esp32s3 esp32c2 esp32c3 esp32c5 esp32c6 esp32h2 esp32p4
}
build_stage1() {
CONFIG_STR=$(get_config_str sdkconfig.ci2.*=)
python -m idf_build_apps build -v \
idf-build-apps build \
-p ${TEMPLATE_APP_PATH} \
-t all \
${CONFIG_STR} \
--work-dir ${BUILD_PATH}/cmake \
--build-dir ${BUILD_DIR} \
--build-log ${BUILD_LOG_CMAKE} \
--size-file size.json \
--collect-size-info size_info.txt \
--default-build-targets esp32 esp32s2 esp32s3 esp32c2 esp32c3 esp32c5 esp32c6 esp32h2 esp32p4
}

View File

@@ -1,33 +1,27 @@
#!/usr/bin/env python
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import argparse
import inspect
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
import yaml
from idf_ci_utils import get_all_manifest_files
from idf_build_apps import App
from idf_build_apps import find_apps
from idf_build_apps.constants import SUPPORTED_TARGETS
from idf_ci_utils import IDF_PATH
# Copy of the constant from idf_pytest.constants, to avoid installing
# additional dependencies (pytest, pytest-embedded) in pre-commit hooks
DEFAULT_CONFIG_RULES_STR = ['sdkconfig.ci=default', 'sdkconfig.ci.*=', '=default']
YES = u'\u2713'
NO = u'\u2717'
from idf_ci_utils import idf_relpath
from rich.console import Console
from rich.table import Table
# | Supported Target | ... |
# | ---------------- | --- |
SUPPORTED_TARGETS_TABLE_REGEX = re.compile(
r'^\|\s*Supported Targets.+$\n^\|(?:\s*|-).+$\n?', re.MULTILINE
)
SUPPORTED_TARGETS_TABLE_REGEX = re.compile(r'^\|\s*Supported Targets.+$\n^\|(?:\s*|-).+$\n?', re.MULTILINE)
USUAL_TO_FORMAL = {
'esp32': 'ESP32',
@@ -43,350 +37,265 @@ USUAL_TO_FORMAL = {
'linux': 'Linux',
}
FORMAL_TO_USUAL = {
'ESP32': 'esp32',
'ESP32-S2': 'esp32s2',
'ESP32-S3': 'esp32s3',
'ESP32-C3': 'esp32c3',
'ESP32-C2': 'esp32c2',
'ESP32-C6': 'esp32c6',
'ESP32-C5': 'esp32c5',
'ESP32-H2': 'esp32h2',
'ESP32-P4': 'esp32p4',
'ESP32-C61': 'esp32c61',
'Linux': 'linux',
}
FORMAL_TO_USUAL = {v: k for k, v in USUAL_TO_FORMAL.items()}
# Targets under bringup: suppress check-test-scripts warnings for these.
# Reset to [] once bringup is complete.
BYPASS_CHECK_TEST_TARGETS: list[str] = [
'esp32c5',
'esp32c61',
]
def doublequote(s: str) -> str:
if s.startswith('"') and s.endswith('"'):
return s
def print_diff_table(
list1: list[str],
list2: list[str],
title1: str,
title2: str,
excluded: list[str] | None = None,
) -> None:
left_set = set(list1)
right_set = set(list2)
return f'"{s}"'
if left_set == right_set:
return
all_items = sorted(left_set.union(right_set))
left_column = []
right_column = []
for item in all_items:
if item in left_set and item not in right_set:
if excluded and item in excluded:
left_column.append(f'[red]{item}[/red] (excluded)')
else:
left_column.append(f'[red]{item}[/red]')
right_column.append('')
elif item in right_set and item not in left_set:
if excluded and item in excluded:
right_column.append(f'[green]{item}[/green] (excluded)')
else:
right_column.append(f'[green]{item}[/green]')
left_column.append('')
else:
left_column.append(item)
right_column.append(item)
table = Table(show_header=True, header_style='bold magenta')
table.add_column(title1.strip(), justify='left')
table.add_column(title2.strip(), justify='left')
for l_col, r_col in zip(left_column, right_column):
table.add_row(l_col, r_col)
console = Console()
console.print(table)
def get_readme_path(app_dir: str) -> str | None:
readme_path = os.path.join(app_dir, 'README.md')
if not os.path.isfile(readme_path):
readme_path = os.path.join(app_dir, '..', 'README.md')
if not os.path.isfile(readme_path):
readme_path = None # type: ignore
return readme_path
def generate_new_support_table(targets: list[str]) -> str:
# extra space here
table_headers = [f'{USUAL_TO_FORMAL[target]}' for target in targets]
table_headers = ['Supported Targets'] + table_headers
res = '| ' + ' | '.join(table_headers) + ' |\n'
res += '| ' + ' | '.join(['-' * len(item) for item in table_headers]) + ' |'
return res
def parse_existing_table(app_dir: str) -> tuple[str | None, list[str]]:
readme_path = get_readme_path(app_dir)
if not readme_path:
return None, SUPPORTED_TARGETS
with open(readme_path, encoding='utf8') as _fr:
readme_str = _fr.read()
support_string = SUPPORTED_TARGETS_TABLE_REGEX.findall(readme_str)
if not support_string:
return None, SUPPORTED_TARGETS
# old style
parts = [part.strip() for part in support_string[0].split('\n', 1)[0].split('|') if part.strip()]
return support_string[0].strip(), [FORMAL_TO_USUAL[part] for part in parts[1:] if part in FORMAL_TO_USUAL]
def get_grouped_apps(
paths: list[str],
exclude_dirs: list[str] | None = None,
) -> dict[str, list[App]]:
apps = sorted(
find_apps(
paths,
'all',
exclude_list=exclude_dirs or [],
)
)
grouped_apps: dict[str, list[App]] = defaultdict(list)
for app in apps:
grouped_apps[app.app_dir].append(app)
return grouped_apps
def check_readme(
paths: List[str],
exclude_dirs: Optional[List[str]] = None,
extra_default_build_targets: Optional[List[str]] = None,
paths: list[str],
exclude_dirs: list[str] | None = None,
) -> None:
from idf_build_apps import App, find_apps
from idf_build_apps.constants import SUPPORTED_TARGETS
grouped_apps = get_grouped_apps(paths, exclude_dirs)
exit_code = 0
def app_build_targets(_app: App) -> List[str]:
return sorted(
for app_dir, apps in grouped_apps.items():
old_table, old_targets = parse_existing_table(app_dir)
manifest_targets = sorted(
{
target
for app in apps
for target in (
App.MANIFEST.enable_build_targets(_app.app_dir)
+ App.MANIFEST.enable_build_targets(_app.app_dir, config_name=_app.config_name)
App.MANIFEST.enable_build_targets(app_dir)
+ App.MANIFEST.enable_build_targets(app_dir, config_name=app.config_name)
)
}
)
def get_readme_path(_app: App) -> Optional[str]:
_readme_path = os.path.join(_app.app_dir, 'README.md')
if not os.path.isfile(_readme_path):
_readme_path = os.path.join(_app.app_dir, '..', 'README.md')
if not os.path.isfile(_readme_path):
_readme_path = None # type: ignore
return _readme_path
def _generate_new_support_table_str(_app: App) -> str:
manifest_targets = app_build_targets(_app)
# extra space here
table_headers = [
f'{USUAL_TO_FORMAL[target]}' for target in manifest_targets
]
table_headers = ['Supported Targets'] + table_headers
res = '| ' + ' | '.join(table_headers) + ' |\n'
res += '| ' + ' | '.join(['-' * len(item) for item in table_headers]) + ' |'
return res
def _parse_existing_support_table_str(_app: App) -> Tuple[Optional[str], List[str]]:
_readme_path = get_readme_path(_app)
if not _readme_path:
return None, SUPPORTED_TARGETS
with open(_readme_path, encoding='utf8') as _fr:
_readme_str = _fr.read()
support_string = SUPPORTED_TARGETS_TABLE_REGEX.findall(_readme_str)
if not support_string:
return None, SUPPORTED_TARGETS
# old style
parts = [
part.strip()
for part in support_string[0].split('\n', 1)[0].split('|')
if part.strip()
]
return support_string[0].strip(), [FORMAL_TO_USUAL[part] for part in parts[1:] if part in FORMAL_TO_USUAL]
def check_enable_build(_app: App, _old_supported_targets: List[str]) -> bool:
manifest_targets = app_build_targets(_app)
if manifest_targets == sorted(_old_supported_targets):
return True
_readme_path = get_readme_path(_app)
if_clause = f'IDF_TARGET in [{", ".join([doublequote(target) for target in sorted(_old_supported_targets)])}]'
print(
inspect.cleandoc(
f'''
{_app.app_dir}:
- enable build targets according to the manifest file: {manifest_targets}
- enable build targets according to the old Supported Targets table under readme "{_readme_path}": {_old_supported_targets}
If you want to disable some targets, please use the following snippet:
# Please combine this with the original one
#
# Notes:
# - please keep in mind to avoid duplicated folders as yaml keys
# - please use parentheses to group conditions, the "or" and "and" operators could only accept two operands
{_app.app_dir}:
enable:
- if: {if_clause}
temporary: true
reason: <why only enable build jobs for these targets>
'''
)
)
return False
apps = sorted(
find_apps(
paths,
'all',
recursive=True,
exclude_list=exclude_dirs or [],
manifest_files=get_all_manifest_files(),
default_build_targets=SUPPORTED_TARGETS + extra_default_build_targets,
config_rules_str=DEFAULT_CONFIG_RULES_STR,
)
)
exit_code = 0
checked_app_dirs = set()
for app in apps:
if app.app_dir not in checked_app_dirs:
checked_app_dirs.add(app.app_dir)
else:
continue
replace_str, old_supported_targets = _parse_existing_support_table_str(app)
success = check_enable_build(app, old_supported_targets)
if not success:
print(f'check_enable_build failed for app: {app}')
print('-' * 80)
if sorted(manifest_targets) != sorted(old_targets):
print(f'Build target MISMATCH!!!: {app_dir}')
print_diff_table(manifest_targets, old_targets, 'Manifest', 'README')
print(f'Corresponding manifest file: {App.MANIFEST.most_suitable_rule(app_dir).by_manifest_file}')
exit_code = 1
readme_path = get_readme_path(app)
readme_path = get_readme_path(app_dir)
new_table = generate_new_support_table(manifest_targets)
# no readme, create a new file
if not readme_path:
with open(os.path.join(app.app_dir, 'README.md'), 'w') as fw:
fw.write(_generate_new_support_table_str(app) + '\n')
print(f'Added new README file: {os.path.join(app.app_dir, "README.md")}')
print('-' * 80)
with open(os.path.join(app_dir, 'README.md'), 'w') as fw:
fw.write(new_table + '\n')
print(f'Auto-fixing: Added new README: {os.path.join(app_dir, "README.md")}')
exit_code = 1
# has old table, but different string
elif replace_str and replace_str != _generate_new_support_table_str(app):
elif old_table and old_table != new_table:
with open(readme_path) as fr:
readme_str = fr.read()
with open(readme_path, 'w') as fw:
fw.write(
readme_str.replace(
replace_str, _generate_new_support_table_str(app)
)
)
print(f'Modified README file: {readme_path}')
print('-' * 80)
fw.write(readme_str.replace(old_table, new_table))
print(f'Auto-fixing: Updated README: {readme_path}')
exit_code = 1
# does not have old table
elif not replace_str:
elif not old_table:
with open(readme_path) as fr:
readme_str = fr.read()
with open(readme_path, 'w') as fw:
fw.write(
_generate_new_support_table_str(app) + '\n\n' + readme_str
) # extra new line
print(f'Modified README file: {readme_path}')
print('-' * 80)
fw.write(new_table + '\n\n' + readme_str) # extra new line
print(f'Auto-fixing: Modified README: {readme_path}')
exit_code = 1
if exit_code != 0:
print('') # extra new line for readability
print('Related documentation:')
print(
'\t- https://docs.espressif.com/projects/idf-build-apps/en/latest/references/manifest.html#enable-disable-rules'
)
sys.exit(exit_code)
def get_grouped_cases(paths: list[str]) -> dict[str, dict[str, set[str]]]:
"""
return something like this:
{
app_dir: {
'script_paths': {'path/to/script1', 'path/to/script2'},
'targets': {'esp32', 'esp32s2', 'esp32s3', 'esp32c3', 'esp32c2', 'linux'},
}
}
"""
from idf_ci import get_pytest_cases
pytest_cases = get_pytest_cases(
paths=paths,
marker_expr=None, # don't filter host_test
)
grouped_cases = {}
for case in pytest_cases:
for pytest_app in case.apps:
app_dir = idf_relpath(pytest_app.path)
if app_dir not in grouped_cases:
grouped_cases[app_dir] = {
'script_paths': {case.path},
'targets': {pytest_app.target},
}
else:
grouped_cases[app_dir]['script_paths'].add(case.path)
grouped_cases[app_dir]['targets'].add(pytest_app.target)
return grouped_cases
def check_test_scripts(
paths: List[str],
exclude_dirs: Optional[List[str]] = None,
bypass_check_test_targets: Optional[List[str]] = None,
extra_default_build_targets: Optional[List[str]] = None,
paths: list[str],
exclude_dirs: list[str] | None = None,
*,
bypass_targets: list[str] | None = None,
) -> None:
from idf_build_apps import App, find_apps
from idf_build_apps.constants import SUPPORTED_TARGETS
from idf_pytest.script import get_pytest_cases
# takes long time, run only in CI
# dict:
# {
# app_dir: {
# 'script_path': 'path/to/script',
# 'targets': ['esp32', 'esp32s2', 'esp32s3', 'esp32c3', 'esp32c2', 'linux'],
# }
# }
def check_enable_test(
_app: App,
_pytest_app_dir_targets_dict: Dict[str, Dict[str, str]],
) -> bool:
if _app.app_dir in _pytest_app_dir_targets_dict:
test_script_path = _pytest_app_dir_targets_dict[_app.app_dir]['script_path']
actual_verified_targets = sorted(
set(_pytest_app_dir_targets_dict[_app.app_dir]['targets'])
)
else:
return True # no test case
grouped_apps = get_grouped_apps(paths, exclude_dirs)
grouped_cases = get_grouped_cases(paths)
exit_code = 0
manifest_targets = sorted({
target for target in (
App.MANIFEST.enable_test_targets(_app.app_dir)
+ App.MANIFEST.enable_test_targets(_app.app_dir, config_name=_app.config_name)
)
})
for app_dir, apps in grouped_apps.items():
if app_dir not in grouped_cases:
continue
actual_extra_tested_targets = set(actual_verified_targets) - set(manifest_targets)
if actual_extra_tested_targets - set(bypass_check_test_targets or []):
print(
inspect.cleandoc(
f'''
{_app.app_dir}:
- enable test targets according to the manifest file: {manifest_targets}
- enable test targets according to the test scripts: {actual_verified_targets}
test scripts enabled targets should be a subset of the manifest file declared ones.
Please check the test script: {test_script_path}.
'''
# manifest defined ones
manifest_targets = sorted(
{
target
for app in apps
for target in (
App.MANIFEST.enable_test_targets(app_dir)
+ App.MANIFEST.enable_test_targets(app_dir, config_name=app.config_name)
)
)
return False
}
)
actual_targets = sorted(grouped_cases[app_dir]['targets'])
if manifest_targets == actual_verified_targets:
return True
if not ((set(manifest_targets) ^ set(actual_verified_targets)) - set(bypass_check_test_targets or [])):
print(f'WARNING: bypass test script check on {_app.app_dir} for targets {bypass_check_test_targets} ')
return True
if manifest_targets == actual_targets:
continue
if not ((set(manifest_targets) ^ set(actual_targets)) - set(bypass_targets or [])):
continue
if_clause = f'IDF_TARGET in [{", ".join([doublequote(target) for target in sorted(set(manifest_targets) - set(actual_verified_targets))])}]'
print(f'Test target MISMATCH!!!: {app_dir}')
print_diff_table(manifest_targets, actual_targets, 'Manifest', 'Test Scripts', bypass_targets)
print(f'Corresponding manifest file: {App.MANIFEST.most_suitable_rule(app_dir).by_manifest_file}')
print('Corresponding test scripts:')
for script_path in grouped_cases[app_dir]['script_paths']:
print(' - ' + script_path)
exit_code = 1
if exit_code != 0:
print('') # extra new line for readability
print('Related documentation:')
print(
inspect.cleandoc(
f'''
{_app.app_dir}:
- enable test targets according to the manifest file: {manifest_targets}
- enable test targets according to the test scripts: {actual_verified_targets}
the test scripts enabled test targets should be the same with the manifest file enabled ones. Please check
the test script manually: {test_script_path}.
If you want to enable test targets in the pytest test scripts, please add `@pytest.mark.MISSING_TARGET`
marker above the test case function.
If you want to disable the test targets in the manifest file, please modify your manifest file with
the following code snippet:
# Please combine this with the original one
#
# Notes:
# - please keep in mind to avoid duplicated folders as yaml keys
# - please use parentheses to group conditions, the "or" and "and" operators could only accept two operands
{_app.app_dir}:
disable_test:
- if: {if_clause}
temporary: true
reason: <why you disable this test>
'''
)
'\t- https://docs.espressif.com/projects/idf-build-apps/en/latest/references/manifest.html#enable-disable-rules'
)
return False
apps = sorted(
find_apps(
paths,
'all',
recursive=True,
exclude_list=exclude_dirs or [],
manifest_files=get_all_manifest_files(),
default_build_targets=SUPPORTED_TARGETS + extra_default_build_targets,
config_rules_str=DEFAULT_CONFIG_RULES_STR,
print(
'\t- https://docs.espressif.com/projects/esp-idf/en/latest/esp32/contribute/esp-idf-tests-with-pytest.html'
)
)
exit_code = 0
pytest_cases = get_pytest_cases(paths)
pytest_app_dir_targets_dict = {}
for case in pytest_cases:
for pytest_app in case.apps:
app_dir = os.path.relpath(pytest_app.path, IDF_PATH)
if app_dir not in pytest_app_dir_targets_dict:
pytest_app_dir_targets_dict[app_dir] = {
'script_path': case.path,
'targets': [pytest_app.target],
}
else:
pytest_app_dir_targets_dict[app_dir]['targets'].append(
pytest_app.target
)
checked_app_dirs = set()
for app in apps:
if app.app_dir not in checked_app_dirs:
checked_app_dirs.add(app.app_dir)
else:
continue
success = check_enable_test(
app, pytest_app_dir_targets_dict
)
if not success:
print(f'check_enable_test failed for app: {app}')
print('-' * 80)
exit_code = 1
continue
sys.exit(exit_code)
def check_exist() -> None:
exit_code = 0
config_files = get_all_manifest_files()
for file in config_files:
if 'managed_components' in Path(file).parts:
continue
with open(file) as fr:
configs = yaml.safe_load(fr)
for path in configs.keys():
if path.startswith('.'):
continue
if not os.path.isdir(path):
print(f'Path \'{path}\' referred in \'{file}\' does not exist!')
exit_code = 1
sys.exit(exit_code)
@@ -398,92 +307,57 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description='ESP-IDF apps build/test checker')
action = parser.add_subparsers(dest='action')
_check_readme = action.add_parser('check-readmes')
_check_readme.add_argument('paths', nargs='+', help='check under paths')
_check_readme.add_argument(
'-c',
'--config',
default=os.path.join(IDF_PATH, '.gitlab', 'ci', 'default-build-test-rules.yml'),
help='default build test rules config file',
)
_check_test_scripts = action.add_parser('check-test-scripts')
_check_test_scripts.add_argument('paths', nargs='+', help='check under paths')
_check_test_scripts.add_argument(
'-c',
'--config',
default=os.path.join(IDF_PATH, '.gitlab', 'ci', 'default-build-test-rules.yml'),
help='default build test rules config file',
)
_check_exist = action.add_parser('check-exist')
readme_parser = action.add_parser('check-readmes')
readme_parser.add_argument('paths', nargs='+', help='check under paths')
test_parser = action.add_parser('check-test-scripts')
test_parser.add_argument('paths', nargs='+', help='check under paths')
arg = parser.parse_args()
# Since this script is executed from the pre-commit hook environment, make sure IDF_PATH is set
os.environ['IDF_PATH'] = os.path.realpath(
os.path.join(os.path.dirname(__file__), '..', '..')
check_dirs = set()
# check if *_caps.h files changed
check_all = False
soc_caps_header_files = list((Path(IDF_PATH) / 'components' / 'soc').glob('**/*_caps.h'))
for p in arg.paths:
if Path(p).resolve() in soc_caps_header_files:
check_all = True
break
if os.path.isfile(p):
check_dirs.add(os.path.dirname(p))
else:
check_dirs.add(p)
if 'tools/idf_py_actions/constants.py' in arg.paths or 'tools/ci/check_build_test_rules.py' in arg.paths:
check_all = True
if check_all:
check_dirs = {IDF_PATH}
_exclude_dirs = [
os.path.join(IDF_PATH, 'tools', 'unit-test-app'),
os.path.join(IDF_PATH, 'tools', 'test_build_system'),
os.path.join(IDF_PATH, 'tools', 'templates', 'sample_project'),
os.path.join(IDF_PATH, 'tools', 'cmakev2', 'test'),
]
os.environ.update(
{
'IDF_PATH': IDF_PATH,
'INCLUDE_NIGHTLY_RUN': '1',
'NIGHTLY_RUN': '1',
}
)
if arg.action == 'check-exist':
check_exist()
else:
check_dirs = set()
# check if *_caps.h files changed
check_all = False
soc_caps_header_files = list(
(Path(IDF_PATH) / 'components' / 'soc').glob('**/*_caps.h')
if arg.action == 'check-readmes':
check_readme(
list(check_dirs),
_exclude_dirs,
)
elif arg.action == 'check-test-scripts':
check_test_scripts(
list(check_dirs),
_exclude_dirs,
bypass_targets=BYPASS_CHECK_TEST_TARGETS,
)
for p in arg.paths:
if Path(p).resolve() in soc_caps_header_files:
check_all = True
break
if os.path.isfile(p):
check_dirs.add(os.path.dirname(p))
else:
check_dirs.add(p)
if 'tools/idf_py_actions/constants.py' in arg.paths or 'tools/ci/check_build_test_rules.py' in arg.paths:
check_all = True
if check_all:
check_dirs = {IDF_PATH}
_exclude_dirs = [os.path.join(IDF_PATH, 'tools', 'unit-test-app'),
os.path.join(IDF_PATH, 'tools', 'test_build_system', 'build_test_app'),
os.path.join(IDF_PATH, 'tools', 'templates', 'sample_project')]
else:
_exclude_dirs = [os.path.join(IDF_PATH, 'tools', 'templates', 'sample_project')]
extra_default_build_targets_list: List[str] = []
bypass_check_test_targets_list: List[str] = []
if arg.config:
with open(arg.config) as fr:
configs = yaml.safe_load(fr)
if configs:
extra_default_build_targets_list = (
configs.get('extra_default_build_targets') or []
)
bypass_check_test_targets_list = (
configs.get('bypass_check_test_targets') or []
)
if arg.action == 'check-readmes':
os.environ['INCLUDE_NIGHTLY_RUN'] = '1'
os.environ['NIGHTLY_RUN'] = '1'
check_readme(
list(check_dirs),
exclude_dirs=_exclude_dirs,
extra_default_build_targets=extra_default_build_targets_list,
)
elif arg.action == 'check-test-scripts':
os.environ['INCLUDE_NIGHTLY_RUN'] = '1'
os.environ['NIGHTLY_RUN'] = '1'
check_test_scripts(
list(check_dirs),
exclude_dirs=_exclude_dirs,
bypass_check_test_targets=bypass_check_test_targets_list,
extra_default_build_targets=extra_default_build_targets_list,
)

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env python
#
# internal use only for CI
# get latest MR information by source branch
#
# SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import argparse
import logging
import os
import subprocess
import typing as t
from pathlib import Path
from gitlab_api import Gitlab
from idf_ci_utils import IDF_PATH
if t.TYPE_CHECKING:
from gitlab.v4.objects import ProjectCommit, ProjectMergeRequest
def _get_mr_obj(source_branch: str) -> t.Optional['ProjectMergeRequest']:
gl = Gitlab(os.getenv('CI_PROJECT_ID', 'espressif/esp-idf'))
if not gl.project:
return None
mrs = gl.project.mergerequests.list(state='opened', source_branch=source_branch)
if mrs:
return mrs[0] # one source branch can only have one opened MR at one moment
else:
return None
def get_mr_iid(source_branch: str) -> str:
mr = _get_mr_obj(source_branch)
if not mr:
return ''
else:
return str(mr.iid)
def get_mr_changed_files(source_branch: str) -> t.List[str]:
mr = _get_mr_obj(source_branch)
if not mr:
return []
git_output = subprocess.check_output(
['git', 'diff', '--name-only', '--diff-filter=d', f'origin/{mr.target_branch}...origin/{source_branch}']
).decode('utf8')
return [line.strip() for line in git_output.splitlines() if line.strip()]
def get_mr_commits(source_branch: str) -> t.List['ProjectCommit']:
mr = _get_mr_obj(source_branch)
if not mr:
return []
return list(mr.commits())
_COMPONENT_NAME_DIR_RECORDS = {}
def get_modified_component(filepath: str) -> t.Optional[str]:
"""Return the component name if the file is in a component directory, otherwise None."""
try:
f_path = Path(filepath).resolve().relative_to(IDF_PATH)
except ValueError: # not in IDF_PATH
return None
# skip md files, etc.
if f_path.suffix in ['.md', '.yml']:
return None
# skip test_apps files
if 'test_apps' in f_path.parts:
return None
component_parent_dirs = [f_path.parts[0]]
for part in f_path.parts[1:]:
if component_parent_dirs[-1] == 'components' or component_parent_dirs[-1].endswith('common_components'):
if part not in _COMPONENT_NAME_DIR_RECORDS:
logging.debug('Found component "%s" in path "%s"' % (part, component_parent_dirs))
_COMPONENT_NAME_DIR_RECORDS[part] = component_parent_dirs
elif _COMPONENT_NAME_DIR_RECORDS.get(part) != component_parent_dirs:
logging.debug(
'WARNING!!! Found component "%s" in path "%s" and "%s"'
% (part, component_parent_dirs, _COMPONENT_NAME_DIR_RECORDS.get(part))
)
return part
component_parent_dirs.append(part)
return None
def get_mr_components(
source_branch: t.Optional[str] = None, modified_files: t.Optional[t.List[str]] = None
) -> t.List[str]:
components: t.Set[str] = set()
if modified_files is None:
if not source_branch:
raise RuntimeError('--src-branch is required if --modified-files is not provided')
modified_files = get_mr_changed_files(source_branch)
for f in modified_files:
modified_component = get_modified_component(f)
if modified_component:
components.add(modified_component)
return list(components)
def get_target_in_tags(tags: str) -> str:
from idf_pytest.constants import TARGET_MARKERS
for x in tags.split(','):
if x in TARGET_MARKERS:
return x
raise RuntimeError(f'No target marker found in {tags}')
def _print_list(_list: t.List[str], separator: str = '\n') -> None:
print(separator.join(_list))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Get the latest merge request info by pipeline')
actions = parser.add_subparsers(dest='action', help='info type', required=True)
common_args = argparse.ArgumentParser(add_help=False)
common_args.add_argument('--src-branch', help='source branch')
common_args.add_argument(
'--modified-files',
nargs='+',
help='space-separated list specifies the modified files. will be detected by --src-branch if not provided',
)
actions.add_parser('id', parents=[common_args])
actions.add_parser('commits', parents=[common_args])
actions.add_parser('components', parents=[common_args])
target = actions.add_parser('target_in_tags')
target.add_argument('tags', help='comma separated tags, e.g., esp32,generic')
args = parser.parse_args()
if args.action == 'id':
if not args.src_branch:
raise RuntimeError('--src-branch is required')
print(get_mr_iid(args.src_branch))
elif args.action == 'commits':
if not args.src_branch:
raise RuntimeError('--src-branch is required')
_print_list([commit.id for commit in get_mr_commits(args.src_branch)])
elif args.action == 'components':
_print_list(get_mr_components(args.src_branch, args.modified_files))
elif args.action == 'target_in_tags':
print(get_target_in_tags(args.tags))
else:
raise NotImplementedError('not possible to get here')

View File

@@ -1,217 +0,0 @@
#!/usr/bin/env python3
#
# CI script to deploy docs to a webserver. Not useful outside of CI environment
#
#
# SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
#
import glob
import os
import os.path
import re
import stat
import subprocess
import sys
import tarfile
from typing import Any, List, Tuple
import packaging.version
def env(variable: str, default: str=None) -> str:
""" Shortcut to return the expanded version of an environment variable """
return os.path.expandvars(os.environ.get(variable, default) if default else os.environ[variable])
# import sanitize_version from the docs directory, shared with here
sys.path.append(os.path.join(env('IDF_PATH'), 'docs'))
from sanitize_version import sanitize_version # noqa
def main() -> None:
# if you get KeyErrors on the following lines, it's probably because you're not running in Gitlab CI
git_ver = env('GIT_VER') # output of git describe --always
ci_ver = env('CI_COMMIT_REF_NAME', git_ver) # branch or tag we're building for (used for 'release' & URL)
version = sanitize_version(ci_ver)
print('Git version: {}'.format(git_ver))
print('CI Version: {}'.format(ci_ver))
print('Deployment version: {}'.format(version))
if not version:
raise RuntimeError('A version is needed to deploy')
build_dir = env('DOCS_BUILD_DIR') # top-level local build dir, where docs have already been built
if not build_dir:
raise RuntimeError('Valid DOCS_BUILD_DIR is needed to deploy')
url_base = env('DOCS_DEPLOY_URL_BASE') # base for HTTP URLs, used to print the URL to the log after deploying
docs_server = env('DOCS_DEPLOY_SERVER') # ssh server to deploy to
docs_user = env('DOCS_DEPLOY_SERVER_USER')
docs_path = env('DOCS_DEPLOY_PATH') # filesystem path on DOCS_SERVER
if not docs_server:
raise RuntimeError('Valid DOCS_DEPLOY_SERVER is needed to deploy')
if not docs_user:
raise RuntimeError('Valid DOCS_DEPLOY_SERVER_USER is needed to deploy')
docs_server = '{}@{}'.format(docs_user, docs_server)
if not docs_path:
raise RuntimeError('Valid DOCS_DEPLOY_PATH is needed to deploy')
print('DOCS_DEPLOY_SERVER {} DOCS_DEPLOY_PATH {}'.format(docs_server, docs_path))
tarball_path, version_urls = build_doc_tarball(version, git_ver, build_dir)
deploy(version, tarball_path, docs_path, docs_server)
print('Docs URLs:')
doc_deploy_type = os.getenv('TYPE')
for vurl in version_urls:
language, _, target = vurl.split('/')
tag = '{}_{}'.format(language, target)
url = '{}/{}/index.html'.format(url_base, vurl) # (index.html needed for the preview server)
url = re.sub(r'([^:])//', r'\1/', url) # get rid of any // that isn't in the https:// part
print('[document {}][{}] {}'.format(doc_deploy_type, tag, url))
# note: it would be neater to use symlinks for stable, but because of the directory order
# (language first) it's kind of a pain to do on a remote server, so we just repeat the
# process but call the version 'stable' this time
if is_stable_version(version):
print('Deploying again as stable version...')
tarball_path, version_urls = build_doc_tarball('stable', git_ver, build_dir)
deploy('stable', tarball_path, docs_path, docs_server)
def deploy(version: str, tarball_path: str, docs_path: str, docs_server: str) -> None:
def run_ssh(commands: List) -> None:
""" Log into docs_server and run a sequence of commands using ssh """
print('Running ssh: {}'.format(commands))
subprocess.run(['ssh', '-o', 'BatchMode=yes', docs_server, '-x', ' && '.join(commands)], check=True)
# copy the version tarball to the server
run_ssh(['mkdir -p {}'.format(docs_path)])
print('Running scp {} to {}'.format(tarball_path, '{}:{}'.format(docs_server, docs_path)))
subprocess.run(['scp', '-B', tarball_path, '{}:{}'.format(docs_server, docs_path)], check=True)
tarball_name = os.path.basename(tarball_path)
run_ssh(['cd {}'.format(docs_path),
'rm -rf ./*/{}'.format(version), # remove any pre-existing docs matching this version
'tar -zxvf {}'.format(tarball_name), # untar the archive with the new docs
'rm {}'.format(tarball_name)])
# Note: deleting and then extracting the archive is a bit awkward for updating stable/latest/etc
# as the version will be invalid for a window of time. Better to do it atomically, but this is
# another thing made much more complex by the directory structure putting language before version...
def build_doc_tarball(version: str, git_ver: str, build_dir: str) -> Tuple[str, List]:
""" Make a tar.gz archive of the docs, in the directory structure used to deploy as
the given version """
version_paths = []
tarball_path = '{}/{}.tar.gz'.format(build_dir, version)
# find all the 'html/' directories under build_dir
html_dirs = glob.glob('{}/**/html/'.format(build_dir), recursive=True)
print('Found %d html directories' % len(html_dirs))
pdfs = glob.glob('{}/**/latex/build/*.pdf'.format(build_dir), recursive=True)
print('Found %d PDFs in latex directories' % len(pdfs))
# add symlink for stable and latest and adds them to PDF blob
symlinks = create_and_add_symlinks(version, git_ver, pdfs)
def not_sources_dir(ti: Any) -> Any:
print(type(ti))
""" Filter the _sources directories out of the tarballs """
if ti.name.endswith('/_sources'):
return None
ti.mode |= stat.S_IWGRP # make everything group-writeable
return ti
try:
os.remove(tarball_path)
except OSError:
pass
with tarfile.open(tarball_path, 'w:gz') as tarball:
for html_dir in html_dirs:
# html_dir has the form '<ignored>/<language>/<target>/html/'
target_dirname = os.path.dirname(os.path.dirname(html_dir))
target = os.path.basename(target_dirname)
language = os.path.basename(os.path.dirname(target_dirname))
# when deploying, we want the top-level directory layout 'language/version/target'
archive_path = '{}/{}/{}'.format(language, version, target)
print("Archiving '{}' as '{}'...".format(html_dir, archive_path))
tarball.add(html_dir, archive_path, filter=not_sources_dir)
version_paths.append(archive_path)
for pdf_path in pdfs:
# pdf_path has the form '<ignored>/<language>/<target>/latex/build'
latex_dirname = os.path.dirname(pdf_path)
pdf_filename = os.path.basename(pdf_path)
target_dirname = os.path.dirname(os.path.dirname(latex_dirname))
target = os.path.basename(target_dirname)
language = os.path.basename(os.path.dirname(target_dirname))
# when deploying, we want the layout 'language/version/target/pdf'
archive_path = '{}/{}/{}/{}'.format(language, version, target, pdf_filename)
print("Archiving '{}' as '{}'...".format(pdf_path, archive_path))
tarball.add(pdf_path, archive_path)
for symlink in symlinks:
os.unlink(symlink)
return (os.path.abspath(tarball_path), version_paths)
def create_and_add_symlinks(version: str, git_ver: str, pdfs: List) -> List:
""" Create symbolic links for PDFs for 'latest' and 'stable' releases """
symlinks = []
if 'stable' in version or 'latest' in version:
for pdf_path in pdfs:
symlink_path = pdf_path.replace(git_ver, version)
os.symlink(pdf_path, symlink_path)
symlinks.append(symlink_path)
pdfs.extend(symlinks)
print('Found %d PDFs in latex directories after adding symlink' % len(pdfs))
return symlinks
def is_stable_version(version: str) -> bool:
""" Heuristic for whether this is the latest stable release """
if not version.startswith('v'):
return False # branch name
if '-' in version:
return False # prerelease tag
git_out = subprocess.check_output(['git', 'tag', '-l']).decode('utf-8')
versions = [v.strip() for v in git_out.split('\n')]
versions = [v for v in versions if re.match(r'^v[\d\.]+$', v.strip())] # include vX.Y.Z only
versions_pack = [packaging.version.parse(v) for v in versions]
max_version = max(versions_pack)
if max_version.public != version[1:]:
print('Stable version is v{}. This version is {}.'.format(max_version.public, version))
return False
else:
print('This version {} is the stable version'.format(version))
return True
if __name__ == '__main__':
main()

View File

@@ -1,49 +1,9 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import os
from idf_ci_utils import IDF_PATH
# use relative path to avoid absolute path in pipeline
DEFAULT_TEST_PATHS = [
'examples',
os.path.join('tools', 'test_apps'),
'components',
]
DEFAULT_APPS_BUILD_PER_JOB = 60
DEFAULT_CASES_TEST_PER_JOB = 30
DEFAULT_BUILD_CHILD_PIPELINE_FILEPATH = os.path.join(IDF_PATH, 'build_child_pipeline.yml')
DEFAULT_TARGET_TEST_CHILD_PIPELINE_FILEPATH = os.path.join(IDF_PATH, 'target_test_child_pipeline.yml')
DEFAULT_BUILD_CHILD_PIPELINE_NAME = 'Build Child Pipeline'
DEFAULT_TARGET_TEST_CHILD_PIPELINE_NAME = 'Target Test Child Pipeline'
TEST_RELATED_BUILD_JOB_NAME = 'build_test_related_apps'
NON_TEST_RELATED_BUILD_JOB_NAME = 'build_non_test_related_apps'
COMMENT_START_MARKER = '### Dynamic Pipeline Report'
TEST_RELATED_APPS_FILENAME = 'test_related_apps.txt'
NON_TEST_RELATED_APPS_FILENAME = 'non_test_related_apps.txt'
TEST_RELATED_APPS_DOWNLOAD_URLS_FILENAME = 'test_related_apps_download_urls.yml'
REPORT_TEMPLATE_FILEPATH = os.path.join(
IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'report.template.html'
)
RETRY_JOB_PICTURE_PATH = 'tools/ci/dynamic_pipelines/templates/retry-jobs.png'
RETRY_JOB_TITLE = '\n\nRetry failed jobs with with help of "retry_failed_jobs" stage of the pipeline:'
RETRY_JOB_PICTURE_LINK = '![Retry Jobs Image]({pic_url})'
BUILD_ONLY_LABEL = 'For Maintainers: Only Build Tests'
KNOWN_GENERATE_TEST_CHILD_PIPELINE_WARNINGS_FILEPATH = os.path.join(
IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'known_generate_test_child_pipeline_warnings.yml'
)
CI_JOB_TOKEN = os.getenv('CI_JOB_TOKEN', '')
CI_DASHBOARD_API = os.getenv('CI_DASHBOARD_API', '')
CI_PAGES_URL = os.getenv('CI_PAGES_URL', '')
CI_PROJECT_URL = os.getenv('CI_PROJECT_URL', '')
CI_MERGE_REQUEST_SOURCE_BRANCH_SHA = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', '')

Some files were not shown because too many files have changed in this diff Show More