From f4087259df1f9906f4c66878ab7182db7ff64307 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 12:03:16 +0200 Subject: [PATCH 01/30] Added more tests --- test/unit/general/test_attribute.py | 21 +++++++++++++ test/unit/general/test_module.py | 30 +++++++++++++++++++ .../general/test_verticalmeanmetersiris.py | 3 +- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/test/unit/general/test_attribute.py b/test/unit/general/test_attribute.py index 3e2cce9b..8473f8c5 100644 --- a/test/unit/general/test_attribute.py +++ b/test/unit/general/test_attribute.py @@ -1,5 +1,9 @@ # coding=utf-8 from unittest import TestCase +import os +from tempfile import mktemp + +import dummydata from earthdiagnostics.diagnostic import DiagnosticVariableOption from earthdiagnostics.box import Box @@ -21,6 +25,11 @@ class TestAttribute(TestCase): self.box = Box() self.box.min_depth = 0 self.box.max_depth = 100 + self.var_file = mktemp('.nc') + + def tearDown(self): + if os.path.exists(self.var_file): + os.remove(self.var_file) def fake_parse(self, value): return value @@ -53,3 +62,15 @@ class TestAttribute(TestCase): self.assertEqual(str(mixed), 'Write attributte output Startdate: 20010101 Member: 0 Chunk: 0 Variable: atmos:var ' 'Attributte: att:value Grid: grid') + + def test_compute(self): + dummydata.model3.Model3(oname=self.var_file, var='ta', start_year=2000, stop_year=2000, method='constant', + constant=1) + + diag = Attribute(self.data_manager, '20010101', 0, 0, ModelingRealms.atmos, 'ta', 'grid', 'att', 'value') + + diag.variable_file = Mock() + diag.variable_file.local_file = self.var_file + diag.corrected = Mock() + diag.compute() + diag.corrected.set_local_file.assert_called_once() diff --git a/test/unit/general/test_module.py b/test/unit/general/test_module.py index 39549556..fe8fc4a0 100644 --- a/test/unit/general/test_module.py +++ b/test/unit/general/test_module.py @@ -1,5 +1,9 @@ # coding=utf-8 from unittest import TestCase +import os +from tempfile import mktemp + +import dummydata from earthdiagnostics.diagnostic import DiagnosticVariableOption from earthdiagnostics.box import Box @@ -21,6 +25,14 @@ class TestModule(TestCase): self.box = Box() self.box.min_depth = 0 self.box.max_depth = 100 + self.varu_file = mktemp('.nc') + self.varv_file = mktemp('.nc') + + def tearDown(self): + if os.path.exists(self.varu_file): + os.remove(self.varu_file) + if os.path.exists(self.varv_file): + os.remove(self.varv_file) def fake_parse(self, value): return value @@ -53,3 +65,21 @@ class TestModule(TestCase): self.assertEqual(str(mixed), 'Calculate module Startdate: 20010101 Member: 0 Chunk: 0 ' 'Variables: atmos:varu,varv,varmodule Grid: grid') + + def test_compute(self): + dummydata.model2.Model2(oname=self.varu_file, var='ua', start_year=2000, stop_year=2000, method='constant', + constant=1) + + dummydata.model2.Model2(oname=self.varv_file, var='va', start_year=2000, stop_year=2000, method='constant', + constant=1) + + diag = Module(self.data_manager, '20010101', 0, 0, ModelingRealms.atmos, 'ua', 'va', 'varmodule', 'grid') + + diag.component_u_file = Mock() + diag.component_u_file.local_file = self.varu_file + + diag.component_v_file = Mock() + diag.component_v_file.local_file = self.varv_file + diag.module_file = Mock() + diag.compute() + diag.module_file.set_local_file.assert_called_once() diff --git a/test/unit/general/test_verticalmeanmetersiris.py b/test/unit/general/test_verticalmeanmetersiris.py index 99dd5766..136539fc 100644 --- a/test/unit/general/test_verticalmeanmetersiris.py +++ b/test/unit/general/test_verticalmeanmetersiris.py @@ -16,7 +16,6 @@ from earthdiagnostics.modelingrealm import ModelingRealms class TestVerticalMeanMetersIris(TestCase): def setUp(self): - self.var_file = mktemp('.nc') self.data_manager = Mock() self.diags = Mock() @@ -27,6 +26,7 @@ class TestVerticalMeanMetersIris(TestCase): self.box = Box() self.box.min_depth = 0 self.box.max_depth = 100 + self.var_file = mktemp('.nc') def tearDown(self): if os.path.exists(self.var_file): @@ -97,3 +97,4 @@ class TestVerticalMeanMetersIris(TestCase): diag.variable_file.local_file = self.var_file diag.results = Mock() diag.compute() + diag.results.set_local_file.assert_called_once() -- GitLab From 7fc129930c2095227ae33f820c890e044d0cc687 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 12:07:36 +0200 Subject: [PATCH 02/30] Added more tests --- test/unit/general/test_rewrite.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/test/unit/general/test_rewrite.py b/test/unit/general/test_rewrite.py index ed0cb80f..0f4a5e01 100644 --- a/test/unit/general/test_rewrite.py +++ b/test/unit/general/test_rewrite.py @@ -1,5 +1,10 @@ # coding=utf-8 from unittest import TestCase +import os +from tempfile import mktemp + +import dummydata + from earthdiagnostics.diagnostic import DiagnosticVariableOption from earthdiagnostics.box import Box @@ -22,7 +27,12 @@ class TestRewrite(TestCase): self.box.min_depth = 0 self.box.max_depth = 100 - self.mixed = Rewrite(self.data_manager, '20000101', 1, 1, ModelingRealms.atmos, 'var', 'grid') + self.diag = Rewrite(self.data_manager, '20000101', 1, 1, ModelingRealms.atmos, 'ta', 'grid') + self.var_file = mktemp('.nc') + + def tearDown(self): + if os.path.exists(self.var_file): + os.remove(self.var_file) def fake_parse(self, value): return value @@ -47,5 +57,15 @@ class TestRewrite(TestCase): Rewrite.generate_jobs(self.diags, ['diagnostic', '0', '0', '0', '0', '0', '0', '0']) def test_str(self): - self.assertEqual(str(self.mixed), - 'Rewrite output Startdate: 20000101 Member: 1 Chunk: 1 Variable: atmos:var Grid: grid') + self.assertEqual(str(self.diag), + 'Rewrite output Startdate: 20000101 Member: 1 Chunk: 1 Variable: atmos:ta Grid: grid') + + def test_compute(self): + dummydata.model2.Model2(oname=self.var_file, var='ta', start_year=2000, stop_year=2000, method='constant', + constant=1) + + self.diag.variable_file = Mock() + self.diag.variable_file.local_file = self.var_file + self.diag.corrected = Mock() + self.diag.compute() + self.diag.corrected.set_local_file.assert_called_once() -- GitLab From 7ecb354c4199f30f7eafb73478ab8df92d89f8d7 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 12:28:23 +0200 Subject: [PATCH 03/30] Added tests for rewrite and scale --- test/unit/general/test_rewrite.py | 1 - test/unit/general/test_scale.py | 54 +++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/test/unit/general/test_rewrite.py b/test/unit/general/test_rewrite.py index 0f4a5e01..143eb174 100644 --- a/test/unit/general/test_rewrite.py +++ b/test/unit/general/test_rewrite.py @@ -5,7 +5,6 @@ from tempfile import mktemp import dummydata - from earthdiagnostics.diagnostic import DiagnosticVariableOption from earthdiagnostics.box import Box from earthdiagnostics.general.rewrite import Rewrite diff --git a/test/unit/general/test_scale.py b/test/unit/general/test_scale.py index 92321f39..84609bcc 100644 --- a/test/unit/general/test_scale.py +++ b/test/unit/general/test_scale.py @@ -2,6 +2,11 @@ from unittest import TestCase from mock import Mock, patch +import os +from tempfile import mktemp + +import dummydata +import iris from earthdiagnostics.box import Box from earthdiagnostics.diagnostic import DiagnosticVariableOption, DiagnosticOptionError @@ -24,6 +29,12 @@ class TestScale(TestCase): self.box.min_depth = 0 self.box.max_depth = 100 + self.var_file = mktemp('.nc') + + def tearDown(self): + if os.path.exists(self.var_file): + os.remove(self.var_file) + def fake_parse(self, value): return value @@ -73,8 +84,47 @@ class TestScale(TestCase): 'extra']) def test_str(self): - mixed = Scale(self.data_manager, '20010101', 0, 0, 0, 0, ModelingRealms.atmos, 'var', 'grid', 0, 100, + scale = Scale(self.data_manager, '20010101', 0, 0, 0, 0, ModelingRealms.atmos, 'var', 'grid', 0, 100, Frequencies.three_hourly, False) - self.assertEqual(str(mixed), + self.assertEqual(str(scale), 'Scale output Startdate: 20010101 Member: 0 Chunk: 0 Scale value: 0 Offset: 0 ' 'Variable: atmos:var Frequency: 3hr Apply mask: False') + + def test_compute_factor(self): + + scale = Scale(self.data_manager, '20010101', 0, 0, 10, 0, ModelingRealms.atmos, 'ta', 'grid', 1, 100, + Frequencies.three_hourly, False) + cube = self._get_data_and_test(scale) + self.assertEqual(cube.data.max(), 10) + + def test_compute_offset(self): + scale = Scale(self.data_manager, '20010101', 0, 0, 1, 10, ModelingRealms.atmos, 'ta', 'grid', 1, 100, + Frequencies.three_hourly, False) + cube = self._get_data_and_test(scale) + self.assertEqual(cube.data.max(), 11) + + def test_compute_too_low(self): + scale = Scale(self.data_manager, '20010101', 0, 0, 0, 10, ModelingRealms.atmos, 'ta', 'grid', 10, 100, + Frequencies.three_hourly, False) + cube = self._get_data_and_test(scale) + self.assertEqual(cube.data.max(), 1) + + def test_compute_too_high(self): + scale = Scale(self.data_manager, '20010101', 0, 0, 0, 10, ModelingRealms.atmos, 'ta', 'grid', 0, 0.5, + Frequencies.three_hourly, False) + cube = self._get_data_and_test(scale) + self.assertEqual(cube.data.max(), 1) + + def _get_data_and_test(self, scale): + dummydata.model2.Model2(oname=self.var_file, var='ta', start_year=2000, stop_year=2000, method='constant', + constant=1) + scale.variable_file = Mock() + scale.variable_file.local_file = self.var_file + scale.corrected = Mock() + scale.compute() + scale.corrected.set_local_file.assert_called_once() + cube = iris.load_cube(scale.corrected.set_local_file.call_args[0][0]) + self.assertEqual(cube.data.max(), cube.data.min()) + return cube + + -- GitLab From e9a32d62d7a9a274a73283e0dad2a160c9c04e27 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 12:30:38 +0200 Subject: [PATCH 04/30] Fixed lint --- test/unit/general/test_scale.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/general/test_scale.py b/test/unit/general/test_scale.py index 84609bcc..0e64ab2e 100644 --- a/test/unit/general/test_scale.py +++ b/test/unit/general/test_scale.py @@ -126,5 +126,3 @@ class TestScale(TestCase): cube = iris.load_cube(scale.corrected.set_local_file.call_args[0][0]) self.assertEqual(cube.data.max(), cube.data.min()) return cube - - -- GitLab From 476cd238693483e4819aef30cf923c95afd0939b Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 15:16:27 +0200 Subject: [PATCH 05/30] Added tests for select levels --- earthdiagnostics/general/select_levels.py | 13 +++++++--- test/unit/general/test_select_levels.py | 29 +++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/earthdiagnostics/general/select_levels.py b/earthdiagnostics/general/select_levels.py index 3a6c38f1..35fa33f4 100644 --- a/earthdiagnostics/general/select_levels.py +++ b/earthdiagnostics/general/select_levels.py @@ -90,9 +90,16 @@ class SelectLevels(Diagnostic): def compute(self): """Run the diagnostic""" temp = TempFile.get() - - Utils.nco.ncks(input=self.variable_file, output=temp, - options=('-O -d lev,{0.min_depth},{0.max_depth}'.format(self.box),)) + handler = Utils.open_cdf(self.variable_file.local_file) + var_name = "" + for var in ('lev', 'plev'): + if var in handler.variables: + var_name = var + continue + handler.close() + + Utils.nco.ncks(input=self.variable_file.local_file, output=temp, + options=('-O -d {1},{0.min_depth},{0.max_depth}'.format(self.box, var_name),)) self.result.set_local_file(temp) @staticmethod diff --git a/test/unit/general/test_select_levels.py b/test/unit/general/test_select_levels.py index 146f0744..69cba3d5 100644 --- a/test/unit/general/test_select_levels.py +++ b/test/unit/general/test_select_levels.py @@ -1,5 +1,11 @@ # coding=utf-8 from unittest import TestCase +import os +from tempfile import mktemp + +import numpy as np +import dummydata +import iris from earthdiagnostics.diagnostic import DiagnosticVariableListOption, DiagnosticOptionError from earthdiagnostics.box import Box @@ -23,6 +29,11 @@ class TestSelectLevels(TestCase): self.box = Box() self.box.min_depth = 0 self.box.max_depth = 100 + self.var_file = mktemp('.nc') + + def tearDown(self): + if os.path.exists(self.var_file): + os.remove(self.var_file) def fake_parse(self, value): return value.split('-') @@ -61,7 +72,21 @@ class TestSelectLevels(TestCase): SelectLevels.generate_jobs(self.diags, ['diagnostic', 'atmos', 'var', '0', '20', 'grid', 'extra']) def test_str(self): - mixed = SelectLevels(self.data_manager, '20010101', 0, 0, ModelingRealms.atmos, 'var', 'grid', 0, 20) - self.assertEqual(str(mixed), + select = SelectLevels(self.data_manager, '20010101', 0, 0, ModelingRealms.atmos, 'var', 'grid', 0, 20) + self.assertEqual(str(select), 'Select levels Startdate: 20010101 Member: 0 Chunk: 0 Variable: atmos:var ' 'Levels: 0-20 Grid: grid') + + def test_compute(self): + dummydata.model3.Model3(oname=self.var_file, var='ta', start_year=2000, stop_year=2000, method='constant', + constant=1) + select = SelectLevels(self.data_manager, '20010101', 0, 0, ModelingRealms.atmos, 'ta', 'grid', 0, 3) + select.variable_file = Mock() + select.variable_file.local_file = self.var_file + + select.result = Mock() + select.compute() + select.result.set_local_file.assert_called_once() + cube = iris.load_cube(select.result.set_local_file.call_args[0][0]) + original_cube = iris.load_cube(select.result.set_local_file.call_args[0][0]) + self.assertTrue(np.all(cube.coord('air_pressure').points == original_cube.coord('air_pressure').points[0:4])) -- GitLab From 3a448de83ccb727041b6fc72c594bd089bbd6a28 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 15:27:07 +0200 Subject: [PATCH 06/30] Added inspections to tests --- .codacy.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.codacy.yml b/.codacy.yml index 445a627b..3e730e5f 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -5,9 +5,6 @@ engines: coverage: enabled: true - exclude_paths: [ - 'tests', - ] metrics: enabled: true duplication: @@ -20,6 +17,5 @@ engines: exclude_paths: [ 'doc/**', - 'test/**', 'earthdiagnostics/cmor_tables/**', ] -- GitLab From cc320de2038cb63d25c413cebe4c57c88b89427e Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 15:33:46 +0200 Subject: [PATCH 07/30] Fixed lint --- test/unit/general/test_select_levels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/general/test_select_levels.py b/test/unit/general/test_select_levels.py index 69cba3d5..a9dac99a 100644 --- a/test/unit/general/test_select_levels.py +++ b/test/unit/general/test_select_levels.py @@ -89,4 +89,4 @@ class TestSelectLevels(TestCase): select.result.set_local_file.assert_called_once() cube = iris.load_cube(select.result.set_local_file.call_args[0][0]) original_cube = iris.load_cube(select.result.set_local_file.call_args[0][0]) - self.assertTrue(np.all(cube.coord('air_pressure').points == original_cube.coord('air_pressure').points[0:4])) + self.assertTrue(np.all(cube.coord('air_pressure').points == original_cube.coord('air_pressure').points[0:4])) -- GitLab From eb7d76cd46d5ceb3a221bb09c0d370483e2ebc8b Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 4 Jun 2018 16:33:46 +0200 Subject: [PATCH 08/30] Updated style and added doc to tests --- test/unit/test_diagnostic.py | 11 +++++++++-- test/unit/test_lint.py | 2 +- test/unit/test_workmanager.py | 36 ++++++++++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/test/unit/test_diagnostic.py b/test/unit/test_diagnostic.py index f425083b..4cba0c11 100644 --- a/test/unit/test_diagnostic.py +++ b/test/unit/test_diagnostic.py @@ -1,5 +1,12 @@ # coding=utf-8 -from earthdiagnostics.diagnostic import * +from earthdiagnostics.diagnostic import Diagnostic, DiagnosticOption, DiagnosticOptionError, DiagnosticFloatOption, \ + DiagnosticDomainOption, DiagnosticIntOption, DiagnosticBoolOption, DiagnosticComplexStrOption, \ + DiagnosticListIntOption, DiagnosticChoiceOption, DiagnosticVariableOption, DiagnosticVariableListOption, \ + DiagnosticBasinOption, DiagnosticStatus +from earthdiagnostics.datafile import StorageStatus + +from earthdiagnostics.constants import Basins + from unittest import TestCase from earthdiagnostics.modelingrealm import ModelingRealms @@ -397,7 +404,7 @@ class TestDiagnostic(TestCase): class TestDiagnosticBasinOption(TestCase): @patch.object(Basins, 'parse') - def test_not_recognized(self, parse_mock): + def test_parse(self, parse_mock): parse_mock.return_value = 'var1' diag = DiagnosticBasinOption('basin') self.assertEqual('var1', diag.parse('var1')) diff --git a/test/unit/test_lint.py b/test/unit/test_lint.py index 287de6a6..4c1d1d91 100644 --- a/test/unit/test_lint.py +++ b/test/unit/test_lint.py @@ -1,6 +1,5 @@ """ Lint tests """ import os -import textwrap import unittest import pycodestyle # formerly known as pep8 @@ -10,6 +9,7 @@ class TestLint(unittest.TestCase): def test_pep8_conformance(self): """Test that we conform to PEP-8.""" + check_paths = [ 'earthdiagnostics', 'test', diff --git a/test/unit/test_workmanager.py b/test/unit/test_workmanager.py index 119d3c6f..58c0463d 100644 --- a/test/unit/test_workmanager.py +++ b/test/unit/test_workmanager.py @@ -1,3 +1,4 @@ +"""Test work_manager file""" # coding=utf-8 from unittest import TestCase from mock import Mock @@ -9,21 +10,26 @@ from bscearth.utils import log class TestDownloader(TestCase): + """Test downloader""" def setUp(self): + """Set up tests""" self.downloader = Downloader() def test_start_and_stop(self): + """Basic test with no data""" self.downloader.start() self.assertTrue(self.downloader._thread.is_alive()) self.downloader.shutdown() self.assertFalse(self.downloader._thread.is_alive()) def test_submit(self): + """Test submit method""" datafile = Mock() self.downloader.submit(datafile) def test_download(self): + """Test download call for method""" datafile = Mock() self.downloader.submit(datafile) self.downloader.start() @@ -31,6 +37,7 @@ class TestDownloader(TestCase): datafile.download.assert_called_once() def test_download_fails(self): + """Test bahaviour when download fail""" datafile = Mock() def _download(): @@ -46,6 +53,7 @@ class TestDownloader(TestCase): self.downloader.shutdown() def test_download_smaller_first(self): + """Test smaller downloads going first""" small_file = self._create_datafile_mock(size=1) self.downloader.submit(small_file) large_file = self._create_datafile_mock(size=10) @@ -62,6 +70,7 @@ class TestDownloader(TestCase): self.downloader.shutdown() def test_download_not_none_first(self): + """Test downloads with known size go first""" small_file = self._create_datafile_mock(size=None) self.downloader.submit(small_file) large_file = self._create_datafile_mock(size=10) @@ -78,6 +87,7 @@ class TestDownloader(TestCase): self.downloader.shutdown() def test_download_not_none_second(self): + """Test downloads with unknown size go last""" small_file = self._create_datafile_mock(size=1) self.downloader.submit(small_file) large_file = self._create_datafile_mock(size=None) @@ -94,6 +104,7 @@ class TestDownloader(TestCase): self.downloader.shutdown() def test_download_both_none(self): + """Test downloads work when all have unknown size""" small_file = self._create_datafile_mock(size=None) self.downloader.submit(small_file) large_file = self._create_datafile_mock(size=None) @@ -110,6 +121,7 @@ class TestDownloader(TestCase): self.downloader.shutdown() def test_download_can_not_order(self): + """Test downloads work when order can not be asigned""" small_file = self._create_datafile_mock(size=1) self.downloader.submit(small_file) large_file = self._create_datafile_mock(size=1) @@ -126,6 +138,7 @@ class TestDownloader(TestCase): self.downloader.shutdown() def test_download_more_suscribers_first(self): + """Test downloads requested by more diagnostics go first""" no_suscribers = self._create_datafile_mock() self.downloader.submit(no_suscribers) suscriber = Mock() @@ -143,7 +156,7 @@ class TestDownloader(TestCase): self.downloader.shutdown() def test_download_more_suscribers_waiting_first(self): - + """Test downloads with more diagnostics waiting go first""" class StatusDiag(Diagnostic): def __init__(self, data_manager, pending): super(StatusDiag, self).__init__(data_manager) @@ -187,30 +200,37 @@ class TestDownloader(TestCase): class MockFile(DataFile): + """Mock DataFile class for testing""" def __init__(self): super(MockFile, self).__init__() self.upload_fails = False def download(self): + """Simulate download step""" self.local_status = LocalStatus.READY def upload(self): + """Upload method""" if self.upload_fails: self.storage_status = StorageStatus.FAILED else: self.storage_status = StorageStatus.READY def clean_local(self): + """Simulate clean local call""" pass def check_is_in_storage(self): + """Simulate check storage""" return False class TestWorkManager(TestCase): + """Tests for workmanager class""" def setUp(self): + """Set up tests""" self.config = Mock() self.data_manager = Mock() self.config.max_cores = 1 @@ -221,7 +241,7 @@ class TestWorkManager(TestCase): self.work_manager = WorkManager(self.config, self.data_manager) def test_prepare_job_list(self): - + """Test job list preparation""" class Diag1(Diagnostic): alias = 'diag1' @@ -237,6 +257,7 @@ class TestWorkManager(TestCase): self.work_manager.prepare_job_list() def test_run(self): + """Test run""" self.config.max_cores = -1 class EmptyDiag(Diagnostic): @@ -262,6 +283,7 @@ class TestWorkManager(TestCase): self.assertFalse(self.work_manager.had_errors) def test_diag_can_be_skipped(self): + """Test run skipping diagnostics already done""" self.config.skip_diags_done = True class SkippedDiag(Diagnostic): @@ -290,6 +312,11 @@ class TestWorkManager(TestCase): self.assertFalse(self.work_manager.had_errors) def test_diag_can_be_skipped_but_no(self): + """ + Test run skipping diagnostics already done + + Diagnostic can be skipped in this case + """ self.config.skip_diags_done = False class SkippedDiag(Diagnostic): @@ -318,6 +345,7 @@ class TestWorkManager(TestCase): self.assertTrue(self.work_manager.had_errors) def test_failed_run(self): + """Test run when a diagnostic fails""" class FailDiag(Diagnostic): alias = 'diag1' @@ -360,6 +388,7 @@ class TestWorkManager(TestCase): return req def test_run_data(self): + """Test run with data diagnostic""" self.data_manager.config.max_cores = -1 class DataDiag(Diagnostic): @@ -385,7 +414,7 @@ class TestWorkManager(TestCase): self.assertFalse(self.work_manager.had_errors) def test_failed_run_upload_failed(self): - + """Test run failing at upload step""" self.data_manager.config.max_cores = -1 class DataDiag(Diagnostic): @@ -412,6 +441,7 @@ class TestWorkManager(TestCase): self.assertTrue(self.work_manager.had_errors) def test_run_empty(self): + """Test run with empty list""" if six.PY3: with self.assertLogs(log.Log.log) as cmd: self.work_manager.run() -- GitLab From ed7ff753fd0e03274f3f8f4ef6162273d2ce2bf9 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Tue, 5 Jun 2018 17:33:16 +0200 Subject: [PATCH 09/30] Fixed create link error when variable was not found --- earthdiagnostics/data_convention.py | 6 +++- earthdiagnostics/modelingrealm.py | 2 +- earthdiagnostics/variable.py | 54 ++++++++++++++++++----------- test/unit/test_variable.py | 10 +++--- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/earthdiagnostics/data_convention.py b/earthdiagnostics/data_convention.py index 43122007..98f9e9f5 100644 --- a/earthdiagnostics/data_convention.py +++ b/earthdiagnostics/data_convention.py @@ -630,7 +630,11 @@ class Cmor3Convention(DataConvention): frequency = self.config.var_manager.tables[table].frequency Log.debug('Creating links for table {0}', table) for var in os.listdir(os.path.join(path, member, table)): - domain = self.config.var_manager.get_variable(var, silent=True).domain + cmor_var = self.config.var_manager.get_variable(var, silent=True) + if var is not None: + domain = cmor_var.domain + else: + domain = self.config.var_manager.tables[table].domain for grid in os.listdir(os.path.join(path, member, table, var)): if member_str is not None and member_str != member: continue diff --git a/earthdiagnostics/modelingrealm.py b/earthdiagnostics/modelingrealm.py index 7c9b607c..ce91670d 100644 --- a/earthdiagnostics/modelingrealm.py +++ b/earthdiagnostics/modelingrealm.py @@ -120,7 +120,7 @@ class ModelingRealm(object): """ table_name = self.get_table_name(frequency, data_convention) from earthdiagnostics.variable import CMORTable - return CMORTable(table_name, frequency, 'December 2013') + return CMORTable(table_name, frequency, 'December 2013', self) class ModelingRealms(object): diff --git a/earthdiagnostics/variable.py b/earthdiagnostics/variable.py index 705aea3b..c4218a51 100644 --- a/earthdiagnostics/variable.py +++ b/earthdiagnostics/variable.py @@ -33,6 +33,7 @@ class VariableManager(object): self._dict_variables = {} self._dict_aliases = {} self.tables = {} + self.table_name = None def get_variable(self, original_name, silent=False): """ @@ -89,37 +90,38 @@ class VariableManager(object): table_name: str """ + self.table_name = table_name self._dict_variables = dict() - self._load_variable_list(table_name) + self._load_variable_list() self._load_missing_defaults() - self._load_known_aliases(table_name) + self._load_known_aliases() self.create_aliases_dict() - def _load_variable_list(self, table_name): + def _load_variable_list(self): - xlsx_path = self._get_xlsx_path(table_name) + xlsx_path = self._get_xlsx_path() if xlsx_path: self._load_xlsx(xlsx_path) return - json_folder = self._get_json_folder(table_name) + json_folder = self._get_json_folder() if os.path.isdir(json_folder): self._load_json(json_folder) return - csv_path = self._get_csv_path(table_name) + csv_path = self._get_csv_path() if os.path.isfile(csv_path): - self._load_file(table_name) + self._load_file(self.table_name) return - raise Exception('Data convention {0} unknown'.format(table_name)) + raise Exception('Data convention {0} unknown'.format(self.table_name)) def _get_csv_path(self, table_name): csv_table_path = os.path.join(self._cmor_tables_folder, '{0}.csv'.format(table_name)) return csv_table_path - def _get_json_folder(self, table_name): - json_folder = os.path.join(self._cmor_tables_folder, '{0}/Tables'.format(table_name)) + def _get_json_folder(self): + json_folder = os.path.join(self._cmor_tables_folder, '{0}/Tables'.format(self.table_name)) return json_folder def _load_file(self, csv_table_path, default=False): @@ -169,7 +171,8 @@ class VariableManager(object): table_id = data['Header']['table_id'][6:] table = CMORTable(table_id, Frequency(data['variable_entry'].values()[0]['frequency']), - data['Header']['table_date']) + data['Header']['table_date'], + ModelingRealms.parse(data['Header']['realm'])) self.tables[table_id] = table self._load_json_variables(data['variable_entry'], table) @@ -189,9 +192,9 @@ class VariableManager(object): except VariableJsonException: Log.error('Could not read variable {0}'.format(short_name)) - def _load_known_aliases(self, table_name): + def _load_known_aliases(self): self._load_alias_csv('default') - self._load_alias_csv(table_name) + self._load_alias_csv(self.table_name) def _load_alias_csv(self, filename): file_path = self._get_aliases_csv_path(filename) @@ -262,12 +265,12 @@ class VariableManager(object): for alias in cmor_var.known_aliases: self._dict_aliases[alias.alias] = (alias, cmor_var) - def _get_xlsx_path(self, table_name): - xlsx_table_path = os.path.join(self._cmor_tables_folder, '{0}.xlsx'.format(table_name)) + def _get_xlsx_path(self): + xlsx_table_path = os.path.join(self._cmor_tables_folder, '{0}.xlsx'.format(self.table_name)) if os.path.isfile(xlsx_table_path): return xlsx_table_path - xlsx_table_path = os.path.join(self._cmor_tables_folder, table_name, 'etc', '*.xlsx') + xlsx_table_path = os.path.join(self._cmor_tables_folder, self.table_name, 'etc', '*.xlsx') xlsx_table_path = glob.glob(xlsx_table_path) if len(xlsx_table_path) == 1: return xlsx_table_path[0] @@ -292,10 +295,18 @@ class VariableManager(object): def _load_xlsx_table(self, sheet, table_data): try: table_frequency, table_date = table_data[sheet.title] - table = CMORTable(sheet.title, table_frequency, table_date) + realm = None + json_path = os.path.join(self._get_json_folder(), '{0}.json'.format(sheet.title)) + if os.path.isfile(json_path): + with open(json_path) as json_file: + json_data = json_file.read() + data = json.loads(json_data) + realm = ModelingRealms.parse(data['Header']['realm']) + + table = CMORTable(sheet.title, table_frequency, table_date, realm) self.tables[sheet.title] = table for row in sheet.rows: - if row[0].value == 'Priority' or not row[5].value: + if row[0].value in ('Priority', 'rm') or not row[5].value: continue self._parse_xlsx_var_row(row, table) except Exception as ex: @@ -487,7 +498,7 @@ class Variable(object): return table if self.domain: table_name = self.domain.get_table_name(frequency, data_convention) - return CMORTable(table_name, frequency, 'December 2013') + return CMORTable(table_name, frequency, 'December 2013', self.domain) raise ValueError('Can not get table for {0} and frequency {1}'.format(self, frequency)) @staticmethod @@ -554,16 +565,17 @@ class CMORTable(object): date: str """ - def __init__(self, name, frequency, date): + def __init__(self, name, frequency, date, domain): self.name = name self.frequency = frequency self.date = date + self.domain = domain def __str__(self): return self.name def __repr__(self): - return '{0.name} ({0.frequency}, {0.date})'.format(self) + return '{0.name} ({0.domain} {0.frequency}, {0.date})'.format(self) def __lt__(self, other): return self.name < other.name diff --git a/test/unit/test_variable.py b/test/unit/test_variable.py index f6c86e2a..5772a26b 100644 --- a/test/unit/test_variable.py +++ b/test/unit/test_variable.py @@ -14,13 +14,13 @@ class TestCMORTable(TestCase): self.frequency = Mock() def test_str(self): - self.assertEqual(str(CMORTable('name', 'm', 'Month YEAR')), 'name') + self.assertEqual(str(CMORTable('name', 'm', 'Month YEAR', 'realm')), 'name') def test_repr(self): - self.assertEqual(repr(CMORTable('name', 'm', 'Month YEAR')), 'name (m, Month YEAR)') + self.assertEqual(repr(CMORTable('name', 'm', 'Month YEAR', 'realm')), 'name (realm m, Month YEAR)') def test_lt(self): - self.assertLess(CMORTable('a', 'm', 'Month YEAR'), CMORTable('b', 'm', 'Month YEAR')) + self.assertLess(CMORTable('a', 'm', 'Month YEAR', 'realm'), CMORTable('b', 'm', 'Month YEAR', 'realm')) class TestVariableAlias(TestCase): @@ -188,7 +188,7 @@ class TestVariable(TestCase): convention.name = 'specs' var = Variable() var.domain = ModelingRealms.atmos - var.add_table(CMORTable('Amon', Frequencies.monthly, 'December 2013')) + var.add_table(CMORTable('Amon', Frequencies.monthly, 'December 2013', ModelingRealms.atmos)) table = var.get_table(Frequencies.monthly, convention) self.assertEqual(table.frequency, Frequencies.monthly) self.assertEqual(table.name, 'Amon') @@ -199,7 +199,7 @@ class TestVariable(TestCase): convention.name = 'specs' var = Variable() var.domain = ModelingRealms.atmos - var.add_table(CMORTable('Amon', Frequencies.monthly, 'December 2013')) + var.add_table(CMORTable('Amon', Frequencies.monthly, 'December 2013', ModelingRealms.atmos)) table = var.get_table(Frequencies.daily, convention) self.assertEqual(table.frequency, Frequencies.daily) self.assertEqual(table.name, 'day') -- GitLab From 139f492c088fa570bbbe1b276cc52912e6b31740 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Tue, 5 Jun 2018 18:31:46 +0200 Subject: [PATCH 10/30] Fixed tests --- earthdiagnostics/variable.py | 2 +- test/unit/test_diagnostic.py | 51 +++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/earthdiagnostics/variable.py b/earthdiagnostics/variable.py index c4218a51..aad15d28 100644 --- a/earthdiagnostics/variable.py +++ b/earthdiagnostics/variable.py @@ -109,7 +109,7 @@ class VariableManager(object): self._load_json(json_folder) return - csv_path = self._get_csv_path() + csv_path = self._get_csv_path(self.table_name) if os.path.isfile(csv_path): self._load_file(self.table_name) return diff --git a/test/unit/test_diagnostic.py b/test/unit/test_diagnostic.py index 4cba0c11..bf72aa9e 100644 --- a/test/unit/test_diagnostic.py +++ b/test/unit/test_diagnostic.py @@ -1,3 +1,4 @@ +"""Test for diagnostic module""" # coding=utf-8 from earthdiagnostics.diagnostic import Diagnostic, DiagnosticOption, DiagnosticOptionError, DiagnosticFloatOption, \ DiagnosticDomainOption, DiagnosticIntOption, DiagnosticBoolOption, DiagnosticComplexStrOption, \ @@ -14,166 +15,213 @@ from mock import patch, Mock class TestDiagnosticOption(TestCase): + """Test diagnostic basic string option""" def test_good_default_value(self): + """Test default_value""" diag = DiagnosticOption('option', 'default') self.assertEqual('default', diag.parse('')) def test_no_default_value(self): + """Test raises if not value provided and no default is available""" diag = DiagnosticOption('option') with self.assertRaises(DiagnosticOptionError): self.assertEqual('default', diag.parse('')) def test_parse_value(self): + """Test parse simple value""" diag = DiagnosticOption('option') self.assertEqual('value', diag.parse('value')) class TestDiagnosticFloatOption(TestCase): + """Test diagnostic float option""" def test_float_default_value(self): + """Test float default_value""" diag = DiagnosticFloatOption('option', 3.0) self.assertEqual(3.0, diag.parse('')) def test_str_default_value(self): + """Test str default_value""" diag = DiagnosticFloatOption('option', '3') self.assertEqual(3.0, diag.parse('')) def test_bad_default_value(self): + """Test fails if default_value can not be converted to float""" diag = DiagnosticFloatOption('option', 'default') with self.assertRaises(ValueError): self.assertEqual('default', diag.parse('')) def test_no_default_value(self): + """Test raises if not value provided and no default is available""" diag = DiagnosticFloatOption('option') with self.assertRaises(DiagnosticOptionError): self.assertEqual('default', diag.parse('')) def test_parse_value(self): + """Test parse simple value""" diag = DiagnosticFloatOption('option') self.assertEqual(3.25, diag.parse('3.25')) class TestDiagnosticDomainOption(TestCase): + """Test diagnostic domain option""" def test_domain_default_value(self): + """Test domain default_value""" diag = DiagnosticDomainOption('option', ModelingRealms.ocean) self.assertEqual(ModelingRealms.ocean, diag.parse('')) def test_str_default_value(self): + """Test str default_value""" diag = DiagnosticDomainOption('option', 'atmos') self.assertEqual(ModelingRealms.atmos, diag.parse('')) def test_bad_default_value(self): + """Test raises if default_value can not be converted to ModellingRealm""" diag = DiagnosticDomainOption('option', 'default') with self.assertRaises(ValueError): diag.parse('') def test_no_default_value(self): + """Test no value and default_value is provided""" diag = DiagnosticDomainOption('option') with self.assertRaises(DiagnosticOptionError): diag.parse('') def test_parse_value(self): + """Test parse option""" diag = DiagnosticDomainOption('option') self.assertEqual(ModelingRealms.seaIce, diag.parse('seaice')) class TestDiagnosticIntOption(TestCase): + """Test diagnostic int option""" + def test_int_default_value(self): + """Test int default_value""" diag = DiagnosticIntOption('option', 3) self.assertEqual(3, diag.parse('')) def test_str_default_value(self): + """Test str default_value""" diag = DiagnosticIntOption('option', '3') self.assertEqual(3, diag.parse('')) def test_bad_default_value(self): + """Test raises if default_value can not be parsed to int""" diag = DiagnosticIntOption('option', 'default') with self.assertRaises(ValueError): diag.parse('') def test_no_default_value(self): + """Test raises if no value and default_value are provided""" diag = DiagnosticIntOption('option') with self.assertRaises(DiagnosticOptionError): diag.parse('') def test_parse_value(self): + """Test parse int value""" diag = DiagnosticIntOption('option') self.assertEqual(3, diag.parse('3')) def test_parse_bad_value(self): + """Test raises if value can not be parsed to int""" diag = DiagnosticIntOption('option') with self.assertRaises(ValueError): diag.parse('3.5') def test_good_low_limit(self): + """Test parse if low limit is configured""" diag = DiagnosticIntOption('option', None, 0) self.assertEqual(1, diag.parse('1')) def test_bad_low_limit(self): + """"Test raises if low limit is configured and value does not conform to it""" diag = DiagnosticIntOption('option', None, 0) with self.assertRaises(DiagnosticOptionError): diag.parse('-1') def test_good_high_limit(self): + """Test parse if high limit is configured""" diag = DiagnosticIntOption('option', None, None, 0) self.assertEqual(-1, diag.parse('-1')) def test_bad_high_limit(self): + """Test raises if high limit is configured and value does not conform to it""" diag = DiagnosticIntOption('option', None, None, 0) with self.assertRaises(DiagnosticOptionError): diag.parse('1') class TestDiagnosticBoolOption(TestCase): + """Test diagnostic bool option""" + def test_bool_default_value(self): + """Test parse bool default_value""" diag = DiagnosticBoolOption('option', True) self.assertEqual(True, diag.parse('')) def test_str_default_value(self): + """Test parse str default_value""" diag = DiagnosticBoolOption('option', 'False') self.assertEqual(False, diag.parse('')) def test_no_default_value(self): + """Test raises if neither value or default_value are provided""" diag = DiagnosticBoolOption('option') with self.assertRaises(DiagnosticOptionError): diag.parse('') def test_parse_True(self): + """Test parses 'True' as boolean True""" diag = DiagnosticBoolOption('option') - self.assertTrue(diag.parse('true')) + self.assertTrue(diag.parse('True')) def test_parse_true(self): + """Test parses 'true' as boolean True""" diag = DiagnosticBoolOption('option') self.assertTrue(diag.parse('true')) def test_parse_t(self): + """Test parses 't' as boolean True""" diag = DiagnosticBoolOption('option') self.assertTrue(diag.parse('t')) def test_parse_yes(self): + """Test parses 'YES' as boolean True""" diag = DiagnosticBoolOption('option') self.assertTrue(diag.parse('YES')) def test_parse_bad_value(self): + """Test parses random str as boolean False""" diag = DiagnosticBoolOption('option') self.assertFalse(diag.parse('3.5')) class TestDiagnosticComplexStrOption(TestCase): + """ + Test diagnostic complex string option + + It replaces '&.' por ' ' and '&;' por ',' + """ def test_complex_default_value(self): + """Test default value""" diag = DiagnosticComplexStrOption('option', 'default&.str&;&.working') self.assertEqual('default str, working', diag.parse('')) def test_simple_default_value(self): + """Test simple default value""" diag = DiagnosticComplexStrOption('default str, working', 'default str, working') self.assertEqual('default str, working', diag.parse('')) def test_no_default_value(self): + """Test raises if neither value or default_value are provided""" diag = DiagnosticComplexStrOption('option') with self.assertRaises(DiagnosticOptionError): diag.parse('') def test_parse_value(self): + """Test parse value""" diag = DiagnosticComplexStrOption('option') self.assertEqual('complex string, for testing', diag.parse('complex&.string&;&.for&.testing')) @@ -197,6 +245,7 @@ class TestDiagnosticListIntOption(TestCase): diag.parse('') def test_no_default_value(self): + """Test raises if neither value or default_value are provided""" diag = DiagnosticListIntOption('option') with self.assertRaises(DiagnosticOptionError): diag.parse('') -- GitLab From 65219393f908e32b925602d59d3ef7bb729d4f1c Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Wed, 6 Jun 2018 10:51:52 +0200 Subject: [PATCH 11/30] Fixed tests --- earthdiagnostics/data_convention.py | 2 +- test/unit/test_config.py | 34 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/earthdiagnostics/data_convention.py b/earthdiagnostics/data_convention.py index 98f9e9f5..d846c75c 100644 --- a/earthdiagnostics/data_convention.py +++ b/earthdiagnostics/data_convention.py @@ -631,7 +631,7 @@ class Cmor3Convention(DataConvention): Log.debug('Creating links for table {0}', table) for var in os.listdir(os.path.join(path, member, table)): cmor_var = self.config.var_manager.get_variable(var, silent=True) - if var is not None: + if cmor_var is not None: domain = cmor_var.domain else: domain = self.config.var_manager.tables[table].domain diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 4a0e51a7..9cc7be86 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -12,6 +12,8 @@ from earthdiagnostics.data_convention import SPECSConvention, PrimaveraConventio class VariableMock(object): + """Mock for Variable""" + def __init__(self): self.domain = ModelingRealms.ocean self.short_name = 'tos' @@ -21,7 +23,10 @@ class VariableMock(object): class VariableManagerMock(object): + """Mock for Variable manager""" + def get_variable(self, alias, silent=False): + """Return a VariableMock given an alias""" if alias == 'bad': return None var = VariableMock() @@ -30,15 +35,18 @@ class VariableManagerMock(object): class ParserMock(mock.Mock): + """ConfigParser Mock""" def __init__(self, **kwargs): super(mock.Mock, self).__init__(**kwargs) self._values = {} def add_value(self, section, var, value): + """Add value to the parser""" self._values[self.get_var_string(section, var)] = value def get_var_string(self, section, var): + """""" return '{0}:{1}'.format(section, var) def get_value(self, section, var, default): @@ -84,12 +92,15 @@ class ParserMock(mock.Mock): class TestCMORConfig(TestCase): + """Tests for CMORConfig class""" def setUp(self): + """Prepare tests""" self.mock_parser = ParserMock() self.var_manager = VariableManagerMock() def test_basic_config(self): + """Test minimum configuration""" config = CMORConfig(self.mock_parser, self.var_manager) self.assertEqual(config.ocean, True) self.assertEqual(config.atmosphere, True) @@ -113,11 +124,13 @@ class TestCMORConfig(TestCase): self.assertEqual(config.append_startdate, False) def test_cmorize(self): + """Test cmorize returns true always if list is not configured""" config = CMORConfig(self.mock_parser, self.var_manager) self.assertTrue(config.cmorize(VariableMock())) self.assertTrue(config.cmorize(None)) def test_cmorize_list(self): + """Test cmorize return true for variables in the given list""" self.mock_parser.add_value('CMOR', 'VARIABLE_LIST', 'ocean:thetao ocean:tos') config = CMORConfig(self.mock_parser, self.var_manager) @@ -129,6 +142,7 @@ class TestCMORConfig(TestCase): self.assertTrue(config.cmorize(thetao_mock)) def test_bad_list(self): + """Test cmorize return false for malformed variables""" self.mock_parser.add_value('CMOR', 'VARIABLE_LIST', '#ocean:tos') with self.assertRaises(ConfigException): CMORConfig(self.mock_parser, self.var_manager) @@ -142,6 +156,7 @@ class TestCMORConfig(TestCase): CMORConfig(self.mock_parser, self.var_manager) def test_not_cmorize(self): + """Test cmorize return false for variables not in the given list""" self.mock_parser.add_value('CMOR', 'VARIABLE_LIST', 'ocean:tos') config = CMORConfig(self.mock_parser, self.var_manager) @@ -160,6 +175,7 @@ class TestCMORConfig(TestCase): self.assertFalse(config.cmorize(thetao_mock)) def test_comment(self): + """Test cmorize return false for variables after the comment mark""" self.mock_parser.add_value('CMOR', 'VARIABLE_LIST', 'ocean:tos #ocean:thetao ') config = CMORConfig(self.mock_parser, self.var_manager) @@ -175,10 +191,12 @@ class TestCMORConfig(TestCase): CMORConfig(self.mock_parser, self.var_manager) def test_cmorization_chunk(self): + """Test chunk cmorization requested return always true if not given a list""" config = CMORConfig(self.mock_parser, self.var_manager) self.assertTrue(config.chunk_cmorization_requested(1)) def test_cmorize_only_some_chunks(self): + """Test chunk cmorization requested return true only if the value is in the list""" self.mock_parser.add_value('CMOR', 'CHUNKS', '3 5') config = CMORConfig(self.mock_parser, self.var_manager) self.assertTrue(config.chunk_cmorization_requested(3)) @@ -188,6 +206,7 @@ class TestCMORConfig(TestCase): self.assertFalse(config.chunk_cmorization_requested(6)) def test_any_required(self): + """Test any_required method""" config = CMORConfig(self.mock_parser, self.var_manager) self.assertTrue(config.any_required(['tos'])) @@ -200,6 +219,7 @@ class TestCMORConfig(TestCase): self.assertFalse(config.any_required(['tas'])) def test_hourly_vars(self): + """Test hourly vars specification""" config = CMORConfig(self.mock_parser, self.var_manager) self.assertEqual(config.get_variables(Frequencies.six_hourly), {}) @@ -218,6 +238,7 @@ class TestCMORConfig(TestCase): self.assertEqual(config.get_levels(Frequencies.six_hourly, 132), '0,5') def test_daily_vars(self): + """Test daily vars specification""" config = CMORConfig(self.mock_parser, self.var_manager) self.assertEqual(config.get_variables(Frequencies.daily), {}) @@ -236,6 +257,7 @@ class TestCMORConfig(TestCase): self.assertEqual(config.get_levels(Frequencies.daily, 132), '0,5') def test_monthly_vars(self): + """Test monthly vars specification""" config = CMORConfig(self.mock_parser, self.var_manager) self.assertEqual(config.get_variables(Frequencies.monthly), {}) @@ -254,11 +276,13 @@ class TestCMORConfig(TestCase): self.assertEqual(config.get_levels(Frequencies.monthly, 132), '0,5') def test_bad_frequency_vars(self): + """Test get variables fails if a bada frequency is specified""" config = CMORConfig(self.mock_parser, self.var_manager) with self.assertRaises(ValueError): config.get_variables(Frequencies.climatology) def test_requested_codes(self): + """Test requested codes returns a set of the codes requested at all frequencies""" self.mock_parser.add_value('CMOR', 'ATMOS_HOURLY_VARS', '128,129:1,130:1-2,131:1:10,132:0:10:5') self.mock_parser.add_value('CMOR', 'ATMOS_DAILY_VARS', '128,129:1,130:1-2,131:1:10,132:0:10:5') self.mock_parser.add_value('CMOR', 'ATMOS_MONTHLY_VARS', '128,129:1,130:1-2,131:1:10,132:0:10:5') @@ -268,42 +292,52 @@ class TestCMORConfig(TestCase): class TestTHREDDSConfig(TestCase): + """Test THREDDS config""" def setUp(self): + """Prepare tests""" self.mock_parser = ParserMock() def test_basic_config(self): + """Test basic configuration""" config = THREDDSConfig(self.mock_parser) self.assertEqual(config.server_url, '') def test_url(self): + """Test SERVER_URL parameter""" self.mock_parser.add_value('THREDDS', 'SERVER_URL', 'test_url') config = THREDDSConfig(self.mock_parser) self.assertEqual(config.server_url, 'test_url') class TestReportConfig(TestCase): + """Test report config""" def setUp(self): + """Prepare tests""" self.mock_parser = ParserMock() def test_basic_config(self): + """test default config""" config = ReportConfig(self.mock_parser) self.assertEqual(config.path, '') self.assertEqual(config.maximum_priority, 10) def test_path(self): + """Test path configuration""" self.mock_parser.add_value('REPORT', 'PATH', 'new_path') config = ReportConfig(self.mock_parser) self.assertEqual(config.path, 'new_path') def test_priority(self): + """Test maximum priority configuration""" self.mock_parser.add_value('REPORT', 'MAXIMUM_PRIORITY', 3) config = ReportConfig(self.mock_parser) self.assertEqual(config.maximum_priority, 3) class TestExperimentConfig(TestCase): + """Test experiment config""" def setUp(self): self.mock_parser = ParserMock() -- GitLab From 4d0cda813a01012d3f456db71a1ce6cc6a2ce05f Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Wed, 6 Jun 2018 17:50:36 +0200 Subject: [PATCH 12/30] Fixed link bug --- earthdiagnostics/data_convention.py | 6 ++++-- earthdiagnostics/variable.py | 21 +++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/earthdiagnostics/data_convention.py b/earthdiagnostics/data_convention.py index d846c75c..901d49d2 100644 --- a/earthdiagnostics/data_convention.py +++ b/earthdiagnostics/data_convention.py @@ -229,7 +229,8 @@ class DataConvention(object): if not grid: grid = 'original' - + if domain is None: + pass variable_folder = domain.get_varfolder(var, self.config.experiment.ocean_timestep, self.config.experiment.atmos_timestep) vargrid_folder = domain.get_varfolder(var, self.config.experiment.ocean_timestep, @@ -631,9 +632,10 @@ class Cmor3Convention(DataConvention): Log.debug('Creating links for table {0}', table) for var in os.listdir(os.path.join(path, member, table)): cmor_var = self.config.var_manager.get_variable(var, silent=True) + domain = None if cmor_var is not None: domain = cmor_var.domain - else: + if domain is None: domain = self.config.var_manager.tables[table].domain for grid in os.listdir(os.path.join(path, member, table, var)): if member_str is not None and member_str != member: diff --git a/earthdiagnostics/variable.py b/earthdiagnostics/variable.py index aad15d28..2468d5fd 100644 --- a/earthdiagnostics/variable.py +++ b/earthdiagnostics/variable.py @@ -295,14 +295,7 @@ class VariableManager(object): def _load_xlsx_table(self, sheet, table_data): try: table_frequency, table_date = table_data[sheet.title] - realm = None - json_path = os.path.join(self._get_json_folder(), '{0}.json'.format(sheet.title)) - if os.path.isfile(json_path): - with open(json_path) as json_file: - json_data = json_file.read() - data = json.loads(json_data) - realm = ModelingRealms.parse(data['Header']['realm']) - + realm = self._read_realm_from_json(sheet.title) table = CMORTable(sheet.title, table_frequency, table_date, realm) self.tables[sheet.title] = table for row in sheet.rows: @@ -314,6 +307,18 @@ class VariableManager(object): import traceback traceback.print_exc() + def _read_realm_from_json(self, table_name): + for prefix in ('CMIP6', 'PRIMAVERA'): + json_path = os.path.join(self._get_json_folder(), '{0}_{1}.json'.format(prefix, table_name)) + if os.path.isfile(json_path): + with open(json_path) as json_file: + json_data = json_file.read() + data = json.loads(json_data) + # Cogemos el primer realm para las tablas que tienen varios + # Solo se usa al generar los links para una startdate concreta + return ModelingRealms.parse(data['Header']['realm'].split(' ')[0]) + return None + def _parse_xlsx_var_row(self, row, table): cmor_name = row[11].value if not cmor_name: -- GitLab From 65d6f75e9e2d9ef468f0e0a82ce7c71fd8241d2b Mon Sep 17 00:00:00 2001 From: jvegas Date: Thu, 7 Jun 2018 17:06:49 +0200 Subject: [PATCH 13/30] Added some docstrings --- test/unit/test_box.py | 18 ++++++++++++++++++ test/unit/test_variable.py | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/test/unit/test_box.py b/test/unit/test_box.py index 8f69f2fa..3134ef6f 100644 --- a/test/unit/test_box.py +++ b/test/unit/test_box.py @@ -1,11 +1,14 @@ +"""Tests for box module""" # coding=utf-8 from unittest import TestCase from earthdiagnostics.box import Box class TestBox(TestCase): + """Test Box class""" def setUp(self): + """Prepare tests""" self.box1 = Box() self.box1.max_lat = 0 self.box1.min_lat = -20 @@ -33,6 +36,11 @@ class TestBox(TestCase): self.box4.max_depth = 20 def test_max_lat(self): + """ + Test max latitude setter + + Include tests for exceptions raising if value is out of range + """ with self.assertRaises(ValueError): Box().max_lat = 100 with self.assertRaises(ValueError): @@ -42,6 +50,11 @@ class TestBox(TestCase): Box().max_lat = 20 def test_min_lat(self): + """ + Test min latitude setter + + Include tests for exceptions raising if value is out of range + """ with self.assertRaises(ValueError): Box().min_lat = 100 with self.assertRaises(ValueError): @@ -51,6 +64,11 @@ class TestBox(TestCase): Box().min_lat = 90 def test_max_lon(self): + """ + Test max longitude setter + + Include tests for exceptions raising if value is out of range + """ with self.assertRaises(ValueError): Box().max_lon = 360 with self.assertRaises(ValueError): diff --git a/test/unit/test_variable.py b/test/unit/test_variable.py index 5772a26b..3b28c39a 100644 --- a/test/unit/test_variable.py +++ b/test/unit/test_variable.py @@ -1,3 +1,4 @@ +"""Test variable module""" # coding=utf-8 from mock import Mock @@ -9,14 +10,18 @@ from earthdiagnostics.frequency import Frequencies class TestCMORTable(TestCase): + """Test CMORTable class""" def setUp(self): + """Set up tests""" self.frequency = Mock() def test_str(self): + """Test string representation""" self.assertEqual(str(CMORTable('name', 'm', 'Month YEAR', 'realm')), 'name') def test_repr(self): + """Test string Representation""" self.assertEqual(repr(CMORTable('name', 'm', 'Month YEAR', 'realm')), 'name (realm m, Month YEAR)') def test_lt(self): -- GitLab From f414b1d2a2a65740b5d79a92320afc656fb6f45c Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Fri, 8 Jun 2018 10:35:06 +0200 Subject: [PATCH 14/30] Added more docstrings --- .coveragerc | 2 +- test/unit/test_diagnostic.py | 64 ++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6c77e847..95b76079 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ [run] branch = True -source = earthdiagnostics +source = earthdiagnostics test [html] title = Coverage report for EarthDiagnostics diff --git a/test/unit/test_diagnostic.py b/test/unit/test_diagnostic.py index bf72aa9e..5dd3f58c 100644 --- a/test/unit/test_diagnostic.py +++ b/test/unit/test_diagnostic.py @@ -227,19 +227,25 @@ class TestDiagnosticComplexStrOption(TestCase): class TestDiagnosticListIntOption(TestCase): + """Test DiagnosticListIntOption Class""" + def test_tuple_default_value(self): + """Test use tuple default_value if no option is provided""" diag = DiagnosticListIntOption('option', (3,)) self.assertEqual((3,), diag.parse('')) def test_list_default_value(self): + """Test use list default_value if no option is provided""" diag = DiagnosticListIntOption('option', [3]) self.assertEqual([3], diag.parse('')) def test_str_default_value(self): + """Test use str default_value if no option is provided""" diag = DiagnosticListIntOption('option', '3-4') self.assertEqual([3, 4], diag.parse('')) def test_bad_default_value(self): + """Test raises if default value can not be converted to int""" diag = DiagnosticListIntOption('option', 'default') with self.assertRaises(ValueError): diag.parse('') @@ -251,19 +257,23 @@ class TestDiagnosticListIntOption(TestCase): diag.parse('') def test_parse_value(self): + """Test parse a list of integers""" diag = DiagnosticListIntOption('option') self.assertEqual([3, 2], diag.parse('3-2')) def test_parse_single_value(self): + """Test parse a single integer""" diag = DiagnosticListIntOption('option') self.assertEqual([3], diag.parse('3')) def test_too_low(self): + """Test raises if low limit is configured and value does not conform to it""" diag = DiagnosticListIntOption('option', min_limit=5) with self.assertRaises(DiagnosticOptionError): diag.parse('3') def test_too_high(self): + """Test raises if high limit is configured and value does not conform to it""" diag = DiagnosticListIntOption('option', max_limit=5) with self.assertRaises(DiagnosticOptionError): diag.parse('8') @@ -275,20 +285,25 @@ class TestDiagnosticListIntOption(TestCase): class TestDiagnosticChoiceOption(TestCase): + """Test DiagnosticChoiceOption class""" def test_choice_value(self): + """Test parsing a choice""" diag = DiagnosticChoiceOption('option', ('a', 'b')) self.assertEqual('a', diag.parse('a')) def test_choice_default_value(self): + """Test use default value""" diag = DiagnosticChoiceOption('option', ('a', 'b'), default_value='a') self.assertEqual('a', diag.parse('')) def test_bad_default_value(self): + """Test error is raised if default option is not a valid choice""" with self.assertRaises(DiagnosticOptionError): DiagnosticChoiceOption('option', ('a', 'b'), default_value='c') def test_ignore_case_value(self): + """Test ignore_case option""" diag = DiagnosticChoiceOption('option', ('a', 'b')) self.assertEqual('b', diag.parse('b')) self.assertEqual('b', diag.parse('B')) @@ -300,20 +315,23 @@ class TestDiagnosticChoiceOption(TestCase): class TestDiagnosticVariableOption(TestCase): + """Test DiagnosticVariableOption class""" - def get_var_mock(self, name): + def _get_var_mock(self, name): mock = Mock() mock.short_name = name return mock def test_parse(self): + """Test parse""" var_manager_mock = Mock() - var_manager_mock.get_variable.return_value = self.get_var_mock('var1') + var_manager_mock.get_variable.return_value = self._get_var_mock('var1') diag = DiagnosticVariableOption(var_manager_mock) self.assertEqual('var1', diag.parse('var1')) def test_not_recognized(self): + """Test parsing a not recognized variable""" var_manager_mock = Mock() var_manager_mock.get_variable.return_value = None @@ -322,34 +340,40 @@ class TestDiagnosticVariableOption(TestCase): class TestDiagnosticVariableListOption(TestCase): + """Test DiagnosticVariableListOption class""" def test_parse_multiple(self): + """Test parsing multiple vars""" var_manager_mock = Mock() - var_manager_mock.get_variable.side_effect = (self.get_var_mock('var1'), self.get_var_mock('var2')) + var_manager_mock.get_variable.side_effect = (self._get_var_mock('var1'), self._get_var_mock('var2')) diag = DiagnosticVariableListOption(var_manager_mock, 'variables') self.assertEqual(['var1', 'var2'], diag.parse('var1:var2')) def test_parse_one(self): + """Test parsing only one var""" var_manager_mock = Mock() - var_manager_mock.get_variable.return_value = self.get_var_mock('var1') + var_manager_mock.get_variable.return_value = self._get_var_mock('var1') diag = DiagnosticVariableListOption(var_manager_mock, 'variables') self.assertEqual(['var1'], diag.parse('var1')) def test_not_recognized(self): + """Test parsing one var that can not be recognized""" var_manager_mock = Mock() var_manager_mock.get_variable.return_value = None diag = DiagnosticVariableListOption(var_manager_mock, 'variables') self.assertEqual(['var1'], diag.parse('var1')) - def get_var_mock(self, name): + def _get_var_mock(self, name): mock = Mock() mock.short_name = name return mock class TestDiagnostic(TestCase): + """Test base Diagnostic class""" def setUp(self): + """Prepare tests""" class MockDiag(Diagnostic): @classmethod def generate_jobs(cls, diags, options): @@ -367,33 +391,44 @@ class TestDiagnostic(TestCase): self.MockDiag = MockDiag def test_str(self): + """Test base __str__ implementation""" self.assertEqual(str(Diagnostic(None)), 'Developer must override base class __str__ method') + def test_repr(self): + """Test base __repr__ implementation""" + self.assertEqual(Diagnostic(None).__repr__(), str(Diagnostic(None))) + def test_compute_is_virtual(self): + """Test compute raises NotImplementedError""" with self.assertRaises(NotImplementedError): Diagnostic(None).compute() def test_declare_data_generated_is_virtual(self): + """Test declare_data_generated raises NotImplementedError""" with self.assertRaises(NotImplementedError): Diagnostic(None).declare_data_generated() def test_request_data_is_virtual(self): + """Test request_data raises NotImplementedError""" with self.assertRaises(NotImplementedError): Diagnostic(None).request_data() @patch.object(Diagnostic, 'dispatch') def test_set_status_call_dispatch(self, dispatch_mock): + """Test dispatch is called when setting a different statuts""" diag = Diagnostic(None) diag.status = DiagnosticStatus.FAILED dispatch_mock.assert_called_once_with(diag) @patch.object(Diagnostic, 'dispatch') - def test_set_status_call_dispatch(self, dispatch_mock): + def test_set_status_call_dispatch_not_called(self, dispatch_mock): + """Test dispatch is not called when setting again the same status""" diag = Diagnostic(None) diag.status = diag.status assert not dispatch_mock.called, 'Dispatch should not have been called' def test_register(self): + """Test register diagnostic""" with self.assertRaises(ValueError): Diagnostic.register(TestDiagnostic) @@ -404,26 +439,23 @@ class TestDiagnostic(TestCase): Diagnostic.register(self.MockDiag) def test_get_diagnostic(self): + """Test get diagnostic""" self.assertIsNone(Diagnostic.get_diagnostic('none')) self.MockDiag.alias = 'mock' Diagnostic.register(self.MockDiag) self.assertIs(self.MockDiag, Diagnostic.get_diagnostic('mock')) def test_generate_jobs(self): + """Test generate_jobs raises NotImplementedError""" with self.assertRaises(NotImplementedError): Diagnostic.generate_jobs(None, ['']) - def test_compute(self): - with self.assertRaises(NotImplementedError): - Diagnostic(None).compute() - - def test_repr(self): - self.assertEqual(Diagnostic(None).__repr__(), str(Diagnostic(None))) - def test_empty_process_options(self): + """Test process options works even without options""" self.assertEqual(len(Diagnostic.process_options(('diag_name',), tuple())), 0) def test_diagnostic_can_be_skipped(self): + """Test diagnostic can be skipped""" diag = self._get_diag_for_skipping() self.assertTrue(diag.can_skip_run()) @@ -438,28 +470,34 @@ class TestDiagnostic(TestCase): return diag def test_diagnostic_can_not_be_skipped_do_not_generate_files(self): + """Test diagnostic can not be skipped because it does not generate files""" diag = Diagnostic(None) self.assertFalse(diag.can_skip_run()) def test_diagnostic_can_not_be_skipped_modified(self): + """Test diagnostic can not be skipped because it modifies files""" diag = self._get_diag_for_skipping(modified=True) self.assertFalse(diag.can_skip_run()) def test_diagnostic_can_not_be_skipped_data_not_ready(self): + """Test diagnostic can not be skipped because results do not yet exist""" diag = self._get_diag_for_skipping(status=StorageStatus.PENDING) self.assertFalse(diag.can_skip_run()) class TestDiagnosticBasinOption(TestCase): + """Test for DiagnosticBasinOPtion""" @patch.object(Basins, 'parse') def test_parse(self, parse_mock): + """Test parse good basin""" parse_mock.return_value = 'var1' diag = DiagnosticBasinOption('basin') self.assertEqual('var1', diag.parse('var1')) @patch.object(Basins, 'parse') def test_not_recognized(self, parse_mock): + """Test parsing a bad basin""" parse_mock.return_value = None diag = DiagnosticBasinOption('basin') with self.assertRaises(DiagnosticOptionError): -- GitLab From 598a8ee84daa7365ad2ba5c8ff473425ab31aba1 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Fri, 8 Jun 2018 11:25:19 +0200 Subject: [PATCH 15/30] Added more docstrings and fixed tests --- test/unit/test_config.py | 39 ++++++++++++++++++++++++++++++++++++ test/unit/test_diagnostic.py | 3 ++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 9cc7be86..4d44897a 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -340,9 +340,11 @@ class TestExperimentConfig(TestCase): """Test experiment config""" def setUp(self): + """Set up tests""" self.mock_parser = ParserMock() def test_basic_config(self): + """Test default values""" config = ExperimentConfig() config.parse_ini(self.mock_parser) @@ -356,6 +358,14 @@ class TestExperimentConfig(TestCase): self.assertEqual(config.ocean_timestep, 6) def test_members(self): + """ + Test all ways of specifing the members + + Including: + - Adding the prefix string or not + - Providing them as a space separated list + - Providing them as a range with start and end separated by - (both extremes included) + """ self.mock_parser.add_value('EXPERIMENT', 'MEMBERS', 'fc0 1') config = ExperimentConfig() config.parse_ini(self.mock_parser) @@ -377,6 +387,14 @@ class TestExperimentConfig(TestCase): self.assertEqual(config.members, [1, 2, 3]) def test_startdates(self): + """ + Test startadates parsing + + Two options: + - A simple list + - Using a regex + + """ self.mock_parser.add_value('EXPERIMENT', 'STARTDATES', '20001101 20011101') config = ExperimentConfig() config.parse_ini(self.mock_parser) @@ -395,6 +413,19 @@ class TestExperimentConfig(TestCase): u'20020801', u'20021101']) def test_auto_startdates(self): + """ + Test parsing startdates using the automatic generation + + + Reference syntax: {START,STOP,INTERVAL} where start and stop are dates and the interval is given with the code + NI where N is the number of intervals (default 1) and I is the base interval (Y,M,W,D) + + """ + self.mock_parser.add_value('EXPERIMENT', 'STARTDATES', '{2000,2001,1Y}') + config = ExperimentConfig() + config.parse_ini(self.mock_parser) + self.assertEqual(config.startdates, ['20000101', '20010101']) + self.mock_parser.add_value('EXPERIMENT', 'STARTDATES', '{20001101,20011101,1Y}') config = ExperimentConfig() config.parse_ini(self.mock_parser) @@ -426,11 +457,13 @@ class TestExperimentConfig(TestCase): config.parse_ini(self.mock_parser) def test_get_member_str(self): + """Test get member str""" config = ExperimentConfig() config.parse_ini(self.mock_parser) self.assertEqual(config.get_member_str(1), 'fc1') def test_get_full_years(self): + """Test get full years method""" self.mock_parser.add_value('EXPERIMENT', 'CHUNK_SIZE', 3) self.mock_parser.add_value('EXPERIMENT', 'CHUNKS', 15) @@ -440,6 +473,7 @@ class TestExperimentConfig(TestCase): self.assertEqual(config.get_full_years('20000101'), [2000, 2001, 2002, 2003]) def test_get_year_chunks(self): + """Test get chunk years""" self.mock_parser.add_value('EXPERIMENT', 'CHUNK_SIZE', 3) self.mock_parser.add_value('EXPERIMENT', 'CHUNKS', 13) @@ -453,6 +487,7 @@ class TestExperimentConfig(TestCase): self.assertEqual(config.get_year_chunks('20000601', 1999), []) def test_get_chunk_list(self): + """Test get complete chunk list""" config = ExperimentConfig() config.startdates = ('20010101', ) config.members = (0, 1, 2) @@ -465,12 +500,14 @@ class TestExperimentConfig(TestCase): ('20010101', 1, 2), ('20010101', 2, 1), ('20010101', 2, 2)]) def test_get_member_list(self): + """Test get member list""" config = ExperimentConfig() config.startdates = ('20010101', ) config.members = (0, 1, 2) self.assertEqual(config.get_member_list(), [('20010101', 0), ('20010101', 1), ('20010101', 2)]) def test_get_chunk_start_str(self): + """Test get_chunk_start_str""" config = ExperimentConfig() self.mock_parser.add_value('EXPERIMENT', 'CHUNK_SIZE', 12) self.mock_parser.add_value('EXPERIMENT', 'CHUNKS', 3) @@ -478,6 +515,7 @@ class TestExperimentConfig(TestCase): self.assertEqual(config.get_chunk_start_str('20001101', 3), '20021101') def test_get_chunk_start_str_datetime(self): + """Test get_chunk_start_str when receiving a date object""" config = ExperimentConfig() self.mock_parser.add_value('EXPERIMENT', 'CHUNK_SIZE', 12) self.mock_parser.add_value('EXPERIMENT', 'CHUNKS', 3) @@ -486,6 +524,7 @@ class TestExperimentConfig(TestCase): self.assertEqual(config.get_chunk_start_str(date, 3), '20021101') def test_get_chunk_end_str(self): + """Test get_chunk_end_str""" config = ExperimentConfig() self.mock_parser.add_value('EXPERIMENT', 'CHUNK_SIZE', 12) self.mock_parser.add_value('EXPERIMENT', 'CHUNKS', 3) diff --git a/test/unit/test_diagnostic.py b/test/unit/test_diagnostic.py index 5dd3f58c..f4baa226 100644 --- a/test/unit/test_diagnostic.py +++ b/test/unit/test_diagnostic.py @@ -417,8 +417,9 @@ class TestDiagnostic(TestCase): def test_set_status_call_dispatch(self, dispatch_mock): """Test dispatch is called when setting a different statuts""" diag = Diagnostic(None) + old_status = diag.status diag.status = DiagnosticStatus.FAILED - dispatch_mock.assert_called_once_with(diag) + dispatch_mock.assert_called_once_with(diag, old_status) @patch.object(Diagnostic, 'dispatch') def test_set_status_call_dispatch_not_called(self, dispatch_mock): -- GitLab From e375bb979778b73e37cbcd0f1e0588c3d1dadc55 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Fri, 8 Jun 2018 11:35:50 +0200 Subject: [PATCH 16/30] Added more docstrings --- test/integration/test_cmorizer.py | 69 +++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index abe27695..e14872b9 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -1,3 +1,4 @@ +"""Tests for earthdiagnostics.cmorizer module""" from earthdiagnostics.cmorizer import Cmorizer from earthdiagnostics.utils import TempFile, Utils from earthdiagnostics.data_convention import DataConvention @@ -18,6 +19,7 @@ import calendar class TestCmorizer(TestCase): + """Tests for Cmorizer class""" def _get_variable_and_alias(self, variable): mock_alias = Mock() @@ -42,6 +44,7 @@ class TestCmorizer(TestCase): return os.path.join(self.tmp_dir, args[3], str(args[6]), '{0[3]}.nc'.format(args)) def setUp(self): + """Prepare tests""" self.tmp_dir = tempfile.mkdtemp() self.data_manager = Mock() @@ -86,7 +89,7 @@ class TestCmorizer(TestCase): os.makedirs(self.data_manager.config.data_dir) os.makedirs(self.data_manager.config.scratch_dir) - def create_ocean_files(self, filename, tar_name, gzip=False, backup=False): + def _create_ocean_files(self, filename, tar_name, gzip=False, backup=False): coord_data = np.array([1, 2], np.float) lat = DimCoord(coord_data, standard_name='latitude', long_name='latitude', var_name='lat', units='degrees_north') @@ -132,9 +135,11 @@ class TestCmorizer(TestCase): os.remove(file_path) def tearDown(self): + """Clean up after tests""" shutil.rmtree(self.tmp_dir) def test_skip_ocean_cmorization(self): + """Test ocean cmorization flag disabled option """ self.data_manager.config.cmor.ocean = False if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -147,6 +152,7 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_skip_atmos_cmorization(self): + """Test atmos cmorization flag disabled option """ self.data_manager.config.cmor.atmosphere = False if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -159,7 +165,8 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_skip_when_cmorized(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test cmorization skipped if already done """ + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.is_cmorized.return_value = True if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -172,7 +179,8 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_skip_when_not_requested(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test cmorization skipped if chunk is not requested""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.chunk_cmorization_requested.return_value = False if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -185,12 +193,14 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_force(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test cmorization force works """ + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.is_cmorized.return_value = True self.data_manager.config.cmor.force = True self._test_ocean_cmor() def test_ocean_cmorization_no_files(self): + """Test ocean cmorization report error if no input data """ if six.PY3: with self.assertLogs(log.Log.log) as cmd: cmorizer = Cmorizer(self.data_manager, '19900101', 0) @@ -204,7 +214,8 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_ocean_cmorization_not_vars_requested(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test ocean cmorization report success if no vars qhere requested""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.any_required.return_value = False if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -221,7 +232,8 @@ class TestCmorizer(TestCase): self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_no_vars_recognized(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test ocean cmorization report success if no vars where recognized""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') def not_recognized(*args): return None, None @@ -241,7 +253,8 @@ class TestCmorizer(TestCase): self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_var2_not_requested(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test ocean cmorization with var2 not recognized""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') def _reject_var2(cmor_var): return cmor_var.short_name != 'var2' @@ -275,20 +288,23 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_ocean_cmorization(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test basic ocean cmorization""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self._test_ocean_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_with_filter(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test ocean cmorization filtering files""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.filter_files = 'expid' self._test_ocean_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_with_bad_filter(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') + """Test ocean cmorization fails if a bad filter is added""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.filter_files = 'badfilter' if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -299,37 +315,42 @@ class TestCmorizer(TestCase): self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) self.assertTrue([record for record in cmd.records if record.levelno == log.Log.WARNING]) else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) + cmorizer = Cmorizer(self.data_manager, '19900101', """Test ocean cmorization filtering files""") cmorizer.cmorize_ocean() self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_gzip(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar', gzip=True) + """Test ocean cmorization if tars are also zipped""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar', gzip=True) self._test_ocean_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_backup(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar', backup=True) + """Test ocean cmorization when files are in backup path""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar', backup=True) self._test_ocean_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_PPO(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'PPO_expid_1D_xx_19900101_19900131.tar') + """Test ocean cmorization when files are PPO""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'PPO_expid_1D_xx_19900101_19900131.tar') self._test_ocean_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_ocean_cmorization_diags(self): - self.create_ocean_files('expid_1d_19900101_19900131.nc', 'diags_expid_1D_xx_19900101_19900131.tar') + """Test ocean cmorization when files are diags""" + self._create_ocean_files('expid_1d_19900101_19900131.nc', 'diags_expid_1D_xx_19900101_19900131.tar') self._test_ocean_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) def test_atmos_cmorization(self): - self.create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') + """Test basic atmos cmorization from nc""" + self._create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') self._test_atmos_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) @@ -354,7 +375,7 @@ class TestCmorizer(TestCase): cmorizer = Cmorizer(self.data_manager, '19900101', 0) cmorizer.cmorize_atmos() - def create_mma_files(self, filename, tar_name, gzip=False): + def _create_mma_files(self, filename, tar_name, gzip=False): coord_data = np.array([0, 1], np.float) folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', 'outputs') @@ -400,7 +421,8 @@ class TestCmorizer(TestCase): return file_path, filename def test_skip_when_not_requested_mma(self): - self.create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') + """Test atmos cmorization is skipped if chunk is not requested""" + self._create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') self.data_manager.config.cmor.chunk_cmorization_requested.return_value = False if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -413,12 +435,14 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_force_mma(self): - self.create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') + """Test force atmos cmorization""" + self._create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') self.data_manager.is_cmorized.return_value = True self.data_manager.config.cmor.force = True self._test_atmos_cmor() def test_atmos_cmorization_no_mma_files(self): + """Test atmos cmorization report error if there are no files""" if six.PY3: with self.assertLogs(log.Log.log) as cmd: cmorizer = Cmorizer(self.data_manager, '19900101', 0) @@ -431,7 +455,7 @@ class TestCmorizer(TestCase): cmorizer = Cmorizer(self.data_manager, '19900101', 0) cmorizer.cmorize_atmos() - def create_grib_files(self, filename, month): + def _create_grib_files(self, filename, month): filename = filename.format(month) coord_data = np.array([0, 1], np.float) folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', @@ -476,10 +500,11 @@ class TestCmorizer(TestCase): os.remove(file_path) def test_grib_cmorization(self): + """Test atmos cmorization from grib""" self.data_manager.config.experiment.chunk_size = 2 self.data_manager.get_file_path = self._get_file_path_grib - self.create_grib_files('ICM??expid+19900{}.nc', 1) - self.create_grib_files('ICM??expid+19900{}.nc', 2) + self._create_grib_files('ICM??expid+19900{}.nc', 1) + self._create_grib_files('ICM??expid+19900{}.nc', 2) self._test_atmos_cmor() variables = { 'CP': 143, -- GitLab From d337b19f0ad5296af5330c0e49eb5da2031a860d Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Fri, 8 Jun 2018 11:53:04 +0200 Subject: [PATCH 17/30] Added more docstrings --- test/unit/data_convention/test_primavera.py | 42 ++++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/test/unit/data_convention/test_primavera.py b/test/unit/data_convention/test_primavera.py index 2eb75af9..42fe37cb 100644 --- a/test/unit/data_convention/test_primavera.py +++ b/test/unit/data_convention/test_primavera.py @@ -1,3 +1,4 @@ +"""Tests for PRIMAVERA Convention""" import os import shutil import tempfile @@ -12,8 +13,10 @@ from earthdiagnostics.data_convention import PrimaveraConvention class TestPrimaveraConvention(TestCase): + """Tests for PRIMAVERA convetion class""" def setUp(self): + """Prepare tests""" self.tmp_dir = tempfile.mkdtemp() os.mkdir(os.path.join(self.tmp_dir, 'expid')) self.config = Mock() @@ -37,22 +40,27 @@ class TestPrimaveraConvention(TestCase): self.convention = PrimaveraConvention('name', self.config) def tearDown(self): + """Cleanup""" shutil.rmtree(self.tmp_dir) def test_get_startdate_path(self): + """Test get startdate path""" self.assertEqual(self.convention.get_startdate_path('19900101'), os.path.join(self.tmp_dir, 'expid/cmorfiles/activity/institute/model/experiment_name')) def test_experiment_name(self): + """Test get expriment name""" self.assertEqual(self.convention.experiment_name('19900101'), 'experiment_name') def test_experiment_name_append(self): + """Test get expriment name when appending startdate""" self.config.cmor.append_startdate = True self.assertEqual(self.convention.experiment_name('19900101'), 'experiment_nameS19900101') def test_get_cmor_folder_path(self): + """Test get cmor foilder path""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -64,6 +72,7 @@ class TestPrimaveraConvention(TestCase): 'r2i1p1f1/Omon/var/ocean_grid/version')) def test_get_cmor_folder_path_no_cmor_var(self): + """Test get cmor folder path when not passing cmor_var""" file_path = self.convention.get_cmor_folder_path('19900101', 1, ModelingRealms.ocean, 'var', Frequencies.monthly, None, None) self.assertEqual(file_path, @@ -71,6 +80,7 @@ class TestPrimaveraConvention(TestCase): 'r2i1p1f1/Omon/var/ocean_grid/version')) def test_get_cmor_folder_path_atmos(self): + """Test get cmor foilder path for the atmos""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -82,6 +92,7 @@ class TestPrimaveraConvention(TestCase): 'r2i1p1f1/Omon/var/atmos_grid/version')) def test_get_cmor_folder_path_custom_grid(self): + """Test get cmor foilder path for a custom grid""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -93,6 +104,7 @@ class TestPrimaveraConvention(TestCase): 'r2i1p1f1/Omon/var/grid/version')) def test_get_cmor_folder_path_no_cmor(self): + """Test get cmor folder path with no cmor_var""" frequency = Mock() frequency.__str__ = Mock() frequency.__str__.return_value = 'mon' @@ -104,6 +116,7 @@ class TestPrimaveraConvention(TestCase): 'r2i1p1f1/Omon/var/ocean_grid/version')) def test_get_file_path_no_version_primavera(self): + """Test get cmor folder path with no version""" self.config.cmor.version = '' cmor_var = Mock() omon = Mock() @@ -113,6 +126,7 @@ class TestPrimaveraConvention(TestCase): self.convention.get_cmor_folder_path('19900101', 1, ModelingRealms.ocean, 'var', 'mon', 'grid', cmor_var) def test_get_filename(self): + """Test get_filename""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -123,6 +137,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_ocean_grid_199001-199001.nc') def test_get_filename_no_cmor_var(self): + """Test get_filename not passing cmor_var""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -133,6 +148,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_ocean_grid_199001-199001.nc') def test_get_filename_daily(self): + """Test get_filename for daily frequency""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -144,6 +160,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_ocean_grid_19900101-19900131.nc') def test_get_filename_6hourly(self): + """Test get_filename for 6hourly files""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -155,6 +172,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_ocean_grid_199001010000-199001311800.nc') def test_get_filename_3hourly(self): + """Test get_filename for 3hourly files""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -166,6 +184,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_ocean_grid_199001010000-199001312100.nc') def test_get_filename_atmos(self): + """Test get_filename for atmos""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -176,6 +195,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_atmos_grid_199001-199001.nc') def test_get_filename_grid(self): + """Test get_filename for a custom grid""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -186,6 +206,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_grid_199001-199001.nc') def test_get_filename_year(self): + """Test get_filename for a whole year""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -196,6 +217,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_ocean_grid_1990.nc') def test_get_filename_date_Str(self): + """Test get_filename passing date_Str""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -206,6 +228,7 @@ class TestPrimaveraConvention(TestCase): 'var_Omon_model_experiment_name_r2i1p1f1_ocean_grid_date_str.nc') def test_get_filename_no_date_info(self): + """Test get_filename with no date info raises ValueError""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -217,6 +240,7 @@ class TestPrimaveraConvention(TestCase): @mock.patch('os.path.isfile') def test_is_cmorized(self, mock_is_file): + """Test is cmorized""" mock_is_file.return_value = True cmor_var = Mock() omon = Mock() @@ -230,6 +254,7 @@ class TestPrimaveraConvention(TestCase): @mock.patch('os.path.isfile') def test_is_not_cmorized(self, mock_is_file): + """Test is cmorized false""" mock_is_file.return_value = False cmor_var = Mock() omon = Mock() @@ -242,6 +267,7 @@ class TestPrimaveraConvention(TestCase): self.assertFalse(self.convention.is_cmorized('20000101', 1, 1, ModelingRealms.ocean)) def test_is_cmorized_false_not_member_folder(self): + """Test is cmorized false bacause ther is no member folder""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -253,6 +279,7 @@ class TestPrimaveraConvention(TestCase): self.assertFalse(self.convention.is_cmorized('20000101', 1, 1, ModelingRealms.ocean)) def test_is_cmorized_false_not_table_folder(self): + """Test is cmorized false bacause ther is no table folder""" cmor_var = Mock() omon = Mock() omon.name = 'Omon' @@ -265,6 +292,7 @@ class TestPrimaveraConvention(TestCase): @mock.patch('os.path.isfile') def test_is_cmorized_not_enough_vars(self, mock_is_file): + """Test is cmorized false because thera are not eniouch variables""" mock_is_file.return_value = True cmor_var = Mock() omon = Mock() @@ -276,18 +304,9 @@ class TestPrimaveraConvention(TestCase): 'mon/ocean/var')) self.assertFalse(self.convention.is_cmorized('20000101', 1, 1, ModelingRealms.ocean)) - def test_is_cmorized_not_domain_folder(self): - cmor_var = Mock() - omon = Mock() - omon.name = 'Omon' - cmor_var.get_table.return_value = omon - self.config.var_manager.get_variable.return_value = cmor_var - self.config.cmor.min_cmorized_vars = 2 - os.makedirs(os.path.join(self.tmp_dir, 'expid/cmorfiles/institute/model/experiment_name/S20000101/mon')) - self.assertFalse(self.convention.is_cmorized('20000101', 1, 1, ModelingRealms.ocean)) - @mock.patch('earthdiagnostics.data_convention.PrimaveraConvention.create_link') def test_create_links_primavera(self, mock_create_link): + """Test create links""" member_path = os.path.join(self.tmp_dir, 'expid/cmorfiles/activity/institute/model/experiment_name/r2i1p1f1/Omon/var/gn') os.makedirs(member_path) @@ -298,6 +317,7 @@ class TestPrimaveraConvention(TestCase): @mock.patch('earthdiagnostics.data_convention.PrimaveraConvention.create_link') def test_create_links_with_version_primavera(self, mock_create_link): + """Test create links with version""" member_path = os.path.join(self.tmp_dir, 'expid/cmorfiles/activity/institute/model/experiment_name/r2i1p1f1/Omon/' 'var/gn/version') @@ -309,6 +329,7 @@ class TestPrimaveraConvention(TestCase): @mock.patch('earthdiagnostics.data_convention.PrimaveraConvention.create_link') def test_create_links_with_version_primavera_no_member(self, mock_create_link): + """Test create links with version full startdate""" member_path = os.path.join(self.tmp_dir, 'expid/cmorfiles/activity/institute/model/experiment_name/r2i1p1f1/Omon/' 'var/gn/version') @@ -320,6 +341,7 @@ class TestPrimaveraConvention(TestCase): @mock.patch('earthdiagnostics.data_convention.PrimaveraConvention.create_link') def test_create_links_member_not_found_primavera(self, mock_create_link): + """Test create links when the member can not be found""" member_path = os.path.join(self.tmp_dir, 'expid/cmorfiles/activity/institute/model/experiment_name/r1i1p1f1/Omon/var/gn') os.makedirs(member_path) -- GitLab From aa6f66f36e75b76e8961f1e7095c6ee42d48bff2 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Fri, 8 Jun 2018 15:44:05 +0200 Subject: [PATCH 18/30] Fix cmorization bug and added hfxout --- earthdiagnostics/cmor_tables/default.csv | 3 ++- earthdiagnostics/cmorizer.py | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/earthdiagnostics/cmor_tables/default.csv b/earthdiagnostics/cmor_tables/default.csv index fd13cc4f..9d562abf 100644 --- a/earthdiagnostics/cmor_tables/default.csv +++ b/earthdiagnostics/cmor_tables/default.csv @@ -1,4 +1,5 @@ Variable,Shortname,Name,Long name,Domain,Basin,Units,Valid min,Valid max,Grid,Tables +hfxout,hfxout,,Ocean surface heat flux,ocean,,W m-2,,,,, iiceages:siage:iice_otd,ageice,age_of_sea_ice,Age of sea ice,seaIce,,,,,, al,al,surface_albedo,Albedo,atmos,,,,,, bgfrcsal,bgfrcsal,change_over_time_in_heat_content_from_forcing,Change over time in salt content from forcing,ocean,,,,,, @@ -345,4 +346,4 @@ zqsb,hfsso,surface_downward_sensible_heat_flux,Surface Downward Sensible Heat Fl zqlw,rlntds,surface_net_downward_longwave_flux,Surface Net Downward Longwave Radiation,ocean,,W m-2,,,, var78,tclw,total_column_liquid_water,Total column liquid water,atmos,,kg m-2,,,, var79,tciw,total_column_ice_water,Total column ice water,atmos,,kg m-2,,,, -rho,rhopoto,sea_water_potential_density,Sea Water Potential Density,ocean,,kg m-3,,,, \ No newline at end of file +rho,rhopoto,sea_water_potential_density,Sea Water Potential Density,ocean,,kg m-3,,,, diff --git a/earthdiagnostics/cmorizer.py b/earthdiagnostics/cmorizer.py index 87c2169a..97c4b60b 100644 --- a/earthdiagnostics/cmorizer.py +++ b/earthdiagnostics/cmorizer.py @@ -50,23 +50,21 @@ class Cmorizer(object): def __init__(self, data_manager, startdate, member): self.data_manager = data_manager + self.startdate = startdate self.member = member self.config = data_manager.config self.experiment = self.config.experiment self.cmor = self.config.cmor + self.convetion = self.config.data_convention self.member_str = self.experiment.get_member_str(member) self.original_files_path = os.path.join(self.config.data_dir, self.experiment.expid, 'original_files', self.startdate, self.member_str, 'outputs') self.atmos_timestep = None self.cmor_scratch = str(os.path.join(self.config.scratch_dir, 'CMOR', self.startdate, self.member_str)) - if self.config.data_convention in ('primavera', 'cmip6'): - self.lon_name = 'longitude' - self.lat_name = 'latitude' - else: - self.lon_name = 'lon' - self.lat_name = 'lat' + self.lon_name = self.config.data_convention.lon_name + self.lat_name = self.config.data_convention.lat_name self.alt_coord_names = {'time_counter': 'time', 'time_counter_bnds': 'time_bnds', 'time_counter_bounds': 'time_bnds', @@ -465,7 +463,7 @@ class Cmorizer(object): netcdf_file = NetCDFFile() netcdf_file.data_manager = self.data_manager netcdf_file.local_file = temp - netcdf_file.remote_file = self.data_manager.get_file_path(self.startdate, self.member, + netcdf_file.remote_file = self.config.data_convention.get_file_path(self.startdate, self.member, var_cmor.domain, var_cmor.short_name, var_cmor, None, frequency, grid=alias.grid, year=None, date_str=date_str) @@ -494,7 +492,9 @@ class Cmorizer(object): cube.coord(lev_original).var_name = lev_target except iris.exceptions.CoordinateNotFoundError: pass + iris.save(cube, temp, zlib=True) + Utils.rename_variables(temp, {'dim1': 'j', 'dim2': 'i'}, must_exist=False, rename_dimension=True) def _set_coordinates_attribute(self, file_path, var_cmor, variable): handler = Utils.open_cdf(file_path) -- GitLab From ce9eb28595eb985ef23d72363a9a16e194a26d8f Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Fri, 8 Jun 2018 15:44:22 +0200 Subject: [PATCH 19/30] Bumped version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index e2859adc..7a113d6c 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -3.0.0rc9 +3.0.0rc10 -- GitLab From ff70588eb8d21d19651c29ef2a4ca493379621d1 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Fri, 8 Jun 2018 17:08:27 +0200 Subject: [PATCH 20/30] Added more docstrings --- test/unit/test_variable.py | 11 +++++++++++ test/unit/test_variable_type.py | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/test/unit/test_variable.py b/test/unit/test_variable.py index 3b28c39a..874ad658 100644 --- a/test/unit/test_variable.py +++ b/test/unit/test_variable.py @@ -219,34 +219,43 @@ class TestVariable(TestCase): class TestVariableManager(TestCase): + """Tests for VariableManager""" def setUp(self): + """Prepare tests""" self.var_manager = VariableManager() def tearDown(self): + """Cleanup""" self.var_manager.clean() def test_load_primavera(self): + """Test loading SPECS tables""" self.var_manager.load_variables('primavera') self.assertTrue(self.var_manager.get_all_variables()) def test_load_cmip6(self): + """Test loading SPECS tables""" self.var_manager.load_variables('cmip6') self.assertTrue(self.var_manager.get_all_variables()) def test_load_specs(self): + """Test loading SPECS tables""" self.var_manager.load_variables('specs') self.assertTrue(self.var_manager.get_all_variables()) def test_load_preface(self): + """Test loading SPECS tables""" self.var_manager.load_variables('preface') self.assertTrue(self.var_manager.get_all_variables()) def test_bad_load(self): + """Test loading a bad table raises an exception""" with self.assertRaises(Exception): self.var_manager.load_variables('badconvention') def test_get_variable(self): + """Test get variable""" var1 = self._get_var_mock('var1', ['var1_alias']) self.var_manager.register_variable(var1) self.var_manager.create_aliases_dict() @@ -258,6 +267,7 @@ class TestVariableManager(TestCase): self.assertIsNone(self.var_manager.get_variable('var2', True)) def test_get_variable_and_alias(self): + """Test get variable and alias""" var1 = self._get_var_mock('var1', ['var1_alias']) self.var_manager.register_variable(var1) self.var_manager.create_aliases_dict() @@ -283,6 +293,7 @@ class TestVariableManager(TestCase): return var1 def test_get_all_variables(self): + """Test get all variables""" var1 = self._get_var_mock('var1', ['var1_alias']) var2 = self._get_var_mock('var2', ['var2_alias']) diff --git a/test/unit/test_variable_type.py b/test/unit/test_variable_type.py index cb3dfa05..52cf76df 100644 --- a/test/unit/test_variable_type.py +++ b/test/unit/test_variable_type.py @@ -1,3 +1,4 @@ +"""Tests for VariableType class""" # coding=utf-8 from unittest import TestCase @@ -5,13 +6,17 @@ from earthdiagnostics.variable import VariableType class TestVariableType(TestCase): + """Tests for VariableType class""" def test_mean(self): + """Test to_str of MEAN""" self.assertEqual(VariableType.to_str(VariableType.MEAN), 'mean') def test_statistics(self): + """Test to_str of STATISTIC""" self.assertEqual(VariableType.to_str(VariableType.STATISTIC), 'statistics') def test_bad_one(self): + """Test to_str raises if type not recognized""" with self.assertRaises(ValueError): VariableType.to_str('bad type') -- GitLab From 92c6949f7e492c1cdfaf2bc6618b6af80a766432 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 10:06:36 +0200 Subject: [PATCH 21/30] Fixed cmorizer bug --- earthdiagnostics/cmorizer.py | 7 ++++--- test/integration/test_cmorizer.py | 5 ++++- test/unit/test_cmormanager.py | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/earthdiagnostics/cmorizer.py b/earthdiagnostics/cmorizer.py index 97c4b60b..7e80ebb0 100644 --- a/earthdiagnostics/cmorizer.py +++ b/earthdiagnostics/cmorizer.py @@ -464,9 +464,10 @@ class Cmorizer(object): netcdf_file.data_manager = self.data_manager netcdf_file.local_file = temp netcdf_file.remote_file = self.config.data_convention.get_file_path(self.startdate, self.member, - var_cmor.domain, var_cmor.short_name, var_cmor, - None, frequency, - grid=alias.grid, year=None, date_str=date_str) + var_cmor.domain, var_cmor.short_name, + var_cmor, None, frequency, + grid=alias.grid, year=None, + date_str=date_str) netcdf_file.data_convention = self.config.data_convention netcdf_file.region = region diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index e14872b9..a3b67133 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -48,13 +48,16 @@ class TestCmorizer(TestCase): self.tmp_dir = tempfile.mkdtemp() self.data_manager = Mock() - self.data_manager.get_file_path = self._get_file_path + self.data_manager.is_cmorized.return_value = False self.data_manager.config.data_dir = os.path.join(self.tmp_dir, 'data') self.data_manager.config.scratch_dir = os.path.join(self.tmp_dir, 'scratch') TempFile.scratch_folder = self.data_manager.config.scratch_dir self.data_manager.config.data_convention = create_autospec(DataConvention) self.data_manager.config.data_convention.name = 'data_convention' + self.data_manager.config.data_convention.lat_name = 'lat' + self.data_manager.config.data_convention.lon_name = 'lon' + self.data_manager.config.data_convention.get_file_path = self._get_file_path self.data_manager.config.var_manager.get_variable_and_alias = self._get_variable_and_alias self.data_manager.config.var_manager.get_variable = self._get_variable diff --git a/test/unit/test_cmormanager.py b/test/unit/test_cmormanager.py index e4c4463c..c003a401 100644 --- a/test/unit/test_cmormanager.py +++ b/test/unit/test_cmormanager.py @@ -20,6 +20,8 @@ class TestCMORManager(TestCase): self.convention = create_autospec(DataConvention) self.convention.name = 'specs' + self.convention.lat_name = 'lat' + self.convention.lon_name = 'lon' self.convention.get_startdate_path.return_value = os.path.join(self.tmp_dir, 'expid', 'startdate') self.convention.get_cmor_folder_path.return_value = os.path.join(self.tmp_dir, 'expid', 'startdate', 'cmor') self.convention.get_file_name.return_value = 'filename' -- GitLab From c38efaae6c66f0220a7b745453712db520aa1985 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 10:27:11 +0200 Subject: [PATCH 22/30] Fixed cmorizer test bug --- test/integration/test_cmorizer.py | 3 ++- test/unit/test_config.py | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index a3b67133..c89f28f7 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -38,7 +38,7 @@ class TestCmorizer(TestCase): return mock_variable def _get_file_path(self, *args, **kwargs): - return os.path.join(self.tmp_dir, args[3], '{0[3]}.nc'.format(args)) + return os.path.join(self.tmp_dir, args[3], '{0[3]}.nc'.format(args)) def _get_file_path_grib(self, *args, **kwargs): return os.path.join(self.tmp_dir, args[3], str(args[6]), '{0[3]}.nc'.format(args)) @@ -504,6 +504,7 @@ class TestCmorizer(TestCase): def test_grib_cmorization(self): """Test atmos cmorization from grib""" + self.data_manager.config.data_convention.get_file_path = self._get_file_path_grib self.data_manager.config.experiment.chunk_size = 2 self.data_manager.get_file_path = self._get_file_path_grib self._create_grib_files('ICM??expid+19900{}.nc', 1) diff --git a/test/unit/test_config.py b/test/unit/test_config.py index 4d44897a..b18d7ebb 100644 --- a/test/unit/test_config.py +++ b/test/unit/test_config.py @@ -1,3 +1,4 @@ +"""Tests for config module""" # coding=utf-8 from unittest import TestCase import datetime @@ -46,47 +47,57 @@ class ParserMock(mock.Mock): self._values[self.get_var_string(section, var)] = value def get_var_string(self, section, var): - """""" + """Get var string""" return '{0}:{1}'.format(section, var) def get_value(self, section, var, default): + """Get value from mock parser""" try: return self._values[self.get_var_string(section, var)] except KeyError: return default def get_bool_option(self, section, var, default): + """Get bool option""" return self.get_value(section, var, default) def get_path_option(self, section, var, default=""): + """Get path option""" return self.get_value(section, var, default) def get_int_option(self, section, var, default=0): + """Get integer option""" return self.get_value(section, var, default) def get_choice_option(self, section, var, choices, default, ignore_case=True): + """Get choice option""" return self.get_value(section, var, default) def get_int_list_option(self, section, var, default=list(), separator=' '): + """Get integer list option""" try: return [int(val) for val in self._values[self.get_var_string(section, var)].split(separator)] except KeyError: return default def get_list_option(self, section, var, default=list(), separator=' '): + """Get list option""" try: return [val for val in self._values[self.get_var_string(section, var)].split(separator)] except KeyError: return default def get_option(self, section, var, default=None): + """get option""" return self.get_value(section, var, default) def has_section(self, section): + """Check if section exists""" start = '{0}:'.format(section) return any(x.startswith(start) for x in self._values) def options(self, section): + """Return all options for a given section""" start = '{0}:'.format(section) return [x[len(start):] for x in self._values if x.startswith(start)] @@ -533,8 +544,10 @@ class TestExperimentConfig(TestCase): class TestConfig(TestCase): + """Tests for Config class""" def setUp(self): + """Prepare tests""" self.mock_parser = ParserMock() self.mock_parser.add_value('DIAGNOSTICS', 'FREQUENCY', 'mon') self.mock_parser.add_value('DIAGNOSTICS', 'DIAGS', 'diag1 diag2') @@ -542,6 +555,7 @@ class TestConfig(TestCase): self._environ = dict(os.environ) def tearDown(self): + """Cleanup""" os.environ.clear() os.environ.update(self._environ) @@ -562,17 +576,20 @@ class TestConfig(TestCase): config.parse('path') def test_diags(self): + """Test diags parsing""" config = Config() self.mock_parser.add_value('DIAGNOSTICS', 'DIAGS', 'diag1 diag2,opt1,opt2 # Commented diag') self._parse(config) self.assertEqual(config.get_commands(), (['diag1', 'diag2,opt1,opt2'])) def test_file_not_found(self): + """Test value error is raised if config file is not found""" config = Config() with self.assertRaises(ValueError): config.parse('path') def test_parse(self): + """Test parse minimal config""" config = Config() self._parse(config) self.assertEqual(config.frequency, Frequencies.monthly) @@ -588,12 +605,14 @@ class TestConfig(TestCase): self.assertEqual(os.environ['NAM_CDF_NAMES'], namelist) def test_alias(self): + """Test alias parsing""" config = Config() self.mock_parser.add_value('ALIAS', 'diag1', 'diag3') self._parse(config) self.assertEqual(config.get_commands(), ['diag3', 'diag2']) def test_auto_clean_ram_disk(self): + """Test that USE_RAMDISK forces AUTO_CLEAN to true""" config = Config() self.mock_parser.add_value('DIAGNOSTICS', 'AUTO_CLEAN', False) self.mock_parser.add_value('DIAGNOSTICS', 'USE_RAMDISK', True) @@ -602,6 +621,7 @@ class TestConfig(TestCase): self.assertEqual(config.use_ramdisk, True) def test_data_convention_primavera(self): + """Test parsing data convention PRIMAVERA""" config = Config() self.mock_parser.add_value('DIAGNOSTICS', 'DATA_CONVENTION', 'primavera') self._parse(config) @@ -612,6 +632,7 @@ class TestConfig(TestCase): self.assertEqual(os.environ['NAM_CDF_NAMES'], namelist) def test_data_convention_cmip6(self): + """Test parsing data convention CMIP6""" config = Config() self.mock_parser.add_value('DIAGNOSTICS', 'DATA_CONVENTION', 'cmip6') self._parse(config) @@ -622,6 +643,7 @@ class TestConfig(TestCase): self.assertEqual(os.environ['NAM_CDF_NAMES'], namelist) def test_data_convention_meteofrance(self): + """Test parsing data convention MeteoFrance""" config = Config() self.mock_parser.add_value('DIAGNOSTICS', 'DATA_CONVENTION', 'meteofrance') self._parse(config) @@ -632,6 +654,7 @@ class TestConfig(TestCase): self.assertEqual(os.environ['NAM_CDF_NAMES'], namelist) def test_data_convention_preface(self): + """Test parsing data convention Preface""" config = Config() self.mock_parser.add_value('DIAGNOSTICS', 'DATA_CONVENTION', 'preface') self._parse(config) -- GitLab From 0a24ac001ae8bf746f24040f00785f9237305a63 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 11:27:42 +0200 Subject: [PATCH 23/30] Removed lots of duplication in test --- test/integration/test_cmorizer.py | 251 +++++++++--------------------- 1 file changed, 77 insertions(+), 174 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index c89f28f7..8a2dff01 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -93,26 +93,8 @@ class TestCmorizer(TestCase): os.makedirs(self.data_manager.config.scratch_dir) def _create_ocean_files(self, filename, tar_name, gzip=False, backup=False): - coord_data = np.array([1, 2], np.float) - lat = DimCoord(coord_data, standard_name='latitude', long_name='latitude', var_name='lat', - units='degrees_north') - lon = DimCoord(coord_data, standard_name='longitude', long_name='longitude', var_name='lon', - units='degrees_east') - time = DimCoord(coord_data, standard_name='time', long_name='time', var_name='time', - units='days since 1950-01-01') - depth = DimCoord(coord_data, standard_name='depth', long_name='Depth', var_name='lev', units='m') - - var1 = iris.cube.Cube(np.random.rand(2, 2, 2).astype(np.float), long_name='Variable 1', var_name='var1') - var1.add_dim_coord(time, 0) - var1.add_dim_coord(lat, 1) - var1.add_dim_coord(lon, 2) - - var2 = iris.cube.Cube(np.random.rand(2, 2, 2, 2).astype(np.float), long_name='Variable 2', var_name='var2') - time.bounds = np.array([[0.5, 1.5], [1.5, 2.5]], np.float) - var2.add_dim_coord(time, 0) - var2.add_dim_coord(lat, 1) - var2.add_dim_coord(lon, 2) - var2.add_dim_coord(depth, 3) + var1 = self._create_sample_cube('Variable 1', 'var1', threed=False, time_bounds=True) + var2 = self._create_sample_cube('Variable 2', 'var2', threed=True, time_bounds=True) folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', 'outputs') @@ -137,23 +119,76 @@ class TestCmorizer(TestCase): tar.close() os.remove(file_path) + def _create_sample_cube(self, long_name, var_name, threed, time_bounds): + coord_data = np.array([1, 2], np.float) + lat = DimCoord(coord_data, standard_name='latitude', long_name='latitude', var_name='lat', + units='degrees_north') + lon = DimCoord(coord_data, standard_name='longitude', long_name='longitude', var_name='lon', + units='degrees_east') + time = DimCoord(coord_data, standard_name='time', long_name='time', var_name='time', + units='days since 1950-01-01') + if time_bounds: + time.bounds = np.array([[0.5, 1.5], [1.5, 2.5]], np.float) + + if threed: + data = np.random.rand(2, 2, 2, 2).astype(np.float) + depth = DimCoord(coord_data, standard_name='depth', long_name='Depth', var_name='lev', units='m') + else: + data = np.random.rand(2, 2, 2).astype(np.float) + + cube = iris.cube.Cube(data, long_name=long_name, var_name=var_name) + cube.add_dim_coord(time, 0) + cube.add_dim_coord(lat, 1) + cube.add_dim_coord(lon, 2) + if threed: + cube.add_dim_coord(depth, 3) + return cube + def tearDown(self): """Clean up after tests""" shutil.rmtree(self.tmp_dir) - def test_skip_ocean_cmorization(self): - """Test ocean cmorization flag disabled option """ - self.data_manager.config.cmor.ocean = False + def _test_ocean_cmor(self, success=True, error=False, critical=False, warnings=True, message=''): if six.PY3: with self.assertLogs(log.Log.log) as cmd: cmorizer = Cmorizer(self.data_manager, '19900101', 0) cmorizer.cmorize_ocean() - self.assertLogs([record for record in cmd.records if - record.message == 'Skipping ocean cmorization due to configuration']) + if message: + self.assertTrue([record for record in cmd.records if record.message == message]) + else: + for level, value in {log.Log.RESULT: success, log.Log.ERROR: error, log.Log.CRITICAL: critical, + log.Log.WARNING: warnings}: + if value: + self.assertTrue([record for record in cmd.records if record.levelno == level]) + else: + self.assertFalse([record for record in cmd.records if record.levelno == level]) else: cmorizer = Cmorizer(self.data_manager, '19900101', 0) cmorizer.cmorize_ocean() + def _test_atmos_cmor(self, success=True, error=False, critical=False, warnings=True, message=''): + if six.PY3: + with self.assertLogs(log.Log.log) as cmd: + cmorizer = Cmorizer(self.data_manager, '19900101', 0) + cmorizer.cmorize_atmos() + if message: + self.assertTrue([record for record in cmd.records if record.message == message]) + else: + for level, value in {log.Log.RESULT: success, log.Log.ERROR: error, log.Log.CRITICAL: critical, + log.Log.WARNING: warnings}: + if value: + self.assertTrue([record for record in cmd.records if record.levelno == level]) + else: + self.assertFalse([record for record in cmd.records if record.levelno == level]) + else: + cmorizer = Cmorizer(self.data_manager, '19900101', 0) + cmorizer.cmorize_atmos() + + def test_skip_ocean_cmorization(self): + """Test ocean cmorization flag disabled option """ + self.data_manager.config.cmor.ocean = False + self._test_ocean_cmor(message='Skipping ocean cmorization due to configuration') + def test_skip_atmos_cmorization(self): """Test atmos cmorization flag disabled option """ self.data_manager.config.cmor.atmosphere = False @@ -171,29 +206,13 @@ class TestCmorizer(TestCase): """Test cmorization skipped if already done """ self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.is_cmorized.return_value = True - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertTrue([record for record in cmd.records if - record.message == 'No need to unpack file 1/1']) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + self._test_ocean_cmor(message='No need to unpack file 1/1') def test_skip_when_not_requested(self): """Test cmorization skipped if chunk is not requested""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.chunk_cmorization_requested.return_value = False - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertTrue([record for record in cmd.records if - record.message == 'No need to unpack file 1/1']) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + self._test_ocean_cmor(message='No need to unpack file 1/1') def test_force(self): """Test cmorization force works """ @@ -204,33 +223,13 @@ class TestCmorizer(TestCase): def test_ocean_cmorization_no_files(self): """Test ocean cmorization report error if no input data """ - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.RESULT]) - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.ERROR]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.WARNING]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + self._test_ocean_cmor(success=False, error=True) def test_ocean_cmorization_not_vars_requested(self): """Test ocean cmorization report success if no vars qhere requested""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.any_required.return_value = False - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.RESULT]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.ERROR]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.WARNING]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + self._test_ocean_cmor() self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) @@ -241,17 +240,7 @@ class TestCmorizer(TestCase): def not_recognized(*args): return None, None self.data_manager.config.var_manager.get_variable_and_alias = not_recognized - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.RESULT]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.ERROR]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.WARNING]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + self._test_ocean_cmor() self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) @@ -263,33 +252,10 @@ class TestCmorizer(TestCase): return cmor_var.short_name != 'var2' self.data_manager.config.cmor.cmorize = _reject_var2 - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.RESULT]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.ERROR]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.WARNING]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + self._test_ocean_cmor() self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) - def _test_ocean_cmor(self): - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.RESULT]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.ERROR]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.WARNING]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - def test_ocean_cmorization(self): """Test basic ocean cmorization""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') @@ -309,17 +275,7 @@ class TestCmorizer(TestCase): """Test ocean cmorization fails if a bad filter is added""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.filter_files = 'badfilter' - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.RESULT]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.ERROR]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.WARNING]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', """Test ocean cmorization filtering files""") - cmorizer.cmorize_ocean() + self._test_ocean_cmor(warnings=True) self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) @@ -358,32 +314,11 @@ class TestCmorizer(TestCase): self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) - def _test_atmos_cmor(self): - - # if six.PY3: - # - # with self.assertLogs(log.Log.log) as cmd: - # cmorizer = Cmorizer(self.data_manager, '19900101', 0) - # cmorizer.cmorize_atmos() - # - # try: - # self.assertTrue([record for record in cmd.records if record.levelno == log.Log.RESULT]) - # self.assertFalse([record for record in cmd.records if record.levelno == log.Log.ERROR]) - # self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - # self.assertFalse([record for record in cmd.records if record.levelno == log.Log.WARNING]) - # except AssertionError: - # print(cmd) - # raise - # else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_atmos() - def _create_mma_files(self, filename, tar_name, gzip=False): - coord_data = np.array([0, 1], np.float) folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', 'outputs') - filepath_gg, filename_gg = self._create_file(coord_data, folder_path, filename.replace('??', 'GG'), gzip) - filepath_sh, filename_sh = self._create_file(coord_data, folder_path, filename.replace('??', 'SH'), gzip) + filepath_gg, filename_gg = self._create_file(folder_path, filename.replace('??', 'GG'), gzip) + filepath_sh, filename_sh = self._create_file(folder_path, filename.replace('??', 'SH'), gzip) tar = tarfile.TarFile(os.path.join(folder_path, tar_name), mode='w') tar.add(filepath_gg, arcname=filename_gg, recursive=False) @@ -392,23 +327,9 @@ class TestCmorizer(TestCase): os.remove(filepath_gg) os.remove(filepath_sh) - def _create_file(self, coord_data, folder_path, filename, gzip): - lat = DimCoord(coord_data, standard_name='latitude', long_name='latitude', var_name='lat', - units='degrees_north') - lon = DimCoord(coord_data, standard_name='longitude', long_name='longitude', var_name='lon', - units='degrees_east') - time = DimCoord(coord_data, standard_name='time', long_name='time', var_name='time', - units='days since 1950-01-01') - depth = DimCoord(coord_data, standard_name='depth', long_name='Depth', var_name='lev', units='m') - var1 = iris.cube.Cube(np.random.rand(2, 2, 2).astype(np.float), long_name='Variable 1', var_name='var1') - var1.add_dim_coord(time, 0) - var1.add_dim_coord(lat, 1) - var1.add_dim_coord(lon, 2) - var2 = iris.cube.Cube(np.random.rand(2, 2, 2, 2).astype(np.float), long_name='Variable 2', var_name='var2') - var2.add_dim_coord(time, 0) - var2.add_dim_coord(lat, 1) - var2.add_dim_coord(lon, 2) - var2.add_dim_coord(depth, 3) + def _create_file(self, folder_path, filename, gzip): + var1 = self._create_sample_cube('Variable 1', 'var1', threed=False, time_bounds=True) + var2 = self._create_sample_cube('Variable 2', 'var2', threed=True, time_bounds=True) if not os.path.isdir(folder_path): os.makedirs(folder_path) file_path = os.path.join(folder_path, filename) @@ -427,15 +348,7 @@ class TestCmorizer(TestCase): """Test atmos cmorization is skipped if chunk is not requested""" self._create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') self.data_manager.config.cmor.chunk_cmorization_requested.return_value = False - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_atmos() - self.assertTrue([record for record in cmd.records if - record.message == 'No need to unpack file 1/1']) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + self._test_atmos_cmor(message='No need to unpack file 1/1') def test_force_mma(self): """Test force atmos cmorization""" @@ -446,17 +359,7 @@ class TestCmorizer(TestCase): def test_atmos_cmorization_no_mma_files(self): """Test atmos cmorization report error if there are no files""" - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_atmos() - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.RESULT]) - self.assertTrue([record for record in cmd.records if record.levelno == log.Log.ERROR]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.CRITICAL]) - self.assertFalse([record for record in cmd.records if record.levelno == log.Log.WARNING]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_atmos() + self._test_atmos_cmor(success=False, error=True) def _create_grib_files(self, filename, month): filename = filename.format(month) @@ -478,7 +381,7 @@ class TestCmorizer(TestCase): time_data = np.arange(0.25, month_days + 0.25, 0.25, np.float) + month * 31 time = DimCoord(time_data, standard_name='time', long_name='time', var_name='time', units='days since 1990-01-01 00:00:00') - vars = [] + variables = [] for code in codes: var = iris.cube.Cube(np.ones((month_days * 4, 2, 2), np.float) * code, long_name='Variable {}'.format(code), @@ -490,12 +393,12 @@ class TestCmorizer(TestCase): var.add_dim_coord(lon, 2) var.attributes['table'] = np.int32(128) var.attributes['code'] = np.int32(code) - vars.append(var) + variables.append(var) if not os.path.isdir(folder_path): os.makedirs(folder_path) file_path = os.path.join(folder_path, filename) - iris.save(vars, file_path, zlib=True, local_keys=('table', 'code')) + iris.save(variables, file_path, zlib=True, local_keys=('table', 'code')) Utils.cdo.settaxis('1990-0{}-01,06:00,6hour'.format(month + 1), input=file_path, output=file_path.replace('.nc', '.grb'), -- GitLab From 7c354d08d273fb4a359850f1db54e4507940763d Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 11:52:45 +0200 Subject: [PATCH 24/30] Removed more duplication --- test/integration/test_cmorizer.py | 85 ++++++++++++------------------- 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index 8a2dff01..b7ae49be 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -148,11 +148,23 @@ class TestCmorizer(TestCase): """Clean up after tests""" shutil.rmtree(self.tmp_dir) - def _test_ocean_cmor(self, success=True, error=False, critical=False, warnings=True, message=''): + def _test_ocean_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars={}): + self._test_cmorization(success=success, error=error, critical=critical, warnings=warnings, message=message, + ocean=True) + + def _test_atmos_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars={}): + self._test_cmorization(success=success, error=error, critical=critical, warnings=warnings, message=message, + ocean=False) + + def _test_cmorization(self, success=True, error=False, critical=False, warnings=True, message='', ocean=True, + check_vars={}): if six.PY3: with self.assertLogs(log.Log.log) as cmd: cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() + if ocean: + cmorizer.cmorize_ocean() + else: + cmorizer.cmorize_atmos() if message: self.assertTrue([record for record in cmd.records if record.message == message]) else: @@ -164,25 +176,16 @@ class TestCmorizer(TestCase): self.assertFalse([record for record in cmd.records if record.levelno == level]) else: cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_ocean() - - def _test_atmos_cmor(self, success=True, error=False, critical=False, warnings=True, message=''): - if six.PY3: - with self.assertLogs(log.Log.log) as cmd: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) + if ocean: + cmorizer.cmorize_ocean() + else: cmorizer.cmorize_atmos() - if message: - self.assertTrue([record for record in cmd.records if record.message == message]) + + for variable, status in check_vars: + if status: + self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) else: - for level, value in {log.Log.RESULT: success, log.Log.ERROR: error, log.Log.CRITICAL: critical, - log.Log.WARNING: warnings}: - if value: - self.assertTrue([record for record in cmd.records if record.levelno == level]) - else: - self.assertFalse([record for record in cmd.records if record.levelno == level]) - else: - cmorizer = Cmorizer(self.data_manager, '19900101', 0) - cmorizer.cmorize_atmos() + self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) def test_skip_ocean_cmorization(self): """Test ocean cmorization flag disabled option """ @@ -229,9 +232,7 @@ class TestCmorizer(TestCase): """Test ocean cmorization report success if no vars qhere requested""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.any_required.return_value = False - self._test_ocean_cmor() - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': False, 'var2': False}) def test_ocean_cmorization_no_vars_recognized(self): """Test ocean cmorization report success if no vars where recognized""" @@ -240,9 +241,7 @@ class TestCmorizer(TestCase): def not_recognized(*args): return None, None self.data_manager.config.var_manager.get_variable_and_alias = not_recognized - self._test_ocean_cmor() - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': False, 'var2': False}) def test_ocean_cmorization_var2_not_requested(self): """Test ocean cmorization with var2 not recognized""" @@ -252,67 +251,49 @@ class TestCmorizer(TestCase): return cmor_var.short_name != 'var2' self.data_manager.config.cmor.cmorize = _reject_var2 - self._test_ocean_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': True, 'var2': False}) def test_ocean_cmorization(self): """Test basic ocean cmorization""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') - self._test_ocean_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': True, 'var2': True}) def test_ocean_cmorization_with_filter(self): """Test ocean cmorization filtering files""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.filter_files = 'expid' - self._test_ocean_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': True, 'var2': True}) def test_ocean_cmorization_with_bad_filter(self): """Test ocean cmorization fails if a bad filter is added""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.config.cmor.filter_files = 'badfilter' - self._test_ocean_cmor(warnings=True) - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(warnings=True, check_vars={'var1': False, 'var2': False}) def test_ocean_cmorization_gzip(self): """Test ocean cmorization if tars are also zipped""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar', gzip=True) - self._test_ocean_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': True, 'var2': True}) def test_ocean_cmorization_backup(self): """Test ocean cmorization when files are in backup path""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar', backup=True) - self._test_ocean_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': True, 'var2': True}) def test_ocean_cmorization_PPO(self): """Test ocean cmorization when files are PPO""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'PPO_expid_1D_xx_19900101_19900131.tar') - self._test_ocean_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': True, 'var2': True}) def test_ocean_cmorization_diags(self): """Test ocean cmorization when files are diags""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'diags_expid_1D_xx_19900101_19900131.tar') - self._test_ocean_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_ocean_cmor(check_vars={'var1': True, 'var2': True}) def test_atmos_cmorization(self): """Test basic atmos cmorization from nc""" self._create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') - self._test_atmos_cmor() - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var1', 'var1.nc'))) - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, 'var2', 'var2.nc'))) + self._test_atmos_cmor(check_vars={'var1': True, 'var2': True}) def _create_mma_files(self, filename, tar_name, gzip=False): folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', -- GitLab From 0e42ca42f37a6f0f578039c37f7e14a2c85baacd Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 12:06:03 +0200 Subject: [PATCH 25/30] Fixed issues in test_cmorizer --- test/integration/test_cmorizer.py | 72 +++++++++++++++++++------------ 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index b7ae49be..ce1d6cb7 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -93,15 +93,38 @@ class TestCmorizer(TestCase): os.makedirs(self.data_manager.config.scratch_dir) def _create_ocean_files(self, filename, tar_name, gzip=False, backup=False): - var1 = self._create_sample_cube('Variable 1', 'var1', threed=False, time_bounds=True) - var2 = self._create_sample_cube('Variable 2', 'var2', threed=True, time_bounds=True) + folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', + 'outputs') + file_path, filename = self._create_file(folder_path, filename, gzip) + if backup: + filename = os.path.join('backup', filename) + + tar = tarfile.TarFile(os.path.join(folder_path, tar_name), mode='w') + tar.add(file_path, arcname=filename, recursive=False) + tar.close() + os.remove(file_path) + + def _create_mma_files(self, filename, tar_name, gzip=False): folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', 'outputs') - os.makedirs(folder_path) + filepath_gg, filename_gg = self._create_file(folder_path, filename.replace('??', 'GG'), gzip) + filepath_sh, filename_sh = self._create_file(folder_path, filename.replace('??', 'SH'), gzip) + + tar = tarfile.TarFile(os.path.join(folder_path, tar_name), mode='w') + tar.add(filepath_gg, arcname=filename_gg, recursive=False) + tar.add(filepath_sh, arcname=filename_sh, recursive=False) + tar.close() + os.remove(filepath_gg) + os.remove(filepath_sh) + + def _create_file(self, folder_path, filename, gzip): + var1 = self._create_sample_cube('Variable 1', 'var1', threed=False, time_bounds=True) + var2 = self._create_sample_cube('Variable 2', 'var2', threed=True, time_bounds=True) + if not os.path.isdir(folder_path): + os.makedirs(folder_path) file_path = os.path.join(folder_path, filename) iris.save((var1, var2), file_path, zlib=True) - if gzip: import subprocess process = subprocess.Popen(('gzip', file_path), stdout=subprocess.PIPE) @@ -110,14 +133,7 @@ class TestCmorizer(TestCase): filename = "{0}.gz".format(filename) if process.returncode != 0: raise Exception('Can not compress: {0}'.format(comunicate)) - - if backup: - filename = os.path.join('backup', filename) - - tar = tarfile.TarFile(os.path.join(folder_path, tar_name), mode='w') - tar.add(file_path, arcname=filename, recursive=False) - tar.close() - os.remove(file_path) + return file_path, filename def _create_sample_cube(self, long_name, var_name, threed, time_bounds): coord_data = np.array([1, 2], np.float) @@ -148,16 +164,16 @@ class TestCmorizer(TestCase): """Clean up after tests""" shutil.rmtree(self.tmp_dir) - def _test_ocean_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars={}): + def _test_ocean_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars=None): self._test_cmorization(success=success, error=error, critical=critical, warnings=warnings, message=message, - ocean=True) + ocean=True, check_vars=check_vars) - def _test_atmos_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars={}): + def _test_atmos_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars=None): self._test_cmorization(success=success, error=error, critical=critical, warnings=warnings, message=message, - ocean=False) + ocean=False, check_vars=check_vars) def _test_cmorization(self, success=True, error=False, critical=False, warnings=True, message='', ocean=True, - check_vars={}): + check_vars=None): if six.PY3: with self.assertLogs(log.Log.log) as cmd: cmorizer = Cmorizer(self.data_manager, '19900101', 0) @@ -180,20 +196,20 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() else: cmorizer.cmorize_atmos() - - for variable, status in check_vars: - if status: - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) - else: - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) + if check_vars: + for variable, status in six.iteritems(check_vars): + if status: + self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) + else: + self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) def test_skip_ocean_cmorization(self): - """Test ocean cmorization flag disabled option """ + """Test ocean cmorization flag disabled option""" self.data_manager.config.cmor.ocean = False self._test_ocean_cmor(message='Skipping ocean cmorization due to configuration') def test_skip_atmos_cmorization(self): - """Test atmos cmorization flag disabled option """ + """Test atmos cmorization flag disabled option""" self.data_manager.config.cmor.atmosphere = False if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -206,7 +222,7 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() def test_skip_when_cmorized(self): - """Test cmorization skipped if already done """ + """Test cmorization skipped if already done""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.is_cmorized.return_value = True self._test_ocean_cmor(message='No need to unpack file 1/1') @@ -218,14 +234,14 @@ class TestCmorizer(TestCase): self._test_ocean_cmor(message='No need to unpack file 1/1') def test_force(self): - """Test cmorization force works """ + """Test cmorization force works""" self._create_ocean_files('expid_1d_19900101_19900131.nc', 'MMO_19900101-19900131.tar') self.data_manager.is_cmorized.return_value = True self.data_manager.config.cmor.force = True self._test_ocean_cmor() def test_ocean_cmorization_no_files(self): - """Test ocean cmorization report error if no input data """ + """Test ocean cmorization report error if no input data""" self._test_ocean_cmor(success=False, error=True) def test_ocean_cmorization_not_vars_requested(self): -- GitLab From 7129ef50c305a1e5e47defb9c3b4a3647f389394 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 12:10:20 +0200 Subject: [PATCH 26/30] Removed duplicated functions --- test/integration/test_cmorizer.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index ce1d6cb7..dfa58df2 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -311,36 +311,6 @@ class TestCmorizer(TestCase): self._create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') self._test_atmos_cmor(check_vars={'var1': True, 'var2': True}) - def _create_mma_files(self, filename, tar_name, gzip=False): - folder_path = os.path.join(self.data_manager.config.data_dir, 'expid', 'original_files', '19900101', 'member', - 'outputs') - filepath_gg, filename_gg = self._create_file(folder_path, filename.replace('??', 'GG'), gzip) - filepath_sh, filename_sh = self._create_file(folder_path, filename.replace('??', 'SH'), gzip) - - tar = tarfile.TarFile(os.path.join(folder_path, tar_name), mode='w') - tar.add(filepath_gg, arcname=filename_gg, recursive=False) - tar.add(filepath_sh, arcname=filename_sh, recursive=False) - tar.close() - os.remove(filepath_gg) - os.remove(filepath_sh) - - def _create_file(self, folder_path, filename, gzip): - var1 = self._create_sample_cube('Variable 1', 'var1', threed=False, time_bounds=True) - var2 = self._create_sample_cube('Variable 2', 'var2', threed=True, time_bounds=True) - if not os.path.isdir(folder_path): - os.makedirs(folder_path) - file_path = os.path.join(folder_path, filename) - iris.save((var1, var2), file_path, zlib=True) - if gzip: - import subprocess - process = subprocess.Popen(('gzip', file_path), stdout=subprocess.PIPE) - comunicate = process.communicate() - file_path = "{0}.gz".format(file_path) - filename = "{0}.gz".format(filename) - if process.returncode != 0: - raise Exception('Can not compress: {0}'.format(comunicate)) - return file_path, filename - def test_skip_when_not_requested_mma(self): """Test atmos cmorization is skipped if chunk is not requested""" self._create_mma_files('MMA_1d_??_19900101_19900131.nc', 'MMA_expid_19901101_fc0_19900101-19900131.tar') -- GitLab From 9fe1cac3dd980e4845d875f6b764f5d8b925c544 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 12:42:30 +0200 Subject: [PATCH 27/30] Fixed tests --- test/integration/test_cmorizer.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index dfa58df2..28fac720 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -164,15 +164,15 @@ class TestCmorizer(TestCase): """Clean up after tests""" shutil.rmtree(self.tmp_dir) - def _test_ocean_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars=None): + def _test_ocean_cmor(self, success=True, error=False, critical=False, warnings=False, message='', check_vars=None): self._test_cmorization(success=success, error=error, critical=critical, warnings=warnings, message=message, ocean=True, check_vars=check_vars) - def _test_atmos_cmor(self, success=True, error=False, critical=False, warnings=True, message='', check_vars=None): + def _test_atmos_cmor(self, success=True, error=False, critical=False, warnings=False, message='', check_vars=None): self._test_cmorization(success=success, error=error, critical=critical, warnings=warnings, message=message, ocean=False, check_vars=check_vars) - def _test_cmorization(self, success=True, error=False, critical=False, warnings=True, message='', ocean=True, + def _test_cmorization(self, success=True, error=False, critical=False, warnings=False, message='', ocean=True, check_vars=None): if six.PY3: with self.assertLogs(log.Log.log) as cmd: @@ -184,12 +184,15 @@ class TestCmorizer(TestCase): if message: self.assertTrue([record for record in cmd.records if record.message == message]) else: - for level, value in {log.Log.RESULT: success, log.Log.ERROR: error, log.Log.CRITICAL: critical, - log.Log.WARNING: warnings}: - if value: - self.assertTrue([record for record in cmd.records if record.levelno == level]) - else: - self.assertFalse([record for record in cmd.records if record.levelno == level]) + for level, value in six.iteritems({log.Log.RESULT: success, log.Log.ERROR: error, + log.Log.CRITICAL: critical, log.Log.WARNING: warnings}): + try: + if value: + self.assertTrue([record for record in cmd.records if record.levelno == level]) + else: + self.assertFalse([record for record in cmd.records if record.levelno == level]) + except: + raise else: cmorizer = Cmorizer(self.data_manager, '19900101', 0) if ocean: -- GitLab From 7766608794894544da7fbd80e68a84705cfabe06 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 12:49:15 +0200 Subject: [PATCH 28/30] Remove debug code --- test/integration/test_cmorizer.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index 28fac720..b845c02b 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -186,13 +186,10 @@ class TestCmorizer(TestCase): else: for level, value in six.iteritems({log.Log.RESULT: success, log.Log.ERROR: error, log.Log.CRITICAL: critical, log.Log.WARNING: warnings}): - try: - if value: - self.assertTrue([record for record in cmd.records if record.levelno == level]) - else: - self.assertFalse([record for record in cmd.records if record.levelno == level]) - except: - raise + if value: + self.assertTrue([record for record in cmd.records if record.levelno == level]) + else: + self.assertFalse([record for record in cmd.records if record.levelno == level]) else: cmorizer = Cmorizer(self.data_manager, '19900101', 0) if ocean: -- GitLab From 76a5e161c1820ff7ae662c33251ec6aea25c4c48 Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 15:24:15 +0200 Subject: [PATCH 29/30] Removed codacy warning --- test/integration/test_cmorizer.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/integration/test_cmorizer.py b/test/integration/test_cmorizer.py index b845c02b..bcb41e72 100644 --- a/test/integration/test_cmorizer.py +++ b/test/integration/test_cmorizer.py @@ -174,6 +174,15 @@ class TestCmorizer(TestCase): def _test_cmorization(self, success=True, error=False, critical=False, warnings=False, message='', ocean=True, check_vars=None): + self._check_logs(critical, error, message, ocean, success, warnings) + if check_vars: + for variable, status in six.iteritems(check_vars): + if status: + self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) + else: + self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) + + def _check_logs(self, critical, error, message, ocean, success, warnings): if six.PY3: with self.assertLogs(log.Log.log) as cmd: cmorizer = Cmorizer(self.data_manager, '19900101', 0) @@ -196,12 +205,6 @@ class TestCmorizer(TestCase): cmorizer.cmorize_ocean() else: cmorizer.cmorize_atmos() - if check_vars: - for variable, status in six.iteritems(check_vars): - if status: - self.assertTrue(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) - else: - self.assertFalse(os.path.isfile(os.path.join(self.tmp_dir, variable, '{}.nc'.format(variable)))) def test_skip_ocean_cmorization(self): """Test ocean cmorization flag disabled option""" -- GitLab From a2b61e2080bd63b135bbf2328b1ca55a310c713a Mon Sep 17 00:00:00 2001 From: Javier Vegas-Regidor Date: Mon, 11 Jun 2018 15:24:51 +0200 Subject: [PATCH 30/30] Removed cdftoolspython --- earthdiagnostics/cdftoolspython.so | Bin 294192 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 earthdiagnostics/cdftoolspython.so diff --git a/earthdiagnostics/cdftoolspython.so b/earthdiagnostics/cdftoolspython.so deleted file mode 100755 index 2d3916295e882a4723b0fdda32ceab0ed0d26105..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294192 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 -- GitLab