diff --git a/.github/workflows/acceptance_tests_cpython.yml b/.github/workflows/acceptance_tests_cpython.yml index 6f087f50f5e..fa2cfe9a528 100644 --- a/.github/workflows/acceptance_tests_cpython.yml +++ b/.github/workflows/acceptance_tests_cpython.yml @@ -5,6 +5,7 @@ on: branches: - main - master + - v*-maintenance paths: - '.github/workflows/**' - 'src/**' @@ -18,7 +19,7 @@ jobs: fail-fast: false matrix: os: [ 'ubuntu-latest', 'windows-latest' ] - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.8' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.8' ] include: - os: ubuntu-latest set_display: export DISPLAY=:99; Xvfb :99 -screen 0 1024x768x24 -ac -noreset & sleep 3 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index aaa235708cd..6746c36077a 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -5,6 +5,7 @@ on: branches: - main - master + - v*-maintenance paths: - '.github/workflows/**' - 'src/**' @@ -19,7 +20,7 @@ jobs: fail-fast: false matrix: os: [ 'ubuntu-latest', 'windows-latest' ] - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', 'pypy-3.8' ] + python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.8' ] exclude: - os: windows-latest python-version: 'pypy-3.8' diff --git a/BUILD.rst b/BUILD.rst index 1c0d907b4ba..699b1824eb2 100644 --- a/BUILD.rst +++ b/BUILD.rst @@ -76,7 +76,7 @@ __ https://github.com/robotframework/robotframework/tree/master/atest#schema-val Preparation ----------- -1. Check that you are on the master branch and have nothing left to commit, +1. Check that you are on the right branch and have nothing left to commit, pull, or push:: git branch @@ -93,13 +93,13 @@ Preparation VERSION= - For example, ``VERSION=3.0.1`` or ``VERSION=3.1a2``. + For example, ``VERSION=7.1.1`` or ``VERSION=7.2a2``. No ``v`` prefix! Release notes ------------- -1. Create personal `GitHub access token`__ to be able to access issue tracker - programmatically. The token needs only the `repo/public_repo` scope. +1. Create a personal `GitHub access token`__ to be able to access issue tracker + programmatically. The token needs only the ``repo/public_repo`` scope. 2. Set GitHub user information into shell variables to ease running the ``invoke release-notes`` command in the next step:: @@ -119,7 +119,7 @@ Release notes `__. Omit the ``-w`` option if you just want to get release notes printed to the console, not written to a file. - When generating release notes for a preview release like ``3.0.2rc1``, + When generating release notes for a preview release like ``7.2rc1``, the list of issues is only going to contain issues with that label (e.g. ``rc1``) or with a label of an earlier preview release (e.g. ``alpha1``, ``beta2``). diff --git a/atest/robot/cli/console/disable_standard_streams.py b/atest/robot/cli/console/disable_standard_streams.py new file mode 100644 index 00000000000..fc898f4f1cd --- /dev/null +++ b/atest/robot/cli/console/disable_standard_streams.py @@ -0,0 +1,3 @@ +import sys + +sys.stdin = sys.stdout = sys.stderr = sys.__stdin__ = sys.__stdout__ = sys.__stderr__ = None diff --git a/atest/robot/cli/console/standard_streams_disabled.robot b/atest/robot/cli/console/standard_streams_disabled.robot new file mode 100644 index 00000000000..44af45e78b2 --- /dev/null +++ b/atest/robot/cli/console/standard_streams_disabled.robot @@ -0,0 +1,23 @@ +*** Settings *** +Suite Setup Run tests with standard streams disabled +Resource console_resource.robot + +*** Test Cases *** +Execution succeeds + Should Be Equal ${SUITE.name} Log + +Console outputs are disabled + Stdout Should Be empty.txt + Stderr Should Be empty.txt + +Log To Console keyword succeeds + Check Test Case Log to console + +*** Keywords *** +Run tests with standard streams disabled + [Documentation] Streams are disabled by using the sitecustomize module: + ... https://docs.python.org/3/library/site.html#module-sitecustomize + Copy File ${CURDIR}/disable_standard_streams.py %{TEMPDIR}/sitecustomize.py + Set Environment Variable PYTHONPATH %{TEMPDIR} + Run Tests ${EMPTY} standard_libraries/builtin/log.robot + [Teardown] Remove File %{TEMPDIR}/sitecustomize.py diff --git a/atest/robot/output/listener_interface/change_status.robot b/atest/robot/output/listener_interface/change_status.robot index b333c36d1ce..a994c652f38 100644 --- a/atest/robot/output/listener_interface/change_status.robot +++ b/atest/robot/output/listener_interface/change_status.robot @@ -10,21 +10,30 @@ ${MODIFIER} output/listener_interface/body_items_v3/ChangeStatus.py Fail to pass ${tc} = Check Test Case ${TEST NAME} Check Keyword Data ${tc.body[0]} BuiltIn.Fail args=Pass me! status=PASS message=Failure hidden! + Check Log Message ${tc.body[0].msgs[0]} Pass me! level=FAIL Check Keyword Data ${tc.body[1]} BuiltIn.Log args=I'm run. status=PASS message= Pass to fail ${tc} = Check Test Case ${TEST NAME} Check Keyword Data ${tc.body[0]} BuiltIn.Log args=Fail me! status=FAIL message=Ooops!! + Check Log Message ${tc.body[0].msgs[0]} Fail me! level=INFO + Check Keyword Data ${tc.body[1]} BuiltIn.Log args=I'm not run. status=NOT RUN message= + +Pass to fail without a message + ${tc} = Check Test Case ${TEST NAME} + Check Keyword Data ${tc.body[0]} BuiltIn.Log args=Silent fail! status=FAIL message= Check Keyword Data ${tc.body[1]} BuiltIn.Log args=I'm not run. status=NOT RUN message= Skip to fail ${tc} = Check Test Case ${TEST NAME} Check Keyword Data ${tc.body[0]} BuiltIn.Skip args=Fail me! status=FAIL message=Failing! + Check Log Message ${tc.body[0].msgs[0]} Fail me! level=SKIP Check Keyword Data ${tc.body[1]} BuiltIn.Log args=I'm not run. status=NOT RUN message= Fail to skip ${tc} = Check Test Case ${TEST NAME} Check Keyword Data ${tc.body[0]} BuiltIn.Fail args=Skip me! status=SKIP message=Skipping! + Check Log Message ${tc.body[0].msgs[0]} Skip me! level=FAIL Check Keyword Data ${tc.body[1]} BuiltIn.Log args=I'm not run. status=NOT RUN message= Not run to fail diff --git a/atest/robot/running/failures_in_teardown.robot b/atest/robot/running/failures_in_teardown.robot index 8ab85190675..af23f132a65 100644 --- a/atest/robot/running/failures_in_teardown.robot +++ b/atest/robot/running/failures_in_teardown.robot @@ -29,14 +29,34 @@ Execution Stops After Keyword Timeout Length Should Be ${tc.teardown.kws} 2 Should Be Equal ${tc.teardown.kws[-1].status} NOT RUN -Execution Continues After Keyword Timeout Occurs In Executed Keyword +Execution continues if executed keyword fails for keyword timeout ${tc} = Check Test Case ${TESTNAME} - Length Should Be ${tc.teardown.body} 2 - Length Should Be ${tc.teardown.body[0].body} 2 - Should Be Equal ${tc.teardown.body[0].body[0].status} FAIL - Should Be Equal ${tc.teardown.body[0].body[1].status} NOT RUN - Should Be Equal ${tc.teardown.body[0].status} FAIL - Should Be Equal ${tc.teardown.body[1].status} FAIL + Length Should Be ${tc.teardown.body} 2 + Should Be Equal ${tc.teardown.body[0].status} FAIL + Should Be Equal ${tc.teardown.body[1].status} FAIL + Length Should Be ${tc.teardown.body[0].body} 2 + Should Be Equal ${tc.teardown.body[0].body[0].status} FAIL + Check Log Message ${tc.teardown.body[0].body[0].body[0]} Keyword timeout 42 milliseconds exceeded. FAIL + Should Be Equal ${tc.teardown.body[0].body[1].status} NOT RUN + Length Should Be ${tc.teardown.body[1].body} 1 + Check Log Message ${tc.teardown.body[1].body[0]} This should be executed FAIL + +Execution stops after keyword timeout if keyword uses WUKS + ${tc} = Check Test Case ${TESTNAME} + Length Should Be ${tc.teardown.body} 2 + Should Be Equal ${tc.teardown.body[0].status} FAIL + Should Be Equal ${tc.teardown.body[1].status} NOT RUN + Length Should Be ${tc.teardown.body[0].body} 2 + Should Be Equal ${tc.teardown.body[0].body[0].status} FAIL + Should Be Equal ${tc.teardown.body[0].body[1].status} FAIL + Length Should Be ${tc.teardown.body[0].body[0].body} 2 + Should Be Equal ${tc.teardown.body[0].body[0].body[0].status} PASS + Should Be Equal ${tc.teardown.body[0].body[0].body[1].status} FAIL + Check Log Message ${tc.teardown.body[0].body[0].body[1].body[0]} Failing! FAIL + Length Should Be ${tc.teardown.body[0].body[1].body} 2 + Should Be Equal ${tc.teardown.body[0].body[1].body[0].status} FAIL + Check Log Message ${tc.teardown.body[0].body[1].body[0].body[0]} Keyword timeout 100 milliseconds exceeded. FAIL + Should Be Equal ${tc.teardown.body[0].body[1].body[1].status} NOT RUN Execution Continues If Variable Does Not Exist ${tc} = Check Test Case ${TESTNAME} diff --git a/atest/robot/test_libraries/error_msg_and_details.robot b/atest/robot/test_libraries/error_msg_and_details.robot index 60bd663600e..b61410575ed 100644 --- a/atest/robot/test_libraries/error_msg_and_details.robot +++ b/atest/robot/test_libraries/error_msg_and_details.robot @@ -4,11 +4,17 @@ Resource atest_resource.robot Test Template Verify Test Case And Error In Log *** Test Cases *** -Exception Type is Removed From Generic Failures +Exception name is not included with generic exceptions Generic Failure foo != bar -Exception Type is Removed with Exception Attribute - Exception Name Suppressed in Error Message No Exception Name +Exception name can be supppressed explicitly + Exception name suppressed explicitly No Exception Name + +Even suppressed name is included if message is empty + ${TEST NAME} ExceptionWithSuppressedName + +Exception with empty message and name is handled properly + ${TEST NAME} ${EMPTY} Exception Type is Included In Non-Generic Failures Non Generic Failure FloatingPointError: Too Large A Number !! diff --git a/atest/robot/test_libraries/libraries_extending_existing_classes.robot b/atest/robot/test_libraries/libraries_extending_existing_classes.robot index 446e3b9bee2..67b8614faf7 100644 --- a/atest/robot/test_libraries/libraries_extending_existing_classes.robot +++ b/atest/robot/test_libraries/libraries_extending_existing_classes.robot @@ -16,4 +16,4 @@ Keyword In Python Class Using Method From Parent Class Check Test Case Keyword In Python Class Using Method From Parent Class Message Of Importing Library Should Be In Syslog - Syslog Should Contain Imported library 'ExtendPythonLib' with arguments [ ] (version , class type, TEST scope, 33 keywords) + Syslog Should Contain Imported library 'ExtendPythonLib' with arguments [ ] (version , class type, TEST scope, 34 keywords) diff --git a/atest/testdata/output/listener_interface/body_items_v3/ChangeStatus.py b/atest/testdata/output/listener_interface/body_items_v3/ChangeStatus.py index 67b9c43fdd6..b55f9f84067 100644 --- a/atest/testdata/output/listener_interface/body_items_v3/ChangeStatus.py +++ b/atest/testdata/output/listener_interface/body_items_v3/ChangeStatus.py @@ -7,6 +7,8 @@ def end_keyword(data, result): elif result.passed and 'Fail me!' in result.args: result.failed = True result.message = 'Ooops!!' + elif result.passed and 'Silent fail!' in result.args: + result.failed = True elif result.skipped: result.failed = True result.message = 'Failing!' diff --git a/atest/testdata/output/listener_interface/body_items_v3/change_status.robot b/atest/testdata/output/listener_interface/body_items_v3/change_status.robot index b930eda4245..b34a934ba77 100644 --- a/atest/testdata/output/listener_interface/body_items_v3/change_status.robot +++ b/atest/testdata/output/listener_interface/body_items_v3/change_status.robot @@ -8,6 +8,11 @@ Pass to fail Log Fail me! Log I'm not run. +Pass to fail without a message + [Documentation] FAIL + Log Silent fail! + Log I'm not run. + Skip to fail [Documentation] FAIL Failing! Skip Fail me! diff --git a/atest/testdata/running/failures_in_teardown.robot b/atest/testdata/running/failures_in_teardown.robot index cd678a5793b..f5859997118 100644 --- a/atest/testdata/running/failures_in_teardown.robot +++ b/atest/testdata/running/failures_in_teardown.robot @@ -43,7 +43,7 @@ Failure When Setting Variables No Operation [Teardown] Failure when setting variables -Failure In For Loop +Failure In FOR Loop [Documentation] FAIL Teardown failed: ... Several failures occurred: ... @@ -57,7 +57,7 @@ Failure In For Loop ... ... ${SUITE TEARDOWN FAILED} No Operation - [Teardown] Failures In For Loop + [Teardown] Failures In FOR Loop Execution Continues After Test Timeout [Documentation] FAIL Teardown failed: @@ -76,7 +76,7 @@ Execution Stops After Keyword Timeout No Operation [Teardown] Keyword Timeout Occurs -Execution Continues After Keyword Timeout Occurs In Executed Keyword +Execution continues if executed keyword fails for keyword timeout [Documentation] FAIL Teardown failed: ... Several failures occurred: ... @@ -88,6 +88,14 @@ Execution Continues After Keyword Timeout Occurs In Executed Keyword No Operation [Teardown] Keyword Timeout Occurs In Executed Keyword +Execution stops after keyword timeout if keyword uses WUKS + [Documentation] FAIL Teardown failed: + ... Keyword timeout 100 milliseconds exceeded. + ... + ... ${SUITE TEARDOWN FAILED} + No Operation + [Teardown] Keyword Using WUKS + Execution Continues If Variable Does Not Exist [Documentation] FAIL Teardown failed: ... Several failures occurred: @@ -150,7 +158,7 @@ Failure when setting variables ${ret} = Fail Return values is None Should Be Equal ${ret} ${None} -Failures In For Loop +Failures In FOR Loop FOR ${animal} IN cat dog Fail ${animal} Fail again @@ -169,6 +177,16 @@ Keyword Timeout Occurs In Executed Keyword Keyword Timeout Occurs Fail This should be executed +Keyword Using WUKS + [Timeout] 100ms + Wait Until Keyword Succeeds 4x 1s + ... Fail Slowly + Fail This should not be executed + +Fail Slowly + Sleep 0.51ms + Fail Failing! + Missing Variables Log ${this var does not exist} Log This should be executed diff --git a/atest/testdata/test_libraries/error_msg_and_details.robot b/atest/testdata/test_libraries/error_msg_and_details.robot index 7c260d5be3e..acde316af5e 100644 --- a/atest/testdata/test_libraries/error_msg_and_details.robot +++ b/atest/testdata/test_libraries/error_msg_and_details.robot @@ -3,14 +3,22 @@ Library ExampleLibrary Library nön_äscii_dïr/valid.py *** Test Cases *** -Generic Failure +Generic failure [Documentation] FAIL foo != bar Exception AssertionError foo != bar -Exception Name Suppressed in Error Message +Exception name suppressed explicitly [Documentation] FAIL No Exception Name Fail with suppressed exception name No Exception Name +Even suppressed name is included if message is empty + [Documentation] FAIL ExceptionWithSuppressedName + Fail with suppressed exception name ${EMPTY} + +Exception with empty message and name is handled properly + [Documentation] FAIL + Exception with empty message and name + Non Generic Failure [Documentation] FAIL FloatingPointError: Too Large A Number !! Exception FloatingPointError Too Large A Number !! diff --git a/atest/testresources/testlibs/ExampleLibrary.py b/atest/testresources/testlibs/ExampleLibrary.py index 40252bfd118..c9875460ee7 100644 --- a/atest/testresources/testlibs/ExampleLibrary.py +++ b/atest/testresources/testlibs/ExampleLibrary.py @@ -179,12 +179,22 @@ def __str__(self): return FailingStr(), FailingStr() def fail_with_suppressed_exception_name(self, msg): - raise MyException(msg) + raise ExceptionWithSuppressedName(msg) + + def exception_with_empty_message_and_name(self): + raise ExceptionWithEmptyName('') class _MyList(list): pass -class MyException(AssertionError): +class ExceptionWithSuppressedName(AssertionError): ROBOT_SUPPRESS_NAME = True + + +class ExceptionWithEmptyName(AssertionError): + pass + + +ExceptionWithEmptyName.__name__ = '' diff --git a/doc/releasenotes/rf-7.1.1.rst b/doc/releasenotes/rf-7.1.1.rst new file mode 100644 index 00000000000..36f2d132bee --- /dev/null +++ b/doc/releasenotes/rf-7.1.1.rst @@ -0,0 +1,97 @@ +===================== +Robot Framework 7.1.1 +===================== + +.. default-role:: code + +`Robot Framework`_ 7.1.1 is the first and also the only planned bug fix release +in the Robot Framework 7.1.x series. It fixes all reported regressions as well as +some issues affecting also earlier versions. + +Questions and comments related to the release can be sent to the `#devel` +channel on `Robot Framework Slack`_ and possible bugs submitted to +the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --upgrade robotframework + +to install the latest available release or use + +:: + + pip install robotframework==7.1.1 + +to install exactly this version. Alternatively, you can download the package +from PyPI_ and install it manually. For more details and other installation +approaches, see the `installation instructions`_. + +Robot Framework 7.1.1 was released on Saturday October 19, 2024. + +.. _Robot Framework: http://robotframework.org +.. _Robot Framework Foundation: http://robotframework.org/foundation +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework +.. _issue tracker: https://github.com/robotframework/robotframework/issues +.. _Slack: http://slack.robotframework.org +.. _Robot Framework Slack: Slack_ +.. _installation instructions: ../../INSTALL.rst + +.. contents:: + :depth: 2 + :local: + +Acknowledgements +================ + +Robot Framework development is sponsored by the `Robot Framework Foundation`_ +and its over 60 member organizations. If your organization is using Robot Framework +and benefiting from it, consider joining the foundation to support its +development as well. + +Big thanks to the Foundation and to everyone who has submitted bug reports, debugged +problems, or otherwise helped with Robot Framework development. + +| `Pekka Klärck `_ +| Robot Framework lead developer + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#5205`_ + - bug + - high + - Execution fails at the end when using `--output NONE` and console hyperlinks are enabled + * - `#5224`_ + - bug + - high + - Test is not failed if listener sets keyword status to fail and leaves message empty + * - `#5212`_ + - bug + - medium + - Execution fails if standard streams are not available + * - `#5237`_ + - bug + - medium + - Keyword timeout is not effective in teardown if keyword uses `Wait Until Keyword Succeeds` + * - `#5206`_ + - enhancement + - medium + - Document that `output_file` listener method is called with `None` when using `--output NONE` + +Altogether 5 issues. View on the `issue tracker `__. + +.. _#5205: https://github.com/robotframework/robotframework/issues/5205 +.. _#5224: https://github.com/robotframework/robotframework/issues/5224 +.. _#5212: https://github.com/robotframework/robotframework/issues/5212 +.. _#5237: https://github.com/robotframework/robotframework/issues/5237 +.. _#5206: https://github.com/robotframework/robotframework/issues/5206 diff --git a/setup.py b/setup.py index e0922388ed0..ba9c892cd1f 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ # Version number typically updated by running `invoke set-version `. # Run `invoke --help set-version` or see tasks.py for details. -VERSION = '7.1.1.dev1' +VERSION = '7.1.2.dev1' with open(join(dirname(abspath(__file__)), 'README.rst')) as f: LONG_DESCRIPTION = f.read() base_url = 'https://github.com/robotframework/robotframework/blob/master' diff --git a/src/robot/errors.py b/src/robot/errors.py index 29b1dc4e1db..4e1d3786aad 100644 --- a/src/robot/errors.py +++ b/src/robot/errors.py @@ -148,8 +148,6 @@ def can_continue(self, context, templated=False): if templated: return context.continue_on_failure(default=True) if self.keyword_timeout: - if context.in_teardown: - self.keyword_timeout = False return False return self.continue_on_failure or context.continue_on_failure() diff --git a/src/robot/libraries/BuiltIn.py b/src/robot/libraries/BuiltIn.py index 25cc59aa753..688cdc916d2 100644 --- a/src/robot/libraries/BuiltIn.py +++ b/src/robot/libraries/BuiltIn.py @@ -2415,6 +2415,7 @@ def wait_until_keyword_succeeds(self, retry, retry_interval, name, *args): try: return self.run_keyword(name, *args) except ExecutionFailed as err: + self._reset_keyword_timeout_in_teardown(err, self._context) if err.dont_continue or err.skip: raise count -= 1 @@ -2434,6 +2435,17 @@ def wait_until_keyword_succeeds(self, retry, retry_interval, name, *args): ) self._sleep_in_parts(sleep_time) + def _reset_keyword_timeout_in_teardown(self, err, context): + # Keyword timeouts in teardowns have been converted to normal failures + # to allow execution to continue on higher level: + # https://github.com/robotframework/robotframework/issues/3398 + # We need to reset it here to not continue unnecessarily: + # https://github.com/robotframework/robotframework/issues/5237 + if context.in_teardown: + timeouts = [t for t in context.timeouts if t.type == 'Keyword'] + if timeouts and min(timeouts).timed_out(): + err.keyword_timeout = True + @run_keyword_variant(resolve=1) def set_variable_if(self, condition, *values): """Sets variable based on the given condition. diff --git a/src/robot/output/console/highlighting.py b/src/robot/output/console/highlighting.py index fe646970875..b52eb3f348c 100644 --- a/src/robot/output/console/highlighting.py +++ b/src/robot/output/console/highlighting.py @@ -43,10 +43,12 @@ class ConsoleScreenBufferInfo(Structure): class HighlightingStream: def __init__(self, stream, colors='AUTO', links='AUTO'): - self.stream = stream + self.stream = stream or NullStream() self._highlighter = self._get_highlighter(stream, colors, links) def _get_highlighter(self, stream, colors, links): + if not stream: + return NoHighlighting() options = {'AUTO': Highlighter if isatty(stream) else NoHighlighting, 'ON': Highlighter, 'OFF': NoHighlighting, @@ -102,7 +104,7 @@ def highlight(self, text, status=None, flush=True): def error(self, message, level): self.write('[ ', flush=False) self.highlight(level, flush=False) - self.write(' ] %s\n' % message) + self.write(f' ] {message}\n') @contextmanager def _highlighting(self, status): @@ -123,6 +125,15 @@ def result_file(self, kind, path): self.write(f"{kind + ':':8} {path}\n") +class NullStream: + + def write(self, text): + pass + + def flush(self): + pass + + def Highlighter(stream, links=True): if os.sep == '/': return AnsiHighlighter(stream, links) @@ -172,6 +183,9 @@ def _set_color(self, color): class NoHighlighting(AnsiHighlighter): + def __init__(self, stream=None, links=True): + super().__init__(stream, links) + def link(self, path): return path diff --git a/src/robot/output/loggerhelper.py b/src/robot/output/loggerhelper.py index c5a2fe83faa..7f54dfd8311 100644 --- a/src/robot/output/loggerhelper.py +++ b/src/robot/output/loggerhelper.py @@ -40,8 +40,9 @@ def write_to_console(msg, newline=True, stream='stdout'): if newline: msg += '\n' stream = sys.__stdout__ if stream.lower() != 'stderr' else sys.__stderr__ - stream.write(console_encode(msg, stream=stream)) - stream.flush() + if stream: + stream.write(console_encode(msg, stream=stream)) + stream.flush() class AbstractLogger: diff --git a/src/robot/running/outputcapture.py b/src/robot/running/outputcapture.py index 50c013a7190..fc1de79966f 100644 --- a/src/robot/running/outputcapture.py +++ b/src/robot/running/outputcapture.py @@ -52,7 +52,8 @@ def _release_and_log(self): LOGGER.log_output(stdout) if stderr: LOGGER.log_output(stderr) - sys.__stderr__.write(console_encode(stderr, stream=sys.__stderr__)) + if sys.__stderr__: + sys.__stderr__.write(console_encode(stderr, stream=sys.__stderr__)) def _release(self): stdout = self.stdout.release() diff --git a/src/robot/running/signalhandler.py b/src/robot/running/signalhandler.py index f09d5b182e5..3a5199ec6cd 100644 --- a/src/robot/running/signalhandler.py +++ b/src/robot/running/signalhandler.py @@ -31,14 +31,18 @@ def __init__(self): def __call__(self, signum, frame): self._signal_count += 1 - LOGGER.info('Received signal: %s.' % signum) + LOGGER.info(f'Received signal: {signum}.') if self._signal_count > 1: - sys.__stderr__.write('Execution forcefully stopped.\n') + self._write_to_stderr('Execution forcefully stopped.') raise SystemExit() - sys.__stderr__.write('Second signal will force exit.\n') + self._write_to_stderr('Second signal will force exit.') if self._running_keyword: self._stop_execution_gracefully() + def _write_to_stderr(self, message): + if sys.__stderr__: + sys.__stderr__.write(message + '\n') + def _stop_execution_gracefully(self): raise ExecutionFailed('Execution terminated by signal', exit=True) diff --git a/src/robot/running/status.py b/src/robot/running/status.py index 72c7bcede0a..1fac5c61cdb 100644 --- a/src/robot/running/status.py +++ b/src/robot/running/status.py @@ -29,9 +29,13 @@ def __init__(self): self.teardown_skipped = None def __bool__(self): - return bool( - self.setup or self.test or self.teardown or - self.setup_skipped or self.test_skipped or self.teardown_skipped + return ( + self.setup is not None + or self.test is not None + or self.teardown is not None + or self.setup_skipped is not None + or self.test_skipped is not None + or self.teardown_skipped is not None ) diff --git a/src/robot/running/userkeywordrunner.py b/src/robot/running/userkeywordrunner.py index a2dd9868907..bc152e20f2b 100644 --- a/src/robot/running/userkeywordrunner.py +++ b/src/robot/running/userkeywordrunner.py @@ -85,6 +85,10 @@ def _run(self, data: KeywordData, kw: 'UserKeyword', result: KeywordResult, cont with context.timeout(timeout): exception, return_value = self._execute(kw, result, context) if exception and not exception.can_continue(context): + if context.in_teardown and exception.keyword_timeout: + # Allow execution to continue on teardowns after timeout. + # https://github.com/robotframework/robotframework/issues/3398 + exception.keyword_timeout = False raise exception return_value = self._handle_return_value(return_value, variables) if exception: diff --git a/src/robot/version.py b/src/robot/version.py index 72903eaab5d..70273c20308 100644 --- a/src/robot/version.py +++ b/src/robot/version.py @@ -18,7 +18,7 @@ # Version number typically updated by running `invoke set-version `. # Run `invoke --help set-version` or see tasks.py for details. -VERSION = '7.1.1.dev1' +VERSION = '7.1.2.dev1' def get_version(naked=False):