Skip to content

Commit 2d28ea9

Browse files
committed
increase test coverage
1 parent 0f8fea7 commit 2d28ea9

3 files changed

Lines changed: 337 additions & 2 deletions

File tree

deep_code/tests/tools/test_publish.py

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,6 @@ def test_publish_dataset_updates_project_collection_when_exists(
386386
self.assertIn(mock_gen.update_deepesdl_collection, update_methods)
387387

388388
def test_publish_dataset_raises_when_stac_root_missing(self):
389-
# stac_catalog_s3_root is mandatory; publish_dataset must raise ValueError
390389
self.publisher.dataset_config = {
391390
"collection_id": "test-collection",
392391
"dataset_id": "test-dataset",
@@ -395,6 +394,166 @@ def test_publish_dataset_raises_when_stac_root_missing(self):
395394
with pytest.raises(ValueError, match="stac_catalog_s3_root"):
396395
self.publisher.publish_dataset(write_to_file=False)
397396

397+
def test_publish_dataset_raises_when_no_dataset_config(self):
398+
self.publisher.dataset_config = None
399+
with pytest.raises(ValueError, match="No dataset config"):
400+
self.publisher.publish_dataset(write_to_file=False)
401+
402+
def test_publish_dataset_raises_when_ids_missing(self):
403+
self.publisher.dataset_config = {"collection_id": "", "dataset_id": ""}
404+
with pytest.raises(ValueError, match="Dataset ID or Collection ID missing"):
405+
self.publisher.publish_dataset(write_to_file=False)
406+
407+
def test_publish_dataset_raises_when_license_missing(self):
408+
self.publisher.dataset_config = {
409+
"collection_id": "test-collection",
410+
"dataset_id": "test-dataset",
411+
}
412+
with pytest.raises(ValueError, match="license_type is required"):
413+
self.publisher.publish_dataset(write_to_file=False)
414+
415+
def test_write_to_file_serializes_dict(self):
416+
import json
417+
import os
418+
import tempfile
419+
420+
with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as f:
421+
path = f.name
422+
try:
423+
Publisher._write_to_file(path, {"a": 1})
424+
with open(path) as f:
425+
result = json.load(f)
426+
self.assertEqual(result, {"a": 1})
427+
finally:
428+
os.unlink(path)
429+
430+
def test_update_and_add_to_file_dict(self):
431+
file_dict = {}
432+
self.publisher.gh_publisher.github_automation.local_clone_dir = "/tmp"
433+
update_method = MagicMock(return_value={"key": "value"})
434+
self.publisher._update_and_add_to_file_dict(file_dict, "some/catalog.json", update_method)
435+
update_method.assert_called_once()
436+
assert any("some/catalog.json" in str(k) for k in file_dict)
437+
438+
def test_update_variable_catalogs_creates_new_when_missing(self):
439+
mock_gen = MagicMock()
440+
mock_gen.variables_metadata = {"var1": {"variable_id": "var1"}}
441+
mock_gen.build_variable_catalog.return_value.to_dict.return_value = {"id": "var1"}
442+
self.publisher.gh_publisher.github_automation.file_exists.return_value = False
443+
444+
file_dict = {}
445+
self.publisher._update_variable_catalogs(mock_gen, file_dict, ["var1"])
446+
447+
mock_gen.build_variable_catalog.assert_called_once()
448+
assert "variables/var1/catalog.json" in file_dict
449+
450+
def test_update_variable_catalogs_updates_existing(self):
451+
mock_gen = MagicMock()
452+
self.publisher.gh_publisher.github_automation.file_exists.return_value = True
453+
self.publisher.gh_publisher.github_automation.local_clone_dir = "/tmp"
454+
mock_gen.update_existing_variable_catalog.return_value = {"id": "var1"}
455+
456+
file_dict = {}
457+
self.publisher._update_variable_catalogs(mock_gen, file_dict, ["var1"])
458+
459+
mock_gen.update_existing_variable_catalog.assert_called_once()
460+
assert "variables/var1/catalog.json" in file_dict
461+
462+
# ------------------------------------------------------------------
463+
# generate_workflow_experiment_records
464+
# ------------------------------------------------------------------
465+
466+
def _setup_workflow_mocks(self):
467+
"""Patch all internals of generate_workflow_experiment_records."""
468+
mock_rg = MagicMock()
469+
mock_props = MagicMock()
470+
mock_props.jupyter_kernel_info.to_dict.return_value = {}
471+
mock_rg.build_record_properties.return_value = mock_props
472+
473+
mock_wf_record = MagicMock()
474+
mock_wf_record.to_dict.return_value = {"id": "wf", "properties": {}}
475+
476+
mock_exp_record = MagicMock()
477+
mock_exp_record.to_dict.return_value = {
478+
"id": "wf",
479+
"properties": {},
480+
"jupyter_notebook_url": "url",
481+
"collection_id": "col",
482+
}
483+
return mock_rg, mock_props, mock_wf_record, mock_exp_record
484+
485+
@patch("deep_code.tools.publish.WorkflowAsOgcRecord")
486+
@patch("deep_code.tools.publish.LinksBuilder")
487+
@patch("deep_code.tools.publish.OSCWorkflowOGCApiRecordGenerator")
488+
def test_generate_workflow_records_mode_workflow(self, MockRG, MockLinks, MockWF):
489+
mock_rg, mock_props, mock_wf_record, _ = self._setup_workflow_mocks()
490+
MockRG.return_value = mock_rg
491+
MockWF.return_value = mock_wf_record
492+
493+
self.publisher.workflow_config = {
494+
"workflow_id": "my-workflow",
495+
"properties": {"title": "My WF", "license": "CC-BY-4.0"},
496+
}
497+
with patch.object(self.publisher, "_update_base_catalog", return_value={}):
498+
result = self.publisher.generate_workflow_experiment_records(
499+
write_to_file=False, mode="workflow"
500+
)
501+
502+
self.assertIn("workflows/my-workflow/record.json", result)
503+
self.assertIn("workflows/catalog.json", result)
504+
self.assertNotIn("experiments/catalog.json", result)
505+
506+
@patch("deep_code.tools.publish.ExperimentAsOgcRecord")
507+
@patch("deep_code.tools.publish.WorkflowAsOgcRecord")
508+
@patch("deep_code.tools.publish.LinksBuilder")
509+
@patch("deep_code.tools.publish.OSCWorkflowOGCApiRecordGenerator")
510+
def test_generate_workflow_records_mode_all(self, MockRG, MockLinks, MockWF, MockExp):
511+
mock_rg, mock_props, mock_wf_record, mock_exp_record = self._setup_workflow_mocks()
512+
MockRG.return_value = mock_rg
513+
MockWF.return_value = mock_wf_record
514+
MockExp.return_value = mock_exp_record
515+
516+
self.publisher.workflow_config = {
517+
"workflow_id": "my-workflow",
518+
"properties": {"title": "My WF", "license": "CC-BY-4.0"},
519+
}
520+
self.publisher.collection_id = "my-collection"
521+
with patch.object(self.publisher, "_update_base_catalog", return_value={}):
522+
result = self.publisher.generate_workflow_experiment_records(
523+
write_to_file=False, mode="all"
524+
)
525+
526+
self.assertIn("workflows/my-workflow/record.json", result)
527+
self.assertIn("workflows/catalog.json", result)
528+
self.assertIn("experiments/catalog.json", result)
529+
530+
@patch("deep_code.tools.publish.OSCWorkflowOGCApiRecordGenerator")
531+
def test_generate_workflow_records_raises_when_workflow_id_missing(self, MockRG):
532+
self.publisher.workflow_config = {
533+
"properties": {"title": "My WF", "license": "CC-BY-4.0"},
534+
}
535+
with pytest.raises(ValueError, match="workflow_id is missing"):
536+
self.publisher.generate_workflow_experiment_records(
537+
write_to_file=False, mode="workflow"
538+
)
539+
540+
@patch("deep_code.tools.publish.OSCWorkflowOGCApiRecordGenerator")
541+
def test_generate_workflow_records_raises_when_license_missing(self, MockRG):
542+
self.publisher.workflow_config = {
543+
"workflow_id": "my-wf",
544+
"properties": {"title": "My WF"},
545+
}
546+
with pytest.raises(ValueError, match="license is required"):
547+
self.publisher.generate_workflow_experiment_records(
548+
write_to_file=False, mode="workflow"
549+
)
550+
551+
def test_generate_workflow_records_returns_empty_for_dataset_mode(self):
552+
result = self.publisher.generate_workflow_experiment_records(
553+
write_to_file=False, mode="dataset"
554+
)
555+
self.assertEqual(result, {})
556+
398557

399558
class TestParseGithubNotebookUrl:
400559
@pytest.mark.parametrize(

deep_code/tests/utils/test_dataset_stac_generator.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,159 @@ def test_edge_cases(self):
607607
OscDatasetStacGenerator.format_string("too many spaces"),
608608
"Too Many Spaces",
609609
)
610+
611+
612+
class TestOscDatasetStacGeneratorExtra(unittest.TestCase):
613+
"""Additional tests to cover branches not exercised by TestOSCProductSTACGenerator."""
614+
615+
def _make_generator(self, mock_ds, collection_id="my-collection", **kwargs):
616+
with patch("deep_code.utils.dataset_stac_generator.open_dataset", return_value=mock_ds):
617+
return OscDatasetStacGenerator(
618+
dataset_id="test.zarr",
619+
collection_id=collection_id,
620+
workflow_id="wf",
621+
workflow_title="WF",
622+
license_type="CC-BY-4.0",
623+
**kwargs,
624+
)
625+
626+
def _make_dataset(self, coord_type="lon_lat"):
627+
import numpy as np
628+
from datetime import datetime
629+
if coord_type == "lon_lat":
630+
coords = {
631+
"lon": ("lon", np.linspace(-10, 10, 3)),
632+
"lat": ("lat", np.linspace(-5, 5, 2)),
633+
"time": ("time", [np.datetime64(datetime(2020, 1, 1), "ns")]),
634+
}
635+
elif coord_type == "longitude_latitude":
636+
coords = {
637+
"longitude": ("longitude", np.linspace(-10, 10, 3)),
638+
"latitude": ("latitude", np.linspace(-5, 5, 2)),
639+
"time": ("time", [np.datetime64(datetime(2020, 1, 1), "ns")]),
640+
}
641+
elif coord_type == "x_y":
642+
coords = {
643+
"x": ("x", np.linspace(0, 100, 3)),
644+
"y": ("y", np.linspace(0, 50, 2)),
645+
"time": ("time", [np.datetime64(datetime(2020, 1, 1), "ns")]),
646+
}
647+
else:
648+
coords = {}
649+
from xarray import Dataset
650+
return Dataset(coords=coords)
651+
652+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
653+
def test_collection_id_with_space_raises(self, mock_open_ds):
654+
mock_open_ds.return_value = self._make_dataset()
655+
with self.assertRaisesRegex(ValueError, "must not contain spaces"):
656+
OscDatasetStacGenerator(
657+
dataset_id="test.zarr",
658+
collection_id="bad id",
659+
workflow_id="wf",
660+
workflow_title="WF",
661+
license_type="CC-BY-4.0",
662+
)
663+
664+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
665+
def test_spatial_extent_longitude_latitude(self, mock_open_ds):
666+
ds = self._make_dataset("longitude_latitude")
667+
mock_open_ds.return_value = ds
668+
gen = self._make_generator(ds)
669+
extent = gen._get_spatial_extent()
670+
self.assertAlmostEqual(extent.bboxes[0][0], -10.0)
671+
self.assertAlmostEqual(extent.bboxes[0][1], -5.0)
672+
673+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
674+
def test_spatial_extent_x_y(self, mock_open_ds):
675+
ds = self._make_dataset("x_y")
676+
mock_open_ds.return_value = ds
677+
gen = self._make_generator(ds)
678+
extent = gen._get_spatial_extent()
679+
self.assertAlmostEqual(extent.bboxes[0][0], 0.0)
680+
681+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
682+
def test_spatial_extent_unknown_coords_raises(self, mock_open_ds):
683+
ds = self._make_dataset("none")
684+
mock_open_ds.return_value = ds
685+
gen = self._make_generator(ds)
686+
with self.assertRaisesRegex(ValueError, "recognized spatial coordinates"):
687+
gen._get_spatial_extent()
688+
689+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
690+
def test_temporal_extent_no_time_raises(self, mock_open_ds):
691+
ds = self._make_dataset("none")
692+
mock_open_ds.return_value = ds
693+
gen = self._make_generator(ds)
694+
with self.assertRaisesRegex(ValueError, "time"):
695+
gen._get_temporal_extent()
696+
697+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
698+
def test_normalize_name_none_returns_none(self, mock_open_ds):
699+
ds = self._make_dataset()
700+
mock_open_ds.return_value = ds
701+
self.assertIsNone(OscDatasetStacGenerator._normalize_name(None))
702+
703+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
704+
def test_build_collection_with_cf_params(self, mock_open_ds):
705+
ds = self._make_dataset()
706+
mock_open_ds.return_value = ds
707+
gen = self._make_generator(ds, cf_params=[{"name": "temperature", "units": "K"}])
708+
collection = gen.build_dataset_stac_collection(mode="dataset")
709+
self.assertEqual(collection.extra_fields.get("cf:parameter"), [{"name": "temperature", "units": "K"}])
710+
711+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
712+
def test_build_collection_with_visualisation_link(self, mock_open_ds):
713+
ds = self._make_dataset()
714+
mock_open_ds.return_value = ds
715+
gen = self._make_generator(ds, visualisation_link="https://viewer.example.com/")
716+
collection = gen.build_dataset_stac_collection(mode="dataset")
717+
vis_links = [lnk for lnk in collection.links if lnk.rel == "visualisation"]
718+
self.assertEqual(len(vis_links), 1)
719+
self.assertEqual(vis_links[0].target, "https://viewer.example.com/")
720+
self.assertEqual(vis_links[0].title, "Dataset visualisation")
721+
722+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
723+
def test_build_collection_mode_all_adds_experiment_link(self, mock_open_ds):
724+
ds = self._make_dataset()
725+
mock_open_ds.return_value = ds
726+
gen = self._make_generator(ds)
727+
collection = gen.build_dataset_stac_collection(mode="all")
728+
exp_links = [lnk for lnk in collection.links if "experiments" in str(lnk.target)]
729+
self.assertEqual(len(exp_links), 1)
730+
731+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
732+
def test_s3_to_https(self, mock_open_ds):
733+
self.assertEqual(
734+
OscDatasetStacGenerator._s3_to_https("s3://my-bucket/path/to/file.json"),
735+
"https://my-bucket.s3.amazonaws.com/path/to/file.json",
736+
)
737+
738+
@patch("deep_code.utils.dataset_stac_generator.open_dataset")
739+
def test_update_existing_variable_catalog(self, mock_open_ds):
740+
import json
741+
import os
742+
import tempfile
743+
744+
ds = self._make_dataset()
745+
mock_open_ds.return_value = ds
746+
gen = self._make_generator(ds, osc_themes=["land"])
747+
748+
base = {
749+
"type": "Catalog",
750+
"id": "var1",
751+
"stac_version": "1.0.0",
752+
"description": "Variable catalog",
753+
"links": [],
754+
}
755+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
756+
json.dump(base, f)
757+
tmp_path = f.name
758+
try:
759+
result = gen.update_existing_variable_catalog(tmp_path, "var1")
760+
finally:
761+
os.unlink(tmp_path)
762+
763+
rels = [lnk["rel"] for lnk in result["links"]]
764+
self.assertIn("child", rels)
765+
self.assertIn("related", rels) # theme link

deep_code/tests/utils/test_helper.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import xarray
1111
import xarray as xr
1212

13-
from deep_code.utils.helper import open_dataset
13+
from deep_code.utils.helper import open_dataset, serialize
1414

1515

1616
def make_dummy_dataset():
@@ -156,3 +156,23 @@ def test_uses_provided_logger(self, mock_new_store, mock_get_logger):
156156
custom_logger.info.assert_any_call(
157157
"Successfully opened dataset 'test-id' with configuration: Public store"
158158
)
159+
160+
161+
class TestSerialize(unittest.TestCase):
162+
def test_set_converted_to_list(self):
163+
result = serialize({1, 2, 3})
164+
self.assertIsInstance(result, list)
165+
self.assertCountEqual(result, [1, 2, 3])
166+
167+
def test_object_with_dict_returns_dict(self):
168+
class Obj:
169+
def __init__(self):
170+
self.x = 1
171+
self.y = 2
172+
173+
result = serialize(Obj())
174+
self.assertEqual(result, {"x": 1, "y": 2})
175+
176+
def test_unserializable_raises_type_error(self):
177+
with self.assertRaises(TypeError):
178+
serialize(42)

0 commit comments

Comments
 (0)