On Mon, Nov 14, 2022 at 11:54 AM Juraj Linkeš wrote: > This is the base class that all test suites inherit from. The base class > implements methods common to all test suites. The derived test suites > implement tests and any particular setup needed for the suite or tests. > > Signed-off-by: Juraj Linkeš > --- > dts/conf.yaml | 4 + > dts/framework/config/__init__.py | 33 ++- > dts/framework/config/conf_yaml_schema.json | 49 ++++ > dts/framework/dts.py | 29 +++ > dts/framework/exception.py | 65 ++++++ > dts/framework/settings.py | 25 +++ > dts/framework/test_case.py | 246 +++++++++++++++++++++ > 7 files changed, 450 insertions(+), 1 deletion(-) > create mode 100644 dts/framework/test_case.py > > diff --git a/dts/conf.yaml b/dts/conf.yaml > index 976888a88e..0b0f2c59b0 100644 > --- a/dts/conf.yaml > +++ b/dts/conf.yaml > @@ -7,6 +7,10 @@ executions: > os: linux > cpu: native > compiler: gcc > + perf: false > + func: true > + test_suites: > + - hello_world > system_under_test: "SUT 1" > nodes: > - name: "SUT 1" > diff --git a/dts/framework/config/__init__.py > b/dts/framework/config/__init__.py > index 344d697a69..8874b10030 100644 > --- a/dts/framework/config/__init__.py > +++ b/dts/framework/config/__init__.py > @@ -11,7 +11,7 @@ > import pathlib > from dataclasses import dataclass > from enum import Enum, auto, unique > -from typing import Any, Iterable > +from typing import Any, Iterable, TypedDict > > import warlock # type: ignore > import yaml > @@ -186,9 +186,34 @@ def from_dict(d: dict) -> "BuildTargetConfiguration": > ) > > > +class TestSuiteConfigDict(TypedDict): > + suite: str > + cases: list[str] > + > + > +@dataclass(slots=True, frozen=True) > +class TestSuiteConfig: > + test_suite: str > + test_cases: list[str] > + > + @staticmethod > + def from_dict( > + entry: str | TestSuiteConfigDict, > + ) -> "TestSuiteConfig": > + if isinstance(entry, str): > + return TestSuiteConfig(test_suite=entry, test_cases=[]) > + elif isinstance(entry, dict): > + return TestSuiteConfig(test_suite=entry["suite"], > test_cases=entry["cases"]) > + else: > + raise TypeError(f"{type(entry)} is not valid for a test suite > config.") > + > + > @dataclass(slots=True, frozen=True) > class ExecutionConfiguration: > build_targets: list[BuildTargetConfiguration] > + perf: bool > + func: bool > + test_suites: list[TestSuiteConfig] > system_under_test: NodeConfiguration > > @staticmethod > @@ -196,11 +221,17 @@ def from_dict(d: dict, node_map: dict) -> > "ExecutionConfiguration": > build_targets: list[BuildTargetConfiguration] = list( > map(BuildTargetConfiguration.from_dict, d["build_targets"]) > ) > + test_suites: list[TestSuiteConfig] = list( > + map(TestSuiteConfig.from_dict, d["test_suites"]) > + ) > sut_name = d["system_under_test"] > assert sut_name in node_map, f"Unknown SUT {sut_name} in > execution {d}" > > return ExecutionConfiguration( > build_targets=build_targets, > + perf=d["perf"], > + func=d["func"], > + test_suites=test_suites, > system_under_test=node_map[sut_name], > ) > > diff --git a/dts/framework/config/conf_yaml_schema.json > b/dts/framework/config/conf_yaml_schema.json > index c59d3e30e6..e37ced65fe 100644 > --- a/dts/framework/config/conf_yaml_schema.json > +++ b/dts/framework/config/conf_yaml_schema.json > @@ -63,6 +63,31 @@ > } > }, > "additionalProperties": false > + }, > + "test_suite": { > + "type": "string", > + "enum": [ > + "hello_world" > + ] > + }, > + "test_target": { > + "type": "object", > + "properties": { > + "suite": { > + "$ref": "#/definitions/test_suite" > + }, > + "cases": { > + "type": "array", > + "items": { > + "type": "string" > + }, > + "minimum": 1 > + } > + }, > + "required": [ > + "suite" > + ], > + "additionalProperties": false > } > }, > "type": "object", > @@ -130,6 +155,27 @@ > }, > "minimum": 1 > }, > + "perf": { > + "type": "boolean", > + "description": "Enable performance testing" > + }, > + "func": { > + "type": "boolean", > + "description": "Enable functional testing" > + }, > + "test_suites": { > + "type": "array", > + "items": { > + "oneOf": [ > + { > + "$ref": "#/definitions/test_suite" > + }, > + { > + "$ref": "#/definitions/test_target" > + } > + ] > + } > + }, > "system_under_test": { > "$ref": "#/definitions/node_name" > } > @@ -137,6 +183,9 @@ > "additionalProperties": false, > "required": [ > "build_targets", > + "perf", > + "func", > + "test_suites", > "system_under_test" > ] > }, > diff --git a/dts/framework/dts.py b/dts/framework/dts.py > index a7c243a5c3..ba3f4b4168 100644 > --- a/dts/framework/dts.py > +++ b/dts/framework/dts.py > @@ -15,6 +15,7 @@ > from .logger import DTSLOG, getLogger > from .settings import SETTINGS > from .stats_reporter import TestStats > +from .test_case import TestCase > from .test_result import Result > from .utils import check_dts_python_version > > @@ -129,6 +130,34 @@ def run_suite( > Use the given build_target to run the test suite with possibly only a > subset > of tests. If no subset is specified, run all tests. > """ > + for test_suite_config in execution.test_suites: > + result.test_suite = test_suite_config.test_suite > + full_suite_path = > f"tests.TestSuite_{test_suite_config.test_suite}" > + testcase_classes = TestCase.get_testcases(full_suite_path) > + dts_logger.debug( > + f"Found testcase classes '{testcase_classes}' in > '{full_suite_path}'" > + ) > + for testcase_class in testcase_classes: > + testcase = testcase_class( > + sut_node, test_suite_config.test_suite, build_target, > execution > + ) > + > + testcase.init_log() > + testcase.set_requested_cases(SETTINGS.test_cases) > + testcase.set_requested_cases(test_suite_config.test_cases) > + > + dts_logger.info(f"Running test suite > '{testcase_class.__name__}'") > + try: > + testcase.execute_setup_all() > + testcase.execute_test_cases() > + dts_logger.info( > + f"Finished running test suite > '{testcase_class.__name__}'" > + ) > + result.copy_suite(testcase.get_result()) > + test_stats.save(result) # this was originally after > teardown > + > + finally: > You should probably move the "finished" log message down here, so that it always runs. > + testcase.execute_tear_downall() > > > def quit_execution(nodes: Iterable[Node], return_code: ReturnCode) -> > None: > diff --git a/dts/framework/exception.py b/dts/framework/exception.py > index 93d99432ae..a35eeff640 100644 > --- a/dts/framework/exception.py > +++ b/dts/framework/exception.py > @@ -29,6 +29,10 @@ class ReturnCode(IntEnum): > DPDK_BUILD_ERR = 10 > NODE_SETUP_ERR = 20 > NODE_CLEANUP_ERR = 21 > + SUITE_SETUP_ERR = 30 > + SUITE_EXECUTION_ERR = 31 > + TESTCASE_VERIFY_ERR = 32 > + SUITE_CLEANUP_ERR = 33 > > > class DTSError(Exception): > @@ -153,6 +157,67 @@ def __init__(self): > ) > > > +class TestSuiteNotFound(DTSError): > + """ > + Raised when a configured test suite cannot be imported. > + """ > + > + return_code: ClassVar[ReturnCode] = ReturnCode.SUITE_SETUP_ERR > + > + > +class SuiteSetupError(DTSError): > + """ > + Raised when an error occurs during suite setup. > + """ > + > + return_code: ClassVar[ReturnCode] = ReturnCode.SUITE_SETUP_ERR > + > + def __init__(self): > + super(SuiteSetupError, self).__init__("An error occurred during > suite setup.") > + > + > +class SuiteExecutionError(DTSError): > + """ > + Raised when an error occurs during suite execution. > + """ > + > + return_code: ClassVar[ReturnCode] = ReturnCode.SUITE_EXECUTION_ERR > + > + def __init__(self): > + super(SuiteExecutionError, self).__init__( > + "An error occurred during suite execution." > + ) > + > + > +class VerifyError(DTSError): > + """ > + To be used within the test cases to verify if a command output > + is as it was expected. > + """ > + > + value: str > + return_code: ClassVar[ReturnCode] = ReturnCode.TESTCASE_VERIFY_ERR > + > + def __init__(self, value: str): > + self.value = value > + > + def __str__(self) -> str: > + return repr(self.value) > + > + > +class SuiteCleanupError(DTSError): > + """ > + Raised when an error occurs during suite cleanup. > + """ > + > + return_code: ClassVar[ReturnCode] = ReturnCode.SUITE_CLEANUP_ERR > + > + def __init__(self): > + super(SuiteCleanupError, self).__init__( > + "An error occurred during suite cleanup." > + ) > + > + > def convert_exception(exception: type[DTSError]) -> Callable[..., > Callable[..., None]]: > """ > When a non-DTS exception is raised while executing the decorated > function, > diff --git a/dts/framework/settings.py b/dts/framework/settings.py > index e2bf3d2ce4..069f28ce81 100644 > --- a/dts/framework/settings.py > +++ b/dts/framework/settings.py > @@ -64,6 +64,8 @@ class _Settings: > skip_setup: bool > dpdk_ref: Path > compile_timeout: float > + test_cases: list > + re_run: int > > > def _get_parser() -> argparse.ArgumentParser: > @@ -138,6 +140,25 @@ def _get_parser() -> argparse.ArgumentParser: > help="[DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK.", > ) > > + parser.add_argument( > + "--test-cases", > + action=_env_arg("DTS_TESTCASES"), > + default="", > + required=False, > + help="[DTS_TESTCASES] Comma-separated list of testcases to > execute", > + ) > + > + parser.add_argument( > + "--re-run", > + "--re_run", > + action=_env_arg("DTS_RERUN"), > + default=0, > + type=int, > + required=False, > + help="[DTS_RERUN] Re-run tests the specified amount of times if a > test failure " > + "occurs", > + ) > + > return parser > > > @@ -151,6 +172,10 @@ def _get_settings() -> _Settings: > skip_setup=(parsed_args.skip_setup == "Y"), > dpdk_ref=parsed_args.dpdk_ref, > compile_timeout=parsed_args.compile_timeout, > + test_cases=parsed_args.test_cases.split(",") > + if parsed_args.test_cases != "" > + else [], > + re_run=parsed_args.re_run, > ) > > > diff --git a/dts/framework/test_case.py b/dts/framework/test_case.py > new file mode 100644 > index 0000000000..0479f795bb > --- /dev/null > +++ b/dts/framework/test_case.py > @@ -0,0 +1,246 @@ > +# SPDX-License-Identifier: BSD-3-Clause > +# Copyright(c) 2010-2014 Intel Corporation > +# Copyright(c) 2022 PANTHEON.tech s.r.o. > + > +""" > +A base class for creating DTS test cases. > +""" > + > +import importlib > +import inspect > +import re > +import time > +import traceback > + > +from .exception import ( > + SSHTimeoutError, > + SuiteCleanupError, > + SuiteExecutionError, > + SuiteSetupError, > + TestSuiteNotFound, > + VerifyError, > + convert_exception, > +) > +from .logger import getLogger > +from .settings import SETTINGS > +from .test_result import Result > + > + > +class TestCase(object): > + def __init__(self, sut_node, suitename, target, execution): > + self.sut_node = sut_node > + self.suite_name = suitename > + self.target = target > + > + # local variable > + self._requested_tests = [] > + self._subtitle = None > + > + # result object for save suite result > + self._suite_result = Result() > + self._suite_result.sut = self.sut_node.config.hostname > + self._suite_result.target = target > + self._suite_result.test_suite = self.suite_name > + if self._suite_result is None: > + raise ValueError("Result object should not None") > + > + self._enable_func = execution.func > + > + # command history > + self.setup_history = list() > + self.test_history = list() > + > + def init_log(self): > + # get log handler > + class_name = self.__class__.__name__ > + self.logger = getLogger(class_name) > + > + def set_up_all(self): > + pass > + > + def set_up(self): > + pass > + > + def tear_down(self): > + pass > + > + def tear_down_all(self): > + pass > + > + def verify(self, passed, description): > + if not passed: > + raise VerifyError(description) > + > + def _get_functional_cases(self): > + """ > + Get all functional test cases. > + """ > + return self._get_test_cases(r"test_(?!perf_)") > + > + def _has_it_been_requested(self, test_case, test_name_regex): > + """ > + Check whether test case has been requested for validation. > + """ > + name_matches = re.match(test_name_regex, test_case.__name__) > + if self._requested_tests: > + return name_matches and test_case.__name__ in > self._requested_tests > + > + return name_matches > + > + def set_requested_cases(self, case_list): > + """ > + Pass down input cases list for check > + """ > + self._requested_tests += case_list > + > + def _get_test_cases(self, test_name_regex): > + """ > + Return case list which name matched regex. > + """ > + self.logger.debug(f"Searching for testcases in {self.__class__}") > + for test_case_name in dir(self): > + test_case = getattr(self, test_case_name) > + if callable(test_case) and self._has_it_been_requested( > + test_case, test_name_regex > + ): > + yield test_case > + > + @convert_exception(SuiteSetupError) > + def execute_setup_all(self): > + """ > + Execute suite setup_all function before cases. > + """ > + try: > + self.set_up_all() > + return True > + except Exception as v: > + self.logger.error("set_up_all failed:\n" + > traceback.format_exc()) > + # record all cases blocked > + if self._enable_func: > + for case_obj in self._get_functional_cases(): > + self._suite_result.test_case = case_obj.__name__ > + self._suite_result.test_case_blocked( > + "set_up_all failed: {}".format(str(v)) > + ) > + return False > + > + def _execute_test_case(self, case_obj): > + """ > + Execute specified test case in specified suite. If any exception > occurred in > + validation process, save the result and tear down this case. > + """ > + case_name = case_obj.__name__ > + self._suite_result.test_case = case_obj.__name__ > + > + case_result = True > + try: > + self.logger.info("Test Case %s Begin" % case_name) > + > + self.running_case = case_name > + # run set_up function for each case > + self.set_up() > + # run test case > + case_obj() > + > + self._suite_result.test_case_passed() > + > + self.logger.info("Test Case %s Result PASSED:" % case_name) > + > + except VerifyError as v: > + case_result = False > + self._suite_result.test_case_failed(str(v)) > + self.logger.error("Test Case %s Result FAILED: " % > (case_name) + str(v)) > + except KeyboardInterrupt: > + self._suite_result.test_case_blocked("Skipped") > + self.logger.error("Test Case %s SKIPPED: " % (case_name)) > + self.tear_down() > + raise KeyboardInterrupt("Stop DTS") > + except SSHTimeoutError as e: > + case_result = False > + self._suite_result.test_case_failed(str(e)) > + self.logger.error("Test Case %s Result FAILED: " % > (case_name) + str(e)) > + self.logger.error("%s" % (e.get_output())) > + except Exception: > + case_result = False > + trace = traceback.format_exc() > + self._suite_result.test_case_failed(trace) > + self.logger.error("Test Case %s Result ERROR: " % (case_name) > + trace) > + finally: > + self.execute_tear_down() > + return case_result > + > + @convert_exception(SuiteExecutionError) > + def execute_test_cases(self): > + """ > + Execute all test cases in one suite. > + """ > + # prepare debugger rerun case environment > + if self._enable_func: > + for case_obj in self._get_functional_cases(): > + for i in range(SETTINGS.re_run + 1): > + ret = self.execute_test_case(case_obj) > + > + if ret is False and SETTINGS.re_run: > + self.sut_node.get_session_output(timeout=0.5 * (i > + 1)) > + time.sleep(i + 1) > + self.logger.info( > + " Test case %s failed and re-run %d time" > + % (case_obj.__name__, i + 1) > + ) > + else: > + break > + > + def execute_test_case(self, case_obj): > + """ > + Execute test case or enter into debug mode. > + """ > + return self._execute_test_case(case_obj) > + > + def get_result(self): > + """ > + Return suite test result > + """ > + return self._suite_result > + > + @convert_exception(SuiteCleanupError) > + def execute_tear_downall(self): > + """ > + execute suite tear_down_all function > + """ > + self.tear_down_all() > + > + self.sut_node.kill_cleanup_dpdk_apps() > + > + def execute_tear_down(self): > + """ > + execute suite tear_down function > + """ > + try: > + self.tear_down() > + except Exception: > + self.logger.error("tear_down failed:\n" + > traceback.format_exc()) > + self.logger.warning( > + "tear down %s failed, might iterfere next case's result!" > + % self.running_case > + ) > + > + @staticmethod > + def get_testcases(testsuite_module_path: str) -> > list[type["TestCase"]]: > + def is_testcase(object) -> bool: > + try: > + if issubclass(object, TestCase) and object != TestCase: > + return True > + except TypeError: > + return False > + return False > + > + try: > + testcase_module = > importlib.import_module(testsuite_module_path) > + except ModuleNotFoundError as e: > + raise TestSuiteNotFound( > + f"Testsuite '{testsuite_module_path}' not found." > + ) from e > + return [ > + testcase_class > + for _, testcase_class in inspect.getmembers(testcase_module, > is_testcase) > + ] > -- > 2.30.2 > >