1from robot import result, running 2from robot.libraries.DateTime import Time 3 4# This listener wraps the RetryFailed listener to gracefully skip retrying for timed-out tests. 5# Failure is silenced because the lack of `RetryFailed` isn't a problem if `retry_count` is one. 6try: 7 from RetryFailed import RetryFailed 8except: 9 pass 10 11# The name isn't CamelCase typically used for Python classes only because it 12# has to match the name of the file and snake_case is used for other listeners. 13class retry_and_timeout_listener: 14 ROBOT_LISTENER_API_VERSION = 3 15 16 def __init__(self, retry_count): 17 if int(retry_count) > 1: 18 global_retries = int(retry_count) - 1 # Our `retry_count` includes the original test too. 19 self.retry_failed = RetryFailed(global_retries) 20 21 # Redirects access to all callbacks not implemented in this listener to `RetryFailed` if it's used. 22 def __getattr__(self, name): 23 # This prevents infinite recursion if `self.retry_failed` wasn't initialized. 24 # None is always returned cause `__getattr__` won't be called if it was initialized. 25 if name == 'retry_failed': 26 return None 27 28 if self.retry_failed and hasattr(self.retry_failed, name): 29 return getattr(self.retry_failed, name) 30 31 def start_suite(self, suite: running.TestSuite, result: result.TestSuite): 32 # Grab our RobotTestSuite and restore the original parent. 33 (self.tests_provider, suite.parent) = suite.parent 34 self.after_timeout = False 35 36 # start_suite isn't currently used by RetryFailed but let's check for future versions. 37 if self.retry_failed and hasattr(self.retry_failed, 'start_suite'): 38 self.retry_failed.start_suite(suite, result) 39 40 def end_test(self, test: running.TestCase, result: result.TestCase): 41 timed_out = result.failed and result.timeout and result.timeout.timed_out() 42 timeout_expected = self.tests_provider.timeout_expected_tag in test.tags 43 if timeout_expected: 44 # It passed if timeout occurred within the test timeout +3s. 45 tolerance_seconds = 3 46 over_timeout = result.elapsed_time.seconds - Time(test.timeout).seconds 47 if timed_out and over_timeout < tolerance_seconds: 48 result.status = result.PASS 49 else: 50 result.message = f"Expected timeout didn't occur in {test.timeout} (+{tolerance_seconds}s)" 51 52 # Mark messages that failed after another test timed out except tests that timed out. 53 # It isn't really probable that Renode restart will cause timeouts. 54 if result.failed and self.after_timeout and not timed_out: 55 result.message = result.message + '\n\n' + self.tests_provider.after_timeout_message_suffix 56 57 if timed_out: 58 # See `_create_timeout_handler` in `robot_tests_provider`, mostly restarts Renode. 59 self.tests_provider.timeout_handler(test, result) 60 self.after_timeout = True 61 62 # Let's prevent retrying a test with unexpected timeout but let's still call 63 # `RetryFailed.end_test` as `RetryFailed` might not properly work without it. 64 if self.retry_failed and not timeout_expected: 65 self.retry_failed.max_retries = self.retry_failed.retries 66 67 if self.retry_failed: 68 # `RetryFailed` changes the status of retried tests to skipped. Let's restore 69 # the original status so that our output formatters print them as failed. 70 original_message = result.message 71 original_status = result.status 72 self.retry_failed.end_test(test, result) 73 result.status = original_status 74 75 # Let's also restore the original message without suffixes added by `RetryFailed` 76 # unless it contains the number of attempts required in our test reporting. 77 if not self.tests_provider.retry_test_regex.search(result.message): 78 result.message = original_message 79