1#!/usr/bin/env python3 2# -- coding: utf-8 -- 3# 4# Copyright (c) 2024-2025 Huawei Device Co., Ltd. 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18import argparse 19from functools import cached_property 20from pathlib import Path 21from typing import Any, cast 22 23from runner.common_exceptions import InvalidConfiguration 24from runner.logger import Log 25from runner.options.macros import Macros, ParameterNotFound 26from runner.options.options import IOptions 27from runner.options.options_collections import CollectionsOptions 28from runner.options.options_ets import ETSOptions 29from runner.options.options_general import GeneralOptions 30from runner.options.options_groups import GroupsOptions 31from runner.options.options_test_lists import TestListsOptions 32from runner.utils import check_int, convert_underscore, extract_parameter_name 33 34_LOGGER = Log.get_logger(__file__) 35 36 37class TestSuiteOptions(IOptions): 38 __DATA = "data" 39 __TEST_SUITE = "test-suite" 40 41 __PROPERTIES = "properties" 42 __COLLECTIONS = "collections" 43 __LIST_ROOT = "list-root" 44 __TEST_ROOT = "test-root" 45 46 __PARAMETERS = "parameters" 47 __EXTENSION = "extension" 48 __DEFAULT_EXTENSION = "sts" 49 __FILTER = "filter" 50 __DEFAULT_FILTER = "*" 51 __LOAD_RUNTIMES = "load-runtimes" 52 __DEFAULT_LOAD_RUNTIMES = "ets" 53 __REPEATS = "repeats" 54 __DEFAULT_REPEATS = 1 55 __REPEATS_BY_TIME = "repeats-by-time" 56 __DEFAULT_REPEATS_BY_TIME = 0 57 __WITH_JS = "with-js" 58 __DEFAULT_WITH_JS = False 59 __WORK_DIR = "work-dir" 60 61 def __init__(self, args: dict[str, Any], parent: IOptions): # type: ignore[explicit-any] 62 super().__init__(None) 63 self.__name: str = cast(str, args[self.__TEST_SUITE]) 64 if not isinstance(parent, GeneralOptions): 65 raise InvalidConfiguration( 66 "Incorrect configuration: test suite parent is not GeneralOptions") 67 self._parent: GeneralOptions = parent 68 self.__parameters: dict[str, Any] = self.__process_parameters(args) # type: ignore[explicit-any] 69 self.__data = args[f"{self.__name}.data"] 70 self.__default_list_root: Path = self._parent.static_core_root / 'tests' / 'tests-u-runner' / 'test-lists' 71 self.__list_root: str | None = self.__data[self.__LIST_ROOT] \ 72 if self.__data[self.__LIST_ROOT] else str(self.__default_list_root) 73 self.__test_root: str | None = self.__data[self.__TEST_ROOT] \ 74 if self.__data[self.__TEST_ROOT] else None 75 self.__collections: list[CollectionsOptions] = [] 76 self.test_lists = TestListsOptions(self.__parameters) 77 self.ets = ETSOptions(self.__parameters) 78 self.groups = GroupsOptions(self.__parameters) 79 self.__expand_macros_in_parameters() 80 self.__fill_collections() 81 self.__expand_macros_collections(self._parent) 82 83 def __str__(self) -> str: 84 return self._to_str(indent=1) 85 86 @property 87 def list_root(self) -> Path: 88 if self.__list_root is None: 89 raise InvalidConfiguration("List-root is not specified") 90 return Path(self.__list_root) 91 92 @property 93 def test_root(self) -> Path: 94 if self.__test_root is None: 95 raise InvalidConfiguration("Test-root is not specified") 96 return Path(self.__test_root) 97 98 @property 99 def work_dir(self) -> str: 100 if self.__WORK_DIR not in self.__parameters: 101 raise InvalidConfiguration("work-dir is not specified") 102 return str(self.__parameters[self.__WORK_DIR]) 103 104 @staticmethod 105 def add_cli_args(parser: argparse.ArgumentParser) -> None: 106 parser.add_argument( 107 f'--{TestSuiteOptions.__FILTER}', '-f', action='store', 108 default=TestSuiteOptions.__DEFAULT_FILTER, 109 help=f'test filter wildcard. By default \'{TestSuiteOptions.__DEFAULT_FILTER}\'') 110 repeats_group = parser.add_mutually_exclusive_group(required=False) 111 repeats_group.add_argument( 112 f'--{TestSuiteOptions.__REPEATS}', action='store', 113 type=lambda arg: check_int(arg, f"--{TestSuiteOptions.__REPEATS}", is_zero_allowed=False), 114 default=TestSuiteOptions.__DEFAULT_REPEATS, 115 help=f'how many times to repeat the suite entirely. By default {TestSuiteOptions.__DEFAULT_REPEATS}') 116 repeats_group.add_argument( 117 f'--{TestSuiteOptions.__REPEATS_BY_TIME}', action='store', 118 type=lambda arg: check_int(arg, f"--{TestSuiteOptions.__REPEATS_BY_TIME}", is_zero_allowed=True), 119 default=TestSuiteOptions.__DEFAULT_REPEATS_BY_TIME, 120 help=f'number of seconds during which the suite is repeated. ' 121 f'Number of repeats is always integer. By default {TestSuiteOptions.__DEFAULT_REPEATS_BY_TIME}') 122 parser.add_argument( 123 f'--{TestSuiteOptions.__WITH_JS}', action='store_true', 124 default=TestSuiteOptions.__WITH_JS, 125 help='enable JS-related tests') 126 127 TestListsOptions.add_cli_args(parser) 128 ETSOptions.add_cli_args(parser) 129 GroupsOptions.add_cli_args(parser) 130 131 @staticmethod 132 def __fill_collection(content: dict) -> dict: 133 for _, prop_value in content.items(): 134 if isinstance(prop_value, dict): 135 TestSuiteOptions.__fill_collection_props(prop_value) 136 return content 137 138 @staticmethod 139 def __fill_collection_props(prop_value: dict) -> None: 140 for sub_key, sub_value in prop_value.items(): 141 if isinstance(sub_value, list): 142 prop_value[sub_key] = " ".join(sub_value) 143 144 @cached_property 145 def suite_name(self) -> str: 146 return self.__name 147 148 @cached_property 149 def collections(self) -> list[CollectionsOptions]: 150 return self.__collections 151 152 @cached_property 153 def repeats(self) -> int: 154 return int(self.__parameters.get(self.__REPEATS, self.__DEFAULT_REPEATS)) 155 156 @cached_property 157 def repeats_by_time(self) -> int: 158 return cast(int, self.__parameters.get(self.__REPEATS_BY_TIME, self.__DEFAULT_REPEATS_BY_TIME)) 159 160 @cached_property 161 def filter(self) -> str: 162 return str(self.__parameters.get(self.__FILTER, self.__DEFAULT_FILTER)) 163 164 @cached_property 165 def parameters(self) -> dict[str, Any]: # type: ignore[explicit-any] 166 return self.__parameters 167 168 def extension(self, collection: CollectionsOptions | None = None) -> str: 169 return str(self.get_parameter(self.__EXTENSION, self.__DEFAULT_EXTENSION, collection)) 170 171 def with_js(self, collection: CollectionsOptions | None = None) -> bool: 172 return bool(self.get_parameter(self.__WITH_JS, self.__DEFAULT_WITH_JS, collection)) 173 174 def load_runtimes(self, collection: CollectionsOptions | None = None) -> str: 175 return str(self.get_parameter(self.__LOAD_RUNTIMES, self.__DEFAULT_LOAD_RUNTIMES, collection)) 176 177 def get_parameter(self, key: str, default: Any | None = None, # type: ignore[explicit-any] 178 collection: CollectionsOptions | None = None) -> Any | None: 179 if collection is not None: 180 value = collection.get_parameter(key, None) 181 if value is not None: 182 return value 183 return self.__parameters.get(key, default) 184 185 def is_defined_in_collections(self, key: str) -> bool: 186 key = extract_parameter_name(key) 187 for collection in self.__collections: 188 if key in collection.parameters: 189 return True 190 return False 191 192 def get_command_line(self) -> str: 193 options = [ 194 f'--test-root={self.test_root}' if self.test_root is not None else '', 195 f'--list-root={self.list_root}' if self.list_root is not None else '', 196 ] 197 return ' '.join(options) 198 199 def __fill_collections(self) -> None: 200 if self.__COLLECTIONS not in self.__data: 201 self.__collections.append(CollectionsOptions("", self.__data, self)) 202 return 203 for coll_name, content in self.__data[self.__COLLECTIONS].items(): 204 if content: 205 content = self.__fill_collection(content) 206 self.__collections.append(CollectionsOptions(coll_name, content, self)) 207 208 def __expand_macros_collections(self, parent: IOptions) -> None: 209 for collection in self.__collections: 210 for coll_param, coll_value in collection.parameters.items(): 211 try: 212 new_coll_value = Macros.correct_macro(coll_value, self) 213 except ParameterNotFound: 214 new_coll_value = Macros.correct_macro(coll_value, parent) 215 216 if new_coll_value != coll_value: 217 collection.parameters[coll_param] = new_coll_value 218 219 def __process_parameters(self, args: dict[str, Any]) -> dict[str, Any]: # type: ignore[explicit-any] 220 result: dict[str, Any] = {} # type: ignore[explicit-any] 221 for param_name, param_value in args.items(): 222 if param_name.startswith(f"{self.__name}.{self.__PARAMETERS}."): 223 param_name = convert_underscore(param_name[(len(self.__name) + len(self.__PARAMETERS) + 2):]) 224 result[param_name] = param_value 225 return result 226 227 def __expand_macros_in_parameters(self) -> None: 228 if self.__list_root is not None: 229 self.__list_root = Macros.expand_macros_in_path(self.__list_root, self) 230 if self.__test_root is not None: 231 self.__test_root = Macros.expand_macros_in_path(self.__test_root, self) 232 for param_name, param_value in self.__parameters.items(): 233 if isinstance(param_value, str): 234 self.__parameters[param_name] = Macros.correct_macro(param_value, self) 235