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
4 changes: 4 additions & 0 deletions reframe/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ class JobNotStartedError(JobError):
"""Raised when trying to operate on a unstarted job."""


class DependencyError(ReframeError):
"""Raised when a dependency problem is encountered."""


class ReframeDeprecationWarning(DeprecationWarning):
"""Warning for deprecated features of the ReFrame framework."""

Expand Down
50 changes: 45 additions & 5 deletions reframe/core/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
#

__all__ = ['RegressionTest',
'RunOnlyRegressionTest', 'CompileOnlyRegressionTest']
'RunOnlyRegressionTest', 'CompileOnlyRegressionTest',
'DEPEND_EXACT', 'DEPEND_BY_ENV', 'DEPEND_FULLY']


import inspect
Expand All @@ -22,7 +23,8 @@
from reframe.core.buildsystems import BuildSystem, BuildSystemField
from reframe.core.deferrable import deferrable, _DeferredExpression, evaluate
from reframe.core.environments import Environment, EnvironmentSnapshot
from reframe.core.exceptions import (BuildError, PipelineError, SanityError,
from reframe.core.exceptions import (BuildError, DependencyError,
PipelineError, SanityError,
PerformanceError)
from reframe.core.launchers.registry import getlauncher
from reframe.core.schedulers import Job
Expand All @@ -31,6 +33,12 @@
from reframe.utility.sanity import assert_reference


# Dependency kinds
DEPEND_EXACT = 1
DEPEND_BY_ENV = 2
DEPEND_FULLY = 3


class RegressionTest:
"""Base class for regression tests.

Expand Down Expand Up @@ -627,6 +635,12 @@ def __init__(self, name=None, prefix=None):
# Performance logging
self._perf_logger = logging.null_logger

# List of dependencies specified by the user
self._userdeps = []

# Weak reference to the test case associated with this check
self._case = None

# Export read-only views to interesting fields
@property
def current_environ(self):
Expand Down Expand Up @@ -749,9 +763,6 @@ def build_stdout(self):
def build_stderr(self):
return self._build_job.stderr

def __repr__(self):
return debug.repr(self)

def info(self):
"""Provide live information of a running test.

Expand Down Expand Up @@ -1187,6 +1198,35 @@ def cleanup(self, remove_files=False, unload_env=True):
self._current_environ.unload()
self._current_partition.local_env.unload()

# Dependency API
def user_deps(self):
return util.SequenceView(self._userdeps)

def depends_on(self, target, how=DEPEND_BY_ENV, subdeps=None):
if not isinstance(target, str):
raise TypeError("target argument must be of type: `str'")

if not isinstance(how, int):
raise TypeError("how argument must be of type: `int'")

if (subdeps is not None and
not isinstance(subdeps, typ.Dict[str, typ.List[str]])):
raise TypeError("subdeps argument must be of type "
"`Dict[str, List[str]]' or `None'")

self._userdeps.append((target, how, subdeps))

def getdep(self, target, environ):
if self._case is None or self._case() is None:
raise DependencyError('no test case is associated with this test')

for d in self._case().deps:
if d.check.name == target and d.environ.name == environ:
return d.check

raise DependencyError('could not resolve dependency to (%s, %s)' %
(target, environ))

def __str__(self):
return "%s(name='%s', prefix='%s')" % (type(self).__name__,
self.name, self.prefix)
Expand Down
81 changes: 81 additions & 0 deletions reframe/frontend/dependency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#
# Test case graph functionality
#

import collections

import reframe as rfm
from reframe.core.exceptions import DependencyError


def build_deps(cases):
"""Build dependency graph from test cases.

The graph is represented as an adjacency list in a Python dictionary
holding test cases. The dependency information is also encoded inside each
test cases.
"""

# Index cases for quick access
cases_by_part = {}
cases_revmap = {}
for c in cases:
cname = c.check.name
pname = c.partition.fullname
ename = c.environ.name
cases_by_part.setdefault((cname, pname), [])
cases_revmap.setdefault((cname, pname, ename), None)
cases_by_part[cname, pname].append(c)
cases_revmap[cname, pname, ename] = c

def resolve_dep(target, from_map, *args):
errmsg = 'could not resolve dependency: %s' % target
try:
ret = from_map[args]
except KeyError:
raise DependencyError(errmsg)
else:
if not ret:
raise DependencyError(errmsg)

return ret

# NOTE on variable names
#
# c stands for check or case depending on the context
# p stands for partition
# e stands for environment
# t stands for target

graph = {}
for c in cases:
graph[c] = c.deps
cname = c.check.name
pname = c.partition.fullname
ename = c.environ.name
for dep in c.check.user_deps():
tname, how, subdeps = dep
if how == rfm.DEPEND_FULLY:
c.deps.extend(resolve_dep(c, cases_by_part, tname, pname))
elif how == rfm.DEPEND_BY_ENV:
c.deps.append(resolve_dep(c, cases_revmap,
tname, pname, ename))
elif how == rfm.DEPEND_EXACT:
for env, tenvs in subdeps.items():
if env != ename:
continue

for te in tenvs:
c.deps.append(resolve_dep(c, cases_revmap,
tname, pname, te))

return graph


def print_deps(graph):
for c, deps in graph.items():
print(c, '->', deps)


def validate_deps(graph):
"""Validate dependency graph."""
26 changes: 25 additions & 1 deletion reframe/frontend/executors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import abc
import copy
import sys
import weakref

import reframe.core.debug as debug
import reframe.core.logging as logging
Expand All @@ -22,14 +23,33 @@ class TestCase:
def __init__(self, check, partition, environ):
self.__check_orig = check
self.__check = copy.deepcopy(check)
self.__environ = copy.deepcopy(environ)
self.__partition = copy.deepcopy(partition)
self.__environ = copy.deepcopy(environ)
self.__check._case = weakref.ref(self)
self.__deps = []

def __iter__(self):
# Allow unpacking a test case with a single liner:
# c, p, e = case
return iter([self.__check, self.__partition, self.__environ])

def __hash__(self):
return (hash(self.check.name) ^
hash(self.partition.fullname) ^
hash(self.environ.name))

def __eq__(self, other):
if not isinstance(other, type(self)):
return NotImplemented

return (self.check.name == other.check.name and
self.environ.name == other.environ.name and
self.partition.fullname == other.partition.fullname)

def __repr__(self):
return '(%r, %r, %r)' % (self.check.name,
self.partition.fullname, self.environ.name)

@property
def check(self):
return self.__check
Expand All @@ -42,6 +62,10 @@ def partition(self):
def environ(self):
return self.__environ

@property
def deps(self):
return self.__deps

def clone(self):
# Return a fresh clone, i.e., one based on the original check
return TestCase(self.__check_orig, self.__partition, self.__environ)
Expand Down
37 changes: 37 additions & 0 deletions unittests/resources/checks_unlisted/dependencies/normal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import reframe as rfm
import reframe.utility.sanity as sn


@rfm.simple_test
class Test0(rfm.RunOnlyRegressionTest):
def __init__(self):
super().__init__()
self.valid_systems = ['sys0:p0', 'sys0:p1']
self.valid_prog_environs = ['e0', 'e1']
self.executable = 'echo'
self.executable_opts = [self.name]
self.sanity_patterns = sn.assert_found(self.name, self.stdout)


@rfm.parameterized_test(*([kind] for kind in ['fully', 'by_env',
'exact', 'default']))
class Test1(rfm.RunOnlyRegressionTest):
def __init__(self, kind):
super().__init__()
kindspec = {
'fully': rfm.DEPEND_FULLY,
'by_env': rfm.DEPEND_BY_ENV,
'exact': rfm.DEPEND_EXACT,
}
self.valid_systems = ['sys0:p0', 'sys0:p1']
self.valid_prog_environs = ['e0', 'e1']
self.executable = 'echo'
self.executable_opts = [self.name]
self.sanity_patterns = sn.assert_found(self.name, self.stdout)
if kind == 'default':
self.depends_on('Test0')
elif kindspec[kind] == rfm.DEPEND_EXACT:
self.depends_on('Test0', kindspec[kind],
{'e0': ['e0', 'e1'], 'e1': ['e1']})
else:
self.depends_on('Test0', kindspec[kind])
25 changes: 24 additions & 1 deletion unittests/resources/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ class ReframeSettings:
'descr': 'GPU partition',
}
}
},
'sys0': {
# System used for dependency checking
'descr': 'System for test dependencies unit tests',
'hostnames': [r'sys\d+'],
'partitions': {
'p0': {
'scheduler': 'local',
'environs': ['e0', 'e1'],
},
'p1': {
'scheduler': 'local',
'environs': ['e0', 'e1'],
}
}
}
},
'environments': {
Expand Down Expand Up @@ -90,7 +105,15 @@ class ReframeSettings:
'cc': 'gcc',
'cxx': 'g++',
'ftn': 'gfortran',
}
},
'e0': {
'type': 'ProgEnvironment',
'modules': ['m0'],
},
'e1': {
'type': 'ProgEnvironment',
'modules': ['m1'],
},
}
},
'modes': {
Expand Down
2 changes: 1 addition & 1 deletion unittests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def get_partition(self, system, name):

def test_load_success(self):
self.site_config.load_from_dict(self.dict_config)
self.assertEqual(2, len(self.site_config.systems))
self.assertEqual(3, len(self.site_config.systems))

system = self.site_config.systems['testsys']
self.assertEqual(2, len(system.partitions))
Expand Down
Loading