Skip to content

Commit 2909e1f

Browse files
authored
PYTHON-5085 - Convert test.test_index_management to async (#2101)
1 parent 94b9a54 commit 2909e1f

File tree

3 files changed

+417
-24
lines changed

3 files changed

+417
-24
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
# Copyright 2023-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Run the auth spec tests."""
16+
from __future__ import annotations
17+
18+
import asyncio
19+
import os
20+
import pathlib
21+
import sys
22+
import time
23+
import uuid
24+
from typing import Any, Mapping
25+
26+
import pytest
27+
28+
sys.path[0:0] = [""]
29+
30+
from test.asynchronous import AsyncIntegrationTest, AsyncPyMongoTestCase, unittest
31+
from test.asynchronous.unified_format import generate_test_classes
32+
from test.utils import AllowListEventListener, OvertCommandListener
33+
34+
from pymongo.errors import OperationFailure
35+
from pymongo.operations import SearchIndexModel
36+
from pymongo.read_concern import ReadConcern
37+
from pymongo.write_concern import WriteConcern
38+
39+
_IS_SYNC = False
40+
41+
pytestmark = pytest.mark.index_management
42+
43+
# Location of JSON test specifications.
44+
if _IS_SYNC:
45+
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent, "index_management")
46+
else:
47+
_TEST_PATH = os.path.join(pathlib.Path(__file__).resolve().parent.parent, "index_management")
48+
49+
_NAME = "test-search-index"
50+
51+
52+
class TestCreateSearchIndex(AsyncIntegrationTest):
53+
async def test_inputs(self):
54+
if not os.environ.get("TEST_INDEX_MANAGEMENT"):
55+
raise unittest.SkipTest("Skipping index management tests")
56+
listener = AllowListEventListener("createSearchIndexes")
57+
client = self.simple_client(event_listeners=[listener])
58+
coll = client.test.test
59+
await coll.drop()
60+
definition = dict(mappings=dict(dynamic=True))
61+
model_kwarg_list: list[Mapping[str, Any]] = [
62+
dict(definition=definition, name=None),
63+
dict(definition=definition, name="test"),
64+
]
65+
for model_kwargs in model_kwarg_list:
66+
model = SearchIndexModel(**model_kwargs)
67+
with self.assertRaises(OperationFailure):
68+
await coll.create_search_index(model)
69+
with self.assertRaises(OperationFailure):
70+
await coll.create_search_index(model_kwargs)
71+
72+
listener.reset()
73+
with self.assertRaises(OperationFailure):
74+
await coll.create_search_index({"definition": definition, "arbitraryOption": 1})
75+
self.assertEqual(
76+
{"definition": definition, "arbitraryOption": 1},
77+
listener.events[0].command["indexes"][0],
78+
)
79+
80+
listener.reset()
81+
with self.assertRaises(OperationFailure):
82+
await coll.create_search_index({"definition": definition, "type": "search"})
83+
self.assertEqual(
84+
{"definition": definition, "type": "search"}, listener.events[0].command["indexes"][0]
85+
)
86+
87+
88+
class SearchIndexIntegrationBase(AsyncPyMongoTestCase):
89+
db_name = "test_search_index_base"
90+
91+
@classmethod
92+
def setUpClass(cls) -> None:
93+
if not os.environ.get("TEST_INDEX_MANAGEMENT"):
94+
raise unittest.SkipTest("Skipping index management tests")
95+
cls.url = os.environ.get("MONGODB_URI")
96+
cls.username = os.environ["DB_USER"]
97+
cls.password = os.environ["DB_PASSWORD"]
98+
cls.listener = OvertCommandListener()
99+
100+
async def asyncSetUp(self) -> None:
101+
self.client = self.simple_client(
102+
self.url,
103+
username=self.username,
104+
password=self.password,
105+
event_listeners=[self.listener],
106+
)
107+
await self.client.drop_database(_NAME)
108+
self.db = self.client[self.db_name]
109+
110+
async def asyncTearDown(self):
111+
await self.client.drop_database(_NAME)
112+
113+
async def wait_for_ready(self, coll, name=_NAME, predicate=None):
114+
"""Wait for a search index to be ready."""
115+
indices: list[Mapping[str, Any]] = []
116+
if predicate is None:
117+
predicate = lambda index: index.get("queryable") is True
118+
119+
while True:
120+
indices = await (await coll.list_search_indexes(name)).to_list()
121+
if len(indices) and predicate(indices[0]):
122+
return indices[0]
123+
await asyncio.sleep(5)
124+
125+
126+
class TestSearchIndexIntegration(SearchIndexIntegrationBase):
127+
db_name = "test_search_index"
128+
129+
async def test_comment_field(self):
130+
# Create a collection with the "create" command using a randomly generated name (referred to as ``coll0``).
131+
coll0 = self.db[f"col{uuid.uuid4()}"]
132+
await coll0.insert_one({})
133+
134+
# Create a new search index on ``coll0`` that implicitly passes its type.
135+
search_definition = {"mappings": {"dynamic": False}}
136+
self.listener.reset()
137+
implicit_search_resp = await coll0.create_search_index(
138+
model={"name": _NAME + "-implicit", "definition": search_definition}, comment="foo"
139+
)
140+
event = self.listener.events[0]
141+
self.assertEqual(event.command["comment"], "foo")
142+
143+
# Get the index definition.
144+
self.listener.reset()
145+
await (await coll0.list_search_indexes(name=implicit_search_resp, comment="foo")).next()
146+
event = self.listener.events[0]
147+
self.assertEqual(event.command["comment"], "foo")
148+
149+
150+
class TestSearchIndexProse(SearchIndexIntegrationBase):
151+
db_name = "test_search_index_prose"
152+
153+
async def test_case_1(self):
154+
"""Driver can successfully create and list search indexes."""
155+
156+
# Create a collection with the "create" command using a randomly generated name (referred to as ``coll0``).
157+
coll0 = self.db[f"col{uuid.uuid4()}"]
158+
159+
# Create a new search index on ``coll0`` with the ``createSearchIndex`` helper. Use the following definition:
160+
model = {"name": _NAME, "definition": {"mappings": {"dynamic": False}}}
161+
await coll0.insert_one({})
162+
resp = await coll0.create_search_index(model)
163+
164+
# Assert that the command returns the name of the index: ``"test-search-index"``.
165+
self.assertEqual(resp, _NAME)
166+
167+
# Run ``coll0.listSearchIndexes()`` repeatedly every 5 seconds until the following condition is satisfied and store the value in a variable ``index``:
168+
# An index with the ``name`` of ``test-search-index`` is present and the index has a field ``queryable`` with a value of ``true``.
169+
index = await self.wait_for_ready(coll0)
170+
171+
# . Assert that ``index`` has a property ``latestDefinition`` whose value is ``{ 'mappings': { 'dynamic': false } }``
172+
self.assertIn("latestDefinition", index)
173+
self.assertEqual(index["latestDefinition"], model["definition"])
174+
175+
async def test_case_2(self):
176+
"""Driver can successfully create multiple indexes in batch."""
177+
178+
# Create a collection with the "create" command using a randomly generated name (referred to as ``coll0``).
179+
coll0 = self.db[f"col{uuid.uuid4()}"]
180+
await coll0.insert_one({})
181+
182+
# Create two new search indexes on ``coll0`` with the ``createSearchIndexes`` helper.
183+
name1 = "test-search-index-1"
184+
name2 = "test-search-index-2"
185+
definition = {"mappings": {"dynamic": False}}
186+
index_definitions: list[dict[str, Any]] = [
187+
{"name": name1, "definition": definition},
188+
{"name": name2, "definition": definition},
189+
]
190+
await coll0.create_search_indexes(
191+
[SearchIndexModel(i["definition"], i["name"]) for i in index_definitions]
192+
)
193+
194+
# .Assert that the command returns an array containing the new indexes' names: ``["test-search-index-1", "test-search-index-2"]``.
195+
indices = await (await coll0.list_search_indexes()).to_list()
196+
names = [i["name"] for i in indices]
197+
self.assertIn(name1, names)
198+
self.assertIn(name2, names)
199+
200+
# Run ``coll0.listSearchIndexes()`` repeatedly every 5 seconds until the following condition is satisfied.
201+
# An index with the ``name`` of ``test-search-index-1`` is present and index has a field ``queryable`` with the value of ``true``. Store result in ``index1``.
202+
# An index with the ``name`` of ``test-search-index-2`` is present and index has a field ``queryable`` with the value of ``true``. Store result in ``index2``.
203+
index1 = await self.wait_for_ready(coll0, name1)
204+
index2 = await self.wait_for_ready(coll0, name2)
205+
206+
# Assert that ``index1`` and ``index2`` have the property ``latestDefinition`` whose value is ``{ "mappings" : { "dynamic" : false } }``
207+
for index in [index1, index2]:
208+
self.assertIn("latestDefinition", index)
209+
self.assertEqual(index["latestDefinition"], definition)
210+
211+
async def test_case_3(self):
212+
"""Driver can successfully drop search indexes."""
213+
214+
# Create a collection with the "create" command using a randomly generated name (referred to as ``coll0``).
215+
coll0 = self.db[f"col{uuid.uuid4()}"]
216+
await coll0.insert_one({})
217+
218+
# Create a new search index on ``coll0``.
219+
model = {"name": _NAME, "definition": {"mappings": {"dynamic": False}}}
220+
resp = await coll0.create_search_index(model)
221+
222+
# Assert that the command returns the name of the index: ``"test-search-index"``.
223+
self.assertEqual(resp, "test-search-index")
224+
225+
# Run ``coll0.listSearchIndexes()`` repeatedly every 5 seconds until the following condition is satisfied:
226+
# An index with the ``name`` of ``test-search-index`` is present and index has a field ``queryable`` with the value of ``true``.
227+
await self.wait_for_ready(coll0)
228+
229+
# Run a ``dropSearchIndex`` on ``coll0``, using ``test-search-index`` for the name.
230+
await coll0.drop_search_index(_NAME)
231+
232+
# Run ``coll0.listSearchIndexes()`` repeatedly every 5 seconds until ``listSearchIndexes`` returns an empty array.
233+
t0 = time.time()
234+
while True:
235+
indices = await (await coll0.list_search_indexes()).to_list()
236+
if indices:
237+
break
238+
if (time.time() - t0) / 60 > 5:
239+
raise TimeoutError("Timed out waiting for index deletion")
240+
await asyncio.sleep(5)
241+
242+
async def test_case_4(self):
243+
"""Driver can update a search index."""
244+
# Create a collection with the "create" command using a randomly generated name (referred to as ``coll0``).
245+
coll0 = self.db[f"col{uuid.uuid4()}"]
246+
await coll0.insert_one({})
247+
248+
# Create a new search index on ``coll0``.
249+
model = {"name": _NAME, "definition": {"mappings": {"dynamic": False}}}
250+
resp = await coll0.create_search_index(model)
251+
252+
# Assert that the command returns the name of the index: ``"test-search-index"``.
253+
self.assertEqual(resp, _NAME)
254+
255+
# Run ``coll0.listSearchIndexes()`` repeatedly every 5 seconds until the following condition is satisfied:
256+
# An index with the ``name`` of ``test-search-index`` is present and index has a field ``queryable`` with the value of ``true``.
257+
await self.wait_for_ready(coll0)
258+
259+
# Run a ``updateSearchIndex`` on ``coll0``.
260+
# Assert that the command does not error and the server responds with a success.
261+
model2: dict[str, Any] = {"name": _NAME, "definition": {"mappings": {"dynamic": True}}}
262+
await coll0.update_search_index(_NAME, model2["definition"])
263+
264+
# Run ``coll0.listSearchIndexes()`` repeatedly every 5 seconds until the following condition is satisfied:
265+
# An index with the ``name`` of ``test-search-index`` is present. This index is referred to as ``index``.
266+
# The index has a field ``queryable`` with a value of ``true`` and has a field ``status`` with the value of ``READY``.
267+
predicate = lambda index: index.get("queryable") is True and index.get("status") == "READY"
268+
await self.wait_for_ready(coll0, predicate=predicate)
269+
270+
# Assert that an index is present with the name ``test-search-index`` and the definition has a property ``latestDefinition`` whose value is ``{ 'mappings': { 'dynamic': true } }``.
271+
index = (await (await coll0.list_search_indexes(_NAME)).to_list())[0]
272+
self.assertIn("latestDefinition", index)
273+
self.assertEqual(index["latestDefinition"], model2["definition"])
274+
275+
async def test_case_5(self):
276+
"""``dropSearchIndex`` suppresses namespace not found errors."""
277+
# Create a driver-side collection object for a randomly generated collection name. Do not create this collection on the server.
278+
coll0 = self.db[f"col{uuid.uuid4()}"]
279+
280+
# Run a ``dropSearchIndex`` command and assert that no error is thrown.
281+
await coll0.drop_search_index("foo")
282+
283+
async def test_case_6(self):
284+
"""Driver can successfully create and list search indexes with non-default readConcern and writeConcern."""
285+
# Create a collection with the "create" command using a randomly generated name (referred to as ``coll0``).
286+
coll0 = self.db[f"col{uuid.uuid4()}"]
287+
await coll0.insert_one({})
288+
289+
# Apply a write concern ``WriteConcern(w=1)`` and a read concern with ``ReadConcern(level="majority")`` to ``coll0``.
290+
coll0 = coll0.with_options(
291+
write_concern=WriteConcern(w="1"), read_concern=ReadConcern(level="majority")
292+
)
293+
294+
# Create a new search index on ``coll0`` with the ``createSearchIndex`` helper.
295+
name = "test-search-index-case6"
296+
model = {"name": name, "definition": {"mappings": {"dynamic": False}}}
297+
resp = await coll0.create_search_index(model)
298+
299+
# Assert that the command returns the name of the index: ``"test-search-index-case6"``.
300+
self.assertEqual(resp, name)
301+
302+
# Run ``coll0.listSearchIndexes()`` repeatedly every 5 seconds until the following condition is satisfied and store the value in a variable ``index``:
303+
# - An index with the ``name`` of ``test-search-index-case6`` is present and the index has a field ``queryable`` with a value of ``true``.
304+
index = await self.wait_for_ready(coll0, name)
305+
306+
# Assert that ``index`` has a property ``latestDefinition`` whose value is ``{ 'mappings': { 'dynamic': false } }``
307+
self.assertIn("latestDefinition", index)
308+
self.assertEqual(index["latestDefinition"], model["definition"])
309+
310+
async def test_case_7(self):
311+
"""Driver handles index types."""
312+
313+
# Create a collection with the "create" command using a randomly generated name (referred to as ``coll0``).
314+
coll0 = self.db[f"col{uuid.uuid4()}"]
315+
await coll0.insert_one({})
316+
317+
# Use these search and vector search definitions for indexes.
318+
search_definition = {"mappings": {"dynamic": False}}
319+
vector_search_definition = {
320+
"fields": [
321+
{
322+
"type": "vector",
323+
"path": "plot_embedding",
324+
"numDimensions": 1536,
325+
"similarity": "euclidean",
326+
},
327+
]
328+
}
329+
330+
# Create a new search index on ``coll0`` that implicitly passes its type.
331+
implicit_search_resp = await coll0.create_search_index(
332+
model={"name": _NAME + "-implicit", "definition": search_definition}
333+
)
334+
335+
# Get the index definition.
336+
resp = await (await coll0.list_search_indexes(name=implicit_search_resp)).next()
337+
338+
# Assert that the index model contains the correct index type: ``"search"``.
339+
self.assertEqual(resp["type"], "search")
340+
341+
# Create a new search index on ``coll0`` that explicitly passes its type.
342+
explicit_search_resp = await coll0.create_search_index(
343+
model={"name": _NAME + "-explicit", "type": "search", "definition": search_definition}
344+
)
345+
346+
# Get the index definition.
347+
resp = await (await coll0.list_search_indexes(name=explicit_search_resp)).next()
348+
349+
# Assert that the index model contains the correct index type: ``"search"``.
350+
self.assertEqual(resp["type"], "search")
351+
352+
# Create a new vector search index on ``coll0`` that explicitly passes its type.
353+
explicit_vector_resp = await coll0.create_search_index(
354+
model={
355+
"name": _NAME + "-vector",
356+
"type": "vectorSearch",
357+
"definition": vector_search_definition,
358+
}
359+
)
360+
361+
# Get the index definition.
362+
resp = await (await coll0.list_search_indexes(name=explicit_vector_resp)).next()
363+
364+
# Assert that the index model contains the correct index type: ``"vectorSearch"``.
365+
self.assertEqual(resp["type"], "vectorSearch")
366+
367+
# Catch the error raised when trying to create a vector search index without specifying the type
368+
with self.assertRaises(OperationFailure) as e:
369+
await coll0.create_search_index(
370+
model={"name": _NAME + "-error", "definition": vector_search_definition}
371+
)
372+
self.assertIn("Attribute mappings missing.", e.exception.details["errmsg"])
373+
374+
375+
globals().update(
376+
generate_test_classes(
377+
_TEST_PATH,
378+
module=__name__,
379+
)
380+
)
381+
382+
if __name__ == "__main__":
383+
unittest.main()

0 commit comments

Comments
 (0)