-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtest_store.py
320 lines (271 loc) · 13 KB
/
test_store.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# MIT License
#
# Copyright (c) 2020-2021 Brockmann Consult GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Unit tests for the xcube CDS store
The CDSStoreTest class in this module contains only "general" tests, i.e.
those not related to a particular dataset. Each supported dataset has its
own test module containing tests specific to that dataset.
Most of the tests use a mocked CDS API client which matches exact requests
and returns a saved result file originally downloaded from the real CDS API.
To create a new unit test of this kind,
1. Write a test which uses the real CDS API.
2. Temporarily add the _save_request_to and _save_file_to arguments to
the open_data call.
3. Create a new subdirectory of test/mock_results, and move the saved request
and results into it (as request.json and result, respectively). The name
of the subdirectory is arbitrary, but it is useful to give it the same
name as the unit test method.
4. Remove the _save_request_to and _save_file_to arguments from the open_data
call, and add a 'client_class=CDSClientMock' argument to the CDSDataOpener
constructor.
"""
import os
import tempfile
import unittest
from collections.abc import Iterator
import xcube
import xcube.core
from xcube.core.store import DataDescriptor
from xcube.core.store import DataStoreError
from xcube.core.store import TYPE_SPECIFIER_CUBE
from xcube.core.store import TYPE_SPECIFIER_DATASET
from test.mocks import CDSClientMock
from xcube_cds.constants import CDS_DATA_OPENER_ID
from xcube_cds.datasets.reanalysis_era5 import ERA5DatasetHandler
from xcube_cds.store import CDSDataOpener
from xcube_cds.store import CDSDataStore
from xcube_cds.store import CDSDatasetHandler
_CDS_API_URL = 'dummy'
_CDS_API_KEY = 'dummy'
class CDSStoreTest(unittest.TestCase):
def test_invalid_data_id(self):
store = CDSDataStore(endpoint_url=_CDS_API_URL,
cds_api_key=_CDS_API_KEY)
with self.assertRaises(ValueError):
store.open_data(
'this-data-id-does-not-exist',
variable_names=['2m_temperature'],
hours=[0], months=[1], years=[2019]
)
def test_list_and_describe_data_ids(self):
store = CDSDataStore(endpoint_url=_CDS_API_URL,
cds_api_key=_CDS_API_KEY)
data_ids = store.get_data_ids(include_attrs=['title'])
self.assertIsInstance(data_ids, Iterator)
for data_id in data_ids:
self.assertIsInstance(data_id, tuple)
self.assertTrue(1 <= len(data_id) <= 2)
self.assertIsInstance(data_id[0], str)
self.assertIsInstance(data_id[1], dict)
descriptor = store.describe_data(data_id[0])
self.assertIsInstance(descriptor, DataDescriptor)
def test_convert_invalid_time_range(self):
with self.assertRaises(ValueError):
CDSDatasetHandler.convert_time_range([]) # incorrect list length
def test_get_open_params_schema_without_data_id(self):
opener = CDSDataOpener(endpoint_url=_CDS_API_URL,
cds_api_key=_CDS_API_KEY)
schema = opener.get_open_data_params_schema()
actual = schema.to_dict()
self.assertCountEqual(['type', 'properties', 'required'], actual.keys())
self.assertEqual('object', actual['type'])
self.assertCountEqual(
['bbox', 'spatial_res', 'variable_names', 'time_range'],
actual['required'])
self.assertCountEqual(
['dataset_name', 'variable_names', 'crs', 'bbox', 'spatial_res',
'time_range', 'time_period'],
actual['properties'].keys()
)
def test_search_data_invalid_id(self):
store = CDSDataStore(endpoint_url=_CDS_API_URL,
cds_api_key=_CDS_API_KEY)
with self.assertRaises(DataStoreError):
store.search_data('This is an invalid ID.')
def test_search_data_valid_id(self):
store = CDSDataStore(endpoint_url=_CDS_API_URL,
cds_api_key=_CDS_API_KEY)
result = list(store.search_data('dataset'))
self.assertTrue(len(result) > 0)
def test_get_data_store_params_schema(self):
self.assertDictEqual({
'type': 'object',
'properties': {
'normalize_names': {'type': 'boolean', 'default': False},
'num_retries': {'type': 'integer', 'default': 200,
'minimum': 0}},
'additionalProperties': False
}, CDSDataStore.get_data_store_params_schema().to_dict())
def test_get_type_specifiers(self):
type_specifiers = CDSDataStore.get_type_specifiers()
self.assertEqual(1, len(type_specifiers))
self.assertIsInstance(type_specifiers[0], str)
self.assertTupleEqual(('dataset[cube]',), type_specifiers)
def test_has_data_false(self):
self.assertFalse(CDSDataStore().has_data('nonexistent data ID'))
def test_get_data_opener_ids_invalid_type_id(self):
with self.assertRaises(DataStoreError):
CDSDataStore().get_data_opener_ids(CDS_DATA_OPENER_ID,
'this is an invalid ID')
def test_get_data_opener_ids_invalid_opener_id(self):
with self.assertRaises(ValueError):
CDSDataStore().get_data_opener_ids('this is an invalid ID',
TYPE_SPECIFIER_DATASET)
def test_get_data_opener_ids_with_default_arguments(self):
self.assertTupleEqual((CDS_DATA_OPENER_ID, ),
CDSDataStore().get_data_opener_ids())
def test_get_store_open_params_schema_without_data_id(self):
self.assertIsInstance(
CDSDataStore().get_open_data_params_schema(),
xcube.util.jsonschema.JsonObjectSchema
)
def test_get_data_ids(self):
store = CDSDataStore(client_class=CDSClientMock,
endpoint_url=_CDS_API_URL,
cds_api_key=_CDS_API_KEY)
self.assertEqual([], list(store.get_data_ids('unsupported_type_spec')))
self.assertEqual([],
list(store.get_data_ids('dataset[unsupported_flag]')))
# The number of available datasets is expected to increase over time,
# so to avoid overfitting the test we just check that more than a few
# datasets and/or cubes are available. "a few" is semi-arbitrarily
# defined to be 5.
minimum_expected_datasets = 5
self.assertGreater(len(list(store.get_data_ids('dataset'))),
minimum_expected_datasets)
self.assertGreater(len(list(store.get_data_ids('dataset[cube]'))),
minimum_expected_datasets)
def test_era5_transform_params_empty_variable_list(self):
handler = ERA5DatasetHandler()
with self.assertRaises(ValueError):
handler.transform_params(
dict(bbox=[0, 0, 10, 10], spatial_res=0.5, variable_names=[]),
'reanalysis-era5-land'
)
class ClientUrlTest(unittest.TestCase):
"""Tests connected with passing CDS API URL and key to opener or store."""
def setUp(self):
self.old_environment = dict(os.environ)
self.temp_dir = tempfile.TemporaryDirectory()
def tearDown(self):
os.environ.clear()
os.environ.update(self.old_environment)
self.temp_dir.cleanup()
def test_client_url_and_key_parameters(self):
"""Test passing URL and key parameters to client constructor
This test verifies that the CDS API URL and key, when specified as
parameters to CDSDataOpener, are correctly passed to the CDS client,
overriding any configuration file or environment variable settings.
"""
self._set_up_api_configuration('wrong URL 1', 'wrong key 1',
'wrong URL 2', 'wrong key 2')
endpoint_url = 'https://example.com/'
cds_api_key = 'xyzzy'
opener = CDSDataOpener(client_class=CDSClientMock,
endpoint_url=endpoint_url,
cds_api_key=cds_api_key)
opener.open_data(
'reanalysis-era5-single-levels-monthly-means:'
'monthly_averaged_reanalysis',
variable_names=['2m_temperature'],
bbox=[-180, -90, 180, 90],
spatial_res=0.25,
time_range=['2015-10-15', '2015-10-15'],
)
client = opener.last_instantiated_client
self.assertEqual(endpoint_url, client.url)
self.assertEqual(cds_api_key, client.key)
def test_client_url_and_key_environment_variables(self):
"""Test passing URL and key parameters via environment variables
This test verifies that the CDS API URL and key, when specified in
environment variables, are correctly passed to the CDS client,
overriding any configuration file settings.
"""
endpoint_url = 'https://example.com/'
cds_api_key = 'xyzzy'
self._set_up_api_configuration('wrong URL 1', 'wrong key 1',
endpoint_url, cds_api_key)
client = self._get_client()
self.assertEqual(endpoint_url, client.url)
self.assertEqual(cds_api_key, client.key)
def test_client_url_and_key_rc_file(self):
"""Test passing URL and key parameters via environment variables
This test verifies that the CDS API URL and key, when specified in
a configuration file, are correctly passed to the CDS client.
"""
endpoint_url = 'https://example.com/'
cds_api_key = 'xyzzy'
self._set_up_api_configuration(endpoint_url, cds_api_key)
opener = CDSDataOpener(client_class=CDSClientMock)
opener.open_data(
'reanalysis-era5-single-levels-monthly-means:'
'monthly_averaged_reanalysis',
variable_names=['2m_temperature'],
bbox=[-180, -90, 180, 90],
spatial_res=0.25,
time_range=['2015-10-15', '2015-10-15'],
)
client = opener.last_instantiated_client
self.assertEqual(endpoint_url, client.url)
self.assertEqual(cds_api_key, client.key)
@staticmethod
def _get_client(**opener_args):
"""Return the client instantiated to open a dataset
Open a dataset and return the client that was instantiated to execute
the CDS API query.
"""
opener = CDSDataOpener(client_class=CDSClientMock, **opener_args)
opener.open_data(
'reanalysis-era5-single-levels-monthly-means:'
'monthly_averaged_reanalysis',
variable_names=['2m_temperature'],
bbox=[-180, -90, 180, 90],
spatial_res=0.25,
time_range=['2015-10-15', '2015-10-15'],
)
return opener.last_instantiated_client
def _set_up_api_configuration(self, url_rc, key_rc,
url_env=None, key_env=None):
"""Set up a configuration file and, optionally, environment variables
The teardown function will take care of the clean-up.
:param url_rc: API URL to be written to configuration file
:param key_rc: API key to be written to configuration file
:param url_env: API URL to be written to environment variable
:param key_env: API key to be written to environment variable
:return: an instantiated CDS client object
"""
rc_path = os.path.join(self.temp_dir.name, "cdsapi.rc")
with open(rc_path, 'w') as fh:
fh.write(f'url: {url_rc}\n')
fh.write(f'key: {key_rc}\n')
ClientUrlTest._erase_environment_variables()
os.environ['CDSAPI_RC'] = rc_path
if url_env is not None:
os.environ['CDSAPI_URL'] = url_env
if key_env is not None:
os.environ['CDSAPI_KEY'] = key_env
@staticmethod
def _erase_environment_variables():
for var in 'CDSAPI_URL', 'CDSAPI_KEY', 'CDSAPI_RC':
if var in os.environ:
del os.environ[var]
if __name__ == '__main__':
unittest.main()