1# SPDX-License-Identifier: BSD-3-Clause 2# Copyright(c) 2010-2021 Intel Corporation 3# Copyright(c) 2022-2023 PANTHEON.tech s.r.o. 4# Copyright(c) 2022 University of New Hampshire 5 6"""Environment variables and command line arguments parsing. 7 8This is a simple module utilizing the built-in argparse module to parse command line arguments, 9augment them with values from environment variables and make them available across the framework. 10 11The command line value takes precedence, followed by the environment variable value, 12followed by the default value defined in this module. 13 14The command line arguments along with the supported environment variables are: 15 16.. option:: --config-file 17.. envvar:: DTS_CFG_FILE 18 19 The path to the YAML test run configuration file. 20 21.. option:: --output-dir, --output 22.. envvar:: DTS_OUTPUT_DIR 23 24 The directory where DTS logs and results are saved. 25 26.. option:: --compile-timeout 27.. envvar:: DTS_COMPILE_TIMEOUT 28 29 The timeout for compiling DPDK. 30 31.. option:: -t, --timeout 32.. envvar:: DTS_TIMEOUT 33 34 The timeout for all DTS operation except for compiling DPDK. 35 36.. option:: -v, --verbose 37.. envvar:: DTS_VERBOSE 38 39 Set to any value to enable logging everything to the console. 40 41.. option:: -s, --skip-setup 42.. envvar:: DTS_SKIP_SETUP 43 44 Set to any value to skip building DPDK. 45 46.. option:: --tarball, --snapshot, --git-ref 47.. envvar:: DTS_DPDK_TARBALL 48 49 The path to a DPDK tarball, git commit ID, tag ID or tree ID to test. 50 51.. option:: --test-cases 52.. envvar:: DTS_TESTCASES 53 54 A comma-separated list of test cases to execute. Unknown test cases will be silently ignored. 55 56.. option:: --re-run, --re_run 57.. envvar:: DTS_RERUN 58 59 Re-run each test case this many times in case of a failure. 60 61The module provides one key module-level variable: 62 63Attributes: 64 SETTINGS: The module level variable storing framework-wide DTS settings. 65 66Typical usage example:: 67 68 from framework.settings import SETTINGS 69 foo = SETTINGS.foo 70""" 71 72import argparse 73import os 74from collections.abc import Callable, Iterable, Sequence 75from dataclasses import dataclass, field 76from pathlib import Path 77from typing import Any, TypeVar 78 79from .utils import DPDKGitTarball 80 81_T = TypeVar("_T") 82 83 84def _env_arg(env_var: str) -> Any: 85 """A helper method augmenting the argparse Action with environment variables. 86 87 If the supplied environment variable is defined, then the default value 88 of the argument is modified. This satisfies the priority order of 89 command line argument > environment variable > default value. 90 91 Arguments with no values (flags) should be defined using the const keyword argument 92 (True or False). When the argument is specified, it will be set to const, if not specified, 93 the default will be stored (possibly modified by the corresponding environment variable). 94 95 Other arguments work the same as default argparse arguments, that is using 96 the default 'store' action. 97 98 Returns: 99 The modified argparse.Action. 100 """ 101 102 class _EnvironmentArgument(argparse.Action): 103 def __init__( 104 self, 105 option_strings: Sequence[str], 106 dest: str, 107 nargs: str | int | None = None, 108 const: bool | None = None, 109 default: Any = None, 110 type: Callable[[str], _T | argparse.FileType | None] = None, 111 choices: Iterable[_T] | None = None, 112 required: bool = False, 113 help: str | None = None, 114 metavar: str | tuple[str, ...] | None = None, 115 ) -> None: 116 env_var_value = os.environ.get(env_var) 117 default = env_var_value or default 118 if const is not None: 119 nargs = 0 120 default = const if env_var_value else default 121 type = None 122 choices = None 123 metavar = None 124 super(_EnvironmentArgument, self).__init__( 125 option_strings, 126 dest, 127 nargs=nargs, 128 const=const, 129 default=default, 130 type=type, 131 choices=choices, 132 required=required, 133 help=help, 134 metavar=metavar, 135 ) 136 137 def __call__( 138 self, 139 parser: argparse.ArgumentParser, 140 namespace: argparse.Namespace, 141 values: Any, 142 option_string: str = None, 143 ) -> None: 144 if self.const is not None: 145 setattr(namespace, self.dest, self.const) 146 else: 147 setattr(namespace, self.dest, values) 148 149 return _EnvironmentArgument 150 151 152@dataclass(slots=True) 153class Settings: 154 """Default framework-wide user settings. 155 156 The defaults may be modified at the start of the run. 157 """ 158 159 #: 160 config_file_path: Path = Path(__file__).parent.parent.joinpath("conf.yaml") 161 #: 162 output_dir: str = "output" 163 #: 164 timeout: float = 15 165 #: 166 verbose: bool = False 167 #: 168 skip_setup: bool = False 169 #: 170 dpdk_tarball_path: Path | str = "dpdk.tar.xz" 171 #: 172 compile_timeout: float = 1200 173 #: 174 test_cases: list[str] = field(default_factory=list) 175 #: 176 re_run: int = 0 177 178 179SETTINGS: Settings = Settings() 180 181 182def _get_parser() -> argparse.ArgumentParser: 183 parser = argparse.ArgumentParser( 184 description="Run DPDK test suites. All options may be specified with the environment " 185 "variables provided in brackets. Command line arguments have higher priority.", 186 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 187 ) 188 189 parser.add_argument( 190 "--config-file", 191 action=_env_arg("DTS_CFG_FILE"), 192 default=SETTINGS.config_file_path, 193 type=Path, 194 help="[DTS_CFG_FILE] configuration file that describes the test cases, SUTs and targets.", 195 ) 196 197 parser.add_argument( 198 "--output-dir", 199 "--output", 200 action=_env_arg("DTS_OUTPUT_DIR"), 201 default=SETTINGS.output_dir, 202 help="[DTS_OUTPUT_DIR] Output directory where DTS logs and results are saved.", 203 ) 204 205 parser.add_argument( 206 "-t", 207 "--timeout", 208 action=_env_arg("DTS_TIMEOUT"), 209 default=SETTINGS.timeout, 210 type=float, 211 help="[DTS_TIMEOUT] The default timeout for all DTS operations except for compiling DPDK.", 212 ) 213 214 parser.add_argument( 215 "-v", 216 "--verbose", 217 action=_env_arg("DTS_VERBOSE"), 218 default=SETTINGS.verbose, 219 const=True, 220 help="[DTS_VERBOSE] Specify to enable verbose output, logging all messages " 221 "to the console.", 222 ) 223 224 parser.add_argument( 225 "-s", 226 "--skip-setup", 227 action=_env_arg("DTS_SKIP_SETUP"), 228 const=True, 229 help="[DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG nodes.", 230 ) 231 232 parser.add_argument( 233 "--tarball", 234 "--snapshot", 235 "--git-ref", 236 action=_env_arg("DTS_DPDK_TARBALL"), 237 default=SETTINGS.dpdk_tarball_path, 238 type=Path, 239 help="[DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git commit ID, " 240 "tag ID or tree ID to test. To test local changes, first commit them, " 241 "then use the commit ID with this option.", 242 ) 243 244 parser.add_argument( 245 "--compile-timeout", 246 action=_env_arg("DTS_COMPILE_TIMEOUT"), 247 default=SETTINGS.compile_timeout, 248 type=float, 249 help="[DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK.", 250 ) 251 252 parser.add_argument( 253 "--test-cases", 254 action=_env_arg("DTS_TESTCASES"), 255 default="", 256 help="[DTS_TESTCASES] Comma-separated list of test cases to execute. " 257 "Unknown test cases will be silently ignored.", 258 ) 259 260 parser.add_argument( 261 "--re-run", 262 "--re_run", 263 action=_env_arg("DTS_RERUN"), 264 default=SETTINGS.re_run, 265 type=int, 266 help="[DTS_RERUN] Re-run each test case the specified number of times " 267 "if a test failure occurs", 268 ) 269 270 return parser 271 272 273def get_settings() -> Settings: 274 """Create new settings with inputs from the user. 275 276 The inputs are taken from the command line and from environment variables. 277 """ 278 parsed_args = _get_parser().parse_args() 279 return Settings( 280 config_file_path=parsed_args.config_file, 281 output_dir=parsed_args.output_dir, 282 timeout=parsed_args.timeout, 283 verbose=parsed_args.verbose, 284 skip_setup=parsed_args.skip_setup, 285 dpdk_tarball_path=Path( 286 Path(DPDKGitTarball(parsed_args.tarball, parsed_args.output_dir)) 287 if not os.path.exists(parsed_args.tarball) 288 else Path(parsed_args.tarball) 289 ), 290 compile_timeout=parsed_args.compile_timeout, 291 test_cases=(parsed_args.test_cases.split(",") if parsed_args.test_cases else []), 292 re_run=parsed_args.re_run, 293 ) 294