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
11 changes: 6 additions & 5 deletions cscs-checks/cuda/multi_gpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import reframe.utility.sanity as sn
import reframe as rfm
from reframe.core.deferrable import evaluate


@rfm.required_version('>=2.16-dev0')
Expand Down Expand Up @@ -90,7 +89,7 @@ def do_sanity_check(self):
self.stdout, 1
))

evaluate(sn.assert_eq(
sn.evaluate(sn.assert_eq(
self.job.num_tasks, len(devices_found),
msg='requested {0} node(s), got {1} (nodelist: %s)' %
','.join(sorted(devices_found))))
Expand All @@ -100,9 +99,11 @@ def do_sanity_check(self):
self.stdout, 1
))

evaluate(sn.assert_eq(devices_found, good_nodes,
msg='check failed on the following node(s): %s' %
','.join(sorted(devices_found - good_nodes))))
sn.evaluate(sn.assert_eq(
devices_found, good_nodes,
msg='check failed on the following node(s): %s' %
','.join(sorted(devices_found - good_nodes)))
)

# Sanity is fine, fill in the perf. patterns based on the exact node id
for nodename in devices_found:
Expand Down
22 changes: 11 additions & 11 deletions docs/deferrables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ If you try to call ``foo()``, its code will not execute:
Instead, a special object is returned that represents the function whose execution is deferred.
Notice the more general *deferred expression* name of this object. We shall see later on why this name is used.

In order to explicitly trigger the execution of ``foo()``, you have to call :func:`evaluate <reframe.core.deferrable.evaluate>` on it:
In order to explicitly trigger the execution of ``foo()``, you have to call :func:`evaluate <reframe.utility.sanity.evaluate>` on it:

.. code-block:: pycon

>>> from reframe.core.deferrable import evaluate
>>> from reframe.utility.sanity import evaluate
>>> evaluate(foo())
hello

If the argument passed to :func:`evaluate <reframe.core.deferrable.evaluate>` is not a deferred expression, it will be simply returned as is.
If the argument passed to :func:`evaluate <reframe.utility.sanity.evaluate>` is not a deferred expression, it will be simply returned as is.

Deferrable functions may also be combined as we do with normal functions. Let's extend our example with ``foo()`` accepting an argument and printing it:

Expand Down Expand Up @@ -186,7 +186,7 @@ In summary deferrable functions have the following characteristics:

* You can make any function deferrable by preceding it with the :func:`@sanity_function <reframe.utility.sanity.sanity_function>` or the :func:`@deferrable <reframe.core.deferrable.deferrable>` decorator.
* When you call a deferrable function, its body is not executed but its arguments are *captured* and an object representing the deferred function is returned.
* You can execute the body of a deferrable function at any later point by calling :func:`evaluate <reframe.core.deferrable.evaluate>` on the deferred expression object that it has been returned by the call to the deferred function.
* You can execute the body of a deferrable function at any later point by calling :func:`evaluate <reframe.utility.sanity.evaluate>` on the deferred expression object that it has been returned by the call to the deferred function.
* Deferred functions can accept other deferred expressions as arguments and may also return a deferred expression.
* When you evaluate a deferrable function, any other deferrable function down the call tree will also be evaluated.
* You can include a call to a deferrable function in any Python expression and the result will be another deferred expression.
Expand All @@ -195,7 +195,7 @@ How a Deferred Expression Is Evaluated?
---------------------------------------

As discussed before, you can create a new deferred expression by calling a function whose definition is decorated by the ``@sanity_function`` or ``@deferrable`` decorator or by including an already deferred expression in any sort of arithmetic operation.
When you call :func:`evaluate <reframe.core.deferrable.evaluate>` on a deferred expression, you trigger the evaluation of the whole subexpression tree.
When you call :func:`evaluate <reframe.utility.sanity.evaluate>` on a deferred expression, you trigger the evaluation of the whole subexpression tree.
Here is how the evaluation process evolves:

A deferred expression object is merely a placeholder of the target function and its arguments at the moment you call it.
Expand Down Expand Up @@ -244,7 +244,7 @@ The following figure shows how the evaluation evolves for this particular exampl
Implicit evaluation of a deferred expression
--------------------------------------------

Although you can trigger the evaluation of a deferred expression at any time by calling :func:`evaluate <reframe.core.deferrable.evaluate>`, there are some cases where the evaluation is triggered implicitly:
Although you can trigger the evaluation of a deferred expression at any time by calling :func:`evaluate <reframe.utility.evaluate>`, there are some cases where the evaluation is triggered implicitly:

* When you try to get the truthy value of a deferred expression by calling :func:`bool <python:bool>` on it.
This happens for example when you include a deferred expression in an :keyword:`if` statement or as an argument to the :keyword:`and`, :keyword:`or`, :keyword:`not` and :keyword:`in` (:func:`__contains__ <python:object.__contains__>`) operators.
Expand All @@ -262,8 +262,8 @@ Although you can trigger the evaluation of a deferred expression at any time by

.. code-block:: pycon

>>> from reframe.core.deferrable import make_deferrable
>>> l = make_deferrable([1, 2, 3])
>>> from reframe.utility.sanity import defer
>>> l = defer([1, 2, 3])
>>> l
<reframe.core.deferrable._DeferredExpression object at 0x2b1288f54cf8>
>>> evaluate(l)
Expand All @@ -273,7 +273,7 @@ Although you can trigger the evaluation of a deferred expression at any time by
>>> 3 in l
True

The :func:`make_deferrable <reframe.core.deferrable.make_deferrable>` is simply a deferrable version of the identity function (a function that simply returns its argument).
The :func:`defer <reframe.utility.sanity.defer>` is simply a deferrable version of the identity function (a function that simply returns its argument).
As expected, ``l`` is a deferred expression that evaluates to the ``[1, 2, 3]`` list. When we apply the :keyword:`in` operator, the deferred expression is immediately evaluated.

.. note:: Python expands this expression into ``bool(l.__contains__(3))``.
Expand Down Expand Up @@ -328,7 +328,7 @@ You can call other deferrable functions from within a deferrable function.
Thanks to the implicit evaluation rules as well as the fact that the return value of a deferrable function is also evaluated if it is a deferred expression, you can write a deferrable function without caring much about whether the functions you call are themselves deferrable or not.
However, you should be aware of passing mutable objects to deferrable functions.
If these objects happen to change between the actual call and the implicit evaluation of the deferrable function, you might run into surprises.
In any case, if you want the immediate evaluation of a deferrable function or expression, you can always do that by calling :func:`evaluate <reframe.core.deferrable.evaluate>` on it.
In any case, if you want the immediate evaluation of a deferrable function or expression, you can always do that by calling :func:`evaluate <reframe.utility.sanity.evaluate>` on it.

The following example demonstrates two different ways writing a deferrable function that checks the average of the elements of an iterable:

Expand Down Expand Up @@ -422,7 +422,7 @@ Notice that you cannot include generators in expressions, whereas you can genera

* Generators are iterator objects, while deferred expressions are not.
As a result, you can trigger the evaluation of a generator expression using the :func:`next <python:next>` builtin function.
For a deferred expression you should use :func:`evaluate <reframe.core.deferrable.evaluate>` instead.
For a deferred expression you should use :func:`evaluate <reframe.utility.sanity.evaluate>` instead.

* A generator object is iterable, whereas a deferrable object will be iterable if and only if the result of its evaluation is iterable.

Expand Down
13 changes: 6 additions & 7 deletions reframe/core/deferrable.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'''Provides utilities for deferred execution of expressions.'''

import builtins
import functools

from reframe.core.exceptions import user_deprecation_warning


def deferrable(func):
'''Function decorator for converting a function to a deferred
Expand Down Expand Up @@ -335,13 +335,10 @@ def __invert__(a):
return ~a


# Utility functions

def evaluate(expr):
'''Evaluate a deferred expression.
user_deprecation_warning('evaluate() is deprecated: '
'please use reframe.utility.sanity.evaluate')

If `expr` is not a deferred expression, it will returned as is.
'''
if isinstance(expr, _DeferredExpression):
return expr.evaluate()
else:
Expand All @@ -350,4 +347,6 @@ def evaluate(expr):

@deferrable
def make_deferrable(a):
user_deprecation_warning('make_deferrable() is deprecated: '
'please use reframe.utility.sanity.defer')
return a
21 changes: 11 additions & 10 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
import reframe.core.runtime as rt
import reframe.utility as util
import reframe.utility.os_ext as os_ext
import reframe.utility.sanity as sn
import reframe.utility.typecheck as typ
from reframe.core.buildsystems import BuildSystemField
from reframe.core.containers import ContainerPlatform, ContainerPlatformField
from reframe.core.deferrable import deferrable, _DeferredExpression, evaluate
from reframe.core.deferrable import _DeferredExpression
from reframe.core.exceptions import (BuildError, DependencyError,
PipelineError, SanityError,
PerformanceError)
Expand Down Expand Up @@ -812,7 +813,7 @@ def outputdir(self):
return self._outputdir

@property
@deferrable
@sn.sanity_function
def stdout(self):
'''The name of the file containing the standard output of the test.

Expand All @@ -826,7 +827,7 @@ def stdout(self):
return self._job.stdout

@property
@deferrable
@sn.sanity_function
def stderr(self):
'''The name of the file containing the standard error of the test.

Expand All @@ -840,12 +841,12 @@ def stderr(self):
return self._job.stderr

@property
@deferrable
@sn.sanity_function
def build_stdout(self):
return self._build_job.stdout

@property
@deferrable
@sn.sanity_function
def build_stderr(self):
return self._build_job.stderr

Expand Down Expand Up @@ -1222,7 +1223,7 @@ def check_sanity(self):
raise SanityError('sanity_patterns not set')

with os_ext.change_dir(self._stagedir):
success = evaluate(self.sanity_patterns)
success = sn.evaluate(self.sanity_patterns)
if not success:
raise SanityError()

Expand Down Expand Up @@ -1273,7 +1274,7 @@ def check_performance(self):
# check them against the reference. This way we always log them
# even if the don't meet the reference.
for tag, expr in self.perf_patterns.items():
value = evaluate(expr)
value = sn.evaluate(expr)
key = '%s:%s' % (self._current_partition.fullname, tag)
if key not in self.reference:
raise SanityError(
Expand All @@ -1288,7 +1289,7 @@ def check_performance(self):
val, ref, low_thres, high_thres, *_ = values
tag = key.split(':')[-1]
try:
evaluate(
sn.evaluate(
assert_reference(
val, ref, low_thres, high_thres,
msg=('failed to meet reference: %s={0}, '
Expand Down Expand Up @@ -1450,12 +1451,12 @@ def setup(self, partition, environ, **job_opts):
self._setup_paths()

@property
@deferrable
@sn.sanity_function
def stdout(self):
return self._build_job.stdout

@property
@deferrable
@sn.sanity_function
def stderr(self):
return self._build_job.stderr

Expand Down
24 changes: 23 additions & 1 deletion reframe/utility/sanity.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
import re

import reframe.utility as util
from reframe.core.deferrable import deferrable, evaluate
from reframe.core.deferrable import deferrable, _DeferredExpression
from reframe.core.exceptions import SanityError


Expand Down Expand Up @@ -676,6 +676,28 @@ def allx(iterable):
return util.allx(iterable)


@deferrable
def defer(x):
'''Defer the evaluation of variable ``x``.

.. versionadded:: 2.21
'''
return x


def evaluate(expr):
'''Evaluate a deferred expression.

If ``expr`` is not a deferred expression, it will be returned as is.

.. versionadded:: 2.21
'''
if isinstance(expr, _DeferredExpression):
return expr.evaluate()
else:
return expr


@deferrable
def getitem(container, item):
'''Get ``item`` from ``container``.
Expand Down
3 changes: 1 addition & 2 deletions unittests/resources/checks_unlisted/deps_complex.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os
import reframe as rfm
import reframe.utility.sanity as sn
from reframe.core.deferrable import make_deferrable

#
# The following tests implement the dependency graph below:
Expand Down Expand Up @@ -37,7 +36,7 @@ def __init__(self):
self.sourcesdir = None
self.executable = 'echo'
self._count = int(type(self).__name__[1:])
self.sanity_patterns = make_deferrable(True)
self.sanity_patterns = sn.defer(True)
self.keep_files = ['out.txt']

@property
Expand Down
Loading