Skip to content

Commit cc67d00

Browse files
author
Vasileios Karakasis
authored
Merge branch 'master' into test/fftw_new_syntax
2 parents 1fc8794 + 78e7ce2 commit cc67d00

File tree

18 files changed

+567
-92
lines changed

18 files changed

+567
-92
lines changed

docs/manpage.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,60 @@ Options controlling ReFrame execution
386386
.. versionchanged:: 3.6.1
387387
Multiple report files are now accepted.
388388

389+
.. option:: -S, --setvar=[TEST.]VAR=VAL
390+
391+
Set variable ``VAR`` in all tests or optionally only in test ``TEST`` to ``VAL``.
392+
393+
Multiple variables can be set at the same time by passing this option multiple times.
394+
This option *cannot* change arbitrary test attributes, but only test variables declared with the :attr:`~reframe.core.pipeline.RegressionMixin.variable` built-in.
395+
If an attempt is made to change an inexistent variable or a test parameter, a warning will be issued.
396+
397+
ReFrame will try to convert ``VAL`` to the type of the variable.
398+
If it does not succeed, a warning will be issued and the variable will not be set.
399+
``VAL`` can take the special value ``@none`` to denote that the variable must be set to :obj:`None`.
400+
401+
Sequence and mapping types can also be set from the command line by using the following syntax:
402+
403+
- Sequence types: ``-S seqvar=1,2,3,4``
404+
- Mapping types: ``-S mapvar=a:1,b:2,c:3``
405+
406+
Conversions to arbitrary objects are also supported.
407+
See :class:`~reframe.utility.typecheck.ConvertibleType` for more details.
408+
409+
410+
The optional ``TEST.`` prefix refers to the test class name, *not* the test name.
411+
412+
Variable assignments passed from the command line happen *before* the test is instantiated and is the exact equivalent of assigning a new value to the variable *at the end* of the test class body.
413+
This has a number of implications that users of this feature should be aware of:
414+
415+
- In the following test, :attr:`num_tasks` will have always the value ``1`` regardless of any command-line assignment of the variable :attr:`foo`:
416+
417+
.. code-block:: python
418+
419+
@rfm.simple_test
420+
class my_test(rfm.RegressionTest):
421+
foo = variable(int, value=1)
422+
num_tasks = foo
423+
424+
- If the variable is set in any pipeline hook, the command line assignment will have an effect until the variable assignment in the pipeline hook is reached.
425+
The variable will be then overwritten.
426+
- The `test filtering <#test-filtering>`__ happens *after* a test is instantiated, so the only way to scope a variable assignment is to prefix it with the test class name.
427+
However, this has some positive side effects:
428+
429+
- Passing ``-S valid_systems='*'`` and ``-S valid_prog_environs='*'`` is the equivalent of passing the :option:`--skip-system-check` and :option:`--skip-prgenv-check` options.
430+
- Users could alter the behavior of tests based on tag values that they pass from the command line, by changing the behavior of a test in a post-init hook based on the value of the :attr:`~reframe.core.pipeline.RegressionTest.tags` attribute.
431+
- Users could force a test with required variables to run if they set these variables from the command line.
432+
For example, the following test could only be run if invoked with ``-S num_tasks=<NUM>``:
433+
434+
.. code-block:: python
435+
436+
@rfm.simple_test
437+
class my_test(rfm.RegressionTest):
438+
num_tasks = required
439+
440+
.. versionadded:: 3.8.0
441+
442+
389443
----------------------------------
390444
Options controlling job submission
391445
----------------------------------

reframe/core/fields.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,26 @@
1515
from reframe.utility import ScopedDict
1616

1717

18+
class _Convertible:
19+
'''Wrapper for values that allowed to be converted implicitly'''
20+
21+
__slots__ = ('value')
22+
23+
def __init__(self, value):
24+
self.value = value
25+
26+
27+
def make_convertible(value):
28+
return _Convertible(value)
29+
30+
31+
def remove_convertible(value):
32+
if isinstance(value, _Convertible):
33+
return value.value
34+
else:
35+
return value
36+
37+
1838
class Field:
1939
'''Base class for attribute validators.'''
2040

@@ -34,7 +54,7 @@ def __get__(self, obj, objtype):
3454
(objtype.__name__, self._name)) from None
3555

3656
def __set__(self, obj, value):
37-
obj.__dict__[self._name] = value
57+
obj.__dict__[self._name] = remove_convertible(value)
3858

3959

4060
class TypedField(Field):
@@ -46,6 +66,10 @@ def __init__(self, main_type, *other_types):
4666
raise TypeError('{0} is not a sequence of types'.
4767
format(self._types))
4868

69+
@property
70+
def valid_types(self):
71+
return self._types
72+
4973
def _check_type(self, value):
5074
if not any(isinstance(value, t) for t in self._types):
5175
typedescr = '|'.join(t.__name__ for t in self._types)
@@ -54,8 +78,33 @@ def _check_type(self, value):
5478
(self._name, value, typedescr))
5579

5680
def __set__(self, obj, value):
57-
self._check_type(value)
58-
super().__set__(obj, value)
81+
try:
82+
self._check_type(value)
83+
except TypeError:
84+
raw_value = remove_convertible(value)
85+
if raw_value is value:
86+
# value was not convertible; reraise
87+
raise
88+
89+
# Try to convert value to any of the supported types
90+
value = raw_value
91+
for t in self._types:
92+
try:
93+
value = t(value)
94+
except TypeError:
95+
continue
96+
else:
97+
super().__set__(obj, value)
98+
return
99+
100+
# Conversion failed
101+
raise TypeError(
102+
f'failed to set field {self._name!r}: '
103+
f'could not convert to any of the supported types: '
104+
f'{self._types}'
105+
)
106+
else:
107+
super().__set__(obj, value)
59108

60109

61110
class ConstantField(Field):
@@ -88,6 +137,7 @@ def __init__(self, *other_types):
88137
super().__init__(str, int, float, *other_types)
89138

90139
def __set__(self, obj, value):
140+
value = remove_convertible(value)
91141
self._check_type(value)
92142
if isinstance(value, str):
93143
time_match = re.match(r'^((?P<days>\d+)d)?'
@@ -119,6 +169,7 @@ def __init__(self, valuetype, *other_types):
119169
ScopedDict, *other_types)
120170

121171
def __set__(self, obj, value):
172+
value = remove_convertible(value)
122173
self._check_type(value)
123174
if not isinstance(value, ScopedDict):
124175
value = ScopedDict(value) if value is not None else value

reframe/core/meta.py

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ def __getitem__(self, key):
136136
# raise the exception from the base __getitem__.
137137
raise err from None
138138

139+
def reset(self, key):
140+
'''Reset an item to rerun it through the __setitem__ logic.'''
141+
self[key] = self[key]
142+
139143
class WrappedFunction:
140144
'''Descriptor to wrap a free function as a bound-method.
141145
@@ -212,14 +216,32 @@ def __prepare__(metacls, name, bases, **kwargs):
212216
namespace['required'] = variables.Undefined
213217

214218
# Utility decorators
219+
namespace['_rfm_ext_bound'] = set()
220+
215221
def bind(fn, name=None):
216222
'''Directive to bind a free function to a class.
217223
218224
See online docs for more information.
225+
226+
.. note::
227+
Functions bound using this directive must be re-inspected after
228+
the class body execution has completed. This directive attaches
229+
the external method into the class namespace and returns the
230+
associated instance of the :class:`WrappedFunction`. However,
231+
this instance may be further modified by other ReFrame builtins
232+
such as :func:`run_before`, :func:`run_after`, :func:`final` and
233+
so on after it was added to the namespace, which would bypass
234+
the logic implemented in the :func:`__setitem__` method from the
235+
:class:`MetaNamespace` class. Hence, we track the items set by
236+
this directive in the ``_rfm_ext_bound`` set, so they can be
237+
later re-inspected.
219238
'''
220239

221240
inst = metacls.WrappedFunction(fn, name)
222241
namespace[inst.__name__] = inst
242+
243+
# Track the imported external functions
244+
namespace['_rfm_ext_bound'].add(inst.__name__)
223245
return inst
224246

225247
def final(fn):
@@ -324,6 +346,10 @@ class was created or even at the instance level (e.g. doing
324346
for b in directives:
325347
namespace.pop(b, None)
326348

349+
# Reset the external functions imported through the bind directive.
350+
for item in namespace.pop('_rfm_ext_bound'):
351+
namespace.reset(item)
352+
327353
return super().__new__(metacls, name, bases, dict(namespace), **kwargs)
328354

329355
def __init__(cls, name, bases, namespace, **kwargs):
@@ -473,6 +499,41 @@ def __getattr__(cls, name):
473499
f'class {cls.__qualname__!r} has no attribute {name!r}'
474500
) from None
475501

502+
def setvar(cls, name, value):
503+
'''Set the value of a variable.
504+
505+
:param name: The name of the variable.
506+
:param value: The value of the variable.
507+
508+
:returns: :class:`True` if the variable was set.
509+
A variable will *not* be set, if it does not exist or when an
510+
attempt is made to set it with its underlying descriptor.
511+
This happens during the variable injection time and it should be
512+
delegated to the class' :func:`__setattr__` method.
513+
514+
:raises ReframeSyntaxError: If an attempt is made to override a
515+
variable with a descriptor other than its underlying one.
516+
517+
'''
518+
519+
try:
520+
var_space = super().__getattribute__('_rfm_var_space')
521+
if name in var_space:
522+
if not hasattr(value, '__get__'):
523+
var_space[name].define(value)
524+
return True
525+
elif var_space[name].field is not value:
526+
desc = '.'.join([cls.__qualname__, name])
527+
raise ReframeSyntaxError(
528+
f'cannot override variable descriptor {desc!r}'
529+
)
530+
else:
531+
# Variable is being injected
532+
return False
533+
except AttributeError:
534+
'''Catch early access attempt to the variable space.'''
535+
return False
536+
476537
def __setattr__(cls, name, value):
477538
'''Handle the special treatment required for variables and parameters.
478539
@@ -489,31 +550,20 @@ class attribute. This behavior does not apply when the assigned value
489550
is not allowed. This would break the parameter space internals.
490551
'''
491552

492-
# Set the value of a variable (except when the value is a descriptor).
493-
try:
494-
var_space = super().__getattribute__('_rfm_var_space')
495-
if name in var_space:
496-
if not hasattr(value, '__get__'):
497-
var_space[name].define(value)
498-
return
499-
elif not var_space[name].field is value:
500-
desc = '.'.join([cls.__qualname__, name])
501-
raise ReframeSyntaxError(
502-
f'cannot override variable descriptor {desc!r}'
503-
)
504-
505-
except AttributeError:
506-
pass
553+
# Try to treat `name` as variable
554+
if cls.setvar(name, value):
555+
return
507556

508-
# Catch attempts to override a test parameter
557+
# Try to treat `name` as a parameter
509558
try:
559+
# Catch attempts to override a test parameter
510560
param_space = super().__getattribute__('_rfm_param_space')
511561
if name in param_space.params:
512562
raise ReframeSyntaxError(f'cannot override parameter {name!r}')
513-
514563
except AttributeError:
515-
pass
564+
'''Catch early access attempt to the parameter space.'''
516565

566+
# Treat `name` as normal class attribute
517567
super().__setattr__(name, value)
518568

519569
@property

reframe/core/pipeline.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ def pipeline_hooks(cls):
213213
#: The name of the test.
214214
#:
215215
#: :type: string that can contain any character except ``/``
216+
#: :default: For non-parameterised tests, the default name is the test
217+
#: class name. For parameterised tests, the default name is constructed
218+
#: by concatenating the test class name and the string representations
219+
#: of every test parameter: ``TestClassName_<param1>_<param2>``.
220+
#: Any non-alphanumeric value in a parameter's representation is
221+
#: converted to ``_``.
216222
name = variable(typ.Str[r'[^\/]+'])
217223

218224
#: List of programming environments supported by this test.
@@ -1711,7 +1717,7 @@ def check_performance(self):
17111717
if self.perf_variables or self._rfm_perf_fns:
17121718
if hasattr(self, 'perf_patterns'):
17131719
raise ReframeSyntaxError(
1714-
f"assigning a value to 'perf_pattenrs' conflicts ",
1720+
f"assigning a value to 'perf_patterns' conflicts ",
17151721
f"with using the 'performance_function' decorator ",
17161722
f"or setting a value to 'perf_variables'"
17171723
)

reframe/core/schedulers/slurm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def filternodes(self, job, nodes):
292292
# Collect options that restrict node selection, but we need to first
293293
# create a mutable list out of the immutable SequenceView that
294294
# sched_access is
295-
options = list(job.sched_access + job.options + job.cli_options)
295+
options = job.sched_access + job.options + job.cli_options
296296
option_parser = ArgumentParser()
297297
option_parser.add_argument('--reservation')
298298
option_parser.add_argument('-p', '--partition')

reframe/core/variables.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,6 @@ def join(self, other, cls):
448448
:param cls: the target class.
449449
'''
450450
for key, var in other.items():
451-
452451
# Make doubly declared vars illegal. Note that this will be
453452
# triggered when inheriting from multiple RegressionTest classes.
454453
if key in self.vars:

0 commit comments

Comments
 (0)