Skip to content

Commit

Permalink
Block timeout introduction
Browse files Browse the repository at this point in the history
This commit introduces a new timeout feature, which adds the possibility
to set a specific timeout for a block of code with context manager like
this:

	with self.wait_max(3):
		#code which should take max 3 seconds
		...

The  `wait_max` method will send `SIGALRM` if the code doesn't end
within 3 seconds. This signal will be caught by avocado-instrumented
runner, which will interrupt the test, the same way as with a regular
timeout.

Reference: avocado-framework#5994
Signed-off-by: Jan Richter <[email protected]>
  • Loading branch information
richtja committed Sep 17, 2024
1 parent 536b6f7 commit 506225e
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 2 deletions.
21 changes: 21 additions & 0 deletions avocado/core/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
import logging
import os
import shutil
import signal
import sys
import tempfile
import time
import unittest
import warnings
from contextlib import contextmanager

from avocado.core import exceptions, parameters
from avocado.core.settings import settings
Expand Down Expand Up @@ -514,6 +516,25 @@ def phase(self):
"""
return self.__phase

@contextmanager
def wait_max(self, timeout):
"""
Context manager for getting block of code with its specific timeout.
Usage:
with self.wait_max(3):
# code which should take max 3 seconds
...
:param timeout: Timeout in seconds for block of code.
:type timeout: int
"""
timeout_factor = float(self.params.get("timeout_factor", default=1.0))
timeout = round(float(timeout) * timeout_factor)
signal.alarm(timeout)
yield timeout
signal.alarm(0)

def __str__(self):
return str(self.name)

Expand Down
3 changes: 2 additions & 1 deletion avocado/plugins/runners/avocado_instrumented.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class and method names should be separated by a ":". One

@staticmethod
def signal_handler(signum, frame): # pylint: disable=W0613
if signum == signal.SIGTERM.value:
if signum in [signal.SIGTERM.value, signal.SIGALRM.value]:
raise TestInterrupt("Test interrupted: Timeout reached")

@staticmethod
Expand Down Expand Up @@ -77,6 +77,7 @@ def _run_avocado(runnable, queue):
# To be defined: if the resolution uri should be composed like
# this, or broken down and stored into other data fields
signal.signal(signal.SIGTERM, AvocadoInstrumentedTestRunner.signal_handler)
signal.signal(signal.SIGALRM, AvocadoInstrumentedTestRunner.signal_handler)
module_path, klass_method = runnable.uri.split(":", 1)

klass, method = klass_method.split(".", 1)
Expand Down
16 changes: 16 additions & 0 deletions docs/source/guides/writer/chapters/writing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,19 @@ runner task, making it raise a
process is specific to spawner implementation, for more information
see :class:`avocado.core.plugin_interfaces.Spawner.terminate_task`.
Block Timeout
-------------
On more complex (and thus usually) longer tests, there may be multiple
steps to complete. It may be known that some of these steps should not
take more than a small percentage of the overall expected time for the
test as a whole. Therefore, it is not convenient to set the timeout for
the whole test, but it would be better to have timeout for each of those
steps. For such use-case avocado supports `wait_max` context manager,
which let you set specific timeout (in seconds) for a block of code:
.. literalinclude:: ../../../../../examples/tests/blocktimeouttest.py
Timeout Factor
~~~~~~~~~~~~~~
Expand Down Expand Up @@ -810,6 +823,9 @@ test logs. For the previous test execution it shows::
...
[stdlog] 2023-11-29 11:16:23,746 test L0354 DEBUG| actual timeout: 6.0
.. note:: Be aweare that timeout factor will also affect timeouts created by `wait_max`` context manager.
Skipping Tests
--------------
Expand Down
22 changes: 22 additions & 0 deletions examples/tests/blocktimeouttest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import time

from avocado import Test


class TimeoutTest(Test):
"""
Functional test for avocado. Throw a TestTimeoutError.
:param sleep_time: How long should the test sleep
"""

def test(self):
"""
This should throw a TestTimeoutError.
"""
with self.wait_max(3):
sleep_time = float(self.params.get("sleep_time", default=5.0))
self.log.info(
"Sleeping for %.2f seconds (2 more than the timeout)", sleep_time
)
time.sleep(sleep_time)
2 changes: 1 addition & 1 deletion selftests/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"nrunner-requirement": 28,
"unit": 678,
"jobs": 11,
"functional-parallel": 307,
"functional-parallel": 308,
"functional-serial": 7,
"optional-plugins": 0,
"optional-plugins-golang": 2,
Expand Down
26 changes: 26 additions & 0 deletions selftests/functional/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,32 @@ def test_runner_timeout(self):
# Ensure no test aborted error messages show up
self.assertNotIn(b"TestAbortError: Test aborted unexpectedly", output)

def test_runner_block_timeout(self):
cmd_line = (
f"{AVOCADO} run --disable-sysinfo --job-results-dir "
f"{self.tmpdir.name} examples/tests/blocktimeouttest.py"
)
result = process.run(cmd_line, ignore_status=True)
json_path = os.path.join(self.tmpdir.name, "latest", "results.json")
with open(json_path, encoding="utf-8") as json_file:
result_json = json.load(json_file)
output = result.stdout
expected_rc = exit_codes.AVOCADO_JOB_INTERRUPTED
unexpected_rc = exit_codes.AVOCADO_FAIL
self.assertNotEqual(
result.exit_status,
unexpected_rc,
f"Avocado crashed (rc {unexpected_rc}):\n{result}",
)
self.assertEqual(
result.exit_status,
expected_rc,
f"Avocado did not return rc {expected_rc}:\n{result}",
)
self.assertIn("Timeout reached", result_json["tests"][0]["fail_reason"])
# Ensure no test aborted error messages show up
self.assertNotIn(b"TestAbortError: Test aborted unexpectedly", output)

def test_runner_timeout_factor(self):
cmd_line = (
f"{AVOCADO} run --disable-sysinfo --job-results-dir "
Expand Down

0 comments on commit 506225e

Please sign in to comment.