|
13 | 13 | # See the License for the specific language governing permissions and |
14 | 14 | # limitations under the License. |
15 | 15 |
|
| 16 | +import asyncio |
16 | 17 | import sys |
17 | 18 | import unittest |
18 | 19 | from pathlib import Path |
| 20 | +from unittest.mock import AsyncMock, MagicMock, patch |
19 | 21 |
|
20 | 22 | import torch |
21 | 23 | from tensordict import TensorDict |
@@ -97,6 +99,232 @@ def test_merge_kv_to_tensordict(self): |
97 | 99 |
|
98 | 100 | self.assertEqual(reconstructed.batch_size, torch.Size([3])) |
99 | 101 |
|
| 102 | + def test_get_shape_type_custom_meta_list_without_custom_meta(self): |
| 103 | + """Test _get_shape_type_custom_meta_list returns correct shapes and dtypes without custom_meta.""" |
| 104 | + shapes, dtypes, custom_meta_list = KVStorageManager._get_shape_type_custom_meta_list(self.metadata) |
| 105 | + |
| 106 | + # Expected order: sorted by field name (label, mask, text), then by global_index order |
| 107 | + # 3 fields * 3 samples = 9 entries |
| 108 | + self.assertEqual(len(shapes), 9) |
| 109 | + self.assertEqual(len(dtypes), 9) |
| 110 | + self.assertEqual(len(custom_meta_list), 9) |
| 111 | + |
| 112 | + # Check shapes - order is label, mask, text (sorted alphabetically) |
| 113 | + # label shapes: [()]*3, mask shapes: [(1,)]*3, text shapes: [(2,)]*3 |
| 114 | + expected_shapes = [ |
| 115 | + torch.Size([]), # label[0] |
| 116 | + torch.Size([]), # label[1] |
| 117 | + torch.Size([]), # label[2] |
| 118 | + torch.Size([1]), # mask[0] |
| 119 | + torch.Size([1]), # mask[1] |
| 120 | + torch.Size([1]), # mask[2] |
| 121 | + torch.Size([2]), # text[0] |
| 122 | + torch.Size([2]), # text[1] |
| 123 | + torch.Size([2]), # text[2] |
| 124 | + ] |
| 125 | + self.assertEqual(shapes, expected_shapes) |
| 126 | + |
| 127 | + # All dtypes should be torch.int64 |
| 128 | + for dtype in dtypes: |
| 129 | + self.assertEqual(dtype, torch.int64) |
| 130 | + |
| 131 | + # No custom_meta provided, so all should be None |
| 132 | + for meta in custom_meta_list: |
| 133 | + self.assertIsNone(meta) |
| 134 | + |
| 135 | + def test_get_shape_type_custom_meta_list_with_custom_meta(self): |
| 136 | + """Test _get_shape_type_custom_meta_list returns correct custom_meta when provided.""" |
| 137 | + # Add custom_meta to metadata |
| 138 | + custom_meta = { |
| 139 | + 8: {"text": {"key1": "value1"}, "label": {"key2": "value2"}, "mask": {"key3": "value3"}}, |
| 140 | + 9: {"text": {"key4": "value4"}, "label": {"key5": "value5"}, "mask": {"key6": "value6"}}, |
| 141 | + 10: {"text": {"key7": "value7"}, "label": {"key8": "value8"}, "mask": {"key9": "value9"}}, |
| 142 | + } |
| 143 | + self.metadata.update_custom_meta(custom_meta) |
| 144 | + |
| 145 | + shapes, dtypes, custom_meta_list = KVStorageManager._get_shape_type_custom_meta_list(self.metadata) |
| 146 | + |
| 147 | + # Check custom_meta - order is label, mask, text (sorted alphabetically) by global_index |
| 148 | + expected_custom_meta = [ |
| 149 | + {"key2": "value2"}, # label, global_index=8 |
| 150 | + {"key5": "value5"}, # label, global_index=9 |
| 151 | + {"key8": "value8"}, # label, global_index=10 |
| 152 | + {"key3": "value3"}, # mask, global_index=8 |
| 153 | + {"key6": "value6"}, # mask, global_index=9 |
| 154 | + {"key9": "value9"}, # mask, global_index=10 |
| 155 | + {"key1": "value1"}, # text, global_index=8 |
| 156 | + {"key4": "value4"}, # text, global_index=9 |
| 157 | + {"key7": "value7"}, # text, global_index=10 |
| 158 | + ] |
| 159 | + self.assertEqual(custom_meta_list, expected_custom_meta) |
| 160 | + |
| 161 | + def test_get_shape_type_custom_meta_list_with_partial_custom_meta(self): |
| 162 | + """Test _get_shape_type_custom_meta_list handles partial custom_meta correctly.""" |
| 163 | + # Add custom_meta only for some global_indexes and fields |
| 164 | + custom_meta = { |
| 165 | + 8: {"text": {"key1": "value1"}}, # Only text field |
| 166 | + # global_index 9 has no custom_meta |
| 167 | + 10: {"label": {"key2": "value2"}, "mask": {"key3": "value3"}}, # label and mask only |
| 168 | + } |
| 169 | + self.metadata.update_custom_meta(custom_meta) |
| 170 | + |
| 171 | + shapes, dtypes, custom_meta_list = KVStorageManager._get_shape_type_custom_meta_list(self.metadata) |
| 172 | + |
| 173 | + # Check custom_meta - order is label, mask, text (sorted alphabetically) by global_index |
| 174 | + expected_custom_meta = [ |
| 175 | + None, # label, global_index=8 (not in custom_meta) |
| 176 | + None, # label, global_index=9 (not in custom_meta) |
| 177 | + {"key2": "value2"}, # label, global_index=10 |
| 178 | + None, # mask, global_index=8 (not in custom_meta) |
| 179 | + None, # mask, global_index=9 (not in custom_meta) |
| 180 | + {"key3": "value3"}, # mask, global_index=10 |
| 181 | + {"key1": "value1"}, # text, global_index=8 |
| 182 | + None, # text, global_index=9 (not in custom_meta) |
| 183 | + None, # text, global_index=10 (not in custom_meta for text) |
| 184 | + ] |
| 185 | + self.assertEqual(custom_meta_list, expected_custom_meta) |
| 186 | + |
| 187 | + |
| 188 | +class TestPutDataWithCustomMeta(unittest.TestCase): |
| 189 | + """Test put_data with custom_meta functionality.""" |
| 190 | + |
| 191 | + def setUp(self): |
| 192 | + """Set up test fixtures for put_data tests.""" |
| 193 | + self.field_names = ["text", "label"] |
| 194 | + self.global_indexes = [0, 1, 2] |
| 195 | + |
| 196 | + # Create test data |
| 197 | + self.data = TensorDict( |
| 198 | + { |
| 199 | + "text": torch.tensor([[1, 2], [3, 4], [5, 6]]), |
| 200 | + "label": torch.tensor([0, 1, 2]), |
| 201 | + }, |
| 202 | + batch_size=3, |
| 203 | + ) |
| 204 | + |
| 205 | + # Create metadata without production status set (for insert mode) |
| 206 | + samples = [] |
| 207 | + for sample_id in range(self.data.batch_size[0]): |
| 208 | + fields_dict = {} |
| 209 | + for field_name in self.data.keys(): |
| 210 | + tensor = self.data[field_name][sample_id] |
| 211 | + field_meta = FieldMeta(name=field_name, dtype=tensor.dtype, shape=tensor.shape, production_status=0) |
| 212 | + fields_dict[field_name] = field_meta |
| 213 | + sample = SampleMeta( |
| 214 | + partition_id="test_partition", |
| 215 | + global_index=self.global_indexes[sample_id], |
| 216 | + fields=fields_dict, |
| 217 | + ) |
| 218 | + samples.append(sample) |
| 219 | + self.metadata = BatchMeta(samples=samples) |
| 220 | + |
| 221 | + @patch.object(KVStorageManager, "_connect_to_controller") |
| 222 | + @patch.object(KVStorageManager, "notify_data_update", new_callable=AsyncMock) |
| 223 | + def test_put_data_with_custom_meta_from_storage_client(self, mock_notify, mock_connect): |
| 224 | + """Test that put_data correctly processes custom_meta returned by storage client.""" |
| 225 | + # Create a mock storage client |
| 226 | + mock_storage_client = MagicMock() |
| 227 | + # Simulate storage client returning custom_meta (one per key) |
| 228 | + # Keys order: label[0,1,2], text[0,1,2] (sorted by field name) |
| 229 | + mock_custom_meta = [ |
| 230 | + {"storage_key": "0@label"}, |
| 231 | + {"storage_key": "1@label"}, |
| 232 | + {"storage_key": "2@label"}, |
| 233 | + {"storage_key": "0@text"}, |
| 234 | + {"storage_key": "1@text"}, |
| 235 | + {"storage_key": "2@text"}, |
| 236 | + ] |
| 237 | + mock_storage_client.put.return_value = mock_custom_meta |
| 238 | + |
| 239 | + # Create manager with mocked dependencies |
| 240 | + config = {"client_name": "MockClient"} |
| 241 | + with patch( |
| 242 | + "transfer_queue.storage.managers.base.StorageClientFactory.create", return_value=mock_storage_client |
| 243 | + ): |
| 244 | + manager = KVStorageManager(config) |
| 245 | + |
| 246 | + # Run put_data |
| 247 | + asyncio.run(manager.put_data(self.data, self.metadata)) |
| 248 | + |
| 249 | + # Verify storage client was called with correct keys and values |
| 250 | + mock_storage_client.put.assert_called_once() |
| 251 | + call_args = mock_storage_client.put.call_args |
| 252 | + keys = call_args[0][0] |
| 253 | + values = call_args[0][1] |
| 254 | + |
| 255 | + # Verify keys are correct |
| 256 | + expected_keys = ["0@label", "1@label", "2@label", "0@text", "1@text", "2@text"] |
| 257 | + self.assertEqual(keys, expected_keys) |
| 258 | + self.assertEqual(len(values), 6) |
| 259 | + |
| 260 | + # Verify notify_data_update was called with correct custom_meta structure |
| 261 | + mock_notify.assert_called_once() |
| 262 | + notify_call_args = mock_notify.call_args |
| 263 | + per_field_custom_meta = notify_call_args[0][5] # 6th positional argument |
| 264 | + |
| 265 | + # Verify custom_meta is structured correctly: {global_index: {field: meta}} |
| 266 | + self.assertIn(0, per_field_custom_meta) |
| 267 | + self.assertIn(1, per_field_custom_meta) |
| 268 | + self.assertIn(2, per_field_custom_meta) |
| 269 | + |
| 270 | + self.assertEqual(per_field_custom_meta[0]["label"], {"storage_key": "0@label"}) |
| 271 | + self.assertEqual(per_field_custom_meta[0]["text"], {"storage_key": "0@text"}) |
| 272 | + self.assertEqual(per_field_custom_meta[1]["label"], {"storage_key": "1@label"}) |
| 273 | + self.assertEqual(per_field_custom_meta[1]["text"], {"storage_key": "1@text"}) |
| 274 | + self.assertEqual(per_field_custom_meta[2]["label"], {"storage_key": "2@label"}) |
| 275 | + self.assertEqual(per_field_custom_meta[2]["text"], {"storage_key": "2@text"}) |
| 276 | + |
| 277 | + # Verify metadata was updated with custom_meta |
| 278 | + all_custom_meta = self.metadata.get_all_custom_meta() |
| 279 | + self.assertEqual(all_custom_meta[0]["label"], {"storage_key": "0@label"}) |
| 280 | + self.assertEqual(all_custom_meta[2]["text"], {"storage_key": "2@text"}) |
| 281 | + |
| 282 | + @patch.object(KVStorageManager, "_connect_to_controller") |
| 283 | + @patch.object(KVStorageManager, "notify_data_update", new_callable=AsyncMock) |
| 284 | + def test_put_data_without_custom_meta(self, mock_notify, mock_connect): |
| 285 | + """Test that put_data works correctly when storage client returns no custom_meta.""" |
| 286 | + # Create a mock storage client that returns None for custom_meta |
| 287 | + mock_storage_client = MagicMock() |
| 288 | + mock_storage_client.put.return_value = None |
| 289 | + |
| 290 | + # Create manager with mocked dependencies |
| 291 | + config = {"client_name": "MockClient"} |
| 292 | + with patch( |
| 293 | + "transfer_queue.storage.managers.base.StorageClientFactory.create", return_value=mock_storage_client |
| 294 | + ): |
| 295 | + manager = KVStorageManager(config) |
| 296 | + |
| 297 | + # Run put_data |
| 298 | + asyncio.run(manager.put_data(self.data, self.metadata)) |
| 299 | + |
| 300 | + # Verify notify_data_update was called with empty dict for custom_meta |
| 301 | + mock_notify.assert_called_once() |
| 302 | + notify_call_args = mock_notify.call_args |
| 303 | + per_field_custom_meta = notify_call_args[0][5] # 6th positional argument |
| 304 | + self.assertEqual(per_field_custom_meta, {}) |
| 305 | + |
| 306 | + @patch.object(KVStorageManager, "_connect_to_controller") |
| 307 | + @patch.object(KVStorageManager, "notify_data_update", new_callable=AsyncMock) |
| 308 | + def test_put_data_custom_meta_length_mismatch_raises_error(self, mock_notify, mock_connect): |
| 309 | + """Test that put_data raises ValueError when custom_meta length doesn't match keys.""" |
| 310 | + # Create a mock storage client that returns mismatched custom_meta length |
| 311 | + mock_storage_client = MagicMock() |
| 312 | + # Return only 3 custom_meta entries when 6 are expected |
| 313 | + mock_storage_client.put.return_value = [{"key": "1"}, {"key": "2"}, {"key": "3"}] |
| 314 | + |
| 315 | + # Create manager with mocked dependencies |
| 316 | + config = {"client_name": "MockClient"} |
| 317 | + with patch( |
| 318 | + "transfer_queue.storage.managers.base.StorageClientFactory.create", return_value=mock_storage_client |
| 319 | + ): |
| 320 | + manager = KVStorageManager(config) |
| 321 | + |
| 322 | + # Run put_data and expect ValueError |
| 323 | + with self.assertRaises(ValueError) as context: |
| 324 | + asyncio.run(manager.put_data(self.data, self.metadata)) |
| 325 | + |
| 326 | + self.assertIn("does not match", str(context.exception)) |
| 327 | + |
100 | 328 |
|
101 | 329 | if __name__ == "__main__": |
102 | 330 | unittest.main() |
0 commit comments