From 93a73e048d42f57a625f7afc7ad9ac55b8d7fe24 Mon Sep 17 00:00:00 2001 From: John Sharples <41682323+John-Sharples@users.noreply.github.com> Date: Thu, 19 Oct 2023 02:42:24 +1100 Subject: [PATCH] Feature 2253 command builder tests (#2378) --- .../command_builder/test_command_builder.py | 240 +++++++++++++++++- metplus/wrappers/command_builder.py | 2 +- 2 files changed, 239 insertions(+), 3 deletions(-) diff --git a/internal/tests/pytests/wrappers/command_builder/test_command_builder.py b/internal/tests/pytests/wrappers/command_builder/test_command_builder.py index 8822e165df..93a3bcb80a 100644 --- a/internal/tests/pytests/wrappers/command_builder/test_command_builder.py +++ b/internal/tests/pytests/wrappers/command_builder/test_command_builder.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 import pytest +from unittest import mock import os import datetime - +import metplus.wrappers.command_builder as cb_wrapper from metplus.wrappers.command_builder import CommandBuilder from metplus.util import ti_calculate, add_field_info_to_time_info @@ -1001,4 +1002,239 @@ def test_get_env_copy(metplus_config, shell, expected): actual = cb.get_env_copy({'MET_TMP_DIR', 'OMP_NUM_THREADS'}) assert expected in actual - \ No newline at end of file + + +def _in_last_err(msg, mock_logger): + last_msg = mock_logger.error.call_args_list[-1][0][0] + return msg in last_msg + + +@pytest.mark.wrapper +def test_get_command(metplus_config): + config = metplus_config + + cb = CommandBuilder(config) + cb.app_path = '/jabberwocky/' + cb.infiles = ['O','frabjous','day'] + cb.outfile = 'callooh' + cb.param = 'callay' + + with mock.patch.object(os.path, 'dirname', return_value='callooh'): + with mock.patch.object(cb_wrapper, 'mkdir_p'): + actual = cb.get_command() + assert actual == '/jabberwocky/ -v 2 O frabjous day callooh callay' + + with mock.patch.object(os.path, 'dirname', return_value=None): + actual = cb.get_command() + assert actual is None + assert _in_last_err('Must specify path to output file', cb.logger) + + cb.outfile = None + actual = cb.get_command() + assert actual is None + assert _in_last_err('No output filename specified', cb.logger) + + cb.infiles = None + actual = cb.get_command() + assert actual is None + assert _in_last_err('No input filenames specified', cb.logger) + + cb.app_path = None + actual = cb.get_command() + assert actual is None + assert _in_last_err('No app path specified.', cb.logger) + + +@pytest.mark.parametrize( + 'd_type, curly, values, expected', [ + ('fcst', + True, + [['0.2','1.0'],'A24','Z0','extra'], + ['{ name="A24"; level="Z0"; cat_thresh=[ 0.2,1.0 ]; extra; }'] + ), + ('fcst', + False, + [['20'],'apcp','3000',None], + ['\'name="apcp"; level="3000"; cat_thresh=[ 20 ];\''] + ), + ('obs', + True, + [['0.2','1.0'],'A24','Z0',None], + ['{ name="A24"; level="Z0"; cat_thresh=[ 0.2,1.0 ]; }'] + ), + ] +) +@pytest.mark.wrapper +def test_format_field_info(metplus_config, + d_type, + curly, + values, + expected): + var_keys = [ + f'{d_type}_thresh', + f'{d_type}_name', + f'{d_type}_level', + f'{d_type}_extra', + ] + var_info = dict(zip(var_keys, values)) + + cb = CommandBuilder(metplus_config) + actual = cb.format_field_info(var_info, d_type, curly) + assert actual == expected + + +@pytest.mark.parametrize( + 'log_metplus', [ + (True),(False) + ] +) +@pytest.mark.wrapper +def test_run_command_error(metplus_config, log_metplus): + config = metplus_config + if log_metplus: + config.set('config', 'LOG_METPLUS', '/fake/file.log') + else: + config.set('config', 'LOG_METPLUS', '') + + cb = CommandBuilder(metplus_config) + with mock.patch.object(cb.cmdrunner, 'run_cmd', return_value=('ERR',None)): + actual = cb.run_command('foo') + assert not actual + assert _in_last_err('Command returned a non-zero return code: foo', cb.logger) + + +@pytest.mark.wrapper +def test_find_input_files_ensemble(metplus_config): + config = metplus_config + cb = CommandBuilder(metplus_config) + + time_info = ti_calculate({ + 'valid': datetime.datetime.strptime("201802010000", '%Y%m%d%H%M'), + 'lead': 0, + }) + + # can't write file list + with mock.patch.object(cb, 'write_list_file', return_value=None): + with mock.patch.object(cb, 'find_model', return_value=['file']): + actual = cb.find_input_files_ensemble(time_info, False) + assert actual is False + assert _in_last_err('Could not write filelist file', cb.logger) + + # not _check_expected_ensembles + with mock.patch.object(cb, '_check_expected_ensembles', return_value=None): + with mock.patch.object(cb, 'find_model', return_value=['file']): + actual = cb.find_input_files_ensemble(time_info) + assert actual is False + + # no input files + with mock.patch.object(cb, 'find_model', return_value=[]): + actual = cb.find_input_files_ensemble(time_info) + assert actual is False + assert _in_last_err('Could not find any input files', cb.logger) + + # file list does/doesn't exist + cb.c_dict['FCST_INPUT_FILE_LIST'] = 'fcst_file_list' + actual = cb.find_input_files_ensemble(time_info) + assert actual is False + assert _in_last_err('Could not find file list file', cb.logger) + + with mock.patch.object(cb_wrapper.os.path, 'exists', return_value=True): + actual = cb.find_input_files_ensemble(time_info) + assert actual is True + assert cb.infiles[-1] == 'fcst_file_list' + + # ctrl file not found + cb.c_dict['CTRL_INPUT_TEMPLATE'] = 'ctrl_file' + with mock.patch.object(cb, 'find_data', return_value=None): + actual = cb.find_input_files_ensemble(time_info) + assert actual is False + + +@pytest.mark.wrapper +def test_errors_and_defaults(metplus_config): + """ + A test to check various functions produce expected log messages + and return values on error or unexpected input. + """ + config = metplus_config + app_name = 'command_builder' + config.set('config', f'{app_name.upper()}_OUTPUT_PREFIX', 'prefix') + cb = CommandBuilder(metplus_config) + cb.app_name = app_name + + # smoke test run_all_times + cb.run_all_times() + assert cb.isOK + + # test get_output_prefix without time_info + actual = cb.get_output_prefix(time_info=None) + assert actual == 'prefix' + + # test handle_climo_dict errors counted correctly + starting_errs = cb.errors + with mock.patch.object(cb_wrapper, 'handle_climo_dict', return_value=False): + for x in range(2): + cb.handle_climo_dict() + assert starting_errs + 2 == cb.errors + + # test missing FLAGS returns none + actual = cb.handle_flags('foo') + assert actual is None + + # test get_env_var_value empty and list + actual = cb.get_env_var_value('foo',{'foo':''},'list') + assert actual == '[]' + + # test add_met_config_dict not OK + assert cb.isOK + with mock.patch.object(cb_wrapper, 'add_met_config_dict', return_value=False): + actual = cb.add_met_config_dict('foo', 'bar') + assert actual is False + assert not cb.isOK + + # test build when no cmd + with mock.patch.object(cb, 'get_command', return_value=None): + actual = cb.build() + assert actual == False + assert _in_last_err('Could not generate command', cb.logger) + + # test python embedding error + with mock.patch.object(cb_wrapper, 'is_python_script', return_value=True): + actual = cb.check_for_python_embedding('FCST',{'fcst_name':'pyEmbed'}) + assert actual == None + assert _in_last_err('must be set to a valid Python Embedding type', cb.logger) + + cb.c_dict['FCST_INPUT_DATATYPE'] = 'PYTHON_XARRAY' + with mock.patch.object(cb_wrapper, 'is_python_script', return_value=True): + actual = cb.check_for_python_embedding('FCST',{'fcst_name':'pyEmbed'}) + assert actual == 'python_embedding' + + # test field_info not set + cb.c_dict['CURRENT_VAR_INFO'] = None + actual = cb.set_current_field_config() + assert actual is None + + # test check_gempaktocf + cb.isOK = True + cb.check_gempaktocf(False) + assert cb.isOK == False + assert _in_last_err('[exe] GEMPAKTOCF_JAR was not set in configuration file.', cb.logger) + + # test expected ensemble mismatch + cb.c_dict['N_MEMBERS'] = 1 + actual = cb._check_expected_ensembles(['file1', 'file2']) + assert actual is False + assert _in_last_err('Found more files than expected!', cb.logger) + + # format field info + with mock.patch.object(cb_wrapper, 'format_field_info', return_value='bar'): + actual = cb.format_field_info({},'foo') + assert actual is None + assert _in_last_err('bar', cb.logger) + + # check get_field_info + with mock.patch.object(cb_wrapper, 'get_field_info', return_value='bar'): + actual = cb.get_field_info({},'foo') + assert actual is None + assert _in_last_err('bar', cb.logger) + diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 058a9e93f7..3a70fe99ea 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -1075,7 +1075,7 @@ def check_for_gempak(self): def check_gempaktocf(self, gempaktocf_jar): if not gempaktocf_jar: - self.log_error("[exe] GEMPAKTOCF_JAR was not set if configuration file. " + self.log_error("[exe] GEMPAKTOCF_JAR was not set in configuration file. " "This is required to process Gempak data.") self.logger.info("Refer to the GempakToCF use case documentation for information " "on how to obtain the tool: parm/use_cases/met_tool_wrapper/GempakToCF/GempakToCF.py")