1010import pytest
1111
1212from iris .coords import AuxCoord , DimCoord
13+ import iris .cube
1314from iris .cube import Cube , CubeList
1415from iris .tests ._shared_utils import (
1516 assert_array_equal ,
1920)
2021import iris .tests .stock
2122
23+ _ORIGINAL_MERGE = iris .cube .CubeList .merge
24+ _ORIGINAL_MERGE_CUBE = iris .cube .CubeList .merge_cube
25+
26+ # Testing options for checking that merge works ~same when some inputs are dataless
27+ _DATALESS_TEST_OPTIONS = [
28+ "dataless_none" ,
29+ "dataless_one" ,
30+ "dataless_all" ,
31+ "dataless_allbut1" ,
32+ ]
33+
34+
35+ @pytest .fixture (params = _DATALESS_TEST_OPTIONS )
36+ def dataless_option (request ):
37+ return request .param
38+
39+
40+ def mangle_cubelist (cubelist , dataless_option ):
41+ """Return a modified cubelist, where some cubes are dataless.
42+
43+ 'dataless_option' controls whether 0, 1, N or N-1 cubes are made dataless.
44+ """
45+ assert isinstance (cubelist , CubeList )
46+ n_cubes = len (cubelist )
47+ result = CubeList ([])
48+ ind_one = len (cubelist ) // 3
49+ for i_cube , cube in enumerate (cubelist ):
50+ if (
51+ (dataless_option == "dataless_one" and i_cube == ind_one )
52+ or (dataless_option == "dataless_allbut1" and i_cube != ind_one )
53+ or dataless_option == "dataless_all"
54+ ):
55+ # Make this one dataless
56+ cube = cube .copy ()
57+ cube .data = None
58+
59+ result .append (cube )
60+
61+ # Do a quick post-test
62+ assert len (result ) == len (cubelist )
63+ count = sum ([cube .is_dataless () for cube in result ])
64+ expected = {
65+ "dataless_none" : 0 ,
66+ "dataless_one" : 1 ,
67+ "dataless_all" : n_cubes ,
68+ "dataless_allbut1" : n_cubes - 1 ,
69+ }[dataless_option ]
70+ assert count == expected
71+
72+ return result
73+
74+
75+ def check_merge_against_dataless_cases (
76+ function , original_input , * args , dataless_option = None
77+ ):
78+ # Compute the "normal" result.
79+ original_result = function (original_input , * args )
80+
81+ if dataless_option != "dataless_none" :
82+ # Re-run with "mangled" inputs, and compare the result with the normal case.
83+ mangled_input = mangle_cubelist (original_input , dataless_option )
84+ mangled_result = function (mangled_input , * args )
85+
86+ # Normalise to get a list of cubes
87+ if isinstance (original_result , Cube ): # I.E. not if a single Cube
88+ result_cubes = [original_result ]
89+ mangled_cubes = [mangled_result ]
90+ else :
91+ result_cubes = original_result
92+ mangled_cubes = mangled_result
93+
94+ # If **all** input is dataless, all output should be dataless too
95+ if dataless_option == "dataless_all" :
96+ assert all ([cube .is_dataless () for cube in mangled_cubes ])
97+
98+ # We should get all the same cubes, **except** for the data content
99+ assert len (mangled_cubes ) == len (result_cubes )
100+ for cube1 , cube2 in zip (mangled_cubes , result_cubes ):
101+ cube1 , cube2 = [cube .copy () for cube in (cube1 , cube2 )]
102+ for cube in (cube1 , cube2 ):
103+ cube .data = None
104+ if cube1 != cube2 :
105+ assert cube1 == cube2
106+
107+ return original_result
108+
109+
110+ class DatalessMixin :
111+ # Mixin class to make every merge check for operation with dataless cubes
112+ @pytest .fixture (autouse = True )
113+ def setup_patch (self , mocker , dataless_option ):
114+ # NB these patch functions must be generated dynamically (for each test
115+ # parametrisation), so that they can access the 'dataless_option' switch.
116+ def patched_merge (cubelist , unique = True ):
117+ return check_merge_against_dataless_cases (
118+ _ORIGINAL_MERGE , cubelist , unique , dataless_option = dataless_option
119+ )
120+
121+ def patched_merge_cube (cubelist ):
122+ return check_merge_against_dataless_cases (
123+ _ORIGINAL_MERGE_CUBE , cubelist , dataless_option = dataless_option
124+ )
125+
126+ # Patch **all** uses of CubeList.merge/merge_cube within these tests, to compare
127+ # "normal" results with those which have some dataless inputs.
128+ mocker .patch ("iris.cube.CubeList.merge" , patched_merge )
129+ mocker .patch ("iris.cube.CubeList.merge_cube" , patched_merge_cube )
130+
22131
23132class MergeMixin :
24133 """Mix-in class for attributes & utilities common to these test cases."""
@@ -45,15 +154,15 @@ def test_duplication(self):
45154
46155
47156@skip_data
48- class TestSingleCube (MergeMixin ):
157+ class TestSingleCube (MergeMixin , DatalessMixin ):
49158 def setup_method (self ):
50159 self ._data_path = get_data_path (("PP" , "globClim1" , "theta.pp" ))
51160 self ._num_cubes = 1
52161 self ._prefix = "theta"
53162
54163
55164@skip_data
56- class TestMultiCube (MergeMixin ):
165+ class TestMultiCube (MergeMixin , DatalessMixin ):
57166 def setup_method (self ):
58167 self ._data_path = get_data_path (("PP" , "globClim1" , "dec_subset.pp" ))
59168 self ._num_cubes = 4
@@ -75,7 +184,7 @@ def custom_coord_callback(cube, field, filename):
75184
76185
77186@skip_data
78- class TestColpex :
187+ class TestColpex ( DatalessMixin ) :
79188 def setup_method (self ):
80189 self ._data_path = get_data_path (("PP" , "COLPEX" , "small_colpex_theta_p_alt.pp" ))
81190
@@ -86,7 +195,7 @@ def test_colpex(self, request):
86195
87196
88197@skip_data
89- class TestDataMerge :
198+ class TestDataMerge ( DatalessMixin ) :
90199 def test_extended_proxy_data (self , request ):
91200 # Get the empty theta cubes for T+1.5 and T+2
92201 data_path = get_data_path (("PP" , "COLPEX" , "theta_and_orog_subset.pp" ))
@@ -119,7 +228,7 @@ def test_real_data(self, request):
119228 assert_CML (request , cubes , ["merge" , "theta.cml" ])
120229
121230
122- class TestDimensionSplitting :
231+ class TestDimensionSplitting ( DatalessMixin ) :
123232 def _make_cube (self , a , b , c , data ):
124233 cube_data = np .empty ((4 , 5 ), dtype = np .float32 )
125234 cube_data [:] = data
@@ -182,7 +291,7 @@ def test_multi_split(self, request):
182291 assert_CML (request , cube , ("merge" , "multi_split.cml" ))
183292
184293
185- class TestCombination :
294+ class TestCombination ( DatalessMixin ) :
186295 def _make_cube (self , a , b , c , d , data = 0 ):
187296 cube_data = np .empty ((4 , 5 ), dtype = np .float32 )
188297 cube_data [:] = data
@@ -245,7 +354,7 @@ def add(*args):
245354 )
246355
247356
248- class TestDimSelection :
357+ class TestDimSelection ( DatalessMixin ) :
249358 def _make_cube (self , a , b , data = 0 , a_dim = False , b_dim = False ):
250359 cube_data = np .empty ((4 , 5 ), dtype = np .float32 )
251360 cube_data [:] = data
@@ -360,7 +469,7 @@ def test_a_dim_b_dim(self, request):
360469 assert cube .coord ("b" ) in cube .aux_coords
361470
362471
363- class TestTimeTripleMerging :
472+ class TestTimeTripleMerging ( DatalessMixin ) :
364473 def _make_cube (self , a , b , c , data = 0 ):
365474 cube_data = np .empty ((4 , 5 ), dtype = np .float32 )
366475 cube_data [:] = data
@@ -583,7 +692,7 @@ def test_simple3(self, request):
583692 assert_CML (request , cube , ("merge" , "time_triple_merging5.cml" ), checksum = False )
584693
585694
586- class TestCubeMergeTheoretical :
695+ class TestCubeMergeTheoretical ( DatalessMixin ) :
587696 def test_simple_bounds_merge (self , request ):
588697 cube1 = iris .tests .stock .simple_2d ()
589698 cube2 = iris .tests .stock .simple_2d ()
@@ -649,7 +758,7 @@ def test_simple_points_merge(self, request):
649758 assert_CML (request , r , ("cube_merge" , "test_simple_attributes3.cml" ))
650759
651760
652- class TestContiguous :
761+ class TestContiguous ( DatalessMixin ) :
653762 def test_form_contiguous_dimcoord (self ):
654763 # Test that cube sliced up and remerged in the opposite order maintains
655764 # contiguity.
0 commit comments