Skip to content

Commit

Permalink
Feature 2253 command builder tests (#2378)
Browse files Browse the repository at this point in the history
  • Loading branch information
John-Sharples authored Oct 18, 2023
1 parent 00bb47b commit 93a73e0
Show file tree
Hide file tree
Showing 2 changed files with 239 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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



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)

2 changes: 1 addition & 1 deletion metplus/wrappers/command_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 93a73e0

Please sign in to comment.