Skip to content

Commit dc0c441

Browse files
committed
Add integration tests for managed attribute load+save.
1 parent 406ae39 commit dc0c441

File tree

2 files changed

+374
-0
lines changed

2 files changed

+374
-0
lines changed
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Copyright Iris contributors
2+
#
3+
# This file is part of Iris and is released under the BSD license.
4+
# See LICENSE in the root of the repository for full licensing details.
5+
"""Integration tests for netcdf loading of attributes with "special" handling."""
6+
7+
import warnings
8+
9+
from iris_grib.grib_phenom_translation._gribcode import (
10+
GenericConcreteGRIBCode,
11+
GRIBCode,
12+
)
13+
import netCDF4 as nc
14+
import numpy as np
15+
import pytest
16+
17+
import iris
18+
from iris.cube import Cube
19+
from iris.fileformats.pp import STASH
20+
21+
22+
@pytest.fixture(autouse=True, scope="session")
23+
def iris_futures():
24+
iris.FUTURE.save_split_attrs = True
25+
26+
27+
class LoadTestCommon:
28+
@pytest.fixture(autouse=True)
29+
def tmp_filepath(self, tmp_path_factory):
30+
tmp_dir = tmp_path_factory.mktemp("tmp_nc")
31+
# We can reuse the same path all over, as it is recreated for each test.
32+
self.tmp_ncpath = tmp_dir / "tmp.nc"
33+
yield
34+
35+
def _check_load_inner(self, iris_name, nc_name, value):
36+
# quickly create a valid netcdf file with a simple cube in it.
37+
cube = Cube([1], var_name="x")
38+
# Save : NB can fail
39+
iris.save(cube, self.tmp_ncpath)
40+
# Reopen for updating with netcdf
41+
ds = nc.Dataset(self.tmp_ncpath, "r+")
42+
# Add the test attribute content.
43+
ds.variables["x"].setncattr(nc_name, value)
44+
ds.close()
45+
# Now load back + see what Iris loader makes of the attribute value.
46+
cube = iris.load_cube(self.tmp_ncpath, "x")
47+
# NB can be absent -> None result.
48+
result = cube.attributes.get(iris_name)
49+
return result
50+
51+
_LOAD_FAIL_MSG = "Invalid content for attribute.* set to.* untranslated raw value"
52+
53+
54+
class TestStash(LoadTestCommon):
55+
def _check_load(self, value):
56+
return self._check_load_inner("STASH", "um_stash_source", value)
57+
58+
def test_simple_object(self):
59+
stash_string = "m01s02i324"
60+
result = self._check_load(stash_string)
61+
assert isinstance(result, STASH)
62+
assert result == STASH(1, 2, 324)
63+
64+
def test_alternate_format(self):
65+
stash_string = " m1s2i3 " # slight tolerance in STASH conversion function
66+
result = self._check_load(stash_string)
67+
assert isinstance(result, STASH)
68+
assert result == STASH(1, 2, 3)
69+
70+
def test_bad_string__fail(self):
71+
stash_string = "xxx"
72+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
73+
result = self._check_load(stash_string)
74+
assert result == "xxx"
75+
76+
def test_empty_string__fail(self):
77+
stash_string = ""
78+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
79+
result = self._check_load(stash_string)
80+
assert result == ""
81+
82+
def test_numeric_value__fail(self):
83+
value = 3
84+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
85+
result = self._check_load(value)
86+
# written directly, comes back as an array scalar of int64
87+
assert result.dtype == np.int64
88+
assert result == 3
89+
90+
def test_tuple_value__fail(self):
91+
# As they cast to arrays, we expect lists to behave the same
92+
value = (2, 3, 7)
93+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
94+
result = self._check_load(value)
95+
# written directly, comes back as an array of int64, shape (3,)
96+
assert result.dtype == np.int64
97+
assert result.shape == (3,)
98+
assert np.all(result == value)
99+
100+
101+
class TestUkmoProcessFlags(LoadTestCommon):
102+
def _check_load(self, value):
103+
return self._check_load_inner(
104+
"ukmo__process_flags", "ukmo__process_flags", value
105+
)
106+
107+
def test_simple_string(self):
108+
flag_string = "one two"
109+
result = self._check_load(flag_string)
110+
assert isinstance(result, tuple)
111+
assert result == ("one", "two")
112+
113+
def test_alternate_string(self):
114+
string = " one two t hree " # merges multiple separators
115+
result = self._check_load(string)
116+
assert result == ("", "one", "two", "", "t", "", "", "hree", "")
117+
118+
def test_special_elements(self):
119+
string = "one <EMPTY> two_three"
120+
result = self._check_load(string)
121+
assert result == ("one", "", "two three")
122+
123+
def test_empty_string(self):
124+
string = ""
125+
# This is NOT an error
126+
with warnings.catch_warnings():
127+
warnings.simplefilter("error")
128+
result = self._check_load(string)
129+
assert result == ()
130+
131+
def test_numeric_value__fail(self):
132+
value = 3
133+
# Note: not a failure, because conversion forces to a string ...
134+
result = self._check_load(value)
135+
# ... but the answer isn't what you might want.
136+
assert result == ("3",)
137+
138+
def test_tuple_value__fail(self):
139+
value = (2, 3, 7)
140+
result = self._check_load(value)
141+
# force to a string, the result is not pretty !
142+
assert result == ("[2", "3", "7]")
143+
144+
145+
class TestGribParam(LoadTestCommon):
146+
def _check_load(self, value):
147+
return self._check_load_inner("GRIB_PARAM", "GRIB_PARAM", value)
148+
149+
def test_standard_string(self):
150+
string = "GRIBCode(edition=1, table_version=2, centre_number=3, number=4)"
151+
result = self._check_load(string)
152+
assert isinstance(result, GenericConcreteGRIBCode)
153+
assert result == GRIBCode(1, 2, 3, 4)
154+
155+
def test_confused_string(self):
156+
string = "GRIBCode(edition=1, centre=2, nonsense=3, table_version=4)"
157+
result = self._check_load(string)
158+
assert isinstance(result, GenericConcreteGRIBCode)
159+
assert result == GRIBCode(1, 2, 3, 4)
160+
161+
def test_alternate_format_string(self):
162+
string = "grib(1, 2, 3, 4)"
163+
result = self._check_load(string)
164+
assert isinstance(result, GenericConcreteGRIBCode)
165+
assert result == GRIBCode(1, 2, 3, 4)
166+
167+
def test_minimal_string(self):
168+
string = "1 2 3 4"
169+
result = self._check_load(string)
170+
assert isinstance(result, GenericConcreteGRIBCode)
171+
assert result == GRIBCode(1, 2, 3, 4)
172+
173+
def test_invalid_string__fail(self):
174+
string = "grib(1, 2, 3)"
175+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
176+
result = self._check_load(string)
177+
assert result == string
178+
179+
def test_junk_string__fail(self):
180+
string = "xxx"
181+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
182+
result = self._check_load(string)
183+
assert result == string
184+
185+
def test_empty_string__fail(self):
186+
string = ""
187+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
188+
result = self._check_load(string)
189+
assert result == string
190+
191+
def test_numeric_value__fail(self):
192+
value = 3
193+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
194+
result = self._check_load(value)
195+
# written directly, comes back as an array scalar of int64
196+
assert result.dtype == np.int64
197+
assert result == 3
198+
199+
def test_tuple_value__fail(self):
200+
# As they cast to arrays, we expect lists to behave the same
201+
value = (2, 3, 7)
202+
with pytest.warns(UserWarning, match=self._LOAD_FAIL_MSG):
203+
result = self._check_load(value)
204+
# written directly, comes back as an array of int64, shape (3,)
205+
assert result.dtype == np.int64
206+
assert result.shape == (3,)
207+
assert np.all(result == value)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Copyright Iris contributors
2+
#
3+
# This file is part of Iris and is released under the BSD license.
4+
# See LICENSE in the root of the repository for full licensing details.
5+
"""Integration tests for netcdf saving of attributes with "special" handling."""
6+
7+
from iris_grib.grib_phenom_translation._gribcode import GRIBCode
8+
import netCDF4 as nc
9+
import pytest
10+
11+
import iris
12+
from iris.cube import Cube
13+
from iris.fileformats.pp import STASH
14+
15+
16+
@pytest.fixture(autouse=True, scope="session")
17+
def iris_futures():
18+
iris.FUTURE.save_split_attrs = True
19+
20+
21+
class SaveTestCommon:
22+
@pytest.fixture(autouse=True)
23+
def tmp_filepath(self, tmp_path_factory):
24+
tmp_dir = tmp_path_factory.mktemp("tmp_nc")
25+
# We can reuse the same path all over, as it is recreated for each test.
26+
self.tmp_ncpath = tmp_dir / "tmp.nc"
27+
yield
28+
29+
def _check_save_inner(self, iris_name, nc_name, value):
30+
cube = Cube([1], var_name="x", attributes={iris_name: value})
31+
# Save : NB can fail
32+
iris.save(cube, self.tmp_ncpath)
33+
ds = nc.Dataset(self.tmp_ncpath)
34+
result = ds.variables["x"].getncattr(nc_name)
35+
return result
36+
37+
38+
class TestStash(SaveTestCommon):
39+
def _check_save(self, value):
40+
return self._check_save_inner("STASH", "um_stash_source", value)
41+
42+
def test_simple_object(self):
43+
stash = STASH(1, 2, 324)
44+
result = self._check_save(stash)
45+
assert result == "m01s02i324"
46+
47+
def test_simple_string(self):
48+
stash_str = "m1s2i324"
49+
result = self._check_save(stash_str)
50+
assert result == "m01s02i324"
51+
52+
def test_bad_string__fail(self):
53+
bad_str = "xxx"
54+
with pytest.warns(UserWarning, match="Invalid value in managed.* attribute"):
55+
result = self._check_save(bad_str)
56+
assert result == "xxx"
57+
58+
def test_empty_string__fail(self):
59+
with pytest.warns(UserWarning, match="Invalid value in managed.* attribute"):
60+
result = self._check_save("")
61+
assert result == ""
62+
63+
def test_bad_object__fail(self):
64+
with pytest.warns(UserWarning, match="Invalid value in managed.* attribute"):
65+
result = self._check_save({})
66+
assert result == "{}"
67+
68+
def test_none_object__fail(self):
69+
with pytest.warns(UserWarning, match="Invalid value in managed.* attribute"):
70+
result = self._check_save(None)
71+
assert result == "None"
72+
73+
74+
class TestUkmoProcessFlags(SaveTestCommon):
75+
def _check_save(self, value):
76+
return self._check_save_inner(
77+
"ukmo__process_flags", "ukmo__process_flags", value
78+
)
79+
80+
def test_simple_object(self):
81+
flags = ("one", "two")
82+
result = self._check_save(flags)
83+
assert result == "one two"
84+
85+
def test_simple_string(self):
86+
string = "one two three"
87+
result = self._check_save(string)
88+
assert result == string
89+
90+
def test_empty_tuple(self):
91+
obj = ()
92+
result = self._check_save(obj)
93+
assert result == ""
94+
95+
def test_string_w_spaces(self):
96+
obj = ("one", "two three")
97+
result = self._check_save(obj)
98+
assert result == "one two_three"
99+
100+
def test_string_w_underscores(self):
101+
obj = ("one", "two_three")
102+
result = self._check_save(obj)
103+
assert result == "one two_three"
104+
105+
def test_tuple_w_empty_string(self):
106+
obj = ("one", "", "two")
107+
result = self._check_save(obj)
108+
assert result == "one <EMPTY> two"
109+
110+
def test_bad_object(self):
111+
obj = {}
112+
with pytest.warns(UserWarning, match="Invalid value in managed.* attribute"):
113+
result = self._check_save(obj)
114+
assert result == "{}"
115+
116+
def test_none_object(self):
117+
obj = None
118+
with pytest.warns(UserWarning, match="Invalid value in managed.* attribute"):
119+
result = self._check_save(obj)
120+
assert result == "None"
121+
122+
123+
class TestGribParam(SaveTestCommon):
124+
def _check_save(self, value):
125+
return self._check_save_inner("GRIB_PARAM", "GRIB_PARAM", value)
126+
127+
def test_simple_object(self):
128+
code = GRIBCode(1, 2, 3, 4)
129+
result = self._check_save(code)
130+
assert (
131+
result == "GRIBCode(edition=1, table_version=2, centre_number=3, number=4)"
132+
)
133+
134+
def test_simple_string(self):
135+
code_string = "1, 2, 3,,, 4" # the converter is highly tolerant
136+
result = self._check_save(code_string)
137+
assert (
138+
result == "GRIBCode(edition=1, table_version=2, centre_number=3, number=4)"
139+
)
140+
141+
_encode_fail_msg = (
142+
r"Invalid value in managed.* attribute.* set to raw \(string\) value"
143+
)
144+
145+
def test_bad_string_toofew__fail(self):
146+
code_string = "1, 2, 3"
147+
with pytest.warns(UserWarning, match=self._encode_fail_msg):
148+
result = self._check_save(code_string)
149+
assert result == "1, 2, 3"
150+
151+
def test_bad_string_junk__fail(self):
152+
code_string = "xxx"
153+
with pytest.warns(UserWarning, match=self._encode_fail_msg):
154+
result = self._check_save(code_string)
155+
assert result == "xxx"
156+
157+
def test_bad_object__fail(self):
158+
obj = {}
159+
with pytest.warns(UserWarning, match=self._encode_fail_msg):
160+
result = self._check_save(obj)
161+
assert result == "{}"
162+
163+
def test_none_object__fail(self):
164+
obj = None
165+
with pytest.warns(UserWarning, match=self._encode_fail_msg):
166+
result = self._check_save(obj)
167+
assert result == "None"

0 commit comments

Comments
 (0)