| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | # SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | # SPDX-License-Identifier: Apache-2.0 | 
					
						
							| 
									
										
										
										
											2021-12-02 10:14:18 +08:00
										 |  |  | # pylint: disable=W0621  # redefined-outer-name | 
					
						
							| 
									
										
										
										
											2024-01-12 14:42:00 +01:00
										 |  |  | # | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | # IDF is using [pytest](https://github.com/pytest-dev/pytest) and | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | # [pytest-embedded plugin](https://github.com/espressif/pytest-embedded) as its test framework. | 
					
						
							| 
									
										
										
										
											2024-01-12 14:42:00 +01:00
										 |  |  | # | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | # if you found any bug or have any question, | 
					
						
							|  |  |  | # please report to https://github.com/espressif/pytest-embedded/issues | 
					
						
							|  |  |  | # or discuss at https://github.com/espressif/pytest-embedded/discussions | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import sys | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if os.path.join(os.path.dirname(__file__), 'tools', 'ci') not in sys.path: | 
					
						
							|  |  |  |     sys.path.append(os.path.join(os.path.dirname(__file__), 'tools', 'ci')) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if os.path.join(os.path.dirname(__file__), 'tools', 'ci', 'python_packages') not in sys.path: | 
					
						
							|  |  |  |     sys.path.append(os.path.join(os.path.dirname(__file__), 'tools', 'ci', 'python_packages')) | 
					
						
							| 
									
										
										
										
											2022-10-21 11:46:24 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  | import glob | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | import io | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | import logging | 
					
						
							|  |  |  | import os | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  | import re | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  | import typing as t | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | import zipfile | 
					
						
							| 
									
										
										
										
											2023-09-18 10:59:05 +02:00
										 |  |  | from copy import deepcopy | 
					
						
							| 
									
										
										
										
											2024-04-25 16:54:48 +08:00
										 |  |  | from urllib.parse import quote | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | import common_test_methods  # noqa: F401 | 
					
						
							|  |  |  | import gitlab_api | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | import pytest | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | import requests | 
					
						
							|  |  |  | import yaml | 
					
						
							| 
									
										
										
										
											2023-07-31 12:49:08 +08:00
										 |  |  | from _pytest.config import Config | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | from _pytest.fixtures import FixtureRequest | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | from artifacts_handler import ArtifactType | 
					
						
							|  |  |  | from dynamic_pipelines.constants import TEST_RELATED_APPS_DOWNLOAD_URLS_FILENAME | 
					
						
							|  |  |  | from idf_ci.app import import_apps_from_txt | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  | from idf_ci.uploader import AppDownloader, AppUploader | 
					
						
							| 
									
										
										
										
											2024-01-23 10:32:14 +01:00
										 |  |  | from idf_ci_utils import IDF_PATH, idf_relpath | 
					
						
							| 
									
										
										
										
											2024-05-09 10:32:52 +02:00
										 |  |  | from idf_pytest.constants import DEFAULT_SDKCONFIG, ENV_MARKERS, SPECIAL_MARKERS, TARGET_MARKERS, PytestCase, \ | 
					
						
							|  |  |  |     DEFAULT_LOGDIR | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | from idf_pytest.plugin import IDF_PYTEST_EMBEDDED_KEY, ITEM_PYTEST_CASE_KEY, IdfPytestEmbedded | 
					
						
							|  |  |  | from idf_pytest.utils import format_case_id | 
					
						
							| 
									
										
										
										
											2022-04-25 17:26:29 +08:00
										 |  |  | from pytest_embedded.plugin import multi_dut_argument, multi_dut_fixture | 
					
						
							| 
									
										
										
										
											2022-05-18 14:59:34 +08:00
										 |  |  | from pytest_embedded_idf.dut import IdfDut | 
					
						
							| 
									
										
										
										
											2023-09-04 14:56:35 +08:00
										 |  |  | from pytest_embedded_idf.unity_tester import CaseTester | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-07-06 23:58:59 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-01 11:35:56 +08:00
										 |  |  | ############ | 
					
						
							|  |  |  | # Fixtures # | 
					
						
							|  |  |  | ############ | 
					
						
							| 
									
										
										
										
											2022-12-01 17:40:03 +08:00
										 |  |  | @pytest.fixture(scope='session') | 
					
						
							|  |  |  | def idf_path() -> str: | 
					
						
							|  |  |  |     return os.path.dirname(__file__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-05-07 09:19:00 +02:00
										 |  |  | @pytest.fixture(scope='session') | 
					
						
							|  |  |  | def session_root_logdir(idf_path: str) -> str: | 
					
						
							|  |  |  |     """Session scoped log dir for pytest-embedded""" | 
					
						
							|  |  |  |     return idf_path | 
					
						
							| 
									
										
										
										
											2022-04-02 16:47:58 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-22 21:46:56 +08:00
										 |  |  | @pytest.fixture | 
					
						
							| 
									
										
										
										
											2023-09-04 14:56:35 +08:00
										 |  |  | def case_tester(unity_tester: CaseTester) -> CaseTester: | 
					
						
							|  |  |  |     return unity_tester | 
					
						
							| 
									
										
										
										
											2022-09-22 21:46:56 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | @pytest.fixture | 
					
						
							| 
									
										
										
										
											2022-04-25 17:26:29 +08:00
										 |  |  | @multi_dut_argument | 
					
						
							| 
									
										
										
										
											2021-12-01 11:35:56 +08:00
										 |  |  | def config(request: FixtureRequest) -> str: | 
					
						
							| 
									
										
										
										
											2023-07-31 12:49:08 +08:00
										 |  |  |     return getattr(request, 'param', None) or DEFAULT_SDKCONFIG  # type: ignore | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-15 14:33:48 +01:00
										 |  |  | @pytest.fixture | 
					
						
							|  |  |  | @multi_dut_fixture | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  | def target(request: FixtureRequest, dut_total: int, dut_index: int) -> str: | 
					
						
							| 
									
										
										
										
											2024-01-15 14:33:48 +01:00
										 |  |  |     plugin = request.config.stash[IDF_PYTEST_EMBEDDED_KEY] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if dut_total == 1: | 
					
						
							|  |  |  |         return plugin.target[0]  # type: ignore | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return plugin.target[dut_index]  # type: ignore | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-02-11 14:58:50 +08:00
										 |  |  | @pytest.fixture | 
					
						
							|  |  |  | def test_func_name(request: FixtureRequest) -> str: | 
					
						
							|  |  |  |     return request.node.function.__name__  # type: ignore | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-19 12:12:15 +08:00
										 |  |  | @pytest.fixture | 
					
						
							|  |  |  | def test_case_name(request: FixtureRequest, target: str, config: str) -> str: | 
					
						
							| 
									
										
										
										
											2023-07-25 10:06:57 +08:00
										 |  |  |     is_qemu = request._pyfuncitem.get_closest_marker('qemu') is not None | 
					
						
							| 
									
										
										
										
											2023-09-18 10:59:05 +02:00
										 |  |  |     if hasattr(request._pyfuncitem, 'callspec'): | 
					
						
							|  |  |  |         params = deepcopy(request._pyfuncitem.callspec.params)  # type: ignore | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         params = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     filtered_params = {} | 
					
						
							|  |  |  |     for k, v in params.items(): | 
					
						
							|  |  |  |         if k not in request.session._fixturemanager._arg2fixturedefs:  # type: ignore | 
					
						
							|  |  |  |             filtered_params[k] = v  # not fixture ones | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return format_case_id(target, config, request.node.originalname, is_qemu=is_qemu, params=filtered_params)  # type: ignore | 
					
						
							| 
									
										
										
										
											2022-01-19 12:12:15 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | @pytest.fixture(scope='session') | 
					
						
							|  |  |  | def pipeline_id(request: FixtureRequest) -> t.Optional[str]: | 
					
						
							|  |  |  |     return request.config.getoption('pipeline_id', None) or os.getenv('PARENT_PIPELINE_ID', None)  # type: ignore | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  | class BuildReportDownloader(AppDownloader): | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |     def __init__(self, presigned_url_yaml: str) -> None: | 
					
						
							|  |  |  |         self.app_presigned_urls_dict: t.Dict[str, t.Dict[str, str]] = yaml.safe_load(presigned_url_yaml) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  |     def _download_app(self, app_build_path: str, artifact_type: ArtifactType) -> None: | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |         url = self.app_presigned_urls_dict[app_build_path][artifact_type.value] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  |         logging.info('Downloading app from %s', url) | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |         with io.BytesIO() as f: | 
					
						
							|  |  |  |             for chunk in requests.get(url).iter_content(chunk_size=1024 * 1024): | 
					
						
							|  |  |  |                 if chunk: | 
					
						
							|  |  |  |                     f.write(chunk) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             f.seek(0) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             with zipfile.ZipFile(f) as zip_ref: | 
					
						
							| 
									
										
										
										
											2024-01-12 08:43:20 +01:00
										 |  |  |                 zip_ref.extractall(IDF_PATH) | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  |     def download_app(self, app_build_path: str, artifact_type: t.Optional[ArtifactType] = None) -> None: | 
					
						
							|  |  |  |         if app_build_path not in self.app_presigned_urls_dict: | 
					
						
							|  |  |  |             raise ValueError( | 
					
						
							|  |  |  |                 f'No presigned url found for {app_build_path}. ' | 
					
						
							|  |  |  |                 f'Usually this should not happen, please re-trigger a pipeline.' | 
					
						
							|  |  |  |                 f'If this happens again, please report this bug to the CI channel.' | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         super().download_app(app_build_path, artifact_type) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | @pytest.fixture(scope='session') | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  | def app_downloader(pipeline_id: t.Optional[str]) -> t.Optional[AppDownloader]: | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |     if not pipeline_id: | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |         'IDF_S3_BUCKET' in os.environ | 
					
						
							|  |  |  |         and 'IDF_S3_ACCESS_KEY' in os.environ | 
					
						
							|  |  |  |         and 'IDF_S3_SECRET_KEY' in os.environ | 
					
						
							|  |  |  |         and 'IDF_S3_SERVER' in os.environ | 
					
						
							|  |  |  |         and 'IDF_S3_BUCKET' in os.environ | 
					
						
							|  |  |  |     ): | 
					
						
							|  |  |  |         return AppUploader(pipeline_id) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     logging.info('Downloading build report from the build pipeline %s', pipeline_id) | 
					
						
							|  |  |  |     test_app_presigned_urls_file = None | 
					
						
							| 
									
										
										
										
											2024-01-23 12:11:07 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     gl = gitlab_api.Gitlab(os.getenv('CI_PROJECT_ID', 'espressif/esp-idf')) | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for child_pipeline in gl.project.pipelines.get(pipeline_id, lazy=True).bridges.list(iterator=True): | 
					
						
							|  |  |  |         if child_pipeline.name == 'build_child_pipeline': | 
					
						
							|  |  |  |             for job in gl.project.pipelines.get(child_pipeline.downstream_pipeline['id'], lazy=True).jobs.list( | 
					
						
							|  |  |  |                 iterator=True | 
					
						
							|  |  |  |             ): | 
					
						
							|  |  |  |                 if job.name == 'generate_pytest_build_report': | 
					
						
							|  |  |  |                     test_app_presigned_urls_file = gl.download_artifact( | 
					
						
							|  |  |  |                         job.id, [TEST_RELATED_APPS_DOWNLOAD_URLS_FILENAME] | 
					
						
							|  |  |  |                     )[0] | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if test_app_presigned_urls_file: | 
					
						
							|  |  |  |         return BuildReportDownloader(test_app_presigned_urls_file) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | @pytest.fixture | 
					
						
							| 
									
										
										
										
											2022-04-25 17:26:29 +08:00
										 |  |  | @multi_dut_fixture | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | def build_dir( | 
					
						
							|  |  |  |     request: FixtureRequest, | 
					
						
							|  |  |  |     app_path: str, | 
					
						
							|  |  |  |     target: t.Optional[str], | 
					
						
							|  |  |  |     config: t.Optional[str], | 
					
						
							| 
									
										
										
										
											2024-01-22 10:14:17 +01:00
										 |  |  |     app_downloader: t.Optional[AppDownloader], | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | ) -> str: | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     Check local build dir with the following priority: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     1. build_<target>_<config> | 
					
						
							|  |  |  |     2. build_<target> | 
					
						
							|  |  |  |     3. build_<config> | 
					
						
							|  |  |  |     4. build | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Returns: | 
					
						
							|  |  |  |         valid build directory | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |     # download from minio on CI | 
					
						
							|  |  |  |     case: PytestCase = request._pyfuncitem.stash[ITEM_PYTEST_CASE_KEY] | 
					
						
							|  |  |  |     if app_downloader: | 
					
						
							|  |  |  |         # somehow hardcoded... | 
					
						
							| 
									
										
										
										
											2024-01-23 10:32:14 +01:00
										 |  |  |         app_build_path = os.path.join(idf_relpath(app_path), f'build_{target}_{config}') | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |         if case.requires_elf_or_map: | 
					
						
							|  |  |  |             app_downloader.download_app(app_build_path) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             app_downloader.download_app(app_build_path, ArtifactType.BUILD_DIR_WITHOUT_MAP_AND_ELF_FILES) | 
					
						
							|  |  |  |         check_dirs = [f'build_{target}_{config}'] | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         check_dirs = [] | 
					
						
							| 
									
										
										
										
											2024-04-30 17:57:17 +02:00
										 |  |  |         build_dir_arg = request.config.getoption('build_dir', None) | 
					
						
							|  |  |  |         if build_dir_arg: | 
					
						
							|  |  |  |             check_dirs.append(build_dir_arg) | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |         if target is not None and config is not None: | 
					
						
							|  |  |  |             check_dirs.append(f'build_{target}_{config}') | 
					
						
							|  |  |  |         if target is not None: | 
					
						
							|  |  |  |             check_dirs.append(f'build_{target}') | 
					
						
							|  |  |  |         if config is not None: | 
					
						
							|  |  |  |             check_dirs.append(f'build_{config}') | 
					
						
							|  |  |  |         check_dirs.append('build') | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     for check_dir in check_dirs: | 
					
						
							|  |  |  |         binary_path = os.path.join(app_path, check_dir) | 
					
						
							|  |  |  |         if os.path.isdir(binary_path): | 
					
						
							| 
									
										
										
										
											2023-06-20 11:10:47 +08:00
										 |  |  |             logging.info(f'found valid binary path: {binary_path}') | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  |             return check_dir | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-01 10:18:44 +08:00
										 |  |  |         logging.warning('checking binary path: %s... missing... try another place', binary_path) | 
					
						
							| 
									
										
										
										
											2021-11-17 17:09:36 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-21 11:46:24 +08:00
										 |  |  |     raise ValueError( | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  |         f'no build dir valid. Please build the binary via "idf.py -B {check_dirs[0]} build" and run pytest again' | 
					
						
							| 
									
										
										
										
											2022-02-18 15:37:39 +08:00
										 |  |  |     ) | 
					
						
							| 
									
										
										
										
											2021-12-01 11:35:56 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.fixture(autouse=True) | 
					
						
							| 
									
										
										
										
											2022-04-25 17:26:29 +08:00
										 |  |  | @multi_dut_fixture | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  | def junit_properties(test_case_name: str, record_xml_attribute: t.Callable[[str, object], None]) -> None: | 
					
						
							| 
									
										
										
										
											2021-12-01 11:35:56 +08:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     This fixture is autoused and will modify the junit report test case name to <target>.<config>.<case_name> | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2022-01-19 12:12:15 +08:00
										 |  |  |     record_xml_attribute('name', test_case_name) | 
					
						
							| 
									
										
										
										
											2021-12-02 10:14:18 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | @pytest.fixture(autouse=True) | 
					
						
							|  |  |  | @multi_dut_fixture | 
					
						
							|  |  |  | def ci_job_url(record_xml_attribute: t.Callable[[str, object], None]) -> None: | 
					
						
							|  |  |  |     if ci_job_url := os.getenv('CI_JOB_URL'): | 
					
						
							|  |  |  |         record_xml_attribute('ci_job_url', ci_job_url) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-23 18:59:25 +08:00
										 |  |  | @pytest.fixture(autouse=True) | 
					
						
							|  |  |  | def set_test_case_name(request: FixtureRequest, test_case_name: str) -> None: | 
					
						
							|  |  |  |     request.node.funcargs['test_case_name'] = test_case_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-02 17:38:41 +08:00
										 |  |  | @pytest.fixture(autouse=True) | 
					
						
							|  |  |  | def set_dut_log_url(record_xml_attribute: t.Callable[[str, object], None], _pexpect_logfile: str) -> t.Generator: | 
					
						
							|  |  |  |     # Record the "dut_log_url" attribute in the XML report once test execution finished | 
					
						
							|  |  |  |     yield | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not isinstance(_pexpect_logfile, str): | 
					
						
							|  |  |  |         record_xml_attribute('dut_log_url', 'No log URL found') | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ci_pages_url = os.getenv('CI_PAGES_URL') | 
					
						
							|  |  |  |     logdir_pattern = re.compile(rf'({DEFAULT_LOGDIR}/.*)') | 
					
						
							|  |  |  |     match = logdir_pattern.search(_pexpect_logfile) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not match: | 
					
						
							|  |  |  |         record_xml_attribute('dut_log_url', 'No log URL found') | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not ci_pages_url: | 
					
						
							|  |  |  |         record_xml_attribute('dut_log_url', _pexpect_logfile) | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     job_id = os.getenv('CI_JOB_ID', '0') | 
					
						
							|  |  |  |     modified_ci_pages_url = ci_pages_url.replace('esp-idf', '-/esp-idf') | 
					
						
							|  |  |  |     log_url = f'{modified_ci_pages_url}/-/jobs/{job_id}/artifacts/{match.group(1)}' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     record_xml_attribute('dut_log_url', log_url) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  | ###################### | 
					
						
							|  |  |  | # Log Util Functions # | 
					
						
							|  |  |  | ###################### | 
					
						
							|  |  |  | @pytest.fixture | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  | def log_performance(record_property: t.Callable[[str, object], None]) -> t.Callable[[str, str], None]: | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     log performance item with pre-defined format to the console | 
					
						
							|  |  |  |     and record it under the ``properties`` tag in the junit report if available. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def real_func(item: str, value: str) -> None: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         :param item: performance item name | 
					
						
							|  |  |  |         :param value: performance value | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         logging.info('[Performance][%s]: %s', item, value) | 
					
						
							|  |  |  |         record_property(item, value) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return real_func | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.fixture | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  | def check_performance(idf_path: str) -> t.Callable[[str, float, str], None]: | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |     """
 | 
					
						
							|  |  |  |     check if the given performance item meets the passing standard or not | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def real_func(item: str, value: float, target: str) -> None: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         :param item: performance item name | 
					
						
							|  |  |  |         :param value: performance item value | 
					
						
							|  |  |  |         :param target: target chip | 
					
						
							|  |  |  |         :raise: AssertionError: if check fails | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2023-01-10 14:58:02 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |         def _find_perf_item(operator: str, path: str) -> float: | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |             with open(path) as f: | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |                 data = f.read() | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |             match = re.search(fr'#define\s+IDF_PERFORMANCE_{operator}_{item.upper()}\s+([\d.]+)', data) | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |             return float(match.group(1))  # type: ignore | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def _check_perf(operator: str, standard_value: float) -> None: | 
					
						
							|  |  |  |             if operator == 'MAX': | 
					
						
							|  |  |  |                 ret = value <= standard_value | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 ret = value >= standard_value | 
					
						
							|  |  |  |             if not ret: | 
					
						
							|  |  |  |                 raise AssertionError( | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |                     f"[Performance] {item} value is {value}, doesn't meet pass standard {standard_value}" | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |                 ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         path_prefix = os.path.join(idf_path, 'components', 'idf_test', 'include') | 
					
						
							|  |  |  |         performance_files = ( | 
					
						
							|  |  |  |             os.path.join(path_prefix, target, 'idf_performance_target.h'), | 
					
						
							|  |  |  |             os.path.join(path_prefix, 'idf_performance.h'), | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         found_item = False | 
					
						
							|  |  |  |         for op in ['MIN', 'MAX']: | 
					
						
							|  |  |  |             for performance_file in performance_files: | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     standard = _find_perf_item(op, performance_file) | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |                 except (OSError, AttributeError): | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |                     # performance file doesn't exist or match is not found in it | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 _check_perf(op, standard) | 
					
						
							|  |  |  |                 found_item = True | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if not found_item: | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |             raise AssertionError(f'Failed to get performance standard for {item}') | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return real_func | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.fixture | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  | def log_minimum_free_heap_size(dut: IdfDut, config: str) -> t.Callable[..., None]: | 
					
						
							| 
									
										
										
										
											2022-12-15 11:41:09 +08:00
										 |  |  |     def real_func() -> None: | 
					
						
							|  |  |  |         res = dut.expect(r'Minimum free heap size: (\d+) bytes') | 
					
						
							|  |  |  |         logging.info( | 
					
						
							|  |  |  |             '\n------ heap size info ------\n' | 
					
						
							|  |  |  |             '[app_name] {}\n' | 
					
						
							|  |  |  |             '[config_name] {}\n' | 
					
						
							|  |  |  |             '[target] {}\n' | 
					
						
							|  |  |  |             '[minimum_free_heap_size] {} Bytes\n' | 
					
						
							|  |  |  |             '------ heap size end ------'.format( | 
					
						
							|  |  |  |                 os.path.basename(dut.app.app_path), | 
					
						
							|  |  |  |                 config, | 
					
						
							|  |  |  |                 dut.target, | 
					
						
							|  |  |  |                 res.group(1).decode('utf8'), | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return real_func | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | @pytest.fixture(scope='session') | 
					
						
							| 
									
										
										
										
											2023-03-31 17:24:16 +02:00
										 |  |  | def dev_password(request: FixtureRequest) -> str: | 
					
						
							|  |  |  |     return request.config.getoption('dev_passwd') or '' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  | @pytest.fixture(scope='session') | 
					
						
							| 
									
										
										
										
											2023-03-31 17:24:16 +02:00
										 |  |  | def dev_user(request: FixtureRequest) -> str: | 
					
						
							|  |  |  |     return request.config.getoption('dev_user') or '' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-02 10:14:18 +08:00
										 |  |  | ################## | 
					
						
							|  |  |  | # Hook functions # | 
					
						
							|  |  |  | ################## | 
					
						
							| 
									
										
										
										
											2022-02-18 15:37:39 +08:00
										 |  |  | def pytest_addoption(parser: pytest.Parser) -> None: | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  |     idf_group = parser.getgroup('idf') | 
					
						
							|  |  |  |     idf_group.addoption( | 
					
						
							| 
									
										
										
										
											2022-02-18 15:37:39 +08:00
										 |  |  |         '--sdkconfig', | 
					
						
							|  |  |  |         help='sdkconfig postfix, like sdkconfig.ci.<config>. (Default: None, which would build all found apps)', | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  |     idf_group.addoption( | 
					
						
							| 
									
										
										
										
											2023-03-31 17:24:16 +02:00
										 |  |  |         '--dev-user', | 
					
						
							|  |  |  |         help='user name associated with some specific device/service used during the test execution', | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  |     idf_group.addoption( | 
					
						
							| 
									
										
										
										
											2023-03-31 17:24:16 +02:00
										 |  |  |         '--dev-passwd', | 
					
						
							|  |  |  |         help='password associated with some specific device/service used during the test execution', | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  |     idf_group.addoption( | 
					
						
							|  |  |  |         '--app-info-filepattern', | 
					
						
							|  |  |  |         help='glob pattern to specify the files that include built app info generated by ' | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |         '`idf-build-apps --collect-app-info ...`. will not raise ValueError when binary ' | 
					
						
							|  |  |  |         'paths not exist in local file system if not listed recorded in the app info.', | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  |     ) | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |     idf_group.addoption( | 
					
						
							|  |  |  |         '--pipeline-id', | 
					
						
							|  |  |  |         help='main pipeline id, not the child pipeline id. Specify this option to download the artifacts ' | 
					
						
							|  |  |  |         'from the minio server for debugging purpose.', | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2022-02-18 15:37:39 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-14 11:53:36 +08:00
										 |  |  | def pytest_configure(config: Config) -> None: | 
					
						
							| 
									
										
										
										
											2022-05-22 00:38:17 +08:00
										 |  |  |     # cli option "--target" | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |     target = [_t.strip().lower() for _t in (config.getoption('target', '') or '').split(',') if _t.strip()] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # add markers based on idf_pytest/constants.py | 
					
						
							|  |  |  |     for name, description in { | 
					
						
							|  |  |  |         **TARGET_MARKERS, | 
					
						
							|  |  |  |         **ENV_MARKERS, | 
					
						
							|  |  |  |         **SPECIAL_MARKERS, | 
					
						
							|  |  |  |     }.items(): | 
					
						
							|  |  |  |         config.addinivalue_line('markers', f'{name}: {description}') | 
					
						
							| 
									
										
										
										
											2022-05-22 00:38:17 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     help_commands = ['--help', '--fixtures', '--markers', '--version'] | 
					
						
							|  |  |  |     for cmd in help_commands: | 
					
						
							|  |  |  |         if cmd in config.invocation_params.args: | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |             target = ['unneeded'] | 
					
						
							| 
									
										
										
										
											2022-05-22 00:38:17 +08:00
										 |  |  |             break | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-28 14:38:47 +01:00
										 |  |  |     markexpr = config.getoption('markexpr') or '' | 
					
						
							|  |  |  |     # check marker expr set via "pytest -m" | 
					
						
							|  |  |  |     if not target and markexpr: | 
					
						
							|  |  |  |         # we use `-m "esp32 and generic"` in our CI to filter the test cases | 
					
						
							|  |  |  |         # this doesn't cover all use cases, but fit what we do in CI. | 
					
						
							|  |  |  |         for marker in markexpr.split('and'): | 
					
						
							|  |  |  |             marker = marker.strip() | 
					
						
							|  |  |  |             if marker in TARGET_MARKERS: | 
					
						
							|  |  |  |                 target.append(marker) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # "--target" must be set | 
					
						
							|  |  |  |     if not target: | 
					
						
							|  |  |  |         raise SystemExit( | 
					
						
							|  |  |  |             """Pass `--target TARGET[,TARGET...]` to specify all targets the test cases are using.
 | 
					
						
							|  |  |  |     - for single DUT, we run with `pytest --target esp32` | 
					
						
							|  |  |  |     - for multi DUT, we run with `pytest --target esp32,esp32,esp32s2` to indicate all DUTs | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2022-05-22 00:38:17 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |     apps = None | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  |     app_info_filepattern = config.getoption('app_info_filepattern') | 
					
						
							|  |  |  |     if app_info_filepattern: | 
					
						
							| 
									
										
										
										
											2023-12-18 15:29:58 +01:00
										 |  |  |         apps = [] | 
					
						
							|  |  |  |         for f in glob.glob(os.path.join(IDF_PATH, app_info_filepattern)): | 
					
						
							|  |  |  |             apps.extend(import_apps_from_txt(f)) | 
					
						
							| 
									
										
										
										
											2023-05-24 10:53:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-12 14:42:00 +01:00
										 |  |  |     if '--collect-only' not in config.invocation_params.args: | 
					
						
							|  |  |  |         config.stash[IDF_PYTEST_EMBEDDED_KEY] = IdfPytestEmbedded( | 
					
						
							| 
									
										
										
										
											2024-03-12 11:04:37 +01:00
										 |  |  |             config_name=config.getoption('sdkconfig'), | 
					
						
							| 
									
										
										
										
											2024-01-12 14:42:00 +01:00
										 |  |  |             target=target, | 
					
						
							|  |  |  |             apps=apps, | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         config.pluginmanager.register(config.stash[IDF_PYTEST_EMBEDDED_KEY]) | 
					
						
							| 
									
										
										
										
											2022-03-14 11:53:36 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def pytest_unconfigure(config: Config) -> None: | 
					
						
							| 
									
										
										
										
											2023-07-31 12:49:08 +08:00
										 |  |  |     _pytest_embedded = config.stash.get(IDF_PYTEST_EMBEDDED_KEY, None) | 
					
						
							| 
									
										
										
										
											2022-03-14 11:53:36 +08:00
										 |  |  |     if _pytest_embedded: | 
					
						
							| 
									
										
										
										
											2023-07-31 12:49:08 +08:00
										 |  |  |         del config.stash[IDF_PYTEST_EMBEDDED_KEY] | 
					
						
							| 
									
										
										
										
											2022-03-14 11:53:36 +08:00
										 |  |  |         config.pluginmanager.unregister(_pytest_embedded) | 
					
						
							| 
									
										
										
										
											2023-10-31 19:01:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | dut_artifacts_url = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @pytest.hookimpl(hookwrapper=True) | 
					
						
							|  |  |  | def pytest_runtest_makereport(item, call):  # type: ignore | 
					
						
							|  |  |  |     outcome = yield | 
					
						
							|  |  |  |     report = outcome.get_result() | 
					
						
							|  |  |  |     report.sections = [] | 
					
						
							|  |  |  |     if report.failed: | 
					
						
							|  |  |  |         _dut = item.funcargs.get('dut') | 
					
						
							|  |  |  |         if not _dut: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         job_id = os.getenv('CI_JOB_ID', 0) | 
					
						
							|  |  |  |         url = os.getenv('CI_PAGES_URL', '').replace('esp-idf', '-/esp-idf') | 
					
						
							| 
									
										
										
										
											2024-05-09 10:32:52 +02:00
										 |  |  |         template = f'{url}/-/jobs/{job_id}/artifacts/{DEFAULT_LOGDIR}/{{}}' | 
					
						
							| 
									
										
										
										
											2023-10-31 19:01:40 +08:00
										 |  |  |         logs_files = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def get_path(x: str) -> str: | 
					
						
							| 
									
										
										
										
											2024-05-09 10:32:52 +02:00
										 |  |  |             return x.split(f'{DEFAULT_LOGDIR}/', 1)[1] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-10-31 19:01:40 +08:00
										 |  |  |         if isinstance(_dut, list): | 
					
						
							|  |  |  |             logs_files.extend([template.format(get_path(d.logfile)) for d in _dut]) | 
					
						
							|  |  |  |             dut_artifacts_url.append('{}:'.format(_dut[0].test_case_name)) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             logs_files.append(template.format(get_path(_dut.logfile))) | 
					
						
							|  |  |  |             dut_artifacts_url.append('{}:'.format(_dut.test_case_name)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for file in logs_files: | 
					
						
							| 
									
										
										
										
											2024-04-25 16:54:48 +08:00
										 |  |  |             dut_artifacts_url.append('    - {}'.format(quote(file, safe=':/'))) | 
					
						
							| 
									
										
										
										
											2023-10-31 19:01:40 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def pytest_terminal_summary(terminalreporter, exitstatus, config):  # type: ignore | 
					
						
							|  |  |  |     if dut_artifacts_url: | 
					
						
							|  |  |  |         terminalreporter.ensure_newline() | 
					
						
							|  |  |  |         terminalreporter.section('Failed Test Artifacts URL', sep='-', red=True, bold=True) | 
					
						
							|  |  |  |         terminalreporter.line('\n'.join(dut_artifacts_url)) |