From 113660af777740338b0c58264397afdee738d52a Mon Sep 17 00:00:00 2001
From: John Sharples <john.sharples@bom.gov.au>
Date: Wed, 4 Oct 2023 05:04:03 +0000
Subject: [PATCH 1/5] feature 2253: command_builder tests

---
 .../command_builder/test_command_builder.py   | 201 +++++++++++++++++-
 metplus/wrappers/command_builder.py           |   2 +-
 2 files changed, 200 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 8822e165d..37ad13a48 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,200 @@ 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
+    #config.set('user_env_vars','CMD_BUILD_USER_TEST', )
+
+    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.wrapper
+def test_format_field_info_error(metplus_config):
+    cb = CommandBuilder(metplus_config)
+    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)
+
+ 
+@pytest.mark.wrapper
+def test_get_field_info_error(metplus_config):
+    cb = CommandBuilder(metplus_config)
+    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)
+
+ 
+@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_errors_and_defaults(metplus_config):
+    """A test to check various errors and default return"""
+    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)
+    
diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py
index 058a9e93f..3a70fe99e 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")

From 7ba0ab415616e4791e333d07fc9dc7b1c241db44 Mon Sep 17 00:00:00 2001
From: John Sharples <john.sharples@bom.gov.au>
Date: Thu, 5 Oct 2023 04:00:05 +0000
Subject: [PATCH 2/5] feature 2253: command_builder tests

---
 .../command_builder/test_command_builder.py   | 46 +++++++++++++++++++
 1 file changed, 46 insertions(+)

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 37ad13a48..cef47053e 100644
--- a/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
+++ b/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
@@ -1126,6 +1126,52 @@ def test_run_command_error(metplus_config, log_metplus):
 
 
  
+@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 errors and default return"""

From 70164bf60069cf03e7083fc3fb4a5b723956b54a Mon Sep 17 00:00:00 2001
From: John Sharples <john.sharples@bom.gov.au>
Date: Thu, 5 Oct 2023 04:08:16 +0000
Subject: [PATCH 3/5] feature 2253: tidy up

---
 .../command_builder/test_command_builder.py         | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 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 cef47053e..68427fa9d 100644
--- a/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
+++ b/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
@@ -1008,10 +1008,10 @@ 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
-    #config.set('user_env_vars','CMD_BUILD_USER_TEST', )
 
     cb = CommandBuilder(config)
     cb.app_path = '/jabberwocky/'
@@ -1037,8 +1037,6 @@ def test_get_command(metplus_config):
     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
@@ -1090,7 +1088,6 @@ def test_format_field_info_error(metplus_config):
     cb = CommandBuilder(metplus_config)
     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)
 
@@ -1100,7 +1097,6 @@ def test_get_field_info_error(metplus_config):
     cb = CommandBuilder(metplus_config)
     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)
 
@@ -1124,7 +1120,6 @@ def test_run_command_error(metplus_config, log_metplus):
     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):
@@ -1172,9 +1167,13 @@ def test_find_input_files_ensemble(metplus_config):
         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 errors and default return"""
+    """
+    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')

From afecc6eddb463a9a27db4a84c30b64474a410042 Mon Sep 17 00:00:00 2001
From: John Sharples <john.sharples@bom.gov.au>
Date: Thu, 5 Oct 2023 05:39:18 +0000
Subject: [PATCH 4/5] feature 2253: rearrange some tests

---
 .../command_builder/test_command_builder.py   | 30 ++++++++-----------
 1 file changed, 13 insertions(+), 17 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 68427fa9d..493d86d00 100644
--- a/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
+++ b/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
@@ -1083,22 +1083,6 @@ def test_format_field_info(metplus_config,
     assert actual == expected
 
 
-@pytest.mark.wrapper
-def test_format_field_info_error(metplus_config):
-    cb = CommandBuilder(metplus_config)
-    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)
-
- 
-@pytest.mark.wrapper
-def test_get_field_info_error(metplus_config):
-    cb = CommandBuilder(metplus_config)
-    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)
 
  
 @pytest.mark.parametrize(
@@ -1243,4 +1227,16 @@ def test_errors_and_defaults(metplus_config):
     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)
+

From 9d42c0e03ca1a8b96c48b896b27a549fb95f427e Mon Sep 17 00:00:00 2001
From: John Sharples <john.sharples@bom.gov.au>
Date: Thu, 5 Oct 2023 06:15:52 +0000
Subject: [PATCH 5/5] feature 2253: cleanup whitespace

---
 .../pytests/wrappers/command_builder/test_command_builder.py    | 2 --
 1 file changed, 2 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 493d86d00..93a3bcb80 100644
--- a/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
+++ b/internal/tests/pytests/wrappers/command_builder/test_command_builder.py
@@ -1082,8 +1082,6 @@ def test_format_field_info(metplus_config,
     actual = cb.format_field_info(var_info, d_type, curly)
     assert actual == expected
 
-
-
  
 @pytest.mark.parametrize(
     'log_metplus', [