1- import os
1+ #!/usr/bin/env python3
2+ # Copyright (c) 2025 by Brockmann Consult GmbH
3+ # Permissions are hereby granted under the terms of the MIT License:
4+ # https://opensource.org/licenses/MIT.
5+
26import unittest
3- from datetime import datetime
7+ from datetime import datetime , timezone
48from unittest .mock import MagicMock , patch
59
610import numpy as np
7- from pystac import Collection
8- from xarray import Dataset
9-
11+ from pystac import Catalog , Collection , Link
12+ from xarray import Dataset , DataArray
13+
14+ from deep_code .constants import (
15+ DEEPESDL_COLLECTION_SELF_HREF ,
16+ OSC_THEME_SCHEME ,
17+ PRODUCT_BASE_CATALOG_SELF_HREF ,
18+ VARIABLE_BASE_CATALOG_SELF_HREF ,
19+ )
1020from deep_code .utils .dataset_stac_generator import OscDatasetStacGenerator
21+ from deep_code .utils .dataset_stac_generator import Theme , ThemeConcept
1122
1223
1324class TestOSCProductSTACGenerator (unittest .TestCase ):
14- @patch ("deep_code.utils.dataset_stac_generator.new_data_store " )
25+ @patch ("deep_code.utils.dataset_stac_generator.open_dataset " )
1526 def setUp (self , mock_data_store ):
1627 """Set up a mock dataset and generator."""
1728 self .mock_dataset = Dataset (
@@ -50,7 +61,7 @@ def setUp(self, mock_data_store):
5061 )
5162 mock_store = MagicMock ()
5263 mock_store .open_data .return_value = self .mock_dataset
53- mock_data_store .return_value = mock_store
64+ mock_data_store .return_value = self . mock_dataset
5465
5566 self .generator = OscDatasetStacGenerator (
5667 dataset_id = "mock-dataset-id" ,
@@ -65,9 +76,8 @@ def setUp(self, mock_data_store):
6576 def test_open_dataset (self ):
6677 """Test if the dataset is opened correctly."""
6778 self .assertIsInstance (self .generator .dataset , Dataset )
68- self .assertIn ("lon" , self .generator .dataset .coords )
69- self .assertIn ("lat" , self .generator .dataset .coords )
70- self .assertIn ("time" , self .generator .dataset .coords )
79+ for coord in ("lon" , "lat" , "time" ):
80+ self .assertIn (coord , self .generator .dataset .coords )
7181
7282 def test_get_spatial_extent (self ):
7383 """Test spatial extent extraction."""
@@ -77,146 +87,93 @@ def test_get_spatial_extent(self):
7787 def test_get_temporal_extent (self ):
7888 """Test temporal extent extraction."""
7989 extent = self .generator ._get_temporal_extent ()
80- expected_intervals = [datetime (2023 , 1 , 1 , 0 , 0 ), datetime (2023 , 1 , 2 , 0 , 0 )]
81- self .assertEqual (extent .intervals [0 ], expected_intervals )
90+ # TemporalExtent.intervals is a list of [start, end]
91+ interval = extent .intervals [0 ]
92+ self .assertEqual (interval [0 ], datetime (2023 , 1 , 1 , 0 , 0 ))
93+ self .assertEqual (interval [1 ], datetime (2023 , 1 , 2 , 0 , 0 ))
8294
8395 def test_get_variables (self ):
84- """Test variable extraction."""
85- variables = self .generator .get_variable_ids ()
86- self .assertEqual ( variables , ["var1" , "var2" ])
96+ """Test variable ID extraction."""
97+ vars_ = self .generator .get_variable_ids ()
98+ self .assertCountEqual ( vars_ , ["var1" , "var2" ])
8799
88100 def test_get_general_metadata (self ):
89101 """Test general metadata extraction."""
90- metadata = self .generator ._get_general_metadata ()
91- self .assertEqual (metadata ["description" ], "Mock dataset for testing." )
92-
93- @patch ("pystac.Collection.add_link" )
94- @patch ("pystac.Collection.set_self_href" )
95- def test_build_stac_collection (self , mock_set_self_href , mock_add_link ):
96- """Test STAC collection creation."""
97- collection = self .generator .build_dataset_stac_collection ()
98- self .assertIsInstance (collection , Collection )
99- self .assertEqual (collection .id , "mock-collection-id" )
100- self .assertEqual (collection .description , "Mock dataset for testing." )
101- self .assertEqual (
102- collection .extent .spatial .bboxes [0 ], [- 180.0 , - 90.0 , 180.0 , 90.0 ]
103- )
104- self .assertEqual (
105- collection .extent .temporal .intervals [0 ],
106- [datetime (2023 , 1 , 1 , 0 , 0 ), datetime (2023 , 1 , 2 , 0 , 0 )],
107- )
108- mock_set_self_href .assert_called_once ()
109- mock_add_link .assert_called ()
110-
111- def test_invalid_spatial_extent (self ):
112- """Test spatial extent extraction with missing coordinates."""
113- self .generator .dataset = Dataset (coords = {"x" : [], "y" : []})
114- with self .assertRaises (ValueError ):
115- self .generator ._get_spatial_extent ()
116-
117- def test_invalid_temporal_extent (self ):
118- """Test temporal extent extraction with missing time."""
119- self .generator .dataset = Dataset (coords = {})
120- with self .assertRaises (ValueError ):
121- self .generator ._get_temporal_extent ()
122-
123- @patch ("deep_code.utils.dataset_stac_generator.new_data_store" )
124- @patch ("deep_code.utils.dataset_stac_generator.logging.getLogger" )
125- def test_open_dataset_success_public_store (self , mock_logger , mock_new_data_store ):
126- """Test dataset opening with the public store configuration."""
127- # Create a mock store and mock its `open_data` method
128- mock_store = MagicMock ()
129- mock_new_data_store .return_value = mock_store
130- mock_store .open_data .return_value = self .mock_dataset
131-
132- # Instantiate the generator (this will implicitly call _open_dataset)
133- generator = OscDatasetStacGenerator ("mock-dataset-id" , "mock-collection-id" )
134-
135- # Validate that the dataset is assigned correctly
136- self .assertEqual (generator .dataset , "mock_dataset" )
137-
138- # Validate that `new_data_store` was called once with the correct parameters
139- mock_new_data_store .assert_called_once_with (
140- "s3" , root = "deep-esdl-public" , storage_options = {"anon" : True }
141- )
142-
143- # Ensure `open_data` was called once on the returned store
144- mock_store .open_data .assert_called_once_with ("mock-dataset-id" )
145-
146- # Validate logging behavior
147- mock_logger ().info .assert_any_call (
148- "Attempting to open dataset with configuration: Public store"
149- )
150- mock_logger ().info .assert_any_call (
151- "Successfully opened dataset with configuration: Public store"
152- )
153-
154- @patch ("deep_code.utils.dataset_stac_generator.new_data_store" )
155- @patch ("deep_code.utils.dataset_stac_generator.logging.getLogger" )
156- def test_open_dataset_success_authenticated_store (
157- self , mock_logger , mock_new_data_store
158- ):
159- """Test dataset opening with the authenticated store configuration."""
160- # Simulate public store failure
161- mock_store = MagicMock ()
162- mock_new_data_store .side_effect = [
163- Exception ("Public store failure" ),
164- # First call (public store) raises an exception
165- mock_store ,
166- # Second call (authenticated store) returns a mock store
167- ]
168- mock_store .open_data .return_value = self .mock_dataset
169-
170- os .environ ["S3_USER_STORAGE_BUCKET" ] = "mock-bucket"
171- os .environ ["S3_USER_STORAGE_KEY" ] = "mock-key"
172- os .environ ["S3_USER_STORAGE_SECRET" ] = "mock-secret"
173-
174- generator = OscDatasetStacGenerator ("mock-dataset-id" , "mock-collection-id" )
175-
176- # Validate that the dataset was successfully opened with the authenticated store
177- self .assertEqual (generator .dataset , "mock_dataset" )
178- self .assertEqual (mock_new_data_store .call_count , 2 )
179-
180- # Validate calls to `new_data_store`
181- mock_new_data_store .assert_any_call (
182- "s3" , root = "deep-esdl-public" , storage_options = {"anon" : True }
183- )
184- mock_new_data_store .assert_any_call (
185- "s3" ,
186- root = "mock-bucket" ,
187- storage_options = {"anon" : False , "key" : "mock-key" , "secret" : "mock-secret" },
188- )
189-
190- # Validate logging calls
191- mock_logger ().info .assert_any_call (
192- "Attempting to open dataset with configuration: Public store"
193- )
194- mock_logger ().info .assert_any_call (
195- "Attempting to open dataset with configuration: Authenticated store"
196- )
197- mock_logger ().info .assert_any_call (
198- "Successfully opened dataset with configuration: Authenticated store"
199- )
200-
201- @patch ("deep_code.utils.dataset_stac_generator.new_data_store" )
202- @patch ("deep_code.utils.dataset_stac_generator.logging.getLogger" )
203- def test_open_dataset_failure (self , mock_logger , mock_new_data_store ):
204- """Test dataset opening failure with all configurations."""
205- # Simulate all store failures
206- mock_new_data_store .side_effect = Exception ("Store failure" )
207- os .environ ["S3_USER_STORAGE_BUCKET" ] = "mock-bucket"
208- os .environ ["S3_USER_STORAGE_KEY" ] = "mock-key"
209- os .environ ["S3_USER_STORAGE_SECRET" ] = "mock-secret"
210-
211- with self .assertRaises (ValueError ) as context :
212- OscDatasetStacGenerator ("mock-dataset-id" , "mock-collection-id" )
213-
214- self .assertIn (
215- "Failed to open Zarr dataset with ID mock-dataset-id" ,
216- str (context .exception ),
217- )
218- self .assertIn ("Public store, Authenticated store" , str (context .exception ))
219- self .assertEqual (mock_new_data_store .call_count , 2 )
102+ meta = self .generator ._get_general_metadata ()
103+ self .assertEqual (meta .get ("description" ), "Mock dataset for testing." )
104+
105+ def test_extract_metadata_for_variable (self ):
106+ """Test single variable metadata extraction."""
107+ da : DataArray = self .mock_dataset .data_vars ['var1' ]
108+ var_meta = self .generator .extract_metadata_for_variable (da )
109+ self .assertEqual (var_meta ['variable_id' ], 'var1' )
110+ self .assertEqual (var_meta ['description' ], 'dummy' )
111+ self .assertEqual (var_meta ['gcmd_keyword_url' ], 'https://dummy' )
112+
113+ def test_get_variables_metadata (self ):
114+ """Test metadata dict for all variables."""
115+ meta_dict = self .generator .get_variables_metadata ()
116+ self .assertIn ('var1' , meta_dict )
117+ self .assertIn ('var2' , meta_dict )
118+ self .assertIsInstance (meta_dict ['var1' ], dict )
119+
120+ def test_build_theme (self ):
121+ """Test Theme builder static method."""
122+ themes = ["a" , "b" ]
123+ theme_obj : Theme = OscDatasetStacGenerator .build_theme (themes )
124+ self .assertEqual (theme_obj .scheme , OSC_THEME_SCHEME )
125+ ids = [tc .id for tc in theme_obj .concepts ]
126+ self .assertListEqual (ids , ['a' , 'b' ])
127+
128+ @patch .object (OscDatasetStacGenerator , '_add_gcmd_link_to_var_catalog' )
129+ @patch .object (OscDatasetStacGenerator , 'add_themes_as_related_links_var_catalog' )
130+ def test_build_variable_catalog (self , mock_add_themes , mock_add_gcmd ):
131+ """Test building of variable-level STAC catalog."""
132+ var_meta = self .generator .variables_metadata ['var1' ]
133+ catalog = self .generator .build_variable_catalog (var_meta )
134+ self .assertIsInstance (catalog , Catalog )
135+ self .assertEqual (catalog .id , 'var1' )
136+ # Title should be capitalized
137+ self .assertEqual (catalog .title , 'Var1' )
138+ # Self href ends with var1/catalog.json
139+ self .assertTrue (catalog .self_href .endswith ('/var1/catalog.json' ))
140+
141+ @patch ('pystac.Catalog.from_file' )
142+ def test_update_product_base_catalog (self , mock_from_file ):
143+ """Test linking product catalog."""
144+ mock_cat = MagicMock (spec = Catalog )
145+ mock_from_file .return_value = mock_cat
146+
147+ result = self .generator .update_product_base_catalog ('path.json' )
148+ self .assertIs (result , mock_cat )
149+ mock_cat .add_link .assert_called_once ()
150+ mock_cat .set_self_href .assert_called_once_with (PRODUCT_BASE_CATALOG_SELF_HREF )
151+
152+ @patch ('pystac.Catalog.from_file' )
153+ def test_update_variable_base_catalog (self , mock_from_file ):
154+ """Test linking variable base catalog."""
155+ mock_cat = MagicMock (spec = Catalog )
156+ mock_from_file .return_value = mock_cat
157+
158+ vars_ = ['v1' , 'v2' ]
159+ result = self .generator .update_variable_base_catalog ('vars.json' , vars_ )
160+ self .assertIs (result , mock_cat )
161+ # Expect one add_link per variable
162+ self .assertEqual (mock_cat .add_link .call_count , len (vars_ ))
163+ mock_cat .set_self_href .assert_called_once_with (VARIABLE_BASE_CATALOG_SELF_HREF )
164+
165+ @patch ('pystac.Collection.from_file' )
166+ def test_update_deepesdl_collection (self , mock_from_file ):
167+ """Test updating DeepESDL collection."""
168+ mock_coll = MagicMock (spec = Collection )
169+ mock_from_file .return_value = mock_coll
170+
171+ result = self .generator .update_deepesdl_collection ('deep.json' )
172+ self .assertIs (result , mock_coll )
173+ # Expect child and theme related links for each theme
174+ calls = mock_coll .add_link .call_count
175+ self .assertGreaterEqual (calls , 1 + len (self .generator .osc_themes ))
176+ mock_coll .set_self_href .assert_called_once_with (DEEPESDL_COLLECTION_SELF_HREF )
220177
221178
222179class TestFormatString (unittest .TestCase ):
0 commit comments