From 6af48c1cf80029fcf1f7405f115fab2de6ecb473 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Thu, 10 Sep 2020 17:26:32 +0200 Subject: [PATCH 01/27] Allow dependencies between partitions --- reframe/core/pipeline.py | 13 +++++++++++-- reframe/frontend/dependency.py | 27 ++++++++++++++++++++++++--- unittests/test_dependencies.py | 10 +++++----- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index ee94751ea7..a781abe846 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -9,7 +9,8 @@ __all__ = [ 'CompileOnlyRegressionTest', 'RegressionTest', 'RunOnlyRegressionTest', - 'DEPEND_BY_ENV', 'DEPEND_EXACT', 'DEPEND_FULLY', 'final' + 'DEPEND_BY_ENV', 'DEPEND_EXACT', 'DEPEND_FULLY', 'DEPEND_BY_PARTITION', + 'final' ] @@ -56,12 +57,20 @@ #: This constant is directly available under the :mod:`reframe` module. DEPEND_BY_ENV = 2 +#: Constant to be passed as the ``how`` argument of the +#: :func:`RegressionTest.depends_on` method. It denotes that the test cases of +#: the current test will depend only on the corresponding test cases of the +#: target test that use the same partition. +#: +#: This constant is directly available under the :mod:`reframe` module. +DEPEND_BY_PARTITION = 3 + #: Constant to be passed as the ``how`` argument of the #: :func:`RegressionTest.depends_on` method. It denotes that each test case of #: this test depends on all the test cases of the target test. #: #: This constant is directly available under the :mod:`reframe` module. -DEPEND_FULLY = 3 +DEPEND_FULLY = 4 def _run_hooks(name=None): diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index 04f81c9003..84a82556ff 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -23,6 +23,19 @@ def build_deps(cases, default_cases=None): test case. ''' + # Index cases for quick access + def build_name_index(cases): + if cases is None: + return {} + + ret = {} + for c in cases: + cname = c.check.name + ret.setdefault(cname, []) + ret[cname].append(c) + + return ret + # Index cases for quick access def build_partition_index(cases): if cases is None: @@ -51,12 +64,16 @@ def build_cases_index(cases): def resolve_dep(target, from_map, fallback_map, *args): errmsg = 'could not resolve dependency: %s -> %s' % (target, args) + if len(args) == 1: + key = args[0] + else: + key = args try: - ret = from_map[args] + ret = from_map[key] except KeyError: # try to resolve the dependency in the fallback map try: - ret = fallback_map[args] + ret = fallback_map[key] except KeyError: raise DependencyError(errmsg) from None @@ -65,8 +82,10 @@ def resolve_dep(target, from_map, fallback_map, *args): return ret + all_cases = build_name_index(cases) cases_by_part = build_partition_index(cases) cases_revmap = build_cases_index(cases) + default_all_cases = build_name_index(default_cases) default_cases_by_part = build_partition_index(default_cases) default_cases_revmap = build_cases_index(default_cases) @@ -86,7 +105,7 @@ def resolve_dep(target, from_map, fallback_map, *args): ename = c.environ.name for dep in c.check.user_deps(): tname, how, subdeps = dep - if how == rfm.DEPEND_FULLY: + if how == rfm.DEPEND_BY_PARTITION: c.deps.extend(resolve_dep(c, cases_by_part, default_cases_by_part, tname, pname)) elif how == rfm.DEPEND_BY_ENV: @@ -104,6 +123,8 @@ def resolve_dep(target, from_map, fallback_map, *args): resolve_dep(c, cases_revmap, default_cases_revmap, tname, pname, te) ) + elif how == rfm.DEPEND_FULLY: + c.deps.extend(resolve_dep(c, all_cases, default_all_cases, tname)) graph[c] = util.OrderedSet(c.deps) diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 2d0ffacb6e..9c56ab6daf 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -126,7 +126,7 @@ def test_build_deps(loader, exec_ctx): dependency.validate_deps(deps) # Check DEPEND_FULLY dependencies - assert num_deps(deps, 'Test1_fully') == 8 + assert num_deps(deps, 'Test1_fully') == 16 for p in ['sys0:p0', 'sys0:p1']: for e0 in ['e0', 'e1']: for e1 in ['e0', 'e1']: @@ -165,15 +165,15 @@ def test_build_deps(loader, exec_ctx): # 1 from Test1_by_env, # 1 from Test1_exact, # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 5 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 5 + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 7 + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 7 # 2 from Test1_fully, # 1 from Test1_by_env, # 2 from Test1_exact, # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 6 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 6 + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 8 + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 8 # Pick a check to test getdep() check_e0 = find_case('Test1_exact', 'e0', cases).check From 18cf9b7bd518d1cba8433f1e872bd9f0fde02a50 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Wed, 30 Sep 2020 16:10:11 +0200 Subject: [PATCH 02/27] update DEPEND_EXACT dependencies and unittests --- reframe/core/pipeline.py | 9 ++- reframe/frontend/dependency.py | 8 +- .../resources/checks_unlisted/deps_simple.py | 6 +- unittests/test_dependencies.py | 78 +++++++++++-------- 4 files changed, 62 insertions(+), 39 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index a781abe846..6085cc4e33 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1587,7 +1587,8 @@ def depends_on(self, target, how=DEPEND_BY_ENV, subdeps=None): .. code-block:: python self.depends_on('T0', how=rfm.DEPEND_EXACT, - subdeps={'E0': ['E0', 'E1'], 'E1': ['E1']}) + subdeps={('P0', 'E0'): [('P0', 'E0'), ('P0', 'E1')], + ('P0', 'E1'): [('P0', 'E1')]}) For more details on how test dependencies work in ReFrame, please refer to `How Test Dependencies Work In ReFrame `__. @@ -1602,9 +1603,11 @@ def depends_on(self, target, how=DEPEND_BY_ENV, subdeps=None): raise TypeError("how argument must be of type: `int'") if (subdeps is not None and - not isinstance(subdeps, typ.Dict[str, typ.List[str]])): + not isinstance(subdeps, typ.Dict[typ.Tuple[str, str], + typ.List[typ.Tuple[str, str]]])): raise TypeError("subdeps argument must be of type " - "`Dict[str, List[str]]' or `None'") + "`Dict[Tuple[str, str], List[Tuple[str, str]]]' " + "or `None'") self._userdeps.append((target, how, subdeps)) diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index 84a82556ff..6efccb2390 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -9,6 +9,7 @@ import collections import itertools +import re import reframe as rfm import reframe.utility as util @@ -115,13 +116,14 @@ def resolve_dep(target, from_map, fallback_map, *args): ) elif how == rfm.DEPEND_EXACT: for env, tenvs in subdeps.items(): - if env != ename: + if env != (pname.split(':')[1], ename): continue - for te in tenvs: + for tp, te in tenvs: + tp = re.sub(r':\S+', f':{tp}', pname) c.deps.append( resolve_dep(c, cases_revmap, default_cases_revmap, - tname, pname, te) + tname, tp, te) ) elif how == rfm.DEPEND_FULLY: c.deps.extend(resolve_dep(c, all_cases, default_all_cases, tname)) diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index c0b9c7e8f7..0a773943da 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -17,12 +17,13 @@ def __init__(self): self.sanity_patterns = sn.assert_found(self.name, self.stdout) -@rfm.parameterized_test(*([kind] for kind in ['fully', 'by_env', +@rfm.parameterized_test(*([kind] for kind in ['fully', 'by_part', 'by_env', 'exact', 'default'])) class Test1(rfm.RunOnlyRegressionTest): def __init__(self, kind): kindspec = { 'fully': rfm.DEPEND_FULLY, + 'by_part': rfm.DEPEND_BY_PARTITION, 'by_env': rfm.DEPEND_BY_ENV, 'exact': rfm.DEPEND_EXACT, } @@ -35,6 +36,7 @@ def __init__(self, kind): self.depends_on('Test0') elif kindspec[kind] == rfm.DEPEND_EXACT: self.depends_on('Test0', kindspec[kind], - {'e0': ['e0', 'e1'], 'e1': ['e1']}) + {('p0', 'e0'): [('p0', 'e0'), ('p1', 'e1')], + ('p1', 'e1'): [('p0', 'e1')]}) else: self.depends_on('Test0', kindspec[kind]) diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 9c56ab6daf..4449b8a125 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -72,9 +72,11 @@ def find_check(name, checks): return None -def find_case(cname, ename, cases): +def find_case(cname, pname, ename, cases): for c in cases: - if c.check.name == cname and c.environ.name == ename: + if (c.check.name == cname and + c.partition.fullname == pname and + c.environ.name == ename): return c @@ -102,8 +104,8 @@ def loader(): def test_eq_hash(loader, exec_ctx): cases = executors.generate_testcases(loader.load_all()) - case0 = find_case('Test0', 'e0', cases) - case1 = find_case('Test0', 'e1', cases) + case0 = find_case('Test0', 'sys0:p0', 'e0', cases) + case1 = find_case('Test0', 'sys0:p0', 'e1', cases) case0_copy = case0.clone() assert case0 == case0_copy @@ -127,11 +129,21 @@ def test_build_deps(loader, exec_ctx): # Check DEPEND_FULLY dependencies assert num_deps(deps, 'Test1_fully') == 16 + for p0 in ['sys0:p0', 'sys0:p1']: + for p1 in ['sys0:p0', 'sys0:p1']: + for e0 in ['e0', 'e1']: + for e1 in ['e0', 'e1']: + assert has_edge(deps, + Node('Test1_fully', p0, e0), + Node('Test0', p1, e1)) + + # Check DEPEND_BY_PARTITION dependencies + assert num_deps(deps, 'Test1_by_part') == 8 for p in ['sys0:p0', 'sys0:p1']: for e0 in ['e0', 'e1']: for e1 in ['e0', 'e1']: assert has_edge(deps, - Node('Test1_fully', p, e0), + Node('Test1_by_part', p, e0), Node('Test0', p, e1)) # Check DEPEND_BY_ENV @@ -147,37 +159,38 @@ def test_build_deps(loader, exec_ctx): Node('Test0', p, e)) # Check DEPEND_EXACT - assert num_deps(deps, 'Test1_exact') == 6 - for p in ['sys0:p0', 'sys0:p1']: - assert has_edge(deps, - Node('Test1_exact', p, 'e0'), - Node('Test0', p, 'e0')) - assert has_edge(deps, - Node('Test1_exact', p, 'e0'), - Node('Test0', p, 'e1')) - assert has_edge(deps, - Node('Test1_exact', p, 'e1'), - Node('Test0', p, 'e1')) + assert num_deps(deps, 'Test1_exact') == 3 + assert has_edge(deps, + Node('Test1_exact', 'sys0:p0', 'e0'), + Node('Test0', 'sys0:p0', 'e0')) + assert has_edge(deps, + Node('Test1_exact', 'sys0:p0', 'e0'), + Node('Test0', 'sys0:p1', 'e1')) + assert has_edge(deps, + Node('Test1_exact', 'sys0:p1', 'e1'), + Node('Test0', 'sys0:p0', 'e1')) # Check in-degree of Test0 - # 2 from Test1_fully, + # 4 from Test1_fully, + # 2 from Test1_by_part, # 1 from Test1_by_env, - # 1 from Test1_exact, + # 1 from Test1_exact (only for p0), # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 7 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 7 + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 9 + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 8 - # 2 from Test1_fully, + # 4 from Test1_fully, + # 2 from Test1_by_part, # 1 from Test1_by_env, - # 2 from Test1_exact, + # 1 from Test1_exact, # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 8 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 8 + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 9 + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 9 # Pick a check to test getdep() - check_e0 = find_case('Test1_exact', 'e0', cases).check - check_e1 = find_case('Test1_exact', 'e1', cases).check + check_e0 = find_case('Test1_exact', 'sys0:p0', 'e0', cases).check + check_e1 = find_case('Test1_exact', 'sys0:p1', 'e1', cases).check with pytest.raises(DependencyError): check_e0.getdep('Test0') @@ -209,7 +222,8 @@ def test_build_deps_unknown_test(loader, exec_ctx): if depkind == 'default': test1.depends_on('TestX') elif depkind == 'exact': - test1.depends_on('TestX', rfm.DEPEND_EXACT, {'e0': ['e0']}) + test1.depends_on('TestX', rfm.DEPEND_EXACT, + {('p0', 'e0'): [('p0', 'e0')]}) elif depkind == 'fully': test1.depends_on('TestX', rfm.DEPEND_FULLY) elif depkind == 'by_env': @@ -225,7 +239,8 @@ def test_build_deps_unknown_target_env(loader, exec_ctx): # Add some inexistent dependencies test0 = find_check('Test0', checks) test1 = find_check('Test1_default', checks) - test1.depends_on('Test0', rfm.DEPEND_EXACT, {'e0': ['eX']}) + test1.depends_on('Test0', rfm.DEPEND_EXACT, + {('p0', 'e0'): [('p0', 'eX')]}) with pytest.raises(DependencyError): dependency.build_deps(executors.generate_testcases(checks)) @@ -236,7 +251,8 @@ def test_build_deps_unknown_source_env(loader, exec_ctx): # Add some inexistent dependencies test0 = find_check('Test0', checks) test1 = find_check('Test1_default', checks) - test1.depends_on('Test0', rfm.DEPEND_EXACT, {'eX': ['e0']}) + test1.depends_on('Test0', rfm.DEPEND_EXACT, + {('p0', 'eX'): [('p0', 'e0')]}) # Unknown source is ignored, because it might simply be that the test # is not executed for eX @@ -351,8 +367,8 @@ def test_cyclic_deps(make_test, exec_ctx): def test_cyclic_deps_by_env(make_test, exec_ctx): t0 = make_test('t0') t1 = make_test('t1') - t1.depends_on('t0', rfm.DEPEND_EXACT, {'e0': ['e0']}) - t0.depends_on('t1', rfm.DEPEND_EXACT, {'e1': ['e1']}) + t1.depends_on('t0', rfm.DEPEND_EXACT, {('p0', 'e0'): [('p0', 'e0')]}) + t0.depends_on('t1', rfm.DEPEND_EXACT, {('p0', 'e1'): [('p0', 'e1')]}) deps = dependency.build_deps( executors.generate_testcases([t0, t1]) ) From e84ff84eda81389b8f406b080b5ec06365ddf52d Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Wed, 30 Sep 2020 16:32:25 +0200 Subject: [PATCH 03/27] update dependency documentation --- docs/dependencies.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/dependencies.rst b/docs/dependencies.rst index 1c79a26133..e108f259d6 100644 --- a/docs/dependencies.rst +++ b/docs/dependencies.rst @@ -26,10 +26,9 @@ Conceptually, this dependency can be viewed at the test level as follows: For most of the cases, this is sufficient to reason about test dependencies. In reality, as mentioned above, dependencies are handled at the level of test cases. -Test cases on different partitions are always independent. -If not specified differently, test cases using programming environments are also independent. +If not specified differently, test cases on different partitions or programming environments are independent. This is the default behavior of the :func:`depends_on` function. -The following image shows the actual test case dependencies assuming that both tests support the ``E0`` and ``E1`` programming environments (for simplicity, we have omitted the partitions, since tests are always independent in that dimension): +The following image shows the actual test case dependencies assuming that both tests support the ``E0`` and ``E1`` programming environments (for simplicity, we have omitted the partitions): .. figure:: _static/img/test-deps-by-env.svg :align: center @@ -69,7 +68,7 @@ These dependencies can be achieved as follows: def __init__(self): ... self.depends_on('T0', how=rfm.DEPEND_EXACT, - subdeps={'E0': ['E0', 'E1'], 'E1': ['E1']}) + subdeps={('P0', 'E0'): [('P0', 'E0'), ('P0', 'E1')], '('P0', 'E1')': [('P0', 'E1')]}) The ``subdeps`` argument defines the sub-dependencies between the test cases of :class:`T1` and :class:`T0` using an adjacency list representation. From 0e981fd413155749b138fd61248edd84295a23e4 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Wed, 30 Sep 2020 16:41:05 +0200 Subject: [PATCH 04/27] fix long line --- reframe/frontend/dependency.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index 6efccb2390..21ea2be11d 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -126,7 +126,9 @@ def resolve_dep(target, from_map, fallback_map, *args): tname, tp, te) ) elif how == rfm.DEPEND_FULLY: - c.deps.extend(resolve_dep(c, all_cases, default_all_cases, tname)) + c.deps.extend( + resolve_dep(c, all_cases, default_all_cases, tname) + ) graph[c] = util.OrderedSet(c.deps) From 023d1ec0dab165fca995eb1a0bfecbd4b6b379b1 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 13 Oct 2020 13:59:10 +0200 Subject: [PATCH 05/27] update depends_on signature --- reframe/core/pipeline.py | 77 +++++++++++++++--- reframe/frontend/dependency.py | 31 ++----- .../resources/checks_unlisted/deps_simple.py | 5 +- unittests/test_dependencies.py | 80 ++++++++----------- 4 files changed, 105 insertions(+), 88 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 6085cc4e33..a31b27d04a 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -35,7 +35,8 @@ from reframe.core.deferrable import _DeferredExpression from reframe.core.exceptions import (BuildError, DependencyError, PipelineError, SanityError, - PerformanceError) + PerformanceError, + user_deprecation_warning) from reframe.core.meta import RegressionTestMeta from reframe.core.schedulers import Job @@ -1564,7 +1565,7 @@ def cleanup(self, remove_files=False): def user_deps(self): return util.SequenceView(self._userdeps) - def depends_on(self, target, how=DEPEND_BY_ENV, subdeps=None): + def depends_on(self, target, when=None, *args, **kwargs): '''Add a dependency to ``target`` in this test. :arg target: The name of the target test. @@ -1587,8 +1588,7 @@ def depends_on(self, target, how=DEPEND_BY_ENV, subdeps=None): .. code-block:: python self.depends_on('T0', how=rfm.DEPEND_EXACT, - subdeps={('P0', 'E0'): [('P0', 'E0'), ('P0', 'E1')], - ('P0', 'E1'): [('P0', 'E1')]}) + subdeps={'E0': ['E0', 'E1'], 'E1': ['E1']}) For more details on how test dependencies work in ReFrame, please refer to `How Test Dependencies Work In ReFrame `__. @@ -1599,17 +1599,68 @@ 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'") + def same_env_and_partition(src, dst): + return src == dst + + # Old syntax: depends_on(self, target, how=None, subdeps=None) + # New syntax: depends_on(self, target, when=None) + if when is None: + when = same_env_and_partition + elif ('how' in kwargs or 'subdeps' in kwargs or args or + isinstance(when, int)): + msg = ("the arguments `how' and `subdeps' are deprecated, " + "please use the argument `when'") + # user_deprecation_warning(msg) + + # if `when' is callable ignore other arguments, otherwise + # fix the argument to be the appropriate callable + if when is not callable(when): + if isinstance(when, int): + how = when + # If there is an extra argument it should be subdeps + if args: + subdeps = args[0] + else: + subdeps = None + else: + how = kwargs.get('how', default=DEPEND_BY_ENV) + subdeps = kwargs.get('how', default=None) + + # some sanity checking + if how is not None and 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'") + + def exact(src, dst): + if not subdeps: + return False + + return (src[0] == dst[0]) and (src[1] in subdeps) and (dst[1] in subdeps[src[1]]) + + def same_partition(src, dst): + return src[0] == dst[0] + + # Follow the old definitions + # DEPEND_BY_ENV used to mean same env, same partition & same system + if how == DEPEND_BY_ENV: + when = same_env_and_partition + # DEPEND_BY_ENV used to mean same partition & same system + elif how == DEPEND_FULLY: + when = same_partition + # DEPEND_EXACT allows dependencies inside the same partition + elif how == DEPEND_EXACT: + when = exact + else: + raise TypeError("invalid type of dependency") - if (subdeps is not None and - not isinstance(subdeps, typ.Dict[typ.Tuple[str, str], - typ.List[typ.Tuple[str, str]]])): - raise TypeError("subdeps argument must be of type " - "`Dict[Tuple[str, str], List[Tuple[str, str]]]' " - "or `None'") + if not callable(when): + raise TypeError("how argument must be callable") - self._userdeps.append((target, how, subdeps)) + self._userdeps.append((target, when)) def getdep(self, target, environ=None): '''Retrieve the test case of a target dependency. diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependency.py index 21ea2be11d..2406c21329 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependency.py @@ -105,30 +105,13 @@ def resolve_dep(target, from_map, fallback_map, *args): pname = c.partition.fullname ename = c.environ.name for dep in c.check.user_deps(): - tname, how, subdeps = dep - if how == rfm.DEPEND_BY_PARTITION: - c.deps.extend(resolve_dep(c, cases_by_part, - default_cases_by_part, tname, pname)) - elif how == rfm.DEPEND_BY_ENV: - c.deps.append( - resolve_dep(c, cases_revmap, default_cases_revmap, - tname, pname, ename) - ) - elif how == rfm.DEPEND_EXACT: - for env, tenvs in subdeps.items(): - if env != (pname.split(':')[1], ename): - continue - - for tp, te in tenvs: - tp = re.sub(r':\S+', f':{tp}', pname) - c.deps.append( - resolve_dep(c, cases_revmap, default_cases_revmap, - tname, tp, te) - ) - elif how == rfm.DEPEND_FULLY: - c.deps.extend( - resolve_dep(c, all_cases, default_all_cases, tname) - ) + tname, when = dep + for d in resolve_dep(c, all_cases, default_all_cases, tname): + dep_cname = d.check.name + dep_pname = d.partition.fullname + dep_ename = d.environ.name + if not when or when((pname, ename), (dep_pname, dep_ename)): + c.deps.append(d) graph[c] = util.OrderedSet(c.deps) diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index 0a773943da..27195fff6f 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -17,7 +17,7 @@ def __init__(self): self.sanity_patterns = sn.assert_found(self.name, self.stdout) -@rfm.parameterized_test(*([kind] for kind in ['fully', 'by_part', 'by_env', +@rfm.parameterized_test(*([kind] for kind in ['fully', 'by_env', 'exact', 'default'])) class Test1(rfm.RunOnlyRegressionTest): def __init__(self, kind): @@ -36,7 +36,6 @@ def __init__(self, kind): self.depends_on('Test0') elif kindspec[kind] == rfm.DEPEND_EXACT: self.depends_on('Test0', kindspec[kind], - {('p0', 'e0'): [('p0', 'e0'), ('p1', 'e1')], - ('p1', 'e1'): [('p0', 'e1')]}) + {'e0': ['e0', 'e1'], 'e1': ['e1']}) else: self.depends_on('Test0', kindspec[kind]) diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 4449b8a125..2d0ffacb6e 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -72,11 +72,9 @@ def find_check(name, checks): return None -def find_case(cname, pname, ename, cases): +def find_case(cname, ename, cases): for c in cases: - if (c.check.name == cname and - c.partition.fullname == pname and - c.environ.name == ename): + if c.check.name == cname and c.environ.name == ename: return c @@ -104,8 +102,8 @@ def loader(): def test_eq_hash(loader, exec_ctx): cases = executors.generate_testcases(loader.load_all()) - case0 = find_case('Test0', 'sys0:p0', 'e0', cases) - case1 = find_case('Test0', 'sys0:p0', 'e1', cases) + case0 = find_case('Test0', 'e0', cases) + case1 = find_case('Test0', 'e1', cases) case0_copy = case0.clone() assert case0 == case0_copy @@ -128,22 +126,12 @@ def test_build_deps(loader, exec_ctx): dependency.validate_deps(deps) # Check DEPEND_FULLY dependencies - assert num_deps(deps, 'Test1_fully') == 16 - for p0 in ['sys0:p0', 'sys0:p1']: - for p1 in ['sys0:p0', 'sys0:p1']: - for e0 in ['e0', 'e1']: - for e1 in ['e0', 'e1']: - assert has_edge(deps, - Node('Test1_fully', p0, e0), - Node('Test0', p1, e1)) - - # Check DEPEND_BY_PARTITION dependencies - assert num_deps(deps, 'Test1_by_part') == 8 + assert num_deps(deps, 'Test1_fully') == 8 for p in ['sys0:p0', 'sys0:p1']: for e0 in ['e0', 'e1']: for e1 in ['e0', 'e1']: assert has_edge(deps, - Node('Test1_by_part', p, e0), + Node('Test1_fully', p, e0), Node('Test0', p, e1)) # Check DEPEND_BY_ENV @@ -159,38 +147,37 @@ def test_build_deps(loader, exec_ctx): Node('Test0', p, e)) # Check DEPEND_EXACT - assert num_deps(deps, 'Test1_exact') == 3 - assert has_edge(deps, - Node('Test1_exact', 'sys0:p0', 'e0'), - Node('Test0', 'sys0:p0', 'e0')) - assert has_edge(deps, - Node('Test1_exact', 'sys0:p0', 'e0'), - Node('Test0', 'sys0:p1', 'e1')) - assert has_edge(deps, - Node('Test1_exact', 'sys0:p1', 'e1'), - Node('Test0', 'sys0:p0', 'e1')) + assert num_deps(deps, 'Test1_exact') == 6 + for p in ['sys0:p0', 'sys0:p1']: + assert has_edge(deps, + Node('Test1_exact', p, 'e0'), + Node('Test0', p, 'e0')) + assert has_edge(deps, + Node('Test1_exact', p, 'e0'), + Node('Test0', p, 'e1')) + assert has_edge(deps, + Node('Test1_exact', p, 'e1'), + Node('Test0', p, 'e1')) # Check in-degree of Test0 - # 4 from Test1_fully, - # 2 from Test1_by_part, + # 2 from Test1_fully, # 1 from Test1_by_env, - # 1 from Test1_exact (only for p0), + # 1 from Test1_exact, # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 9 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 8 + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 5 + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 5 - # 4 from Test1_fully, - # 2 from Test1_by_part, + # 2 from Test1_fully, # 1 from Test1_by_env, - # 1 from Test1_exact, + # 2 from Test1_exact, # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 9 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 9 + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 6 + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 6 # Pick a check to test getdep() - check_e0 = find_case('Test1_exact', 'sys0:p0', 'e0', cases).check - check_e1 = find_case('Test1_exact', 'sys0:p1', 'e1', cases).check + check_e0 = find_case('Test1_exact', 'e0', cases).check + check_e1 = find_case('Test1_exact', 'e1', cases).check with pytest.raises(DependencyError): check_e0.getdep('Test0') @@ -222,8 +209,7 @@ def test_build_deps_unknown_test(loader, exec_ctx): if depkind == 'default': test1.depends_on('TestX') elif depkind == 'exact': - test1.depends_on('TestX', rfm.DEPEND_EXACT, - {('p0', 'e0'): [('p0', 'e0')]}) + test1.depends_on('TestX', rfm.DEPEND_EXACT, {'e0': ['e0']}) elif depkind == 'fully': test1.depends_on('TestX', rfm.DEPEND_FULLY) elif depkind == 'by_env': @@ -239,8 +225,7 @@ def test_build_deps_unknown_target_env(loader, exec_ctx): # Add some inexistent dependencies test0 = find_check('Test0', checks) test1 = find_check('Test1_default', checks) - test1.depends_on('Test0', rfm.DEPEND_EXACT, - {('p0', 'e0'): [('p0', 'eX')]}) + test1.depends_on('Test0', rfm.DEPEND_EXACT, {'e0': ['eX']}) with pytest.raises(DependencyError): dependency.build_deps(executors.generate_testcases(checks)) @@ -251,8 +236,7 @@ def test_build_deps_unknown_source_env(loader, exec_ctx): # Add some inexistent dependencies test0 = find_check('Test0', checks) test1 = find_check('Test1_default', checks) - test1.depends_on('Test0', rfm.DEPEND_EXACT, - {('p0', 'eX'): [('p0', 'e0')]}) + test1.depends_on('Test0', rfm.DEPEND_EXACT, {'eX': ['e0']}) # Unknown source is ignored, because it might simply be that the test # is not executed for eX @@ -367,8 +351,8 @@ def test_cyclic_deps(make_test, exec_ctx): def test_cyclic_deps_by_env(make_test, exec_ctx): t0 = make_test('t0') t1 = make_test('t1') - t1.depends_on('t0', rfm.DEPEND_EXACT, {('p0', 'e0'): [('p0', 'e0')]}) - t0.depends_on('t1', rfm.DEPEND_EXACT, {('p0', 'e1'): [('p0', 'e1')]}) + t1.depends_on('t0', rfm.DEPEND_EXACT, {'e0': ['e0']}) + t0.depends_on('t1', rfm.DEPEND_EXACT, {'e1': ['e1']}) deps = dependency.build_deps( executors.generate_testcases([t0, t1]) ) From 29208a2834d5958f97dac33a083f02c0bf492647 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 13 Oct 2020 14:05:52 +0200 Subject: [PATCH 06/27] remove DependencyError for unknown target environment --- unittests/test_dependencies.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 2d0ffacb6e..bca7476209 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -219,17 +219,6 @@ def test_build_deps_unknown_test(loader, exec_ctx): dependency.build_deps(executors.generate_testcases(checks)) -def test_build_deps_unknown_target_env(loader, exec_ctx): - checks = loader.load_all() - - # Add some inexistent dependencies - test0 = find_check('Test0', checks) - test1 = find_check('Test1_default', checks) - test1.depends_on('Test0', rfm.DEPEND_EXACT, {'e0': ['eX']}) - with pytest.raises(DependencyError): - dependency.build_deps(executors.generate_testcases(checks)) - - def test_build_deps_unknown_source_env(loader, exec_ctx): checks = loader.load_all() From 6b368b94f53f58521920b055c5f635d0286355c9 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 13 Oct 2020 14:22:11 +0200 Subject: [PATCH 07/27] remove DEPEND_BY_PARTITION option --- reframe/core/pipeline.py | 26 +++++++------------ .../resources/checks_unlisted/deps_simple.py | 1 - 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index a31b27d04a..2d767ba55b 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -9,8 +9,7 @@ __all__ = [ 'CompileOnlyRegressionTest', 'RegressionTest', 'RunOnlyRegressionTest', - 'DEPEND_BY_ENV', 'DEPEND_EXACT', 'DEPEND_FULLY', 'DEPEND_BY_PARTITION', - 'final' + 'DEPEND_BY_ENV', 'DEPEND_EXACT', 'DEPEND_FULLY', 'final', ] @@ -58,20 +57,12 @@ #: This constant is directly available under the :mod:`reframe` module. DEPEND_BY_ENV = 2 -#: Constant to be passed as the ``how`` argument of the -#: :func:`RegressionTest.depends_on` method. It denotes that the test cases of -#: the current test will depend only on the corresponding test cases of the -#: target test that use the same partition. -#: -#: This constant is directly available under the :mod:`reframe` module. -DEPEND_BY_PARTITION = 3 - #: Constant to be passed as the ``how`` argument of the #: :func:`RegressionTest.depends_on` method. It denotes that each test case of #: this test depends on all the test cases of the target test. #: #: This constant is directly available under the :mod:`reframe` module. -DEPEND_FULLY = 4 +DEPEND_FULLY = 3 def _run_hooks(name=None): @@ -1602,14 +1593,12 @@ def depends_on(self, target, when=None, *args, **kwargs): def same_env_and_partition(src, dst): return src == dst - # Old syntax: depends_on(self, target, how=None, subdeps=None) - # New syntax: depends_on(self, target, when=None) if when is None: when = same_env_and_partition elif ('how' in kwargs or 'subdeps' in kwargs or args or - isinstance(when, int)): + isinstance(when, int)): msg = ("the arguments `how' and `subdeps' are deprecated, " - "please use the argument `when'") + "please use the argument `when'") # user_deprecation_warning(msg) # if `when' is callable ignore other arguments, otherwise @@ -1622,6 +1611,7 @@ def same_env_and_partition(src, dst): subdeps = args[0] else: subdeps = None + else: how = kwargs.get('how', default=DEPEND_BY_ENV) subdeps = kwargs.get('how', default=None) @@ -1639,13 +1629,15 @@ def exact(src, dst): if not subdeps: return False - return (src[0] == dst[0]) and (src[1] in subdeps) and (dst[1] in subdeps[src[1]]) + return ((src[0] == dst[0]) and (src[1] in subdeps) and + (dst[1] in subdeps[src[1]])) def same_partition(src, dst): return src[0] == dst[0] # Follow the old definitions - # DEPEND_BY_ENV used to mean same env, same partition & same system + # DEPEND_BY_ENV used to mean same env, same partition + # & same system if how == DEPEND_BY_ENV: when = same_env_and_partition # DEPEND_BY_ENV used to mean same partition & same system diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index 27195fff6f..c0b9c7e8f7 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -23,7 +23,6 @@ class Test1(rfm.RunOnlyRegressionTest): def __init__(self, kind): kindspec = { 'fully': rfm.DEPEND_FULLY, - 'by_part': rfm.DEPEND_BY_PARTITION, 'by_env': rfm.DEPEND_BY_ENV, 'exact': rfm.DEPEND_EXACT, } From 6465c797e3c4df3c45743419e9b81d91e8a45f89 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 13 Oct 2020 15:50:23 +0200 Subject: [PATCH 08/27] add description of new depends_on argument --- reframe/core/pipeline.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 2d767ba55b..03513d45c4 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -9,7 +9,7 @@ __all__ = [ 'CompileOnlyRegressionTest', 'RegressionTest', 'RunOnlyRegressionTest', - 'DEPEND_BY_ENV', 'DEPEND_EXACT', 'DEPEND_FULLY', 'final', + 'DEPEND_BY_ENV', 'DEPEND_EXACT', 'DEPEND_FULLY', 'final' ] @@ -1560,32 +1560,35 @@ def depends_on(self, target, when=None, *args, **kwargs): '''Add a dependency to ``target`` in this test. :arg target: The name of the target test. - :arg how: How the dependency should be mapped in the test cases space. - This argument can accept any of the three constants - :attr:`DEPEND_EXACT`, :attr:`DEPEND_BY_ENV` (default), - :attr:`DEPEND_FULLY`. - - :arg subdeps: An adjacency list representation of how this test's test - cases depend on those of the target test. This is only relevant if - ``how == DEPEND_EXACT``. The value of this argument is a - dictionary having as keys the names of this test's supported - programming environments. The values are lists of the programming - environments names of the target test that this test's test cases - will depend on. In the following example, this test's ``E0`` - programming environment case will depend on both ``E0`` and ``E1`` - test cases of the target test ``T0``, but its ``E1`` case will - depend only on the ``E1`` test case of ``T0``: + :arg when: A callable that defines the mapping of the dependencies. + The function the user passes should take as argument the source + and destination testcase. When case B depends on case `A' we + consider as source case `A' and destination case `B'. In the + following example, each case will depend on every case from T0, + that belongs in the same partition. .. code-block:: python - self.depends_on('T0', how=rfm.DEPEND_EXACT, - subdeps={'E0': ['E0', 'E1'], 'E1': ['E1']}) + def part_equal(src, dst): + p0, _ = src + p1, _ = dst + return p0 == p1 + + self.depends_on('T0', when=part_equal) + + By default each testcase will depend on the case from target + that has the same environment and partition, if it exists. For more details on how test dependencies work in ReFrame, please refer to `How Test Dependencies Work In ReFrame `__. .. versionadded:: 2.21 + .. versionchanged:: 3.3 + Dependencies between cases from different partitions are now allowed + and the arguments `how' and `subdeps' are deprecated. You should + use the `when' argument. + ''' if not isinstance(target, str): raise TypeError("target argument must be of type: `str'") From a799466d5011465bbe632fc7445bdbbf2214314d Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 13 Oct 2020 16:49:35 +0200 Subject: [PATCH 09/27] small fix --- reframe/core/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 65b22158ca..a195ff8eeb 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1622,7 +1622,7 @@ def part_equal(src, dst): def same_env_and_partition(src, dst): return src == dst - if when is None: + if when is None and not 'how' in kwargs: when = same_env_and_partition elif ('how' in kwargs or 'subdeps' in kwargs or args or isinstance(when, int)): From ba84b95a802b2bb952bb06ec0cdd0d55a3f13e13 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Sun, 25 Oct 2020 07:56:59 +0100 Subject: [PATCH 10/27] implement dependencies when callables and fix unittests --- reframe/core/pipeline.py | 30 +- reframe/frontend/cli.py | 8 +- .../{dependency.py => dependencies.py} | 54 +-- reframe/frontend/executors/__init__.py | 6 +- reframe/utility/dependencies.py | 88 ++++ .../resources/checks_unlisted/deps_simple.py | 30 +- unittests/test_dependencies.py | 404 +++++++++++++----- unittests/test_pipeline.py | 6 +- unittests/test_policies.py | 8 +- 9 files changed, 454 insertions(+), 180 deletions(-) rename reframe/frontend/{dependency.py => dependencies.py} (78%) create mode 100644 reframe/utility/dependencies.py diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index a195ff8eeb..285e3bd329 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -25,6 +25,7 @@ import reframe.core.logging as logging import reframe.core.runtime as rt import reframe.utility as util +import reframe.utility.dependencies as udeps import reframe.utility.os_ext as os_ext import reframe.utility.sanity as sn import reframe.utility.typecheck as typ @@ -1619,16 +1620,13 @@ def part_equal(src, dst): if not isinstance(target, str): raise TypeError("target argument must be of type: `str'") - def same_env_and_partition(src, dst): - return src == dst - if when is None and not 'how' in kwargs: - when = same_env_and_partition + when = udeps.part_env_equal elif ('how' in kwargs or 'subdeps' in kwargs or args or isinstance(when, int)): msg = ("the arguments `how' and `subdeps' are deprecated, " "please use the argument `when'") - # user_deprecation_warning(msg) + user_deprecation_warning(msg) # if `when' is callable ignore other arguments, otherwise # fix the argument to be the appropriate callable @@ -1658,20 +1656,18 @@ def exact(src, dst): if not subdeps: return False - return ((src[0] == dst[0]) and (src[1] in subdeps) and + return ((src[0] == dst[0]) and + (src[1] in subdeps) and (dst[1] in subdeps[src[1]])) - def same_partition(src, dst): - return src[0] == dst[0] - # Follow the old definitions # DEPEND_BY_ENV used to mean same env, same partition # & same system if how == DEPEND_BY_ENV: - when = same_env_and_partition + when = udeps.part_env_equal # DEPEND_BY_ENV used to mean same partition & same system elif how == DEPEND_FULLY: - when = same_partition + when = udeps.part_equal # DEPEND_EXACT allows dependencies inside the same partition elif how == DEPEND_EXACT: when = exact @@ -1683,7 +1679,7 @@ def same_partition(src, dst): self._userdeps.append((target, when)) - def getdep(self, target, environ=None): + def getdep(self, target, environ=None, part=None): '''Retrieve the test case of a target dependency. This is a low-level method. The :func:`@require_deps @@ -1706,15 +1702,19 @@ def getdep(self, target, environ=None): if environ is None: environ = self.current_environ.name + if part is None: + part = self.current_partition.name + 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: + if (d.check.name == target and d.environ.name == environ + and d.partition.name == part): return d.check - raise DependencyError('could not resolve dependency to (%s, %s)' % - (target, environ)) + raise DependencyError(f'could not resolve dependency to ({target}, ' + f'{part}, {environ})') def __str__(self): return "%s(name='%s', prefix='%s')" % (type(self).__name__, diff --git a/reframe/frontend/cli.py b/reframe/frontend/cli.py index a7676961d4..00be4cc4df 100644 --- a/reframe/frontend/cli.py +++ b/reframe/frontend/cli.py @@ -19,7 +19,7 @@ import reframe.core.runtime as runtime import reframe.frontend.argparse as argparse import reframe.frontend.check_filters as filters -import reframe.frontend.dependency as dependency +import reframe.frontend.dependencies as dependencies import reframe.utility.os_ext as os_ext import reframe.utility.json as jsonext from reframe.core.exceptions import ( @@ -637,9 +637,9 @@ def print_infoline(param, value): options.skip_system_check, options.skip_prgenv_check, allowed_environs) - testgraph = dependency.build_deps(testcases) - dependency.validate_deps(testgraph) - testcases = dependency.toposort(testgraph) + testgraph = dependencies.build_deps(testcases) + dependencies.validate_deps(testgraph) + testcases = dependencies.toposort(testgraph) # Manipulate ReFrame's environment if site_config.get('general/0/purge_environment'): diff --git a/reframe/frontend/dependency.py b/reframe/frontend/dependencies.py similarity index 78% rename from reframe/frontend/dependency.py rename to reframe/frontend/dependencies.py index 2406c21329..3b25be8cf8 100644 --- a/reframe/frontend/dependency.py +++ b/reframe/frontend/dependencies.py @@ -25,7 +25,7 @@ def build_deps(cases, default_cases=None): ''' # Index cases for quick access - def build_name_index(cases): + def build_index(cases): if cases is None: return {} @@ -37,38 +37,8 @@ def build_name_index(cases): return ret - # Index cases for quick access - def build_partition_index(cases): - if cases is None: - return {} - - ret = {} - for c in cases: - cname, pname = c.check.name, c.partition.fullname - ret.setdefault((cname, pname), []) - ret[cname, pname].append(c) - - return ret - - def build_cases_index(cases): - if cases is None: - return {} - - ret = {} - for c in cases: - cname = c.check.name - pname = c.partition.fullname - ename = c.environ.name - ret.setdefault((cname, pname, ename), c) - - return ret - - def resolve_dep(target, from_map, fallback_map, *args): - errmsg = 'could not resolve dependency: %s -> %s' % (target, args) - if len(args) == 1: - key = args[0] - else: - key = args + def resolve_dep(target, from_map, fallback_map, key): + errmsg = 'could not resolve dependency: %s -> %s' % (target, key) try: ret = from_map[key] except KeyError: @@ -83,12 +53,8 @@ def resolve_dep(target, from_map, fallback_map, *args): return ret - all_cases = build_name_index(cases) - cases_by_part = build_partition_index(cases) - cases_revmap = build_cases_index(cases) - default_all_cases = build_name_index(default_cases) - default_cases_by_part = build_partition_index(default_cases) - default_cases_revmap = build_cases_index(default_cases) + all_cases = build_index(cases) + default_all_cases = build_index(default_cases) # NOTE on variable names # @@ -101,16 +67,16 @@ def resolve_dep(target, from_map, fallback_map, *args): # partitions and environments graph = collections.OrderedDict() for c in cases: - cname = c.check.name - pname = c.partition.fullname + pname = c.partition.name + # pname = c.partition.fullname ename = c.environ.name for dep in c.check.user_deps(): tname, when = dep for d in resolve_dep(c, all_cases, default_all_cases, tname): - dep_cname = d.check.name - dep_pname = d.partition.fullname + dep_pname = d.partition.name + # dep_pname = d.partition.fullname dep_ename = d.environ.name - if not when or when((pname, ename), (dep_pname, dep_ename)): + if when((pname, ename), (dep_pname, dep_ename)): c.deps.append(d) graph[c] = util.OrderedSet(c.deps) diff --git a/reframe/frontend/executors/__init__.py b/reframe/frontend/executors/__init__.py index 4b2b9accfe..2978b565f7 100644 --- a/reframe/frontend/executors/__init__.py +++ b/reframe/frontend/executors/__init__.py @@ -13,7 +13,7 @@ import reframe.core.environments as env import reframe.core.logging as logging import reframe.core.runtime as runtime -import reframe.frontend.dependency as dependency +import reframe.frontend.dependencies as dependencies from reframe.core.exceptions import (AbortTaskError, JobNotStartedError, ReframeForceExitError, TaskExit) from reframe.core.schedulers.local import LocalJobScheduler @@ -402,8 +402,8 @@ def _retry_failed(self, cases): # Clone failed cases and rebuild dependencies among them failed_cases = [t.testcase.clone() for t in failures] - cases_graph = dependency.build_deps(failed_cases, cases) - failed_cases = dependency.toposort(cases_graph, is_subgraph=True) + cases_graph = dependencies.build_deps(failed_cases, cases) + failed_cases = dependencies.toposort(cases_graph, is_subgraph=True) self._runall(failed_cases) failures = self._stats.failures() diff --git a/reframe/utility/dependencies.py b/reframe/utility/dependencies.py new file mode 100644 index 0000000000..e4963f0a17 --- /dev/null +++ b/reframe/utility/dependencies.py @@ -0,0 +1,88 @@ +# Copyright 2016-2020 Swiss National Supercomputing Centre (CSCS/ETH Zurich) +# ReFrame Project Developers. See the top-level LICENSE file for details. +# +# SPDX-License-Identifier: BSD-3-Clause + +import builtins + + +# Dependency `when' functions +def always(src, dst): + return True + + +# fully connected in the same partition +def part_equal(src, dst): + return src[0] == dst[0] + + +# different partitions but same env +def env_equal(src, dst): + return src[1] == dst[1] + + +# same env and part, which is the default also +def part_env_equal(src, dst): + return src == dst + + +def part_is(name): + def _part_is(src, dst): + if src and dst: + return src[0] == name and dst[0] == name + + if src: + return src[0] == name + + if dst: + return dst[0] == name + + return False + + return _part_is + + + +def env_is(name): + def _env_is(src, dst): + if src and dst: + return src[1] == name and dst[1] == name + + if src: + return src[1] == name + + if dst: + return dst[1] == name + + return False + + return _env_is + + +def source(fn): + def _source(src, dst): + return fn(src, None) + + return _source + + +def dest(fn): + def _dest(src, dst): + return fn(None, dst) + + return _dest + + +def any(*when_funcs): + def _any(src, dst): + return builtins.any(fn(src, dst) for fn in when_funcs) + + return _any + + +def all(*when_funcs): + def _all(src, dst): + return builtins.all(fn(src, dst) for fn in when_funcs) + + return _all + diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index c0b9c7e8f7..cf79ac3197 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause import reframe as rfm +import reframe.utility.dependencies as udeps import reframe.utility.sanity as sn @@ -17,14 +18,28 @@ def __init__(self): self.sanity_patterns = sn.assert_found(self.name, self.stdout) -@rfm.parameterized_test(*([kind] for kind in ['fully', 'by_env', - 'exact', 'default'])) +@rfm.parameterized_test(*([kind] for kind in ['default', 'always', + 'part_equal', 'part_env_equal', + 'custom', 'any', 'all'])) class Test1(rfm.RunOnlyRegressionTest): def __init__(self, kind): + def custom_deps(src, dst): + return ( + src[0] == 'p0' and + src[1] == 'e0' and + dst[0] == 'p1' and + dst[1] == 'e1' + ) + kindspec = { - 'fully': rfm.DEPEND_FULLY, - 'by_env': rfm.DEPEND_BY_ENV, - 'exact': rfm.DEPEND_EXACT, + 'always': udeps.always, + 'part_equal': udeps.part_equal, + 'part_env_equal': udeps.part_env_equal, + 'any': udeps.any(udeps.source(udeps.part_is('p0')), + udeps.dest(udeps.env_is('e1'))), + 'all': udeps.all(udeps.part_is('p0'), + udeps.dest(udeps.env_is('e0'))), + 'custom': custom_deps, } self.valid_systems = ['sys0:p0', 'sys0:p1'] self.valid_prog_environs = ['e0', 'e1'] @@ -33,8 +48,5 @@ def __init__(self, kind): 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]) + self.depends_on('Test0', kindspec[kind]) \ No newline at end of file diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index bca7476209..5104053ba4 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -8,11 +8,13 @@ import reframe as rfm import reframe.core.runtime as rt -import reframe.frontend.dependency as dependency +import reframe.frontend.dependencies as dependencies import reframe.frontend.executors as executors import reframe.utility as util +import reframe.utility.dependencies as udeps from reframe.core.environments import Environment -from reframe.core.exceptions import DependencyError +from reframe.core.exceptions import (DependencyError, + ReframeDeprecationWarning) from reframe.frontend.loader import RegressionCheckLoader import unittests.fixtures as fixtures @@ -72,9 +74,10 @@ def find_check(name, checks): return None -def find_case(cname, ename, cases): +def find_case(cname, ename, partname, cases): for c in cases: - if c.check.name == cname and c.environ.name == ename: + if (c.check.name == cname and c.environ.name == ename + and c.partition.name == partname): return c @@ -102,8 +105,8 @@ def loader(): def test_eq_hash(loader, exec_ctx): cases = executors.generate_testcases(loader.load_all()) - case0 = find_case('Test0', 'e0', cases) - case1 = find_case('Test0', 'e1', cases) + case0 = find_case('Test0', 'e0', 'p0', cases) + case1 = find_case('Test0', 'e1', 'p0', cases) case0_copy = case0.clone() assert case0 == case0_copy @@ -111,130 +114,335 @@ def test_eq_hash(loader, exec_ctx): assert case1 != case0 assert hash(case1) != hash(case0) +def test_dependecies_when_functions(): + t0_cases = [(p, e) for p in ['p0', 'p1'] + for e in ['e0', 'e1', 'e2']] + t1_cases = [(p, e) for p in ['p0', 'p1', 'p2'] + for e in ['e0', 'e1']] + + when = udeps.always + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + } + assert len(deps) == 36 + + when = udeps.part_equal + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if t0[0] == t1[0] + } + assert len(deps) == 12 + + when = udeps.env_equal + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if t0[1] == t1[1] + } + assert len(deps) == 12 + + when = udeps.part_env_equal + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == t1[0] and t0[1] == t1[1]) + } + assert len(deps) == 4 + + when = udeps.part_is('p0') + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == 'p0' and t1[0] == 'p0') + } + assert len(deps) == 6 + + when = udeps.source(udeps.part_is('p0')) + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if t0[0] == 'p0' + } + assert len(deps) == 18 + + when = udeps.dest(udeps.part_is('p0')) + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if t1[0] == 'p0' + } + assert len(deps) == 12 + + when = udeps.env_is('e0') + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if (t0[1] == 'e0' and t1[1] == 'e0') + } + assert len(deps) == 6 + + when = udeps.source(udeps.env_is('e0')) + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if t0[1] == 'e0' + } + assert len(deps) == 12 + + when = udeps.dest(udeps.env_is('e0')) + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if t1[1] == 'e0' + } + assert len(deps) == 18 + + when = udeps.any(udeps.source(udeps.part_is('p0')), + udeps.dest(udeps.env_is('e1'))) + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == 'p0' or t1[1] == 'e1') + } + assert len(deps) == 27 + + when = udeps.all(udeps.source(udeps.part_is('p0')), + udeps.dest(udeps.env_is('e1'))) + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == 'p0' and t1[1] == 'e1') + } + assert len(deps) == 9 + +def test_build_deps_deprecated_syntax(loader, exec_ctx): + class Test0(rfm.RegressionTest): + def __init__(self): + 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'])) + class Test1_deprecated(rfm.RunOnlyRegressionTest): + def __init__(self, kind): + 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] + if kindspec[kind] == rfm.DEPEND_EXACT: + self.depends_on('Test0', kindspec[kind], + {'e0': ['e0', 'e1'], 'e1': ['e1']}) + else: + self.depends_on('Test0', kindspec[kind]) + + with pytest.warns(ReframeDeprecationWarning): + t1 = Test1_deprecated('fully') + assert(t1._userdeps == [('Test0', udeps.part_equal)]) + + with pytest.warns(ReframeDeprecationWarning): + t1 = Test1_deprecated('by_env') + assert(t1._userdeps == [('Test0', udeps.part_env_equal)]) + + with pytest.warns(ReframeDeprecationWarning): + t1 = Test1_deprecated('exact') + when = t1._userdeps[0][1] + t0_cases = [(p, e) for p in ['p0', 'p1'] + for e in ['e0', 'e1']] + t1_cases = [(p, e) for p in ['p0', 'p1'] + for e in ['e0', 'e1']] + deps = {(t0, t1) for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if ((t0[0] == t1[0] and t0[1] == 'e0') or + (t0[0] == t1[0] and t0[1] == 'e1' and t1[1] == 'e1')) + } + assert len(deps) == 6 + def test_build_deps(loader, exec_ctx): checks = loader.load_all() cases = executors.generate_testcases(checks) # Test calling getdep() before having built the graph - t = find_check('Test1_exact', checks) + t = find_check('Test1_always', checks) with pytest.raises(DependencyError): - t.getdep('Test0', 'e0') + t.getdep('Test0', 'e0', 'p0') # Build dependencies and continue testing - deps = dependency.build_deps(cases) - dependency.validate_deps(deps) - - # Check DEPEND_FULLY dependencies - assert num_deps(deps, 'Test1_fully') == 8 + deps = dependencies.build_deps(cases) + dependencies.validate_deps(deps) + + # Check dependencies for fully connected graph + assert num_deps(deps, 'Test1_always') == 16 + for p0 in ['sys0:p0', 'sys0:p1']: + for p1 in ['sys0:p0', 'sys0:p1']: + for e0 in ['e0', 'e1']: + for e1 in ['e0', 'e1']: + assert has_edge(deps, + Node('Test1_always', p0, e0), + Node('Test0', p1, e1)) + + # Check dependencies with same partition + assert num_deps(deps, 'Test1_part_equal') == 8 for p in ['sys0:p0', 'sys0:p1']: for e0 in ['e0', 'e1']: for e1 in ['e0', 'e1']: assert has_edge(deps, - Node('Test1_fully', p, e0), + Node('Test1_part_equal', p, e0), Node('Test0', p, e1)) - # Check DEPEND_BY_ENV - assert num_deps(deps, 'Test1_by_env') == 4 + # Check dependencies with same partition environment + assert num_deps(deps, 'Test1_part_env_equal') == 4 assert num_deps(deps, 'Test1_default') == 4 for p in ['sys0:p0', 'sys0:p1']: for e in ['e0', 'e1']: assert has_edge(deps, - Node('Test1_by_env', p, e), + Node('Test1_part_env_equal', p, e), Node('Test0', p, e)) assert has_edge(deps, Node('Test1_default', p, e), Node('Test0', p, e)) - # Check DEPEND_EXACT - assert num_deps(deps, 'Test1_exact') == 6 - for p in ['sys0:p0', 'sys0:p1']: - assert has_edge(deps, - Node('Test1_exact', p, 'e0'), - Node('Test0', p, 'e0')) - assert has_edge(deps, - Node('Test1_exact', p, 'e0'), - Node('Test0', p, 'e1')) - assert has_edge(deps, - Node('Test1_exact', p, 'e1'), - Node('Test0', p, 'e1')) + assert num_deps(deps, 'Test1_any') == 12 + for p0 in ['sys0:p0', 'sys0:p1']: + for p1 in ['sys0:p0', 'sys0:p1']: + for e0 in ['e0', 'e1']: + for e1 in ['e0', 'e1']: + if (p0 == 'sys0:p0' or e1 == 'e1'): + assert has_edge(deps, + Node('Test1_any', p0, e0), + Node('Test0', p1, e1)) + + assert num_deps(deps, 'Test1_all') == 2 + for p0 in ['sys0:p0', 'sys0:p1']: + for p1 in ['sys0:p0', 'sys0:p1']: + for e0 in ['e0', 'e1']: + for e1 in ['e0', 'e1']: + if (p0 == 'sys0:p0' and p1 == 'sys0:p0' and e1 == 'e1'): + assert has_edge(deps, + Node('Test1_any', p0, e0), + Node('Test0', p1, e1)) + + # Check custom dependencies + assert num_deps(deps, 'Test1_custom') == 1 + assert has_edge(deps, + Node('Test1_custom', 'sys0:p0', 'e0'), + Node('Test0', 'sys0:p1', 'e1')) # Check in-degree of Test0 - # 2 from Test1_fully, - # 1 from Test1_by_env, - # 1 from Test1_exact, + # 4 from Test1_always, + # 2 from Test1_part_equal, + # 1 from Test1_part_env_equal, + # 2 from Test1_any, + # 2 from Test1_all, + # 0 from Test1_custom, # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 5 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 5 - - # 2 from Test1_fully, - # 1 from Test1_by_env, - # 2 from Test1_exact, + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 12 + + # 4 from Test1_always, + # 2 from Test1_part_equal, + # 1 from Test1_part_env_equal, + # 2 from Test1_any, + # 0 from Test1_all, + # 0 from Test1_custom, + # 1 from Test1_default + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 10 + + # 4 from Test1_always, + # 2 from Test1_part_equal, + # 1 from Test1_part_env_equal, + # 4 from Test1_any, + # 0 from Test1_all, + # 0 from Test1_custom, + # 1 from Test1_default + assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 12 + + # 4 from Test1_always, + # 2 from Test1_part_equal, + # 1 from Test1_part_env_equal, + # 4 from Test1_any, + # 0 from Test1_all, + # 1 from Test1_custom, # 1 from Test1_default - assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 6 - assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 6 + assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 13 # Pick a check to test getdep() - check_e0 = find_case('Test1_exact', 'e0', cases).check - check_e1 = find_case('Test1_exact', 'e1', cases).check + check_e0 = find_case('Test1_part_equal', 'e0', 'p0', cases).check + check_e1 = find_case('Test1_part_equal', 'e1', 'p0', cases).check with pytest.raises(DependencyError): - check_e0.getdep('Test0') + check_e0.getdep('Test0', 'p0') # Set the current environment check_e0._current_environ = Environment('e0') check_e1._current_environ = Environment('e1') - assert check_e0.getdep('Test0', 'e0').name == 'Test0' - assert check_e0.getdep('Test0', 'e1').name == 'Test0' - assert check_e1.getdep('Test0', 'e1').name == 'Test0' + assert check_e0.getdep('Test0', 'e0', 'p0').name == 'Test0' + assert check_e0.getdep('Test0', 'e1', 'p0').name == 'Test0' + assert check_e1.getdep('Test0', 'e1', 'p0').name == 'Test0' with pytest.raises(DependencyError): - check_e0.getdep('TestX', 'e0') + check_e0.getdep('TestX_deprecated', 'e0', 'p0') with pytest.raises(DependencyError): - check_e0.getdep('Test0', 'eX') + check_e0.getdep('Test0', 'eX', 'p0') with pytest.raises(DependencyError): - check_e1.getdep('Test0', 'e0') - - -def test_build_deps_unknown_test(loader, exec_ctx): - checks = loader.load_all() - - # Add some inexistent dependencies - test0 = find_check('Test0', checks) - for depkind in ('default', 'fully', 'by_env', 'exact'): - test1 = find_check('Test1_' + depkind, checks) - if depkind == 'default': - test1.depends_on('TestX') - elif depkind == 'exact': - test1.depends_on('TestX', rfm.DEPEND_EXACT, {'e0': ['e0']}) - elif depkind == 'fully': - test1.depends_on('TestX', rfm.DEPEND_FULLY) - elif depkind == 'by_env': - test1.depends_on('TestX', rfm.DEPEND_BY_ENV) - - with pytest.raises(DependencyError): - dependency.build_deps(executors.generate_testcases(checks)) - - -def test_build_deps_unknown_source_env(loader, exec_ctx): - checks = loader.load_all() - - # Add some inexistent dependencies - test0 = find_check('Test0', checks) - test1 = find_check('Test1_default', checks) - test1.depends_on('Test0', rfm.DEPEND_EXACT, {'eX': ['e0']}) - - # Unknown source is ignored, because it might simply be that the test - # is not executed for eX - deps = dependency.build_deps(executors.generate_testcases(checks)) - assert num_deps(deps, 'Test1_default') == 4 + check_e1.getdep('Test0', 'e0', 'p1') def test_build_deps_empty(exec_ctx): - assert {} == dependency.build_deps([]) + assert {} == dependencies.build_deps([]) @pytest.fixture @@ -283,8 +491,8 @@ def test_valid_deps(make_test, exec_ctx): t6.depends_on('t5') t7.depends_on('t5') t8.depends_on('t7') - dependency.validate_deps( - dependency.build_deps( + dependencies.validate_deps( + dependencies.build_deps( executors.generate_testcases([t0, t1, t2, t3, t4, t5, t6, t7, t8]) ) @@ -321,13 +529,13 @@ def test_cyclic_deps(make_test, exec_ctx): t6.depends_on('t5') t7.depends_on('t5') t8.depends_on('t7') - deps = dependency.build_deps( + deps = dependencies.build_deps( executors.generate_testcases([t0, t1, t2, t3, t4, t5, t6, t7, t8]) ) with pytest.raises(DependencyError) as exc_info: - dependency.validate_deps(deps) + dependencies.validate_deps(deps) assert ('t4->t2->t1->t4' in str(exc_info.value) or 't2->t1->t4->t2' in str(exc_info.value) or @@ -340,20 +548,20 @@ def test_cyclic_deps(make_test, exec_ctx): def test_cyclic_deps_by_env(make_test, exec_ctx): t0 = make_test('t0') t1 = make_test('t1') - t1.depends_on('t0', rfm.DEPEND_EXACT, {'e0': ['e0']}) - t0.depends_on('t1', rfm.DEPEND_EXACT, {'e1': ['e1']}) - deps = dependency.build_deps( + t1.depends_on('t0', udeps.env_is('e0')) + t0.depends_on('t1', udeps.env_is('e1')) + deps = dependencies.build_deps( executors.generate_testcases([t0, t1]) ) with pytest.raises(DependencyError) as exc_info: - dependency.validate_deps(deps) + dependencies.validate_deps(deps) assert ('t1->t0->t1' in str(exc_info.value) or 't0->t1->t0' in str(exc_info.value)) def test_validate_deps_empty(exec_ctx): - dependency.validate_deps({}) + dependencies.validate_deps({}) def assert_topological_order(cases, graph): @@ -420,11 +628,11 @@ def test_toposort(make_test, exec_ctx): t6.depends_on('t5') t7.depends_on('t5') t8.depends_on('t7') - deps = dependency.build_deps( + deps = dependencies.build_deps( executors.generate_testcases([t0, t1, t2, t3, t4, t5, t6, t7, t8]) ) - cases = dependency.toposort(deps) + cases = dependencies.toposort(deps) assert_topological_order(cases, deps) @@ -451,11 +659,11 @@ def test_toposort_subgraph(make_test, exec_ctx): t3.depends_on('t2') t4.depends_on('t2') t4.depends_on('t3') - full_deps = dependency.build_deps( + full_deps = dependencies.build_deps( executors.generate_testcases([t0, t1, t2, t3, t4]) ) - partial_deps = dependency.build_deps( + partial_deps = dependencies.build_deps( executors.generate_testcases([t3, t4]), full_deps ) - cases = dependency.toposort(partial_deps, is_subgraph=True) + cases = dependencies.toposort(partial_deps, is_subgraph=True) assert_topological_order(cases, partial_deps) diff --git a/unittests/test_pipeline.py b/unittests/test_pipeline.py index 412a1ab5db..c4ef658554 100644 --- a/unittests/test_pipeline.py +++ b/unittests/test_pipeline.py @@ -646,7 +646,7 @@ def x(self): def test_require_deps(local_exec_ctx): - import reframe.frontend.dependency as dependency + import reframe.frontend.dependencies as dependencies import reframe.frontend.executors as executors @fixtures.custom_prefix('unittests/resources/checks') @@ -675,8 +675,8 @@ def setz(self, T0): self.z = T0().x + 2 cases = executors.generate_testcases([T0(), T1()]) - deps = dependency.build_deps(cases) - for c in dependency.toposort(deps): + deps = dependencies.build_deps(cases) + for c in dependencies.toposort(deps): _run(*c) for c in cases: diff --git a/unittests/test_policies.py b/unittests/test_policies.py index d5643a7c5c..8aedd97301 100644 --- a/unittests/test_policies.py +++ b/unittests/test_policies.py @@ -13,7 +13,7 @@ import reframe import reframe.core.runtime as rt -import reframe.frontend.dependency as dependency +import reframe.frontend.dependencies as dependencies import reframe.frontend.executors as executors import reframe.frontend.executors.policies as policies import reframe.utility as util @@ -89,9 +89,9 @@ def _make_cases(checks=None, sort=False, *args, **kwargs): cases = executors.generate_testcases(checks, *args, **kwargs) if sort: - depgraph = dependency.build_deps(cases) - dependency.validate_deps(depgraph) - cases = dependency.toposort(depgraph) + depgraph = dependencies.build_deps(cases) + dependencies.validate_deps(depgraph) + cases = dependencies.toposort(depgraph) return cases From 1d8dfaa98bbbf4f7a579f46b574445078ea9cee1 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Sun, 25 Oct 2020 08:17:40 +0100 Subject: [PATCH 11/27] move _depends_on_func on a seperate function --- reframe/core/pipeline.py | 93 ++++++++++++++------------------ reframe/frontend/dependencies.py | 2 - 2 files changed, 41 insertions(+), 54 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 285e3bd329..599bd19c53 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1583,6 +1583,41 @@ def cleanup(self, remove_files=False): def user_deps(self): return util.SequenceView(self._userdeps) + def _depends_on_func(self, how, subdeps=None, *args, **kwargs): + if args or kwargs: + raise ValueError('invalid arguments passed') + + msg = ("the arguments `how' and `subdeps' are deprecated, " + "please use the argument `when'") + user_deprecation_warning(msg) + 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'") + + # Now return a proper when function + def exact(src, dst): + if not subdeps: + return False + + return ((src[0] == dst[0]) and + (src[1] in subdeps) and + (dst[1] in subdeps[src[1]])) + + # Follow the old definitions + # DEPEND_BY_ENV used to mean same env, same partition + # & same system + if how == DEPEND_BY_ENV: + return udeps.part_env_equal + # DEPEND_BY_ENV used to mean same partition & same system + elif how == DEPEND_FULLY: + return udeps.part_equal + # DEPEND_EXACT allows dependencies inside the same partition + elif how == DEPEND_EXACT: + return exact + else: + raise TypeError("invalid type of dependency") + def depends_on(self, target, when=None, *args, **kwargs): '''Add a dependency to ``target`` in this test. @@ -1620,59 +1655,13 @@ def part_equal(src, dst): if not isinstance(target, str): raise TypeError("target argument must be of type: `str'") - if when is None and not 'how' in kwargs: - when = udeps.part_env_equal - elif ('how' in kwargs or 'subdeps' in kwargs or args or - isinstance(when, int)): - msg = ("the arguments `how' and `subdeps' are deprecated, " - "please use the argument `when'") - user_deprecation_warning(msg) - - # if `when' is callable ignore other arguments, otherwise - # fix the argument to be the appropriate callable - if when is not callable(when): - if isinstance(when, int): - how = when - # If there is an extra argument it should be subdeps - if args: - subdeps = args[0] - else: - subdeps = None + if (isinstance(when, int) or 'how' in kwargs): + # We are probably using the old syntax; try to get a + # proper when function + when = self._depends_on_func(when, *args, **kwargs) - else: - how = kwargs.get('how', default=DEPEND_BY_ENV) - subdeps = kwargs.get('how', default=None) - - # some sanity checking - if how is not None and 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'") - - def exact(src, dst): - if not subdeps: - return False - - return ((src[0] == dst[0]) and - (src[1] in subdeps) and - (dst[1] in subdeps[src[1]])) - - # Follow the old definitions - # DEPEND_BY_ENV used to mean same env, same partition - # & same system - if how == DEPEND_BY_ENV: - when = udeps.part_env_equal - # DEPEND_BY_ENV used to mean same partition & same system - elif how == DEPEND_FULLY: - when = udeps.part_equal - # DEPEND_EXACT allows dependencies inside the same partition - elif how == DEPEND_EXACT: - when = exact - else: - raise TypeError("invalid type of dependency") + if when is None: + when = udeps.part_env_equal if not callable(when): raise TypeError("when argument must be callable") diff --git a/reframe/frontend/dependencies.py b/reframe/frontend/dependencies.py index 3b25be8cf8..fae84a010f 100644 --- a/reframe/frontend/dependencies.py +++ b/reframe/frontend/dependencies.py @@ -68,13 +68,11 @@ def resolve_dep(target, from_map, fallback_map, key): graph = collections.OrderedDict() for c in cases: pname = c.partition.name - # pname = c.partition.fullname ename = c.environ.name for dep in c.check.user_deps(): tname, when = dep for d in resolve_dep(c, all_cases, default_all_cases, tname): dep_pname = d.partition.name - # dep_pname = d.partition.fullname dep_ename = d.environ.name if when((pname, ename), (dep_pname, dep_ename)): c.deps.append(d) From 3354640828dd9a4c7e1068ef183ceae31f3ea97f Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Sun, 25 Oct 2020 08:22:36 +0100 Subject: [PATCH 12/27] fix pep8 issues --- reframe/utility/dependencies.py | 2 -- unittests/test_dependencies.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/reframe/utility/dependencies.py b/reframe/utility/dependencies.py index e4963f0a17..fc9b793702 100644 --- a/reframe/utility/dependencies.py +++ b/reframe/utility/dependencies.py @@ -42,7 +42,6 @@ def _part_is(src, dst): return _part_is - def env_is(name): def _env_is(src, dst): if src and dst: @@ -85,4 +84,3 @@ def _all(src, dst): return builtins.all(fn(src, dst) for fn in when_funcs) return _all - diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 5104053ba4..bd41457cb5 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -114,6 +114,7 @@ def test_eq_hash(loader, exec_ctx): assert case1 != case0 assert hash(case1) != hash(case0) + def test_dependecies_when_functions(): t0_cases = [(p, e) for p in ['p0', 'p1'] for e in ['e0', 'e1', 'e2']] From dfd2e2ba53f931a050129d79ddc98fda27afdc54 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Sun, 25 Oct 2020 08:29:59 +0100 Subject: [PATCH 13/27] fix pep8 issues --- reframe/core/pipeline.py | 2 +- unittests/test_dependencies.py | 185 +++++++++++++++++++-------------- 2 files changed, 106 insertions(+), 81 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 599bd19c53..75ee4f3bf1 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1611,7 +1611,7 @@ def exact(src, dst): return udeps.part_env_equal # DEPEND_BY_ENV used to mean same partition & same system elif how == DEPEND_FULLY: - return udeps.part_equal + return udeps.part_equal # DEPEND_EXACT allows dependencies inside the same partition elif how == DEPEND_EXACT: return exact diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index bd41457cb5..6fd87b5a9f 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -116,145 +116,170 @@ def test_eq_hash(loader, exec_ctx): def test_dependecies_when_functions(): - t0_cases = [(p, e) for p in ['p0', 'p1'] - for e in ['e0', 'e1', 'e2']] - t1_cases = [(p, e) for p in ['p0', 'p1', 'p2'] - for e in ['e0', 'e1']] + t0_cases = [(p, e) + for p in ['p0', 'p1'] + for e in ['e0', 'e1', 'e2']] + t1_cases = [(p, e) + for p in ['p0', 'p1', 'p2'] + for e in ['e0', 'e1']] when = udeps.always - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases } assert len(deps) == 36 when = udeps.part_equal - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if t0[0] == t1[0] + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if t0[0] == t1[0] } assert len(deps) == 12 when = udeps.env_equal - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if t0[1] == t1[1] + for t1 in t1_cases + if t0[1] == t1[1] } assert len(deps) == 12 when = udeps.part_env_equal - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if (t0[0] == t1[0] and t0[1] == t1[1]) + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == t1[0] and t0[1] == t1[1]) } assert len(deps) == 4 when = udeps.part_is('p0') deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if (t0[0] == 'p0' and t1[0] == 'p0') + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == 'p0' and t1[0] == 'p0') } assert len(deps) == 6 when = udeps.source(udeps.part_is('p0')) - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if t0[0] == 'p0' + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if t0[0] == 'p0' } assert len(deps) == 18 when = udeps.dest(udeps.part_is('p0')) - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if t1[0] == 'p0' + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if t1[0] == 'p0' } assert len(deps) == 12 when = udeps.env_is('e0') - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if (t0[1] == 'e0' and t1[1] == 'e0') + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if (t0[1] == 'e0' and t1[1] == 'e0') } assert len(deps) == 6 when = udeps.source(udeps.env_is('e0')) - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if t0[1] == 'e0' + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if t0[1] == 'e0' } assert len(deps) == 12 when = udeps.dest(udeps.env_is('e0')) - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if t1[1] == 'e0' + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if t1[1] == 'e0' } assert len(deps) == 18 when = udeps.any(udeps.source(udeps.part_is('p0')), udeps.dest(udeps.env_is('e1'))) - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if (t0[0] == 'p0' or t1[1] == 'e1') + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == 'p0' or t1[1] == 'e1') } assert len(deps) == 27 when = udeps.all(udeps.source(udeps.part_is('p0')), udeps.dest(udeps.env_is('e1'))) - deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if when(t0, t1)} assert deps == { - (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if (t0[0] == 'p0' and t1[1] == 'e1') + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if (t0[0] == 'p0' and t1[1] == 'e1') } assert len(deps) == 9 + def test_build_deps_deprecated_syntax(loader, exec_ctx): class Test0(rfm.RegressionTest): def __init__(self): @@ -295,17 +320,17 @@ def __init__(self, kind): t1 = Test1_deprecated('exact') when = t1._userdeps[0][1] t0_cases = [(p, e) for p in ['p0', 'p1'] - for e in ['e0', 'e1']] + for e in ['e0', 'e1']] t1_cases = [(p, e) for p in ['p0', 'p1'] - for e in ['e0', 'e1']] + for e in ['e0', 'e1']] deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + for t1 in t1_cases + if when(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases - for t1 in t1_cases - if ((t0[0] == t1[0] and t0[1] == 'e0') or - (t0[0] == t1[0] and t0[1] == 'e1' and t1[1] == 'e1')) + for t1 in t1_cases + if ((t0[0] == t1[0] and t0[1] == 'e0') or + (t0[0] == t1[0] and t0[1] == 'e1' and t1[1] == 'e1')) } assert len(deps) == 6 From 900fd5358c09157a829b2a499fa11feb470454c0 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Wed, 28 Oct 2020 18:34:56 +0100 Subject: [PATCH 14/27] Address PR comments --- reframe/core/pipeline.py | 36 ++++++++++--------- reframe/frontend/dependencies.py | 27 +++++++------- reframe/utility/{dependencies.py => udeps.py} | 0 .../resources/checks_unlisted/deps_simple.py | 7 ++-- unittests/test_dependencies.py | 13 +++++-- 5 files changed, 46 insertions(+), 37 deletions(-) rename reframe/utility/{dependencies.py => udeps.py} (100%) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 0890e5a048..06bc8e2862 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -25,10 +25,10 @@ import reframe.core.logging as logging import reframe.core.runtime as rt import reframe.utility as util -import reframe.utility.dependencies as udeps import reframe.utility.osext as osext import reframe.utility.sanity as sn import reframe.utility.typecheck as typ +import reframe.utility.udeps as udeps from reframe.core.backends import (getlauncher, getscheduler) from reframe.core.buildsystems import BuildSystemField from reframe.core.containers import ContainerPlatform, ContainerPlatformField @@ -1587,9 +1587,11 @@ def _depends_on_func(self, how, subdeps=None, *args, **kwargs): if args or kwargs: raise ValueError('invalid arguments passed') - msg = ("the arguments `how' and `subdeps' are deprecated, " - "please use the argument `when'") - user_deprecation_warning(msg) + user_deprecation_warning("the arguments `how' and `subdeps' are " + "deprecated, please use the argument `when'") + if not isinstance(how, int): + raise TypeError("subdeps 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 " @@ -1600,23 +1602,22 @@ def exact(src, dst): if not subdeps: return False - return ((src[0] == dst[0]) and - (src[1] in subdeps) and - (dst[1] in subdeps[src[1]])) + p0, e0 = src + p1, e1 = dst + # DEPEND_EXACT allows dependencies inside the same partition + return ((p0 == p1) and (e0 in subdeps) and (e1 in subdeps[e0])) # Follow the old definitions - # DEPEND_BY_ENV used to mean same env, same partition - # & same system + # DEPEND_BY_ENV used to mean same env and same partition if how == DEPEND_BY_ENV: return udeps.part_env_equal - # DEPEND_BY_ENV used to mean same partition & same system + # DEPEND_BY_ENV used to mean same partition elif how == DEPEND_FULLY: return udeps.part_equal - # DEPEND_EXACT allows dependencies inside the same partition elif how == DEPEND_EXACT: return exact else: - raise TypeError("invalid type of dependency") + raise ValueError(f"unknown value passed to 'how' argument: {how}") def depends_on(self, target, when=None, *args, **kwargs): '''Add a dependency to ``target`` in this test. @@ -1664,7 +1665,7 @@ def part_equal(src, dst): when = udeps.part_env_equal if not callable(when): - raise TypeError("when argument must be callable") + raise TypeError("'when' argument must be callable") self._userdeps.append((target, when)) @@ -1698,12 +1699,13 @@ def getdep(self, target, environ=None, part=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 - and d.partition.name == part): + if (d.check.name == target and + d.environ.name == environ and + d.partition.name == part): return d.check - raise DependencyError(f'could not resolve dependency to ({target}, ' - f'{part}, {environ})') + raise DependencyError(f'could not resolve dependency to ({target!r}, ' + f'{part!r}, {environ!r})') def __str__(self): return "%s(name='%s', prefix='%s')" % (type(self).__name__, diff --git a/reframe/frontend/dependencies.py b/reframe/frontend/dependencies.py index fae84a010f..4a78ee9ced 100644 --- a/reframe/frontend/dependencies.py +++ b/reframe/frontend/dependencies.py @@ -9,7 +9,6 @@ import collections import itertools -import re import reframe as rfm import reframe.utility as util @@ -37,14 +36,17 @@ def build_index(cases): return ret - def resolve_dep(target, from_map, fallback_map, key): - errmsg = 'could not resolve dependency: %s -> %s' % (target, key) + all_cases_map = build_index(cases) + default_cases_map = build_index(default_cases) + + def resolve_dep(src, dst): + errmsg = f'could not resolve dependency: {src} -> {dst}' try: - ret = from_map[key] + ret = all_cases_map[dst] except KeyError: # try to resolve the dependency in the fallback map try: - ret = fallback_map[key] + ret = default_cases_map[dst] except KeyError: raise DependencyError(errmsg) from None @@ -53,9 +55,6 @@ def resolve_dep(target, from_map, fallback_map, key): return ret - all_cases = build_index(cases) - default_all_cases = build_index(default_cases) - # NOTE on variable names # # c stands for check or case depending on the context @@ -67,14 +66,14 @@ def resolve_dep(target, from_map, fallback_map, key): # partitions and environments graph = collections.OrderedDict() for c in cases: - pname = c.partition.name - ename = c.environ.name + psrc = c.partition.name + esrc = c.environ.name for dep in c.check.user_deps(): tname, when = dep - for d in resolve_dep(c, all_cases, default_all_cases, tname): - dep_pname = d.partition.name - dep_ename = d.environ.name - if when((pname, ename), (dep_pname, dep_ename)): + for d in resolve_dep(c, tname): + pdst = d.partition.name + edst = d.environ.name + if when((psrc, esrc), (pdst, edst)): c.deps.append(d) graph[c] = util.OrderedSet(c.deps) diff --git a/reframe/utility/dependencies.py b/reframe/utility/udeps.py similarity index 100% rename from reframe/utility/dependencies.py rename to reframe/utility/udeps.py diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index cf79ac3197..ccb04504e3 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -4,8 +4,8 @@ # SPDX-License-Identifier: BSD-3-Clause import reframe as rfm -import reframe.utility.dependencies as udeps import reframe.utility.sanity as sn +import reframe.utility.udeps as udeps @rfm.simple_test @@ -20,7 +20,7 @@ def __init__(self): @rfm.parameterized_test(*([kind] for kind in ['default', 'always', 'part_equal', 'part_env_equal', - 'custom', 'any', 'all'])) + 'custom', 'any', 'all', 'never'])) class Test1(rfm.RunOnlyRegressionTest): def __init__(self, kind): def custom_deps(src, dst): @@ -40,6 +40,7 @@ def custom_deps(src, dst): 'all': udeps.all(udeps.part_is('p0'), udeps.dest(udeps.env_is('e0'))), 'custom': custom_deps, + 'never': (lambda s, d: False), } self.valid_systems = ['sys0:p0', 'sys0:p1'] self.valid_prog_environs = ['e0', 'e1'] @@ -49,4 +50,4 @@ def custom_deps(src, dst): if kind == 'default': self.depends_on('Test0') else: - self.depends_on('Test0', kindspec[kind]) \ No newline at end of file + self.depends_on('Test0', kindspec[kind]) diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 6fd87b5a9f..8135bc76da 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -11,10 +11,10 @@ import reframe.frontend.dependencies as dependencies import reframe.frontend.executors as executors import reframe.utility as util -import reframe.utility.dependencies as udeps +import reframe.utility.udeps as udeps from reframe.core.environments import Environment -from reframe.core.exceptions import (DependencyError, - ReframeDeprecationWarning) +from reframe.core.exceptions import DependencyError +from reframe.core.warnings import ReframeDeprecationWarning from reframe.frontend.loader import RegressionCheckLoader import unittests.fixtures as fixtures @@ -405,6 +405,9 @@ def test_build_deps(loader, exec_ctx): Node('Test1_custom', 'sys0:p0', 'e0'), Node('Test0', 'sys0:p1', 'e1')) + # Check dependencies of Test1_never + assert num_deps(deps, 'Test1_never') == 0 + # Check in-degree of Test0 # 4 from Test1_always, @@ -414,6 +417,7 @@ def test_build_deps(loader, exec_ctx): # 2 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default + # 0 from Test1_never assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 12 # 4 from Test1_always, @@ -423,6 +427,7 @@ def test_build_deps(loader, exec_ctx): # 0 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default + # 0 from Test1_never assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 10 # 4 from Test1_always, @@ -432,6 +437,7 @@ def test_build_deps(loader, exec_ctx): # 0 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default + # 0 from Test1_never assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 12 # 4 from Test1_always, @@ -441,6 +447,7 @@ def test_build_deps(loader, exec_ctx): # 0 from Test1_all, # 1 from Test1_custom, # 1 from Test1_default + # 0 from Test1_never assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 13 # Pick a check to test getdep() From 0cd70b4a5aa84318d640c1445e5da4a2551d5789 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Wed, 28 Oct 2020 18:40:48 +0100 Subject: [PATCH 15/27] rename when argument back to how --- reframe/core/pipeline.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 06bc8e2862..335dfdcf7d 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1587,10 +1587,9 @@ def _depends_on_func(self, how, subdeps=None, *args, **kwargs): if args or kwargs: raise ValueError('invalid arguments passed') - user_deprecation_warning("the arguments `how' and `subdeps' are " - "deprecated, please use the argument `when'") - if not isinstance(how, int): - raise TypeError("subdeps argument must be of type `int'") + user_deprecation_warning("the old syntax of dependencies is " + "deprecated, please pass a callable to " + "the `how' argument") if (subdeps is not None and not isinstance(subdeps, typ.Dict[str, typ.List[str]])): @@ -1619,11 +1618,11 @@ def exact(src, dst): else: raise ValueError(f"unknown value passed to 'how' argument: {how}") - def depends_on(self, target, when=None, *args, **kwargs): + def depends_on(self, target, how=None, *args, **kwargs): '''Add a dependency to ``target`` in this test. :arg target: The name of the target test. - :arg when: A callable that defines the mapping of the dependencies. + :arg how: A callable that defines the mapping of the dependencies. The function the user passes should take as argument the source and destination testcase. When case B depends on case `A' we consider as source case `A' and destination case `B'. In the @@ -1637,7 +1636,7 @@ def part_equal(src, dst): p1, _ = dst return p0 == p1 - self.depends_on('T0', when=part_equal) + self.depends_on('T0', how=part_equal) By default each testcase will depend on the case from target that has the same environment and partition, if it exists. @@ -1650,24 +1649,24 @@ def part_equal(src, dst): .. versionchanged:: 3.3 Dependencies between cases from different partitions are now allowed and the arguments `how' and `subdeps' are deprecated. You should - use the `when' argument. + pass a callable to the `how' argument. ''' if not isinstance(target, str): raise TypeError("target argument must be of type: `str'") - if (isinstance(when, int) or 'how' in kwargs): + if (isinstance(how, int)): # We are probably using the old syntax; try to get a - # proper when function - when = self._depends_on_func(when, *args, **kwargs) + # proper how function + how = self._depends_on_func(how, *args, **kwargs) - if when is None: - when = udeps.part_env_equal + if how is None: + how = udeps.part_env_equal - if not callable(when): - raise TypeError("'when' argument must be callable") + if not callable(how): + raise TypeError("'how' argument must be callable") - self._userdeps.append((target, when)) + self._userdeps.append((target, how)) def getdep(self, target, environ=None, part=None): '''Retrieve the test case of a target dependency. From 38678c22480bfda27376bd55329af79091202070 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Fri, 30 Oct 2020 00:07:01 +0100 Subject: [PATCH 16/27] Update the documentation topic about dependencies --- docs/_static/img/test-deps-by-case.svg | 3 + docs/_static/img/test-deps-by-env.svg | 2 +- docs/_static/img/test-deps-by-part.svg | 3 + docs/_static/img/test-deps-by-xcase.svg | 3 + docs/_static/img/test-deps-by-xenv.svg | 3 + docs/_static/img/test-deps-by-xpart.svg | 3 + docs/_static/img/test-deps-custom.svg | 3 + docs/_static/img/test-deps-cycle.svg | 2 +- docs/_static/img/test-deps-dangling.svg | 3 - docs/_static/img/test-deps-exact.svg | 3 - docs/_static/img/test-deps-fully.svg | 2 +- docs/dependencies.rst | 171 ++++++++++++++++-------- 12 files changed, 134 insertions(+), 67 deletions(-) create mode 100644 docs/_static/img/test-deps-by-case.svg create mode 100644 docs/_static/img/test-deps-by-part.svg create mode 100644 docs/_static/img/test-deps-by-xcase.svg create mode 100644 docs/_static/img/test-deps-by-xenv.svg create mode 100644 docs/_static/img/test-deps-by-xpart.svg create mode 100644 docs/_static/img/test-deps-custom.svg delete mode 100644 docs/_static/img/test-deps-dangling.svg delete mode 100644 docs/_static/img/test-deps-exact.svg diff --git a/docs/_static/img/test-deps-by-case.svg b/docs/_static/img/test-deps-by-case.svg new file mode 100644 index 0000000000..b827c714e4 --- /dev/null +++ b/docs/_static/img/test-deps-by-case.svg @@ -0,0 +1,3 @@ + + +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_case
by_case
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-env.svg b/docs/_static/img/test-deps-by-env.svg index 54b6a25cf2..95c7de5cc9 100644 --- a/docs/_static/img/test-deps-by-env.svg +++ b/docs/_static/img/test-deps-by-env.svg @@ -1,3 +1,3 @@ -
T0,E0
T0,E0
T0,E1
T0,E1
T1,E0
T1,E0
T1,E1
T1,E1
T0
T0
T1
T1
\ No newline at end of file +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_env
by_env
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-part.svg b/docs/_static/img/test-deps-by-part.svg new file mode 100644 index 0000000000..87113ca0ff --- /dev/null +++ b/docs/_static/img/test-deps-by-part.svg @@ -0,0 +1,3 @@ + + +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_part
by_part
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-xcase.svg b/docs/_static/img/test-deps-by-xcase.svg new file mode 100644 index 0000000000..2f8f6e3617 --- /dev/null +++ b/docs/_static/img/test-deps-by-xcase.svg @@ -0,0 +1,3 @@ + + +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_xcase
by_xcase
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-xenv.svg b/docs/_static/img/test-deps-by-xenv.svg new file mode 100644 index 0000000000..f4be2c8133 --- /dev/null +++ b/docs/_static/img/test-deps-by-xenv.svg @@ -0,0 +1,3 @@ + + +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_xenv
by_xenv
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-xpart.svg b/docs/_static/img/test-deps-by-xpart.svg new file mode 100644 index 0000000000..de6c10dcef --- /dev/null +++ b/docs/_static/img/test-deps-by-xpart.svg @@ -0,0 +1,3 @@ + + +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_xpart
by_xpart
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-custom.svg b/docs/_static/img/test-deps-custom.svg new file mode 100644 index 0000000000..63188a1f66 --- /dev/null +++ b/docs/_static/img/test-deps-custom.svg @@ -0,0 +1,3 @@ + + +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
custom
custom
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-cycle.svg b/docs/_static/img/test-deps-cycle.svg index 35a15fd9ff..7279f42da0 100644 --- a/docs/_static/img/test-deps-cycle.svg +++ b/docs/_static/img/test-deps-cycle.svg @@ -1,3 +1,3 @@ -
T0,E0
T0,E0
T0,E1
T0,E1
T1,E0
T1,E0
T1,E1
T1,E1
T0
T0
T1
T1
X
X
\ No newline at end of file +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
X
X
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-dangling.svg b/docs/_static/img/test-deps-dangling.svg deleted file mode 100644 index 544dc80478..0000000000 --- a/docs/_static/img/test-deps-dangling.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
T0,E0
T0,E0
T1,E0
T1,E0
T1,E1
T1,E1
T0
T0
T1
T1
X
X
\ No newline at end of file diff --git a/docs/_static/img/test-deps-exact.svg b/docs/_static/img/test-deps-exact.svg deleted file mode 100644 index 1ad602664e..0000000000 --- a/docs/_static/img/test-deps-exact.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
T0,E0
T0,E0
T0,E1
T0,E1
T1,E0
T1,E0
T1,E1
T1,E1
T0
T0
T1
T1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-fully.svg b/docs/_static/img/test-deps-fully.svg index 92fbdb8891..ef1464135b 100644 --- a/docs/_static/img/test-deps-fully.svg +++ b/docs/_static/img/test-deps-fully.svg @@ -1,3 +1,3 @@ -
T0,E0
T0,E0
T0,E1
T0,E1
T1,E0
T1,E0
T1,E1
T1,E1
T0
T0
T1
T1
\ No newline at end of file +
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
fully
fully
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/dependencies.rst b/docs/dependencies.rst index e108f259d6..29ee052794 100644 --- a/docs/dependencies.rst +++ b/docs/dependencies.rst @@ -6,15 +6,25 @@ Dependencies in ReFrame are defined at the test level using the :func:`depends_o We will see the rules of that projection in a while. The dependency graph construction and the subsequent dependency analysis happen also at the level of the test cases. -Let's assume that test :class:`T1` depends in :class:`T0`. +Let's assume that test :class:`T1` depends on :class:`T0`. This can be expressed inside :class:`T1` using the :func:`depends_on` method: .. code:: python + @rfm.simple_test + class T0(rfm.RegressionTest): + def __init__(self): + ... + self.valid_systems = ['P0', 'P1'] + self.valid_prog_environs = ['E0', 'E1'] + + @rfm.simple_test class T1(rfm.RegressionTest): def __init__(self): ... + self.valid_systems = ['P0', 'P1'] + self.valid_prog_environs = ['E0', 'E1'] self.depends_on('T0') Conceptually, this dependency can be viewed at the test level as follows: @@ -22,17 +32,20 @@ Conceptually, this dependency can be viewed at the test level as follows: .. figure:: _static/img/test-deps.svg :align: center - :alt: Simple test dependency presented conceptually. + + :sub:`Simple test dependency presented conceptually.` For most of the cases, this is sufficient to reason about test dependencies. In reality, as mentioned above, dependencies are handled at the level of test cases. If not specified differently, test cases on different partitions or programming environments are independent. This is the default behavior of the :func:`depends_on` function. -The following image shows the actual test case dependencies assuming that both tests support the ``E0`` and ``E1`` programming environments (for simplicity, we have omitted the partitions): +The following image shows the actual test case dependencies of the two tests above: -.. figure:: _static/img/test-deps-by-env.svg +.. figure:: _static/img/test-deps-by-case.svg :align: center - :alt: Test case dependencies by environment (default). + + :sub:`Test case dependencies partitioned by case (default).` + This means that test cases of :class:`T1` may start executing before all test cases of :class:`T0` have finished. You can impose a stricter dependency between tests, such that :class:`T1` does not start execution unless all test cases of :class:`T0` have finished. @@ -40,99 +53,141 @@ You can achieve this as follows: .. code:: python + import reframe.utility.udeps as udeps + + @rfm.simple_test class T1(rfm.RegressionTest): def __init__(self): ... - self.depends_on('T0', how=rfm.DEPEND_FULLY) + self.depends_on('T0', how=udeps.fully) -This will create the following test case graph: + +This will create a fully connected graph between the test cases of the two tests as it is shown in the following figure: .. figure:: _static/img/test-deps-fully.svg :align: center - :alt: Fully dependent test cases. + + :sub:`Fully dependent test cases.` + + +There are more options that the test case subgraph can be split than the two extremes we presented so far. +The following figures show the different splittings. -You may also create arbitrary dependencies between the test cases of different tests, like in the following example, where the dependencies cannot be represented in any of the other two ways: +Split by partition +------------------ -.. figure:: _static/img/test-deps-exact.svg +The test cases are split in fully connected components per partition. +Test cases from different partitions are independent. + +.. figure:: _static/img/test-deps-by-part.svg :align: center - :alt: Arbitrary test cases dependencies -These dependencies can be achieved as follows: + :sub:`Test case dependencies partitioned by partition.` -.. code:: python - @rfm.simple_test - class T1(rfm.RegressionTest): - def __init__(self): - ... - self.depends_on('T0', how=rfm.DEPEND_EXACT, - subdeps={('P0', 'E0'): [('P0', 'E0'), ('P0', 'E1')], '('P0', 'E1')': [('P0', 'E1')]}) +Split by environment +-------------------- -The ``subdeps`` argument defines the sub-dependencies between the test cases of :class:`T1` and :class:`T0` using an adjacency list representation. +The test cases are split in fully connected components per environment. +Test cases from different environments are independent. +.. figure:: _static/img/test-deps-by-env.svg + :align: center -Cyclic dependencies -------------------- + :sub:`Test case dependencies partitioned by environment.` -Obviously, cyclic dependencies between test cases are not allowed. -Cyclic dependencies between tests are not allowed either, even if the test case dependency graph is acyclic. -For example, the following dependency set up is invalid: -.. figure:: _static/img/test-deps-cycle.svg +Split by exclusive partition +---------------------------- + +The test cases are split in fully connected components that do not contain the same partition. +Test cases from the same partition are independent. + +.. figure:: _static/img/test-deps-by-xpart.svg :align: center - :alt: Any cyclic dependencies between tests are not allowed, even if the underlying test case dependencies are not forming a cycle. -The test case dependencies here, clearly, do not form a cycle, but the edge from ``(T0, E0)`` to ``(T1, E1)`` introduces a dependency from ``T0`` to ``T1`` forming a cycle at the test level. -The reason we impose this restriction is that we wanted to keep the original processing of tests by ReFrame, where all the test cases of a test are processed before moving to the next one. -Supporting this type of dependencies would require to change substantially ReFrame's output. + :sub:`Test case dependencies partitioned by exclusive partition.` -Dangling dependencies ---------------------- +Split by exclusive environment +------------------------------ -In our discussion so far, :class:`T0` and :class:`T1` had the same valid programming environments. -What happens if they do not? -Assume, for example, that :class:`T0` and :class:`T1` are defined as follows: +The test cases are split in fully connected components that do not contain the same environment. +Test cases from the same environment are independent. -.. code:: python +.. figure:: _static/img/test-deps-by-xenv.svg + :align: center - import reframe as rfm - import reframe.utility.sanity as sn + :sub:`Test case dependencies partitioned by exclusive environment.` - @rfm.simple_test - class T0(rfm.RegressionTest): - def __init__(self): - self.valid_systems = ['P0'] - self.valid_prog_environs = ['E0'] - ... +Split by exclusive case +----------------------- + +The test cases are split in fully connected components that do not contain the same environment and the same partition. +Test cases from the same environment and the same partition are independent. + +.. figure:: _static/img/test-deps-by-xcase.svg + :align: center + + :sub:`Test case dependencies partitioned by exclusive case.` + + + +Custom splits +------------- + +Users may define custom dependency patterns by supplying their own ``how`` function. +The ``how`` argument accepts a :py:class:`callable` which takes as arguments the source and destination of a possible edge in the test case subgraph. +If the callable returns :class:`True`, then ReFrame will place an edge (i.e., a dependency) otherwise not. +The following code will create dependencies only if the source partition is ``P0`` and the destination environment is ``E1``: + +.. code:: python + + def myway(src, dst): + psrc, esrc = src + pdst, edst = dst + return psrc == 'P0' and edst == 'E1' @rfm.simple_test class T1(rfm.RegressionTest): def __init__(self): - self.valid_systems = ['P0'] - self.valid_prog_environs = ['E0', 'E1'] - self.depends_on('T0') ... + self.depends_on('T0', how=myway) + + +This corresponds to the following test case dependency subgraph: + + +.. figure:: _static/img/test-deps-custom.svg + :align: center + + :sub:`Custom test case dependencies.` + + +Notice how all the rest test cases are completely independent. -As discussed previously, :func:`depends_on` will create one-to-one dependencies between the different programming environment test cases. -So in this case it will try to create an edge from ``(T1, E1)`` to ``(T0, E1)`` as shown below: -.. figure:: _static/img/test-deps-dangling.svg +Cyclic dependencies +------------------- + +Obviously, cyclic dependencies between test cases are not allowed. +Cyclic dependencies between tests are not allowed either, even if the test case dependency graph is acyclic. +For example, the following dependency set up is invalid: + +.. figure:: _static/img/test-deps-cycle.svg :align: center - :alt: When the target test is valid for less programming environments than the source test, a dangling dependency would be created. + :alt: Any cyclic dependencies between tests are not allowed, even if the underlying test case dependencies are not forming a cycle. +The test case dependencies here, clearly, do not form a cycle, but the edge from ``(T0, P0, E0)`` to ``(T1, P0, E1)`` introduces a dependency from ``T0`` to ``T1`` forming a cycle at the test level. +If you end up requiring such type of dependency in your tests, you might have to reconsider how you organize your tests. -This edge cannot be resolved since the target test case does not exist. -ReFrame will complain and issue an error while trying to build the test dependency graph. -The remedy to this is to use either ``DEPEND_FULLY`` or pass the exact dependencies with ``DEPEND_EXACT`` to :func:`depends_on`. +.. note:: + Technically, the framework could easily support such types of dependencies, but ReFrame's output would have to change substantially. -If :class:`T0` and :class:`T1` had their :attr:`valid_prog_environs` swapped, such that :class:`T0` supported ``E0`` and ``E1`` and :class:`T1` supported only ``E0``, -the default :func:`depends_on` mode would work fine. -The ``(T0, E1)`` test case would simply have no dependent test cases. Resolving dependencies @@ -142,7 +197,7 @@ As shown in the :doc:`tutorial_deps`, test dependencies would be of limited usag Let's reiterate over the :func:`set_executable` function of the :class:`OSULatencyTest` that we presented previously: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py - :lines: 32-38 + :lines: 37-43 The ``@require_deps`` decorator does some magic -- we will unravel this shortly -- with the function arguments of the :func:`set_executable` function and binds them to the target test dependencies by their name. However, as discussed in this section, dependencies are defined at test case level, so the ``OSUBuildTest`` function argument is bound to a special function that allows you to retrieve an actual test case of the target dependency. From ab2ec22cc0631cc6c15468601844f3739af61906 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Fri, 30 Oct 2020 15:20:38 +0100 Subject: [PATCH 17/27] Addess PR comments --- reframe/core/pipeline.py | 13 +-- reframe/frontend/dependencies.py | 2 +- reframe/utility/udeps.py | 2 +- .../resources/checks_unlisted/deps_simple.py | 9 ++- unittests/test_dependencies.py | 81 +++++++++---------- 5 files changed, 54 insertions(+), 53 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 335dfdcf7d..7c2c636656 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1587,9 +1587,9 @@ def _depends_on_func(self, how, subdeps=None, *args, **kwargs): if args or kwargs: raise ValueError('invalid arguments passed') - user_deprecation_warning("the old syntax of dependencies is " - "deprecated, please pass a callable to " - "the `how' argument") + user_deprecation_warning("passing 'how' as an integer or passing " + "'subdeps' is deprecated; please have a " + "look at the user documentation") if (subdeps is not None and not isinstance(subdeps, typ.Dict[str, typ.List[str]])): @@ -1603,6 +1603,7 @@ def exact(src, dst): p0, e0 = src p1, e1 = dst + # DEPEND_EXACT allows dependencies inside the same partition return ((p0 == p1) and (e0 in subdeps) and (e1 in subdeps[e0])) @@ -1624,8 +1625,8 @@ def depends_on(self, target, how=None, *args, **kwargs): :arg target: The name of the target test. :arg how: A callable that defines the mapping of the dependencies. The function the user passes should take as argument the source - and destination testcase. When case B depends on case `A' we - consider as source case `A' and destination case `B'. In the + and destination testcase. When case B depends on case 'A' we + consider as source case 'A' and destination case 'B'. In the following example, each case will depend on every case from T0, that belongs in the same partition. @@ -1648,7 +1649,7 @@ def part_equal(src, dst): .. versionchanged:: 3.3 Dependencies between cases from different partitions are now allowed - and the arguments `how' and `subdeps' are deprecated. You should + and the arguments 'how' and 'subdeps' are deprecated. You should pass a callable to the `how' argument. ''' diff --git a/reframe/frontend/dependencies.py b/reframe/frontend/dependencies.py index 4a78ee9ced..f1e2f0af58 100644 --- a/reframe/frontend/dependencies.py +++ b/reframe/frontend/dependencies.py @@ -40,7 +40,7 @@ def build_index(cases): default_cases_map = build_index(default_cases) def resolve_dep(src, dst): - errmsg = f'could not resolve dependency: {src} -> {dst}' + errmsg = f'could not resolve dependency: {src!r} -> {dst!r}' try: ret = all_cases_map[dst] except KeyError: diff --git a/reframe/utility/udeps.py b/reframe/utility/udeps.py index fc9b793702..a420aa1926 100644 --- a/reframe/utility/udeps.py +++ b/reframe/utility/udeps.py @@ -7,7 +7,7 @@ # Dependency `when' functions -def always(src, dst): +def fully(src, dst): return True diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index ccb04504e3..de4b2e8fce 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -18,9 +18,10 @@ def __init__(self): self.sanity_patterns = sn.assert_found(self.name, self.stdout) -@rfm.parameterized_test(*([kind] for kind in ['default', 'always', +@rfm.parameterized_test(*([kind] for kind in ['default', 'fully', 'part_equal', 'part_env_equal', - 'custom', 'any', 'all', 'never'])) + 'custom', 'any', 'all', + 'edgeless'])) class Test1(rfm.RunOnlyRegressionTest): def __init__(self, kind): def custom_deps(src, dst): @@ -32,7 +33,7 @@ def custom_deps(src, dst): ) kindspec = { - 'always': udeps.always, + 'fully': udeps.fully, 'part_equal': udeps.part_equal, 'part_env_equal': udeps.part_env_equal, 'any': udeps.any(udeps.source(udeps.part_is('p0')), @@ -40,7 +41,7 @@ def custom_deps(src, dst): 'all': udeps.all(udeps.part_is('p0'), udeps.dest(udeps.env_is('e0'))), 'custom': custom_deps, - 'never': (lambda s, d: False), + 'edgeless': (lambda s, d: False), } self.valid_systems = ['sys0:p0', 'sys0:p1'] self.valid_prog_environs = ['e0', 'e1'] diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 8135bc76da..99d01881ea 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -115,7 +115,7 @@ def test_eq_hash(loader, exec_ctx): assert hash(case1) != hash(case0) -def test_dependecies_when_functions(): +def test_dependecies_how_functions(): t0_cases = [(p, e) for p in ['p0', 'p1'] for e in ['e0', 'e1', 'e2']] @@ -123,11 +123,11 @@ def test_dependecies_when_functions(): for p in ['p0', 'p1', 'p2'] for e in ['e0', 'e1']] - when = udeps.always + how = udeps.fully deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) @@ -136,11 +136,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 36 - when = udeps.part_equal + how = udeps.part_equal deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -149,11 +149,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 12 - when = udeps.env_equal + how = udeps.env_equal deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -161,11 +161,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 12 - when = udeps.part_env_equal + how = udeps.part_env_equal deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -174,10 +174,10 @@ def test_dependecies_when_functions(): } assert len(deps) == 4 - when = udeps.part_is('p0') + how = udeps.part_is('p0') deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -186,11 +186,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 6 - when = udeps.source(udeps.part_is('p0')) + how = udeps.source(udeps.part_is('p0')) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -199,11 +199,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 18 - when = udeps.dest(udeps.part_is('p0')) + how = udeps.dest(udeps.part_is('p0')) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -212,11 +212,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 12 - when = udeps.env_is('e0') + how = udeps.env_is('e0') deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -225,11 +225,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 6 - when = udeps.source(udeps.env_is('e0')) + how = udeps.source(udeps.env_is('e0')) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -238,11 +238,11 @@ def test_dependecies_when_functions(): } assert len(deps) == 12 - when = udeps.dest(udeps.env_is('e0')) + how = udeps.dest(udeps.env_is('e0')) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -251,12 +251,12 @@ def test_dependecies_when_functions(): } assert len(deps) == 18 - when = udeps.any(udeps.source(udeps.part_is('p0')), + how = udeps.any(udeps.source(udeps.part_is('p0')), udeps.dest(udeps.env_is('e1'))) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -265,12 +265,12 @@ def test_dependecies_when_functions(): } assert len(deps) == 27 - when = udeps.all(udeps.source(udeps.part_is('p0')), + how = udeps.all(udeps.source(udeps.part_is('p0')), udeps.dest(udeps.env_is('e1'))) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases - if when(t0, t1)} + if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases @@ -318,14 +318,13 @@ def __init__(self, kind): with pytest.warns(ReframeDeprecationWarning): t1 = Test1_deprecated('exact') - when = t1._userdeps[0][1] + how = t1._userdeps[0][1] t0_cases = [(p, e) for p in ['p0', 'p1'] for e in ['e0', 'e1']] t1_cases = [(p, e) for p in ['p0', 'p1'] for e in ['e0', 'e1']] deps = {(t0, t1) for t0 in t0_cases - for t1 in t1_cases - if when(t0, t1)} + for t1 in t1_cases if how(t0, t1)} assert deps == { (t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -340,7 +339,7 @@ def test_build_deps(loader, exec_ctx): cases = executors.generate_testcases(checks) # Test calling getdep() before having built the graph - t = find_check('Test1_always', checks) + t = find_check('Test1_fully', checks) with pytest.raises(DependencyError): t.getdep('Test0', 'e0', 'p0') @@ -349,13 +348,13 @@ def test_build_deps(loader, exec_ctx): dependencies.validate_deps(deps) # Check dependencies for fully connected graph - assert num_deps(deps, 'Test1_always') == 16 + assert num_deps(deps, 'Test1_fully') == 16 for p0 in ['sys0:p0', 'sys0:p1']: for p1 in ['sys0:p0', 'sys0:p1']: for e0 in ['e0', 'e1']: for e1 in ['e0', 'e1']: assert has_edge(deps, - Node('Test1_always', p0, e0), + Node('Test1_fully', p0, e0), Node('Test0', p1, e1)) # Check dependencies with same partition @@ -405,49 +404,49 @@ def test_build_deps(loader, exec_ctx): Node('Test1_custom', 'sys0:p0', 'e0'), Node('Test0', 'sys0:p1', 'e1')) - # Check dependencies of Test1_never - assert num_deps(deps, 'Test1_never') == 0 + # Check dependencies of Test1_edgeless + assert num_deps(deps, 'Test1_edgeless') == 0 # Check in-degree of Test0 - # 4 from Test1_always, + # 4 from Test1_fully, # 2 from Test1_part_equal, # 1 from Test1_part_env_equal, # 2 from Test1_any, # 2 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default - # 0 from Test1_never + # 0 from Test1_edgeless assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 12 - # 4 from Test1_always, + # 4 from Test1_fully, # 2 from Test1_part_equal, # 1 from Test1_part_env_equal, # 2 from Test1_any, # 0 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default - # 0 from Test1_never + # 0 from Test1_edgeless assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 10 - # 4 from Test1_always, + # 4 from Test1_fully, # 2 from Test1_part_equal, # 1 from Test1_part_env_equal, # 4 from Test1_any, # 0 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default - # 0 from Test1_never + # 0 from Test1_edgeless assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 12 - # 4 from Test1_always, + # 4 from Test1_fully, # 2 from Test1_part_equal, # 1 from Test1_part_env_equal, # 4 from Test1_any, # 0 from Test1_all, # 1 from Test1_custom, # 1 from Test1_default - # 0 from Test1_never + # 0 from Test1_edgeless assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 13 # Pick a check to test getdep() From 58fb4c719c4f06d6cf2024d8c8f3df71557ab6ec Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Mon, 2 Nov 2020 10:52:38 +0100 Subject: [PATCH 18/27] Update dependencies how functions --- docs/utility_functions_reference.rst | 8 ++ reframe/core/pipeline.py | 10 +-- reframe/utility/udeps.py | 55 ++++++++++-- .../resources/checks_unlisted/deps_simple.py | 6 +- unittests/test_dependencies.py | 86 +++++++++++++------ 5 files changed, 126 insertions(+), 39 deletions(-) diff --git a/docs/utility_functions_reference.rst b/docs/utility_functions_reference.rst index 82c617031c..36d5ef4022 100644 --- a/docs/utility_functions_reference.rst +++ b/docs/utility_functions_reference.rst @@ -31,3 +31,11 @@ Type Checking Utilities .. automodule:: reframe.utility.typecheck :members: :show-inheritance: + + +Dependencies Utilities +-------------------------------- + +.. automodule:: reframe.utility.udeps + :members: + :show-inheritance: diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 7c2c636656..ca24043336 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1610,10 +1610,10 @@ def exact(src, dst): # Follow the old definitions # DEPEND_BY_ENV used to mean same env and same partition if how == DEPEND_BY_ENV: - return udeps.part_env_equal + return udeps.by_case # DEPEND_BY_ENV used to mean same partition elif how == DEPEND_FULLY: - return udeps.part_equal + return udeps.by_part elif how == DEPEND_EXACT: return exact else: @@ -1632,12 +1632,12 @@ def depends_on(self, target, how=None, *args, **kwargs): .. code-block:: python - def part_equal(src, dst): + def by_part(src, dst): p0, _ = src p1, _ = dst return p0 == p1 - self.depends_on('T0', how=part_equal) + self.depends_on('T0', how=by_part) By default each testcase will depend on the case from target that has the same environment and partition, if it exists. @@ -1662,7 +1662,7 @@ def part_equal(src, dst): how = self._depends_on_func(how, *args, **kwargs) if how is None: - how = udeps.part_env_equal + how = udeps.by_case if not callable(how): raise TypeError("'how' argument must be callable") diff --git a/reframe/utility/udeps.py b/reframe/utility/udeps.py index a420aa1926..b28e9ab04f 100644 --- a/reframe/utility/udeps.py +++ b/reframe/utility/udeps.py @@ -3,29 +3,70 @@ # # SPDX-License-Identifier: BSD-3-Clause +'''Dependecies `how` functions. + +This module defines various functions that can be used with the dependencies. +You can find out more about the existing options that the test case subgraph +can be split `here `__. + +''' + import builtins -# Dependency `when' functions def fully(src, dst): + '''Creates a fully connected graph. + ''' return True -# fully connected in the same partition -def part_equal(src, dst): +def by_part(src, dst): + '''The test cases are split in fully connected components per partition. + Test cases from different partitions are independent. + ''' return src[0] == dst[0] -# different partitions but same env -def env_equal(src, dst): +def by_xpart(src, dst): + '''The test cases are split in fully connected components that do not + contain the same partition. Test cases from the same partition are + independent. + ''' + return src[0] != dst[0] + + +def by_env(src, dst): + '''The test cases are split in fully connected components per environment. + Test cases from different environments are independent. + ''' return src[1] == dst[1] -# same env and part, which is the default also -def part_env_equal(src, dst): +def by_xenv(src, dst): + '''The test cases are split in fully connected components that do not + contain the same environment. Test cases from the same environment are + independent. + ''' + return src[1] != dst[1] + + +def by_case(src, dst): + ''' If not specified differently, test cases on different partitions or + programming environments are independent. This is the default behavior + of the depends_on() function. + ''' return src == dst +def by_xcase(src, dst): + '''The test cases are split in fully connected components that do not + contain the same environment and the same partition. Test cases from + the same environment and the same partition are independent. + ''' + return src != dst + + +# Undocumented 'how' functions def part_is(name): def _part_is(src, dst): if src and dst: diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index de4b2e8fce..a52c8f3432 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -19,7 +19,7 @@ def __init__(self): @rfm.parameterized_test(*([kind] for kind in ['default', 'fully', - 'part_equal', 'part_env_equal', + 'by_part', 'by_case', 'custom', 'any', 'all', 'edgeless'])) class Test1(rfm.RunOnlyRegressionTest): @@ -34,8 +34,8 @@ def custom_deps(src, dst): kindspec = { 'fully': udeps.fully, - 'part_equal': udeps.part_equal, - 'part_env_equal': udeps.part_env_equal, + 'by_part': udeps.by_part, + 'by_case': udeps.by_case, 'any': udeps.any(udeps.source(udeps.part_is('p0')), udeps.dest(udeps.env_is('e1'))), 'all': udeps.all(udeps.part_is('p0'), diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 99d01881ea..a93947e1b6 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -136,7 +136,7 @@ def test_dependecies_how_functions(): } assert len(deps) == 36 - how = udeps.part_equal + how = udeps.by_part deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -149,7 +149,20 @@ def test_dependecies_how_functions(): } assert len(deps) == 12 - how = udeps.env_equal + how = udeps.by_xpart + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if how(t0, t1)} + assert deps == { + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if t0[0] != t1[0] + } + assert len(deps) == 24 + + how = udeps.by_env deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -161,7 +174,19 @@ def test_dependecies_how_functions(): } assert len(deps) == 12 - how = udeps.part_env_equal + how = udeps.by_xenv + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if how(t0, t1)} + assert deps == { + (t0, t1) for t0 in t0_cases + for t1 in t1_cases + if t0[1] != t1[1] + } + assert len(deps) == 24 + + how = udeps.by_case deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -174,6 +199,19 @@ def test_dependecies_how_functions(): } assert len(deps) == 4 + how = udeps.by_xcase + deps = {(t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if how(t0, t1)} + assert deps == { + (t0, t1) + for t0 in t0_cases + for t1 in t1_cases + if (t0[0] != t1[0] or t0[1] != t1[1]) + } + assert len(deps) == 32 + how = udeps.part_is('p0') deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -252,7 +290,7 @@ def test_dependecies_how_functions(): assert len(deps) == 18 how = udeps.any(udeps.source(udeps.part_is('p0')), - udeps.dest(udeps.env_is('e1'))) + udeps.dest(udeps.env_is('e1'))) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -266,7 +304,7 @@ def test_dependecies_how_functions(): assert len(deps) == 27 how = udeps.all(udeps.source(udeps.part_is('p0')), - udeps.dest(udeps.env_is('e1'))) + udeps.dest(udeps.env_is('e1'))) deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -289,13 +327,13 @@ def __init__(self): 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', + @rfm.parameterized_test(*([kind] for kind in ['fully', 'by_case', 'exact'])) class Test1_deprecated(rfm.RunOnlyRegressionTest): def __init__(self, kind): kindspec = { 'fully': rfm.DEPEND_FULLY, - 'by_env': rfm.DEPEND_BY_ENV, + 'by_case': rfm.DEPEND_BY_ENV, 'exact': rfm.DEPEND_EXACT, } self.valid_systems = ['sys0:p0', 'sys0:p1'] @@ -310,11 +348,11 @@ def __init__(self, kind): with pytest.warns(ReframeDeprecationWarning): t1 = Test1_deprecated('fully') - assert(t1._userdeps == [('Test0', udeps.part_equal)]) + assert(t1._userdeps == [('Test0', udeps.by_part)]) with pytest.warns(ReframeDeprecationWarning): - t1 = Test1_deprecated('by_env') - assert(t1._userdeps == [('Test0', udeps.part_env_equal)]) + t1 = Test1_deprecated('by_case') + assert(t1._userdeps == [('Test0', udeps.by_case)]) with pytest.warns(ReframeDeprecationWarning): t1 = Test1_deprecated('exact') @@ -358,21 +396,21 @@ def test_build_deps(loader, exec_ctx): Node('Test0', p1, e1)) # Check dependencies with same partition - assert num_deps(deps, 'Test1_part_equal') == 8 + assert num_deps(deps, 'Test1_by_part') == 8 for p in ['sys0:p0', 'sys0:p1']: for e0 in ['e0', 'e1']: for e1 in ['e0', 'e1']: assert has_edge(deps, - Node('Test1_part_equal', p, e0), + Node('Test1_by_part', p, e0), Node('Test0', p, e1)) # Check dependencies with same partition environment - assert num_deps(deps, 'Test1_part_env_equal') == 4 + assert num_deps(deps, 'Test1_by_case') == 4 assert num_deps(deps, 'Test1_default') == 4 for p in ['sys0:p0', 'sys0:p1']: for e in ['e0', 'e1']: assert has_edge(deps, - Node('Test1_part_env_equal', p, e), + Node('Test1_by_case', p, e), Node('Test0', p, e)) assert has_edge(deps, Node('Test1_default', p, e), @@ -410,8 +448,8 @@ def test_build_deps(loader, exec_ctx): # Check in-degree of Test0 # 4 from Test1_fully, - # 2 from Test1_part_equal, - # 1 from Test1_part_env_equal, + # 2 from Test1_by_part, + # 1 from Test1_by_case, # 2 from Test1_any, # 2 from Test1_all, # 0 from Test1_custom, @@ -420,8 +458,8 @@ def test_build_deps(loader, exec_ctx): assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 12 # 4 from Test1_fully, - # 2 from Test1_part_equal, - # 1 from Test1_part_env_equal, + # 2 from Test1_by_part, + # 1 from Test1_by_case, # 2 from Test1_any, # 0 from Test1_all, # 0 from Test1_custom, @@ -430,8 +468,8 @@ def test_build_deps(loader, exec_ctx): assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 10 # 4 from Test1_fully, - # 2 from Test1_part_equal, - # 1 from Test1_part_env_equal, + # 2 from Test1_by_part, + # 1 from Test1_by_case, # 4 from Test1_any, # 0 from Test1_all, # 0 from Test1_custom, @@ -440,8 +478,8 @@ def test_build_deps(loader, exec_ctx): assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 12 # 4 from Test1_fully, - # 2 from Test1_part_equal, - # 1 from Test1_part_env_equal, + # 2 from Test1_by_part, + # 1 from Test1_by_case, # 4 from Test1_any, # 0 from Test1_all, # 1 from Test1_custom, @@ -450,8 +488,8 @@ def test_build_deps(loader, exec_ctx): assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 13 # Pick a check to test getdep() - check_e0 = find_case('Test1_part_equal', 'e0', 'p0', cases).check - check_e1 = find_case('Test1_part_equal', 'e1', 'p0', cases).check + check_e0 = find_case('Test1_by_part', 'e0', 'p0', cases).check + check_e1 = find_case('Test1_by_part', 'e1', 'p0', cases).check with pytest.raises(DependencyError): check_e0.getdep('Test0', 'p0') From b64e6aee4299f230b5609413391c5a27f5cd6e98 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Mon, 2 Nov 2020 15:05:22 +0100 Subject: [PATCH 19/27] Address PR comments --- .../resources/checks_unlisted/deps_simple.py | 2 +- unittests/test_dependencies.py | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index a52c8f3432..acfef5b317 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -41,7 +41,7 @@ def custom_deps(src, dst): 'all': udeps.all(udeps.part_is('p0'), udeps.dest(udeps.env_is('e0'))), 'custom': custom_deps, - 'edgeless': (lambda s, d: False), + 'nodeps': lambda s, d: False, } self.valid_systems = ['sys0:p0', 'sys0:p1'] self.valid_prog_environs = ['e0', 'e1'] diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index a93947e1b6..19871bbe5d 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -212,6 +212,15 @@ def test_dependecies_how_functions(): } assert len(deps) == 32 + +def test_dependecies_how_functions_undoc(): + t0_cases = [(p, e) + for p in ['p0', 'p1'] + for e in ['e0', 'e1', 'e2']] + t1_cases = [(p, e) + for p in ['p0', 'p1', 'p2'] + for e in ['e0', 'e1']] + how = udeps.part_is('p0') deps = {(t0, t1) for t0 in t0_cases for t1 in t1_cases @@ -442,8 +451,8 @@ def test_build_deps(loader, exec_ctx): Node('Test1_custom', 'sys0:p0', 'e0'), Node('Test0', 'sys0:p1', 'e1')) - # Check dependencies of Test1_edgeless - assert num_deps(deps, 'Test1_edgeless') == 0 + # Check dependencies of Test1_nodeps + assert num_deps(deps, 'Test1_nodeps') == 0 # Check in-degree of Test0 @@ -454,7 +463,7 @@ def test_build_deps(loader, exec_ctx): # 2 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default - # 0 from Test1_edgeless + # 0 from Test1_nodeps assert in_degree(deps, Node('Test0', 'sys0:p0', 'e0')) == 12 # 4 from Test1_fully, @@ -464,7 +473,7 @@ def test_build_deps(loader, exec_ctx): # 0 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default - # 0 from Test1_edgeless + # 0 from Test1_nodeps assert in_degree(deps, Node('Test0', 'sys0:p1', 'e0')) == 10 # 4 from Test1_fully, @@ -474,7 +483,7 @@ def test_build_deps(loader, exec_ctx): # 0 from Test1_all, # 0 from Test1_custom, # 1 from Test1_default - # 0 from Test1_edgeless + # 0 from Test1_nodeps assert in_degree(deps, Node('Test0', 'sys0:p0', 'e1')) == 12 # 4 from Test1_fully, @@ -484,7 +493,7 @@ def test_build_deps(loader, exec_ctx): # 0 from Test1_all, # 1 from Test1_custom, # 1 from Test1_default - # 0 from Test1_edgeless + # 0 from Test1_nodeps assert in_degree(deps, Node('Test0', 'sys0:p1', 'e1')) == 13 # Pick a check to test getdep() From 2e276c5806271ff600242fc2b61eef23f53d53f4 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Mon, 2 Nov 2020 15:13:13 +0100 Subject: [PATCH 20/27] Address PR comments --- unittests/resources/checks_unlisted/deps_simple.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unittests/resources/checks_unlisted/deps_simple.py b/unittests/resources/checks_unlisted/deps_simple.py index acfef5b317..d6e7cdfbe9 100644 --- a/unittests/resources/checks_unlisted/deps_simple.py +++ b/unittests/resources/checks_unlisted/deps_simple.py @@ -21,7 +21,7 @@ def __init__(self): @rfm.parameterized_test(*([kind] for kind in ['default', 'fully', 'by_part', 'by_case', 'custom', 'any', 'all', - 'edgeless'])) + 'nodeps'])) class Test1(rfm.RunOnlyRegressionTest): def __init__(self, kind): def custom_deps(src, dst): From fd20742d39dd6489735a0e47b785e36c7951826a Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Mon, 2 Nov 2020 16:58:52 +0100 Subject: [PATCH 21/27] Update and fix documentation --- docs/_static/img/test-deps-by-case.svg | 2 +- docs/_static/img/test-deps-by-env.svg | 2 +- docs/_static/img/test-deps-by-part.svg | 2 +- docs/_static/img/test-deps-by-xcase.svg | 2 +- docs/_static/img/test-deps-by-xenv.svg | 2 +- docs/_static/img/test-deps-by-xpart.svg | 2 +- docs/_static/img/test-deps-custom.svg | 2 +- docs/_static/img/test-deps-cycle.svg | 2 +- docs/_static/img/test-deps-fully.svg | 2 +- docs/utility_functions_reference.rst | 6 +- reframe/core/pipeline.py | 55 ++++++++++++----- reframe/utility/udeps.py | 79 ++++++++++++++++++------- 12 files changed, 112 insertions(+), 46 deletions(-) diff --git a/docs/_static/img/test-deps-by-case.svg b/docs/_static/img/test-deps-by-case.svg index b827c714e4..3f12db930f 100644 --- a/docs/_static/img/test-deps-by-case.svg +++ b/docs/_static/img/test-deps-by-case.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_case
by_case
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
by_case
by_case
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-env.svg b/docs/_static/img/test-deps-by-env.svg index 95c7de5cc9..37588b0cff 100644 --- a/docs/_static/img/test-deps-by-env.svg +++ b/docs/_static/img/test-deps-by-env.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_env
by_env
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
by_env
by_env
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-part.svg b/docs/_static/img/test-deps-by-part.svg index 87113ca0ff..25fd88d072 100644 --- a/docs/_static/img/test-deps-by-part.svg +++ b/docs/_static/img/test-deps-by-part.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_part
by_part
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
by_part
by_part
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-xcase.svg b/docs/_static/img/test-deps-by-xcase.svg index 2f8f6e3617..9780d097f7 100644 --- a/docs/_static/img/test-deps-by-xcase.svg +++ b/docs/_static/img/test-deps-by-xcase.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_xcase
by_xcase
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
by_xcase
by_xcase
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-xenv.svg b/docs/_static/img/test-deps-by-xenv.svg index f4be2c8133..68c4accd54 100644 --- a/docs/_static/img/test-deps-by-xenv.svg +++ b/docs/_static/img/test-deps-by-xenv.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_xenv
by_xenv
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
by_xenv
by_xenv
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-by-xpart.svg b/docs/_static/img/test-deps-by-xpart.svg index de6c10dcef..d60e6e9c3f 100644 --- a/docs/_static/img/test-deps-by-xpart.svg +++ b/docs/_static/img/test-deps-by-xpart.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
by_xpart
by_xpart
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
by_xpart
by_xpart
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-custom.svg b/docs/_static/img/test-deps-custom.svg index 63188a1f66..d8ccc49d12 100644 --- a/docs/_static/img/test-deps-custom.svg +++ b/docs/_static/img/test-deps-custom.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
custom
custom
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
custom
custom
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-cycle.svg b/docs/_static/img/test-deps-cycle.svg index 7279f42da0..f309764de7 100644 --- a/docs/_static/img/test-deps-cycle.svg +++ b/docs/_static/img/test-deps-cycle.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
X
X
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
X
X
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/_static/img/test-deps-fully.svg b/docs/_static/img/test-deps-fully.svg index ef1464135b..83ed057454 100644 --- a/docs/_static/img/test-deps-fully.svg +++ b/docs/_static/img/test-deps-fully.svg @@ -1,3 +1,3 @@ -
T0,P0, E0
T0,P0, E0
T0,P0, E1
T0,P0, E1
T0
T0
T0,P1, E0
T0,P1, E0
T0,P1, E1
T0,P1, E1
T1,P0, E0
T1,P0, E0
T1,P0, E1
T1,P0, E1
T1
T1
T1,P1, E0
T1,P1, E0
T1,P1, E1
T1,P1, E1
fully
fully
Viewer does not support full SVG 1.1
\ No newline at end of file +
T0, P0, E0
T0, P0, E0
T0, P0, E1
T0, P0, E1
T0
T0
T0, P1, E0
T0, P1, E0
T0, P1, E1
T0, P1, E1
T1, P0, E0
T1, P0, E0
T1, P0, E1
T1, P0, E1
T1
T1
T1, P1, E0
T1, P1, E0
T1, P1, E1
T1, P1, E1
fully
fully
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/utility_functions_reference.rst b/docs/utility_functions_reference.rst index 36d5ef4022..068a51ac00 100644 --- a/docs/utility_functions_reference.rst +++ b/docs/utility_functions_reference.rst @@ -33,8 +33,10 @@ Type Checking Utilities :show-inheritance: -Dependencies Utilities --------------------------------- +.. _test-case-deps-management: + +Test Case Dependencies Management +--------------------------------- .. automodule:: reframe.utility.udeps :members: diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index ca24043336..d7e197a23d 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1620,15 +1620,29 @@ def exact(src, dst): raise ValueError(f"unknown value passed to 'how' argument: {how}") def depends_on(self, target, how=None, *args, **kwargs): - '''Add a dependency to ``target`` in this test. + '''Add a dependency to another test. - :arg target: The name of the target test. - :arg how: A callable that defines the mapping of the dependencies. - The function the user passes should take as argument the source - and destination testcase. When case B depends on case 'A' we - consider as source case 'A' and destination case 'B'. In the - following example, each case will depend on every case from T0, - that belongs in the same partition. + :arg target: The name of the test that this one will depend on. + :arg how: A callable that defines how the test cases of this test + depend on the the test cases of the target test. + This callable should accept two arguments: + + - The source test case (i.e., a test case of this test) + represented as a two-element tuple containing the names of the + partition and the environment of the current test case. + - Test destination test case (i.e., a test case of the target + test) represented as a two-element tuple containing the names of + the partition and the environment of the current target test case. + + It should return :class:`True` if a dependency between the source + and destination test cases exists, :class:`False` otherwise. + + This function will be called multiple times by the framework when + the test DAG is constructed, in order to determine the + connectivity of the two tests. + + In the following example, this test depends on ``T1`` when their + partitions match, otherwise their test cases are independent. .. code-block:: python @@ -1639,18 +1653,29 @@ def by_part(src, dst): self.depends_on('T0', how=by_part) - By default each testcase will depend on the case from target - that has the same environment and partition, if it exists. + The framework offers already a set of predefined relations between + the test cases of inter-dependent tests. See the + :mod:`reframe.utility.udeps` for more details. + + The default ``how`` function is + :func:`reframe.utility.udeps.by_case`, where test cases on + different partitions and environments are independent. + + .. seealso:: + - :doc:`dependencies` + - :ref:`test-case-deps-management` + - For more details on how test dependencies work in ReFrame, please - refer to `How Test Dependencies Work In ReFrame `__. .. versionadded:: 2.21 .. versionchanged:: 3.3 - Dependencies between cases from different partitions are now allowed - and the arguments 'how' and 'subdeps' are deprecated. You should - pass a callable to the `how' argument. + Dependencies between test cases from different partitions are now allowed. + The ``how`` argument now accepts a callable. + + .. deprecated:: 3.3 + Passing an integer to the ``how`` argument as well as using the + ``subdeps`` argument is deprecated. ''' if not isinstance(target, str): diff --git a/reframe/utility/udeps.py b/reframe/utility/udeps.py index b28e9ab04f..147529c967 100644 --- a/reframe/utility/udeps.py +++ b/reframe/utility/udeps.py @@ -3,11 +3,36 @@ # # SPDX-License-Identifier: BSD-3-Clause -'''Dependecies `how` functions. +'''Managing the test case "micro-dependencies" between two tests. -This module defines various functions that can be used with the dependencies. -You can find out more about the existing options that the test case subgraph -can be split `here `__. +This module defines a set of basic functions that can be used with the ``how`` +argument of the :func:`reframe.core.pipeline.RegressionTest.depends_on` +function to control how the individual dependencies between the test cases of +two tests are formed. + +All functions take two arguments, the source and destination vertices of an +edge in the test case dependency subgraph that connects two tests. In the +relation *"T0 depends on T1"*, the source are the test cases of "T0" and the +destination are the test cases of "T1." The source and destination arguments +are two-element tuples containing the names of the partition and the +environment of the corresponding test cases. These functions return +:class:`True` if there is an edge connecting the two test cases or +:class:`False` otherwise. + +A ``how`` function will be called by the framework multiple times when the +test DAG is built. More specifically, for each test dependency relation, it +will be called once for each test case combination of the two tests. + +The ``how`` functions essentially split the test case subgraph of two +dependent tests into fully connected components based on the values of their +supported partitions and environments. + +The :doc:`dependencies` page contains more information about test dependencies +and shows visually the test case subgraph connectivity that the different +``how`` functions described here achieve. + + +.. versionadded:: 3.3 ''' @@ -15,54 +40,68 @@ def fully(src, dst): - '''Creates a fully connected graph. - ''' + '''The test cases of two dependent tests will be fully connected.''' + return True def by_part(src, dst): - '''The test cases are split in fully connected components per partition. + '''The test cases of two dependent tests will be split by partition. + Test cases from different partitions are independent. ''' + return src[0] == dst[0] def by_xpart(src, dst): - '''The test cases are split in fully connected components that do not - contain the same partition. Test cases from the same partition are - independent. + '''The test cases of two dependent tests will be split by the exclusive + disjunction (XOR) of their partitions. + + Test cases from the same partition are independent. ''' + return src[0] != dst[0] def by_env(src, dst): - '''The test cases are split in fully connected components per environment. + '''The test cases of two dependent tests will be split by environment. + Test cases from different environments are independent. ''' + return src[1] == dst[1] def by_xenv(src, dst): - '''The test cases are split in fully connected components that do not - contain the same environment. Test cases from the same environment are - independent. + '''The test cases of two dependent tests will be split by the exclusive + disjunction (XOR) of their environments. + + Test cases from the same environment are independent. ''' + return src[1] != dst[1] def by_case(src, dst): - ''' If not specified differently, test cases on different partitions or - programming environments are independent. This is the default behavior - of the depends_on() function. + '''The test cases of two dependent tests will be split by partition and by + environment. + + Test cases from different partitions and different environments are + independent. ''' + return src == dst def by_xcase(src, dst): - '''The test cases are split in fully connected components that do not - contain the same environment and the same partition. Test cases from - the same environment and the same partition are independent. + '''The test cases of two dependent tests will be split by the exclusive + disjunction (XOR) of their partitions and environments. + + Test cases from the same environment and the same partition are + independent. ''' + return src != dst From 419c0d386aeea7aa8f58ed7b2587425159f0eefc Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 3 Nov 2020 08:55:03 +0100 Subject: [PATCH 22/27] Adapt OSU benchmarks to the new dependencies --- tutorials/config/settings.py | 2 +- tutorials/deps/osu_benchmarks.py | 49 +++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/tutorials/config/settings.py b/tutorials/config/settings.py index 7eedc90ae8..da164d23f2 100644 --- a/tutorials/config/settings.py +++ b/tutorials/config/settings.py @@ -33,7 +33,7 @@ 'descr': 'Login nodes', 'scheduler': 'local', 'launcher': 'local', - 'environs': ['gnu', 'intel', 'pgi', 'cray'], + 'environs': ['gnu', 'intel', 'pgi', 'cray', 'builtin'], }, { 'name': 'gpu', diff --git a/tutorials/deps/osu_benchmarks.py b/tutorials/deps/osu_benchmarks.py index 25dea2d067..26054f5355 100644 --- a/tutorials/deps/osu_benchmarks.py +++ b/tutorials/deps/osu_benchmarks.py @@ -7,6 +7,7 @@ import reframe as rfm import reframe.utility.sanity as sn +import reframe.utility.udeps as udeps class OSUBenchmarkTestBase(rfm.RunOnlyRegressionTest): @@ -29,7 +30,7 @@ def __init__(self): self.perf_patterns = { 'latency': sn.extractsingle(r'^8\s+(\S+)', self.stdout, 1, float) } - self.depends_on('OSUBuildTest') + self.depends_on('OSUBuildTest', udeps.by_env) self.reference = { '*': {'latency': (0, None, None, 'us')} } @@ -37,8 +38,8 @@ def __init__(self): @rfm.require_deps def set_executable(self, OSUBuildTest): self.executable = os.path.join( - OSUBuildTest().stagedir, - 'osu-micro-benchmarks-5.6.2', 'mpi', 'pt2pt', 'osu_latency' + OSUBuildTest(part='login').stagedir, + 'mpi', 'pt2pt', 'osu_latency' ) self.executable_opts = ['-x', '100', '-i', '1000'] @@ -52,7 +53,7 @@ def __init__(self): 'bandwidth': sn.extractsingle(r'^4194304\s+(\S+)', self.stdout, 1, float) } - self.depends_on('OSUBuildTest') + self.depends_on('OSUBuildTest', udeps.by_env) self.reference = { '*': {'bandwidth': (0, None, None, 'MB/s')} } @@ -60,8 +61,8 @@ def __init__(self): @rfm.require_deps def set_executable(self, OSUBuildTest): self.executable = os.path.join( - OSUBuildTest().stagedir, - 'osu-micro-benchmarks-5.6.2', 'mpi', 'pt2pt', 'osu_bw' + OSUBuildTest(part='login').stagedir, + 'mpi', 'pt2pt', 'osu_bw' ) self.executable_opts = ['-x', '100', '-i', '1000'] @@ -74,7 +75,7 @@ def __init__(self, num_tasks): self.perf_patterns = { 'latency': sn.extractsingle(r'^8\s+(\S+)', self.stdout, 1, float) } - self.depends_on('OSUBuildTest') + self.depends_on('OSUBuildTest', udeps.by_env) self.reference = { '*': {'latency': (0, None, None, 'us')} } @@ -83,8 +84,8 @@ def __init__(self, num_tasks): @rfm.require_deps def set_executable(self, OSUBuildTest): self.executable = os.path.join( - OSUBuildTest().stagedir, - 'osu-micro-benchmarks-5.6.2', 'mpi', 'collective', 'osu_allreduce' + OSUBuildTest(part='login').stagedir, + 'mpi', 'collective', 'osu_allreduce' ) self.executable_opts = ['-m', '8', '-x', '1000', '-i', '20000'] @@ -93,14 +94,30 @@ def set_executable(self, OSUBuildTest): class OSUBuildTest(rfm.CompileOnlyRegressionTest): def __init__(self): self.descr = 'OSU benchmarks build test' - self.valid_systems = ['daint:gpu'] + self.valid_systems = ['daint:login'] self.valid_prog_environs = ['gnu', 'pgi', 'intel'] - self.sourcesdir = None - self.prebuild_cmds = [ - 'wget http://mvapich.cse.ohio-state.edu/download/mvapich/osu-micro-benchmarks-5.6.2.tar.gz', - 'tar xzf osu-micro-benchmarks-5.6.2.tar.gz', - 'cd osu-micro-benchmarks-5.6.2' - ] + self.depends_on('OSUDownloadTest', udeps.fully) self.build_system = 'Autotools' self.build_system.max_concurrency = 8 self.sanity_patterns = sn.assert_not_found('error', self.stderr) + + @rfm.require_deps + def set_sourcedir(self, OSUDownloadTest): + self.sourcesdir = os.path.join( + OSUDownloadTest(environ='builtin').stagedir, + 'osu-micro-benchmarks-5.6.2' + ) + + +@rfm.simple_test +class OSUDownloadTest(rfm.RunOnlyRegressionTest): + def __init__(self): + self.descr = 'OSU benchmarks download sources' + self.valid_systems = ['daint:login'] + self.valid_prog_environs = ['builtin'] + self.executable = 'wget' + self.executable_opts = ['http://mvapich.cse.ohio-state.edu/download/mvapich/osu-micro-benchmarks-5.6.2.tar.gz'] + self.postrun_cmds = [ + 'tar xzf osu-micro-benchmarks-5.6.2.tar.gz' + ] + self.sanity_patterns = sn.assert_not_found('error', self.stderr) From 724114a2ee5533cab1840f013e721a1ab57f7b75 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 3 Nov 2020 09:40:07 +0100 Subject: [PATCH 23/27] Update dependencies tutorial --- docs/tutorial_deps.rst | 147 ++++++++++++++++++++++++----------------- 1 file changed, 87 insertions(+), 60 deletions(-) diff --git a/docs/tutorial_deps.rst b/docs/tutorial_deps.rst index 59ede7272b..d57de8b357 100644 --- a/docs/tutorial_deps.rst +++ b/docs/tutorial_deps.rst @@ -2,39 +2,34 @@ Tutorial 2: Using Dependencies in ReFrame Tests =============================================== -.. versionadded:: 2.21 +.. versionadded:: 3.4 A ReFrame test may define dependencies to other tests. An example scenario is to test different runtime configurations of a benchmark that you need to compile, or run a scaling analysis of a code. -In such cases, you don't want to rebuild your test for each runtime configuration. -You could have a build test, which all runtime tests would depend on. +In such cases, you don't want to download and rebuild your test for each runtime configuration. +You could have a test where you fetches the sources, which all build tests would depend on. +And similarly, all the runtime tests would depend on their corresponding build test. This is the approach we take with the following example, that fetches, builds and runs several `OSU benchmarks `__. -We first create a basic compile-only test, that fetches the benchmarks and builds them for the different programming environments: +We first create a basic run-only test, that fetches the benchmarks: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py - :lines: 92-106 + :lines: 112-123 -There is nothing particular to that test, except perhaps that you can set :attr:`sourcesdir ` to ``None`` even for a test that needs to compile something. -In such a case, you should at least provide the commands that fetch the code inside the :attr:`prebuild_cmds ` attribute. - -For the next test we need to use the OSU benchmark binaries that we just built, so as to run the MPI ping-pong benchmark. -Here is the relevant part: +This test doesn't need any specific programming environment, just the `builtin` environment in the `login` partition. +The build tests would then copy the benchmark and build them for the different programming environments: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py - :lines: 12-44 - -First, since we will have multiple similar benchmarks, we move all the common functionality to the :class:`OSUBenchmarkTestBase` base class. -Again nothing new here; we are going to use two nodes for the benchmark and we set :attr:`sourcesdir ` to ``None``, since none of the benchmark tests will use any additional resources. -The new part comes in with the :class:`OSULatencyTest` test in the following line: + :lines: 93-109 +The new part comes in with the :class:`OSUBuildTest` test in the following line: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py - :lines: 32 + :lines: 99 -Here we tell ReFrame that this test depends on a test named ``OSUBuildTest``. +Here we tell ReFrame that this test depends on a test named ``OSUDownloadTest``. This test may or may not be defined in the same test file; all ReFrame needs is the test name. -By default, the :func:`depends_on() ` function will create dependencies between the individual test cases of the :class:`OSULatencyTest` and the :class:`OSUBuildTest`, such that the :class:`OSULatencyTest` using ``PrgEnv-gnu`` will depend on the outcome of the :class:`OSUBuildTest` using ``PrgEnv-gnu``, but not on the outcome of the :class:`OSUBuildTest` using ``PrgEnv-intel``. +The :func:`depends_on() ` function will create dependencies between the individual test cases of the :class:`OSUBuildTest` and the :class:`OSUDownloadTest`, such that all the instances of :class:`OSUBuildTest` will depend on the outcome of the :class:`OSUDownloadTest` using ``builtin``, but not on the outcome of the other :class:`OSUBuildTest` instances. This behaviour can be changed, but it is covered in detail in :doc:`dependencies`. You can create arbitrary test dependency graphs, but they need to be acyclic. If ReFrame detects cyclic dependencies, it will refuse to execute the set of tests and will issue an error pointing out the cycle. @@ -42,27 +37,51 @@ If ReFrame detects cyclic dependencies, it will refuse to execute the set of tes A ReFrame test with dependencies will execute, i.e., enter its `setup` stage, only after `all` of its dependencies have succeeded. If any of its dependencies fails, the current test will be marked as failure as well. -The next step for the :class:`OSULatencyTest` is to set its executable to point to the binary produced by the :class:`OSUBuildTest`. +The next step for the :class:`OSUBuildTest` is to set its sourcesdir to point to the benchmarks that were fetched by the :class:`OSUDownloadTest`. This is achieved with the following specially decorated function: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py - :lines: 37-43 + :lines: 104-109 The :func:`@require_deps ` decorator will bind the arguments passed to the decorated function to the result of the dependency that each argument names. -In this case, it binds the ``OSUBuildTest`` function argument to the result of a dependency named ``OSUBuildTest``. +In this case, it binds the ``OSUDownloadTest`` function argument to the result of a dependency named ``OSUDownloadTest``. In order for the binding to work correctly the function arguments must be named after the target dependencies. However, referring to a dependency only by the test's name is not enough, since a test might be associated with multiple programming environments. For this reason, a dependency argument is actually bound to a function that accepts as argument the name of a target programming environment. -If no arguments are passed to that function, as in this example, the current programming environment is implied, such that ``OSUBuildTest()`` is equivalent to ``OSUBuildTest(self.current_environ.name)``. +If no arguments are passed to that function, as in this example, the current programming environment is implied, such that ``OSUDownloadTest()`` is equivalent to ``OSUDownloadTest(self.current_environ.name, self.current_partition.name)``. This call returns the actual test case of the dependency that has been executed. -This allows you to access any attribute from the target test, as we do in this example by accessing the target test's stage directory, which we use to construct the path of the executable. +This allows you to access any attribute from the target test, as we do in this example by accessing the target test's stage directory, which we use to construct the sourcesdir of the test. + +For the next test we need to use the OSU benchmark binaries that we just built, so as to run the MPI ping-pong benchmark. +Here is the relevant part: + +.. literalinclude:: ../tutorials/deps/osu_benchmarks.py + :lines: 13-44 + +First, since we will have multiple similar benchmarks, we move all the common functionality to the :class:`OSUBenchmarkTestBase` base class. +Again nothing new here; we are going to use two nodes for the benchmark and we set :attr:`sourcesdir ` to ``None``, since none of the benchmark tests will use any additional resources. +Similar to before, we will define the dependencies with the the following line: + +.. literalinclude:: ../tutorials/deps/osu_benchmarks.py + :lines: 32 + +Here we tell ReFrame that this test depends on a test named ``OSUBuildTest`` ``by_env``. +This means that it will depend only on the instances of this test that have the same environment. +The partition will be different in this case. + +The next step for the :class:`OSULatencyTest` is to set its executable to point to the binary produced by the :class:`OSUBuildTest`. +This is achieved with the following specially decorated function: + +.. literalinclude:: ../tutorials/deps/osu_benchmarks.py + :lines: 38-44 + This concludes the presentation of the :class:`OSULatencyTest` test. The :class:`OSUBandwidthTest` is completely analogous. The :class:`OSUAllreduceTest` shown below is similar to the other two, except that it is parameterized. It is essentially a scalability test that is running the ``osu_allreduce`` executable created by the :class:`OSUBuildTest` for 2, 4, 8 and 16 nodes. .. literalinclude:: ../tutorials/deps/osu_benchmarks.py - :lines: 69-89 + :lines: 70-90 The full set of OSU example tests is shown below: @@ -77,22 +96,29 @@ Here is the output when running the OSU tests with the asynchronous execution po .. code-block:: none [ReFrame Setup] - version: 3.3-dev0 (rev: cb974c13) - command: './bin/reframe -C tutorials/config/settings.py -c tutorials/deps/osu_benchmarks.py -r' - launched by: user@dom101 - working directory: '/users/user/Devel/reframe' + version: 3.3-dev1 (rev: 734a53df) + command: './bin/reframe -c tutorials/deps/osu_benchmarks.py -C tutorials/config/settings.py -r' + launched by: user@daint103 + working directory: '/path/to/reframe' settings file: 'tutorials/config/settings.py' - check search path: '/users/user/Devel/reframe/tutorials/deps/osu_benchmarks.py' - stage directory: '/users/user/Devel/reframe/stage' - output directory: '/users/user/Devel/reframe/output' + check search path: '/path/to/reframe/tutorials/deps/osu_benchmarks.py' + stage directory: '/path/to/reframe/stage' + output directory: '/path/to/reframe/output' + + [==========] Running 8 check(s) + [==========] Started on Tue Nov 3 09:07:19 2020 - [==========] Running 7 check(s) - [==========] Started on Mon Oct 12 20:19:40 2020 + [----------] started processing OSUDownloadTest (OSU benchmarks download sources) + [ RUN ] OSUDownloadTest on daint:login using builtin + [----------] finished processing OSUDownloadTest (OSU benchmarks download sources) [----------] started processing OSUBuildTest (OSU benchmarks build test) - [ RUN ] OSUBuildTest on daint:gpu using gnu - [ RUN ] OSUBuildTest on daint:gpu using intel - [ RUN ] OSUBuildTest on daint:gpu using pgi + [ RUN ] OSUBuildTest on daint:login using gnu + [ DEP ] OSUBuildTest on daint:login using gnu + [ RUN ] OSUBuildTest on daint:login using intel + [ DEP ] OSUBuildTest on daint:login using intel + [ RUN ] OSUBuildTest on daint:login using pgi + [ DEP ] OSUBuildTest on daint:login using pgi [----------] finished processing OSUBuildTest (OSU benchmarks build test) [----------] started processing OSULatencyTest (OSU latency test) @@ -150,32 +176,33 @@ Here is the output when running the OSU tests with the asynchronous execution po [----------] finished processing OSUAllreduceTest_16 (OSU Allreduce test) [----------] waiting for spawned checks to finish - [ OK ] ( 1/21) OSUBuildTest on daint:gpu using pgi [compile: 28.225s run: 0.040s total: 28.277s] - [ OK ] ( 2/21) OSUBuildTest on daint:gpu using gnu [compile: 21.495s run: 66.686s total: 88.208s] - [ OK ] ( 3/21) OSUBuildTest on daint:gpu using intel [compile: 38.376s run: 37.468s total: 75.855s] - [ OK ] ( 4/21) OSUAllreduceTest_16 on daint:gpu using pgi [compile: 0.005s run: 14.180s total: 14.197s] - [ OK ] ( 5/21) OSUAllreduceTest_16 on daint:gpu using gnu [compile: 0.008s run: 17.997s total: 18.736s] - [ OK ] ( 6/21) OSUAllreduceTest_4 on daint:gpu using pgi [compile: 0.007s run: 18.581s total: 21.528s] - [ OK ] ( 7/21) OSUAllreduceTest_2 on daint:gpu using pgi [compile: 0.005s run: 45.562s total: 49.983s] - [ OK ] ( 8/21) OSUAllreduceTest_8 on daint:gpu using pgi [compile: 0.006s run: 49.313s total: 50.789s] - [ OK ] ( 9/21) OSUAllreduceTest_8 on daint:gpu using gnu [compile: 0.006s run: 48.884s total: 51.096s] - [ OK ] (10/21) OSUAllreduceTest_4 on daint:gpu using gnu [compile: 0.007s run: 48.169s total: 51.854s] - [ OK ] (11/21) OSULatencyTest on daint:gpu using pgi [compile: 0.006s run: 53.398s total: 60.785s] - [ OK ] (12/21) OSUAllreduceTest_2 on daint:gpu using gnu [compile: 0.005s run: 56.394s total: 61.531s] - [ OK ] (13/21) OSULatencyTest on daint:gpu using gnu [compile: 0.005s run: 55.499s total: 63.628s] - [ OK ] (14/21) OSUAllreduceTest_2 on daint:gpu using intel [compile: 0.006s run: 67.665s total: 70.079s] - [ OK ] (15/21) OSUAllreduceTest_16 on daint:gpu using intel [compile: 0.005s run: 73.259s total: 73.275s] - [ OK ] (16/21) OSULatencyTest on daint:gpu using intel [compile: 0.006s run: 97.960s total: 101.936s] - [ OK ] (17/21) OSUAllreduceTest_8 on daint:gpu using intel [compile: 0.006s run: 101.123s total: 101.933s] - [ OK ] (18/21) OSUAllreduceTest_4 on daint:gpu using intel [compile: 0.007s run: 100.592s total: 102.215s] - [ OK ] (19/21) OSUBandwidthTest on daint:gpu using pgi [compile: 0.005s run: 117.530s total: 123.408s] - [ OK ] (20/21) OSUBandwidthTest on daint:gpu using gnu [compile: 0.005s run: 117.174s total: 123.765s] - [ OK ] (21/21) OSUBandwidthTest on daint:gpu using intel [compile: 0.005s run: 160.484s total: 163.680s] + [ OK ] ( 1/22) OSUDownloadTest on daint:login using builtin [compile: 0.008s run: 2.290s total: 2.321s] + [ OK ] ( 2/22) OSUBuildTest on daint:login using gnu [compile: 19.934s run: 0.032s total: 83.055s] + [ OK ] ( 3/22) OSUBuildTest on daint:login using pgi [compile: 27.271s run: 55.764s total: 83.050s] + [ OK ] ( 4/22) OSUBuildTest on daint:login using intel [compile: 35.764s run: 36.284s total: 99.353s] + [ OK ] ( 5/22) OSULatencyTest on daint:gpu using pgi [compile: 0.005s run: 12.013s total: 22.614s] + [ OK ] ( 6/22) OSUAllreduceTest_2 on daint:gpu using pgi [compile: 0.006s run: 17.876s total: 22.600s] + [ OK ] ( 7/22) OSUAllreduceTest_4 on daint:gpu using pgi [compile: 0.005s run: 19.411s total: 22.604s] + [ OK ] ( 8/22) OSUAllreduceTest_8 on daint:gpu using pgi [compile: 0.006s run: 20.925s total: 22.608s] + [ OK ] ( 9/22) OSUAllreduceTest_16 on daint:gpu using pgi [compile: 0.005s run: 22.595s total: 22.613s] + [ OK ] (10/22) OSUAllreduceTest_4 on daint:gpu using gnu [compile: 0.005s run: 19.094s total: 23.036s] + [ OK ] (11/22) OSUAllreduceTest_16 on daint:gpu using gnu [compile: 0.006s run: 22.103s total: 23.025s] + [ OK ] (12/22) OSUAllreduceTest_8 on daint:gpu using gnu [compile: 0.007s run: 20.923s total: 23.340s] + [ OK ] (13/22) OSUAllreduceTest_2 on daint:gpu using intel [compile: 0.008s run: 20.634s total: 23.274s] + [ OK ] (14/22) OSUAllreduceTest_8 on daint:gpu using intel [compile: 0.006s run: 22.411s total: 23.279s] + [ OK ] (15/22) OSULatencyTest on daint:gpu using gnu [compile: 0.005s run: 29.278s total: 40.611s] + [ OK ] (16/22) OSUAllreduceTest_4 on daint:gpu using intel [compile: 0.007s run: 23.751s total: 25.519s] + [ OK ] (17/22) OSUAllreduceTest_16 on daint:gpu using intel [compile: 0.005s run: 25.742s total: 25.761s] + [ OK ] (18/22) OSULatencyTest on daint:gpu using intel [compile: 0.007s run: 25.195s total: 30.090s] + [ OK ] (19/22) OSUAllreduceTest_2 on daint:gpu using gnu [compile: 0.008s run: 43.811s total: 49.329s] + [ OK ] (20/22) OSUBandwidthTest on daint:gpu using pgi [compile: 0.008s run: 73.940s total: 82.628s] + [ OK ] (21/22) OSUBandwidthTest on daint:gpu using gnu [compile: 0.008s run: 73.129s total: 82.926s] + [ OK ] (22/22) OSUBandwidthTest on daint:gpu using intel [compile: 0.006s run: 81.195s total: 85.084s] [----------] all spawned checks have finished - [ PASSED ] Ran 21 test case(s) from 7 check(s) (0 failure(s)) - [==========] Finished on Mon Oct 12 20:24:02 2020 - Log file(s) saved in: '/tmp/rfm-m5zww8le.log' + [ PASSED ] Ran 22 test case(s) from 8 check(s) (0 failure(s)) + [==========] Finished on Tue Nov 3 09:10:26 2020 + Log file(s) saved in: '/tmp/rfm-wbx399cp.log' Before starting running the tests, ReFrame topologically sorts them based on their dependencies and schedules them for running using the selected execution policy. With the serial execution policy, ReFrame simply executes the tests to completion as they "arrive", since the tests are already topologically sorted. From 7c1b091575ee6fd0045ad8fe5cfe81cfe2582f13 Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 3 Nov 2020 13:28:33 +0100 Subject: [PATCH 24/27] Change OSUDownloadTest environmet --- docs/tutorial_deps.rst | 58 ++++++++++++++++---------------- tutorials/config/settings.py | 2 +- tutorials/deps/osu_benchmarks.py | 4 +-- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/tutorial_deps.rst b/docs/tutorial_deps.rst index d57de8b357..c3786dee46 100644 --- a/docs/tutorial_deps.rst +++ b/docs/tutorial_deps.rst @@ -2,7 +2,7 @@ Tutorial 2: Using Dependencies in ReFrame Tests =============================================== -.. versionadded:: 3.4 +.. versionadded:: 3.3 A ReFrame test may define dependencies to other tests. @@ -16,7 +16,7 @@ We first create a basic run-only test, that fetches the benchmarks: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py :lines: 112-123 -This test doesn't need any specific programming environment, just the `builtin` environment in the `login` partition. +This test doesn't need any specific programming environment, we pick the `gnu` environment in the `login` partition. The build tests would then copy the benchmark and build them for the different programming environments: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py @@ -29,7 +29,7 @@ The new part comes in with the :class:`OSUBuildTest` test in the following line: Here we tell ReFrame that this test depends on a test named ``OSUDownloadTest``. This test may or may not be defined in the same test file; all ReFrame needs is the test name. -The :func:`depends_on() ` function will create dependencies between the individual test cases of the :class:`OSUBuildTest` and the :class:`OSUDownloadTest`, such that all the instances of :class:`OSUBuildTest` will depend on the outcome of the :class:`OSUDownloadTest` using ``builtin``, but not on the outcome of the other :class:`OSUBuildTest` instances. +The :func:`depends_on() ` function will create dependencies between the individual test cases of the :class:`OSUBuildTest` and the :class:`OSUDownloadTest`, such that all the instances of :class:`OSUBuildTest` will depend on the outcome of the :class:`OSUDownloadTest` using ``gnu``, but not on the outcome of the other :class:`OSUBuildTest` instances. This behaviour can be changed, but it is covered in detail in :doc:`dependencies`. You can create arbitrary test dependency graphs, but they need to be acyclic. If ReFrame detects cyclic dependencies, it will refuse to execute the set of tests and will issue an error pointing out the cycle. @@ -106,10 +106,10 @@ Here is the output when running the OSU tests with the asynchronous execution po output directory: '/path/to/reframe/output' [==========] Running 8 check(s) - [==========] Started on Tue Nov 3 09:07:19 2020 + [==========] Started on Tue Nov 3 13:20:28 2020 [----------] started processing OSUDownloadTest (OSU benchmarks download sources) - [ RUN ] OSUDownloadTest on daint:login using builtin + [ RUN ] OSUDownloadTest on daint:login using gnu [----------] finished processing OSUDownloadTest (OSU benchmarks download sources) [----------] started processing OSUBuildTest (OSU benchmarks build test) @@ -176,33 +176,33 @@ Here is the output when running the OSU tests with the asynchronous execution po [----------] finished processing OSUAllreduceTest_16 (OSU Allreduce test) [----------] waiting for spawned checks to finish - [ OK ] ( 1/22) OSUDownloadTest on daint:login using builtin [compile: 0.008s run: 2.290s total: 2.321s] - [ OK ] ( 2/22) OSUBuildTest on daint:login using gnu [compile: 19.934s run: 0.032s total: 83.055s] - [ OK ] ( 3/22) OSUBuildTest on daint:login using pgi [compile: 27.271s run: 55.764s total: 83.050s] - [ OK ] ( 4/22) OSUBuildTest on daint:login using intel [compile: 35.764s run: 36.284s total: 99.353s] - [ OK ] ( 5/22) OSULatencyTest on daint:gpu using pgi [compile: 0.005s run: 12.013s total: 22.614s] - [ OK ] ( 6/22) OSUAllreduceTest_2 on daint:gpu using pgi [compile: 0.006s run: 17.876s total: 22.600s] - [ OK ] ( 7/22) OSUAllreduceTest_4 on daint:gpu using pgi [compile: 0.005s run: 19.411s total: 22.604s] - [ OK ] ( 8/22) OSUAllreduceTest_8 on daint:gpu using pgi [compile: 0.006s run: 20.925s total: 22.608s] - [ OK ] ( 9/22) OSUAllreduceTest_16 on daint:gpu using pgi [compile: 0.005s run: 22.595s total: 22.613s] - [ OK ] (10/22) OSUAllreduceTest_4 on daint:gpu using gnu [compile: 0.005s run: 19.094s total: 23.036s] - [ OK ] (11/22) OSUAllreduceTest_16 on daint:gpu using gnu [compile: 0.006s run: 22.103s total: 23.025s] - [ OK ] (12/22) OSUAllreduceTest_8 on daint:gpu using gnu [compile: 0.007s run: 20.923s total: 23.340s] - [ OK ] (13/22) OSUAllreduceTest_2 on daint:gpu using intel [compile: 0.008s run: 20.634s total: 23.274s] - [ OK ] (14/22) OSUAllreduceTest_8 on daint:gpu using intel [compile: 0.006s run: 22.411s total: 23.279s] - [ OK ] (15/22) OSULatencyTest on daint:gpu using gnu [compile: 0.005s run: 29.278s total: 40.611s] - [ OK ] (16/22) OSUAllreduceTest_4 on daint:gpu using intel [compile: 0.007s run: 23.751s total: 25.519s] - [ OK ] (17/22) OSUAllreduceTest_16 on daint:gpu using intel [compile: 0.005s run: 25.742s total: 25.761s] - [ OK ] (18/22) OSULatencyTest on daint:gpu using intel [compile: 0.007s run: 25.195s total: 30.090s] - [ OK ] (19/22) OSUAllreduceTest_2 on daint:gpu using gnu [compile: 0.008s run: 43.811s total: 49.329s] - [ OK ] (20/22) OSUBandwidthTest on daint:gpu using pgi [compile: 0.008s run: 73.940s total: 82.628s] - [ OK ] (21/22) OSUBandwidthTest on daint:gpu using gnu [compile: 0.008s run: 73.129s total: 82.926s] - [ OK ] (22/22) OSUBandwidthTest on daint:gpu using intel [compile: 0.006s run: 81.195s total: 85.084s] + [ OK ] ( 1/22) OSUDownloadTest on daint:login using gnu [compile: 0.005s run: 3.373s total: 3.408s] + [ OK ] ( 2/22) OSUBuildTest on daint:login using gnu [compile: 22.410s run: 0.035s total: 87.728s] + [ OK ] ( 3/22) OSUBuildTest on daint:login using pgi [compile: 27.725s run: 59.918s total: 87.691s] + [ OK ] ( 4/22) OSUBuildTest on daint:login using intel [compile: 37.437s run: 32.771s total: 98.034s] + [ OK ] ( 5/22) OSUAllreduceTest_2 on daint:gpu using pgi [compile: 0.007s run: 139.339s total: 144.402s] + [ OK ] ( 6/22) OSUAllreduceTest_4 on daint:gpu using pgi [compile: 0.007s run: 140.896s total: 144.395s] + [ OK ] ( 7/22) OSUAllreduceTest_8 on daint:gpu using pgi [compile: 0.006s run: 142.451s total: 144.372s] + [ OK ] ( 8/22) OSUAllreduceTest_16 on daint:gpu using pgi [compile: 0.005s run: 144.342s total: 144.368s] + [ OK ] ( 9/22) OSUAllreduceTest_4 on daint:gpu using gnu [compile: 0.007s run: 140.555s total: 144.833s] + [ OK ] (10/22) OSUAllreduceTest_16 on daint:gpu using gnu [compile: 0.005s run: 143.642s total: 144.778s] + [ OK ] (11/22) OSUAllreduceTest_8 on daint:gpu using gnu [compile: 0.007s run: 142.456s total: 145.151s] + [ OK ] (12/22) OSUAllreduceTest_2 on daint:gpu using gnu [compile: 0.005s run: 139.685s total: 145.510s] + [ OK ] (13/22) OSUBandwidthTest on daint:gpu using gnu [compile: 0.009s run: 193.440s total: 200.818s] + [ OK ] (14/22) OSUBandwidthTest on daint:gpu using pgi [compile: 0.006s run: 194.465s total: 201.080s] + [ OK ] (15/22) OSULatencyTest on daint:gpu using intel [compile: 0.009s run: 278.603s total: 283.389s] + [ OK ] (16/22) OSUAllreduceTest_4 on daint:gpu using intel [compile: 0.006s run: 281.112s total: 283.365s] + [ OK ] (17/22) OSULatencyTest on daint:gpu using pgi [compile: 0.006s run: 285.499s total: 293.712s] + [ OK ] (18/22) OSUAllreduceTest_2 on daint:gpu using intel [compile: 0.006s run: 280.693s total: 283.756s] + [ OK ] (19/22) OSUAllreduceTest_8 on daint:gpu using intel [compile: 0.006s run: 282.550s total: 283.971s] + [ OK ] (20/22) OSUAllreduceTest_16 on daint:gpu using intel [compile: 0.005s run: 284.573s total: 284.596s] + [ OK ] (21/22) OSULatencyTest on daint:gpu using gnu [compile: 0.006s run: 286.186s total: 295.202s] + [ OK ] (22/22) OSUBandwidthTest on daint:gpu using intel [compile: 0.005s run: 340.005s total: 343.927s] [----------] all spawned checks have finished [ PASSED ] Ran 22 test case(s) from 8 check(s) (0 failure(s)) - [==========] Finished on Tue Nov 3 09:10:26 2020 - Log file(s) saved in: '/tmp/rfm-wbx399cp.log' + [==========] Finished on Tue Nov 3 13:27:54 2020 + Log file(s) saved in: '/tmp/rfm-n4lrqiqf.log' Before starting running the tests, ReFrame topologically sorts them based on their dependencies and schedules them for running using the selected execution policy. With the serial execution policy, ReFrame simply executes the tests to completion as they "arrive", since the tests are already topologically sorted. diff --git a/tutorials/config/settings.py b/tutorials/config/settings.py index da164d23f2..7eedc90ae8 100644 --- a/tutorials/config/settings.py +++ b/tutorials/config/settings.py @@ -33,7 +33,7 @@ 'descr': 'Login nodes', 'scheduler': 'local', 'launcher': 'local', - 'environs': ['gnu', 'intel', 'pgi', 'cray', 'builtin'], + 'environs': ['gnu', 'intel', 'pgi', 'cray'], }, { 'name': 'gpu', diff --git a/tutorials/deps/osu_benchmarks.py b/tutorials/deps/osu_benchmarks.py index 26054f5355..45647b3704 100644 --- a/tutorials/deps/osu_benchmarks.py +++ b/tutorials/deps/osu_benchmarks.py @@ -104,7 +104,7 @@ def __init__(self): @rfm.require_deps def set_sourcedir(self, OSUDownloadTest): self.sourcesdir = os.path.join( - OSUDownloadTest(environ='builtin').stagedir, + OSUDownloadTest(environ='gnu').stagedir, 'osu-micro-benchmarks-5.6.2' ) @@ -114,7 +114,7 @@ class OSUDownloadTest(rfm.RunOnlyRegressionTest): def __init__(self): self.descr = 'OSU benchmarks download sources' self.valid_systems = ['daint:login'] - self.valid_prog_environs = ['builtin'] + self.valid_prog_environs = ['gnu'] self.executable = 'wget' self.executable_opts = ['http://mvapich.cse.ohio-state.edu/download/mvapich/osu-micro-benchmarks-5.6.2.tar.gz'] self.postrun_cmds = [ From c2fd347c1352cb3e2f6d43cf8842eff7310cf64d Mon Sep 17 00:00:00 2001 From: Eirini Koutsaniti Date: Tue, 3 Nov 2020 18:52:46 +0100 Subject: [PATCH 25/27] Address comments --- docs/tutorial_deps.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/tutorial_deps.rst b/docs/tutorial_deps.rst index c3786dee46..1f252c61f7 100644 --- a/docs/tutorial_deps.rst +++ b/docs/tutorial_deps.rst @@ -2,34 +2,34 @@ Tutorial 2: Using Dependencies in ReFrame Tests =============================================== -.. versionadded:: 3.3 +.. versionadded:: 2.21 A ReFrame test may define dependencies to other tests. An example scenario is to test different runtime configurations of a benchmark that you need to compile, or run a scaling analysis of a code. In such cases, you don't want to download and rebuild your test for each runtime configuration. -You could have a test where you fetches the sources, which all build tests would depend on. -And similarly, all the runtime tests would depend on their corresponding build test. +You could have a test where only the sources are fetched, and which all build tests would depend on. +And, similarly, all the runtime tests would depend on their corresponding build test. This is the approach we take with the following example, that fetches, builds and runs several `OSU benchmarks `__. We first create a basic run-only test, that fetches the benchmarks: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py :lines: 112-123 -This test doesn't need any specific programming environment, we pick the `gnu` environment in the `login` partition. -The build tests would then copy the benchmark and build them for the different programming environments: +This test doesn't need any specific programming environment, so we simply pick the `gnu` environment in the `login` partition. +The build tests would then copy the benchmark code and build it for the different programming environments: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py :lines: 93-109 -The new part comes in with the :class:`OSUBuildTest` test in the following line: +The only new thing that comes in with the :class:`OSUBuildTest` test is the following line: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py :lines: 99 Here we tell ReFrame that this test depends on a test named ``OSUDownloadTest``. This test may or may not be defined in the same test file; all ReFrame needs is the test name. -The :func:`depends_on() ` function will create dependencies between the individual test cases of the :class:`OSUBuildTest` and the :class:`OSUDownloadTest`, such that all the instances of :class:`OSUBuildTest` will depend on the outcome of the :class:`OSUDownloadTest` using ``gnu``, but not on the outcome of the other :class:`OSUBuildTest` instances. +The :func:`depends_on() ` function will create dependencies between the individual test cases of the :class:`OSUBuildTest` and the :class:`OSUDownloadTest`, such that all the test case of :class:`OSUBuildTest` will depend on the outcome of the :class:`OSUDownloadTest`. This behaviour can be changed, but it is covered in detail in :doc:`dependencies`. You can create arbitrary test dependency graphs, but they need to be acyclic. If ReFrame detects cyclic dependencies, it will refuse to execute the set of tests and will issue an error pointing out the cycle. @@ -37,7 +37,7 @@ If ReFrame detects cyclic dependencies, it will refuse to execute the set of tes A ReFrame test with dependencies will execute, i.e., enter its `setup` stage, only after `all` of its dependencies have succeeded. If any of its dependencies fails, the current test will be marked as failure as well. -The next step for the :class:`OSUBuildTest` is to set its sourcesdir to point to the benchmarks that were fetched by the :class:`OSUDownloadTest`. +The next step for the :class:`OSUBuildTest` is to set its sourcesdir to point to the source code that was fetched by the :class:`OSUDownloadTest`. This is achieved with the following specially decorated function: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py @@ -60,13 +60,13 @@ Here is the relevant part: First, since we will have multiple similar benchmarks, we move all the common functionality to the :class:`OSUBenchmarkTestBase` base class. Again nothing new here; we are going to use two nodes for the benchmark and we set :attr:`sourcesdir ` to ``None``, since none of the benchmark tests will use any additional resources. -Similar to before, we will define the dependencies with the the following line: +As done previously, we define the dependencies with the the following line: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py :lines: 32 -Here we tell ReFrame that this test depends on a test named ``OSUBuildTest`` ``by_env``. -This means that it will depend only on the instances of this test that have the same environment. +Here we tell ReFrame that this test depends on a test named ``OSUBuildTest`` "by environment." +This means that the test cases of this test will depend on the test cases of the ``OSUBuildTest`` that use the same environment. The partition will be different in this case. The next step for the :class:`OSULatencyTest` is to set its executable to point to the binary produced by the :class:`OSUBuildTest`. From 17b2d43c4a0c643503fd1c7389edf4f3d665c8b4 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 4 Nov 2020 10:38:26 +0100 Subject: [PATCH 26/27] Minor fix in the tutorial for test dependencies --- docs/tutorial_deps.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial_deps.rst b/docs/tutorial_deps.rst index 1f252c61f7..24992768f7 100644 --- a/docs/tutorial_deps.rst +++ b/docs/tutorial_deps.rst @@ -63,11 +63,11 @@ Again nothing new here; we are going to use two nodes for the benchmark and we s As done previously, we define the dependencies with the the following line: .. literalinclude:: ../tutorials/deps/osu_benchmarks.py - :lines: 32 + :lines: 33 Here we tell ReFrame that this test depends on a test named ``OSUBuildTest`` "by environment." -This means that the test cases of this test will depend on the test cases of the ``OSUBuildTest`` that use the same environment. -The partition will be different in this case. +This means that the test cases of this test will only depend on the test cases of the ``OSUBuildTest`` that use the same environment; +partitions may be different. The next step for the :class:`OSULatencyTest` is to set its executable to point to the binary produced by the :class:`OSUBuildTest`. This is achieved with the following specially decorated function: From e3038f62b36c81f334749d20f535ee2252edf242 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Wed, 4 Nov 2020 10:44:38 +0100 Subject: [PATCH 27/27] Fix PEP8 issues --- reframe/core/pipeline.py | 7 ++++--- unittests/test_dependencies.py | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index 90f5589887..f794a99e98 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -1643,7 +1643,8 @@ def depends_on(self, target, how=None, *args, **kwargs): partition and the environment of the current test case. - Test destination test case (i.e., a test case of the target test) represented as a two-element tuple containing the names of - the partition and the environment of the current target test case. + the partition and the environment of the current target test + case. It should return :class:`True` if a dependency between the source and destination test cases exists, :class:`False` otherwise. @@ -1681,8 +1682,8 @@ def by_part(src, dst): .. versionadded:: 2.21 .. versionchanged:: 3.3 - Dependencies between test cases from different partitions are now allowed. - The ``how`` argument now accepts a callable. + Dependencies between test cases from different partitions are now + allowed. The ``how`` argument now accepts a callable. .. deprecated:: 3.3 Passing an integer to the ``how`` argument as well as using the diff --git a/unittests/test_dependencies.py b/unittests/test_dependencies.py index 19871bbe5d..a7e158bfe1 100644 --- a/unittests/test_dependencies.py +++ b/unittests/test_dependencies.py @@ -76,8 +76,9 @@ def find_check(name, checks): def find_case(cname, ename, partname, cases): for c in cases: - if (c.check.name == cname and c.environ.name == ename - and c.partition.name == partname): + if (c.check.name == cname and + c.environ.name == ename and + c.partition.name == partname): return c