diff --git a/conftest.py b/conftest.py index 06699f2e6a..0d2abe9f08 100644 --- a/conftest.py +++ b/conftest.py @@ -40,7 +40,7 @@ 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 from idf_ci.uploader import AppDownloader, AppUploader -from idf_ci_utils import IDF_PATH +from idf_ci_utils import IDF_PATH, idf_relpath from idf_pytest.constants import DEFAULT_SDKCONFIG, ENV_MARKERS, SPECIAL_MARKERS, TARGET_MARKERS, PytestCase from idf_pytest.plugin import IDF_PYTEST_EMBEDDED_KEY, ITEM_PYTEST_CASE_KEY, IdfPytestEmbedded from idf_pytest.utils import format_case_id @@ -219,7 +219,7 @@ def build_dir( case: PytestCase = request._pyfuncitem.stash[ITEM_PYTEST_CASE_KEY] if app_downloader: # somehow hardcoded... - app_build_path = os.path.join(os.path.relpath(app_path, IDF_PATH), f'build_{target}_{config}') + app_build_path = os.path.join(idf_relpath(app_path), f'build_{target}_{config}') if case.requires_elf_or_map: app_downloader.download_app(app_build_path) else: diff --git a/tools/ci/idf_ci_utils.py b/tools/ci/idf_ci_utils.py index ecabd22ada..d8d33d4635 100644 --- a/tools/ci/idf_ci_utils.py +++ b/tools/ci/idf_ci_utils.py @@ -248,3 +248,16 @@ def sanitize_job_name(name: str) -> str: :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 + :param p: path + :return: relpath to IDF_PATH, or absolute path if not under IDF_PATH + """ + abs_path = os.path.abspath(p) + if abs_path.startswith(IDF_PATH): + return os.path.relpath(abs_path, IDF_PATH) + else: + return abs_path diff --git a/tools/ci/idf_pytest/constants.py b/tools/ci/idf_pytest/constants.py index 780975bdc1..77fbb5b21f 100644 --- a/tools/ci/idf_pytest/constants.py +++ b/tools/ci/idf_pytest/constants.py @@ -12,6 +12,7 @@ from pathlib import Path from _pytest.python import Function from idf_ci_utils import IDF_PATH +from idf_ci_utils import idf_relpath from pytest_embedded.utils import to_list SUPPORTED_TARGETS = ['esp32', 'esp32s2', 'esp32c3', 'esp32s3', 'esp32c2', 'esp32c6', 'esp32h2', 'esp32p4'] @@ -149,11 +150,14 @@ class CollectMode(str, Enum): ALL = 'all' -@dataclass class PytestApp: - path: str - target: str - config: str + """ + Pytest App with relative path to IDF_PATH + """ + def __init__(self, path: str, target: str, config: str) -> None: + self.path = idf_relpath(path) + self.target = target + self.config = config def __hash__(self) -> int: return hash((self.path, self.target, self.config)) @@ -245,7 +249,7 @@ class PytestCase: if 'jtag' in self.env_markers or 'usb_serial_jtag' in self.env_markers: return True - if any('panic' in Path(app.path).resolve().parts for app in self.apps): + if any('panic' in Path(app.path).parts for app in self.apps): return True return False diff --git a/tools/ci/idf_pytest/plugin.py b/tools/ci/idf_pytest/plugin.py index 7cf1a4b774..34c4ff9aab 100644 --- a/tools/ci/idf_pytest/plugin.py +++ b/tools/ci/idf_pytest/plugin.py @@ -13,6 +13,7 @@ from _pytest.python import Function from _pytest.runner import CallInfo from idf_build_apps import App from idf_build_apps.constants import BuildStatus +from idf_ci_utils import idf_relpath from pytest_embedded import Dut from pytest_embedded.plugin import parse_multi_dut_args from pytest_embedded.utils import find_by_suffix @@ -73,7 +74,7 @@ class IdfPytestEmbedded: self._single_target_duplicate_mode = single_target_duplicate_mode self.apps_list = ( - [os.path.join(app.app_dir, app.build_dir) for app in apps if app.build_status == BuildStatus.SUCCESS] + [os.path.join(idf_relpath(app.app_dir), app.build_dir) for app in apps if app.build_status == BuildStatus.SUCCESS] if apps else None ) @@ -114,14 +115,8 @@ class IdfPytestEmbedded: configs = to_list(parse_multi_dut_args(count, self.get_param(item, 'config', DEFAULT_SDKCONFIG))) targets = to_list(parse_multi_dut_args(count, self.get_param(item, 'target', self.target[0]))) - def abspath_or_relpath(s: str) -> str: - if os.path.abspath(s) and s.startswith(os.getcwd()): - return os.path.relpath(s) - - return s - return PytestCase( - [PytestApp(abspath_or_relpath(app_paths[i]), targets[i], configs[i]) for i in range(count)], item + [PytestApp(app_paths[i], targets[i], configs[i]) for i in range(count)], item ) @pytest.hookimpl(tryfirst=True) diff --git a/tools/ci/idf_pytest/script.py b/tools/ci/idf_pytest/script.py index 7e3232c0c7..dc7a56c4ee 100644 --- a/tools/ci/idf_pytest/script.py +++ b/tools/ci/idf_pytest/script.py @@ -1,6 +1,5 @@ -# SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 - import fnmatch import io import logging @@ -11,14 +10,22 @@ from pathlib import Path import pytest from _pytest.config import ExitCode -from idf_build_apps import App, find_apps -from idf_build_apps.constants import SUPPORTED_TARGETS, BuildStatus +from idf_build_apps import App +from idf_build_apps import find_apps +from idf_build_apps.constants import BuildStatus +from idf_build_apps.constants import SUPPORTED_TARGETS from idf_ci.app import IdfCMakeApp -from idf_ci_utils import IDF_PATH, get_all_manifest_files, to_list +from idf_ci_utils import get_all_manifest_files +from idf_ci_utils import IDF_PATH +from idf_ci_utils import idf_relpath +from idf_ci_utils import to_list from idf_py_actions.constants import PREVIEW_TARGETS as TOOLS_PREVIEW_TARGETS from idf_py_actions.constants import SUPPORTED_TARGETS as TOOLS_SUPPORTED_TARGETS -from .constants import DEFAULT_BUILD_LOG_FILENAME, DEFAULT_CONFIG_RULES_STR, CollectMode, PytestCase +from .constants import CollectMode +from .constants import DEFAULT_BUILD_LOG_FILENAME +from .constants import DEFAULT_CONFIG_RULES_STR +from .constants import PytestCase from .plugin import IdfPytestEmbedded @@ -176,27 +183,30 @@ def get_all_apps( ) # app_path, target, config - pytest_app_path_tuple_dict: t.Dict[t.Tuple[Path, str, str], PytestCase] = {} + pytest_app_path_tuple_dict: t.Dict[t.Tuple[str, str, str], PytestCase] = {} for case in pytest_cases: for app in case.apps: - pytest_app_path_tuple_dict[(Path(app.path), app.target, app.config)] = case + pytest_app_path_tuple_dict[(app.path, app.target, app.config)] = case - modified_pytest_app_path_tuple_dict: t.Dict[t.Tuple[Path, str, str], PytestCase] = {} + modified_pytest_app_path_tuple_dict: t.Dict[t.Tuple[str, str, str], PytestCase] = {} for case in modified_pytest_cases: for app in case.apps: - modified_pytest_app_path_tuple_dict[(Path(app.path), app.target, app.config)] = case + modified_pytest_app_path_tuple_dict[(app.path, app.target, app.config)] = case test_related_apps: t.Set[App] = set() non_test_related_apps: t.Set[App] = set() for app in all_apps: + # PytestCase.app.path is idf_relpath + app_path = idf_relpath(app.app_dir) + # override build_status if test script got modified - if case := modified_pytest_app_path_tuple_dict.get((Path(app.app_dir), app.target, app.config_name)): + if case := modified_pytest_app_path_tuple_dict.get((app_path, app.target, app.config_name)): test_related_apps.add(app) app.build_status = BuildStatus.SHOULD_BE_BUILT app.preserve = True logging.debug('Found app: %s - required by modified test case %s', app, case.path) elif app.build_status != BuildStatus.SKIPPED: - if case := pytest_app_path_tuple_dict.get((Path(app.app_dir), app.target, app.config_name)): + if case := pytest_app_path_tuple_dict.get((app_path, app.target, app.config_name)): test_related_apps.add(app) # should be built if app.build_status = BuildStatus.SHOULD_BE_BUILT