Source code for robot.errors

#  Copyright 2008-2015 Nokia Networks
#  Copyright 2016-     Robot Framework Foundation
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

"""Exceptions and return codes.

Unless noted otherwise, external libraries should not use exceptions defined here.
"""

# Return codes from Robot and Rebot.
# RC below 250 is the number of failed critical tests and exactly 250
# means that number or more such failures.
# fmt: off
INFO_PRINTED    = 251  # --help or --version
DATA_ERROR      = 252  # Invalid data or cli args
STOPPED_BY_USER = 253  # KeyboardInterrupt or SystemExit
FRAMEWORK_ERROR = 255  # Unexpected error
# fmt: on


[docs] class RobotError(Exception): """Base class for Robot Framework errors. Do not raise this method but use more specific errors instead. """ def __init__(self, message="", details=""): super().__init__(message) self.details = details @property def message(self): return str(self)
[docs] class FrameworkError(RobotError): """Can be used when the core framework goes to unexpected state. It is good to explicitly raise a FrameworkError if some framework component is used incorrectly. This is pretty much same as 'Internal Error' and should of course never happen. """
[docs] class DataError(RobotError): """Used when the provided test data is invalid. DataErrors are not caught by keywords that run other keywords (e.g. `Run Keyword And Expect Error`). """ def __init__(self, message="", details="", syntax=False): super().__init__(message, details) self.syntax = syntax
[docs] class VariableError(DataError): """Used when variable does not exist. VariableErrors are caught by keywords that run other keywords (e.g. `Run Keyword And Expect Error`). """ def __init__(self, message="", details=""): super().__init__(message, details)
[docs] class KeywordError(DataError): """Used when no keyword is found or there is more than one match. KeywordErrors are caught by keywords that run other keywords (e.g. `Run Keyword And Expect Error`). """ def __init__(self, message="", details=""): super().__init__(message, details)
[docs] class TimeoutExceeded(RobotError): """Used when a test or keyword timeout occurs. This exception cannot be caught be TRY/EXCEPT or by keywords running other keywords such as `Wait Until Keyword Succeeds`. Library keywords can catch this exception to handle cleanup activities if a timeout occurs. They should reraise it immediately when they are done. Attributes :attr:`test_timeout` and :attr:`keyword_timeout` are not part of the public API and should not be used by libraries. Prior to Robot Framework 7.3, this exception was named ``TimeoutError``. It was renamed to not conflict with Python's standard exception with the same name. The old name still exists as a backwards compatible alias. """ def __init__(self, message="", test_timeout=True): super().__init__(message) self.test_timeout = test_timeout @property def keyword_timeout(self): return not self.test_timeout
# Backward compatible alias. TimeoutError = TimeoutExceeded
[docs] class Information(RobotError): """Used by argument parser with --help or --version.""" def __init__(self, message: str, status_rc: bool = True): super().__init__(message) self.rc = INFO_PRINTED if status_rc else 0
[docs] class ExecutionStatus(RobotError): """Base class for exceptions communicating status in test execution.""" def __init__( self, message: str, test_timeout: bool = False, keyword_timeout: bool = False, syntax: bool = False, exit: bool = False, continue_on_failure: bool = False, skip: bool = False, return_value: object = None, ): from robot.utils import cut_long_message if "\r\n" in message: message = message.replace("\r\n", "\n") super().__init__(cut_long_message(message)) self.test_timeout = test_timeout self.keyword_timeout = keyword_timeout self.syntax = syntax self.exit = exit self._continue_on_failure = continue_on_failure self.skip = skip self.return_value = return_value @property def timeout(self): return self.test_timeout or self.keyword_timeout @property def dont_continue(self): return self.timeout or self.syntax or self.exit @property def continue_on_failure(self): return self._continue_on_failure @continue_on_failure.setter def continue_on_failure(self, continue_on_failure): self._continue_on_failure = continue_on_failure for child in getattr(self, "_errors", []): if child is not self: child.continue_on_failure = continue_on_failure
[docs] def can_continue(self, context, templated=False): if context.dry_run: return True if self.syntax or self.exit or self.test_timeout: return False if templated: return context.continue_on_failure(default=True) if self.skip: return False if self.keyword_timeout: return False return self.continue_on_failure or context.continue_on_failure()
[docs] def get_errors(self): return [self]
@property def status(self): return "FAIL" if not self.skip else "SKIP"
[docs] class ExecutionFailed(ExecutionStatus): """Used for communicating failures in test execution."""
[docs] class HandlerExecutionFailed(ExecutionFailed): def __init__(self, details): error = details.error timeout = isinstance(error, TimeoutExceeded) test_timeout = timeout and error.test_timeout keyword_timeout = timeout and error.keyword_timeout syntax = isinstance(error, DataError) and error.syntax exit_on_failure = self._get(error, "EXIT_ON_FAILURE") continue_on_failure = self._get(error, "CONTINUE_ON_FAILURE") skip = self._get(error, "SKIP_EXECUTION") super().__init__( details.message, test_timeout, keyword_timeout, syntax, exit_on_failure, continue_on_failure, skip, ) def _get(self, error, attr): return bool(getattr(error, "ROBOT_" + attr, False))
[docs] class ExecutionFailures(ExecutionFailed): def __init__(self, errors, message=None): super().__init__( message or self._format_message(errors), **self._get_attrs(errors), ) self._errors = errors def _format_message(self, errors): messages = [e.message for e in errors] if len(messages) == 1: return messages[0] prefix = "Several failures occurred:" if any(msg.startswith("*HTML*") for msg in messages): html = "*HTML* " messages = [self._html_format(msg) for msg in messages] else: html = "" if any(e.skip for e in errors): skip_idx = errors.index(next(e for e in errors if e.skip)) skip_msg = messages[skip_idx] messages = messages[:skip_idx] + messages[skip_idx + 1 :] if len(messages) == 1: return f"{html}{skip_msg}\n\nAlso failure occurred:\n{messages[0]}" prefix = f"{skip_msg}\n\nAlso failures occurred:" messages = [f"{i}) {m}" for i, m in enumerate(messages, start=1)] return "\n\n".join([html + prefix, *messages]) def _html_format(self, msg): from robot.utils import html_escape if msg.startswith("*HTML*"): return msg[6:].lstrip() return html_escape(msg) def _get_attrs(self, errors): return { "test_timeout": any(e.test_timeout for e in errors), "keyword_timeout": any(e.keyword_timeout for e in errors), "syntax": any(e.syntax for e in errors), "exit": any(e.exit for e in errors), "continue_on_failure": all(e.continue_on_failure for e in errors), "skip": any(e.skip for e in errors), }
[docs] def get_errors(self): return self._errors
[docs] class UserKeywordExecutionFailed(ExecutionFailures): def __init__(self, run_errors=None, teardown_errors=None): super().__init__( self._get_errors(run_errors, teardown_errors), self._get_message(run_errors, teardown_errors), ) if run_errors and not teardown_errors: self._errors = run_errors.get_errors() else: self._errors = [self] def _get_errors(self, *errors): return [err for err in errors if err] def _get_message(self, run_errors, teardown_errors): run_msg = run_errors.message if run_errors else "" td_msg = teardown_errors.message if teardown_errors else "" if not td_msg: return run_msg if not run_msg: return f"Keyword teardown failed:\n{td_msg}" return f"{run_msg}\n\nAlso keyword teardown failed:\n{td_msg}"
[docs] class ExecutionPassed(ExecutionStatus): """Base class for all exceptions communicating that execution passed. Should not be raised directly, but more detailed exceptions used instead. """ def __init__(self, message=None, **kwargs): super().__init__(message, **kwargs) self._earlier_failures = []
[docs] def set_earlier_failures(self, failures): if failures: self._earlier_failures = list(failures) + self._earlier_failures
@property def earlier_failures(self): if not self._earlier_failures: return None return ExecutionFailures(self._earlier_failures) @property def status(self): return "PASS" if not self._earlier_failures else "FAIL"
[docs] class PassExecution(ExecutionPassed): """Used by 'Pass Execution' keyword.""" def __init__(self, message): super().__init__(message)
[docs] class ContinueLoop(ExecutionPassed): """Used by CONTINUE statement.""" def __init__(self): super().__init__("Invalid 'CONTINUE' usage.")
[docs] class BreakLoop(ExecutionPassed): """Used by BREAK statement.""" def __init__(self): super().__init__("Invalid 'BREAK' usage.")
[docs] class ReturnFromKeyword(ExecutionPassed): """Used by 'RETURN' statement.""" def __init__(self, return_value=None, failures=None): super().__init__("Invalid 'RETURN' usage.", return_value=return_value) if failures: self.set_earlier_failures(failures)
[docs] class RemoteError(RobotError): """Used by Remote library to report remote errors.""" def __init__(self, message="", details="", fatal=False, continuable=False): super().__init__(message, details) self.ROBOT_EXIT_ON_FAILURE = fatal self.ROBOT_CONTINUE_ON_FAILURE = continuable