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