From 98eb1ca59123f5f3bf862e8e2d2a757a3854a13c Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Fri, 28 Aug 2020 16:51:21 +0200 Subject: [PATCH 1/2] Control pipeline hooks from the command-line --- reframe/core/meta.py | 6 ++- reframe/core/pipeline.py | 22 ++++++++-- reframe/frontend/cli.py | 87 +++++++++++++++++++++++++------------- requirements.txt | 1 + unittests/test_pipeline.py | 30 +++++++++++++ 5 files changed, 112 insertions(+), 34 deletions(-) diff --git a/reframe/core/meta.py b/reframe/core/meta.py index fc1786b3ea..45a67045df 100644 --- a/reframe/core/meta.py +++ b/reframe/core/meta.py @@ -33,9 +33,11 @@ def __init__(cls, name, bases, namespace, **kwargs): except AttributeError: pass - hooks['post_setup'] = fn_with_deps + hooks.get('post_setup', []) - cls._rfm_pipeline_hooks = hooks + if fn_with_deps: + hooks['post_setup'] = fn_with_deps + hooks.get('post_setup', []) + cls._rfm_pipeline_hooks = hooks + cls._rfm_disabled_hooks = set() cls._final_methods = {v.__name__ for v in namespace.values() if hasattr(v, '_rfm_final')} diff --git a/reframe/core/pipeline.py b/reframe/core/pipeline.py index b30ae0c860..09f5891324 100644 --- a/reframe/core/pipeline.py +++ b/reframe/core/pipeline.py @@ -76,8 +76,12 @@ def hooks(obj, kind): hook_name = 'xxx' func_names = set() - ret = [] + disabled_hooks = set() + func_list = [] for cls in type(obj).mro(): + if hasattr(cls, '_rfm_disabled_hooks'): + disabled_hooks |= cls._rfm_disabled_hooks + try: funcs = cls._rfm_pipeline_hooks.get(hook_name, []) if any(fn.__name__ in func_names for fn in funcs): @@ -85,11 +89,13 @@ def hooks(obj, kind): continue func_names |= {fn.__name__ for fn in funcs} - ret += funcs + func_list += funcs except AttributeError: pass - return ret + # Remove the disabled hooks before returning + return [fn for fn in func_list + if fn.__name__ not in disabled_hooks] '''Run the hooks before and after func.''' @functools.wraps(func) @@ -129,6 +135,16 @@ class RegressionTest(metaclass=RegressionTestMeta): ''' + @classmethod + def disable_hook(cls, hook_name): + '''Disable pipeline hook by name. + + :arg hook_name: The function name of the hook to be disabled. + + :meta private: + ''' + cls._rfm_disabled_hooks.add(hook_name) + #: The name of the test. #: #: :type: string that can contain any character except ``/`` diff --git a/reframe/frontend/cli.py b/reframe/frontend/cli.py index 074568cd54..da3d16c8bc 100644 --- a/reframe/frontend/cli.py +++ b/reframe/frontend/cli.py @@ -33,23 +33,49 @@ from reframe.frontend.printer import PrettyPrinter -def format_check(check, detailed): - lines = [' - %s (found in %s)' % (check.name, - inspect.getfile(type(check)))] - flex = 'flexible' if check.num_tasks <= 0 else 'standard' - - if detailed: - lines += [ - f" description: {check.descr}", - f" systems: {', '.join(check.valid_systems)}", - f" environments: {', '.join(check.valid_prog_environs)}", - f" modules: {', '.join(check.modules)}", - f" task allocation: {flex}", - f" dependencies: " - f"{', '.join([d[0] for d in check.user_deps()])}", - f" tags: {', '.join(check.tags)}", - f" maintainers: {', '.join(check.maintainers)}" - ] +def format_check(check, detailed=False): + def fmt_list(x): + if not x: + return '' + + return ', '.join(x) + + location = inspect.getfile(type(check)) + if not detailed: + return f'- {check.name} (found in {location!r})\n' + + if check.num_tasks > 0: + node_alloc_scheme = f'standard ({check.num_tasks} task(s))' + elif check.num_tasks == 0: + node_alloc_scheme = 'flexible' + else: + node_alloc_scheme = f'flexible (minimum {-check.num_tasks} task(s))' + + check_info = { + 'Dependencies': fmt_list([d[0] for d in check.user_deps()]), + 'Description': check.descr, + 'Environment modules': fmt_list(check.modules), + 'Location': location, + 'Maintainers': fmt_list(check.maintainers), + 'Node allocation': node_alloc_scheme, + 'Pipeline hooks': { + k: fmt_list(fn.__name__ for fn in v) + for k, v in type(check)._rfm_pipeline_hooks.items() + }, + 'Tags': fmt_list(check.tags), + 'Valid environments': fmt_list(check.valid_prog_environs), + 'Valid systems': fmt_list(check.valid_systems) + } + lines = [f'- {check.name}:'] + for prop, val in check_info.items(): + lines.append(f' {prop}:') + if isinstance(val, dict): + for k, v in val.items(): + lines.append(f' {k}: {v}') + else: + lines.append(f' {val}') + + lines.append('') return '\n'.join(lines) @@ -64,10 +90,8 @@ def format_env(envvars): def list_checks(checks, printer, detailed=False): printer.info('[List of matched checks]') - for c in checks: - printer.info(format_check(c, detailed)) - - printer.info('\nFound %d check(s).' % len(checks)) + printer.info('\n'.join(format_check(c, detailed) for c in checks)) + printer.info(f'Found {len(checks)} check(s)') def generate_report_filename(filepatt): @@ -357,7 +381,11 @@ def main(): ) misc_options.add_argument( '--upgrade-config-file', action='store', metavar='OLD[:NEW]', - help='Upgrade old configuration file to new syntax' + help='Upgrade ReFrame 2.x configuration file to ReFrame 3.x syntax' + ) + misc_options.add_argument( + '--disable-hook', action='append', metavar='NAME', dest='hooks', + default=[], help='Disable a pipeline hook for this run' ) misc_options.add_argument( '-V', '--version', action='version', version=os_ext.reframe_version() @@ -613,6 +641,12 @@ def print_infoline(param, value): # Generate the test cases, validate dependencies and sort them checks_matched = list(checks_matched) + + # Disable hooks + for c in checks_matched: + for h in options.hooks: + type(c).disable_hook(h) + testcases = generate_testcases(checks_matched, options.skip_system_check, options.skip_prgenv_check, @@ -675,13 +709,8 @@ def print_infoline(param, value): # Act on checks success = True - if options.list: - # List matched checks - list_checks(list(checks_matched), printer) - elif options.list_detailed: - # List matched checks with details - list_checks(list(checks_matched), printer, detailed=True) - + if options.list or options.list_detailed: + list_checks(list(checks_matched), printer, options.list_detailed) elif options.run: # Setup the execution policy if options.exec_policy == 'serial': diff --git a/requirements.txt b/requirements.txt index f9ba2da65c..4a66202bea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ jsonschema pytest>=5.0.0 +pyyaml coverage setuptools wcwidth diff --git a/unittests/test_pipeline.py b/unittests/test_pipeline.py index b57d3d5073..9424b6e0e0 100644 --- a/unittests/test_pipeline.py +++ b/unittests/test_pipeline.py @@ -630,6 +630,36 @@ def y(self): assert test.foo == 10 +def test_disabled_hooks(local_exec_ctx): + @fixtures.custom_prefix('unittests/resources/checks') + class BaseTest(HelloTest): + def __init__(self): + super().__init__() + self.name = type(self).__name__ + self.executable = os.path.join('.', self.name) + self.var = 0 + self.foo = 0 + + @rfm.run_after('setup') + def x(self): + self.var += 1 + + @rfm.run_before('setup') + def y(self): + self.foo += 1 + + class MyTest(BaseTest): + @rfm.run_after('setup') + def x(self): + self.var += 5 + + test = MyTest() + MyTest.disable_hook('y') + _run(test, *local_exec_ctx) + assert test.var == 5 + assert test.foo == 0 + + def test_require_deps(local_exec_ctx): import reframe.frontend.dependency as dependency import reframe.frontend.executors as executors From 1927c5ee13a18e8e5d91921ce0959e9da7478649 Mon Sep 17 00:00:00 2001 From: Vasileios Karakasis Date: Mon, 31 Aug 2020 13:19:22 +0200 Subject: [PATCH 2/2] Document the --disable-hook option Also: - Move the `--disable-hook` option to run options - Remove pyyaml from `requirements.txt` --- docs/manpage.rst | 9 +++++++++ reframe/frontend/cli.py | 8 ++++---- requirements.txt | 1 - 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/manpage.rst b/docs/manpage.rst index 0722a51a2b..9e3a62dfac 100644 --- a/docs/manpage.rst +++ b/docs/manpage.rst @@ -303,6 +303,15 @@ Options controlling ReFrame execution The test stage and output directories will receive a ``_retry`` suffix every time the test is retried. +.. option:: --disable-hook=HOOK + + Disable the pipeline hook named ``HOOK`` from all the tests that will run. + This feature is useful when you have implemented test workarounds as pipeline hooks, in which case you can quickly disable them from the command line. + This option may be specified multiple times in order to disable multiple hooks at the same time. + + .. versionadded:: 3.2 + + ---------------------------------- Options controlling job submission ---------------------------------- diff --git a/reframe/frontend/cli.py b/reframe/frontend/cli.py index da3d16c8bc..c51273e66f 100644 --- a/reframe/frontend/cli.py +++ b/reframe/frontend/cli.py @@ -316,6 +316,10 @@ def main(): dest='flex_alloc_nodes', metavar='{all|STATE|NUM}', default=None, help='Set strategy for the flexible node allocation (default: "idle").' ) + run_options.add_argument( + '--disable-hook', action='append', metavar='NAME', dest='hooks', + default=[], help='Disable a pipeline hook for this run' + ) env_options.add_argument( '-M', '--map-module', action='append', metavar='MAPPING', dest='module_mappings', default=[], @@ -383,10 +387,6 @@ def main(): '--upgrade-config-file', action='store', metavar='OLD[:NEW]', help='Upgrade ReFrame 2.x configuration file to ReFrame 3.x syntax' ) - misc_options.add_argument( - '--disable-hook', action='append', metavar='NAME', dest='hooks', - default=[], help='Disable a pipeline hook for this run' - ) misc_options.add_argument( '-V', '--version', action='version', version=os_ext.reframe_version() ) diff --git a/requirements.txt b/requirements.txt index 4a66202bea..f9ba2da65c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ jsonschema pytest>=5.0.0 -pyyaml coverage setuptools wcwidth