From 7f606287897fe016a06c21aabdc6e36f861ad620 Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Wed, 7 Jun 2023 12:09:29 -0600 Subject: [PATCH 1/2] Pull out common setup between run procedures There is a little bit of setup that is common to setting up a suite, test case, or step, and we can modularize it better. --- polaris/run/__init__.py | 50 +++++++++++++++++++++++++ polaris/run/serial.py | 82 ++++++++++++++++++++--------------------- 2 files changed, 90 insertions(+), 42 deletions(-) diff --git a/polaris/run/__init__.py b/polaris/run/__init__.py index e69de29bb..f498d82b5 100644 --- a/polaris/run/__init__.py +++ b/polaris/run/__init__.py @@ -0,0 +1,50 @@ +import os +import pickle + +from polaris.config import PolarisConfigParser + + +def unpickle_suite(suite_name): + """ + Unpickle the suite + + Parameters + ---------- + suite_name : str + The name of the test suite + + Returns + ------- + test_suite : dict + The test suite + """ + # Allow a suite name to either include or not the .pickle suffix + if suite_name.endswith('.pickle'): + # code below assumes no suffix, so remove it + suite_name = suite_name[:-len('.pickle')] + # Now open the the suite's pickle file + if not os.path.exists(f'{suite_name}.pickle'): + raise ValueError(f'The suite "{suite_name}" does not appear to have ' + f'been set up here.') + with open(f'{suite_name}.pickle', 'rb') as handle: + test_suite = pickle.load(handle) + return test_suite + + +def setup_config(config_filename): + """ + Set up the config object from the config file + + Parameters + ---------- + config_filename : str + The config filename + + Returns + ------- + config : polaris.config.PolarisConfigParser + The config object + """ + config = PolarisConfigParser() + config.add_from_file(config_filename) + return config diff --git a/polaris/run/serial.py b/polaris/run/serial.py index 047cf19ef..8a7ac8f4f 100644 --- a/polaris/run/serial.py +++ b/polaris/run/serial.py @@ -8,13 +8,13 @@ import mpas_tools.io from mpas_tools.logging import LoggingContext, check_call -from polaris.config import PolarisConfigParser from polaris.logging import log_function_call, log_method_call from polaris.parallel import ( get_available_parallel_resources, run_command, set_cores_per_node, ) +from polaris.run import setup_config, unpickle_suite def run_tests(suite_name, quiet=False, is_test_case=False, steps_to_run=None, @@ -44,23 +44,13 @@ def run_tests(suite_name, quiet=False, is_test_case=False, steps_to_run=None, Typically, these are steps to remove from the defaults """ - # Allow a suite name to either include or not the .pickle suffix - if suite_name.endswith('.pickle'): - # code below assumes no suffix, so remove it - suite_name = suite_name[:-len('.pickle')] - # Now open the the suite's pickle file - if not os.path.exists(f'{suite_name}.pickle'): - raise ValueError(f'The suite "{suite_name}" does not appear to have ' - f'been set up here.') - with open(f'{suite_name}.pickle', 'rb') as handle: - test_suite = pickle.load(handle) + test_suite = unpickle_suite(suite_name) # get the config file for the first test case in the suite test_case = next(iter(test_suite['test_cases'].values())) config_filename = os.path.join(test_case.work_dir, test_case.config_filename) - config = PolarisConfigParser() - config.add_from_file(config_filename) + config = setup_config(config_filename) available_resources = get_available_parallel_resources(config) # start logging to stdout/stderr @@ -104,28 +94,8 @@ def run_tests(suite_name, quiet=False, is_test_case=False, steps_to_run=None, suite_time = time.time() - suite_start os.chdir(cwd) - - stdout_logger.info('Test Runtimes:') - for test_name, test_time in test_times.items(): - secs = round(test_time) - mins = secs // 60 - secs -= 60 * mins - stdout_logger.info(f'{mins:02d}:{secs:02d} ' - f'{success_strs[test_name]} {test_name}') - secs = round(suite_time) - mins = secs // 60 - secs -= 60 * mins - stdout_logger.info(f'Total runtime {mins:02d}:{secs:02d}') - - if failures == 0: - stdout_logger.info('PASS: All passed successfully!') - else: - if failures == 1: - message = '1 test' - else: - message = f'{failures} tests' - stdout_logger.error(f'FAIL: {message} failed, see above.') - sys.exit(1) + _log_test_runtimes(stdout_logger, test_times, success_strs, suite_time, + failures) def run_single_step(step_is_subprocess=False): @@ -146,12 +116,9 @@ def run_single_step(step_is_subprocess=False): if step_is_subprocess: step.run_as_subprocess = False - config = PolarisConfigParser() - config.add_from_file(step.config_filename) - - available_resources = get_available_parallel_resources(config) - + config = setup_config(step.config_filename) test_case.config = config + available_resources = get_available_parallel_resources(config) set_cores_per_node(test_case.config, available_resources['cores_per_node']) mpas_tools.io.default_format = config.get('io', 'format') @@ -198,15 +165,19 @@ def main(): "a step is being run as a subprocess.") args = parser.parse_args(sys.argv[2:]) if args.suite is not None: + # Running a specified suite from the base work directory run_tests(args.suite, quiet=args.quiet) elif os.path.exists('test_case.pickle'): + # Running a test case inside of its work directory run_tests(suite_name='test_case', quiet=args.quiet, is_test_case=True, steps_to_run=args.steps, steps_to_skip=args.skip_steps) elif os.path.exists('step.pickle'): + # Running a step inside of its work directory run_single_step(args.step_is_subprocess) else: pickles = glob.glob('*.pickle') if len(pickles) == 1: + # Running an unspecified suite from the base work directory suite = os.path.splitext(os.path.basename(pickles[0]))[0] run_tests(suite, quiet=args.quiet) elif len(pickles) == 0: @@ -246,6 +217,34 @@ def _update_steps_to_run(steps_to_run, steps_to_skip, config, steps): return steps_to_run +def _log_test_runtimes(stdout_logger, test_times, success_strs, suite_time, + failures): + """ + Log the runtimes for the test case(s) + """ + stdout_logger.info('Test Runtimes:') + for test_name, test_time in test_times.items(): + secs = round(test_time) + mins = secs // 60 + secs -= 60 * mins + stdout_logger.info(f'{mins:02d}:{secs:02d} ' + f'{success_strs[test_name]} {test_name}') + secs = round(suite_time) + mins = secs // 60 + secs -= 60 * mins + stdout_logger.info(f'Total runtime {mins:02d}:{secs:02d}') + + if failures == 0: + stdout_logger.info('PASS: All passed successfully!') + else: + if failures == 1: + message = '1 test' + else: + message = f'{failures} tests' + stdout_logger.error(f'FAIL: {message} failed, see above.') + sys.exit(1) + + def _print_to_stdout(test_case, message): """ Write out a message to stdout if we're not running a single step @@ -290,8 +289,7 @@ def _log_and_run_test(test_case, stdout_logger, test_logger, quiet, os.chdir(test_case.work_dir) - config = PolarisConfigParser() - config.add_from_file(test_case.config_filename) + config = setup_config(test_case.config_filename) test_case.config = config set_cores_per_node(test_case.config, available_resources['cores_per_node']) From 17ecf6a40a8ee71677f29840a59d424c2df0173d Mon Sep 17 00:00:00 2001 From: Althea Denlinger Date: Fri, 9 Jun 2023 12:08:33 -0600 Subject: [PATCH 2/2] Update API docs to reflect change in run --- docs/developers_guide/api.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/developers_guide/api.md b/docs/developers_guide/api.md index 48da9c2de..b9bb08e6b 100644 --- a/docs/developers_guide/api.md +++ b/docs/developers_guide/api.md @@ -68,13 +68,15 @@ ocean/api #### run ```{eval-rst} -.. currentmodule:: polaris.run.serial +.. currentmodule:: polaris.run .. autosummary:: :toctree: generated/ - run_tests - run_single_step + unpickle_suite + setup_config + serial.run_tests + serial.run_single_step ```