diff --git a/ert_shared/cli.py b/ert_shared/cli.py index 54f4764218f..3380f7ac02c 100644 --- a/ert_shared/cli.py +++ b/ert_shared/cli.py @@ -11,7 +11,8 @@ from .models.ensemble_smoother import EnsembleSmoother from .models.multiple_data_assimilation import MultipleDataAssimilation from .models.single_test_run import SingleTestRun - +import logging +logging.basicConfig(level=logging.INFO, format='%(message)s') def run_cli(args): @@ -20,6 +21,10 @@ def run_cli(args): ert = EnKFMain(res_config, strict=True, verbose=args.verbose) notifier = ErtCliNotifier(ert, args.config) ERT.adapt(notifier) + + if args.mode == 'workflow': + _execute_workflow(args.name) + return # Setup model if args.mode == 'test_run': @@ -30,12 +35,25 @@ def run_cli(args): model, argument = _setup_ensemble_smoother(args) elif args.mode == 'es_mda': model, argument = _setup_multiple_data_assimilation(args) + else: raise NotImplementedError( "Run type not supported {}".format(args.mode)) model.runSimulations(argument) +def _execute_workflow(workflow_name): + workflow_list = ERT.ert.getWorkflowList() + try: + workflow = workflow_list[workflow_name] + except KeyError: + logging.error("Workflow {} is not in the list of available workflows".format(workflow_name)) + return + context = workflow_list.getContext() + workflow.run(ert=ERT.ert, verbose=True, context=context) + all_successfull = all([v['completed'] for k, v in workflow.getJobsReport().items()]) + if all_successfull: + logging.info("Workflow {} ran successfully!".format(workflow_name)) def _setup_single_test_run(): model = SingleTestRun() diff --git a/ert_shared/main.py b/ert_shared/main.py index 57fdf19d414..00aa46cc8fc 100644 --- a/ert_shared/main.py +++ b/ert_shared/main.py @@ -210,6 +210,21 @@ def get_ert_parser(parser=None): es_mda_parser.set_defaults(func=run_cli) es_mda_parser.add_argument("config", type=valid_file, help=config_help) + + workflow_description = "Executes the workflow given" + workflow_parser = subparsers.add_parser( + "workflow", help=workflow_description, description=workflow_description + ) + workflow_parser.add_argument( + help="Name of workflow", + dest="name" + ) + workflow_parser.add_argument( + "--verbose", action="store_true", help="Show verbose output", default=False + ) + workflow_parser.set_defaults(func=run_cli) + workflow_parser.add_argument("config", type=valid_file, help=config_help) + return parser diff --git a/setup.py b/setup.py index d1a4cdf13fb..34dc2f34018 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ def package_files(directory): 'matplotlib<3', 'scipy', 'pytest', + 'decorator' ], zip_safe=False, tests_require=['pytest', 'mock'], diff --git a/tests/global/test_cli.py b/tests/global/test_cli.py index 048df36c7e4..ab39ef5deb1 100644 --- a/tests/global/test_cli.py +++ b/tests/global/test_cli.py @@ -1,5 +1,6 @@ from tests import ErtTest from res.test import ErtTestContext +from res.enkf import EnKFMain, ResConfig import os import subprocess from res.enkf import EnKFMain, ResConfig @@ -14,6 +15,11 @@ MultipleDataAssimilation from ert_shared.models.single_test_run import SingleTestRun +import shutil +from tests.utils import tmpdir, SOURCE_DIR + + + class EntryPointTest(ErtTest): @@ -179,3 +185,23 @@ def test_analysis_module_no_hit(self): active_name, modules, iterable=True) self.assertIsNone(name) + + @tmpdir(os.path.join(SOURCE_DIR,'test-data/local/poly_example')) + def test_executing_workflow(self): + print("test") + with open('test_wf', 'w') as wf_file: + wf_file.write('EXPORT_RUNPATH') + + config_file = 'poly.ert' + with open(config_file, 'a') as file: + file.write("LOAD_WORKFLOW test_wf") + + rc = ResConfig(user_config_file=config_file) + rc.convertToCReference(None) + ert = EnKFMain(rc) + notifier = ErtCliNotifier(ert, config_file) + ERT.adapt(notifier) + args = Namespace(name="test_wf") + cli._execute_workflow(args.name) + assert os.path.isfile(".ert_runpath_list") + diff --git a/tests/global/test_main.py b/tests/global/test_main.py index 7cf0ba577df..1274d2e8546 100644 --- a/tests/global/test_main.py +++ b/tests/global/test_main.py @@ -85,7 +85,18 @@ def test_argparse_exec_es_mda_default_weights(self): parsed.config, "test-data/local/poly_example/poly.ert") self.assertEquals(parsed.weights, "4, 2, 1") self.assertEquals(parsed.func.__name__, "run_cli") - self.assertFalse(parsed.verbose) + self.assertFalse(parsed.verbose) + + def test_argparse_exec_workflow(self): + parser = ArgumentParser(prog="test_main") + parsed = ert_parser( + parser, ['workflow', "--verbose", "workflow_name", 'test-data/local/poly_example/poly.ert']) + self.assertEquals(parsed.mode, "workflow") + self.assertEquals(parsed.name, "workflow_name") + self.assertEquals( + parsed.config, "test-data/local/poly_example/poly.ert") + self.assertEquals(parsed.func.__name__, "run_cli") + self.assertTrue(parsed.verbose) if __name__ == '__main__': unittest.main() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 00000000000..7d6cd15aec2 --- /dev/null +++ b/tests/utils/__init__.py @@ -0,0 +1,61 @@ +import contextlib +import logging +import os +import tempfile +import shutil + +import decorator +import time + +""" +Swiped from +https://github.com/equinor/everest/blob/master/tests/utils/__init__.py +""" + +SOURCE_DIR = os.path.realpath(os.path.join(__file__, '../../../')) + +def tmpdir(path=None, teardown=True): + """ Decorator based on the `tmp` context """ + def real_decorator(function): + def wrapper(function, *args, **kwargs): + with tmp(path, teardown=teardown): + return function(*args, **kwargs) + return decorator.decorator(wrapper, function) + return real_decorator + + +@contextlib.contextmanager +def tmp(path=None, teardown=True): + """Create and go into tmp directory, returns the path. + This function creates a temporary directory and enters that directory. The + returned object is the path to the created directory. + If @path is not specified, we create an empty directory, otherwise, it must + be a path to an existing directory. In that case, the directory will be + copied into the temporary directory. + If @teardown is True (defaults to True), the directory is (attempted) + deleted after context, otherwise it is kept as is. + """ + cwd = os.getcwd() + fname = tempfile.NamedTemporaryFile().name + + if path: + if not os.path.isdir(path): + logging.debug('tmp:raise no such path') + raise IOError('No such directory: %s' % path) + shutil.copytree(path, fname) + else: + # no path to copy, create empty dir + os.mkdir(fname) + + os.chdir(fname) + + yield fname # give control to caller scope + + os.chdir(cwd) + + if teardown: + try: + shutil.rmtree(fname) + except OSError as oserr: + logging.debug('tmp:rmtree failed %s (%s)' % (fname, oserr)) + shutil.rmtree(fname, ignore_errors=True)