|
1 | | -from unittest.mock import patch |
| 1 | +""" |
| 2 | +Unit tests for the CMIP6_CMORiser base class. |
| 3 | +
|
| 4 | +These tests focus on the core functionality of the CMIP6_CMORiser class |
| 5 | +without requiring complex dependencies or data files. |
| 6 | +""" |
| 7 | + |
| 8 | +from pathlib import Path |
| 9 | +from unittest.mock import Mock |
2 | 10 |
|
3 | 11 | import pytest |
4 | | -import xarray as xr |
5 | 12 |
|
6 | | -from access_mopper.base import BaseCMORiser |
| 13 | +from access_mopper.base import CMIP6_CMORiser |
| 14 | + |
7 | 15 |
|
| 16 | +class TestCMIP6CMORiser: |
| 17 | + """Unit tests for CMIP6_CMORiser base class.""" |
8 | 18 |
|
9 | | -class TestBaseCMORiser: |
10 | | - """Unit tests for BaseCMORiser class.""" |
| 19 | + @pytest.fixture |
| 20 | + def mock_vocab(self): |
| 21 | + """Mock CMIP6 vocabulary object.""" |
| 22 | + vocab = Mock() |
| 23 | + vocab.get_table = Mock(return_value={"tas": {"units": "K"}}) |
| 24 | + return vocab |
11 | 25 |
|
12 | | - def test_init_with_valid_params(self, mock_config, temp_dir): |
| 26 | + @pytest.fixture |
| 27 | + def mock_mapping(self): |
| 28 | + """Mock variable mapping.""" |
| 29 | + return { |
| 30 | + "CF standard Name": "air_temperature", |
| 31 | + "units": "K", |
| 32 | + "dimensions": {"time": "time", "lat": "lat", "lon": "lon"}, |
| 33 | + "positive": None, |
| 34 | + } |
| 35 | + |
| 36 | + def test_init_with_valid_params(self, mock_vocab, mock_mapping, temp_dir): |
13 | 37 | """Test initialization with valid parameters.""" |
14 | | - cmoriser = BaseCMORiser( |
| 38 | + cmoriser = CMIP6_CMORiser( |
15 | 39 | input_paths=["test.nc"], |
16 | | - compound_name="Amon.tas", |
17 | | - output_path=temp_dir, |
18 | | - **mock_config, |
| 40 | + output_path=str(temp_dir), |
| 41 | + cmor_name="tas", |
| 42 | + cmip6_vocab=mock_vocab, |
| 43 | + variable_mapping=mock_mapping, |
19 | 44 | ) |
20 | 45 |
|
21 | | - assert cmoriser.experiment_id == "historical" |
22 | | - assert cmoriser.compound_name == "Amon.tas" |
23 | | - assert cmoriser.mip_table == "Amon" |
| 46 | + assert cmoriser.input_paths == ["test.nc"] |
| 47 | + assert cmoriser.output_path == str(temp_dir) |
24 | 48 | assert cmoriser.cmor_name == "tas" |
| 49 | + assert cmoriser.vocab == mock_vocab |
| 50 | + assert cmoriser.mapping == mock_mapping |
| 51 | + |
| 52 | + def test_init_with_multiple_input_paths(self, mock_vocab, mock_mapping, temp_dir): |
| 53 | + """Test initialization with multiple input files.""" |
| 54 | + input_files = ["test1.nc", "test2.nc", "test3.nc"] |
| 55 | + cmoriser = CMIP6_CMORiser( |
| 56 | + input_paths=input_files, |
| 57 | + output_path=str(temp_dir), |
| 58 | + cmor_name="tas", |
| 59 | + cmip6_vocab=mock_vocab, |
| 60 | + variable_mapping=mock_mapping, |
| 61 | + ) |
| 62 | + |
| 63 | + assert cmoriser.input_paths == input_files |
25 | 64 |
|
26 | | - def test_init_with_invalid_compound_name(self, mock_config, temp_dir): |
27 | | - """Test initialization fails with invalid compound name.""" |
28 | | - with pytest.raises(ValueError, match="Invalid compound_name format"): |
29 | | - BaseCMORiser( |
30 | | - input_paths=["test.nc"], |
31 | | - compound_name="invalid", |
32 | | - output_path=temp_dir, |
33 | | - **mock_config, |
34 | | - ) |
35 | | - |
36 | | - def test_init_with_missing_required_params(self, temp_dir): |
37 | | - """Test initialization fails with missing required parameters.""" |
38 | | - with pytest.raises(TypeError): |
39 | | - BaseCMORiser( |
40 | | - input_paths=["test.nc"], |
41 | | - compound_name="Amon.tas", |
42 | | - output_path=temp_dir, |
43 | | - # Missing required CMIP6 metadata |
44 | | - ) |
45 | | - |
46 | | - @patch("access_mopper.base.xr.open_mfdataset") |
47 | | - def test_load_data_single_file( |
48 | | - self, mock_open_mfdataset, mock_netcdf_dataset, mock_config, temp_dir |
| 65 | + def test_init_with_single_input_path_string( |
| 66 | + self, mock_vocab, mock_mapping, temp_dir |
49 | 67 | ): |
50 | | - """Test loading data from single file.""" |
51 | | - mock_open_mfdataset.return_value = mock_netcdf_dataset |
| 68 | + """Test initialization with single input path as string.""" |
| 69 | + cmoriser = CMIP6_CMORiser( |
| 70 | + input_paths="single_file.nc", |
| 71 | + output_path=str(temp_dir), |
| 72 | + cmor_name="tas", |
| 73 | + cmip6_vocab=mock_vocab, |
| 74 | + variable_mapping=mock_mapping, |
| 75 | + ) |
| 76 | + |
| 77 | + assert cmoriser.input_paths == ["single_file.nc"] |
52 | 78 |
|
53 | | - cmoriser = BaseCMORiser( |
| 79 | + def test_init_with_drs_root(self, mock_vocab, mock_mapping, temp_dir): |
| 80 | + """Test initialization with DRS root path.""" |
| 81 | + drs_root = temp_dir / "drs" |
| 82 | + cmoriser = CMIP6_CMORiser( |
54 | 83 | input_paths=["test.nc"], |
55 | | - compound_name="Amon.tas", |
56 | | - output_path=temp_dir, |
57 | | - **mock_config, |
| 84 | + output_path=str(temp_dir), |
| 85 | + cmor_name="tas", |
| 86 | + cmip6_vocab=mock_vocab, |
| 87 | + variable_mapping=mock_mapping, |
| 88 | + drs_root=str(drs_root), |
58 | 89 | ) |
59 | 90 |
|
60 | | - result = cmoriser._load_input_data() |
| 91 | + assert cmoriser.drs_root == Path(drs_root) |
| 92 | + |
| 93 | + def test_version_date_format(self, mock_vocab, mock_mapping, temp_dir): |
| 94 | + """Test that version date is set correctly.""" |
| 95 | + cmoriser = CMIP6_CMORiser( |
| 96 | + input_paths=["test.nc"], |
| 97 | + output_path=str(temp_dir), |
| 98 | + cmor_name="tas", |
| 99 | + cmip6_vocab=mock_vocab, |
| 100 | + variable_mapping=mock_mapping, |
| 101 | + ) |
61 | 102 |
|
62 | | - mock_open_mfdataset.assert_called_once() |
63 | | - assert isinstance(result, xr.Dataset) |
| 103 | + # Check that version_date is a string in YYYYMMDD format |
| 104 | + assert isinstance(cmoriser.version_date, str) |
| 105 | + assert len(cmoriser.version_date) == 8 |
| 106 | + assert cmoriser.version_date.isdigit() |
64 | 107 |
|
65 | | - @patch("access_mopper.base.xr.open_mfdataset") |
66 | | - def test_load_data_multiple_files( |
67 | | - self, mock_open_mfdataset, mock_netcdf_dataset, mock_config, temp_dir |
68 | | - ): |
69 | | - """Test loading data from multiple files.""" |
70 | | - mock_open_mfdataset.return_value = mock_netcdf_dataset |
71 | | - |
72 | | - cmoriser = BaseCMORiser( |
73 | | - input_paths=["test1.nc", "test2.nc"], |
74 | | - compound_name="Amon.tas", |
75 | | - output_path=temp_dir, |
76 | | - **mock_config, |
| 108 | + def test_type_mapping_attribute(self, mock_vocab, mock_mapping, temp_dir): |
| 109 | + """Test that type_mapping is available as class attribute.""" |
| 110 | + cmoriser = CMIP6_CMORiser( |
| 111 | + input_paths=["test.nc"], |
| 112 | + output_path=str(temp_dir), |
| 113 | + cmor_name="tas", |
| 114 | + cmip6_vocab=mock_vocab, |
| 115 | + variable_mapping=mock_mapping, |
| 116 | + ) |
| 117 | + |
| 118 | + # type_mapping should be available from utilities |
| 119 | + assert hasattr(cmoriser, "type_mapping") |
| 120 | + assert cmoriser.type_mapping is not None |
| 121 | + |
| 122 | + def test_dataset_proxy_methods(self, mock_vocab, mock_mapping, temp_dir): |
| 123 | + """Test that the CMORiser can proxy dataset operations.""" |
| 124 | + # Create a mock dataset |
| 125 | + mock_dataset = Mock() |
| 126 | + mock_dataset.test_attr = "test_value" |
| 127 | + mock_dataset.__getitem__ = Mock(return_value="dataset_item") |
| 128 | + mock_dataset.__repr__ = Mock(return_value="<Dataset representation>") |
| 129 | + |
| 130 | + cmoriser = CMIP6_CMORiser( |
| 131 | + input_paths=["test.nc"], |
| 132 | + output_path=str(temp_dir), |
| 133 | + cmor_name="tas", |
| 134 | + cmip6_vocab=mock_vocab, |
| 135 | + variable_mapping=mock_mapping, |
77 | 136 | ) |
78 | 137 |
|
79 | | - cmoriser._load_input_data() |
| 138 | + # Set the dataset |
| 139 | + cmoriser.ds = mock_dataset |
80 | 140 |
|
81 | | - mock_open_mfdataset.assert_called_once_with( |
82 | | - ["test1.nc", "test2.nc"], |
83 | | - combine="by_coords", |
84 | | - data_vars="minimal", |
85 | | - coords="minimal", |
86 | | - compat="override", |
| 141 | + # Test __getitem__ proxy |
| 142 | + result = cmoriser["test_key"] |
| 143 | + assert result == "dataset_item" |
| 144 | + mock_dataset.__getitem__.assert_called_with("test_key") |
| 145 | + |
| 146 | + # Test __getattr__ proxy |
| 147 | + assert cmoriser.test_attr == "test_value" |
| 148 | + |
| 149 | + # Test __setitem__ proxy |
| 150 | + cmoriser["new_key"] = "new_value" |
| 151 | + assert cmoriser.ds["new_key"] == "new_value" |
| 152 | + |
| 153 | + # Test __repr__ proxy |
| 154 | + repr_result = repr(cmoriser) |
| 155 | + assert repr_result == "<Dataset representation>" |
| 156 | + |
| 157 | + def test_dataset_none_initially(self, mock_vocab, mock_mapping, temp_dir): |
| 158 | + """Test that dataset is None initially.""" |
| 159 | + cmoriser = CMIP6_CMORiser( |
| 160 | + input_paths=["test.nc"], |
| 161 | + output_path=str(temp_dir), |
| 162 | + cmor_name="tas", |
| 163 | + cmip6_vocab=mock_vocab, |
| 164 | + variable_mapping=mock_mapping, |
87 | 165 | ) |
88 | 166 |
|
89 | | - def test_compound_name_parsing(self, mock_config, temp_dir): |
90 | | - """Test parsing of compound name into table and variable.""" |
91 | | - test_cases = [ |
92 | | - ("Amon.tas", "Amon", "tas"), |
93 | | - ("Omon.tos", "Omon", "tos"), |
94 | | - ("day.pr", "day", "pr"), |
95 | | - ("6hrPlevPt.ua", "6hrPlevPt", "ua"), |
96 | | - ] |
97 | | - |
98 | | - for compound_name, expected_table, expected_var in test_cases: |
99 | | - cmoriser = BaseCMORiser( |
100 | | - input_paths=["test.nc"], |
101 | | - compound_name=compound_name, |
102 | | - output_path=temp_dir, |
103 | | - **mock_config, |
104 | | - ) |
105 | | - assert cmoriser.mip_table == expected_table |
106 | | - assert cmoriser.cmor_name == expected_var |
107 | | - |
108 | | - def test_output_path_creation(self, mock_config, temp_dir): |
109 | | - """Test that output path is created if it doesn't exist.""" |
110 | | - non_existent_path = temp_dir / "new_output_dir" |
111 | | - assert not non_existent_path.exists() |
112 | | - |
113 | | - BaseCMORiser( |
| 167 | + assert cmoriser.ds is None |
| 168 | + |
| 169 | + def test_getattr_fallback(self, mock_vocab, mock_mapping, temp_dir): |
| 170 | + """Test __getattr__ behavior when dataset is None.""" |
| 171 | + cmoriser = CMIP6_CMORiser( |
114 | 172 | input_paths=["test.nc"], |
115 | | - compound_name="Amon.tas", |
116 | | - output_path=non_existent_path, |
117 | | - **mock_config, |
| 173 | + output_path=str(temp_dir), |
| 174 | + cmor_name="tas", |
| 175 | + cmip6_vocab=mock_vocab, |
| 176 | + variable_mapping=mock_mapping, |
118 | 177 | ) |
119 | 178 |
|
120 | | - # This should create the directory |
121 | | - assert non_existent_path.exists() |
| 179 | + # When ds is None, getattr should raise AttributeError |
| 180 | + with pytest.raises(AttributeError): |
| 181 | + _ = cmoriser.nonexistent_attribute |
0 commit comments