mirror of
https://github.com/espressif/esp-idf.git
synced 2026-09-29 09:44:37 +03:00
Merge branch 'ci/apply-common-scripts-v5.5' into 'release/v5.5'
ci: apply common-scripts refactor (v5.5) See merge request espressif/esp-idf!51030
This commit is contained in:
+18
-2
@@ -2,11 +2,25 @@
|
||||
|
||||
include:
|
||||
- project: "ci/actions/common"
|
||||
ref: "v2"
|
||||
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.5.yaml"
|
||||
changes:
|
||||
- ".pre-commit-config.yaml"
|
||||
- project: "ci/actions/common"
|
||||
file: "templates/idf/build-docs.yml"
|
||||
inputs:
|
||||
@@ -20,13 +34,15 @@ include:
|
||||
DOCTGT: "esp32p4"
|
||||
|
||||
- ".gitlab/ci/danger.yml"
|
||||
- ".gitlab/ci/common.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/pre_check.yml"
|
||||
- ".gitlab/ci/build.yml"
|
||||
- ".gitlab/ci/host-test.yml"
|
||||
- ".gitlab/ci/post_deploy.yml"
|
||||
|
||||
+5
-66
@@ -245,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`
|
||||
|
||||
+3
-58
@@ -110,61 +110,6 @@ build_clang_test_apps_riscv:
|
||||
####################
|
||||
# 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
|
||||
script:
|
||||
- run_cmd idf-ci --debug gitlab build-child-pipeline
|
||||
-p components
|
||||
-p examples
|
||||
-p tools/test_apps
|
||||
--modified-files $MR_MODIFIED_FILES
|
||||
|
||||
build_child_pipeline:
|
||||
stage: build
|
||||
needs:
|
||||
- job: fast_template_app
|
||||
optional: true
|
||||
artifacts: false
|
||||
- pipeline_variables
|
||||
- generate_build_child_pipeline
|
||||
variables:
|
||||
MR_MODIFIED_COMPONENTS: $MR_MODIFIED_COMPONENTS
|
||||
MR_MODIFIED_FILES: $MR_MODIFIED_FILES
|
||||
PARENT_PIPELINE_ID: $CI_PIPELINE_ID
|
||||
# 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
|
||||
|
||||
generate_prebuild_report:
|
||||
extends:
|
||||
- .build_template
|
||||
tags: [fast_run, shiny]
|
||||
needs:
|
||||
- pipeline_variables
|
||||
artifacts:
|
||||
paths:
|
||||
- prebuild_report.html
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
script:
|
||||
- run_cmd idf-ci build collect --format html -o prebuild_report.html
|
||||
- echo "Report generated at https://${CI_PAGES_HOSTNAME}:${CI_SERVER_PORT}/-/esp-idf/-/jobs/${CI_JOB_ID}/artifacts/prebuild_report.html"
|
||||
# !!!
|
||||
# configurations moved to ci/actions/common/idf/
|
||||
# !!!
|
||||
|
||||
+24
-188
@@ -11,9 +11,20 @@ stages:
|
||||
- target_test
|
||||
- host_test
|
||||
- test_deploy
|
||||
- pre_deploy
|
||||
- deploy
|
||||
- post_deploy
|
||||
|
||||
include:
|
||||
- project: "ci/actions/common"
|
||||
ref: "v2"
|
||||
file: "templates/idf/common-scripts.yml"
|
||||
inputs:
|
||||
ci_python_constraint_file: "espidf.constraints.v5.5.txt"
|
||||
ci_python_constraint_branch: "backport/idf-ci-1.0-idf-v5.5"
|
||||
macos_image: "registry.hub.docker.com/espressif/macos-idf:sequoia-v5.5"
|
||||
windows_image: "windows_11_idf_v5_5"
|
||||
|
||||
variables:
|
||||
# System environment
|
||||
|
||||
@@ -58,33 +69,29 @@ variables:
|
||||
ESP_ENV_IMAGE: "${CI_REGISTRY}/ci/images/idf-v5.5:1"
|
||||
ESP_IDF_DOC_ENV_IMAGE: "${CI_REGISTRY}/ci/images/idf-v5.5-docs:1"
|
||||
TARGET_TEST_ENV_IMAGE: "${CI_REGISTRY}/ci/images/idf-v5.5-target-test:1"
|
||||
SONARQUBE_SCANNER_IMAGE: "${CI_DOCKER_REGISTRY}/sonarqube-scanner:5"
|
||||
|
||||
# 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.5.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.5.txt"
|
||||
@@ -106,182 +113,18 @@ variables:
|
||||
################################################
|
||||
# `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/ci:$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 ~/.espressif
|
||||
mv /tmp/constraint.txt ~/.espressif/${CI_PYTHON_CONSTRAINT_FILE}
|
||||
fi
|
||||
|
||||
# Mirror
|
||||
if [[ -n "$IDF_DONT_USE_MIRRORS" ]]; then
|
||||
export IDF_MIRROR_PREFIX_MAP=
|
||||
fi
|
||||
# Optimize pip install
|
||||
if echo "${CI_RUNNER_TAGS}" | grep "shiny"; then
|
||||
export PIP_INDEX_URL="${PIP_INDEX_URL_SHINY}"
|
||||
fi
|
||||
if [[ "$(uname -m)" == "x86_64" ]] || [[ "$(uname -m)" == "aarch64" ]]; then
|
||||
export IDF_PIP_WHEELS_URL=""
|
||||
fi
|
||||
|
||||
# install.sh
|
||||
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
|
||||
else
|
||||
run_cmd bash install.sh --enable-ci
|
||||
fi
|
||||
section_end "running_install_sh"
|
||||
else
|
||||
section_start "install_python_env" "Install Python environment, skip required tools check"
|
||||
run_cmd python tools/idf_tools.py install-python-env --features ci,test-specific
|
||||
export IDF_SKIP_TOOLS_CHECK=1
|
||||
section_end "install_python_env"
|
||||
fi
|
||||
|
||||
section_start "source_export" "Source export.sh"
|
||||
source ./export.sh
|
||||
section_end "source_export"
|
||||
|
||||
# Eager upgrade of CI dependencies
|
||||
# Done after sourcing export.sh so that we could easily invoke the right pip
|
||||
section_start "upgrade_ci_dependencies" "Upgrading CI dependencies"
|
||||
pip install --upgrade --upgrade-strategy=eager -r $IDF_PATH/tools/requirements/requirements.ci.txt -c ~/.espressif/${CI_PYTHON_CONSTRAINT_FILE}
|
||||
# we need the latest DEV release for esptool to be installed in every job without bumping the minimal requirement in
|
||||
# the constraint file.
|
||||
pip install --upgrade --pre esptool -c ~/.espressif/${CI_PYTHON_CONSTRAINT_FILE}
|
||||
section_end "upgrade_ci_dependencies"
|
||||
|
||||
REEXPORT_NEEDED=0
|
||||
if [[ ! -z "$INSTALL_EXTRA_TOOLS" ]]; then
|
||||
section_start "installing_optional_tools" "Install optional tools ${INSTALL_EXTRA_TOOLS}"
|
||||
run_cmd $IDF_PATH/tools/idf_tools.py --non-interactive install $INSTALL_EXTRA_TOOLS
|
||||
section_end "installing_optional_tools"
|
||||
|
||||
REEXPORT_NEEDED=1
|
||||
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
|
||||
|
||||
REEXPORT_NEEDED=1
|
||||
fi
|
||||
|
||||
if [[ $REEXPORT_NEEDED -eq 1 ]]; then
|
||||
section_start "re_source_export" "Re-source export.sh"
|
||||
source ./export.sh
|
||||
section_end "re_source_export"
|
||||
fi
|
||||
|
||||
# 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
|
||||
run_cmd idf-ci gitlab upload-artifacts --type log
|
||||
fi
|
||||
|
||||
.before_script:minimal:
|
||||
before_script:
|
||||
- *common-before_scripts
|
||||
- !reference [.common_before_scripts, minimal]
|
||||
|
||||
.after_script:build:
|
||||
after_script:
|
||||
- source tools/ci/utils.sh
|
||||
- source tools/ci/configure_ci_environment.sh
|
||||
- *show_ccache_statistics
|
||||
- *upload_failed_job_log_artifacts
|
||||
|
||||
.brew-macos-settings:
|
||||
variables:
|
||||
GIT_STRATEGY: fetch
|
||||
IDF_CCACHE_ENABLE: "0"
|
||||
CCACHE_DIR: "/var/tmp/cache/idf_ccache"
|
||||
tags:
|
||||
- macos-tart
|
||||
image: macos-sequoia-idf-v5.5
|
||||
after_script: [] # ccache now is disabled for macos brew runners
|
||||
timeout: 30m
|
||||
- !reference [.common_after_scripts, ccache]
|
||||
- run_cmd idf-ci gitlab upload-artifacts --type junit
|
||||
- |
|
||||
if [ $CI_JOB_STATUS = "failed" ]; then
|
||||
run_cmd idf-ci gitlab upload-artifacts --type log
|
||||
fi
|
||||
|
||||
#############
|
||||
# `default` #
|
||||
@@ -290,12 +133,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
|
||||
@@ -303,8 +140,7 @@ 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
|
||||
- export EXTRA_CFLAGS=${PEDANTIC_CFLAGS}
|
||||
|
||||
@@ -1,21 +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.
|
||||
# !!! DEPRECATED: use `additional_build_targets` in .idf_build_apps.toml instead
|
||||
#
|
||||
# - 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
|
||||
|
||||
bypass_check_test_targets:
|
||||
- esp32h21
|
||||
- esp32h4
|
||||
- esp32c5
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
@@ -7,9 +7,6 @@
|
||||
# run host_test jobs immediately, only after upload cache
|
||||
needs:
|
||||
- pipeline_variables
|
||||
- job: upload-pip-cache
|
||||
optional: true
|
||||
artifacts: false
|
||||
- job: upload-submodules-cache
|
||||
optional: true
|
||||
artifacts: false
|
||||
@@ -373,7 +370,6 @@ test_pytest_qemu:
|
||||
- run_cmd pytest
|
||||
--target $IDF_TARGET
|
||||
-m qemu
|
||||
--embedded-services idf,qemu
|
||||
--junitxml=XUNIT_RESULT.xml
|
||||
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
--qemu-extra-args \"-global driver=timer.$IDF_TARGET.timg,property=wdt_disable,value=true\"
|
||||
@@ -398,14 +394,14 @@ test_pytest_linux:
|
||||
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
- run_cmd pytest
|
||||
--target linux
|
||||
--embedded-services idf
|
||||
-m \"not macos\"
|
||||
--junitxml=XUNIT_RESULT.xml
|
||||
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
|
||||
test_pytest_macos:
|
||||
extends:
|
||||
- .host_test_template
|
||||
- .brew-macos-settings
|
||||
- .macos-settings
|
||||
artifacts:
|
||||
paths:
|
||||
- XUNIT_RESULT.xml
|
||||
@@ -480,6 +476,8 @@ test_pytest_macos:
|
||||
- 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}
|
||||
@@ -491,7 +489,7 @@ pytest_build_system:
|
||||
pytest_build_system_macos:
|
||||
extends:
|
||||
- .test_build_system_template
|
||||
- .brew-macos-settings
|
||||
- .macos-settings
|
||||
- .rules:build:macos
|
||||
parallel: 3
|
||||
|
||||
@@ -501,7 +499,7 @@ pytest_build_system_minimal_cmake:
|
||||
pytest_build_system_macos_minimal_cmake:
|
||||
extends:
|
||||
- .test_build_system_minimal_cmake_template
|
||||
- .brew-macos-settings
|
||||
- .macos-settings
|
||||
- .rules:build:macos
|
||||
variables:
|
||||
INSTALL_EXTRA_TOOLS: ninja cmake@3.16.3
|
||||
|
||||
@@ -2,22 +2,6 @@
|
||||
stage: post_deploy
|
||||
image: $ESP_ENV_IMAGE
|
||||
|
||||
generate_failed_jobs_report:
|
||||
extends:
|
||||
- .post_deploy_template
|
||||
tags: [build, shiny]
|
||||
when: always
|
||||
dependencies: # Do not download artifacts from the previous stages
|
||||
needs:
|
||||
- pipeline_variables
|
||||
artifacts:
|
||||
expire_in: 2 week
|
||||
when: always
|
||||
paths:
|
||||
- job_report.html
|
||||
script:
|
||||
- python tools/ci/dynamic_pipelines/scripts/generate_report.py --report-type job
|
||||
|
||||
sync_support_status:
|
||||
extends:
|
||||
- .post_deploy_template
|
||||
|
||||
+2
-124
@@ -1,9 +1,3 @@
|
||||
.pre_check_template:
|
||||
stage: pre_check
|
||||
image: $ESP_ENV_IMAGE
|
||||
tags: [build, shiny]
|
||||
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.
|
||||
@@ -119,127 +113,11 @@ check_test_scripts_build_test_rules:
|
||||
# requires basic pytest dependencies
|
||||
- python tools/ci/check_build_test_rules.py check-test-scripts examples/ tools/test_apps components
|
||||
|
||||
check_submodule_sync:
|
||||
check_gitlab_yaml:
|
||||
extends:
|
||||
- .pre_check_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"
|
||||
|
||||
pipeline_variables:
|
||||
extends:
|
||||
- .pre_check_template
|
||||
tags: [fast_run, shiny]
|
||||
variables:
|
||||
GIT_STRATEGY: "fetch"
|
||||
script:
|
||||
- |
|
||||
# Store the diff output in a temporary file
|
||||
TEMP_FILE=$(mktemp)
|
||||
# merged results pipelines, by default
|
||||
if [[ -n $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA ]]; then
|
||||
git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_SHA
|
||||
git fetch origin $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA
|
||||
|
||||
git diff --name-only $CI_MERGE_REQUEST_TARGET_BRANCH_SHA...$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA > "$TEMP_FILE"
|
||||
GIT_DIFF_OUTPUT=$(cat "$TEMP_FILE")
|
||||
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}
|
||||
git diff --name-only $CI_MERGE_REQUEST_DIFF_BASE_SHA $CI_COMMIT_SHA > "$TEMP_FILE"
|
||||
GIT_DIFF_OUTPUT=$(cat "$TEMP_FILE")
|
||||
# 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}
|
||||
git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA > "$TEMP_FILE"
|
||||
GIT_DIFF_OUTPUT=$(cat "$TEMP_FILE")
|
||||
else
|
||||
# pipeline source could be web, scheduler, etc.
|
||||
git fetch origin $CI_COMMIT_SHA --depth=2 ${GIT_FETCH_EXTRA_FLAGS}
|
||||
git diff --name-only $CI_COMMIT_SHA~1 $CI_COMMIT_SHA > "$TEMP_FILE"
|
||||
GIT_DIFF_OUTPUT=$(cat "$TEMP_FILE")
|
||||
fi
|
||||
# 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
|
||||
# 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 [ -z "$CI_MERGE_REQUEST_IID" ]; 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 [ -n "$PIPELINE_COMMIT_SHA" ]; then
|
||||
echo "PIPELINE_COMMIT_SHA=$PIPELINE_COMMIT_SHA" >> 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
|
||||
- run_cmd idf-ci gitlab pipeline-variables >> pipeline.env
|
||||
- cat pipeline.env
|
||||
- run_cmd idf-ci gitlab upload-artifacts --type env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: pipeline.env
|
||||
paths:
|
||||
- pipeline.env
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
|
||||
baseline_manifest_sha:
|
||||
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
|
||||
- python tools/ci/gitlab_yaml_linter.py
|
||||
|
||||
retry_failed_jobs:
|
||||
extends:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
extends:
|
||||
- .before_script:minimal
|
||||
stage: pre_check
|
||||
image: "${CI_DOCKER_REGISTRY}/esp-idf-pre-commit:1"
|
||||
image: "${CI_REGISTRY}/ci/images/pre-commit-idf-v5.5:1"
|
||||
tags: [pre-commit]
|
||||
variables:
|
||||
# Both shiny and brew runners can pick this job
|
||||
|
||||
@@ -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"
|
||||
+2
-25
@@ -13,16 +13,6 @@
|
||||
- "components/**/Kconfig"
|
||||
- "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
|
||||
@@ -68,6 +58,7 @@
|
||||
- "tools/ci/check_esp_memory_utils_headers.sh"
|
||||
- "tools/ci/check_blobs.sh"
|
||||
- "tools/ci/check_public_headers.py"
|
||||
- "tools/ci/check_public_headers_exceptions.txt"
|
||||
- "tools/ci/check_register_rw_half_word.cmake"
|
||||
- "tools/ci/check_register_rw_half_word.py"
|
||||
- "examples/build_system/**/*"
|
||||
@@ -102,6 +93,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"
|
||||
@@ -177,16 +169,10 @@
|
||||
.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-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")'
|
||||
|
||||
@@ -225,15 +211,6 @@
|
||||
rules:
|
||||
- <<: *if-dev-push
|
||||
|
||||
.rules:upload-python-cache:
|
||||
rules:
|
||||
- <<: *if-release-tag
|
||||
- <<: *if-schedule-nightly
|
||||
- <<: *if-protected-branch-push
|
||||
changes: *patterns-python-cache
|
||||
- <<: *if-label-upload_cache
|
||||
when: manual
|
||||
|
||||
.rules:upload-submodule-cache:
|
||||
rules:
|
||||
- <<: *if-release-tag
|
||||
|
||||
@@ -30,84 +30,3 @@ gcc_static_analyzer:
|
||||
- export EXTRA_CFLAGS=
|
||||
- export EXTRA_CXXFLAGS=
|
||||
- idf-build-apps build -p ${ANALYZING_APP} --disable-targets esp32h4 esp32h21 --no-check-warnings
|
||||
#
|
||||
## 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:check
|
||||
# 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
|
||||
|
||||
+27
-74
@@ -1,25 +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, brew]
|
||||
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
|
||||
variables:
|
||||
GIT_STRATEGY: fetch # use brew local mirror first
|
||||
before_script:
|
||||
- if ($env:IDF_DONT_USE_MIRRORS) {
|
||||
$env:IDF_MIRROR_PREFIX_MAP = ""
|
||||
}
|
||||
after_script: []
|
||||
- job: manual_gate
|
||||
optional: true
|
||||
|
||||
test_cli_installer_win:
|
||||
rules:
|
||||
@@ -34,15 +25,12 @@ test_cli_installer_win:
|
||||
- tools/tools.new.json
|
||||
- tools/test_idf_tools/test_python_env_logs.txt
|
||||
expire_in: 1 week
|
||||
variables:
|
||||
IDF_PATH: "$CI_PROJECT_DIR"
|
||||
timeout: 3h
|
||||
script:
|
||||
# Tools must be downloaded for testing
|
||||
# cmake@version that is supported
|
||||
- python ${IDF_PATH}\tools\idf_tools.py download required qemu-riscv32 qemu-xtensa cmake cmake@3.22.1
|
||||
- cd ${IDF_PATH}\tools\test_idf_tools
|
||||
- python -m pip install jsonschema
|
||||
- python tools\idf_tools.py download required qemu-riscv32 qemu-xtensa cmake cmake@3.22.1
|
||||
- cd tools\test_idf_tools
|
||||
- python .\test_idf_tools.py
|
||||
- python .\test_idf_tools_python_env.py
|
||||
|
||||
@@ -53,82 +41,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
|
||||
- .\export.ps1
|
||||
- python "${SUBMODULE_FETCH_TOOL}" -s "all"
|
||||
- cd ${IDF_PATH}/tools/test_idf_py
|
||||
- cd tools\test_idf_py
|
||||
- idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml=${IDF_PATH}/XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml ${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
|
||||
# Build tests
|
||||
.test_build_system_template_win:
|
||||
stage: host_test
|
||||
variables:
|
||||
# Enable ccache for all build jobs. See configure_ci_environment.sh for more ccache related settings.
|
||||
IDF_CCACHE_ENABLE: "1"
|
||||
PYTHONPATH: "$PYTHONPATH;$IDF_PATH\\tools;$IDF_PATH\\tools\\esp_app_trace;$IDF_PATH\\components\\partition_table;$IDF_PATH\\tools\\ci\\python_packages"
|
||||
before_script: []
|
||||
after_script: []
|
||||
extends: .host_test_win_template
|
||||
timeout: 4 hours
|
||||
artifacts:
|
||||
paths:
|
||||
- XUNIT_RESULT.xml
|
||||
- test_build_system
|
||||
expire_in: 2 days
|
||||
when: always
|
||||
reports:
|
||||
junit: XUNIT_RESULT.xml
|
||||
script:
|
||||
- .\install.ps1 --enable-ci
|
||||
- . .\export.ps1
|
||||
- python "${SUBMODULE_FETCH_TOOL}" -s "all"
|
||||
- cd ${IDF_PATH}\tools\test_build_system
|
||||
- cd tools\test_build_system
|
||||
- idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
- pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml ${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
|
||||
pytest_build_system_win:
|
||||
extends:
|
||||
- .test_build_system_template_win
|
||||
- .rules:labels:windows_pytest_build_system
|
||||
parallel: 2
|
||||
needs:
|
||||
- job: manual_gate
|
||||
optional: true
|
||||
tags: [windows-build, brew]
|
||||
artifacts:
|
||||
paths:
|
||||
- XUNIT_RESULT.xml
|
||||
- test_build_system
|
||||
expire_in: 2 days
|
||||
reports:
|
||||
junit: XUNIT_RESULT.xml
|
||||
when: always
|
||||
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, brew]
|
||||
artifacts:
|
||||
paths:
|
||||
- XUNIT_RESULT.xml
|
||||
- test_build_system
|
||||
expire_in: 2 days
|
||||
reports:
|
||||
junit: XUNIT_RESULT.xml
|
||||
when: always
|
||||
variables:
|
||||
MINIMAL_CMAKE_VERSION: "3.16.3"
|
||||
script:
|
||||
- .\install.ps1 --enable-ci
|
||||
- . .\export.ps1
|
||||
- python ${IDF_PATH}\tools\idf_tools.py install cmake@${MINIMAL_CMAKE_VERSION}
|
||||
- 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]
|
||||
@@ -136,7 +90,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
|
||||
- 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}
|
||||
- pytest -k cmake --junitxml ${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
|
||||
@@ -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-test-specific
|
||||
parallel:
|
||||
matrix:
|
||||
- GEO: [ 'shiny', 'brew' ]
|
||||
|
||||
upload-submodules-cache:
|
||||
extends:
|
||||
- .upload_cache_template
|
||||
|
||||
@@ -31,8 +31,8 @@ additional_build_targets = [
|
||||
]
|
||||
|
||||
# collect
|
||||
collect_app_info_filename = "app_info_${CI_JOB_NAME_SLUG}.txt"
|
||||
junitxml = "build_summary_${CI_JOB_NAME_SLUG}.xml"
|
||||
collect_app_info_filename = "app_info_${CI_JOB_ID}.txt"
|
||||
junitxml = "build_summary_${CI_JOB_ID}.xml"
|
||||
|
||||
# manifest
|
||||
check_manifest_rules = true
|
||||
|
||||
+33
-24
@@ -1,4 +1,6 @@
|
||||
preserve_non_test_related_apps = false
|
||||
filter_non_test_related_apps_by_modified_files = true
|
||||
filter_apps_by_component_target = true
|
||||
exclude_dirs = [
|
||||
'tools/test_mkdfu',
|
||||
'tools/test_idf_size',
|
||||
@@ -20,7 +22,7 @@ IDF_CI_BUILD = "1"
|
||||
|
||||
[gitlab.build_pipeline]
|
||||
workflow_name = "build_child_pipeline"
|
||||
presigned_json_job_name = 'generate_pytest_build_report'
|
||||
runs_per_job = 30
|
||||
|
||||
job_tags = ['build', 'shiny']
|
||||
job_template_name = '.dynamic_build_template'
|
||||
@@ -51,37 +53,44 @@ include:
|
||||
- tools/ci/dynamic_pipelines/templates/.dynamic_jobs.yml
|
||||
"""
|
||||
|
||||
[gitlab.artifacts.s3.debug]
|
||||
[gitlab.artifacts.s3]
|
||||
enable = true
|
||||
|
||||
[gitlab.artifacts.s3.configs.debug]
|
||||
bucket = "idf-artifacts"
|
||||
zip_first = true
|
||||
build_dir_pattern = "**/build*/"
|
||||
patterns = [
|
||||
'**/build*/bootloader/*.map',
|
||||
'**/build*/bootloader/*.elf',
|
||||
'**/build*/*.map',
|
||||
'**/build*/*.elf',
|
||||
'bootloader/*.map',
|
||||
'bootloader/*.elf',
|
||||
'*.map',
|
||||
'*.elf',
|
||||
# customized
|
||||
'**/build*/esp_tee/*.map',
|
||||
'**/build*/esp_tee/*.elf',
|
||||
'**/build*/gdbinit/*',
|
||||
'esp_tee/*.map',
|
||||
'esp_tee/*.elf',
|
||||
'gdbinit/*',
|
||||
]
|
||||
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
|
||||
|
||||
[gitlab.artifacts.s3.flash]
|
||||
[gitlab.artifacts.s3.configs.flash]
|
||||
bucket = "idf-artifacts"
|
||||
zip_first = true
|
||||
build_dir_pattern = "**/build*/"
|
||||
patterns = [
|
||||
'**/build*/bootloader/*.bin',
|
||||
'**/build*/*.bin',
|
||||
'**/build*/partition_table/*.bin',
|
||||
'**/build*/flasher_args.json',
|
||||
'**/build*/flash_project_args',
|
||||
'**/build*/config/sdkconfig.json',
|
||||
'**/build*/sdkconfig',
|
||||
'**/build*/project_description.json',
|
||||
'bootloader/*.bin',
|
||||
'*.bin',
|
||||
'partition_table/*.bin',
|
||||
'flasher_args.json',
|
||||
'flash_project_args',
|
||||
'config/sdkconfig.json',
|
||||
'sdkconfig',
|
||||
'project_description.json',
|
||||
# customized
|
||||
'**/build*/esp_tee/*.bin',
|
||||
'esp_tee/*.bin',
|
||||
]
|
||||
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
|
||||
|
||||
[gitlab.artifacts.s3.app_extra]
|
||||
[gitlab.artifacts.s3.configs.app_extra]
|
||||
bucket = "idf-artifacts"
|
||||
# App-dir files needed at target-test time but outside build_* (uploaded without --build-dir).
|
||||
# Add patterns here as needed, e.g. coverage info produced during build.
|
||||
@@ -90,27 +99,27 @@ patterns = [
|
||||
]
|
||||
if_clause = 'CI_JOB_GROUP_NAME != "build_non_test_related_apps"'
|
||||
|
||||
[gitlab.artifacts.s3.log]
|
||||
[gitlab.artifacts.s3.configs.log]
|
||||
bucket = "idf-artifacts"
|
||||
patterns = [
|
||||
'**/build*/build_log.txt',
|
||||
'**/build*/size*.json',
|
||||
]
|
||||
|
||||
[gitlab.artifacts.s3.junit]
|
||||
[gitlab.artifacts.s3.configs.junit]
|
||||
bucket = "idf-artifacts"
|
||||
patterns = [
|
||||
'**/XUNIT_RESULT_*.xml',
|
||||
'**/build_summary_*.xml',
|
||||
]
|
||||
|
||||
[gitlab.artifacts.s3.env]
|
||||
[gitlab.artifacts.s3.configs.env]
|
||||
bucket = "idf-artifacts"
|
||||
patterns = [
|
||||
'**/pipeline.env',
|
||||
]
|
||||
|
||||
[gitlab.artifacts.s3.longterm]
|
||||
[gitlab.artifacts.s3.configs.longterm]
|
||||
bucket = "longterm"
|
||||
if_clause = 'CI_COMMIT_REF_NAME == "master"'
|
||||
patterns = [
|
||||
|
||||
+2
-10
@@ -146,7 +146,7 @@ repos:
|
||||
require_serial: true
|
||||
additional_dependencies:
|
||||
- PyYAML == 5.3.1
|
||||
- idf-build-apps~=2.13
|
||||
- idf-build-apps~=2.16
|
||||
- id: sort-yaml-files
|
||||
name: sort yaml files
|
||||
entry: tools/ci/sort_yaml.py
|
||||
@@ -167,14 +167,6 @@ repos:
|
||||
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:
|
||||
@@ -229,7 +221,7 @@ repos:
|
||||
- id: check-kconfig-files
|
||||
- id: check-deprecated-kconfig-options
|
||||
- repo: https://github.com/espressif/idf-ci
|
||||
rev: v1.0.0b4
|
||||
rev: v1.2.1
|
||||
hooks:
|
||||
- id: check-tests-missing-config
|
||||
files: 'pytest_.*\.py$|sdkconfig(\..*)?$'
|
||||
|
||||
-2
@@ -11,14 +11,12 @@ def test_bootloader_support(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases()
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||
def test_bootloader_support_qemu_esp32(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases()
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
|
||||
def test_bootloader_support_qemu_esp32c3(dut: Dut) -> None:
|
||||
|
||||
@@ -12,6 +12,11 @@ import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
# Add scripts/ to sys.path so test modules can import the production code
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / 'scripts'
|
||||
@@ -61,11 +66,11 @@ def make_compressor() -> Any:
|
||||
|
||||
def write_yaml_config(
|
||||
tmp_dir: str,
|
||||
tags: list[str],
|
||||
script_path: str | Path,
|
||||
tags: List[str],
|
||||
script_path: Union[str, Path],
|
||||
module_name: str = 'BLE_HOST',
|
||||
log_index_file: str = 'test_log_index.h',
|
||||
tags_with_preserve: list[str] | None = None,
|
||||
tags_with_preserve: Optional[List[str]] = None,
|
||||
) -> Path:
|
||||
"""Write a module_info.yml and return its path."""
|
||||
import yaml
|
||||
@@ -140,9 +145,8 @@ class PipelineContext:
|
||||
shutil.copy2(src, dst)
|
||||
return str(rel_path)
|
||||
|
||||
def run_compression(self, src_list: list[str]) -> dict[str, list[tuple[int, str]]]:
|
||||
"""Run mirror + prepare + compress + header generation. Returns generated macros."""
|
||||
self.compressor.mirror_local_includes()
|
||||
def run_compression(self, src_list: List[str]) -> Dict[str, List[Tuple[int, str]]]:
|
||||
"""Run prepare + compress + header generation. Returns generated macros."""
|
||||
self.compressor.prepare_source_files(src_list)
|
||||
|
||||
files_to_process = []
|
||||
@@ -150,7 +154,7 @@ class PipelineContext:
|
||||
files_to_process.extend([(module, path) for path in info['files_to_process']])
|
||||
files_to_process.sort(key=lambda x: x[1])
|
||||
|
||||
all_macros: dict[str, list[tuple[int, str]]] = {}
|
||||
all_macros: Dict[str, List[Tuple[int, str]]] = {}
|
||||
for file_info in files_to_process:
|
||||
file_macros = self.compressor.compress_file(file_info)
|
||||
for module, log_id, macro in file_macros:
|
||||
@@ -186,8 +190,8 @@ class PipelineContext:
|
||||
|
||||
def assert_header_matches_golden(
|
||||
test_case: unittest.TestCase,
|
||||
generated_path: str | Path,
|
||||
golden_path: str | Path,
|
||||
generated_path: Union[str, Path],
|
||||
golden_path: Union[str, Path],
|
||||
) -> None:
|
||||
"""Compare generated header to golden file, normalizing copyright year."""
|
||||
gen_text = Path(generated_path).read_text()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
import pytest
|
||||
from pytest_embedded import Dut
|
||||
@@ -50,63 +50,59 @@ def do_test_help_quit(dut: Dut) -> None:
|
||||
dut.expect(r'quit\s+Quit REPL environment\s+esp>')
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console(dut: Dut, test_on: str) -> None:
|
||||
def test_console(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases(group='!ignore', timeout=120)
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_repl(dut: Dut, test_on: str) -> None:
|
||||
def test_console_repl(dut: Dut) -> None:
|
||||
do_test_quit(dut)
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_sorted_registration(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_sorted_registration(dut: Dut) -> None:
|
||||
do_test_help_generic(dut, 'sorted')
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_reverse_registration(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_reverse_registration(dut: Dut) -> None:
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
dut.confirm_write('"esp console help command - reverse registration"', expect_str='esp>')
|
||||
|
||||
@@ -131,45 +127,41 @@ def test_console_help_reverse_registration(dut: Dut, test_on: str) -> None:
|
||||
|
||||
@idf_parametrize('config', ['sorted'], indirect=['config'])
|
||||
@idf_parametrize('target', ['linux'], indirect=['target'])
|
||||
@idf_parametrize('test_on,markers', [('host', (pytest.mark.host_test,))])
|
||||
def test_console_sorted_help_sorted_registration(dut: Dut, test_on: str) -> None:
|
||||
def test_console_sorted_help_sorted_registration(dut: Dut) -> None:
|
||||
do_test_help_generic(dut, 'sorted')
|
||||
|
||||
|
||||
@idf_parametrize('config', ['sorted'], indirect=['config'])
|
||||
@idf_parametrize('target', ['linux'], indirect=['target'])
|
||||
@idf_parametrize('test_on,markers', [('host', (pytest.mark.host_test,))])
|
||||
def test_console_sorted_help_reverse_registration(dut: Dut, test_on: str) -> None:
|
||||
def test_console_sorted_help_reverse_registration(dut: Dut) -> None:
|
||||
do_test_help_generic(dut, 'reverse')
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_quit(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_quit(dut: Dut) -> None:
|
||||
do_test_help_quit(dut)
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_verbose_level_0(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_verbose_level_0(dut: Dut) -> None:
|
||||
help_verbose_info = 'Print the summary of all registered commands if no arguments are given,'
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
dut.confirm_write('"esp console help command - set verbose level = 0"', expect_str='esp>')
|
||||
@@ -180,18 +172,17 @@ def test_console_help_verbose_level_0(dut: Dut, test_on: str) -> None:
|
||||
dut.expect_exact('help', not_matching=help_verbose_info)
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_verbose_level_1(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_verbose_level_1(dut: Dut) -> None:
|
||||
help_verbose_info = 'Print the summary of all registered commands if no arguments are given,'
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
dut.confirm_write('"esp console help command - set verbose level = 1"', expect_str='esp>')
|
||||
@@ -201,18 +192,17 @@ def test_console_help_verbose_level_1(dut: Dut, test_on: str) -> None:
|
||||
dut.expect_exact(help_verbose_info)
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_verbose_subcommand(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_verbose_subcommand(dut: Dut) -> None:
|
||||
help_verbose_info = 'Print the summary of all registered commands if no arguments are given,'
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
dut.confirm_write('"esp console help command - --verbose sub command"', expect_str='esp>')
|
||||
@@ -227,18 +217,17 @@ def test_console_help_verbose_subcommand(dut: Dut, test_on: str) -> None:
|
||||
dut.expect_exact(help_verbose_info)
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_deregister(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_deregister(dut: Dut) -> None:
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
dut.confirm_write('"esp console deregister commands"', expect_str='esp>')
|
||||
|
||||
@@ -250,18 +239,17 @@ def test_console_help_deregister(dut: Dut, test_on: str) -> None:
|
||||
dut.expect_exact(cmd_z_description, not_matching=cmd_a_description)
|
||||
|
||||
|
||||
@idf_parametrize('config', ['defaults'], indirect=['config'])
|
||||
@idf_parametrize(
|
||||
'target,test_on,markers',
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'host', (pytest.mark.host_test,)),
|
||||
('esp32', 'target', (pytest.mark.generic,)),
|
||||
('esp32c3', 'target', (pytest.mark.generic,)),
|
||||
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target'],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_help_re_register(dut: Dut, test_on: str) -> None:
|
||||
def test_console_help_re_register(dut: Dut) -> None:
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
dut.confirm_write('"esp console re-register commands"', expect_str='esp>')
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ def test_efuse(dut: Dut) -> None:
|
||||
|
||||
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.host_test
|
||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||
def test_efuse_qemu(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases()
|
||||
|
||||
@@ -19,7 +19,6 @@ def test_esp_event(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases()
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.xfail('config.getvalue("target") == "esp32c3"', reason='Unstable on QEMU, needs investigation')
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -24,7 +24,6 @@ def test_esp_ringbuf(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases()
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.parametrize(
|
||||
'target',
|
||||
|
||||
@@ -15,7 +15,6 @@ def test_esp_rom(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases()
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
|
||||
def test_esp_rom_qemu(dut: Dut) -> None:
|
||||
|
||||
@@ -20,7 +20,6 @@ def test_heap_poisoning(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases()
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.parametrize(
|
||||
'config, embedded_services',
|
||||
|
||||
@@ -46,7 +46,6 @@ def test_pthread_single_core_tls(dut: Dut) -> None:
|
||||
dut.run_all_single_board_cases(group='thread-specific', timeout=300)
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||
def test_pthread_qemu(dut: Dut) -> None:
|
||||
|
||||
+25
-12
@@ -44,6 +44,7 @@ from idf_pytest.plugin import IDF_LOCAL_PLUGIN_KEY
|
||||
from idf_pytest.plugin import IdfLocalPlugin
|
||||
from idf_pytest.plugin import requires_elf_or_map
|
||||
from idf_pytest.utils import format_case_id
|
||||
from pytest_embedded.plugin import _request_param_or_config_option_or_default
|
||||
from pytest_embedded.plugin import multi_dut_fixture
|
||||
from pytest_embedded.utils import to_bytes
|
||||
from pytest_embedded.utils import to_str
|
||||
@@ -127,7 +128,7 @@ class AppDownloader:
|
||||
self.commit_sha = commit_sha
|
||||
self.pipeline_id = pipeline_id
|
||||
|
||||
def download_app(self, app_build_path: str, artifact_type: t.Optional[str] = None) -> None:
|
||||
def download_app(self, app_dir: str, build_dir: str, artifact_type: t.Optional[str] = None) -> None:
|
||||
args = [
|
||||
'idf-ci',
|
||||
'gitlab',
|
||||
@@ -137,15 +138,26 @@ class AppDownloader:
|
||||
]
|
||||
if artifact_type:
|
||||
args.extend(['--type', artifact_type])
|
||||
|
||||
if self.pipeline_id:
|
||||
args.extend(['--pipeline-id', self.pipeline_id])
|
||||
args.append(app_build_path)
|
||||
|
||||
subprocess.run(
|
||||
args,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
args.extend(
|
||||
[
|
||||
app_dir,
|
||||
'--build-dir',
|
||||
build_dir,
|
||||
]
|
||||
)
|
||||
result = subprocess.run(
|
||||
args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=IDF_PATH,
|
||||
)
|
||||
logging.info(result.stdout)
|
||||
if result.stderr:
|
||||
logging.info(result.stderr)
|
||||
|
||||
def download_app_extra(self, app_dir: str) -> None:
|
||||
"""Download app-dir artifacts defined under app_extra in .idf_ci.toml."""
|
||||
@@ -348,12 +360,13 @@ def build_dir(
|
||||
downloader = app_downloader
|
||||
|
||||
if downloader:
|
||||
app_dir = idf_relpath(app_path)
|
||||
build_dir = f'build_{target}_{config}'
|
||||
# somehow hardcoded...
|
||||
app_build_path = os.path.join(idf_relpath(app_path), f'build_{target}_{config}')
|
||||
if requires_elf_or_map(case):
|
||||
downloader.download_app(app_build_path)
|
||||
downloader.download_app(app_dir, build_dir)
|
||||
else:
|
||||
downloader.download_app(app_build_path, 'flash')
|
||||
downloader.download_app(app_dir, build_dir, 'flash')
|
||||
check_dirs = [f'build_{target}_{config}']
|
||||
else:
|
||||
check_dirs = []
|
||||
@@ -537,12 +550,12 @@ def log_minimum_free_heap_size(dut: IdfDut, config: str, idf_path: str) -> t.Cal
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def dev_password(request: FixtureRequest) -> str:
|
||||
return request.config.getoption('dev_passwd') or ''
|
||||
return _request_param_or_config_option_or_default(request, 'dev_password', '') # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def dev_user(request: FixtureRequest) -> str:
|
||||
return request.config.getoption('dev_user') or ''
|
||||
return _request_param_or_config_option_or_default(request, 'dev_user', '') # type: ignore
|
||||
|
||||
|
||||
##################
|
||||
@@ -585,7 +598,7 @@ def pytest_unconfigure(config: Config) -> None:
|
||||
config.pluginmanager.unregister(idf_local_plugin)
|
||||
|
||||
|
||||
dut_artifacts_url = []
|
||||
dut_artifacts_url: t.List[str] = []
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
|
||||
@@ -45,6 +45,8 @@ Inside the ``IDF_TOOLS_PATH`` directory, the tools installation scripts create t
|
||||
- ``python_env`` — not related to the tools; virtual Python environments are installed in the sub-directories. Note that the Python environment directory can be placed elsewhere by setting the ``IDF_PYTHON_ENV_PATH`` environment variable.
|
||||
|
||||
- ``idf_version.txt`` — located within each specific Python environment sub-directory under ``python_env``, this file records the ESP-IDF version corresponding to that environment. The version is stored in a format like ``5.3`` to represent ESP-IDF version ``v5.3``.
|
||||
|
||||
- ``root_managed_components`` — directory managed by ``idf-component-manager`` for components installed globally.
|
||||
- ``espidf.constraints.*.txt`` — one constraint file for each ESP-IDF release containing Python package version requirements.
|
||||
|
||||
GitHub Assets Mirror
|
||||
|
||||
@@ -120,6 +120,52 @@ Next, we have the environment marker:
|
||||
|
||||
Finally, we have the test function. With a ``dut`` fixture. In single-dut test cases, the ``dut`` fixture is an instance of ``IdfDut`` class, for multi-dut test cases, it is a tuple of ``IdfDut`` instances. For more details regarding the ``IdfDut`` class, please refer to `pytest-embedded IdfDut API reference <https://docs.espressif.com/projects/pytest-embedded/en/latest/api.html#pytest_embedded_idf.dut.IdfDut>`__.
|
||||
|
||||
Running Tests on Linux
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To execute a pytest case on the Linux host, set ``target`` to ``linux``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@idf_parametrize('target', ['linux'], indirect=['target'])
|
||||
def test_hello_world_linux(dut) -> None:
|
||||
dut.expect('Hello world!')
|
||||
|
||||
This is the simplest way to run the same test flow on the Linux host instead of on physical hardware.
|
||||
|
||||
For a simple Linux-only test, setting ``target`` to ``linux`` is enough and the ``idf`` embedded services will be selected automatically. The ``pytest.mark.host_test`` marker is not required.
|
||||
|
||||
For a mixed environment matrix, specify ``embedded_services`` manually for each case. See :ref:`Same App With Different Running Environments <pytest-same-app-different-running-environments>` for a more complex example.
|
||||
|
||||
.. only:: TARGET_SUPPORT_QEMU
|
||||
|
||||
Running Tests in QEMU
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To execute a pytest case in QEMU, add the ``@pytest.mark.qemu`` marker to the test function.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
|
||||
def test_hello_world_qemu(dut) -> None:
|
||||
dut.expect('Hello world!')
|
||||
|
||||
This is the simplest way to run the same test flow in QEMU instead of on physical hardware.
|
||||
|
||||
For a simple QEMU-only test, adding ``pytest.mark.qemu`` is enough and the ``idf,qemu`` embedded services will be selected automatically.
|
||||
|
||||
For a mixed environment matrix, specify ``embedded_services`` manually for each case. See the later section in this guide for a more complex example.
|
||||
|
||||
For QEMU installation and setup, refer to page :doc:`/api-guides/tools/qemu`.
|
||||
|
||||
Deprecation of ``pytest.mark.host_test``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``pytest.mark.host_test`` is no longer needed and should not be added to new test cases.
|
||||
|
||||
For Linux target test cases and QEMU test cases, the required behavior is handled dynamically by the test framework. In particular, the embedded services are selected automatically for simple Linux-only and QEMU-only cases.
|
||||
|
||||
Same App With Different sdkconfig Files
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -208,6 +254,49 @@ Now this test function would be replicated to 2 test cases (represented as test
|
||||
* ``esp32.foo.test_foo_bar``
|
||||
* ``esp32s2.bar.test_foo_bar``
|
||||
|
||||
.. _pytest-same-app-different-running-environments:
|
||||
|
||||
Same App With Different Running Environments
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Sometimes the same app should be validated in different running environments, for example on the host with the Linux target, on real hardware, or in QEMU. If a single ``@pytest.mark.qemu`` test is not enough, combine ``target``, ``config``, and ``embedded_services`` in a single ``idf_parametrize`` decorator, and attach the required marker for each case.
|
||||
|
||||
The following example is adapted from :idf_file:`components/console/test_apps/console/pytest_console.py`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@idf_parametrize(
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_repl(dut) -> None:
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
|
||||
This creates four test cases for the same app:
|
||||
|
||||
* Linux host execution with the ``idf`` service
|
||||
* ESP32 hardware execution with the ``esp,idf`` services
|
||||
* ESP32-C3 hardware execution with the ``esp,idf`` services
|
||||
* ESP32 execution in QEMU with the ``idf,qemu`` services
|
||||
|
||||
When running locally, you can select only the environment you want:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pytest --target linux
|
||||
$ pytest -m qemu
|
||||
$ pytest -m qemu --target esp32
|
||||
|
||||
``pytest --target linux`` selects Linux target cases only. ``pytest -m qemu`` selects all QEMU-marked cases. ``pytest -m qemu --target esp32`` further limits the selection to QEMU cases for the ESP32 target.
|
||||
|
||||
Use this pattern when the test logic is the same but the execution environment changes.
|
||||
|
||||
Testing Serial Output (Expecting)
|
||||
---------------------------------
|
||||
|
||||
|
||||
@@ -120,6 +120,52 @@ ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自
|
||||
|
||||
关于测试函数,使用了一个 ``dut`` fixture。在单一 DUT 测试用例中,``dut`` fixture 是 ``IdfDut`` 类的一个实例,对于多个 DUT 测试用例,它是 ``IdfDut`` 实例的一个元组。有关 ``IdfDut`` 类的更多详细信息,请参阅 `pytest-embedded IdfDut API 参考 <https://docs.espressif.com/projects/pytest-embedded/en/latest/api.html#pytest_embedded_idf.dut.IdfDut>`__。
|
||||
|
||||
在 Linux 上运行测试
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
要在 Linux 主机上执行 pytest 测试用例,请将 ``target`` 设置为 ``linux``。
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@idf_parametrize('target', ['linux'], indirect=['target'])
|
||||
def test_hello_world_linux(dut) -> None:
|
||||
dut.expect('Hello world!')
|
||||
|
||||
这是在 Linux 主机上运行与物理硬件相同测试流程的最简单方式。
|
||||
|
||||
对于简单的纯 Linux 测试,只需将 ``target`` 设置为 ``linux``,系统会自动选择 ``idf`` 对应的 embedded services。``pytest.mark.host_test`` marker 不再需要。
|
||||
|
||||
对于混合运行环境矩阵,则需要为每个用例手动指定 ``embedded_services``。更复杂的示例请参阅 :ref:`在不同运行环境中运行相同的应用程序 <pytest-same-app-different-running-environments>` 小节。
|
||||
|
||||
.. only:: TARGET_SUPPORT_QEMU
|
||||
|
||||
在 QEMU 中运行测试
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
要在 QEMU 中执行 pytest 测试用例,请将 ``@pytest.mark.qemu`` 添加到测试函数上。
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
|
||||
def test_hello_world_qemu(dut) -> None:
|
||||
dut.expect('Hello world!')
|
||||
|
||||
这是在 QEMU 中运行与物理硬件相同测试流程的最简单方式。
|
||||
|
||||
对于简单的纯 QEMU 测试,只需添加 ``pytest.mark.qemu``,系统会自动选择 ``idf,qemu`` 对应的 embedded services。
|
||||
|
||||
对于混合运行环境矩阵,则需要为每个用例手动指定 ``embedded_services``。更复杂的示例请参阅本指南后面的对应小节。
|
||||
|
||||
有关 QEMU 的安装和配置,请参阅页面 :doc:`../api-guides/tools/qemu`。
|
||||
|
||||
``pytest.mark.host_test`` 的弃用说明
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``pytest.mark.host_test`` 已不再需要,也不应再添加到新的测试用例中。
|
||||
|
||||
对于 Linux target 测试用例和 QEMU 测试用例,相关行为会由测试框架动态处理。尤其是在简单的纯 Linux 或纯 QEMU 场景下,embedded services 会被自动选择。
|
||||
|
||||
使用不同的 sdkconfig 文件运行相同的应用程序
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -208,6 +254,49 @@ ESP-IDF 在主机端使用 pytest 框架(以及一些 pytest 插件)来自
|
||||
* ``esp32.foo.test_foo_bar``
|
||||
* ``esp32s2.bar.test_foo_bar``
|
||||
|
||||
.. _pytest-same-app-different-running-environments:
|
||||
|
||||
在不同运行环境中运行相同的应用程序
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
有时,同一个应用程序需要在不同的运行环境中进行验证,例如在 Linux target 的主机上、真实硬件上,或在 QEMU 中运行。如果单独使用 ``@pytest.mark.qemu`` 测试还不够,可以在一个 ``idf_parametrize`` 装饰器中组合 ``target``、``config`` 和 ``embedded_services``,并为每种情况附加所需的 marker。
|
||||
|
||||
下面的示例改编自 :idf_file:`components/console/test_apps/console/pytest_console.py`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@idf_parametrize(
|
||||
'target,config,embedded_services,markers',
|
||||
[
|
||||
('linux', 'defaults', 'idf', ()),
|
||||
('esp32', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32c3', 'defaults', 'esp,idf', (pytest.mark.generic,)),
|
||||
('esp32', 'defaults', 'idf,qemu', (pytest.mark.qemu,)),
|
||||
],
|
||||
indirect=['target', 'config', 'embedded_services'],
|
||||
)
|
||||
def test_console_repl(dut) -> None:
|
||||
dut.expect_exact('Press ENTER to see the list of tests')
|
||||
|
||||
这会为同一个应用程序生成 4 个测试用例:
|
||||
|
||||
* 在 Linux 主机上使用 ``idf`` service 运行
|
||||
* 在 ESP32 硬件上使用 ``esp,idf`` services 运行
|
||||
* 在 ESP32-C3 硬件上使用 ``esp,idf`` services 运行
|
||||
* 在 QEMU 中以 ESP32 为目标,使用 ``idf,qemu`` services 运行
|
||||
|
||||
在本地运行时,可以按需只选择某一种运行环境:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pytest --target linux
|
||||
$ pytest -m qemu
|
||||
$ pytest -m qemu --target esp32
|
||||
|
||||
``pytest --target linux`` 只选择 Linux target 的测试用例。``pytest -m qemu`` 选择所有带有 QEMU marker 的测试用例。``pytest -m qemu --target esp32`` 会进一步把范围限制为目标芯片为 ESP32 的 QEMU 测试用例。
|
||||
|
||||
当测试逻辑相同,但执行环境不同的时候,可使用此模式。
|
||||
|
||||
测试串行输出
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from pytest_embedded_idf.utils import idf_parametrize
|
||||
from pytest_embedded_qemu.dut import QemuDut
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
|
||||
def test_pytest_host(dut: QemuDut) -> None:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Callable
|
||||
from collections.abc import Callable
|
||||
|
||||
import pytest
|
||||
from pytest_embedded_idf.dut import IdfDut
|
||||
@@ -45,7 +45,6 @@ def verify_elf_sha256_embedding(app: QemuApp, sha256_reported: str) -> None:
|
||||
raise ValueError('ELF file SHA256 mismatch')
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
|
||||
def test_hello_world_host(app: QemuApp, dut: QemuDut) -> None:
|
||||
|
||||
@@ -29,6 +29,8 @@ from pytest_embedded_idf.utils import idf_parametrize
|
||||
# Testbed configuration
|
||||
|
||||
ETHVM_ENDNODE_USER = 'ci.ethvm'
|
||||
ETHERNET_TEST_USER = os.getenv('ETHERNET_TEST_USER')
|
||||
ETHERNET_TEST_PASSWORD = os.getenv('ETHERNET_TEST_PASSWORD')
|
||||
|
||||
BR_PORTS_NUM = 2
|
||||
IPERF_BW_LIM = 6
|
||||
@@ -636,5 +638,10 @@ def setup_test_environment() -> Generator[None, None, None]:
|
||||
indirect=True,
|
||||
)
|
||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||
@pytest.mark.parametrize(
|
||||
'dev_user, dev_password',
|
||||
[(ETHERNET_TEST_USER, ETHERNET_TEST_PASSWORD)],
|
||||
indirect=True,
|
||||
)
|
||||
def test_esp_eth_bridge(dut: Dut, dev_user: str, dev_password: str) -> None:
|
||||
eth_bridge_test(dut, dev_user, dev_password)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
import logging
|
||||
import os
|
||||
@@ -52,7 +52,6 @@ def test_examples_protocol_https_x509_bundle_dynamic_buffer(dut: Dut) -> None:
|
||||
|
||||
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.parametrize(
|
||||
'config',
|
||||
[
|
||||
|
||||
@@ -900,7 +900,6 @@ def test_examples_efuse_with_virt_sb_v2_and_fe(dut: Dut) -> None:
|
||||
dut.expect('example: Done')
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.parametrize(
|
||||
'config',
|
||||
|
||||
@@ -1110,7 +1110,6 @@ def test_examples_protocol_advanced_https_ota_example_openssl_aligned_bin(dut: D
|
||||
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.nightly_run
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.parametrize(
|
||||
'qemu_extra_args',
|
||||
[
|
||||
@@ -1166,7 +1165,6 @@ def test_examples_protocol_advanced_https_ota_example_verify_min_chip_revision(d
|
||||
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.nightly_run
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.parametrize(
|
||||
'qemu_extra_args',
|
||||
[
|
||||
|
||||
@@ -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
|
||||
@@ -9,7 +9,6 @@ import typing as t
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from idf_build_apps import App
|
||||
from idf_build_apps import find_apps
|
||||
from idf_build_apps.constants import SUPPORTED_TARGETS
|
||||
@@ -40,6 +39,14 @@ USUAL_TO_FORMAL = {
|
||||
|
||||
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: t.List[str] = [
|
||||
'esp32h21',
|
||||
'esp32h4',
|
||||
'esp32c5',
|
||||
]
|
||||
|
||||
|
||||
def print_diff_table(
|
||||
list1: t.List[str], list2: t.List[str], title1: str, title2: str, excluded: t.Optional[t.List[str]] = None
|
||||
@@ -299,21 +306,9 @@ if __name__ == '__main__':
|
||||
|
||||
readme_parser = action.add_parser('check-readmes')
|
||||
readme_parser.add_argument('paths', nargs='+', help='check under paths')
|
||||
readme_parser.add_argument(
|
||||
'-c',
|
||||
'--config',
|
||||
default=os.path.join(IDF_PATH, '.gitlab', 'ci', 'default-build-test-rules.yml'),
|
||||
help='config file',
|
||||
)
|
||||
|
||||
test_parser = action.add_parser('check-test-scripts')
|
||||
test_parser.add_argument('paths', nargs='+', help='check under paths')
|
||||
test_parser.add_argument(
|
||||
'-c',
|
||||
'--config',
|
||||
default=os.path.join(IDF_PATH, '.gitlab', 'ci', 'default-build-test-rules.yml'),
|
||||
help='config file',
|
||||
)
|
||||
arg = parser.parse_args()
|
||||
|
||||
check_dirs = set()
|
||||
@@ -344,14 +339,6 @@ if __name__ == '__main__':
|
||||
os.path.join(IDF_PATH, 'tools', 'cmakev2', 'test'),
|
||||
]
|
||||
|
||||
_bypass_targets: t.List[str] = []
|
||||
if arg.config:
|
||||
with open(arg.config) as fr:
|
||||
configs = yaml.safe_load(fr)
|
||||
|
||||
if configs:
|
||||
_bypass_targets = configs.get('bypass_check_test_targets') or []
|
||||
|
||||
os.environ.update(
|
||||
{
|
||||
'IDF_PATH': IDF_PATH,
|
||||
@@ -369,5 +356,5 @@ if __name__ == '__main__':
|
||||
check_test_scripts(
|
||||
list(check_dirs),
|
||||
_exclude_dirs,
|
||||
bypass_targets=_bypass_targets,
|
||||
bypass_targets=BYPASS_CHECK_TEST_TARGETS,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# Checks all public headers in IDF in the ci
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
import argparse
|
||||
@@ -309,6 +309,10 @@ class PublicHeaderChecker:
|
||||
idf_path = os.getenv('IDF_PATH')
|
||||
if idf_path is None:
|
||||
raise RuntimeError("Environment variable 'IDF_PATH' wasn't set.")
|
||||
|
||||
idf_tools_path = os.getenv('IDF_TOOLS_PATH') or os.path.expanduser(os.path.join('~', '.espressif'))
|
||||
idf_root_dep_path = os.path.join(idf_tools_path, 'root_managed_components')
|
||||
|
||||
project_dir = os.path.join(idf_path, 'examples', 'get-started', 'blink')
|
||||
sdkconfig = os.path.join(self.build_dir, 'sdkconfig')
|
||||
if self.libc_type == 'picolibc':
|
||||
@@ -371,7 +375,11 @@ class PublicHeaderChecker:
|
||||
if os.path.relpath(d, idf_path).startswith(tuple(ignore_dirs)):
|
||||
self.log('{} - directory ignored'.format(d))
|
||||
continue
|
||||
for root, dirnames, filenames in os.walk(d):
|
||||
for root, _, filenames in os.walk(d):
|
||||
if root.startswith(idf_root_dep_path):
|
||||
self.log(f'{root} - directory ignored (inside IDF_TOOLS_PATH/root_managed_components)')
|
||||
continue
|
||||
|
||||
for filename in fnmatch.filter(filenames, '*.h'):
|
||||
all_include_files.append(os.path.join(root, filename))
|
||||
self.main_c = main_c
|
||||
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -1,26 +1,9 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 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
|
||||
|
||||
COMMENT_START_MARKER = '### Dynamic Pipeline Report'
|
||||
|
||||
REPORT_TEMPLATE_FILEPATH = os.path.join(
|
||||
IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'report.template.html'
|
||||
)
|
||||
CSS_STYLES_FILEPATH = os.path.join(IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'styles.css')
|
||||
JS_SCRIPTS_FILEPATH = os.path.join(IDF_PATH, 'tools', 'ci', 'dynamic_pipelines', 'templates', 'scripts.js')
|
||||
TOP_N_APPS_BY_SIZE_DIFF = 10
|
||||
SIZE_DIFFERENCE_BYTES_THRESHOLD = 500
|
||||
BINARY_SIZE_METRIC_NAME = 'binary_size'
|
||||
|
||||
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', '')
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import os
|
||||
import typing as t
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
from idf_ci_utils import IDF_PATH
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestCase:
|
||||
name: str
|
||||
file: str
|
||||
time: float
|
||||
app_path: t.Optional[str] = None
|
||||
failure: t.Optional[str] = None
|
||||
skipped: t.Optional[str] = None
|
||||
ci_job_url: t.Optional[str] = None
|
||||
ci_dashboard_url: t.Optional[str] = None
|
||||
dut_log_url: t.Optional[str] = None
|
||||
latest_total_count: int = 0
|
||||
latest_failed_count: int = 0
|
||||
|
||||
@property
|
||||
def is_failure(self) -> bool:
|
||||
return self.failure is not None
|
||||
|
||||
@property
|
||||
def is_skipped(self) -> bool:
|
||||
return self.skipped is not None
|
||||
|
||||
@property
|
||||
def is_success(self) -> bool:
|
||||
return not self.is_failure and not self.is_skipped
|
||||
|
||||
@classmethod
|
||||
def _get_idf_rel_path(cls, path: str) -> str:
|
||||
if path.startswith(IDF_PATH):
|
||||
return os.path.relpath(path, IDF_PATH)
|
||||
else:
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def from_test_case_node(cls, node: Element) -> t.Optional['TestCase']:
|
||||
if 'name' not in node.attrib:
|
||||
print('WARNING: Node Invalid: ', node)
|
||||
return None
|
||||
|
||||
# url to test cases dashboard
|
||||
grafana_base_url = urllib.parse.urljoin(os.getenv('CI_DASHBOARD_HOST', ''), '/d/Ucg477Fnz/case-list')
|
||||
encoded_params = urllib.parse.urlencode({'var-case_id': node.attrib['name']}, quote_via=urllib.parse.quote)
|
||||
|
||||
kwargs = {
|
||||
'name': node.attrib['name'],
|
||||
'file': node.attrib.get('file'),
|
||||
'app_path': '|'.join(
|
||||
cls._get_idf_rel_path(path) for path in node.attrib.get('app_path', 'unknown').split('|')
|
||||
),
|
||||
'time': float(node.attrib.get('time') or 0),
|
||||
'ci_job_url': node.attrib.get('ci_job_url') or 'Not found',
|
||||
'ci_dashboard_url': f'{grafana_base_url}?{encoded_params}',
|
||||
'dut_log_url': node.attrib.get('dut_log_url') or 'Not found',
|
||||
}
|
||||
|
||||
failure_node = node.find('failure')
|
||||
# bool(failure_node) is False, so compare with None
|
||||
if failure_node is None:
|
||||
failure_node = node.find('error')
|
||||
if failure_node is not None:
|
||||
message = failure_node.attrib.get('message', '')
|
||||
kwargs['failure'] = message
|
||||
|
||||
skipped_node = node.find('skipped')
|
||||
if skipped_node is not None:
|
||||
kwargs['skipped'] = skipped_node.attrib['message']
|
||||
|
||||
return cls(**kwargs) # type: ignore
|
||||
|
||||
|
||||
@dataclass
|
||||
class GitlabJob:
|
||||
id: int
|
||||
name: str
|
||||
stage: str
|
||||
status: str
|
||||
url: str
|
||||
ci_dashboard_url: str
|
||||
failure_reason: t.Optional[str] = None
|
||||
failure_log: t.Optional[str] = None
|
||||
latest_total_count: int = 0
|
||||
latest_failed_count: int = 0
|
||||
|
||||
@property
|
||||
def is_failed(self) -> bool:
|
||||
return self.status == 'failed'
|
||||
|
||||
@property
|
||||
def is_success(self) -> bool:
|
||||
return self.status == 'success'
|
||||
|
||||
@classmethod
|
||||
def from_json_data(cls, job_data: dict, failure_data: dict) -> t.Optional['GitlabJob']:
|
||||
grafana_base_url = urllib.parse.urljoin(os.getenv('CI_DASHBOARD_HOST', ''), '/d/LoUa-qLWz/job-list')
|
||||
encoded_params = urllib.parse.urlencode({'var-job_name': job_data['name']}, quote_via=urllib.parse.quote)
|
||||
|
||||
kwargs = {
|
||||
'id': job_data['id'],
|
||||
'name': job_data['name'],
|
||||
'stage': job_data['stage'],
|
||||
'status': job_data['status'],
|
||||
'url': job_data['url'],
|
||||
'ci_dashboard_url': f'{grafana_base_url}?{encoded_params}',
|
||||
'failure_reason': job_data['failure_reason'],
|
||||
'failure_log': job_data['failure_log'],
|
||||
'latest_total_count': failure_data.get('total_count', 0),
|
||||
'latest_failed_count': failure_data.get('failed_count', 0),
|
||||
}
|
||||
|
||||
return cls(**kwargs) # type: ignore
|
||||
@@ -1,739 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import abc
|
||||
import html
|
||||
import re
|
||||
import typing as t
|
||||
from textwrap import dedent
|
||||
|
||||
from gitlab import GitlabUpdateError
|
||||
from gitlab_api import Gitlab
|
||||
from idf_build_apps.constants import BuildStatus
|
||||
from idf_ci_local.app import AppWithMetricsInfo
|
||||
from idf_ci_utils import idf_relpath
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from .constants import BINARY_SIZE_METRIC_NAME
|
||||
from .constants import CI_DASHBOARD_API
|
||||
from .constants import COMMENT_START_MARKER
|
||||
from .constants import CSS_STYLES_FILEPATH
|
||||
from .constants import JS_SCRIPTS_FILEPATH
|
||||
from .constants import REPORT_TEMPLATE_FILEPATH
|
||||
from .constants import SIZE_DIFFERENCE_BYTES_THRESHOLD
|
||||
from .constants import TOP_N_APPS_BY_SIZE_DIFF
|
||||
from .models import GitlabJob
|
||||
from .models import TestCase
|
||||
from .utils import format_permalink
|
||||
from .utils import get_artifacts_url
|
||||
from .utils import is_url
|
||||
|
||||
|
||||
class ReportGenerator:
|
||||
REGEX_PATTERN = r'#### {}\n[\s\S]*?(?=\n#### |$)'
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
project_id: int,
|
||||
mr_iid: int,
|
||||
pipeline_id: int,
|
||||
job_id: int,
|
||||
commit_id: str,
|
||||
local_commit_id: str,
|
||||
*,
|
||||
title: str,
|
||||
):
|
||||
gl_project = Gitlab(project_id).project
|
||||
if mr_iid is not None:
|
||||
self.mr = gl_project.mergerequests.get(mr_iid)
|
||||
else:
|
||||
self.mr = None
|
||||
self.pipeline_id = pipeline_id
|
||||
self.job_id = job_id
|
||||
self.commit_id = commit_id
|
||||
self.local_commit_id = local_commit_id
|
||||
|
||||
self.title = title
|
||||
self.output_filepath = self.title.lower().replace(' ', '_') + '.html'
|
||||
self.additional_info = ''
|
||||
|
||||
@property
|
||||
def get_commit_summary(self) -> str:
|
||||
return f'with CI commit SHA: {self.commit_id[:8]}, local commit SHA: {self.local_commit_id[:8]}'
|
||||
|
||||
@staticmethod
|
||||
def get_download_link_for_url(url: str) -> str:
|
||||
if url:
|
||||
return f'<a href="{url}">Download</a>'
|
||||
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def write_report_to_file(report_str: str, job_id: int, output_filepath: str) -> t.Optional[str]:
|
||||
"""
|
||||
Writes the report to a file and constructs a modified URL based on environment settings.
|
||||
|
||||
:param report_str: The report content to be written to the file.
|
||||
:param job_id: The job identifier used to construct the URL.
|
||||
:param output_filepath: The path to the output file.
|
||||
:return: The modified URL pointing to the job's artifacts.
|
||||
"""
|
||||
if not report_str:
|
||||
return None
|
||||
with open(output_filepath, 'w') as file:
|
||||
file.write(report_str)
|
||||
|
||||
# for example, {URL}/-/esp-idf/-/jobs/{id}/artifacts/app_info_84.txt
|
||||
# CI_PAGES_URL is {URL}/esp-idf, which missed one `-`
|
||||
report_url: str = get_artifacts_url(job_id, output_filepath)
|
||||
return report_url
|
||||
|
||||
@staticmethod
|
||||
def _load_file_content(filepath: str) -> str:
|
||||
"""
|
||||
Load the content of a file as string
|
||||
|
||||
:param filepath: Path to the file to load
|
||||
:return: Content of the file as string
|
||||
"""
|
||||
try:
|
||||
with open(filepath, encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except (OSError, FileNotFoundError) as e:
|
||||
print(f'Warning: Could not read file {filepath}: {e}')
|
||||
return ''
|
||||
|
||||
def generate_html_report(self, table_str: str) -> str:
|
||||
# we're using bootstrap table
|
||||
table_str = table_str.replace(
|
||||
'<table>',
|
||||
'<table data-toggle="table" data-search-align="left" data-search="true" data-sticky-header="true">',
|
||||
)
|
||||
|
||||
template = self._load_file_content(REPORT_TEMPLATE_FILEPATH)
|
||||
css_content = self._load_file_content(CSS_STYLES_FILEPATH)
|
||||
js_content = self._load_file_content(JS_SCRIPTS_FILEPATH)
|
||||
|
||||
template = template.replace('{{css_content}}', css_content)
|
||||
template = template.replace('{{js_content}}', js_content)
|
||||
template = template.replace('{{pipeline_id}}', str(self.pipeline_id))
|
||||
template = template.replace('{{apiBaseUrl}}', CI_DASHBOARD_API)
|
||||
|
||||
return template.replace('{{title}}', self.title).replace('{{table}}', table_str)
|
||||
|
||||
@staticmethod
|
||||
def table_to_html_str(table: PrettyTable) -> str:
|
||||
return html.unescape(table.get_html_string()) # type: ignore
|
||||
|
||||
def create_table_section(
|
||||
self,
|
||||
title: str,
|
||||
items: list,
|
||||
headers: list,
|
||||
row_attrs: list,
|
||||
value_functions: t.Optional[list] = None,
|
||||
) -> t.List:
|
||||
"""
|
||||
Appends a formatted section to a report based on the provided items. This section includes
|
||||
a header and a table constructed from the items list with specified headers and attributes.
|
||||
|
||||
:param title: Title for the report section. This title is used as a header above the table.
|
||||
:param items: List of item objects to include in the table. Each item should have attributes
|
||||
that correspond to the row_attrs and value_functions specified.
|
||||
:param headers: List of strings that will serve as the column headers in the generated table.
|
||||
:param row_attrs: List of attributes to include from each item for the table rows. These
|
||||
should be attributes or keys that exist on the items in the 'items' list.
|
||||
:param value_functions: Optional list of tuples containing additional header and corresponding
|
||||
value function. Each tuple should specify a header (as a string) and
|
||||
a function that takes an item and returns a string. This is used for
|
||||
generating dynamic columns based on item data.
|
||||
|
||||
:return: List with appended HTML sections.
|
||||
"""
|
||||
if not items:
|
||||
return []
|
||||
|
||||
report_sections = [
|
||||
f"""<h2 id="{format_permalink(title)}">{title}<i class="fas fa-link copy-link-icon"
|
||||
onclick="copyPermalink('#{format_permalink(title)}')"></i></h2>""",
|
||||
self._create_table_for_items(
|
||||
items=items, headers=headers, row_attrs=row_attrs, value_functions=value_functions or []
|
||||
),
|
||||
]
|
||||
return report_sections
|
||||
|
||||
@staticmethod
|
||||
def generate_additional_info_section(
|
||||
title: str, count: int, report_url: t.Optional[str] = None, add_permalink: bool = True
|
||||
) -> str:
|
||||
"""
|
||||
Generate a section for the additional info string.
|
||||
|
||||
:param title: The title of the section.
|
||||
:param count: The count of test cases.
|
||||
:param report_url: The URL of the report. If count = 0, only the count will be included.
|
||||
:param add_permalink: Whether to include a permalink in the report URL. Defaults to True.
|
||||
:return: The formatted additional info section string.
|
||||
"""
|
||||
if count != 0 and report_url:
|
||||
if add_permalink:
|
||||
return f'- **{title}:** [{count}]({report_url}/#{format_permalink(title)})\n'
|
||||
else:
|
||||
return f'- **{title}:** [{count}]({report_url})\n'
|
||||
else:
|
||||
return f'- **{title}:** {count}\n'
|
||||
|
||||
def _create_table_for_items(
|
||||
self,
|
||||
items: t.Union[t.List[TestCase], t.List[GitlabJob]],
|
||||
headers: t.List[str],
|
||||
row_attrs: t.List[str],
|
||||
value_functions: t.Optional[t.List[t.Tuple[str, t.Callable[[t.Union[TestCase, GitlabJob]], str]]]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Create a PrettyTable and convert it to an HTML string for the provided test cases.
|
||||
:param items: List of item objects to include in the table.
|
||||
:param headers: List of strings for the table headers.
|
||||
:param row_attrs: List of attributes to include in each row.
|
||||
:param value_functions: List of tuples containing additional header and corresponding value function.
|
||||
:return: HTML table string.
|
||||
"""
|
||||
table = PrettyTable()
|
||||
table.field_names = headers
|
||||
|
||||
# Create a mapping of header names to their corresponding index in the headers list
|
||||
header_index_map = {header: i for i, header in enumerate(headers)}
|
||||
|
||||
for item in items:
|
||||
row = []
|
||||
for attr in row_attrs:
|
||||
value = str(getattr(item, attr, ''))
|
||||
if is_url(value):
|
||||
link = f'<a href="{value}">link</a>'
|
||||
row.append(link)
|
||||
else:
|
||||
row.append(value)
|
||||
|
||||
# Insert values computed by value functions at the correct column position based on their headers
|
||||
if value_functions:
|
||||
for header, func in value_functions:
|
||||
index = header_index_map.get(header)
|
||||
if index is not None:
|
||||
computed_value = func(item)
|
||||
row.insert(index, computed_value)
|
||||
|
||||
table.add_row(row)
|
||||
|
||||
return self.table_to_html_str(table)
|
||||
|
||||
@staticmethod
|
||||
def _filter_items(
|
||||
items: t.Union[t.List[TestCase], t.List[GitlabJob]], condition: t.Callable[[t.Union[TestCase, GitlabJob]], bool]
|
||||
) -> t.List[TestCase]:
|
||||
"""
|
||||
Filter items s based on a given condition.
|
||||
|
||||
:param items: List of items to filter by given condition.
|
||||
:param condition: A function that evaluates to True or False for each items.
|
||||
:return: List of filtered instances.
|
||||
"""
|
||||
return [item for item in items if condition(item)]
|
||||
|
||||
@staticmethod
|
||||
def _sort_items(
|
||||
items: t.List[t.Union[TestCase, GitlabJob, AppWithMetricsInfo]],
|
||||
key: t.Union[str, t.Callable[[t.Union[TestCase, GitlabJob, AppWithMetricsInfo]], t.Any]],
|
||||
order: str = 'asc',
|
||||
sort_function: t.Optional[t.Callable[[t.Any], t.Any]] = None,
|
||||
) -> t.List[t.Union[TestCase, GitlabJob, AppWithMetricsInfo]]:
|
||||
"""
|
||||
Sort items based on a given key, order, and optional custom sorting function.
|
||||
|
||||
:param items: List of items to sort.
|
||||
:param key: A string representing the attribute name or a function to extract the sorting key.
|
||||
:param order: Order of sorting ('asc' for ascending, 'desc' for descending).
|
||||
:param sort_function: A custom function to control sorting logic
|
||||
(e.g., prioritizing positive/negative/zero values).
|
||||
:return: List of sorted instances.
|
||||
"""
|
||||
key_func = None
|
||||
if isinstance(key, str):
|
||||
|
||||
def key_func(item: t.Any) -> t.Any:
|
||||
return getattr(item, key)
|
||||
|
||||
sorting_key = sort_function if sort_function is not None else key_func
|
||||
try:
|
||||
items = sorted(items, key=sorting_key, reverse=(order == 'desc'))
|
||||
except TypeError:
|
||||
print(f'Comparison for the key {key} is not supported')
|
||||
|
||||
return items
|
||||
|
||||
@abc.abstractmethod
|
||||
def _get_report_str(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def _generate_comment(self) -> str:
|
||||
# Report in HTML format to avoid exceeding length limits
|
||||
comment = f'#### {self.title}\n'
|
||||
report_str = self._get_report_str()
|
||||
comment += f'{self.additional_info}\n'
|
||||
self.write_report_to_file(report_str, self.job_id, self.output_filepath)
|
||||
|
||||
return comment
|
||||
|
||||
def _update_mr_comment(self, comment: str) -> None:
|
||||
new_comment = f'{COMMENT_START_MARKER}\n\n{comment}'
|
||||
|
||||
for note in self.mr.notes.list(iterator=True):
|
||||
if note.body.startswith(COMMENT_START_MARKER):
|
||||
updated_str = self._get_updated_comment(note.body, comment)
|
||||
|
||||
note.body = updated_str
|
||||
try:
|
||||
note.save()
|
||||
except GitlabUpdateError:
|
||||
print('Failed to update MR comment, Creating a new comment')
|
||||
self.mr.notes.create({'body': new_comment})
|
||||
break
|
||||
else:
|
||||
self.mr.notes.create({'body': new_comment})
|
||||
|
||||
def _get_updated_comment(self, existing_comment: str, new_comment: str) -> str:
|
||||
updated_str = re.sub(self.REGEX_PATTERN.format(self.title), new_comment, existing_comment)
|
||||
if updated_str == existing_comment:
|
||||
updated_str = f'{existing_comment.strip()}\n\n{new_comment}'
|
||||
return updated_str
|
||||
|
||||
def post_report(self) -> None:
|
||||
comment = self._generate_comment()
|
||||
|
||||
print(comment)
|
||||
|
||||
if self.mr is None:
|
||||
print('No MR found, skip posting comment')
|
||||
return
|
||||
|
||||
self._update_mr_comment(comment)
|
||||
|
||||
|
||||
class BuildReportGenerator(ReportGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
project_id: int,
|
||||
mr_iid: int,
|
||||
pipeline_id: int,
|
||||
job_id: int,
|
||||
commit_id: str,
|
||||
local_commit_id: str,
|
||||
*,
|
||||
title: str = 'Build Report',
|
||||
apps: list[AppWithMetricsInfo],
|
||||
) -> None:
|
||||
super().__init__(project_id, mr_iid, pipeline_id, job_id, commit_id, local_commit_id, title=title)
|
||||
self.apps = apps
|
||||
self.report_titles_map = {
|
||||
'failed_apps': 'Failed Apps',
|
||||
'built_test_related_apps': 'Built Apps - Test Related',
|
||||
'built_non_test_related_apps': 'Built Apps - Non Test Related',
|
||||
'new_test_related_apps': 'New Apps - Test Related',
|
||||
'new_non_test_related_apps': 'New Apps - Non Test Related',
|
||||
'skipped_apps': 'Skipped Apps',
|
||||
}
|
||||
self.failed_apps_report_file = 'failed_apps.html'
|
||||
self.built_apps_report_file = 'built_apps.html'
|
||||
self.skipped_apps_report_file = 'skipped_apps.html'
|
||||
|
||||
@staticmethod
|
||||
def custom_sort(item: AppWithMetricsInfo) -> tuple[int, t.Any]:
|
||||
"""
|
||||
Custom sort function to:
|
||||
1. Push items with zero binary sizes to the end.
|
||||
2. Sort other items by absolute size_difference_percentage.
|
||||
"""
|
||||
# Priority: 0 for zero binaries, 1 for non-zero binaries
|
||||
zero_binary_priority = (
|
||||
1
|
||||
if item.metrics[BINARY_SIZE_METRIC_NAME].source_value != 0
|
||||
or item.metrics[BINARY_SIZE_METRIC_NAME].target_value != 0
|
||||
else 0
|
||||
)
|
||||
# Secondary sort: Negative absolute size_difference_percentage for descending order
|
||||
size_difference_sort = abs(item.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage)
|
||||
return zero_binary_priority, size_difference_sort
|
||||
|
||||
def _generate_top_n_apps_by_size_table(self) -> str:
|
||||
"""
|
||||
Generate a markdown table for the top N apps by size difference.
|
||||
Only includes apps with size differences greater than 500 bytes.
|
||||
"""
|
||||
filtered_apps = [
|
||||
app
|
||||
for app in self.apps
|
||||
if abs(app.metrics[BINARY_SIZE_METRIC_NAME].difference) > SIZE_DIFFERENCE_BYTES_THRESHOLD
|
||||
]
|
||||
|
||||
top_apps = sorted(
|
||||
filtered_apps, key=lambda app: abs(app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage), reverse=True
|
||||
)[:TOP_N_APPS_BY_SIZE_DIFF]
|
||||
|
||||
if not top_apps:
|
||||
return ''
|
||||
|
||||
table = (
|
||||
f'\n⚠️⚠️⚠️ Top {len(top_apps)} Apps with Binary Size Sorted by Size Difference\n'
|
||||
f'Note: Apps with changes of less than {SIZE_DIFFERENCE_BYTES_THRESHOLD} bytes are not shown.\n'
|
||||
)
|
||||
table += '| App Dir | Build Dir | Size Diff (bytes) | Size Diff (%) |\n'
|
||||
table += '|---------|-----------|-------------------|---------------|\n'
|
||||
for app in top_apps:
|
||||
table += dedent(
|
||||
f'| {app.app_dir} | {app.build_dir} | '
|
||||
f'{app.metrics[BINARY_SIZE_METRIC_NAME].difference} | '
|
||||
f'{app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage}% |\n'
|
||||
)
|
||||
table += (
|
||||
'\n**For more details, please click on the numbers in the summary above '
|
||||
'to view the corresponding report files.** ⬆️⬆️⬆️\n\n'
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
@staticmethod
|
||||
def split_new_and_existing_apps(
|
||||
apps: t.Iterable[AppWithMetricsInfo],
|
||||
) -> tuple[list[AppWithMetricsInfo], list[AppWithMetricsInfo]]:
|
||||
"""
|
||||
Splits apps into new apps and existing apps.
|
||||
|
||||
:param apps: Iterable of apps to process.
|
||||
:return: A tuple (new_apps, existing_apps).
|
||||
"""
|
||||
new_apps = [app for app in apps if app.is_new_app]
|
||||
existing_apps = [app for app in apps if not app.is_new_app]
|
||||
return new_apps, existing_apps
|
||||
|
||||
def filter_apps_by_criteria(self, build_status: str, preserve: bool) -> list[AppWithMetricsInfo]:
|
||||
"""
|
||||
Filters apps based on build status and preserve criteria.
|
||||
|
||||
:param build_status: Build status to filter by.
|
||||
:param preserve: Whether to filter preserved apps.
|
||||
:return: Filtered list of apps.
|
||||
"""
|
||||
return [app for app in self.apps if app.build_status == build_status and app.preserve == preserve]
|
||||
|
||||
def get_built_apps_report_parts(self) -> list[str]:
|
||||
"""
|
||||
Generates report parts for new and existing apps.
|
||||
|
||||
:return: List of report parts.
|
||||
"""
|
||||
new_test_related_apps, built_test_related_apps = self.split_new_and_existing_apps(
|
||||
self.filter_apps_by_criteria(BuildStatus.SUCCESS, True)
|
||||
)
|
||||
|
||||
new_non_test_related_apps, built_non_test_related_apps = self.split_new_and_existing_apps(
|
||||
self.filter_apps_by_criteria(BuildStatus.SUCCESS, False)
|
||||
)
|
||||
|
||||
sections = []
|
||||
|
||||
if new_test_related_apps:
|
||||
new_test_related_apps_table_section = self.create_table_section(
|
||||
title=self.report_titles_map['new_test_related_apps'],
|
||||
items=new_test_related_apps,
|
||||
headers=[
|
||||
'App Dir',
|
||||
'Build Dir',
|
||||
'Download Command',
|
||||
'Your Branch App Size',
|
||||
],
|
||||
row_attrs=[
|
||||
'app_dir',
|
||||
'build_dir',
|
||||
],
|
||||
value_functions=[
|
||||
('Your Branch App Size', lambda _app: str(_app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
|
||||
(
|
||||
'Download Command',
|
||||
lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
|
||||
f'{idf_relpath(_app.build_path)}',
|
||||
),
|
||||
],
|
||||
)
|
||||
sections.extend(new_test_related_apps_table_section)
|
||||
|
||||
if built_test_related_apps:
|
||||
built_test_related_apps = self._sort_items(
|
||||
built_test_related_apps,
|
||||
key='metrics.binary_size.difference_percentage',
|
||||
order='desc',
|
||||
sort_function=self.custom_sort,
|
||||
)
|
||||
|
||||
built_test_related_apps_table_section = self.create_table_section(
|
||||
title=self.report_titles_map['built_test_related_apps'],
|
||||
items=built_test_related_apps,
|
||||
headers=[
|
||||
'App Dir',
|
||||
'Build Dir',
|
||||
'Download Command',
|
||||
'Your Branch App Size',
|
||||
'Target Branch App Size',
|
||||
'Size Diff',
|
||||
'Size Diff, %',
|
||||
],
|
||||
row_attrs=[
|
||||
'app_dir',
|
||||
'build_dir',
|
||||
],
|
||||
value_functions=[
|
||||
('Your Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
|
||||
('Target Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].target_value)),
|
||||
('Size Diff', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference)),
|
||||
('Size Diff, %', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage)),
|
||||
(
|
||||
'Download Command',
|
||||
lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
|
||||
f'{idf_relpath(_app.build_path)}',
|
||||
),
|
||||
],
|
||||
)
|
||||
sections.extend(built_test_related_apps_table_section)
|
||||
|
||||
if new_non_test_related_apps:
|
||||
new_non_test_related_apps_table_section = self.create_table_section(
|
||||
title=self.report_titles_map['new_non_test_related_apps'],
|
||||
items=new_non_test_related_apps,
|
||||
headers=[
|
||||
'App Dir',
|
||||
'Build Dir',
|
||||
'Download Command',
|
||||
'Your Branch App Size',
|
||||
],
|
||||
row_attrs=[
|
||||
'app_dir',
|
||||
'build_dir',
|
||||
],
|
||||
value_functions=[
|
||||
(
|
||||
'Download Command',
|
||||
lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
|
||||
f'{idf_relpath(_app.build_path)}',
|
||||
),
|
||||
('Your Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
|
||||
],
|
||||
)
|
||||
sections.extend(new_non_test_related_apps_table_section)
|
||||
|
||||
if built_non_test_related_apps:
|
||||
built_non_test_related_apps = self._sort_items(
|
||||
built_non_test_related_apps,
|
||||
key='metrics.binary_size.difference_percentage',
|
||||
order='desc',
|
||||
sort_function=self.custom_sort,
|
||||
)
|
||||
built_non_test_related_apps_table_section = self.create_table_section(
|
||||
title=self.report_titles_map['built_non_test_related_apps'],
|
||||
items=built_non_test_related_apps,
|
||||
headers=[
|
||||
'App Dir',
|
||||
'Build Dir',
|
||||
'Download Command',
|
||||
'Your Branch App Size',
|
||||
'Target Branch App Size',
|
||||
'Size Diff',
|
||||
'Size Diff, %',
|
||||
],
|
||||
row_attrs=[
|
||||
'app_dir',
|
||||
'build_dir',
|
||||
],
|
||||
value_functions=[
|
||||
('Your Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].source_value)),
|
||||
('Target Branch App Size', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].target_value)),
|
||||
('Size Diff', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference)),
|
||||
('Size Diff, %', lambda app: str(app.metrics[BINARY_SIZE_METRIC_NAME].difference_percentage)),
|
||||
(
|
||||
'Download Command',
|
||||
lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
|
||||
f'{idf_relpath(_app.build_path)}',
|
||||
),
|
||||
],
|
||||
)
|
||||
sections.extend(built_non_test_related_apps_table_section)
|
||||
|
||||
built_apps_report_url = self.write_report_to_file(
|
||||
self.generate_html_report(''.join(sections)),
|
||||
self.job_id,
|
||||
self.built_apps_report_file,
|
||||
)
|
||||
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['built_test_related_apps'],
|
||||
len(built_test_related_apps),
|
||||
built_apps_report_url,
|
||||
)
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['built_non_test_related_apps'],
|
||||
len(built_non_test_related_apps),
|
||||
built_apps_report_url,
|
||||
)
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['new_test_related_apps'],
|
||||
len(new_test_related_apps),
|
||||
built_apps_report_url,
|
||||
)
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['new_non_test_related_apps'],
|
||||
len(new_non_test_related_apps),
|
||||
built_apps_report_url,
|
||||
)
|
||||
|
||||
self.additional_info += self._generate_top_n_apps_by_size_table()
|
||||
|
||||
return sections
|
||||
|
||||
def get_failed_apps_report_parts(self) -> list[str]:
|
||||
failed_apps = [app for app in self.apps if app.build_status == BuildStatus.FAILED]
|
||||
if not failed_apps:
|
||||
return []
|
||||
|
||||
failed_apps_table_section = self.create_table_section(
|
||||
title=self.report_titles_map['failed_apps'],
|
||||
items=failed_apps,
|
||||
headers=['App Dir', 'Build Dir', 'Failed Reason', 'Download Command'],
|
||||
row_attrs=['app_dir', 'build_dir', 'build_comment'],
|
||||
value_functions=[
|
||||
(
|
||||
'Download Command',
|
||||
lambda _app: f'idf-ci gitlab download-artifacts --pipeline-id {self.pipeline_id} '
|
||||
f'{idf_relpath(_app.build_path)}',
|
||||
),
|
||||
],
|
||||
)
|
||||
failed_apps_report_url = self.write_report_to_file(
|
||||
self.generate_html_report(''.join(failed_apps_table_section)),
|
||||
self.job_id,
|
||||
self.failed_apps_report_file,
|
||||
)
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['failed_apps'], len(failed_apps), failed_apps_report_url
|
||||
)
|
||||
return failed_apps_table_section
|
||||
|
||||
def get_skipped_apps_report_parts(self) -> list[str]:
|
||||
skipped_apps = [app for app in self.apps if app.build_status == BuildStatus.SKIPPED]
|
||||
if not skipped_apps:
|
||||
return []
|
||||
|
||||
skipped_apps_table_section = self.create_table_section(
|
||||
title=self.report_titles_map['skipped_apps'],
|
||||
items=skipped_apps,
|
||||
headers=['App Dir', 'Build Dir', 'Skipped Reason'],
|
||||
row_attrs=['app_dir', 'build_dir', 'build_comment'],
|
||||
)
|
||||
skipped_apps_report_url = self.write_report_to_file(
|
||||
self.generate_html_report(''.join(skipped_apps_table_section)),
|
||||
self.job_id,
|
||||
self.skipped_apps_report_file,
|
||||
)
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['skipped_apps'], len(skipped_apps), skipped_apps_report_url
|
||||
)
|
||||
return skipped_apps_table_section
|
||||
|
||||
def _get_report_str(self) -> str:
|
||||
self.additional_info = (
|
||||
f'**Build Summary ({self.get_commit_summary}):**\n'
|
||||
'\n'
|
||||
'> ℹ️ Note: Binary artifacts stored in MinIO are retained for 4 DAYS from their build date\n'
|
||||
)
|
||||
failed_apps_report_parts = self.get_failed_apps_report_parts()
|
||||
skipped_apps_report_parts = self.get_skipped_apps_report_parts()
|
||||
built_apps_report_parts = self.get_built_apps_report_parts()
|
||||
|
||||
return self.generate_html_report(
|
||||
''.join(failed_apps_report_parts + built_apps_report_parts + skipped_apps_report_parts)
|
||||
)
|
||||
|
||||
|
||||
class JobReportGenerator(ReportGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
project_id: int,
|
||||
mr_iid: int,
|
||||
pipeline_id: int,
|
||||
job_id: int,
|
||||
commit_id: str,
|
||||
local_commit_id: str,
|
||||
*,
|
||||
title: str = 'Job Report',
|
||||
jobs: list[GitlabJob],
|
||||
):
|
||||
super().__init__(project_id, mr_iid, pipeline_id, job_id, commit_id, local_commit_id, title=title)
|
||||
self.jobs = jobs
|
||||
self.report_titles_map = {
|
||||
'failed_jobs': 'Failed Jobs (Excludes "integration_test" and "target_test" jobs)',
|
||||
'succeeded': 'Succeeded Jobs',
|
||||
}
|
||||
self.failed_jobs_report_file = 'job_report.html'
|
||||
|
||||
def _get_report_str(self) -> str:
|
||||
"""
|
||||
Generate a complete HTML report string by processing jobs.
|
||||
:return: Complete HTML report string.
|
||||
"""
|
||||
report_str: str = ''
|
||||
|
||||
if not self.jobs:
|
||||
print('No jobs found, skip generating job report')
|
||||
return 'No Job Found'
|
||||
|
||||
relevant_failed_jobs = self._sort_items(
|
||||
self._filter_items(
|
||||
self.jobs, lambda job: job.is_failed and job.stage not in ['integration_test', 'target_test']
|
||||
),
|
||||
key='latest_failed_count',
|
||||
)
|
||||
succeeded_jobs = self._filter_items(self.jobs, lambda job: job.is_success)
|
||||
|
||||
self.additional_info = f'**Job Summary ({self.get_commit_summary}):**\n'
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['succeeded'], len(succeeded_jobs)
|
||||
)
|
||||
|
||||
if not relevant_failed_jobs:
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['failed_jobs'], len(relevant_failed_jobs)
|
||||
)
|
||||
return report_str
|
||||
|
||||
report_sections = self.create_table_section(
|
||||
title='Failed Jobs (Excludes "integration_test" and "target_test" jobs)',
|
||||
items=relevant_failed_jobs,
|
||||
headers=[
|
||||
'Job Name',
|
||||
'Failure Reason',
|
||||
'Failure Log',
|
||||
'Failures across all other branches (10 latest jobs)',
|
||||
'URL',
|
||||
'CI Dashboard URL',
|
||||
],
|
||||
row_attrs=['name', 'failure_reason', 'failure_log', 'url', 'ci_dashboard_url'],
|
||||
value_functions=[
|
||||
(
|
||||
'Failures across all other branches (10 latest jobs)',
|
||||
lambda item: f'{getattr(item, "latest_failed_count", "")} '
|
||||
f'/ {getattr(item, "latest_total_count", "")}',
|
||||
)
|
||||
],
|
||||
)
|
||||
relevant_failed_jobs_report_url = get_artifacts_url(self.job_id, self.failed_jobs_report_file)
|
||||
self.additional_info += self.generate_additional_info_section(
|
||||
self.report_titles_map['failed_jobs'], len(relevant_failed_jobs), relevant_failed_jobs_report_url
|
||||
)
|
||||
|
||||
report_str = self.generate_html_report(''.join(report_sections))
|
||||
return report_str
|
||||
@@ -1,121 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import typing as t
|
||||
|
||||
import __init__ # noqa: F401 # inject the system path
|
||||
from idf_build_apps import json_list_files_to_apps
|
||||
from idf_ci import GitlabEnvVars
|
||||
from idf_ci_local.app import enrich_apps_with_metrics_info
|
||||
|
||||
from dynamic_pipelines.report import BuildReportGenerator
|
||||
from dynamic_pipelines.report import JobReportGenerator
|
||||
from dynamic_pipelines.utils import fetch_app_metrics
|
||||
from dynamic_pipelines.utils import fetch_failed_jobs
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser: argparse.ArgumentParser = setup_argument_parser()
|
||||
args: argparse.Namespace = parser.parse_args()
|
||||
|
||||
report_actions: dict[str, t.Callable[[argparse.Namespace], None]] = {
|
||||
'build': generate_build_report,
|
||||
'job': generate_jobs_report,
|
||||
}
|
||||
|
||||
report_action = report_actions.get(args.report_type)
|
||||
if report_action is None:
|
||||
raise ValueError('Unknown report type is requested to be generated.')
|
||||
|
||||
report_action(args)
|
||||
|
||||
|
||||
def setup_argument_parser() -> argparse.ArgumentParser:
|
||||
report_type_parser: argparse.ArgumentParser = argparse.ArgumentParser(add_help=False)
|
||||
report_type_parser.add_argument(
|
||||
'--report-type', choices=['build', 'job'], required=True, help='Type of report to generate'
|
||||
)
|
||||
report_type_args: argparse.Namespace
|
||||
remaining_args: list[str]
|
||||
report_type_args, remaining_args = report_type_parser.parse_known_args()
|
||||
|
||||
parser: argparse.ArgumentParser = argparse.ArgumentParser(
|
||||
description='Update reports in MR pipelines based on the selected report type',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
parents=[report_type_parser],
|
||||
)
|
||||
|
||||
common_arguments(parser)
|
||||
conditional_arguments(report_type_args, parser)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def common_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument('--project-id', type=int, default=os.getenv('CI_PROJECT_ID'), help='Project ID')
|
||||
parser.add_argument('--mr-iid', type=int, default=os.getenv('CI_MERGE_REQUEST_IID'), help='Merge Request IID')
|
||||
parser.add_argument('--pipeline-id', type=int, default=os.getenv('PARENT_PIPELINE_ID'), help='Pipeline ID')
|
||||
parser.add_argument('--job-id', type=int, default=os.getenv('CI_JOB_ID'), help='Job ID')
|
||||
parser.add_argument('--commit-id', default=os.getenv('CI_COMMIT_SHA', ''), help='MR merged result commit ID')
|
||||
parser.add_argument('--local-commit-id', default=os.getenv('PIPELINE_COMMIT_SHA', ''), help='local dev commit ID')
|
||||
|
||||
|
||||
def conditional_arguments(report_type_args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
|
||||
if report_type_args.report_type == 'build':
|
||||
parser.add_argument('--app-list-filepattern', default='app_info*.txt', help='Pattern to match app list files')
|
||||
elif report_type_args.report_type == 'target_test':
|
||||
parser.add_argument(
|
||||
'--junit-report-filepattern', default='XUNIT_RESULT*.xml', help='Pattern to match JUnit report files'
|
||||
)
|
||||
|
||||
|
||||
def generate_build_report(args: argparse.Namespace) -> None:
|
||||
# generate presigned url for the artifacts
|
||||
subprocess.check_output(
|
||||
[
|
||||
'idf-ci',
|
||||
'gitlab',
|
||||
'generate-presigned-json',
|
||||
'--commit-sha',
|
||||
args.local_commit_id,
|
||||
'--output',
|
||||
'presigned.json',
|
||||
],
|
||||
)
|
||||
print('generated presigned.json')
|
||||
|
||||
# generate report
|
||||
apps = json_list_files_to_apps(glob.glob(args.app_list_filepattern))
|
||||
print(f'loaded {len(apps)} apps')
|
||||
app_metrics = fetch_app_metrics(
|
||||
source_commit_sha=args.commit_id,
|
||||
target_commit_sha=os.environ.get('CI_MERGE_REQUEST_TARGET_BRANCH_SHA'),
|
||||
)
|
||||
apps = enrich_apps_with_metrics_info(app_metrics, apps)
|
||||
report_generator = BuildReportGenerator(
|
||||
args.project_id, args.mr_iid, args.pipeline_id, args.job_id, args.commit_id, args.local_commit_id, apps=apps
|
||||
)
|
||||
report_generator.post_report()
|
||||
|
||||
|
||||
def generate_jobs_report(args: argparse.Namespace) -> None:
|
||||
jobs: list[t.Any] = fetch_failed_jobs(args.commit_id)
|
||||
|
||||
if not jobs:
|
||||
return
|
||||
|
||||
report_generator = JobReportGenerator(
|
||||
args.project_id, args.mr_iid, args.pipeline_id, args.job_id, args.commit_id, args.local_commit_id, jobs=jobs
|
||||
)
|
||||
report_generator.post_report()
|
||||
|
||||
if GitlabEnvVars().IDF_CI_IS_DEBUG_PIPELINE:
|
||||
print('Debug pipeline detected, exit non-zero to fail the pipeline in order to block merge')
|
||||
exit(30)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -55,7 +55,7 @@ def main(output_filepath: str) -> None:
|
||||
exclude_runner_tags_matching = []
|
||||
for _tag in known_warnings_dict.get('no_runner_tags', []):
|
||||
if '*' not in _tag:
|
||||
exclude_runner_tags_set.add(_tag)
|
||||
exclude_runner_tags_set.add(frozenset(_tag.split(',')))
|
||||
else:
|
||||
if res := _process_match_group(_tag):
|
||||
exclude_runner_tags_matching.append(res)
|
||||
@@ -66,7 +66,7 @@ def main(output_filepath: str) -> None:
|
||||
if exclude_runner_tags := os.getenv('EXCLUDE_RUNNER_TAGS'):
|
||||
for _tag in exclude_runner_tags.split(';'):
|
||||
if '*' not in _tag:
|
||||
exclude_runner_tags_set.add(_tag)
|
||||
exclude_runner_tags_set.add(frozenset(_tag.split(',')))
|
||||
else:
|
||||
if res := _process_match_group(_tag):
|
||||
exclude_runner_tags_matching.append(res)
|
||||
@@ -75,7 +75,7 @@ def main(output_filepath: str) -> None:
|
||||
additional_dict: dict[GroupKey, dict[str, t.Any]] = {}
|
||||
for key, grouped_cases in cases.grouped_cases.items():
|
||||
# skip test cases with no runner tags
|
||||
if ','.join(sorted(key.runner_tags)) in exclude_runner_tags_set:
|
||||
if frozenset(key.runner_tags) in exclude_runner_tags_set:
|
||||
print(f'WARNING: excluding test cases with runner tags: {key.runner_tags}')
|
||||
continue
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
# Since the version 3.21 CMake passes source files and include dirs to ninja using absolute paths.
|
||||
# Needed for pytest junit reports.
|
||||
INSTALL_EXTRA_TOOLS: cmake
|
||||
# Suppress Python warning messages during CI builds to keep logs clean and focused on errors.
|
||||
# This prevents non-critical warnings (e.g., deprecation or resource warnings) from cluttering output.
|
||||
PYTHONWARNINGS: "ignore"
|
||||
needs:
|
||||
- pipeline: $PARENT_PIPELINE_ID
|
||||
job: generate_build_child_pipeline
|
||||
@@ -40,7 +43,6 @@
|
||||
- run_cmd idf-ci build run
|
||||
--parallel-count ${CI_NODE_TOTAL:-1}
|
||||
--parallel-index ${CI_NODE_INDEX:-1}
|
||||
--modified-files ${MR_MODIFIED_FILES}
|
||||
|
||||
.dynamic_target_test_template:
|
||||
image: $TARGET_TEST_ENV_IMAGE
|
||||
@@ -52,16 +54,12 @@
|
||||
# set while generating the pipeline
|
||||
nodes: ""
|
||||
INSTALL_EXTRA_TOOLS: "xtensa-esp-elf-gdb riscv32-esp-elf-gdb openocd-esp32 esp-rom-elfs"
|
||||
PYTEST_EXTRA_FLAGS: "--dev-passwd ${ETHERNET_TEST_PASSWORD} --dev-user ${ETHERNET_TEST_USER} --capture=fd --verbosity=0 --unity-test-report-mode merge"
|
||||
PYTEST_EXTRA_FLAGS: "--capture=fd --verbosity=0 --unity-test-report-mode merge"
|
||||
needs:
|
||||
- pipeline: $PARENT_PIPELINE_ID
|
||||
job: pipeline_variables
|
||||
cache:
|
||||
# Usually do not need submodule-cache in target_test
|
||||
- key: pip-cache-${LATEST_GIT_TAG}
|
||||
paths:
|
||||
- .cache/pip
|
||||
policy: pull
|
||||
- pipeline: $PARENT_PIPELINE_ID
|
||||
job: snapshot_known_failure_cases
|
||||
artifacts:
|
||||
paths:
|
||||
- XUNIT_RESULT*.xml
|
||||
@@ -71,13 +69,16 @@
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
script:
|
||||
# Merge the current known failure cases file with snapshot_known_failure_cases file captured at pipeline start
|
||||
# to avoid retry failures caused by later updates to the known failure cases file.
|
||||
- run_cmd idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
- printf '\n' >> ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
- cat artifacts/${KNOWN_FAILURE_CASES_FILE_NAME}.freeze >> ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
# get runner env config file
|
||||
- retry_failed git clone $TEST_ENV_CONFIG_REPO
|
||||
- run_cmd python $CHECKOUT_REF_SCRIPT ci-test-runner-configs ci-test-runner-configs
|
||||
# CI specific options start from "--known-failure-cases-file xxx". could ignore when running locally
|
||||
- run_cmd pytest $nodes
|
||||
--pipeline-id $PARENT_PIPELINE_ID
|
||||
--junitxml=XUNIT_RESULT_${CI_JOB_ID}.xml
|
||||
--ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME}
|
||||
--parallel-count ${CI_NODE_TOTAL:-1}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{title}}</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="https://www.espressif.com/sites/all/themes/espressif/favicon.ico"
|
||||
type="image/vnd.microsoft.icon"
|
||||
/>
|
||||
<!-- External CSS libraries -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://unpkg.com/bootstrap-table@1.22.1/dist/bootstrap-table.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.0/dist/extensions/sticky-header/bootstrap-table-sticky-header.css"
|
||||
/>
|
||||
<link
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- CSS content will be injected here during template rendering -->
|
||||
<style>
|
||||
/* {{css_content}} */
|
||||
/* Additional styles for disabled tabs */
|
||||
.report-nav-tabs .nav-tab.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Loading animation for tabs */
|
||||
.report-nav-tabs .nav-tab.loading:after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
border-left-color: #333;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-pipeline-id="{{pipeline_id}}">
|
||||
<!-- Navigation progress bar -->
|
||||
<div class="nav-progress-container">
|
||||
<div class="nav-progress-bar" id="nav-progress-bar"></div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<!-- Report header section -->
|
||||
<header class="report-header">
|
||||
<div class="logo-container">
|
||||
<img
|
||||
src="https://www.espressif.com/sites/all/themes/espressif/logo-black.svg"
|
||||
alt="Espressif Logo"
|
||||
class="logo"
|
||||
/>
|
||||
</div>
|
||||
<div class="title-container">
|
||||
<h1>
|
||||
<span style="color: #333">Dynamic Pipeline Report</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
</header>
|
||||
|
||||
<!-- Search and controls section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
class="btn btn-esp btn-sm"
|
||||
id="expand-all-tables"
|
||||
>
|
||||
<i class="fas fa-table"></i> Expand All
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-secondary btn-sm ms-2"
|
||||
id="collapse-all-tables"
|
||||
>
|
||||
<i class="fas fa-minus"></i> Collapse All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<div class="table-container">{{table}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating action buttons -->
|
||||
<div class="floating-actions">
|
||||
<div class="floating-action-btn back-to-top" id="back-to-top">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript libraries -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.22.1/dist/bootstrap-table.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.0/dist/extensions/sticky-header/bootstrap-table-sticky-header.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.22.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.10.21/tableExport.min.js"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.min.js"></script>
|
||||
|
||||
<!-- Custom scripts -->
|
||||
<script>
|
||||
{{js_content}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ all_build_finished:
|
||||
script:
|
||||
- echo "all test jobs finished"
|
||||
|
||||
generate_pytest_build_report:
|
||||
generate_presigned_json:
|
||||
stage: assign_test
|
||||
image: $ESP_ENV_IMAGE
|
||||
tags:
|
||||
@@ -29,16 +29,11 @@ generate_pytest_build_report:
|
||||
job: pipeline_variables
|
||||
artifacts:
|
||||
paths:
|
||||
- failed_apps.html
|
||||
- built_apps.html
|
||||
- skipped_apps.html
|
||||
- build_report.html
|
||||
- presigned.json
|
||||
expire_in: 1 week
|
||||
when: always
|
||||
script:
|
||||
- python tools/ci/dynamic_pipelines/scripts/generate_report.py --report-type build
|
||||
- python tools/ci/previous_stage_job_status.py --stage build
|
||||
- idf-ci gitlab generate-presigned-json --output presigned.json
|
||||
|
||||
generate_pytest_child_pipeline:
|
||||
# finally, we can get some use out of the default behavior that downloads all artifacts from the previous stage
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from .constants import CI_DASHBOARD_API
|
||||
from .constants import CI_JOB_TOKEN
|
||||
from .constants import CI_MERGE_REQUEST_SOURCE_BRANCH_SHA
|
||||
from .constants import CI_PAGES_URL
|
||||
from .constants import CI_PROJECT_URL
|
||||
from .models import GitlabJob
|
||||
|
||||
|
||||
def is_url(string: str) -> bool:
|
||||
"""
|
||||
Check if the string is a valid URL by parsing it and verifying if it contains both a scheme and a network location.
|
||||
|
||||
:param string: The string to check if it is a URL.
|
||||
:return: True if the string is a valid URL, False otherwise.
|
||||
"""
|
||||
parsed = urlparse(string)
|
||||
return bool(parsed.scheme) and bool(parsed.netloc)
|
||||
|
||||
|
||||
def fetch_failed_jobs(commit_id: str) -> list[GitlabJob]:
|
||||
"""
|
||||
Fetches a list of jobs from the specified commit_id using an API request to ci-dashboard-api.
|
||||
:param commit_id: The commit ID for which to fetch jobs.
|
||||
:return: A list of jobs if the request is successful, otherwise an empty list.
|
||||
"""
|
||||
response = requests.get(
|
||||
f'{CI_DASHBOARD_API}/commits/{commit_id}/jobs',
|
||||
headers={'CI-Job-Token': CI_JOB_TOKEN},
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f'Failed to fetch jobs data: {response.status_code} with error: {response.text}')
|
||||
return []
|
||||
|
||||
data = response.json()
|
||||
jobs = data.get('jobs', [])
|
||||
|
||||
if not jobs:
|
||||
return []
|
||||
|
||||
failed_job_names = [job['name'] for job in jobs if job['status'] == 'failed']
|
||||
response = requests.post(
|
||||
f'{CI_DASHBOARD_API}/jobs/failure_ratio',
|
||||
headers={'CI-Job-Token': CI_JOB_TOKEN},
|
||||
json={
|
||||
'job_names': failed_job_names,
|
||||
'exclude_branches': [os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME', '')],
|
||||
},
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f'Failed to fetch jobs failure rate data: {response.status_code} with error: {response.text}')
|
||||
return []
|
||||
|
||||
failure_rate_data = response.json()
|
||||
failure_rates = {item['name']: item for item in failure_rate_data.get('jobs', [])}
|
||||
|
||||
combined_jobs = []
|
||||
for job in jobs:
|
||||
failure_data = failure_rates.get(job['name'], {})
|
||||
combined_jobs.append(GitlabJob.from_json_data(job, failure_data))
|
||||
|
||||
return combined_jobs
|
||||
|
||||
|
||||
def fetch_app_metrics(
|
||||
source_commit_sha: str,
|
||||
target_commit_sha: str,
|
||||
) -> dict:
|
||||
"""
|
||||
Fetches the app metrics for the given source commit SHA and target branch SHA.
|
||||
:param source_commit_sha: The source commit SHA.
|
||||
:param target_branch_sha: The commit SHA of the branch to compare app sizes against.
|
||||
:return: A dict of sizes of built binaries.
|
||||
"""
|
||||
print(f'Fetching bin size info: {source_commit_sha=} {target_commit_sha=}')
|
||||
build_info_map = dict()
|
||||
response = requests.post(
|
||||
f'{CI_DASHBOARD_API}/apps/metrics',
|
||||
headers={'CI-Job-Token': CI_JOB_TOKEN},
|
||||
json={
|
||||
'source_commit_sha': source_commit_sha,
|
||||
'target_commit_sha': target_commit_sha,
|
||||
},
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f'Failed to fetch build info: {response.status_code} - {response.text}')
|
||||
else:
|
||||
response_data = response.json()
|
||||
build_info_map = {
|
||||
f'{info["app_path"]}_{info["config_name"]}_{info["target"]}': info for info in response_data.get('data', [])
|
||||
}
|
||||
|
||||
return build_info_map
|
||||
|
||||
|
||||
def load_file(file_path: str) -> str:
|
||||
"""
|
||||
Loads the content of a file.
|
||||
|
||||
:param file_path: The path to the file needs to be loaded.
|
||||
:return: The content of the file as a string.
|
||||
"""
|
||||
with open(file_path) as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
def format_permalink(s: str) -> str:
|
||||
"""
|
||||
Formats a given string into a permalink.
|
||||
|
||||
:param s: The string to be formatted into a permalink.
|
||||
:return: The formatted permalink as a string.
|
||||
"""
|
||||
end_index = s.find('(')
|
||||
|
||||
if end_index != -1:
|
||||
trimmed_string = s[:end_index].strip()
|
||||
else:
|
||||
trimmed_string = s.strip()
|
||||
|
||||
formatted_string = trimmed_string.lower().replace(' ', '-')
|
||||
|
||||
return formatted_string
|
||||
|
||||
|
||||
def get_artifacts_url(job_id: int, output_filepath: str) -> str:
|
||||
"""
|
||||
Generates the url of the path where the artifact will be stored in the job's artifacts .
|
||||
|
||||
:param job_id: The job identifier used to construct the URL.
|
||||
:param output_filepath: The path to the output file.
|
||||
:return: The modified URL pointing to the job's artifacts.
|
||||
"""
|
||||
url = CI_PAGES_URL.replace('esp-idf', '-/esp-idf')
|
||||
return f'{url}/-/jobs/{job_id}/artifacts/{output_filepath}'
|
||||
|
||||
|
||||
def get_repository_file_url(file_path: str) -> str:
|
||||
"""
|
||||
Generates the url of the file path inside the repository.
|
||||
|
||||
:param file_path: The file path where the file is stored.
|
||||
:return: The modified URL pointing to the file's path in the repository.
|
||||
"""
|
||||
return f'{CI_PROJECT_URL}/-/raw/{CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}/{file_path}'
|
||||
@@ -4,18 +4,15 @@ tools/ci/check_*.py
|
||||
tools/ci/check_*.txt
|
||||
tools/ci/check_*.sh
|
||||
tools/ci/check_copyright_config.yaml
|
||||
tools/ci/get_all_test_results.py
|
||||
tools/gdb_panic_server.py
|
||||
tools/check_term.py
|
||||
tools/python_version_checker.py
|
||||
tools/ci/astyle-rules.yml
|
||||
tools/ci/checkout_project_ref.py
|
||||
tools/ci/ci_fetch_submodule.py
|
||||
tools/ci/ci_get_mr_info.py
|
||||
tools/ci/ci_process_description.py
|
||||
tools/ci/configure_ci_environment.sh
|
||||
tools/ci/generate_rules.py
|
||||
tools/ci/deploy_docs.py
|
||||
tools/ci/envsubst.py
|
||||
tools/ci/*exclude*.txt
|
||||
tools/ci/executable-list.txt
|
||||
@@ -51,7 +48,6 @@ tools/ci/python_packages/idf_iperf_test_util/**/*
|
||||
tools/esp_prov/**/*
|
||||
tools/ci/sort_yaml.py
|
||||
tools/ci/sg_rules/*
|
||||
tools/ci/previous_stage_job_status.py
|
||||
tools/legacy_exports/export_legacy.fish
|
||||
tools/legacy_exports/export_legacy.sh
|
||||
tools/legacy_exports/export_legacy.ps1
|
||||
|
||||
@@ -72,7 +72,6 @@ tools/ci/check_tools_files_patterns.py
|
||||
tools/ci/check_type_comments.py
|
||||
tools/ci/checkout_project_ref.py
|
||||
tools/ci/cleanup_ignore_lists.py
|
||||
tools/ci/deploy_docs.py
|
||||
tools/ci/envsubst.py
|
||||
tools/ci/fix_empty_prototypes.sh
|
||||
tools/ci/generate_rules.py
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import argparse
|
||||
import inspect
|
||||
@@ -12,7 +12,6 @@ from itertools import product
|
||||
|
||||
import yaml
|
||||
from idf_ci_utils import IDF_PATH
|
||||
from idf_ci_utils import GitlabYmlConfig
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import pygraphviz as pgv
|
||||
@@ -90,7 +89,6 @@ class RulesWriter:
|
||||
self.cfg = self.expand_matrices()
|
||||
self.rules = self.expand_rules()
|
||||
|
||||
self.yml_config = GitlabYmlConfig()
|
||||
self.graph = None
|
||||
|
||||
def expand_matrices(self): # type: () -> dict
|
||||
@@ -195,10 +193,6 @@ class RulesWriter:
|
||||
if k.startswith('pattern'):
|
||||
continue
|
||||
|
||||
if '.rules:' + k not in self.yml_config.used_templates:
|
||||
print(f'WARNING: unused rule: {k}, skipping...')
|
||||
continue
|
||||
|
||||
res.append(self.RULES_TEMPLATE.format(k, self._format_rule(k, v)))
|
||||
return '\n\n'.join(res)
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
|
||||
'''
|
||||
Download artifacts from all test jobs
|
||||
|
||||
CI_PROJECT_DIR
|
||||
└── TEST_RESULTS
|
||||
├── <job_id>
|
||||
│ └── XUNIT_RESULT.xml
|
||||
├── <job_id>
|
||||
│ └── XUNIT_RESULT.xml
|
||||
├── <job_id>
|
||||
│ └── TEST_LOGS
|
||||
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
import gitlab_api
|
||||
|
||||
CI_PROJECT_ID = int(os.getenv('CI_PROJECT_ID', ''))
|
||||
CI_PIPELINE_ID = int(os.getenv('CI_PIPELINE_ID', ''))
|
||||
IDF_PATH = os.getenv('IDF_PATH', '.')
|
||||
TEST_RESULT_PATH = os.path.join(IDF_PATH, 'TEST_RESULTS')
|
||||
|
||||
|
||||
def download_all_test_results(result_path: str, include_retried: bool = False) -> None:
|
||||
if os.path.exists(result_path):
|
||||
shutil.rmtree(result_path)
|
||||
os.makedirs(result_path, exist_ok=True)
|
||||
|
||||
gitlab_inst = gitlab_api.Gitlab(CI_PROJECT_ID)
|
||||
pipelines = gitlab_inst.project.pipelines.get(CI_PIPELINE_ID)
|
||||
_include_retried = 'true' if include_retried else 'false'
|
||||
jobs = pipelines.jobs.list(all=True, per_page=100, include_retried=_include_retried)
|
||||
|
||||
job_info_list = []
|
||||
for job in jobs:
|
||||
if job.stage in ['target_test', 'host_test']:
|
||||
log_path = ''
|
||||
if job.status not in ['success', 'failed']:
|
||||
print('Job {}({}) is not finished'.format(job.id, job.name))
|
||||
elif not hasattr(job, 'artifacts_file'):
|
||||
print('Job {}({}) has no artifacts.'.format(job.id, job.name))
|
||||
else:
|
||||
log_path = os.path.join(result_path, 'job_{}'.format(job.id))
|
||||
print('Downloading artifacts from: {}'.format(job.name))
|
||||
os.makedirs(log_path, exist_ok=True)
|
||||
gitlab_inst.download_artifacts(job.id, log_path)
|
||||
|
||||
job_info = {
|
||||
'id': job.id,
|
||||
'name': job.name,
|
||||
'tag_list': job.tag_list,
|
||||
'status': job.status,
|
||||
'stage': job.stage,
|
||||
'web_url': job.web_url,
|
||||
'commit_url': job.commit['web_url'],
|
||||
'log_path': log_path,
|
||||
}
|
||||
job_info_list.append(job_info)
|
||||
|
||||
with open(os.path.join(result_path, 'index.json'), 'w') as f:
|
||||
f.write(json.dumps({'jobs': job_info_list}, indent=1, sort_keys=True))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--path', '-p', default=TEST_RESULT_PATH, help='Path to save result files.')
|
||||
parser.add_argument('--include_retried', action='store_true', help='Including retried jobs.')
|
||||
args = parser.parse_args()
|
||||
|
||||
download_all_test_results(args.path, args.include_retried)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,17 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Check gitlab ci yaml files
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import typing as t
|
||||
from functools import cached_property
|
||||
|
||||
from idf_ci_utils import get_submodule_dirs
|
||||
from idf_ci_utils import GitlabYmlConfig
|
||||
from idf_ci_utils import IDF_PATH
|
||||
from idf_ci_utils import GitlabYmlConfig
|
||||
from idf_ci_utils import get_submodule_dirs
|
||||
|
||||
|
||||
class YmlLinter:
|
||||
@@ -43,17 +44,6 @@ class YmlLinter:
|
||||
|
||||
exit(exit_code)
|
||||
|
||||
# name it like _1_ to make it run first
|
||||
def _lint_1_yml_parser(self) -> None:
|
||||
for k, v in self.yml_config.config.items():
|
||||
if (
|
||||
k not in self.yml_config.global_keys
|
||||
and k not in self.yml_config.anchors
|
||||
and k not in self.yml_config.templates
|
||||
and k not in self.yml_config.jobs
|
||||
):
|
||||
raise SystemExit(f'Parser incorrect. Key {k} not in global keys, anchors, templates, or jobs')
|
||||
|
||||
def _lint_default_values_artifacts(self) -> None:
|
||||
defaults_artifacts = self.yml_config.default.get('artifacts', {})
|
||||
|
||||
@@ -79,19 +69,6 @@ class YmlLinter:
|
||||
for item in undefined_patterns:
|
||||
self._errors.append(f'undefined pattern {item}. Please add {item} to .patterns-submodule')
|
||||
|
||||
def _lint_gitlab_yml_templates(self) -> None:
|
||||
unused_templates = self.yml_config.templates.keys() - self.yml_config.used_templates
|
||||
for item in unused_templates:
|
||||
# known unused ones
|
||||
if item not in [
|
||||
'.before_script:fetch:target_test', # used in dynamic pipeline
|
||||
]:
|
||||
self._errors.append(f'Unused template: {item}, please remove it')
|
||||
|
||||
undefined_templates = self.yml_config.used_templates - self.yml_config.templates.keys()
|
||||
for item in undefined_templates:
|
||||
self._errors.append(f'Undefined template: {item}')
|
||||
|
||||
def _lint_dependencies_and_needs(self) -> None:
|
||||
"""
|
||||
Use `dependencies: []` together with `needs: []` could cause missing artifacts issue.
|
||||
|
||||
@@ -4,17 +4,15 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import typing as t
|
||||
from pathlib import Path
|
||||
|
||||
from dynamic_pipelines.constants import BINARY_SIZE_METRIC_NAME
|
||||
from idf_build_apps import App
|
||||
from idf_build_apps import CMakeApp
|
||||
from idf_build_apps.constants import BuildStatus
|
||||
from idf_build_apps.utils import rmdir
|
||||
from idf_ci_utils import APP_EXTRA_S3_ARTIFACT_TYPE
|
||||
from idf_ci_utils import idf_relpath
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
_SIZE_METRICS_CONFIG_PATH = Path(__file__).parent.parent / 'metrics' / 'size_metrics' / 'size_metrics_config.yml'
|
||||
|
||||
|
||||
class IdfCMakeApp(CMakeApp):
|
||||
@@ -37,6 +35,8 @@ class IdfCMakeApp(CMakeApp):
|
||||
'gitlab',
|
||||
'upload-artifacts',
|
||||
self.app_dir,
|
||||
'--build-dir',
|
||||
self.build_dir,
|
||||
],
|
||||
[
|
||||
'idf-ci',
|
||||
@@ -63,103 +63,3 @@ class IdfCMakeApp(CMakeApp):
|
||||
self.build_path,
|
||||
exclude_file_patterns=['build_log.txt', 'size*.json'],
|
||||
)
|
||||
|
||||
|
||||
class Metrics:
|
||||
"""
|
||||
Represents a metric and its values for source, target, and the differences.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_value: t.Optional[float] = None,
|
||||
target_value: t.Optional[float] = None,
|
||||
difference: t.Optional[float] = None,
|
||||
difference_percentage: t.Optional[float] = None,
|
||||
) -> None:
|
||||
self.source_value = source_value or 0.0
|
||||
self.target_value = target_value or 0.0
|
||||
self.difference = difference or 0.0
|
||||
self.difference_percentage = difference_percentage or 0.0
|
||||
|
||||
def to_dict(self) -> t.Dict[str, t.Any]:
|
||||
"""
|
||||
Converts the Metrics object to a dictionary.
|
||||
"""
|
||||
return {
|
||||
'source_value': self.source_value,
|
||||
'target_value': self.target_value,
|
||||
'difference': self.difference,
|
||||
'difference_percentage': self.difference_percentage,
|
||||
}
|
||||
|
||||
|
||||
class AppWithMetricsInfo(IdfCMakeApp):
|
||||
metrics: t.Dict[str, Metrics]
|
||||
is_new_app: bool
|
||||
|
||||
def __init__(self, **kwargs: t.Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.metrics = {metric_name: metric_data for metric_name, metric_data in kwargs.get('metrics', {}).items()}
|
||||
self.is_new_app = kwargs.get('is_new_app', False)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
def enrich_apps_with_metrics_info(
|
||||
app_metrics_info_map: t.Dict[str, t.Dict[str, t.Any]], apps: t.List[App]
|
||||
) -> t.List[AppWithMetricsInfo]:
|
||||
def _get_full_attributes(obj: App) -> t.Dict[str, t.Any]:
|
||||
"""
|
||||
Retrieves all attributes of an object, including properties and computed fields.
|
||||
"""
|
||||
attributes: t.Dict[str, t.Any] = obj.__dict__.copy()
|
||||
for attr in dir(obj):
|
||||
if not attr.startswith('_'): # Skip private/internal attributes
|
||||
try:
|
||||
value = getattr(obj, attr)
|
||||
# Include only if it's not already in __dict__
|
||||
if attr not in attributes:
|
||||
attributes[attr] = value
|
||||
except Exception:
|
||||
# Skip attributes that raise exceptions (e.g., methods needing args)
|
||||
pass
|
||||
return attributes
|
||||
|
||||
default_metrics_structure = {
|
||||
BINARY_SIZE_METRIC_NAME: Metrics(
|
||||
source_value=0,
|
||||
target_value=0,
|
||||
difference=0,
|
||||
difference_percentage=0.0,
|
||||
),
|
||||
}
|
||||
|
||||
apps_with_metrics_info = []
|
||||
for app in apps:
|
||||
app.app_dir = idf_relpath(app.app_dir)
|
||||
key = f'{app.app_dir}_{app.config_name}_{app.target}'
|
||||
app_attributes = _get_full_attributes(app)
|
||||
|
||||
metrics = {metric_name: default_metric for metric_name, default_metric in default_metrics_structure.items()}
|
||||
is_new_app = False
|
||||
|
||||
if key in app_metrics_info_map:
|
||||
info = app_metrics_info_map[key]
|
||||
for metric_name, metric_data in info.get('metrics', {}).items():
|
||||
metrics[metric_name] = Metrics(
|
||||
source_value=metric_data.get('source_value', 0),
|
||||
target_value=metric_data.get('target_value', 0),
|
||||
difference=metric_data.get('difference', 0),
|
||||
difference_percentage=metric_data.get('difference_percentage', 0.0),
|
||||
)
|
||||
|
||||
is_new_app = info.get('is_new_app', False)
|
||||
|
||||
app_attributes.update({'metrics': metrics, 'is_new_app': is_new_app})
|
||||
|
||||
apps_with_metrics_info.append(AppWithMetricsInfo(**app_attributes))
|
||||
|
||||
return apps_with_metrics_info
|
||||
|
||||
+95
-113
@@ -1,10 +1,9 @@
|
||||
# SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2020-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# internal use only for CI
|
||||
# some CI related util functions
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import typing as t
|
||||
@@ -126,35 +125,106 @@ class GitlabYmlConfig:
|
||||
# avoid unused import in other pre-commit hooks
|
||||
import yaml
|
||||
|
||||
all_config = dict()
|
||||
root_yml = yaml.load(open(root_yml_filepath), Loader=yaml.FullLoader)
|
||||
|
||||
# expanding "include"
|
||||
for item in root_yml.pop('include', []) or []:
|
||||
if isinstance(item, dict):
|
||||
if 'project' in item:
|
||||
continue
|
||||
elif 'local' in item:
|
||||
item = item['local']
|
||||
else:
|
||||
continue
|
||||
|
||||
all_config.update(yaml.load(open(os.path.join(IDF_PATH, item)), Loader=yaml.FullLoader))
|
||||
merged_yaml = self._compile_via_gitlab_api(root_yml_filepath)
|
||||
all_config = yaml.load(merged_yaml, Loader=yaml.FullLoader) or {}
|
||||
|
||||
if 'default' in all_config:
|
||||
self._defaults = all_config.pop('default')
|
||||
|
||||
self._config = all_config
|
||||
|
||||
# anchor is the string that will be reused in templates
|
||||
self._anchor_keys: t.Set[str] = set()
|
||||
# template is a dict that will be extended
|
||||
self._template_keys: t.Set[str] = set()
|
||||
self._used_template_keys: t.Set[str] = set() # tracing the used templates
|
||||
# job is a dict that will be executed
|
||||
self._job_keys: t.Set[str] = set()
|
||||
def _inline_local_includes(self, root_yml_filepath: str) -> str:
|
||||
"""
|
||||
Recursively resolve `include: local` entries straight from disk (so uncommitted local
|
||||
changes are always picked up -- CI runners also work off a disk checkout, so there's no
|
||||
need to fetch a ref remotely via `ci_lint`'s `content_ref`/`ref`/`dry_run_ref` GET
|
||||
params). `include: project` entries (including ones nested inside local files, e.g.
|
||||
`.gitlab/ci/common.yml` including `templates/idf/common-scripts.yml`) are collected and
|
||||
left in the final `include:` list, since those files live in another GitLab project and
|
||||
can only be resolved remotely.
|
||||
|
||||
self.expand_extends()
|
||||
A dedicated Loader/Dumper pair round-trips `!reference` tags as a marker list subclass,
|
||||
since GitLab CI uses `!reference` which plain YAML doesn't know, and we need to parse
|
||||
(to merge dicts, not just string-concat) then re-dump losslessly.
|
||||
|
||||
:param root_yml_filepath: path to the local root yml file to compile
|
||||
:return: yml content with local includes resolved and merged
|
||||
"""
|
||||
import yaml
|
||||
|
||||
class _Loader(yaml.FullLoader):
|
||||
pass
|
||||
|
||||
class _Dumper(yaml.Dumper):
|
||||
pass
|
||||
|
||||
class _Reference(list):
|
||||
pass
|
||||
|
||||
_Loader.add_constructor(
|
||||
'!reference', lambda loader, node: _Reference(loader.construct_sequence(t.cast(yaml.SequenceNode, node)))
|
||||
)
|
||||
_Dumper.add_representer(_Reference, lambda dumper, data: dumper.represent_sequence('!reference', list(data)))
|
||||
|
||||
def resolve(yml_filepath: str) -> t.Tuple[dict, list]:
|
||||
with open(yml_filepath) as fr:
|
||||
data = yaml.load(fr, Loader=_Loader) or {}
|
||||
|
||||
includes = to_list(data.pop('include', None))
|
||||
|
||||
merged: dict = {}
|
||||
remaining_project_includes: list = []
|
||||
for item in includes:
|
||||
if isinstance(item, dict):
|
||||
if 'project' in item:
|
||||
remaining_project_includes.append(item)
|
||||
continue
|
||||
elif 'local' in item:
|
||||
local_path = item['local']
|
||||
else:
|
||||
continue
|
||||
elif isinstance(item, str):
|
||||
local_path = item
|
||||
else:
|
||||
continue
|
||||
|
||||
sub_merged, sub_remaining = resolve(os.path.join(IDF_PATH, local_path.lstrip('/')))
|
||||
merged.update(sub_merged)
|
||||
remaining_project_includes.extend(sub_remaining)
|
||||
|
||||
# this file's own top-level keys override whatever its includes defined
|
||||
merged.update(data)
|
||||
return merged, remaining_project_includes
|
||||
|
||||
merged_config, project_includes = resolve(root_yml_filepath)
|
||||
if project_includes:
|
||||
merged_config['include'] = project_includes
|
||||
|
||||
return yaml.dump(merged_config, Dumper=_Dumper, sort_keys=False) # type: ignore
|
||||
|
||||
def _compile_via_gitlab_api(self, root_yml_filepath: str) -> str:
|
||||
"""
|
||||
Call the GitLab CI Lint API to get the fully compiled (all `include`s resolved) yml,
|
||||
same as what `glab ci config compile` does. This replaces the old recursive local-only
|
||||
parsing, since the project now includes configs from other projects as well.
|
||||
|
||||
:param root_yml_filepath: path to the local root yml file to compile
|
||||
:return: merged (fully resolved) yml content as a string
|
||||
"""
|
||||
sys.path.insert(0, os.path.join(IDF_PATH, 'tools', 'ci', 'python_packages'))
|
||||
import gitlab_api
|
||||
|
||||
content = self._inline_local_includes(root_yml_filepath)
|
||||
|
||||
gitlab_inst = gitlab_api.Gitlab()
|
||||
project_id = os.getenv('CI_PROJECT_ID') or gitlab_inst.get_project_id('esp-idf', namespace='espressif')
|
||||
project = gitlab_inst.gitlab_inst.projects.get(project_id, lazy=True)
|
||||
|
||||
lint_result = project.ci_lint.create({'content': content})
|
||||
if not lint_result.valid:
|
||||
raise RuntimeError(f'Failed to compile {root_yml_filepath} via GitLab CI Lint API: {lint_result.errors}')
|
||||
|
||||
return lint_result.merged_yaml # type: ignore
|
||||
|
||||
@property
|
||||
def default(self) -> t.Dict[str, t.Any]:
|
||||
@@ -168,80 +238,9 @@ class GitlabYmlConfig:
|
||||
def global_keys(self) -> t.List[str]:
|
||||
return ['default', 'include', 'workflow', 'variables', 'stages']
|
||||
|
||||
@cached_property
|
||||
def anchors(self) -> t.Dict[str, t.Any]:
|
||||
return {k: v for k, v in self.config.items() if k in self._anchor_keys}
|
||||
|
||||
@cached_property
|
||||
def jobs(self) -> t.Dict[str, t.Any]:
|
||||
return {k: v for k, v in self.config.items() if k in self._job_keys}
|
||||
|
||||
@cached_property
|
||||
def templates(self) -> t.Dict[str, t.Any]:
|
||||
return {k: v for k, v in self.config.items() if k in self._template_keys}
|
||||
|
||||
@cached_property
|
||||
def used_templates(self) -> t.Set[str]:
|
||||
return self._used_template_keys
|
||||
|
||||
def expand_extends(self) -> None:
|
||||
"""
|
||||
expand the `extends` key in-place.
|
||||
"""
|
||||
for k, v in self.config.items():
|
||||
if k in self.global_keys:
|
||||
continue
|
||||
|
||||
if isinstance(v, (str, list)):
|
||||
self._anchor_keys.add(k)
|
||||
elif k.startswith('.if-'):
|
||||
self._anchor_keys.add(k)
|
||||
elif k.startswith('.'):
|
||||
self._template_keys.add(k)
|
||||
elif isinstance(v, dict):
|
||||
self._job_keys.add(k)
|
||||
else:
|
||||
raise ValueError(f'Unknown type for key {k} with value {v}')
|
||||
|
||||
# no need to expand anchor
|
||||
|
||||
# expand template first
|
||||
for k in self._template_keys:
|
||||
self._expand_extends(k)
|
||||
|
||||
# expand job
|
||||
for k in self._job_keys:
|
||||
self._expand_extends(k)
|
||||
|
||||
def _merge_dict(self, d1: t.Dict[str, t.Any], d2: t.Dict[str, t.Any]) -> t.Any:
|
||||
for k, v in d2.items():
|
||||
if k in d1:
|
||||
if isinstance(v, dict) and isinstance(d1[k], dict):
|
||||
d1[k] = self._merge_dict(d1[k], v)
|
||||
else:
|
||||
d1[k] = v
|
||||
else:
|
||||
d1[k] = v
|
||||
|
||||
return d1
|
||||
|
||||
def _expand_extends(self, name: str) -> t.Dict[str, t.Any]:
|
||||
extends = to_list(self.config[name].pop('extends', None))
|
||||
if not extends:
|
||||
return self.config[name] # type: ignore
|
||||
|
||||
original_d = self.config[name].copy()
|
||||
d = {}
|
||||
while extends:
|
||||
self._used_template_keys.update(extends) # for tracking
|
||||
|
||||
for i in extends:
|
||||
d.update(self._expand_extends(i))
|
||||
|
||||
extends = to_list(self.config[name].pop('extends', None))
|
||||
|
||||
self.config[name] = self._merge_dict(d, original_d)
|
||||
return self.config[name] # type: ignore
|
||||
return {k: v for k, v in self.config.items() if not k.startswith('.') and k not in self.global_keys}
|
||||
|
||||
|
||||
def get_all_manifest_files() -> t.List[str]:
|
||||
@@ -256,23 +255,6 @@ def get_all_manifest_files() -> t.List[str]:
|
||||
return paths
|
||||
|
||||
|
||||
def sanitize_job_name(name: str) -> str:
|
||||
"""
|
||||
Sanitize the job name from CI_JOB_NAME
|
||||
|
||||
- for job with `parallel: int` set, the `CI_JOB_NAME` would be `job_name index/total`, like `foo 1/3`
|
||||
- for job with `parallel: matrix` set, the `CI_JOB_NAME` would be `job_name: [var1, var2]`, like `foo: [a, b]`
|
||||
|
||||
We consider
|
||||
- the jobs generated by `parallel: int` as the same job, i.e., we remove the index/total part.
|
||||
- the jobs generated by `parallel: matrix` as different jobs, so we keep the matrix part.
|
||||
|
||||
:param name: job name
|
||||
:return: sanitized job name
|
||||
"""
|
||||
return re.sub(r' \d+/\d+', '', name)
|
||||
|
||||
|
||||
def idf_relpath(p: str) -> str:
|
||||
"""
|
||||
Turn all paths under IDF_PATH to relative paths
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import os
|
||||
import typing as t
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--stage', type=str, help='Stage name for check jobs status')
|
||||
args = parser.parse_args()
|
||||
|
||||
GITLAB_TOKEN = os.getenv('ESPCI_TOKEN')
|
||||
GITLAB_HTTP_SERVER = os.getenv('GITLAB_HTTP_SERVER')
|
||||
CI_PROJECT_ID = os.getenv('CI_PROJECT_ID')
|
||||
CI_PIPELINE_ID = os.getenv('CI_PIPELINE_ID')
|
||||
|
||||
api_path = f'projects/{CI_PROJECT_ID}/pipelines/{CI_PIPELINE_ID}/jobs?scope[]=failed&per_page=100'
|
||||
|
||||
page = 0
|
||||
while True:
|
||||
response = requests.get(
|
||||
f'{GITLAB_HTTP_SERVER}/api/v4/{api_path}&page={page}',
|
||||
headers={'PRIVATE-TOKEN': GITLAB_TOKEN}
|
||||
)
|
||||
jobs = response.json()
|
||||
if not jobs:
|
||||
break
|
||||
|
||||
for job in jobs:
|
||||
if job['stage'] == args.stage:
|
||||
print(f'Jobs from the previous stage {args.stage} should pass; otherwise, this job will fail.')
|
||||
sys.exit(1)
|
||||
page += 1
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import argparse
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -52,9 +53,11 @@ def retry(func: TR) -> TR:
|
||||
raise e # get out of the loop
|
||||
else:
|
||||
logging.warning(
|
||||
'Network failure in {}, retrying ({})'.format(getattr(func, '__name__', '(unknown callable)'),
|
||||
retried))
|
||||
time.sleep(2 ** retried) # wait a bit more after each retry
|
||||
'Network failure in {}, retrying ({})'.format(
|
||||
getattr(func, '__name__', '(unknown callable)'), retried
|
||||
)
|
||||
)
|
||||
time.sleep(2**retried) # wait a bit more after each retry
|
||||
continue
|
||||
else:
|
||||
break
|
||||
@@ -67,7 +70,6 @@ class Gitlab(object):
|
||||
JOB_NAME_PATTERN = re.compile(r'(\w+)(\s+(\d+)/(\d+))?')
|
||||
|
||||
DOWNLOAD_ERROR_MAX_RETRIES = 3
|
||||
DEFAULT_BUILD_CHILD_PIPELINE_NAME = 'Build Child Pipeline'
|
||||
|
||||
def __init__(self, project_id: Union[int, str, None] = None):
|
||||
config_data_from_env = os.getenv('PYTHON_GITLAB_CONFIG')
|
||||
@@ -117,7 +119,7 @@ class Gitlab(object):
|
||||
:param namespace: namespace to match when we have multiple project with same name
|
||||
:return: project ID
|
||||
"""
|
||||
projects = self.gitlab_inst.projects.list(search=name)
|
||||
projects = self.gitlab_inst.projects.list(search=name, get_all=True)
|
||||
res = []
|
||||
for project in projects:
|
||||
if namespace is None:
|
||||
@@ -152,7 +154,9 @@ class Gitlab(object):
|
||||
archive_file.extractall(destination)
|
||||
|
||||
@retry
|
||||
def download_artifact(self, job_id: int, artifact_path: List[str], destination: Optional[str] = None) -> List[bytes]:
|
||||
def download_artifact(
|
||||
self, job_id: int, artifact_path: List[str], destination: Optional[str] = None
|
||||
) -> List[bytes]:
|
||||
"""
|
||||
download specific path of job artifacts and extract to destination.
|
||||
|
||||
@@ -208,8 +212,9 @@ class Gitlab(object):
|
||||
return job_id_list
|
||||
|
||||
@retry
|
||||
def download_archive(self, ref: str, destination: str, project_id: Optional[int] = None,
|
||||
cache_dir: Optional[str] = None) -> str:
|
||||
def download_archive(
|
||||
self, ref: str, destination: str, project_id: Optional[int] = None, cache_dir: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Download archive of certain commit of a repository and extract to destination path
|
||||
|
||||
@@ -235,8 +240,11 @@ class Gitlab(object):
|
||||
except gitlab.GitlabGetError as e:
|
||||
logging.error('Failed to archive from project {}'.format(project_id))
|
||||
raise e
|
||||
logging.info('Downloaded archive size: {:.03f}MB'.format(
|
||||
float(os.path.getsize(local_archive_file)) / (1024 * 1024)))
|
||||
logging.info(
|
||||
'Downloaded archive size: {:.03f}MB'.format(
|
||||
float(os.path.getsize(local_archive_file)) / (1024 * 1024)
|
||||
)
|
||||
)
|
||||
|
||||
return self.decompress_archive(local_archive_file, destination)
|
||||
|
||||
@@ -248,22 +256,38 @@ class Gitlab(object):
|
||||
logging.error('Failed to archive from project {}'.format(project_id))
|
||||
raise e
|
||||
|
||||
logging.info('Downloaded archive size: {:.03f}MB'.format(float(os.path.getsize(temp_file.name)) / (1024 * 1024)))
|
||||
logging.info(
|
||||
'Downloaded archive size: {:.03f}MB'.format(float(os.path.getsize(temp_file.name)) / (1024 * 1024))
|
||||
)
|
||||
|
||||
return self.decompress_archive(temp_file.name, destination)
|
||||
|
||||
@staticmethod
|
||||
def _to_win32_long_path(path: str) -> str:
|
||||
normalized_path = os.path.normpath(os.path.abspath(path))
|
||||
if normalized_path.startswith('\\\\?\\'):
|
||||
return normalized_path
|
||||
if normalized_path.startswith('\\\\'):
|
||||
return '\\\\?\\UNC\\' + normalized_path[2:]
|
||||
return '\\\\?\\' + normalized_path
|
||||
|
||||
@staticmethod
|
||||
def decompress_archive(path: str, destination: str) -> str:
|
||||
full_destination = os.path.abspath(destination)
|
||||
# By default max path length is set to 260 characters
|
||||
# Prefix `\\?\` extends it to 32,767 characters
|
||||
if sys.platform == 'win32':
|
||||
full_destination = '\\\\?\\' + full_destination
|
||||
|
||||
try:
|
||||
with tarfile.open(path, 'r') as archive_file:
|
||||
root_name = archive_file.getnames()[0]
|
||||
archive_file.extractall(full_destination)
|
||||
members = archive_file.getmembers()
|
||||
root_name = members[0].name
|
||||
if sys.platform == 'win32':
|
||||
# tarfile keeps archive member names in POSIX form. Normalize them before
|
||||
# combining with a long-path-prefixed destination to avoid invalid mixed separators.
|
||||
full_destination = Gitlab._to_win32_long_path(full_destination)
|
||||
members = [copy.copy(member) for member in members]
|
||||
for member in members:
|
||||
member.name = member.name.replace('/', '\\')
|
||||
member.linkname = member.linkname.replace('/', '\\')
|
||||
archive_file.extractall(full_destination, members=members)
|
||||
except tarfile.TarError as e:
|
||||
logging.error(f'Error while decompressing archive {path}')
|
||||
raise e
|
||||
@@ -282,36 +306,28 @@ class Gitlab(object):
|
||||
|
||||
def get_downstream_pipeline_ids(self, main_pipeline_id: int) -> List[int]:
|
||||
"""
|
||||
Retrieve the IDs of all downstream child pipelines for a given main pipeline.
|
||||
Retrieve the IDs of all downstream child pipelines for a given main pipeline,
|
||||
recursing through arbitrarily nested child pipelines.
|
||||
|
||||
:param main_pipeline_id: The ID of the main pipeline to start the search.
|
||||
:return: A list of IDs of all downstream child pipelines.
|
||||
:return: A list of IDs of all downstream child pipelines (all levels).
|
||||
"""
|
||||
bridge_pipeline_ids = []
|
||||
child_pipeline_ids = []
|
||||
child_pipeline_ids: List[int] = []
|
||||
|
||||
main_pipeline_bridges = self.project.pipelines.get(main_pipeline_id).bridges.list()
|
||||
for bridge in main_pipeline_bridges:
|
||||
pipeline_bridges = self.project.pipelines.get(main_pipeline_id).bridges.list()
|
||||
for bridge in pipeline_bridges:
|
||||
downstream_pipeline = bridge.attributes.get('downstream_pipeline')
|
||||
if not downstream_pipeline:
|
||||
continue
|
||||
bridge_pipeline_ids.append(downstream_pipeline['id'])
|
||||
|
||||
for bridge_pipeline_id in bridge_pipeline_ids:
|
||||
child_pipeline_ids.append(bridge_pipeline_id)
|
||||
bridge_pipeline = self.project.pipelines.get(bridge_pipeline_id)
|
||||
|
||||
if not bridge_pipeline.name == self.DEFAULT_BUILD_CHILD_PIPELINE_NAME:
|
||||
downstream_pipeline_id = downstream_pipeline.get('id')
|
||||
if downstream_pipeline_id is None:
|
||||
continue
|
||||
|
||||
child_bridges = bridge_pipeline.bridges.list()
|
||||
for child_bridge in child_bridges:
|
||||
downstream_child_pipeline = child_bridge.attributes.get('downstream_pipeline')
|
||||
if not downstream_child_pipeline:
|
||||
continue
|
||||
child_pipeline_ids.append(downstream_child_pipeline.get('id'))
|
||||
child_pipeline_ids.append(downstream_pipeline_id)
|
||||
# recurse to collect further nested (grandchild+) pipelines
|
||||
child_pipeline_ids.extend(self.get_downstream_pipeline_ids(downstream_pipeline_id))
|
||||
|
||||
return [pid for pid in child_pipeline_ids if pid is not None]
|
||||
return child_pipeline_ids
|
||||
|
||||
def retry_failed_jobs(self, pipeline_id: int, retry_allowed_failures: bool = False) -> List[int]:
|
||||
"""
|
||||
|
||||
@@ -164,38 +164,3 @@ function join_by {
|
||||
printf %s "$f" "${@/#/$d}"
|
||||
fi
|
||||
}
|
||||
|
||||
function is_based_on_commits() {
|
||||
# This function would accept space-separated args as multiple commits.
|
||||
# The return value would be 0 if current HEAD is based on any of the specified commits.
|
||||
#
|
||||
# In our CI, we use environment variable $REQUIRED_ANCESTOR_COMMITS to declare the ancestor commits.
|
||||
# Please remember to set one commit for each release branch.
|
||||
|
||||
commits=$*
|
||||
if [[ -z $commits ]]; then
|
||||
info "Not specifying commits that branches should be based on, skipping check..."
|
||||
return 0
|
||||
fi
|
||||
|
||||
commits_str="$(join_by " or " $commits)" # no doublequotes here, passing array
|
||||
|
||||
info "Checking if current branch is based on $commits_str..."
|
||||
for i in $commits; do
|
||||
if git merge-base --is-ancestor "$i" HEAD >/dev/null 2>&1; then
|
||||
info "Current branch is based on $i"
|
||||
return 0
|
||||
else
|
||||
info "Current branch is not based on $i"
|
||||
fi
|
||||
done
|
||||
|
||||
error "The base commit of your branch is too old."
|
||||
error "The branch should be more recent than either of the following commits:"
|
||||
error " $commits_str"
|
||||
error "To fix the issue:"
|
||||
error " - If your merge request is 'Draft', or has conflicts with the target branch, rebase it to the latest master or release branch"
|
||||
error " - Otherwise, simply run a new pipeline."
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -661,6 +661,21 @@ macro(idf_build_process target)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
idf_build_get_property(prefix __PREFIX)
|
||||
|
||||
file(GLOB root_dep_component_dirs
|
||||
${IDF_TOOLS_PATH}/root_managed_components/idf${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}.${IDF_VERSION_PATCH}/*)
|
||||
list(SORT root_dep_component_dirs)
|
||||
foreach(component_dir ${root_dep_component_dirs})
|
||||
# A potential component must be a directory
|
||||
if(IS_DIRECTORY ${component_dir})
|
||||
__component_dir_quick_check(is_component ${component_dir})
|
||||
if(is_component)
|
||||
__component_add(${component_dir} ${prefix} "idf_managed_components")
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# Perform early expansion of component CMakeLists.txt in CMake scripting mode.
|
||||
# It is here we retrieve the public and private requirements of each component.
|
||||
# It is also here we add the common component requirements to each component's
|
||||
|
||||
+12
-12
@@ -97,9 +97,9 @@ endmacro()
|
||||
#
|
||||
function(__component_dir_quick_check var component_dir)
|
||||
set(res 1)
|
||||
get_filename_component(abs_dir ${component_dir} ABSOLUTE)
|
||||
get_filename_component(abs_dir "${component_dir}" ABSOLUTE)
|
||||
|
||||
get_filename_component(base_dir ${abs_dir} NAME)
|
||||
get_filename_component(base_dir "${abs_dir}" NAME)
|
||||
string(SUBSTRING "${base_dir}" 0 1 first_char)
|
||||
|
||||
# Check the component directory contains a CMakeLists.txt file
|
||||
@@ -149,8 +149,8 @@ function(__component_add component_dir prefix component_source)
|
||||
# so later in the build, these component targets actually contain the properties meant for the
|
||||
# corresponding component library.
|
||||
idf_build_get_property(component_targets __COMPONENT_TARGETS)
|
||||
get_filename_component(abs_dir ${component_dir} ABSOLUTE)
|
||||
get_filename_component(base_dir ${abs_dir} NAME)
|
||||
get_filename_component(abs_dir "${component_dir}" ABSOLUTE)
|
||||
get_filename_component(base_dir "${abs_dir}" NAME)
|
||||
|
||||
if(NOT EXISTS "${abs_dir}/CMakeLists.txt")
|
||||
message(FATAL_ERROR "Directory '${component_dir}' does not contain a component.")
|
||||
@@ -277,15 +277,15 @@ macro(__component_add_sources sources)
|
||||
message(WARNING "SRCS and SRC_DIRS are both specified; ignoring SRC_DIRS.")
|
||||
endif()
|
||||
foreach(src ${__SRCS})
|
||||
get_filename_component(src "${src}" ABSOLUTE BASE_DIR ${COMPONENT_DIR})
|
||||
list(APPEND sources ${src})
|
||||
get_filename_component(src "${src}" ABSOLUTE BASE_DIR "${COMPONENT_DIR}")
|
||||
list(APPEND sources "${src}")
|
||||
endforeach()
|
||||
else()
|
||||
if(__SRC_DIRS)
|
||||
foreach(dir ${__SRC_DIRS})
|
||||
get_filename_component(abs_dir ${dir} ABSOLUTE BASE_DIR ${COMPONENT_DIR})
|
||||
get_filename_component(abs_dir "${dir}" ABSOLUTE BASE_DIR "${COMPONENT_DIR}")
|
||||
|
||||
if(NOT IS_DIRECTORY ${abs_dir})
|
||||
if(NOT IS_DIRECTORY "${abs_dir}")
|
||||
message(FATAL_ERROR "SRC_DIRS entry '${dir}' does not exist.")
|
||||
endif()
|
||||
|
||||
@@ -294,7 +294,7 @@ macro(__component_add_sources sources)
|
||||
|
||||
if(dir_sources)
|
||||
foreach(src ${dir_sources})
|
||||
get_filename_component(src "${src}" ABSOLUTE BASE_DIR ${COMPONENT_DIR})
|
||||
get_filename_component(src "${src}" ABSOLUTE BASE_DIR "${COMPONENT_DIR}")
|
||||
list(APPEND sources "${src}")
|
||||
endforeach()
|
||||
else()
|
||||
@@ -316,11 +316,11 @@ endmacro()
|
||||
|
||||
macro(__component_add_include_dirs lib dirs type)
|
||||
foreach(dir ${dirs})
|
||||
get_filename_component(_dir ${dir} ABSOLUTE BASE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
if(NOT IS_DIRECTORY ${_dir})
|
||||
get_filename_component(_dir "${dir}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_LIST_DIR}")
|
||||
if(NOT IS_DIRECTORY "${_dir}")
|
||||
message(FATAL_ERROR "Include directory '${_dir}' is not a directory.")
|
||||
endif()
|
||||
target_include_directories(${lib} ${type} ${_dir})
|
||||
target_include_directories(${lib} ${type} "${_dir}")
|
||||
endforeach()
|
||||
endmacro()
|
||||
|
||||
|
||||
@@ -52,6 +52,13 @@ if(NOT __idf_env_set)
|
||||
include(prefix_map)
|
||||
include(openocd)
|
||||
|
||||
# ESP-IDF extra dependencies defined in tools/idf_extra_components.yml
|
||||
if(WIN32)
|
||||
set_default(IDF_TOOLS_PATH "$ENV{USERPROFILE}/.espressif")
|
||||
else()
|
||||
set_default(IDF_TOOLS_PATH "$ENV{HOME}/.espressif")
|
||||
endif()
|
||||
|
||||
__build_init("${idf_path}")
|
||||
|
||||
# Check if IDF_ENV_FPGA environment is set
|
||||
|
||||
@@ -131,6 +131,8 @@ foreach(__component_target ${__component_targets})
|
||||
|
||||
if("${__component_source}" STREQUAL "idf_components")
|
||||
list(APPEND __TARGETS_IDF_COMPONENTS ${__component_target})
|
||||
elseif("${__component_source}" STREQUAL "idf_managed_components")
|
||||
list(APPEND __TARGETS_IDF_MANAGED_COMPONENTS ${__component_target})
|
||||
elseif("${__component_source}" STREQUAL "project_managed_components")
|
||||
list(APPEND __TARGETS_PROJECT_MANAGED_COMPONENTS ${__component_target})
|
||||
elseif("${__component_source}" STREQUAL "project_extra_components")
|
||||
@@ -147,6 +149,7 @@ set(__sorted_component_targets "")
|
||||
foreach(__target IN LISTS __TARGETS_PROJECT_COMPONENTS
|
||||
__TARGETS_PROJECT_EXTRA_COMPONENTS
|
||||
__TARGETS_PROJECT_MANAGED_COMPONENTS
|
||||
__TARGETS_IDF_MANAGED_COMPONENTS
|
||||
__TARGETS_IDF_COMPONENTS)
|
||||
__component_get_property(__component_name ${__target} COMPONENT_NAME)
|
||||
list(APPEND __sorted_component_targets ${__target})
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# This file defines extra dependencies for ESP-IDF
|
||||
# the dependencies defined here will be downloaded to
|
||||
# $IDF_TOOLS_PATH/root_managed_components
|
||||
# Each major.minor version of ESP-IDF can have its own subdirectory
|
||||
# For example, for ESP-IDF v6.0, the dependencies will be installed to
|
||||
# $IDF_TOOLS_PATH/root_managed_components/idf6.0
|
||||
|
||||
# The syntax is defined in:
|
||||
# https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/manifest_file.html#dependencies
|
||||
|
||||
#dependencies:
|
||||
@@ -6,7 +6,7 @@
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/tools/idf-tools.html
|
||||
|
||||
# ci
|
||||
idf-ci>=0.3,<1
|
||||
idf-ci
|
||||
|
||||
coverage
|
||||
jsonschema
|
||||
|
||||
@@ -166,7 +166,6 @@ def test_examples_security_secure_boot_ecdsa(dut: Dut) -> None:
|
||||
|
||||
# Test secure boot flow.
|
||||
# Correctly signed bootloader + correctly signed app should work
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.parametrize(
|
||||
'qemu_extra_args',
|
||||
|
||||
@@ -6,7 +6,6 @@ from pytest_embedded import Dut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
|
||||
|
||||
@pytest.mark.host_test
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.parametrize('config', ['secure_update_with_fe'], indirect=True)
|
||||
@idf_parametrize('target', ['esp32c3'], indirect=['target'])
|
||||
|
||||
@@ -6,7 +6,6 @@ from pytest_embedded_idf.utils import idf_parametrize
|
||||
|
||||
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.host_test
|
||||
@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target'])
|
||||
def test_std_filesystem(dut: Dut) -> None:
|
||||
dut.expect_exact('All tests passed', timeout=200)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
import os
|
||||
|
||||
@@ -10,7 +10,6 @@ PROMPT = 'test_intr_dump>'
|
||||
|
||||
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.host_test
|
||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||
def test_esp_intr_dump_nonshared(dut: Dut) -> None:
|
||||
dut.expect_exact(PROMPT, timeout=30)
|
||||
@@ -24,7 +23,6 @@ def test_esp_intr_dump_nonshared(dut: Dut) -> None:
|
||||
|
||||
|
||||
@pytest.mark.qemu
|
||||
@pytest.mark.host_test
|
||||
@idf_parametrize('target', ['esp32'], indirect=['target'])
|
||||
def test_esp_intr_dump_shared(dut: Dut) -> None:
|
||||
dut.expect_exact(PROMPT, timeout=30)
|
||||
@@ -54,5 +52,5 @@ def test_esp_intr_dump_expected_output(dut: Dut) -> None:
|
||||
dut.expect_exact(PROMPT, timeout=30)
|
||||
dut.write('intr_dump\n')
|
||||
exp_out_file = os.path.join(os.path.dirname(__file__), 'expected_output', f'{dut.target}.txt')
|
||||
for line in open(exp_out_file, 'r').readlines():
|
||||
for line in open(exp_out_file).readlines():
|
||||
dut.expect_exact(line.strip())
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from idf_ci_utils import IDF_PATH
|
||||
from pytest_embedded import Dut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
|
||||
@@ -16,12 +15,12 @@ def test_app_mmu_page_size_32k_and_bootloader_mmu_page_size_64k(dut: Dut, app_do
|
||||
|
||||
assert '32K' in config
|
||||
app_config = config.replace('32K', '64K')
|
||||
build_dir = f'build_{dut.target}_{app_config}'
|
||||
|
||||
path_to_mmu_page_size_64k_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
|
||||
if app_downloader:
|
||||
app_downloader.download_app(os.path.relpath(path_to_mmu_page_size_64k_build, IDF_PATH), 'flash')
|
||||
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
|
||||
|
||||
dut.serial.bootloader_flash(path_to_mmu_page_size_64k_build)
|
||||
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
|
||||
dut.expect('MMU page size mismatch')
|
||||
dut.expect('App is running')
|
||||
dut.expect('Partition test done')
|
||||
@@ -36,12 +35,12 @@ def test_app_mmu_page_size_64k_and_bootloader_mmu_page_size_32k(dut: Dut, app_do
|
||||
|
||||
assert '64K' in config
|
||||
app_config = config.replace('64K', '32K')
|
||||
build_dir = f'build_{dut.target}_{app_config}'
|
||||
|
||||
path_to_mmu_page_size_32k_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
|
||||
if app_downloader:
|
||||
app_downloader.download_app(os.path.relpath(path_to_mmu_page_size_32k_build, IDF_PATH), 'flash')
|
||||
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
|
||||
|
||||
dut.serial.bootloader_flash(path_to_mmu_page_size_32k_build)
|
||||
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
|
||||
dut.expect('MMU page size mismatch')
|
||||
dut.expect('App is running')
|
||||
dut.expect('Partition test done')
|
||||
|
||||
@@ -4,7 +4,6 @@ import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from idf_ci_utils import IDF_PATH
|
||||
from pytest_embedded import Dut
|
||||
from pytest_embedded_idf.utils import idf_parametrize
|
||||
|
||||
@@ -19,12 +18,12 @@ def test_multicore_app_and_unicore_bootloader(dut: Dut, app_downloader, config)
|
||||
|
||||
assert 'multicore' in config
|
||||
app_config = config.replace('multicore', 'unicore')
|
||||
build_dir = f'build_{dut.target}_{app_config}'
|
||||
|
||||
path_to_unicore_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
|
||||
if app_downloader:
|
||||
app_downloader.download_app(os.path.relpath(path_to_unicore_build, IDF_PATH), 'flash')
|
||||
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
|
||||
|
||||
dut.serial.bootloader_flash(path_to_unicore_build)
|
||||
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
|
||||
dut.expect('Unicore bootloader')
|
||||
dut.expect('Multicore app')
|
||||
if 'psram' in config:
|
||||
@@ -43,12 +42,12 @@ def test_unicore_app_and_multicore_bootloader(dut: Dut, app_downloader, config)
|
||||
|
||||
assert 'unicore' in config
|
||||
app_config = config.replace('unicore', 'multicore')
|
||||
build_dir = f'build_{dut.target}_{app_config}'
|
||||
|
||||
path_to_multicore_build = os.path.join(dut.app.app_path, f'build_{dut.target}_{app_config}')
|
||||
if app_downloader:
|
||||
app_downloader.download_app(os.path.relpath(path_to_multicore_build, IDF_PATH), 'flash')
|
||||
app_downloader.download_app(dut.app.app_path, build_dir, 'flash')
|
||||
|
||||
dut.serial.bootloader_flash(path_to_multicore_build)
|
||||
dut.serial.bootloader_flash(os.path.join(dut.app.app_path, build_dir))
|
||||
dut.expect('Multicore bootloader')
|
||||
dut.expect('Unicore app')
|
||||
if 'psram' in config:
|
||||
|
||||
@@ -14,6 +14,9 @@ junit_family = xunit1
|
||||
junit_logging = stdout
|
||||
junit_log_passing_tests = False
|
||||
|
||||
filterwarnings =
|
||||
ignore::pytest.PytestExperimentalApiWarning
|
||||
|
||||
## !! When adding new markers, don't forget to update also the tools\test_build_system\README.md !!
|
||||
markers =
|
||||
test_app_copy: specify relative path of the app to copy, and the prefix of the destination directory name
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import json
|
||||
import os.path
|
||||
|
||||
@@ -27,9 +27,9 @@ except ImportError:
|
||||
import idf
|
||||
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
idf_py_path = os.path.join(current_dir, '..', 'idf.py')
|
||||
idf_py_path = os.path.normpath(os.path.join(current_dir, '..', 'idf.py'))
|
||||
extension_path = os.path.join(current_dir, 'test_idf_extensions', 'test_ext')
|
||||
py_actions_path = os.path.join(current_dir, '..', 'idf_py_actions')
|
||||
py_actions_path = os.path.normpath(os.path.join(current_dir, '..', 'idf_py_actions'))
|
||||
link_path = os.path.join(py_actions_path, 'test_ext')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user