Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ The important bit here is the following line that sets the time limit for the te
:lines: 17
:dedent: 8

The :attr:`time_limit <reframe.core.pipeline.RegressionTest.time_limit>` attribute is a three-tuple in the form ``(HOURS, MINUTES, SECONDS)``.
The :attr:`time_limit <reframe.core.pipeline.RegressionTest.time_limit>` attribute is a string in the form ``<DAYS>d<HOURS>h<MINUTES>m<SECONDS>s)``.
Time limits are implemented for all the scheduler backends.

The sanity condition for this test verifies that associated job has been canceled due to the time limit (note that this message is SLURM-specific).
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ For schedulers that do not provide the same functionality, some of the variables
================================================ ===========================================
:class:`RegressionTest` attribute Corresponding SLURM option
================================================ ===========================================
``time_limit = (0, 10, 30)`` ``--time=00:10:30``
``time_limit = '10m30s`` ``--time=00:10:30``
``use_multithreading = True`` ``--hint=multithread``
``use_multithreading = False`` ``--hint=nomultithread``
``exclusive_access = True`` ``--exclusive``
Expand Down
33 changes: 22 additions & 11 deletions reframe/core/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
#

import copy
import datetime
import os
import re

import reframe.utility.typecheck as types
from reframe.core.exceptions import user_deprecation_warning
Expand Down Expand Up @@ -105,23 +107,32 @@ def __set__(self, obj, value):


class TimerField(TypedField):
'''Stores a timer in the form of a tuple ``(hh, mm, ss)``'''
'''Stores a timer in the form of a :class:`datetime.timedelta` object'''

def __init__(self, fieldname, *other_types):
super().__init__(fieldname, types.Tuple[int, int, int], *other_types)
super().__init__(fieldname, datetime.timedelta, str,
types.Tuple[int, int, int], *other_types)

def __set__(self, obj, value):
self._check_type(value)
if value is not None:
# Check also the values for minutes and seconds
if isinstance(value, tuple):
user_deprecation_warning(
"setting a timer field from a tuple is deprecated: "
"please use a string '<days>d<hours>h<minutes>m<seconds>s'")
h, m, s = value
if h < 0 or m < 0 or s < 0:
raise ValueError('timer field must have '
'non-negative values')

if m > 59 or s > 59:
raise ValueError('minutes and seconds in a timer '
'field must not exceed 59')
value = datetime.timedelta(hours=h, minutes=m, seconds=s)

if isinstance(value, str):
time_match = re.match(r'^((?P<days>\d+)d)?'
r'((?P<hours>\d+)h)?'
r'((?P<minutes>\d+)m)?'
r'((?P<seconds>\d+)s)?$',
value)
if not time_match:
raise ValueError('invalid format for timer field')

value = datetime.timedelta(
**{k: int(v) for k, v in time_match.groupdict().items() if v})

# Call Field's __set__() method, type checking is already performed
Field.__set__(self, obj, value)
Expand Down
4 changes: 3 additions & 1 deletion reframe/core/launchers/mpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from reframe.core.launchers import JobLauncher
from reframe.core.launchers.registry import register_launcher
from reframe.utility import seconds_to_hms


@register_launcher('srun')
Expand Down Expand Up @@ -57,7 +58,8 @@ def command(self, job):
ret += ['--job-name=%s' % job.name]

if job.time_limit:
ret += ['--time=%d:%d:%d' % job.time_limit]
h, m, s = seconds_to_hms(job.time_limit.total_seconds())
ret += ['--time=%d:%d:%d' % (h, m, s)]

if job.stdout:
ret += ['--output=%s' % job.stdout]
Expand Down
18 changes: 13 additions & 5 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,20 +545,28 @@ class RegressionTest(metaclass=RegressionTestMeta):

#: Time limit for this test.
#:
#: Time limit is specified as a three-tuple in the form ``(hh, mm, ss)``,
#: with ``hh >= 0``, ``0 <= mm <= 59`` and ``0 <= ss <= 59``.
#: Time limit is specified as a string in the form
#: ``<days>d<hours>h<minutes>m<seconds>s``.
#: If set to :class:`None`, no time limit will be set.
#: The default time limit of the system partition's scheduler will be used.
#:
#: The value is internaly kept as a :class:`datetime.timedelta` object.
#: For example '2h30m' is represented as
#: `datetime.timedelta(hours=2, minutes=30)`
#:
#: :type: :class:`tuple[int]`
#: :default: ``(0, 10, 0)``
#: :type: :class:`str` or :class:`datetime.timedelta`
#: :default: ``'10m'``
#:
#: .. note::
#: .. versionchanged:: 2.15
#:
#: This attribute may be set to :class:`None`.
#:
#: .. warning::
#: .. versionchanged:: 3.0
#:
#: The old syntax using a ``(h, m, s)`` tuple is deprecated.
#:
time_limit = fields.TimerField('time_limit', type(None))

#: Extra resources for this test.
Expand Down Expand Up @@ -715,7 +723,7 @@ def _rfm_init(self, name=None, prefix=None):
self.variables = {}

# Time limit for the check
self.time_limit = (0, 10, 0)
self.time_limit = '10m'

# Runtime information of the test
self._current_partition = None
Expand Down
7 changes: 3 additions & 4 deletions reframe/core/schedulers/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import stat
import subprocess
import time
from datetime import datetime
from datetime import datetime, timedelta

import reframe.core.schedulers as sched
import reframe.utility.os_ext as os_ext
Expand Down Expand Up @@ -125,7 +125,7 @@ def cancel(self, job):

# Set the time limit to the grace period and let wait() do the final
# killing
job.time_limit = (0, 0, self._cancel_grace_period)
job.time_limit = timedelta(seconds=self._cancel_grace_period)
self.wait(job)

def wait(self, job):
Expand All @@ -143,8 +143,7 @@ def wait(self, job):

# Convert job's time_limit to seconds
if job.time_limit is not None:
h, m, s = job.time_limit
timeout = h * 3600 + m * 60 + s
timeout = job.time_limit.total_seconds()
else:
timeout = 0

Expand Down
4 changes: 3 additions & 1 deletion reframe/core/schedulers/pbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from reframe.core.exceptions import SpawnedProcessError, JobError
from reframe.core.logging import getlogger
from reframe.core.schedulers.registry import register_scheduler
from reframe.utility import seconds_to_hms


# Time to wait after a job is finished for its standard output/error to be
Expand Down Expand Up @@ -79,8 +80,9 @@ def emit_preamble(self, job):
]

if job.time_limit is not None:
h, m, s = seconds_to_hms(job.time_limit.total_seconds())
preamble.append(
self._format_option('-l walltime=%d:%d:%d' % job.time_limit))
self._format_option('-l walltime=%d:%d:%d' % (h, m, s)))

if job.sched_partition:
preamble.append(
Expand Down
4 changes: 3 additions & 1 deletion reframe/core/schedulers/slurm.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
JobBlockedError, JobError)
from reframe.core.logging import getlogger
from reframe.core.schedulers.registry import register_scheduler
from reframe.utility import seconds_to_hms


def slurm_state_completed(state):
Expand Down Expand Up @@ -155,8 +156,9 @@ def emit_preamble(self, job):
self._format_option(job.stderr, errfile_fmt)]

if job.time_limit is not None:
h, m, s = seconds_to_hms(job.time_limit.total_seconds())
preamble.append(
self._format_option('%d:%d:%d' % job.time_limit, '--time={0}')
self._format_option('%d:%d:%d' % (h, m, s), '--time={0}')
)

if job.sched_exclusive_access:
Expand Down
8 changes: 8 additions & 0 deletions reframe/utility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
from collections import UserDict


def seconds_to_hms(seconds):
'''Convert time in seconds to a tuple of ``(hour, minutes, seconds)``.'''

m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
return h, m, s


def _get_module_name(filename):
barename, _ = os.path.splitext(filename)
if os.path.basename(filename) == '__init__.py':
Expand Down
2 changes: 1 addition & 1 deletion tutorial/advanced/advanced_example5.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self):
'of a user-defined time limit')
self.valid_systems = ['daint:gpu', 'daint:mc']
self.valid_prog_environs = ['*']
self.time_limit = (0, 1, 0)
self.time_limit = '1m'
self.executable = 'sleep'
self.executable_opts = ['100']
self.sanity_patterns = sn.assert_found(
Expand Down
68 changes: 43 additions & 25 deletions unittests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
#
# SPDX-License-Identifier: BSD-3-Clause

import datetime
import os
import pytest
import unittest

import reframe.core.fields as fields
from reframe.core.exceptions import ReframeDeprecationWarning
from reframe.utility import ScopedDict


Expand Down Expand Up @@ -87,31 +90,48 @@ class FieldTester:
'field_maybe_none', type(None))

tester = FieldTester()
tester.field = (65, 22, 47)
tester.field = '1d65h22m87s'
tester.field_maybe_none = None

self.assertIsInstance(FieldTester.field, fields.TimerField)
self.assertEqual((65, 22, 47), tester.field)
self.assertRaises(TypeError, exec, 'tester.field = (2,)',
globals(), locals())
self.assertRaises(TypeError, exec, 'tester.field = (2, 2)',
globals(), locals())
self.assertRaises(TypeError, exec, 'tester.field = (2, 2, 3.4)',
globals(), locals())
self.assertRaises(TypeError, exec, "tester.field = ('foo', 2, 3)",
globals(), locals())
self.assertRaises(TypeError, exec, 'tester.field = 3',
globals(), locals())
self.assertRaises(ValueError, exec, 'tester.field = (-2, 3, 5)',
globals(), locals())
self.assertRaises(ValueError, exec, 'tester.field = (100, -3, 4)',
globals(), locals())
self.assertRaises(ValueError, exec, 'tester.field = (100, 3, -4)',
globals(), locals())
self.assertRaises(ValueError, exec, 'tester.field = (100, 65, 4)',
globals(), locals())
self.assertRaises(ValueError, exec, 'tester.field = (100, 3, 65)',
globals(), locals())
assert isinstance(FieldTester.field, fields.TimerField)
assert (datetime.timedelta(days=1, hours=65,
minutes=22, seconds=87) == tester.field)
tester.field = datetime.timedelta(days=1, hours=65,
minutes=22, seconds=87)
assert (datetime.timedelta(days=1, hours=65,
minutes=22, seconds=87) == tester.field)
tester.field = ''
assert (datetime.timedelta(days=0, hours=0,
minutes=0, seconds=0) == tester.field)
with pytest.warns(ReframeDeprecationWarning):
tester.field = (65, 22, 87)

with pytest.raises(ValueError):
exec('tester.field = "1e"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "-10m5s"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "10m-5s"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "m10s"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "10m10"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "10m10m1s"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "10m5s3m"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "10ms"', globals(), locals())

with pytest.raises(ValueError):
exec('tester.field = "10"', globals(), locals())

def test_proxy_field(self):
class Target:
Expand All @@ -136,8 +156,6 @@ class Proxy:
self.assertEqual(4, t.b)

def test_deprecated_field(self):
from reframe.core.exceptions import ReframeDeprecationWarning

class FieldTester:
value = fields.DeprecatedField(fields.TypedField('value', int),
'value field is deprecated')
Expand Down
2 changes: 1 addition & 1 deletion unittests/test_launchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def setUp(self):
self.job.num_tasks_per_socket = 1
self.job.num_cpus_per_task = 2
self.job.use_smt = True
self.job.time_limit = (0, 10, 0)
self.job.time_limit = '10m'
self.job.options += ['--gres=gpu:4', '#DW jobdw anything']
self.job.launcher.options = ['--foo']
self.minimal_job = Job.create(FakeJobScheduler(),
Expand Down
6 changes: 3 additions & 3 deletions unittests/test_schedulers.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def assertScriptSanity(self, script_file):

def setup_job(self):
# Mock up a job submission
self.testjob.time_limit = (0, 5, 0)
self.testjob.time_limit = '5m'
self.testjob.num_tasks = 16
self.testjob.num_tasks_per_node = 2
self.testjob.num_tasks_per_core = 1
Expand Down Expand Up @@ -134,7 +134,7 @@ def test_submit(self):
def test_submit_timelimit(self, check_elapsed_time=True):
self.setup_user()
self.parallel_cmd = 'sleep 10'
self.testjob.time_limit = (0, 0, 2)
self.testjob.time_limit = '2s'
self.prepare()
t_job = datetime.now()
self.testjob.submit()
Expand Down Expand Up @@ -243,7 +243,7 @@ def test_cancel_with_grace(self):
self.parallel_cmd = 'sleep 5 &'
self.pre_run = ['trap -- "" TERM']
self.post_run = ['echo $!', 'wait']
self.testjob.time_limit = (0, 1, 0)
self.testjob.time_limit = '1m'
self.testjob.scheduler._cancel_grace_period = 2

self.prepare()
Expand Down