Skip to content

Commit

Permalink
Allow multiplication in duration input value (#2845)
Browse files Browse the repository at this point in the history
To make it easier for adjust the multiplication happens last: No need to
pay attention how adjust creates the value.
  • Loading branch information
lukaszachy authored May 29, 2024
1 parent bde322d commit ae941d1
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 16 deletions.
6 changes: 6 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
Releases
======================

tmt-1.34
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The :ref:`/spec/tests/duration` now supports multiplication.


tmt-1.33
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
31 changes: 25 additions & 6 deletions spec/tests/duration.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ story:
As a test harness I need to know after how long time I should
kill test if it is still running to prevent resource wasting.

description:
In order to prevent stuck tests consuming resources we define a
maximum time for test execution. If the limit is exceeded the
running test is killed by the test harness. Use the same
format as the ``sleep`` command. Must be a ``string``. The
default value is ``5m``.
description: |
In order to prevent stuck tests from consuming resources, we define a
maximum time for test execution. If the limit is exceeded, the
running test is killed by the test harness. Value extends the
format of the ``sleep`` command by allowing multiplication (``*[float]``).
First, all time values are summed together, and only then are they multiplied.
The final value is then rounded up to the whole number.

Must be a ``string``. The default value is ``5m``.

.. versionadded:: 1.34
Multiplication

example:
- |
Expand All @@ -28,13 +34,26 @@ example:
# Combination & repetition of time suffixes (total 4h 2m 3s)
duration: 1h 3h 2m 3

- |
# Multiplication is evaluated last (total 24s: 2s * 3 * 4)
duration: *3 2s *4

- |
# Use context adjust to extend duration for given arch
duration: 5m
adjust:
duration+: 15m
when: arch == aarch64

- |
# Use context adjust to scale duration for given arch
duration: 5m
adjust:
duration+: *2
when: arch == aarch64
duration+: *0.9
when: arch == s390x


link:
- implemented-by: /tmt/base.py
Expand Down
25 changes: 21 additions & 4 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def create_workdir():


def test_duration_to_seconds():
""" Check conversion from sleep time format to seconds """
""" Check conversion from extended sleep time format to seconds """
assert duration_to_seconds(5) == 5
assert duration_to_seconds('5') == 5
assert duration_to_seconds('5s') == 5
Expand All @@ -332,10 +332,27 @@ def test_duration_to_seconds():
# Divergence from 'sleep' as that expects space separated arguments
assert duration_to_seconds('1s2s') == 3
assert duration_to_seconds('1 m2 m') == 180
# Allow multiply but sum first, then multiply: (60+4) * (2+3)
assert duration_to_seconds('*2 1m *3 4') == 384
assert duration_to_seconds('*2 *3 1m4') == 384
# Round up
assert duration_to_seconds('1s *3.3') == 4


@pytest.mark.parametrize("duration", [
'*10m',
'**10',
'10w',
'1sm',
'*10m 3',
'3 *10m',
'1 1ss 5',
'bad',
])
def test_duration_to_seconds_invalid(duration):
""" Catch invalid input duration string """
with pytest.raises(tmt.utils.SpecificationError):
duration_to_seconds('bad')
with pytest.raises(tmt.utils.SpecificationError):
duration_to_seconds('1sm')
duration_to_seconds(duration)


class TestStructuredField(unittest.TestCase):
Expand Down
2 changes: 1 addition & 1 deletion tmt/schemas/common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ definitions:
# https://tmt.readthedocs.io/en/stable/spec/plans.html#script
duration:
type: string
pattern: "^[0-9]+[smhd]?$"
pattern: "^([0-9*. ]+[smhd]? *)+$"

# https://tmt.readthedocs.io/en/stable/spec/tests.html#environment
# https://tmt.readthedocs.io/en/stable/spec/plans.html#environment
Expand Down
44 changes: 39 additions & 5 deletions tmt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from collections import Counter, OrderedDict
from collections.abc import Iterable, Iterator, Sequence
from contextlib import suppress
from math import ceil
from re import Match, Pattern
from threading import Thread
from types import ModuleType
Expand Down Expand Up @@ -3346,19 +3347,52 @@ def shell_variables(


def duration_to_seconds(duration: str) -> int:
""" Convert sleep time format into seconds """
""" Convert extended sleep time format into seconds """
units = {
's': 1,
'm': 60,
'h': 60 * 60,
'd': 60 * 60 * 24,
}
if re.match(r'^(\d+ *?[smhd]? *)+$', str(duration)) is None:
# Couldn't create working validation regexp to accept '2 1m 4'
# thus fixing the string so \b can be used as word boundary
fixed_duration = re.sub(r'([smhd])(\d)', r'\1 \2', str(duration))
fixed_duration = re.sub(r'\s\s+', ' ', fixed_duration)
raw_groups = r'''
( # Group all possibilities
( # Multiply by float number
(?P<asterisk>\*) # "*" character
\s*
(?P<float>\d+(\.\d+)?(?![smhd])) # float part
\s*
)
| # Or
( # Time pattern
(?P<digit>\d+) # digits
\s*
(?P<suffix>[smhd])? # suffix
\s*
)
)\b # Needs to end with word boundary to avoid splitting
'''
re_validate = re.compile(r'''
^( # Match beginning, opening of input group
''' + raw_groups + r'''
\s* # Optional spaces in the case of multiple inputs
)+$ # Inputs can repeat
''', re.VERBOSE)
re_split = re.compile(raw_groups, re.VERBOSE)
if re_validate.match(fixed_duration) is None:
raise SpecificationError(f"Invalid duration '{duration}'.")
total_time = 0
for number, suffix in re.findall(r'(\d+) *([smhd]?)', str(duration)):
total_time += int(number) * units.get(suffix, 1)
return total_time
multiply_by = 1.0
for match in re_split.finditer(fixed_duration):
if match['asterisk'] == '*':
multiply_by *= float(match['float'])
else:
total_time += int(match['digit']) * units.get(match['suffix'], 1)
# Multiply in the end and round up
return ceil(total_time * multiply_by)


@overload
Expand Down

0 comments on commit ae941d1

Please sign in to comment.