From 655289dee767a0414071e059443112b4dbfd287e Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Mon, 3 Feb 2020 18:03:11 +0100 Subject: [PATCH 1/6] Fixed tests for _recipe.py --- esmvalcore/_recipe_checks.py | 4 ++-- tests/integration/test_recipe.py | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/esmvalcore/_recipe_checks.py b/esmvalcore/_recipe_checks.py index 5c4ecc983f..b4c565e2bc 100644 --- a/esmvalcore/_recipe_checks.py +++ b/esmvalcore/_recipe_checks.py @@ -160,8 +160,8 @@ def check_for_temporal_preprocs(profile): preproc for preproc in profile if preproc in temporal_preprocs] if temp_preprocs: raise RecipeError( - "Time coordinate preprocessor step {} not permitted on fx vars \ - please remove them from recipe.".format(", ".join(temp_preprocs))) + "Time coordinate preprocessor step(s) {} not permitted on fx " + "vars, please remove them from recipe".format(temp_preprocs)) def extract_shape(settings): diff --git a/tests/integration/test_recipe.py b/tests/integration/test_recipe.py index 10eff20596..4d9fc77e38 100644 --- a/tests/integration/test_recipe.py +++ b/tests/integration/test_recipe.py @@ -302,12 +302,11 @@ def test_fx_preproc_error(tmp_path, patched_datafinder, config_user): - dataset: MPI-ESM-LR scripts: null """) - rec_err = "Time coordinate preprocessor step extract_season \ - not permitted on fx vars \ - please remove them from recipe." + msg = ("Time coordinate preprocessor step(s) ['extract_season'] not " + "permitted on fx vars, please remove them from recipe") with pytest.raises(Exception) as rec_err_exp: get_recipe(tmp_path, content, config_user) - assert rec_err == rec_err_exp + assert str(rec_err_exp.value) == msg def test_default_preprocessor(tmp_path, patched_datafinder, config_user): @@ -1476,8 +1475,8 @@ def test_extract_shape_raises(tmp_path, patched_datafinder, config_user, """) with pytest.raises(RecipeError) as exc: get_recipe(tmp_path, content, config_user) - assert 'extract_shape' in exc.value - assert invalid_arg in exc.value + assert 'extract_shape' in str(exc.value) + assert invalid_arg in str(exc.value) def test_weighting_landsea_fraction(tmp_path, patched_datafinder, config_user): @@ -2040,9 +2039,10 @@ def test_wrong_project(tmp_path, patched_datafinder, config_user): - {dataset: CanESM2} scripts: null """) + msg = "Project 'CMIP7' not in config-developer.yml" with pytest.raises(ValueError) as wrong_proj: get_recipe(tmp_path, content, config_user) - assert wrong_proj == "Project CMIP7 not in config-developer" + assert str(wrong_proj.value) == msg def test_invalid_fx_var_cmip6(tmp_path, patched_datafinder, config_user): @@ -2072,11 +2072,11 @@ def test_invalid_fx_var_cmip6(tmp_path, patched_datafinder, config_user): - {dataset: CanESM5} scripts: null """) - msg = ("Requested fx variable 'wrong_fx_variable' for CMIP6 not " - "available in any 'fx'-related CMOR table") + msg = ("Requested fx variable 'wrong_fx_variable' not available in any " + "'fx'-related CMOR table") with pytest.raises(RecipeError) as rec_err_exp: get_recipe(tmp_path, content, config_user) - assert msg in rec_err_exp + assert msg in str(rec_err_exp.value) def test_fx_var_invalid_project(tmp_path, patched_datafinder, config_user): @@ -2103,7 +2103,8 @@ def test_fx_var_invalid_project(tmp_path, patched_datafinder, config_user): - {dataset: CanESM5} scripts: null """) - msg = 'Project EMAC not supported with fx variables' + msg = ("Unable to load CMOR table 'EMAC' for variable 'areacella' with " + "mip 'Amon'") with pytest.raises(RecipeError) as rec_err_exp: get_recipe(tmp_path, content, config_user) - assert msg in rec_err_exp + assert str(rec_err_exp.value) == msg From a7014dfed0149f0b0e31eb383961a8300463ced2 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Mon, 3 Feb 2020 18:03:36 +0100 Subject: [PATCH 2/6] Added test for check of data availability --- tests/integration/test_recipe_checks.py | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/integration/test_recipe_checks.py diff --git a/tests/integration/test_recipe_checks.py b/tests/integration/test_recipe_checks.py new file mode 100644 index 0000000000..82309809e3 --- /dev/null +++ b/tests/integration/test_recipe_checks.py @@ -0,0 +1,97 @@ +"""Integration tests for :mod:`esmvalcore._recipe_checks`.""" +from unittest import mock + +import pytest + +import esmvalcore._recipe_checks as check + + +ERR_ALL = 'Looked for files matching%s' +ERR_D = ('Looked for files in %s, but did not find any file pattern to match ' + 'against') +ERR_F = ('Looked for files matching %s, but did not find any existing input ' + 'directory') +ERR_RANGE = 'No input data available for years {} in files {}' +VAR = { + 'filename': 'a/c.nc', + 'frequency': 'mon', + 'short_name': 'tas', + 'start_year': 2020, + 'end_year': 2025, +} +FX_VAR = { + 'filename': 'a/b.nc', + 'frequency': 'fx', + 'short_name': 'areacella', +} +FILES = [ + 'a/b/c_20200101-20201231', + 'a/b/c_20210101-20211231', + 'a/b/c_20220101-20221231', + 'a/b/c_20230101-20231231', + 'a/b/c_20240101-20241231', + 'a/b/c_20250101-20251231', +] + + +DATA_AVAILABILITY_DATA = [ + (FILES, VAR, None), + (FILES, FX_VAR, None), + (FILES[:-1], VAR, ERR_RANGE.format('2025', FILES[:-1])), + (FILES[:-2], VAR, ERR_RANGE.format('2024, 2025', FILES[:-2])), + ([FILES[1]] + [FILES[3]], VAR, ERR_RANGE.format('2024, 2025, 2020, 2022', + [FILES[1]] + [FILES[3]])), + +] + + +@pytest.mark.parametrize('input_files,var,error', DATA_AVAILABILITY_DATA) +@mock.patch('esmvalcore._recipe_checks.logger', autospec=True) +def test_data_availability_data(mock_logger, input_files, var, error): + """Test check for data.""" + if error is None: + check.data_availability(input_files, var, None, None) + mock_logger.error.assert_not_called() + else: + with pytest.raises(check.RecipeError) as rec_err: + check.data_availability(input_files, var, None, None) + assert str(rec_err.value) == error + + +DATA_AVAILABILITY_NO_DATA = [ + ([], [], None), + ([], None, None), + (None, [], None), + (None, None, None), + (['dir1'], [], (ERR_D, ['dir1'])), + (['dir1', 'dir2'], [], (ERR_D, ['dir1', 'dir2'])), + (['dir1'], None, (ERR_D, ['dir1'])), + (['dir1', 'dir2'], None, (ERR_D, ['dir1', 'dir2'])), + ([], ['a*.nc'], (ERR_F, ['a*.nc'])), + ([], ['a*.nc', 'b*.nc'], (ERR_F, ['a*.nc', 'b*.nc'])), + (None, ['a*.nc'], (ERR_F, ['a*.nc'])), + (None, ['a*.nc', 'b*.nc'], (ERR_F, ['a*.nc', 'b*.nc'])), + (['1'], ['a'], (ERR_ALL, ': 1/a')), + (['1'], ['a', 'b'], (ERR_ALL, '\n1/a\n1/b')), + (['1', '2'], ['a'], (ERR_ALL, '\n1/a\n2/a')), + (['1', '2'], ['a', 'b'], (ERR_ALL, '\n1/a\n1/b\n2/a\n2/b')), +] + + +@pytest.mark.parametrize('dirnames,filenames,error', DATA_AVAILABILITY_NO_DATA) +@mock.patch('esmvalcore._recipe_checks.logger', autospec=True) +def test_data_availability_no_data(mock_logger, dirnames, filenames, error): + """Test check for data.""" + error_first = ('No input files found for variable %s', VAR) + error_last = ("Set 'log_level' to 'debug' to get more information", ) + with pytest.raises(check.RecipeError) as rec_err: + check.data_availability([], VAR, dirnames, filenames) + assert str(rec_err.value) == 'Missing data' + if error is None: + assert mock_logger.error.call_count == 2 + errors = [error_first, error_last] + else: + assert mock_logger.error.call_count == 3 + errors = [error_first, error, error_last] + calls = [mock.call(*e) for e in errors] + assert mock_logger.error.call_args_list == calls From 5a09a1b236f70ea56dbb4b5f88de1525d338d2a8 Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Tue, 4 Feb 2020 12:52:28 +0100 Subject: [PATCH 3/6] Expanded tests for data availability (regarding filename) --- esmvalcore/_recipe_checks.py | 1 + tests/integration/test_recipe_checks.py | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/esmvalcore/_recipe_checks.py b/esmvalcore/_recipe_checks.py index b4c565e2bc..43f67df7f7 100644 --- a/esmvalcore/_recipe_checks.py +++ b/esmvalcore/_recipe_checks.py @@ -93,6 +93,7 @@ def variable(var, required_keys): def data_availability(input_files, var, dirnames, filenames): """Check if the required input data is available.""" + var = dict(var) if not input_files: var.pop('filename', None) logger.error("No input files found for variable %s", var) diff --git a/tests/integration/test_recipe_checks.py b/tests/integration/test_recipe_checks.py index 82309809e3..dda838df14 100644 --- a/tests/integration/test_recipe_checks.py +++ b/tests/integration/test_recipe_checks.py @@ -35,12 +35,12 @@ DATA_AVAILABILITY_DATA = [ - (FILES, VAR, None), - (FILES, FX_VAR, None), - (FILES[:-1], VAR, ERR_RANGE.format('2025', FILES[:-1])), - (FILES[:-2], VAR, ERR_RANGE.format('2024, 2025', FILES[:-2])), - ([FILES[1]] + [FILES[3]], VAR, ERR_RANGE.format('2024, 2025, 2020, 2022', - [FILES[1]] + [FILES[3]])), + (FILES, dict(VAR), None), + (FILES, dict(FX_VAR), None), + (FILES[:-1], dict(VAR), ERR_RANGE.format('2025', FILES[:-1])), + (FILES[:-2], dict(VAR), ERR_RANGE.format('2024, 2025', FILES[:-2])), + ([FILES[1]] + [FILES[3]], dict(VAR), ERR_RANGE.format( + '2024, 2025, 2020, 2022', [FILES[1]] + [FILES[3]])), ] @@ -49,6 +49,7 @@ @mock.patch('esmvalcore._recipe_checks.logger', autospec=True) def test_data_availability_data(mock_logger, input_files, var, error): """Test check for data.""" + saved_var = dict(var) if error is None: check.data_availability(input_files, var, None, None) mock_logger.error.assert_not_called() @@ -56,6 +57,7 @@ def test_data_availability_data(mock_logger, input_files, var, error): with pytest.raises(check.RecipeError) as rec_err: check.data_availability(input_files, var, None, None) assert str(rec_err.value) == error + assert var == saved_var DATA_AVAILABILITY_NO_DATA = [ @@ -82,7 +84,13 @@ def test_data_availability_data(mock_logger, input_files, var, error): @mock.patch('esmvalcore._recipe_checks.logger', autospec=True) def test_data_availability_no_data(mock_logger, dirnames, filenames, error): """Test check for data.""" - error_first = ('No input files found for variable %s', VAR) + var = { + 'frequency': 'mon', + 'short_name': 'tas', + 'start_year': 2020, + 'end_year': 2025, + } + error_first = ('No input files found for variable %s', var) error_last = ("Set 'log_level' to 'debug' to get more information", ) with pytest.raises(check.RecipeError) as rec_err: check.data_availability([], VAR, dirnames, filenames) From 4002b3ff0ab0137c085462744325922a95a6b6af Mon Sep 17 00:00:00 2001 From: Manuel Schlund Date: Tue, 4 Feb 2020 12:55:22 +0100 Subject: [PATCH 4/6] Further expansions --- tests/integration/test_recipe_checks.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_recipe_checks.py b/tests/integration/test_recipe_checks.py index dda838df14..dc984afe5b 100644 --- a/tests/integration/test_recipe_checks.py +++ b/tests/integration/test_recipe_checks.py @@ -48,7 +48,7 @@ @pytest.mark.parametrize('input_files,var,error', DATA_AVAILABILITY_DATA) @mock.patch('esmvalcore._recipe_checks.logger', autospec=True) def test_data_availability_data(mock_logger, input_files, var, error): - """Test check for data.""" + """Test check for data when data is present.""" saved_var = dict(var) if error is None: check.data_availability(input_files, var, None, None) @@ -83,17 +83,18 @@ def test_data_availability_data(mock_logger, input_files, var, error): @pytest.mark.parametrize('dirnames,filenames,error', DATA_AVAILABILITY_NO_DATA) @mock.patch('esmvalcore._recipe_checks.logger', autospec=True) def test_data_availability_no_data(mock_logger, dirnames, filenames, error): - """Test check for data.""" - var = { + """Test check for data when no data is present.""" + var = dict(VAR) + var_no_filename = { 'frequency': 'mon', 'short_name': 'tas', 'start_year': 2020, 'end_year': 2025, } - error_first = ('No input files found for variable %s', var) + error_first = ('No input files found for variable %s', var_no_filename) error_last = ("Set 'log_level' to 'debug' to get more information", ) with pytest.raises(check.RecipeError) as rec_err: - check.data_availability([], VAR, dirnames, filenames) + check.data_availability([], var, dirnames, filenames) assert str(rec_err.value) == 'Missing data' if error is None: assert mock_logger.error.call_count == 2 @@ -103,3 +104,4 @@ def test_data_availability_no_data(mock_logger, dirnames, filenames, error): errors = [error_first, error, error_last] calls = [mock.call(*e) for e in errors] assert mock_logger.error.call_args_list == calls + assert var == VAR From ad15244b392ef7d260e5711d7f0c1104e993db2f Mon Sep 17 00:00:00 2001 From: Bouwe Andela Date: Thu, 6 Feb 2020 18:15:26 +0100 Subject: [PATCH 5/6] Improve extract_shape RecipeError test --- esmvalcore/_recipe_checks.py | 1 + tests/integration/test_recipe.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/esmvalcore/_recipe_checks.py b/esmvalcore/_recipe_checks.py index 43f67df7f7..b6df1230d9 100644 --- a/esmvalcore/_recipe_checks.py +++ b/esmvalcore/_recipe_checks.py @@ -175,6 +175,7 @@ def extract_shape(settings): valid = { 'method': {'contains', 'representative'}, 'crop': {True, False}, + 'decomposed': {True, False}, } for key in valid: value = settings.get(key) diff --git a/tests/integration/test_recipe.py b/tests/integration/test_recipe.py index bc28e29cba..01945ea7ae 100644 --- a/tests/integration/test_recipe.py +++ b/tests/integration/test_recipe.py @@ -1448,14 +1448,22 @@ def test_extract_shape(tmp_path, patched_datafinder, config_user): assert product.settings['extract_shape']['shapefile'] == str(shapefile) -@pytest.mark.parametrize('invalid_arg', ['crop', 'shapefile', 'method']) +@pytest.mark.parametrize('invalid_arg', + ['shapefile', 'method', 'crop', 'decomposed']) def test_extract_shape_raises(tmp_path, patched_datafinder, config_user, invalid_arg): + # Create shapefile + shapefile = config_user['auxiliary_data_dir'] / Path('test.shp') + shapefile.parent.mkdir(parents=True, exist_ok=True) + shapefile.touch() + content = dedent(f""" preprocessors: test: extract_shape: - {invalid_arg}: x + crop: true + method: contains + shapefile: test.shp diagnostics: test: @@ -1473,6 +1481,12 @@ def test_extract_shape_raises(tmp_path, patched_datafinder, config_user, dataset: GFDL-CM3 scripts: null """) + + # Add invalid argument + recipe = yaml.safe_load(content) + recipe['preprocessors']['test']['extract_shape'][invalid_arg] = 'x' + content = yaml.safe_dump(recipe) + with pytest.raises(RecipeError) as exc: get_recipe(tmp_path, content, config_user) assert 'extract_shape' in str(exc.value) From 4ddc1ad79fd8753999e9e726534ef9175677c7b8 Mon Sep 17 00:00:00 2001 From: Bouwe Andela Date: Wed, 12 Feb 2020 11:13:29 +0100 Subject: [PATCH 6/6] Fix tests --- .../cmor/_fixes/cmip5/test_ec_earth.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/integration/cmor/_fixes/cmip5/test_ec_earth.py b/tests/integration/cmor/_fixes/cmip5/test_ec_earth.py index e10df4475a..1b5ee52e63 100644 --- a/tests/integration/cmor/_fixes/cmip5/test_ec_earth.py +++ b/tests/integration/cmor/_fixes/cmip5/test_ec_earth.py @@ -6,7 +6,7 @@ from iris.coords import DimCoord from iris.cube import Cube, CubeList -from esmvalcore.cmor._fixes.cmip5.ec_earth import Sftlf, Sic, Tas, Areacello +from esmvalcore.cmor._fixes.cmip5.ec_earth import Areacello, Sftlf, Sic, Tas from esmvalcore.cmor.fix import Fix @@ -108,7 +108,6 @@ def test_tas_fix_metadata(self): class TestAreacello(unittest.TestCase): """Test areacello fixes.""" - def setUp(self): """Prepare tests.""" @@ -133,19 +132,19 @@ def setUp(self): np.ones((2, 2)), var_name='areacello', long_name='Areas of grid cell', - ), - latitude, - longitude - ]) + ), latitude, longitude + ]) - self.fix = Areacello() + self.fix = Areacello(None) def test_get(self): """Test fix get""" self.assertListEqual( - Fix.get_fixes('CMIP5', 'EC-EARTH', 'areacello'), [Areacello()]) + Fix.get_fixes('CMIP5', 'EC-EARTH', 'Omon', 'areacello'), + [Areacello(None)], + ) - def test_tas_fix_metadata(self): + def test_areacello_fix_metadata(self): """Test metadata fix.""" out_cube = self.fix.fix_metadata(self.cubes)