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