<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>