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