|
6 | 6 |
|
7 | 7 | import struct |
8 | 8 | from dataclasses import replace |
| 9 | +from unittest.mock import MagicMock, patch |
9 | 10 |
|
10 | 11 | import pytest |
11 | 12 | from glide_shared.commands.server_modules.ft_options.ft_create_options import DistanceMetricType |
12 | 13 | from glide_shared.commands.server_modules.ft_options.ft_search_options import FtSearchOptions |
13 | 14 | from haystack.dataclasses import Document |
14 | 15 | from haystack.dataclasses.byte_stream import ByteStream |
15 | 16 | from haystack.document_stores.types import DuplicatePolicy |
| 17 | +from haystack.errors import FilterError |
16 | 18 | from haystack.testing.document_store import ( |
17 | 19 | CountDocumentsByFilterTest, |
18 | 20 | CountDocumentsTest, |
|
31 | 33 | from haystack.utils import Secret |
32 | 34 |
|
33 | 35 | from haystack_integrations.document_stores.valkey import ValkeyDocumentStore |
| 36 | +from haystack_integrations.document_stores.valkey import document_store as ds_module |
| 37 | +from haystack_integrations.document_stores.valkey.document_store import ValkeyDocumentStoreError |
34 | 38 |
|
35 | 39 |
|
36 | 40 | def _filterable_docs_embedding_dim_3() -> list[Document]: |
@@ -1087,6 +1091,22 @@ def test_static_methods_dont_need_instance(self): |
1087 | 1091 | ValkeyDocumentStore._validate_documents([Document(id="1", content="test")]) |
1088 | 1092 | ValkeyDocumentStore._validate_policy(DuplicatePolicy.NONE) |
1089 | 1093 |
|
| 1094 | + @pytest.mark.parametrize( |
| 1095 | + "metadata_fields, expected_error", |
| 1096 | + [ |
| 1097 | + ("not_a_dict", r"metadata_fields must be a dictionary"), |
| 1098 | + ({"": str}, r"Field name must be a non-empty string"), |
| 1099 | + ({"category": list}, r"Unsupported field type"), |
| 1100 | + ], |
| 1101 | + ) |
| 1102 | + def test_validate_and_normalize_metadata_fields_rejects_invalid_inputs(self, metadata_fields, expected_error): |
| 1103 | + with pytest.raises(ValueError, match=expected_error): |
| 1104 | + ValkeyDocumentStore._validate_and_normalize_metadata_fields(metadata_fields) |
| 1105 | + |
| 1106 | + def test_validate_policy_logs_warning_for_unsupported_policy(self, caplog): |
| 1107 | + ValkeyDocumentStore._validate_policy(DuplicatePolicy.SKIP) |
| 1108 | + assert "only supports `DuplicatePolicy.OVERWRITE`" in caplog.text |
| 1109 | + |
1090 | 1110 |
|
1091 | 1111 | class TestValkeyDocumentStoreConverters: |
1092 | 1112 | def test_to_dict(self): |
@@ -1205,3 +1225,118 @@ def test_prepare_document_dict_validates_numeric_field_type(): |
1205 | 1225 |
|
1206 | 1226 | with pytest.raises(ValueError, match="Field 'priority' expects numeric value but got str"): |
1207 | 1227 | store._prepare_document_dict(doc) |
| 1228 | + |
| 1229 | + |
| 1230 | +@pytest.fixture |
| 1231 | +def unit_store(): |
| 1232 | + return ValkeyDocumentStore( |
| 1233 | + index_name="unit_test", |
| 1234 | + embedding_dim=3, |
| 1235 | + metadata_fields={"category": str, "priority": int}, |
| 1236 | + ) |
| 1237 | + |
| 1238 | + |
| 1239 | +class TestValkeyDocumentStoreErrorPaths: |
| 1240 | + @pytest.mark.parametrize("cluster_mode", [False, True]) |
| 1241 | + def test_get_connection_wraps_create_errors(self, unit_store, cluster_mode): |
| 1242 | + unit_store._cluster_mode = cluster_mode |
| 1243 | + target = "SyncGlideClusterClient" if cluster_mode else "SyncGlideClient" |
| 1244 | + with patch.object(ds_module, target) as mock_cls: |
| 1245 | + mock_cls.create.side_effect = RuntimeError("connect failed") |
| 1246 | + with pytest.raises(ValkeyDocumentStoreError, match="Failed to connect to Valkey"): |
| 1247 | + unit_store._get_connection() |
| 1248 | + |
| 1249 | + def test_close_swallows_client_exception(self, unit_store, caplog): |
| 1250 | + unit_store._client = MagicMock() |
| 1251 | + unit_store._client.close.side_effect = RuntimeError("close boom") |
| 1252 | + unit_store.close() |
| 1253 | + assert unit_store._client is None |
| 1254 | + assert "Failed to close Valkey client" in caplog.text |
| 1255 | + |
| 1256 | + def test_count_documents_returns_zero_when_no_index(self, unit_store): |
| 1257 | + unit_store._client = MagicMock() |
| 1258 | + with patch.object(unit_store, "_has_index", return_value=False): |
| 1259 | + assert unit_store.count_documents() == 0 |
| 1260 | + |
| 1261 | + def test_embedding_retrieval_returns_empty_when_no_index(self, unit_store, caplog): |
| 1262 | + unit_store._client = MagicMock() |
| 1263 | + with patch.object(unit_store, "_has_index", return_value=False): |
| 1264 | + assert unit_store._embedding_retrieval([0.1, 0.2, 0.3]) == [] |
| 1265 | + assert "does not exist" in caplog.text |
| 1266 | + |
| 1267 | + def test_embedding_retrieval_returns_empty_for_nonpositive_limit(self, unit_store): |
| 1268 | + unit_store._client = MagicMock() |
| 1269 | + with patch.object(unit_store, "_has_index", return_value=True): |
| 1270 | + assert unit_store._embedding_retrieval([0.1, 0.2, 0.3], limit=0) == [] |
| 1271 | + assert unit_store._embedding_retrieval([0.1, 0.2, 0.3], limit=-1) == [] |
| 1272 | + |
| 1273 | + def test_embedding_retrieval_reraises_filter_error(self, unit_store): |
| 1274 | + unit_store._client = MagicMock() |
| 1275 | + with patch.object(unit_store, "_has_index", return_value=True): |
| 1276 | + bad_filters = { |
| 1277 | + "operator": "AND", |
| 1278 | + "conditions": [{"field": "meta.unknown", "operator": "==", "value": "x"}], |
| 1279 | + } |
| 1280 | + with pytest.raises(FilterError): |
| 1281 | + unit_store._embedding_retrieval([0.1, 0.2, 0.3], filters=bad_filters, limit=5) |
| 1282 | + |
| 1283 | + def test_write_documents_empty_list_returns_zero(self, unit_store, caplog): |
| 1284 | + assert unit_store.write_documents([]) == 0 |
| 1285 | + assert "empty list" in caplog.text |
| 1286 | + |
| 1287 | + def test_delete_all_documents_without_index_is_noop(self, unit_store): |
| 1288 | + unit_store._client = MagicMock() |
| 1289 | + with ( |
| 1290 | + patch.object(unit_store, "_has_index", return_value=False), |
| 1291 | + patch.object(ds_module, "sync_ft") as mock_ft, |
| 1292 | + ): |
| 1293 | + unit_store.delete_all_documents() |
| 1294 | + mock_ft.dropindex.assert_not_called() |
| 1295 | + |
| 1296 | + def test_create_index_wraps_non_ok_response(self, unit_store): |
| 1297 | + unit_store._client = MagicMock() |
| 1298 | + with ( |
| 1299 | + patch.object(unit_store, "_has_index", return_value=False), |
| 1300 | + patch.object(ds_module, "sync_ft") as mock_ft, |
| 1301 | + ): |
| 1302 | + mock_ft.create.return_value = b"ERR" |
| 1303 | + with pytest.raises(ValkeyDocumentStoreError, match="Error creating collection"): |
| 1304 | + unit_store._create_index() |
| 1305 | + |
| 1306 | + @pytest.mark.parametrize( |
| 1307 | + "method_name, args, expected_msg", |
| 1308 | + [ |
| 1309 | + ( |
| 1310 | + "filter_documents", |
| 1311 | + ({"field": "meta.category", "operator": "==", "value": "x"},), |
| 1312 | + "Error filtering documents", |
| 1313 | + ), |
| 1314 | + ( |
| 1315 | + "delete_by_filter", |
| 1316 | + ({"field": "meta.category", "operator": "==", "value": "x"},), |
| 1317 | + "Failed to delete documents by filter", |
| 1318 | + ), |
| 1319 | + ( |
| 1320 | + "update_by_filter", |
| 1321 | + ({"field": "meta.category", "operator": "==", "value": "x"}, {"status": "new"}), |
| 1322 | + "Failed to update documents by filter", |
| 1323 | + ), |
| 1324 | + ( |
| 1325 | + "count_documents_by_filter", |
| 1326 | + ({"field": "meta.category", "operator": "==", "value": "x"},), |
| 1327 | + "Failed to count documents by filter", |
| 1328 | + ), |
| 1329 | + ( |
| 1330 | + "count_unique_metadata_by_filter", |
| 1331 | + ({"field": "meta.category", "operator": "==", "value": "x"}, ["category"]), |
| 1332 | + "Failed to count unique metadata by filter", |
| 1333 | + ), |
| 1334 | + ("get_metadata_field_min_max", ("priority",), "Failed to get min/max for field"), |
| 1335 | + ("get_metadata_field_unique_values", ("category",), "Failed to get unique values for field"), |
| 1336 | + ], |
| 1337 | + ) |
| 1338 | + def test_filter_dependent_methods_wrap_internal_errors(self, unit_store, method_name, args, expected_msg): |
| 1339 | + with patch.object(unit_store, "count_documents", side_effect=RuntimeError("boom")): |
| 1340 | + method = getattr(unit_store, method_name) |
| 1341 | + with pytest.raises(ValkeyDocumentStoreError, match=expected_msg): |
| 1342 | + method(*args) |
0 commit comments