Skip to content

Commit c7ee038

Browse files
authored
Port test_grib_load_translations from iris (#191)
* Imported test; initial working with minimal changes. * Remove some redundant parts, as we now test only GRIB-1. * Code style fixes.
1 parent a09fb02 commit c7ee038

File tree

1 file changed

+384
-0
lines changed

1 file changed

+384
-0
lines changed
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
# Copyright iris-grib contributors
2+
#
3+
# This file is part of iris-grib and is released under the LGPL license.
4+
# See COPYING and COPYING.LESSER in the root of the repository for full
5+
# licensing details.
6+
"""
7+
Tests for specific implementation aspects of the grib loaders.
8+
Old, and GRIB-1 specific.
9+
Ported here from 'iris.tests.test_grib_load_translations'.
10+
11+
"""
12+
13+
# Import iris_grib.tests first so that some things can be initialised before
14+
# importing anything else
15+
import iris_grib.tests as tests
16+
17+
import datetime
18+
from unittest import mock
19+
20+
import cf_units
21+
import numpy as np
22+
23+
import iris
24+
import iris.exceptions
25+
26+
import gribapi
27+
import iris.fileformats
28+
import iris_grib
29+
30+
31+
def _mock_gribapi_fetch(message, key):
32+
"""
33+
Fake the gribapi key-fetch.
34+
35+
Fetch key-value from the fake message (dictionary).
36+
If the key is not present, raise the diagnostic exception.
37+
38+
"""
39+
if key in message:
40+
return message[key]
41+
else:
42+
raise _mock_gribapi.errors.GribInternalError
43+
44+
45+
def _mock_gribapi__grib_is_missing(grib_message, keyname):
46+
"""
47+
Fake the gribapi key-existence enquiry.
48+
49+
Return whether the key exists in the fake message (dictionary).
50+
51+
"""
52+
return keyname not in grib_message
53+
54+
55+
def _mock_gribapi__grib_get_native_type(grib_message, keyname):
56+
"""
57+
Fake the gribapi type-discovery operation.
58+
59+
Return type of key-value in the fake message (dictionary).
60+
If the key is not present, raise the diagnostic exception.
61+
62+
"""
63+
if keyname in grib_message:
64+
return type(grib_message[keyname])
65+
raise _mock_gribapi.errors.GribInternalError(keyname)
66+
67+
68+
# Construct a mock object to mimic the gribapi for GribWrapper testing.
69+
_mock_gribapi = mock.Mock(spec=gribapi)
70+
_mock_gribapi.errors.GribInternalError = Exception
71+
72+
_mock_gribapi.grib_get_long = mock.Mock(side_effect=_mock_gribapi_fetch)
73+
_mock_gribapi.grib_get_string = mock.Mock(side_effect=_mock_gribapi_fetch)
74+
_mock_gribapi.grib_get_double = mock.Mock(side_effect=_mock_gribapi_fetch)
75+
_mock_gribapi.grib_get_double_array = mock.Mock(
76+
side_effect=_mock_gribapi_fetch
77+
)
78+
_mock_gribapi.grib_is_missing = mock.Mock(
79+
side_effect=_mock_gribapi__grib_is_missing
80+
)
81+
_mock_gribapi.grib_get_native_type = mock.Mock(
82+
side_effect=_mock_gribapi__grib_get_native_type
83+
)
84+
85+
# define seconds in an hour, for general test usage
86+
_hour_secs = 3600.0
87+
88+
89+
class FakeGribMessage(dict):
90+
"""
91+
A 'fake grib message' object, for testing GribWrapper construction.
92+
93+
Behaves as a dictionary, containing key-values for message keys.
94+
95+
"""
96+
97+
def __init__(self, **kwargs):
98+
"""
99+
Create a fake message object.
100+
101+
General keys can be set/add as required via **kwargs.
102+
The keys 'edition' and 'time_code' are specially managed.
103+
104+
"""
105+
# Start with a bare dictionary
106+
dict.__init__(self)
107+
# Extract specially-recognised keys.
108+
edition = kwargs.pop("edition", 1)
109+
# This testing is only for old-style Grib-1 code.
110+
assert edition == 1
111+
time_code = kwargs.pop("time_code", None)
112+
# Set the minimally required keys.
113+
self._init_minimal_message(edition=edition)
114+
# Also set a time-code, if given.
115+
if time_code is not None:
116+
self.set_timeunit_code(time_code)
117+
# Finally, add any remaining passed key-values.
118+
self.update(**kwargs)
119+
120+
def _init_minimal_message(self, edition=1):
121+
# Set values for all the required keys.
122+
# 'edition' controls the edition-specific keys.
123+
self.update(
124+
{
125+
"Ni": 1,
126+
"Nj": 1,
127+
"numberOfValues": 1,
128+
"alternativeRowScanning": 0,
129+
"centre": "ecmf",
130+
"year": 2007,
131+
"month": 3,
132+
"day": 23,
133+
"hour": 12,
134+
"minute": 0,
135+
"indicatorOfUnitOfTimeRange": 1,
136+
"shapeOfTheEarth": 6,
137+
"gridType": "rotated_ll",
138+
"angleOfRotation": 0.0,
139+
"iDirectionIncrementInDegrees": 0.036,
140+
"jDirectionIncrementInDegrees": 0.036,
141+
"iScansNegatively": 0,
142+
"jScansPositively": 1,
143+
"longitudeOfFirstGridPointInDegrees": -5.70,
144+
"latitudeOfFirstGridPointInDegrees": -4.452,
145+
"jPointsAreConsecutive": 0,
146+
"values": np.array([[1.0]]),
147+
"indicatorOfParameter": 9999,
148+
"parameterNumber": 9999,
149+
"startStep": 24,
150+
"timeRangeIndicator": 1,
151+
"P1": 2,
152+
"P2": 0,
153+
# time unit - needed AS WELL as 'indicatorOfUnitOfTimeRange'
154+
"unitOfTime": 1,
155+
"table2Version": 9999,
156+
}
157+
)
158+
# Add edition-dependent settings.
159+
self["edition"] = edition
160+
161+
def set_timeunit_code(self, timecode):
162+
# Do timecode setting (somewhat edition-dependent).
163+
self["indicatorOfUnitOfTimeRange"] = timecode
164+
# for some odd reason, GRIB1 code uses *both* of these
165+
# NOTE kludge -- the 2 keys are really the same thing
166+
self["unitOfTime"] = timecode
167+
168+
169+
class TestGribTimecodes(tests.IrisTest):
170+
def _run_timetests(self, test_set):
171+
# Check the unit-handling for given units-codes and editions.
172+
173+
# Operates on lists of cases for various time-units and grib-editions.
174+
# Format: (edition, code, expected-exception,
175+
# equivalent-seconds, description-string)
176+
with mock.patch("iris_grib.gribapi", _mock_gribapi):
177+
for test_controls in test_set:
178+
(
179+
grib_edition,
180+
timeunit_codenum,
181+
expected_error,
182+
timeunit_secs,
183+
timeunit_str,
184+
) = test_controls
185+
186+
# Construct a suitable fake test message.
187+
message = FakeGribMessage(
188+
edition=grib_edition, time_code=timeunit_codenum
189+
)
190+
191+
if expected_error:
192+
# Expect GribWrapper construction to fail.
193+
with self.assertRaises(type(expected_error)) as ar_context:
194+
_ = iris_grib.GribWrapper(message)
195+
self.assertEqual(
196+
ar_context.exception.args, expected_error.args
197+
)
198+
continue
199+
200+
# 'ELSE'...
201+
# Expect the wrapper construction to work.
202+
# Make a GribWrapper object and test it.
203+
wrapped_msg = iris_grib.GribWrapper(message)
204+
205+
# Check the units string.
206+
forecast_timeunit = wrapped_msg._forecastTimeUnit
207+
self.assertEqual(
208+
forecast_timeunit,
209+
timeunit_str,
210+
"Bad unit string for edition={ed:01d}, "
211+
"unitcode={code:01d} : "
212+
'expected="{wanted}" GOT="{got}"'.format(
213+
ed=grib_edition,
214+
code=timeunit_codenum,
215+
wanted=timeunit_str,
216+
got=forecast_timeunit,
217+
),
218+
)
219+
220+
# Check the data-starttime calculation.
221+
interval_start_to_end = (
222+
wrapped_msg._phenomenonDateTime -
223+
wrapped_msg._referenceDateTime
224+
)
225+
if grib_edition == 1:
226+
interval_from_units = wrapped_msg.P1
227+
else:
228+
interval_from_units = wrapped_msg.forecastTime
229+
interval_from_units *= datetime.timedelta(0, timeunit_secs)
230+
self.assertEqual(
231+
interval_start_to_end,
232+
interval_from_units,
233+
"Inconsistent start time offset for edition={ed:01d}, "
234+
"unitcode={code:01d} : "
235+
'from-unit="{unit_str}" '
236+
'from-phenom-minus-ref="{e2e_str}"'.format(
237+
ed=grib_edition,
238+
code=timeunit_codenum,
239+
unit_str=interval_from_units,
240+
e2e_str=interval_start_to_end,
241+
),
242+
)
243+
244+
# Test groups of testcases for various time-units and grib-editions.
245+
# Format: (edition, code, expected-exception,
246+
# equivalent-seconds, description-string)
247+
def test_timeunits_common(self):
248+
tests = (
249+
(1, 0, None, 60.0, "minutes"),
250+
(1, 1, None, _hour_secs, "hours"),
251+
(1, 2, None, 24.0 * _hour_secs, "days"),
252+
(1, 10, None, 3.0 * _hour_secs, "3 hours"),
253+
(1, 11, None, 6.0 * _hour_secs, "6 hours"),
254+
(1, 12, None, 12.0 * _hour_secs, "12 hours"),
255+
)
256+
TestGribTimecodes._run_timetests(self, tests)
257+
258+
@staticmethod
259+
def _err_bad_timeunit(code):
260+
return iris.exceptions.NotYetImplementedError(
261+
"Unhandled time unit for forecast "
262+
"indicatorOfUnitOfTimeRange : {code}".format(code=code)
263+
)
264+
265+
def test_timeunits_grib1_specific(self):
266+
tests = (
267+
(1, 13, None, 0.25 * _hour_secs, "15 minutes"),
268+
(1, 14, None, 0.5 * _hour_secs, "30 minutes"),
269+
(1, 254, None, 1.0, "seconds"),
270+
(1, 111, TestGribTimecodes._err_bad_timeunit(111), 1.0, "??"),
271+
)
272+
TestGribTimecodes._run_timetests(self, tests)
273+
274+
def test_timeunits_calendar(self):
275+
tests = (
276+
(1, 3, TestGribTimecodes._err_bad_timeunit(3), 0.0, "months"),
277+
(1, 4, TestGribTimecodes._err_bad_timeunit(4), 0.0, "years"),
278+
(1, 5, TestGribTimecodes._err_bad_timeunit(5), 0.0, "decades"),
279+
(1, 6, TestGribTimecodes._err_bad_timeunit(6), 0.0, "30 years"),
280+
(1, 7, TestGribTimecodes._err_bad_timeunit(7), 0.0, "centuries"),
281+
)
282+
TestGribTimecodes._run_timetests(self, tests)
283+
284+
def test_timeunits_invalid(self):
285+
tests = (
286+
(1, 111, TestGribTimecodes._err_bad_timeunit(111), 1.0, "??"),
287+
)
288+
TestGribTimecodes._run_timetests(self, tests)
289+
290+
def test_warn_unknown_pdts(self):
291+
# Test loading of an unrecognised GRIB Product Definition Template.
292+
293+
# Get a temporary file by name (deleted afterward by context).
294+
with self.temp_filename() as temp_gribfile_path:
295+
# Write a test grib message to the temporary file.
296+
with open(temp_gribfile_path, "wb") as temp_gribfile:
297+
grib_message = gribapi.grib_new_from_samples("GRIB2")
298+
# Set the PDT to something unexpected.
299+
gribapi.grib_set_long(
300+
grib_message, "productDefinitionTemplateNumber", 5
301+
)
302+
gribapi.grib_write(grib_message, temp_gribfile)
303+
304+
# Load the message from the file as a cube.
305+
cube_generator = iris_grib.load_cubes(temp_gribfile_path)
306+
with self.assertRaises(iris.exceptions.TranslationError) as te:
307+
_ = next(cube_generator)
308+
self.assertEqual(
309+
"Product definition template [5]" " is not supported",
310+
str(te.exception),
311+
)
312+
313+
314+
class TestGrib1LoadPhenomenon(tests.IrisTest):
315+
# Test recognition of grib phenomenon types.
316+
def mock_grib(self):
317+
grib = mock.Mock()
318+
grib.edition = 1
319+
grib.startStep = 0
320+
grib.phenomenon_points = lambda unit: 3
321+
grib._forecastTimeUnit = "hours"
322+
grib.productDefinitionTemplateNumber = 0
323+
# define a level type (NB these 2 are effectively the same)
324+
grib.levelType = 1
325+
grib.typeOfFirstFixedSurface = 1
326+
grib.typeOfSecondFixedSurface = 1
327+
return grib
328+
329+
def cube_from_message(self, grib):
330+
# Parameter translation now uses the GribWrapper, so we must convert
331+
# the Mock-based fake message to a FakeGribMessage.
332+
with mock.patch("iris_grib.gribapi", _mock_gribapi):
333+
grib_message = FakeGribMessage(**grib.__dict__)
334+
wrapped_msg = iris_grib.GribWrapper(grib_message)
335+
cube, _, _ = iris.fileformats.rules._make_cube(
336+
wrapped_msg, iris_grib._grib1_load_rules.grib1_convert
337+
)
338+
return cube
339+
340+
def test_grib1_unknownparam(self):
341+
grib = self.mock_grib()
342+
grib.table2Version = 0
343+
grib.indicatorOfParameter = 9999
344+
cube = self.cube_from_message(grib)
345+
self.assertEqual(cube.standard_name, None)
346+
self.assertEqual(cube.long_name, None)
347+
self.assertEqual(cube.units, cf_units.Unit("???"))
348+
349+
def test_grib1_unknown_local_param(self):
350+
grib = self.mock_grib()
351+
grib.table2Version = 128
352+
grib.indicatorOfParameter = 999
353+
cube = self.cube_from_message(grib)
354+
self.assertEqual(cube.standard_name, None)
355+
self.assertEqual(cube.long_name, "UNKNOWN LOCAL PARAM 999.128")
356+
self.assertEqual(cube.units, cf_units.Unit("???"))
357+
358+
def test_grib1_unknown_standard_param(self):
359+
grib = self.mock_grib()
360+
grib.table2Version = 1
361+
grib.indicatorOfParameter = 975
362+
cube = self.cube_from_message(grib)
363+
self.assertEqual(cube.standard_name, None)
364+
self.assertEqual(cube.long_name, "UNKNOWN LOCAL PARAM 975.1")
365+
self.assertEqual(cube.units, cf_units.Unit("???"))
366+
367+
def known_grib1(self, param, standard_str, units_str):
368+
grib = self.mock_grib()
369+
grib.table2Version = 1
370+
grib.indicatorOfParameter = param
371+
cube = self.cube_from_message(grib)
372+
self.assertEqual(cube.standard_name, standard_str)
373+
self.assertEqual(cube.long_name, None)
374+
self.assertEqual(cube.units, cf_units.Unit(units_str))
375+
376+
def test_grib1_known_standard_params(self):
377+
# at present, there are just a very few of these
378+
self.known_grib1(11, "air_temperature", "kelvin")
379+
self.known_grib1(33, "x_wind", "m s-1")
380+
self.known_grib1(34, "y_wind", "m s-1")
381+
382+
383+
if __name__ == "__main__":
384+
tests.main()

0 commit comments

Comments
 (0)