xref: /dpdk/dts/framework/runner.py (revision bd4a5aa413583aa698f10849c4784a3d524566bc)
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright(c) 2010-2019 Intel Corporation
3# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
4# Copyright(c) 2022-2023 University of New Hampshire
5
6"""Test suite runner module.
7
8The module is responsible for running DTS in a series of stages:
9
10    #. Execution stage,
11    #. Build target stage,
12    #. Test suite stage,
13    #. Test case stage.
14
15The execution and build target stages set up the environment before running test suites.
16The test suite stage sets up steps common to all test cases
17and the test case stage runs test cases individually.
18"""
19
20import importlib
21import inspect
22import os
23import re
24import sys
25from pathlib import Path
26from types import MethodType
27from typing import Iterable, Sequence
28
29from .config import (
30    BuildTargetConfiguration,
31    Configuration,
32    ExecutionConfiguration,
33    TestSuiteConfig,
34    load_config,
35)
36from .exception import (
37    BlockingTestSuiteError,
38    ConfigurationError,
39    SSHTimeoutError,
40    TestCaseVerifyError,
41)
42from .logger import DTSLogger, DtsStage, get_dts_logger
43from .settings import SETTINGS
44from .test_result import (
45    BuildTargetResult,
46    DTSResult,
47    ExecutionResult,
48    Result,
49    TestCaseResult,
50    TestSuiteResult,
51    TestSuiteWithCases,
52)
53from .test_suite import TestSuite
54from .testbed_model import SutNode, TGNode
55
56
57class DTSRunner:
58    r"""Test suite runner class.
59
60    The class is responsible for running tests on testbeds defined in the test run configuration.
61    Each setup or teardown of each stage is recorded in a :class:`~framework.test_result.DTSResult`
62    or one of its subclasses. The test case results are also recorded.
63
64    If an error occurs, the current stage is aborted, the error is recorded, everything in
65    the inner stages is marked as blocked and the run continues in the next iteration
66    of the same stage. The return code is the highest `severity` of all
67    :class:`~.framework.exception.DTSError`\s.
68
69    Example:
70        An error occurs in a build target setup. The current build target is aborted,
71        all test suites and their test cases are marked as blocked and the run continues
72        with the next build target. If the errored build target was the last one in the
73        given execution, the next execution begins.
74    """
75
76    _configuration: Configuration
77    _logger: DTSLogger
78    _result: DTSResult
79    _test_suite_class_prefix: str
80    _test_suite_module_prefix: str
81    _func_test_case_regex: str
82    _perf_test_case_regex: str
83
84    def __init__(self):
85        """Initialize the instance with configuration, logger, result and string constants."""
86        self._configuration = load_config(SETTINGS.config_file_path)
87        self._logger = get_dts_logger()
88        if not os.path.exists(SETTINGS.output_dir):
89            os.makedirs(SETTINGS.output_dir)
90        self._logger.add_dts_root_logger_handlers(SETTINGS.verbose, SETTINGS.output_dir)
91        self._result = DTSResult(self._logger)
92        self._test_suite_class_prefix = "Test"
93        self._test_suite_module_prefix = "tests.TestSuite_"
94        self._func_test_case_regex = r"test_(?!perf_)"
95        self._perf_test_case_regex = r"test_perf_"
96
97    def run(self):
98        """Run all build targets in all executions from the test run configuration.
99
100        Before running test suites, executions and build targets are first set up.
101        The executions and build targets defined in the test run configuration are iterated over.
102        The executions define which tests to run and where to run them and build targets define
103        the DPDK build setup.
104
105        The tests suites are set up for each execution/build target tuple and each discovered
106        test case within the test suite is set up, executed and torn down. After all test cases
107        have been executed, the test suite is torn down and the next build target will be tested.
108
109        In order to properly mark test suites and test cases as blocked in case of a failure,
110        we need to have discovered which test suites and test cases to run before any failures
111        happen. The discovery happens at the earliest point at the start of each execution.
112
113        All the nested steps look like this:
114
115            #. Execution setup
116
117                #. Build target setup
118
119                    #. Test suite setup
120
121                        #. Test case setup
122                        #. Test case logic
123                        #. Test case teardown
124
125                    #. Test suite teardown
126
127                #. Build target teardown
128
129            #. Execution teardown
130
131        The test cases are filtered according to the specification in the test run configuration and
132        the :option:`--test-suite` command line argument or
133        the :envvar:`DTS_TESTCASES` environment variable.
134        """
135        sut_nodes: dict[str, SutNode] = {}
136        tg_nodes: dict[str, TGNode] = {}
137        try:
138            # check the python version of the server that runs dts
139            self._check_dts_python_version()
140            self._result.update_setup(Result.PASS)
141
142            # for all Execution sections
143            for execution in self._configuration.executions:
144                self._logger.set_stage(DtsStage.execution_setup)
145                self._logger.info(
146                    f"Running execution with SUT '{execution.system_under_test_node.name}'."
147                )
148                execution_result = self._result.add_execution(execution)
149                # we don't want to modify the original config, so create a copy
150                execution_test_suites = list(
151                    SETTINGS.test_suites if SETTINGS.test_suites else execution.test_suites
152                )
153                if not execution.skip_smoke_tests:
154                    execution_test_suites[:0] = [TestSuiteConfig.from_dict("smoke_tests")]
155                try:
156                    test_suites_with_cases = self._get_test_suites_with_cases(
157                        execution_test_suites, execution.func, execution.perf
158                    )
159                    execution_result.test_suites_with_cases = test_suites_with_cases
160                except Exception as e:
161                    self._logger.exception(
162                        f"Invalid test suite configuration found: " f"{execution_test_suites}."
163                    )
164                    execution_result.update_setup(Result.FAIL, e)
165
166                else:
167                    self._connect_nodes_and_run_execution(
168                        sut_nodes, tg_nodes, execution, execution_result, test_suites_with_cases
169                    )
170
171        except Exception as e:
172            self._logger.exception("An unexpected error has occurred.")
173            self._result.add_error(e)
174            raise
175
176        finally:
177            try:
178                self._logger.set_stage(DtsStage.post_execution)
179                for node in (sut_nodes | tg_nodes).values():
180                    node.close()
181                self._result.update_teardown(Result.PASS)
182            except Exception as e:
183                self._logger.exception("The final cleanup of nodes failed.")
184                self._result.update_teardown(Result.ERROR, e)
185
186        # we need to put the sys.exit call outside the finally clause to make sure
187        # that unexpected exceptions will propagate
188        # in that case, the error that should be reported is the uncaught exception as
189        # that is a severe error originating from the framework
190        # at that point, we'll only have partial results which could be impacted by the
191        # error causing the uncaught exception, making them uninterpretable
192        self._exit_dts()
193
194    def _check_dts_python_version(self) -> None:
195        """Check the required Python version - v3.10."""
196        if sys.version_info.major < 3 or (
197            sys.version_info.major == 3 and sys.version_info.minor < 10
198        ):
199            self._logger.warning(
200                "DTS execution node's python version is lower than Python 3.10, "
201                "is deprecated and will not work in future releases."
202            )
203            self._logger.warning("Please use Python >= 3.10 instead.")
204
205    def _get_test_suites_with_cases(
206        self,
207        test_suite_configs: list[TestSuiteConfig],
208        func: bool,
209        perf: bool,
210    ) -> list[TestSuiteWithCases]:
211        """Test suites with test cases discovery.
212
213        The test suites with test cases defined in the user configuration are discovered
214        and stored for future use so that we don't import the modules twice and so that
215        the list of test suites with test cases is available for recording right away.
216
217        Args:
218            test_suite_configs: Test suite configurations.
219            func: Whether to include functional test cases in the final list.
220            perf: Whether to include performance test cases in the final list.
221
222        Returns:
223            The discovered test suites, each with test cases.
224        """
225        test_suites_with_cases = []
226
227        for test_suite_config in test_suite_configs:
228            test_suite_class = self._get_test_suite_class(test_suite_config.test_suite)
229            test_cases = []
230            func_test_cases, perf_test_cases = self._filter_test_cases(
231                test_suite_class, test_suite_config.test_cases
232            )
233            if func:
234                test_cases.extend(func_test_cases)
235            if perf:
236                test_cases.extend(perf_test_cases)
237
238            test_suites_with_cases.append(
239                TestSuiteWithCases(test_suite_class=test_suite_class, test_cases=test_cases)
240            )
241
242        return test_suites_with_cases
243
244    def _get_test_suite_class(self, module_name: str) -> type[TestSuite]:
245        """Find the :class:`TestSuite` class in `module_name`.
246
247        The full module name is `module_name` prefixed with `self._test_suite_module_prefix`.
248        The module name is a standard filename with words separated with underscores.
249        Search the `module_name` for a :class:`TestSuite` class which starts
250        with `self._test_suite_class_prefix`, continuing with CamelCase `module_name`.
251        The first matching class is returned.
252
253        The CamelCase convention applies to abbreviations, acronyms, initialisms and so on::
254
255            OS -> Os
256            TCP -> Tcp
257
258        Args:
259            module_name: The module name without prefix where to search for the test suite.
260
261        Returns:
262            The found test suite class.
263
264        Raises:
265            ConfigurationError: If the corresponding module is not found or
266                a valid :class:`TestSuite` is not found in the module.
267        """
268
269        def is_test_suite(object) -> bool:
270            """Check whether `object` is a :class:`TestSuite`.
271
272            The `object` is a subclass of :class:`TestSuite`, but not :class:`TestSuite` itself.
273
274            Args:
275                object: The object to be checked.
276
277            Returns:
278                :data:`True` if `object` is a subclass of `TestSuite`.
279            """
280            try:
281                if issubclass(object, TestSuite) and object is not TestSuite:
282                    return True
283            except TypeError:
284                return False
285            return False
286
287        testsuite_module_path = f"{self._test_suite_module_prefix}{module_name}"
288        try:
289            test_suite_module = importlib.import_module(testsuite_module_path)
290        except ModuleNotFoundError as e:
291            raise ConfigurationError(
292                f"Test suite module '{testsuite_module_path}' not found."
293            ) from e
294
295        camel_case_suite_name = "".join(
296            [suite_word.capitalize() for suite_word in module_name.split("_")]
297        )
298        full_suite_name_to_find = f"{self._test_suite_class_prefix}{camel_case_suite_name}"
299        for class_name, class_obj in inspect.getmembers(test_suite_module, is_test_suite):
300            if class_name == full_suite_name_to_find:
301                return class_obj
302        raise ConfigurationError(
303            f"Couldn't find any valid test suites in {test_suite_module.__name__}."
304        )
305
306    def _filter_test_cases(
307        self, test_suite_class: type[TestSuite], test_cases_to_run: Sequence[str]
308    ) -> tuple[list[MethodType], list[MethodType]]:
309        """Filter `test_cases_to_run` from `test_suite_class`.
310
311        There are two rounds of filtering if `test_cases_to_run` is not empty.
312        The first filters `test_cases_to_run` from all methods of `test_suite_class`.
313        Then the methods are separated into functional and performance test cases.
314        If a method matches neither the functional nor performance name prefix, it's an error.
315
316        Args:
317            test_suite_class: The class of the test suite.
318            test_cases_to_run: Test case names to filter from `test_suite_class`.
319                If empty, return all matching test cases.
320
321        Returns:
322            A list of test case methods that should be executed.
323
324        Raises:
325            ConfigurationError: If a test case from `test_cases_to_run` is not found
326                or it doesn't match either the functional nor performance name prefix.
327        """
328        func_test_cases = []
329        perf_test_cases = []
330        name_method_tuples = inspect.getmembers(test_suite_class, inspect.isfunction)
331        if test_cases_to_run:
332            name_method_tuples = [
333                (name, method) for name, method in name_method_tuples if name in test_cases_to_run
334            ]
335            if len(name_method_tuples) < len(test_cases_to_run):
336                missing_test_cases = set(test_cases_to_run) - {
337                    name for name, _ in name_method_tuples
338                }
339                raise ConfigurationError(
340                    f"Test cases {missing_test_cases} not found among methods "
341                    f"of {test_suite_class.__name__}."
342                )
343
344        for test_case_name, test_case_method in name_method_tuples:
345            if re.match(self._func_test_case_regex, test_case_name):
346                func_test_cases.append(test_case_method)
347            elif re.match(self._perf_test_case_regex, test_case_name):
348                perf_test_cases.append(test_case_method)
349            elif test_cases_to_run:
350                raise ConfigurationError(
351                    f"Method '{test_case_name}' matches neither "
352                    f"a functional nor a performance test case name."
353                )
354
355        return func_test_cases, perf_test_cases
356
357    def _connect_nodes_and_run_execution(
358        self,
359        sut_nodes: dict[str, SutNode],
360        tg_nodes: dict[str, TGNode],
361        execution: ExecutionConfiguration,
362        execution_result: ExecutionResult,
363        test_suites_with_cases: Iterable[TestSuiteWithCases],
364    ) -> None:
365        """Connect nodes, then continue to run the given execution.
366
367        Connect the :class:`SutNode` and the :class:`TGNode` of this `execution`.
368        If either has already been connected, it's going to be in either `sut_nodes` or `tg_nodes`,
369        respectively.
370        If not, connect and add the node to the respective `sut_nodes` or `tg_nodes` :class:`dict`.
371
372        Args:
373            sut_nodes: A dictionary storing connected/to be connected SUT nodes.
374            tg_nodes: A dictionary storing connected/to be connected TG nodes.
375            execution: An execution's test run configuration.
376            execution_result: The execution's result.
377            test_suites_with_cases: The test suites with test cases to run.
378        """
379        sut_node = sut_nodes.get(execution.system_under_test_node.name)
380        tg_node = tg_nodes.get(execution.traffic_generator_node.name)
381
382        try:
383            if not sut_node:
384                sut_node = SutNode(execution.system_under_test_node)
385                sut_nodes[sut_node.name] = sut_node
386            if not tg_node:
387                tg_node = TGNode(execution.traffic_generator_node)
388                tg_nodes[tg_node.name] = tg_node
389        except Exception as e:
390            failed_node = execution.system_under_test_node.name
391            if sut_node:
392                failed_node = execution.traffic_generator_node.name
393            self._logger.exception(f"The Creation of node {failed_node} failed.")
394            execution_result.update_setup(Result.FAIL, e)
395
396        else:
397            self._run_execution(
398                sut_node, tg_node, execution, execution_result, test_suites_with_cases
399            )
400
401    def _run_execution(
402        self,
403        sut_node: SutNode,
404        tg_node: TGNode,
405        execution: ExecutionConfiguration,
406        execution_result: ExecutionResult,
407        test_suites_with_cases: Iterable[TestSuiteWithCases],
408    ) -> None:
409        """Run the given execution.
410
411        This involves running the execution setup as well as running all build targets
412        in the given execution. After that, execution teardown is run.
413
414        Args:
415            sut_node: The execution's SUT node.
416            tg_node: The execution's TG node.
417            execution: An execution's test run configuration.
418            execution_result: The execution's result.
419            test_suites_with_cases: The test suites with test cases to run.
420        """
421        self._logger.info(f"Running execution with SUT '{execution.system_under_test_node.name}'.")
422        execution_result.add_sut_info(sut_node.node_info)
423        try:
424            sut_node.set_up_execution(execution)
425            execution_result.update_setup(Result.PASS)
426        except Exception as e:
427            self._logger.exception("Execution setup failed.")
428            execution_result.update_setup(Result.FAIL, e)
429
430        else:
431            for build_target in execution.build_targets:
432                build_target_result = execution_result.add_build_target(build_target)
433                self._run_build_target(
434                    sut_node, tg_node, build_target, build_target_result, test_suites_with_cases
435                )
436
437        finally:
438            try:
439                self._logger.set_stage(DtsStage.execution_teardown)
440                sut_node.tear_down_execution()
441                execution_result.update_teardown(Result.PASS)
442            except Exception as e:
443                self._logger.exception("Execution teardown failed.")
444                execution_result.update_teardown(Result.FAIL, e)
445
446    def _run_build_target(
447        self,
448        sut_node: SutNode,
449        tg_node: TGNode,
450        build_target: BuildTargetConfiguration,
451        build_target_result: BuildTargetResult,
452        test_suites_with_cases: Iterable[TestSuiteWithCases],
453    ) -> None:
454        """Run the given build target.
455
456        This involves running the build target setup as well as running all test suites
457        of the build target's execution.
458        After that, build target teardown is run.
459
460        Args:
461            sut_node: The execution's sut node.
462            tg_node: The execution's tg node.
463            build_target: A build target's test run configuration.
464            build_target_result: The build target level result object associated
465                with the current build target.
466            test_suites_with_cases: The test suites with test cases to run.
467        """
468        self._logger.set_stage(DtsStage.build_target_setup)
469        self._logger.info(f"Running build target '{build_target.name}'.")
470
471        try:
472            sut_node.set_up_build_target(build_target)
473            self._result.dpdk_version = sut_node.dpdk_version
474            build_target_result.add_build_target_info(sut_node.get_build_target_info())
475            build_target_result.update_setup(Result.PASS)
476        except Exception as e:
477            self._logger.exception("Build target setup failed.")
478            build_target_result.update_setup(Result.FAIL, e)
479
480        else:
481            self._run_test_suites(sut_node, tg_node, build_target_result, test_suites_with_cases)
482
483        finally:
484            try:
485                self._logger.set_stage(DtsStage.build_target_teardown)
486                sut_node.tear_down_build_target()
487                build_target_result.update_teardown(Result.PASS)
488            except Exception as e:
489                self._logger.exception("Build target teardown failed.")
490                build_target_result.update_teardown(Result.FAIL, e)
491
492    def _run_test_suites(
493        self,
494        sut_node: SutNode,
495        tg_node: TGNode,
496        build_target_result: BuildTargetResult,
497        test_suites_with_cases: Iterable[TestSuiteWithCases],
498    ) -> None:
499        """Run `test_suites_with_cases` with the current build target.
500
501        The method assumes the build target we're testing has already been built on the SUT node.
502        The current build target thus corresponds to the current DPDK build present on the SUT node.
503
504        If a blocking test suite (such as the smoke test suite) fails, the rest of the test suites
505        in the current build target won't be executed.
506
507        Args:
508            sut_node: The execution's SUT node.
509            tg_node: The execution's TG node.
510            build_target_result: The build target level result object associated
511                with the current build target.
512            test_suites_with_cases: The test suites with test cases to run.
513        """
514        end_build_target = False
515        for test_suite_with_cases in test_suites_with_cases:
516            test_suite_result = build_target_result.add_test_suite(test_suite_with_cases)
517            try:
518                self._run_test_suite(sut_node, tg_node, test_suite_result, test_suite_with_cases)
519            except BlockingTestSuiteError as e:
520                self._logger.exception(
521                    f"An error occurred within {test_suite_with_cases.test_suite_class.__name__}. "
522                    "Skipping build target..."
523                )
524                self._result.add_error(e)
525                end_build_target = True
526            # if a blocking test failed and we need to bail out of suite executions
527            if end_build_target:
528                break
529
530    def _run_test_suite(
531        self,
532        sut_node: SutNode,
533        tg_node: TGNode,
534        test_suite_result: TestSuiteResult,
535        test_suite_with_cases: TestSuiteWithCases,
536    ) -> None:
537        """Set up, execute and tear down `test_suite_with_cases`.
538
539        The method assumes the build target we're testing has already been built on the SUT node.
540        The current build target thus corresponds to the current DPDK build present on the SUT node.
541
542        Test suite execution consists of running the discovered test cases.
543        A test case run consists of setup, execution and teardown of said test case.
544
545        Record the setup and the teardown and handle failures.
546
547        Args:
548            sut_node: The execution's SUT node.
549            tg_node: The execution's TG node.
550            test_suite_result: The test suite level result object associated
551                with the current test suite.
552            test_suite_with_cases: The test suite with test cases to run.
553
554        Raises:
555            BlockingTestSuiteError: If a blocking test suite fails.
556        """
557        test_suite_name = test_suite_with_cases.test_suite_class.__name__
558        self._logger.set_stage(
559            DtsStage.test_suite_setup, Path(SETTINGS.output_dir, test_suite_name)
560        )
561        test_suite = test_suite_with_cases.test_suite_class(sut_node, tg_node)
562        try:
563            self._logger.info(f"Starting test suite setup: {test_suite_name}")
564            test_suite.set_up_suite()
565            test_suite_result.update_setup(Result.PASS)
566            self._logger.info(f"Test suite setup successful: {test_suite_name}")
567        except Exception as e:
568            self._logger.exception(f"Test suite setup ERROR: {test_suite_name}")
569            test_suite_result.update_setup(Result.ERROR, e)
570
571        else:
572            self._execute_test_suite(
573                test_suite,
574                test_suite_with_cases.test_cases,
575                test_suite_result,
576            )
577        finally:
578            try:
579                self._logger.set_stage(DtsStage.test_suite_teardown)
580                test_suite.tear_down_suite()
581                sut_node.kill_cleanup_dpdk_apps()
582                test_suite_result.update_teardown(Result.PASS)
583            except Exception as e:
584                self._logger.exception(f"Test suite teardown ERROR: {test_suite_name}")
585                self._logger.warning(
586                    f"Test suite '{test_suite_name}' teardown failed, "
587                    "the next test suite may be affected."
588                )
589                test_suite_result.update_setup(Result.ERROR, e)
590            if len(test_suite_result.get_errors()) > 0 and test_suite.is_blocking:
591                raise BlockingTestSuiteError(test_suite_name)
592
593    def _execute_test_suite(
594        self,
595        test_suite: TestSuite,
596        test_cases: Iterable[MethodType],
597        test_suite_result: TestSuiteResult,
598    ) -> None:
599        """Execute all `test_cases` in `test_suite`.
600
601        If the :option:`--re-run` command line argument or the :envvar:`DTS_RERUN` environment
602        variable is set, in case of a test case failure, the test case will be executed again
603        until it passes or it fails that many times in addition of the first failure.
604
605        Args:
606            test_suite: The test suite object.
607            test_cases: The list of test case methods.
608            test_suite_result: The test suite level result object associated
609                with the current test suite.
610        """
611        self._logger.set_stage(DtsStage.test_suite)
612        for test_case_method in test_cases:
613            test_case_name = test_case_method.__name__
614            test_case_result = test_suite_result.add_test_case(test_case_name)
615            all_attempts = SETTINGS.re_run + 1
616            attempt_nr = 1
617            self._run_test_case(test_suite, test_case_method, test_case_result)
618            while not test_case_result and attempt_nr < all_attempts:
619                attempt_nr += 1
620                self._logger.info(
621                    f"Re-running FAILED test case '{test_case_name}'. "
622                    f"Attempt number {attempt_nr} out of {all_attempts}."
623                )
624                self._run_test_case(test_suite, test_case_method, test_case_result)
625
626    def _run_test_case(
627        self,
628        test_suite: TestSuite,
629        test_case_method: MethodType,
630        test_case_result: TestCaseResult,
631    ) -> None:
632        """Setup, execute and teardown `test_case_method` from `test_suite`.
633
634        Record the result of the setup and the teardown and handle failures.
635
636        Args:
637            test_suite: The test suite object.
638            test_case_method: The test case method.
639            test_case_result: The test case level result object associated
640                with the current test case.
641        """
642        test_case_name = test_case_method.__name__
643
644        try:
645            # run set_up function for each case
646            test_suite.set_up_test_case()
647            test_case_result.update_setup(Result.PASS)
648        except SSHTimeoutError as e:
649            self._logger.exception(f"Test case setup FAILED: {test_case_name}")
650            test_case_result.update_setup(Result.FAIL, e)
651        except Exception as e:
652            self._logger.exception(f"Test case setup ERROR: {test_case_name}")
653            test_case_result.update_setup(Result.ERROR, e)
654
655        else:
656            # run test case if setup was successful
657            self._execute_test_case(test_suite, test_case_method, test_case_result)
658
659        finally:
660            try:
661                test_suite.tear_down_test_case()
662                test_case_result.update_teardown(Result.PASS)
663            except Exception as e:
664                self._logger.exception(f"Test case teardown ERROR: {test_case_name}")
665                self._logger.warning(
666                    f"Test case '{test_case_name}' teardown failed, "
667                    f"the next test case may be affected."
668                )
669                test_case_result.update_teardown(Result.ERROR, e)
670                test_case_result.update(Result.ERROR)
671
672    def _execute_test_case(
673        self,
674        test_suite: TestSuite,
675        test_case_method: MethodType,
676        test_case_result: TestCaseResult,
677    ) -> None:
678        """Execute `test_case_method` from `test_suite`, record the result and handle failures.
679
680        Args:
681            test_suite: The test suite object.
682            test_case_method: The test case method.
683            test_case_result: The test case level result object associated
684                with the current test case.
685        """
686        test_case_name = test_case_method.__name__
687        try:
688            self._logger.info(f"Starting test case execution: {test_case_name}")
689            test_case_method(test_suite)
690            test_case_result.update(Result.PASS)
691            self._logger.info(f"Test case execution PASSED: {test_case_name}")
692
693        except TestCaseVerifyError as e:
694            self._logger.exception(f"Test case execution FAILED: {test_case_name}")
695            test_case_result.update(Result.FAIL, e)
696        except Exception as e:
697            self._logger.exception(f"Test case execution ERROR: {test_case_name}")
698            test_case_result.update(Result.ERROR, e)
699        except KeyboardInterrupt:
700            self._logger.error(f"Test case execution INTERRUPTED by user: {test_case_name}")
701            test_case_result.update(Result.SKIP)
702            raise KeyboardInterrupt("Stop DTS")
703
704    def _exit_dts(self) -> None:
705        """Process all errors and exit with the proper exit code."""
706        self._result.process()
707
708        if self._logger:
709            self._logger.info("DTS execution has ended.")
710
711        sys.exit(self._result.get_return_code())
712