For the whole series:

Reviewed-by: Jeremy Spewock <jspew...@iol.unh.edu>

On Fri, Mar 1, 2024 at 5:55 AM Juraj Linkeš <juraj.lin...@pantheon.tech> wrote:
>
> The two places where we specify which test suite and test cases to run
> are complimentary and not that intuitive to use. A unified way provides
> a better user experience.
>
> The syntax in test run configuration file has not changed, but the
> environment variable and the command line arguments was changed to match
> the config file syntax. This required changes in the settings module
> which greatly simplified the parsing of the environment variables while
> retaining the same functionality.
>
> Signed-off-by: Juraj Linkeš <juraj.lin...@pantheon.tech>
> ---
>  doc/guides/tools/dts.rst         |  14 ++-
>  dts/framework/config/__init__.py |  12 +-
>  dts/framework/runner.py          |  18 +--
>  dts/framework/settings.py        | 187 ++++++++++++++-----------------
>  dts/framework/test_suite.py      |   2 +-
>  5 files changed, 114 insertions(+), 119 deletions(-)
>
> diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst
> index f686ca487c..d1c3c2af7a 100644
> --- a/doc/guides/tools/dts.rst
> +++ b/doc/guides/tools/dts.rst
> @@ -215,28 +215,30 @@ DTS is run with ``main.py`` located in the ``dts`` 
> directory after entering Poet
>  .. code-block:: console
>
>     (dts-py3.10) $ ./main.py --help
> -   usage: main.py [-h] [--config-file CONFIG_FILE] [--output-dir OUTPUT_DIR] 
> [-t TIMEOUT] [-v] [-s] [--tarball TARBALL] [--compile-timeout 
> COMPILE_TIMEOUT] [--test-cases TEST_CASES] [--re-run RE_RUN]
> +   usage: main.py [-h] [--config-file CONFIG_FILE] [--output-dir OUTPUT_DIR] 
> [-t TIMEOUT] [-v] [-s] [--tarball TARBALL] [--compile-timeout 
> COMPILE_TIMEOUT] [--test-suite TEST_SUITE [TEST_CASES ...]] [--re-run RE_RUN]
>
>     Run DPDK test suites. All options may be specified with the environment 
> variables provided in brackets. Command line arguments have higher priority.
>
>     options:
>     -h, --help            show this help message and exit
>     --config-file CONFIG_FILE
> -                         [DTS_CFG_FILE] configuration file that describes 
> the test cases, SUTs and targets. (default: ./conf.yaml)
> +                         [DTS_CFG_FILE] The configuration file that 
> describes the test cases, SUTs and targets. (default: ./conf.yaml)
>     --output-dir OUTPUT_DIR, --output OUTPUT_DIR
>                           [DTS_OUTPUT_DIR] Output directory where DTS logs 
> and results are saved. (default: output)
>     -t TIMEOUT, --timeout TIMEOUT
>                           [DTS_TIMEOUT] The default timeout for all DTS 
> operations except for compiling DPDK. (default: 15)
>     -v, --verbose         [DTS_VERBOSE] Specify to enable verbose output, 
> logging all messages to the console. (default: False)
> -   -s, --skip-setup      [DTS_SKIP_SETUP] Specify to skip all setup steps on 
> SUT and TG nodes. (default: None)
> +   -s, --skip-setup      [DTS_SKIP_SETUP] Specify to skip all setup steps on 
> SUT and TG nodes. (default: False)
>     --tarball TARBALL, --snapshot TARBALL, --git-ref TARBALL
>                           [DTS_DPDK_TARBALL] Path to DPDK source code tarball 
> or a git commit ID, tag ID or tree ID to test. To test local changes, first 
> commit them, then use the commit ID with this option. (default: dpdk.tar.xz)
>     --compile-timeout COMPILE_TIMEOUT
>                           [DTS_COMPILE_TIMEOUT] The timeout for compiling 
> DPDK. (default: 1200)
> -   --test-cases TEST_CASES
> -                         [DTS_TESTCASES] Comma-separated list of test cases 
> to execute. Unknown test cases will be silently ignored. (default: )
> +   --test-suite TEST_SUITE [TEST_CASES ...]
> +                         [DTS_TEST_SUITES] A list containing a test suite 
> with test cases. The first parameter is the test suite name, and the rest are 
> test case names, which are optional. May be specified multiple times. To 
> specify multiple test suites in the environment
> +                         variable, join the lists with a comma. Examples: 
> --test-suite suite case case --test-suite suite case ... | 
> DTS_TEST_SUITES='suite case case, suite case, ...' | --test-suite suite 
> --test-suite suite case ... | DTS_TEST_SUITES='suite, suite case, ...'
> +                         (default: [])
>     --re-run RE_RUN, --re_run RE_RUN
> -                         [DTS_RERUN] Re-run each test case the specified 
> number of times if a test failure occurs (default: 0)
> +                         [DTS_RERUN] Re-run each test case the specified 
> number of times if a test failure occurs. (default: 0)
>
>
>  The brackets contain the names of environment variables that set the same 
> thing.
> diff --git a/dts/framework/config/__init__.py 
> b/dts/framework/config/__init__.py
> index c6a93b3b89..4cb5c74059 100644
> --- a/dts/framework/config/__init__.py
> +++ b/dts/framework/config/__init__.py
> @@ -35,9 +35,9 @@
>
>  import json
>  import os.path
> -import pathlib
>  from dataclasses import dataclass, fields
>  from enum import auto, unique
> +from pathlib import Path
>  from typing import Union
>
>  import warlock  # type: ignore[import]
> @@ -53,7 +53,6 @@
>      TrafficGeneratorConfigDict,
>  )
>  from framework.exception import ConfigurationError
> -from framework.settings import SETTINGS
>  from framework.utils import StrEnum
>
>
> @@ -571,7 +570,7 @@ def from_dict(d: ConfigurationDict) -> "Configuration":
>          return Configuration(executions=executions)
>
>
> -def load_config() -> Configuration:
> +def load_config(config_file_path: Path) -> Configuration:
>      """Load DTS test run configuration from a file.
>
>      Load the YAML test run configuration file
> @@ -581,13 +580,16 @@ def load_config() -> Configuration:
>      The YAML test run configuration file is specified in the 
> :option:`--config-file` command line
>      argument or the :envvar:`DTS_CFG_FILE` environment variable.
>
> +    Args:
> +        config_file_path: The path to the YAML test run configuration file.
> +
>      Returns:
>          The parsed test run configuration.
>      """
> -    with open(SETTINGS.config_file_path, "r") as f:
> +    with open(config_file_path, "r") as f:
>          config_data = yaml.safe_load(f)
>
> -    schema_path = os.path.join(pathlib.Path(__file__).parent.resolve(), 
> "conf_yaml_schema.json")
> +    schema_path = os.path.join(Path(__file__).parent.resolve(), 
> "conf_yaml_schema.json")
>
>      with open(schema_path, "r") as f:
>          schema = json.load(f)
> diff --git a/dts/framework/runner.py b/dts/framework/runner.py
> index dfee8ebd7c..db8e3ba96b 100644
> --- a/dts/framework/runner.py
> +++ b/dts/framework/runner.py
> @@ -24,7 +24,7 @@
>  import sys
>  from pathlib import Path
>  from types import MethodType
> -from typing import Iterable
> +from typing import Iterable, Sequence
>
>  from .config import (
>      BuildTargetConfiguration,
> @@ -83,7 +83,7 @@ class DTSRunner:
>
>      def __init__(self):
>          """Initialize the instance with configuration, logger, result and 
> string constants."""
> -        self._configuration = load_config()
> +        self._configuration = load_config(SETTINGS.config_file_path)
>          self._logger = get_dts_logger()
>          if not os.path.exists(SETTINGS.output_dir):
>              os.makedirs(SETTINGS.output_dir)
> @@ -129,7 +129,7 @@ def run(self):
>              #. Execution teardown
>
>          The test cases are filtered according to the specification in the 
> test run configuration and
> -        the :option:`--test-cases` command line argument or
> +        the :option:`--test-suite` command line argument or
>          the :envvar:`DTS_TESTCASES` environment variable.
>          """
>          sut_nodes: dict[str, SutNode] = {}
> @@ -147,7 +147,9 @@ def run(self):
>                  )
>                  execution_result = self._result.add_execution(execution)
>                  # we don't want to modify the original config, so create a 
> copy
> -                execution_test_suites = list(execution.test_suites)
> +                execution_test_suites = list(
> +                    SETTINGS.test_suites if SETTINGS.test_suites else 
> execution.test_suites
> +                )
>                  if not execution.skip_smoke_tests:
>                      execution_test_suites[:0] = 
> [TestSuiteConfig.from_dict("smoke_tests")]
>                  try:
> @@ -226,7 +228,7 @@ def _get_test_suites_with_cases(
>              test_suite_class = 
> self._get_test_suite_class(test_suite_config.test_suite)
>              test_cases = []
>              func_test_cases, perf_test_cases = self._filter_test_cases(
> -                test_suite_class, set(test_suite_config.test_cases + 
> SETTINGS.test_cases)
> +                test_suite_class, test_suite_config.test_cases
>              )
>              if func:
>                  test_cases.extend(func_test_cases)
> @@ -302,7 +304,7 @@ def is_test_suite(object) -> bool:
>          )
>
>      def _filter_test_cases(
> -        self, test_suite_class: type[TestSuite], test_cases_to_run: set[str]
> +        self, test_suite_class: type[TestSuite], test_cases_to_run: 
> Sequence[str]
>      ) -> tuple[list[MethodType], list[MethodType]]:
>          """Filter `test_cases_to_run` from `test_suite_class`.
>
> @@ -331,7 +333,9 @@ def _filter_test_cases(
>                  (name, method) for name, method in name_method_tuples if 
> name in test_cases_to_run
>              ]
>              if len(name_method_tuples) < len(test_cases_to_run):
> -                missing_test_cases = test_cases_to_run - {name for name, _ 
> in name_method_tuples}
> +                missing_test_cases = set(test_cases_to_run) - {
> +                    name for name, _ in name_method_tuples
> +                }
>                  raise ConfigurationError(
>                      f"Test cases {missing_test_cases} not found among 
> methods "
>                      f"of {test_suite_class.__name__}."
> diff --git a/dts/framework/settings.py b/dts/framework/settings.py
> index 2b8bfbe0ed..688e8679a7 100644
> --- a/dts/framework/settings.py
> +++ b/dts/framework/settings.py
> @@ -48,10 +48,11 @@
>
>      The path to a DPDK tarball, git commit ID, tag ID or tree ID to test.
>
> -.. option:: --test-cases
> -.. envvar:: DTS_TESTCASES
> +.. option:: --test-suite
> +.. envvar:: DTS_TEST_SUITES
>
> -    A comma-separated list of test cases to execute. Unknown test cases will 
> be silently ignored.
> +        A test suite with test cases which may be specified multiple times.
> +        In the environment variable, the suites are joined with a comma.
>
>  .. option:: --re-run, --re_run
>  .. envvar:: DTS_RERUN
> @@ -71,83 +72,13 @@
>
>  import argparse
>  import os
> -from collections.abc import Callable, Iterable, Sequence
>  from dataclasses import dataclass, field
>  from pathlib import Path
> -from typing import Any, TypeVar
> +from typing import Any
>
> +from .config import TestSuiteConfig
>  from .utils import DPDKGitTarball
>
> -_T = TypeVar("_T")
> -
> -
> -def _env_arg(env_var: str) -> Any:
> -    """A helper method augmenting the argparse Action with environment 
> variables.
> -
> -    If the supplied environment variable is defined, then the default value
> -    of the argument is modified. This satisfies the priority order of
> -    command line argument > environment variable > default value.
> -
> -    Arguments with no values (flags) should be defined using the const 
> keyword argument
> -    (True or False). When the argument is specified, it will be set to 
> const, if not specified,
> -    the default will be stored (possibly modified by the corresponding 
> environment variable).
> -
> -    Other arguments work the same as default argparse arguments, that is 
> using
> -    the default 'store' action.
> -
> -    Returns:
> -          The modified argparse.Action.
> -    """
> -
> -    class _EnvironmentArgument(argparse.Action):
> -        def __init__(
> -            self,
> -            option_strings: Sequence[str],
> -            dest: str,
> -            nargs: str | int | None = None,
> -            const: bool | None = None,
> -            default: Any = None,
> -            type: Callable[[str], _T | argparse.FileType | None] = None,
> -            choices: Iterable[_T] | None = None,
> -            required: bool = False,
> -            help: str | None = None,
> -            metavar: str | tuple[str, ...] | None = None,
> -        ) -> None:
> -            env_var_value = os.environ.get(env_var)
> -            default = env_var_value or default
> -            if const is not None:
> -                nargs = 0
> -                default = const if env_var_value else default
> -                type = None
> -                choices = None
> -                metavar = None
> -            super(_EnvironmentArgument, self).__init__(
> -                option_strings,
> -                dest,
> -                nargs=nargs,
> -                const=const,
> -                default=default,
> -                type=type,
> -                choices=choices,
> -                required=required,
> -                help=help,
> -                metavar=metavar,
> -            )
> -
> -        def __call__(
> -            self,
> -            parser: argparse.ArgumentParser,
> -            namespace: argparse.Namespace,
> -            values: Any,
> -            option_string: str = None,
> -        ) -> None:
> -            if self.const is not None:
> -                setattr(namespace, self.dest, self.const)
> -            else:
> -                setattr(namespace, self.dest, values)
> -
> -    return _EnvironmentArgument
> -
>
>  @dataclass(slots=True)
>  class Settings:
> @@ -171,7 +102,7 @@ class Settings:
>      #:
>      compile_timeout: float = 1200
>      #:
> -    test_cases: list[str] = field(default_factory=list)
> +    test_suites: list[TestSuiteConfig] = field(default_factory=list)
>      #:
>      re_run: int = 0
>
> @@ -180,6 +111,31 @@ class Settings:
>
>
>  def _get_parser() -> argparse.ArgumentParser:
> +    """Create the argument parser for DTS.
> +
> +    Command line options take precedence over environment variables, which 
> in turn take precedence
> +    over default values.
> +
> +    Returns:
> +        argparse.ArgumentParser: The configured argument parser with defined 
> options.
> +    """
> +
> +    def env_arg(env_var: str, default: Any) -> Any:
> +        """A helper function augmenting the argparse with environment 
> variables.
> +
> +        If the supplied environment variable is defined, then the default 
> value
> +        of the argument is modified. This satisfies the priority order of
> +        command line argument > environment variable > default value.
> +
> +        Args:
> +            env_var: Environment variable name.
> +            default: Default value.
> +
> +        Returns:
> +            Environment variable or default value.
> +        """
> +        return os.environ.get(env_var) or default
> +
>      parser = argparse.ArgumentParser(
>          description="Run DPDK test suites. All options may be specified with 
> the environment "
>          "variables provided in brackets. Command line arguments have higher 
> priority.",
> @@ -188,25 +144,23 @@ def _get_parser() -> argparse.ArgumentParser:
>
>      parser.add_argument(
>          "--config-file",
> -        action=_env_arg("DTS_CFG_FILE"),
> -        default=SETTINGS.config_file_path,
> +        default=env_arg("DTS_CFG_FILE", SETTINGS.config_file_path),
>          type=Path,
> -        help="[DTS_CFG_FILE] configuration file that describes the test 
> cases, SUTs and targets.",
> +        help="[DTS_CFG_FILE] The configuration file that describes the test 
> cases, "
> +        "SUTs and targets.",
>      )
>
>      parser.add_argument(
>          "--output-dir",
>          "--output",
> -        action=_env_arg("DTS_OUTPUT_DIR"),
> -        default=SETTINGS.output_dir,
> +        default=env_arg("DTS_OUTPUT_DIR", SETTINGS.output_dir),
>          help="[DTS_OUTPUT_DIR] Output directory where DTS logs and results 
> are saved.",
>      )
>
>      parser.add_argument(
>          "-t",
>          "--timeout",
> -        action=_env_arg("DTS_TIMEOUT"),
> -        default=SETTINGS.timeout,
> +        default=env_arg("DTS_TIMEOUT", SETTINGS.timeout),
>          type=float,
>          help="[DTS_TIMEOUT] The default timeout for all DTS operations 
> except for compiling DPDK.",
>      )
> @@ -214,9 +168,8 @@ def _get_parser() -> argparse.ArgumentParser:
>      parser.add_argument(
>          "-v",
>          "--verbose",
> -        action=_env_arg("DTS_VERBOSE"),
> -        default=SETTINGS.verbose,
> -        const=True,
> +        action="store_true",
> +        default=env_arg("DTS_VERBOSE", SETTINGS.verbose),
>          help="[DTS_VERBOSE] Specify to enable verbose output, logging all 
> messages "
>          "to the console.",
>      )
> @@ -224,8 +177,8 @@ def _get_parser() -> argparse.ArgumentParser:
>      parser.add_argument(
>          "-s",
>          "--skip-setup",
> -        action=_env_arg("DTS_SKIP_SETUP"),
> -        const=True,
> +        action="store_true",
> +        default=env_arg("DTS_SKIP_SETUP", SETTINGS.skip_setup),
>          help="[DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG 
> nodes.",
>      )
>
> @@ -233,8 +186,7 @@ def _get_parser() -> argparse.ArgumentParser:
>          "--tarball",
>          "--snapshot",
>          "--git-ref",
> -        action=_env_arg("DTS_DPDK_TARBALL"),
> -        default=SETTINGS.dpdk_tarball_path,
> +        default=env_arg("DTS_DPDK_TARBALL", SETTINGS.dpdk_tarball_path),
>          type=Path,
>          help="[DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git 
> commit ID, "
>          "tag ID or tree ID to test. To test local changes, first commit 
> them, "
> @@ -243,36 +195,71 @@ def _get_parser() -> argparse.ArgumentParser:
>
>      parser.add_argument(
>          "--compile-timeout",
> -        action=_env_arg("DTS_COMPILE_TIMEOUT"),
> -        default=SETTINGS.compile_timeout,
> +        default=env_arg("DTS_COMPILE_TIMEOUT", SETTINGS.compile_timeout),
>          type=float,
>          help="[DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK.",
>      )
>
>      parser.add_argument(
> -        "--test-cases",
> -        action=_env_arg("DTS_TESTCASES"),
> -        default="",
> -        help="[DTS_TESTCASES] Comma-separated list of test cases to 
> execute.",
> +        "--test-suite",
> +        action="append",
> +        nargs="+",
> +        metavar=("TEST_SUITE", "TEST_CASES"),
> +        default=env_arg("DTS_TEST_SUITES", SETTINGS.test_suites),
> +        help="[DTS_TEST_SUITES] A list containing a test suite with test 
> cases. "
> +        "The first parameter is the test suite name, and the rest are test 
> case names, "
> +        "which are optional. May be specified multiple times. To specify 
> multiple test suites in "
> +        "the environment variable, join the lists with a comma. "
> +        "Examples: "
> +        "--test-suite suite case case --test-suite suite case ... | "
> +        "DTS_TEST_SUITES='suite case case, suite case, ...' | "
> +        "--test-suite suite --test-suite suite case ... | "
> +        "DTS_TEST_SUITES='suite, suite case, ...'",
>      )
>
>      parser.add_argument(
>          "--re-run",
>          "--re_run",
> -        action=_env_arg("DTS_RERUN"),
> -        default=SETTINGS.re_run,
> +        default=env_arg("DTS_RERUN", SETTINGS.re_run),
>          type=int,
>          help="[DTS_RERUN] Re-run each test case the specified number of 
> times "
> -        "if a test failure occurs",
> +        "if a test failure occurs.",
>      )
>
>      return parser
>
>
> +def _process_test_suites(args: str | list[list[str]]) -> 
> list[TestSuiteConfig]:
> +    """Process the given argument to a list of :class:`TestSuiteConfig` to 
> execute.
> +
> +    Args:
> +        args: The arguments to process. The args is a string from an 
> environment variable
> +              or a list of from the user input containing tests suites with 
> tests cases,
> +              each of which is a list of [test_suite, test_case, test_case, 
> ...].
> +
> +    Returns:
> +        A list of test suite configurations to execute.
> +    """
> +    if isinstance(args, str):
> +        # Environment variable in the form of "suite case case, suite case, 
> suite, ..."
> +        args = [suite_with_cases.split() for suite_with_cases in 
> args.split(",")]
> +
> +    test_suites_to_run = []
> +    for suite_with_cases in args:
> +        test_suites_to_run.append(
> +            TestSuiteConfig(test_suite=suite_with_cases[0], 
> test_cases=suite_with_cases[1:])
> +        )
> +
> +    return test_suites_to_run
> +
> +
>  def get_settings() -> Settings:
>      """Create new settings with inputs from the user.
>
>      The inputs are taken from the command line and from environment 
> variables.
> +
> +    Returns:
> +        The new settings object.
>      """
>      parsed_args = _get_parser().parse_args()
>      return Settings(
> @@ -287,6 +274,6 @@ def get_settings() -> Settings:
>              else Path(parsed_args.tarball)
>          ),
>          compile_timeout=parsed_args.compile_timeout,
> -        test_cases=(parsed_args.test_cases.split(",") if 
> parsed_args.test_cases else []),
> +        test_suites=_process_test_suites(parsed_args.test_suite),
>          re_run=parsed_args.re_run,
>      )
> diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
> index 365f80e21a..1957ea7328 100644
> --- a/dts/framework/test_suite.py
> +++ b/dts/framework/test_suite.py
> @@ -40,7 +40,7 @@ class TestSuite(object):
>      and functional test cases (all other test cases).
>
>      By default, all test cases will be executed. A list of testcase names 
> may be specified
> -    in the YAML test run configuration file and in the 
> :option:`--test-cases` command line argument
> +    in the YAML test run configuration file and in the 
> :option:`--test-suite` command line argument
>      or in the :envvar:`DTS_TESTCASES` environment variable to filter which 
> test cases to run.
>      The union of both lists will be used. Any unknown test cases from the 
> latter lists
>      will be silently ignored.
> --
> 2.34.1
>

Reply via email to