Skip to content

Commit 26f7a11

Browse files
committed
PYTHON-5763 Improve async test coverage
1 parent 13085ff commit 26f7a11

File tree

10 files changed

+2550
-0
lines changed

10 files changed

+2550
-0
lines changed

test/asynchronous/test_bulk.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,227 @@ async def test_large_inserts_unordered(self):
781781
self.assertEqual(6, result.inserted_count)
782782
self.assertEqual(6, await self.coll.count_documents({}))
783783

784+
async def test_bulk_write_with_comment(self):
785+
"""Test bulk write operations with comment parameter."""
786+
requests = [
787+
InsertOne({"x": 1}),
788+
UpdateOne({"x": 1}, {"$set": {"y": 1}}),
789+
DeleteOne({"x": 1}),
790+
]
791+
result = await self.coll.bulk_write(requests, comment="bulk_comment")
792+
self.assertEqual(1, result.inserted_count)
793+
self.assertEqual(1, result.modified_count)
794+
self.assertEqual(1, result.deleted_count)
795+
796+
async def test_bulk_write_with_let(self):
797+
"""Test bulk write operations with let parameter."""
798+
if not async_client_context.version.at_least(5, 0):
799+
self.skipTest("let parameter requires MongoDB 5.0+")
800+
801+
await self.coll.insert_one({"x": 1})
802+
requests = [
803+
UpdateOne({"$expr": {"$eq": ["$x", "$$targetVal"]}}, {"$set": {"updated": True}}),
804+
]
805+
result = await self.coll.bulk_write(requests, let={"targetVal": 1})
806+
self.assertEqual(1, result.modified_count)
807+
808+
async def test_bulk_write_all_operation_types(self):
809+
"""Test bulk write with all operation types combined."""
810+
await self.coll.insert_many([{"x": i} for i in range(5)])
811+
812+
requests = [
813+
InsertOne({"x": 100}),
814+
UpdateOne({"x": 0}, {"$set": {"updated": True}}),
815+
UpdateMany({"x": {"$lte": 2}}, {"$set": {"batch_updated": True}}),
816+
ReplaceOne({"x": 3}, {"x": 3, "replaced": True}),
817+
DeleteOne({"x": 4}),
818+
DeleteMany({"x": {"$gt": 50}}),
819+
]
820+
result = await self.coll.bulk_write(requests)
821+
822+
self.assertEqual(1, result.inserted_count)
823+
self.assertGreaterEqual(result.modified_count, 1)
824+
self.assertGreaterEqual(result.deleted_count, 1)
825+
826+
async def test_bulk_write_unordered(self):
827+
"""Test unordered bulk write continues after error."""
828+
await self.coll.create_index([("x", 1)], unique=True)
829+
self.addAsyncCleanup(self.coll.drop_index, [("x", 1)])
830+
831+
requests = [
832+
InsertOne({"x": 1}),
833+
InsertOne({"x": 1}), # Duplicate - will error
834+
InsertOne({"x": 2}),
835+
InsertOne({"x": 3}),
836+
]
837+
838+
with self.assertRaises(BulkWriteError) as ctx:
839+
await self.coll.bulk_write(requests, ordered=False)
840+
841+
# With unordered, should have inserted 3 documents
842+
self.assertEqual(3, ctx.exception.details["nInserted"])
843+
844+
async def test_bulk_write_ordered(self):
845+
"""Test ordered bulk write stops on first error."""
846+
await self.coll.create_index([("x", 1)], unique=True)
847+
self.addAsyncCleanup(self.coll.drop_index, [("x", 1)])
848+
849+
requests = [
850+
InsertOne({"x": 1}),
851+
InsertOne({"x": 1}), # Duplicate - will error
852+
InsertOne({"x": 2}),
853+
InsertOne({"x": 3}),
854+
]
855+
856+
with self.assertRaises(BulkWriteError) as ctx:
857+
await self.coll.bulk_write(requests, ordered=True)
858+
859+
# With ordered, should have inserted only 1 document
860+
self.assertEqual(1, ctx.exception.details["nInserted"])
861+
862+
async def test_bulk_write_bypass_document_validation(self):
863+
"""Test bulk write with bypass_document_validation."""
864+
if not async_client_context.version.at_least(3, 2):
865+
self.skipTest("bypass_document_validation requires MongoDB 3.2+")
866+
867+
# Create collection with validator
868+
await self.coll.drop()
869+
await self.db.create_collection(
870+
self.coll.name, validator={"$jsonSchema": {"required": ["name"]}}
871+
)
872+
873+
# Without bypass, should fail
874+
with self.assertRaises(BulkWriteError):
875+
await self.coll.bulk_write([InsertOne({"x": 1})])
876+
877+
# With bypass, should succeed
878+
result = await self.coll.bulk_write([InsertOne({"x": 1})], bypass_document_validation=True)
879+
self.assertEqual(1, result.inserted_count)
880+
881+
async def test_bulk_write_result_properties(self):
882+
"""Test all BulkWriteResult properties."""
883+
await self.coll.insert_one({"x": 1})
884+
885+
requests = [
886+
InsertOne({"x": 2}),
887+
UpdateOne({"x": 1}, {"$set": {"updated": True}}),
888+
ReplaceOne({"x": 2}, {"x": 2, "replaced": True}, upsert=True),
889+
DeleteOne({"x": 1}),
890+
]
891+
result = await self.coll.bulk_write(requests)
892+
893+
# Check all properties
894+
self.assertTrue(result.acknowledged)
895+
self.assertEqual(1, result.inserted_count)
896+
self.assertGreaterEqual(result.matched_count, 0)
897+
self.assertGreaterEqual(result.modified_count, 0)
898+
self.assertEqual(1, result.deleted_count)
899+
self.assertIsInstance(result.upserted_count, int)
900+
self.assertIsInstance(result.upserted_ids, dict)
901+
902+
async def test_bulk_write_with_upsert(self):
903+
"""Test bulk write upsert operations."""
904+
requests = [
905+
UpdateOne({"x": 1}, {"$set": {"y": 1}}, upsert=True),
906+
UpdateOne({"x": 2}, {"$set": {"y": 2}}, upsert=True),
907+
ReplaceOne({"x": 3}, {"x": 3, "y": 3}, upsert=True),
908+
]
909+
result = await self.coll.bulk_write(requests)
910+
911+
self.assertEqual(3, result.upserted_count)
912+
self.assertEqual(3, len(result.upserted_ids))
913+
914+
async def test_update_one_with_hint(self):
915+
"""Test UpdateOne with hint parameter."""
916+
await self.coll.create_index([("x", 1)])
917+
self.addAsyncCleanup(self.coll.drop_index, [("x", 1)])
918+
919+
await self.coll.insert_one({"x": 1})
920+
921+
requests = [UpdateOne({"x": 1}, {"$set": {"y": 1}}, hint=[("x", 1)])]
922+
result = await self.coll.bulk_write(requests)
923+
self.assertEqual(1, result.modified_count)
924+
925+
async def test_update_many_with_hint(self):
926+
"""Test UpdateMany with hint parameter."""
927+
await self.coll.create_index([("x", 1)])
928+
self.addAsyncCleanup(self.coll.drop_index, [("x", 1)])
929+
930+
await self.coll.insert_many([{"x": 1}, {"x": 1}])
931+
932+
requests = [UpdateMany({"x": 1}, {"$set": {"y": 1}}, hint=[("x", 1)])]
933+
result = await self.coll.bulk_write(requests)
934+
self.assertEqual(2, result.modified_count)
935+
936+
async def test_delete_one_with_hint(self):
937+
"""Test DeleteOne with hint parameter."""
938+
await self.coll.create_index([("x", 1)])
939+
self.addAsyncCleanup(self.coll.drop_index, [("x", 1)])
940+
941+
await self.coll.insert_one({"x": 1})
942+
943+
requests = [DeleteOne({"x": 1}, hint=[("x", 1)])]
944+
result = await self.coll.bulk_write(requests)
945+
self.assertEqual(1, result.deleted_count)
946+
947+
async def test_delete_many_with_hint(self):
948+
"""Test DeleteMany with hint parameter."""
949+
await self.coll.create_index([("x", 1)])
950+
self.addAsyncCleanup(self.coll.drop_index, [("x", 1)])
951+
952+
await self.coll.insert_many([{"x": 1}, {"x": 1}])
953+
954+
requests = [DeleteMany({"x": 1}, hint=[("x", 1)])]
955+
result = await self.coll.bulk_write(requests)
956+
self.assertEqual(2, result.deleted_count)
957+
958+
async def test_update_one_with_array_filters(self):
959+
"""Test UpdateOne with array_filters parameter."""
960+
await self.coll.insert_one({"x": [{"y": 1}, {"y": 2}, {"y": 3}]})
961+
962+
requests = [
963+
UpdateOne({}, {"$set": {"x.$[elem].z": 1}}, array_filters=[{"elem.y": {"$gt": 1}}])
964+
]
965+
result = await self.coll.bulk_write(requests)
966+
self.assertEqual(1, result.modified_count)
967+
968+
doc = await self.coll.find_one()
969+
# Elements with y > 1 should have z = 1
970+
for elem in doc["x"]:
971+
if elem["y"] > 1:
972+
self.assertEqual(1, elem.get("z"))
973+
974+
async def test_replace_one_with_hint(self):
975+
"""Test ReplaceOne with hint parameter."""
976+
await self.coll.create_index([("x", 1)])
977+
self.addAsyncCleanup(self.coll.drop_index, [("x", 1)])
978+
979+
await self.coll.insert_one({"x": 1})
980+
981+
requests = [ReplaceOne({"x": 1}, {"x": 1, "replaced": True}, hint=[("x", 1)])]
982+
result = await self.coll.bulk_write(requests)
983+
self.assertEqual(1, result.modified_count)
984+
985+
async def test_update_with_collation(self):
986+
"""Test update operations with collation."""
987+
await self.coll.insert_many(
988+
[
989+
{"name": "cafe"},
990+
{"name": "Cafe"},
991+
]
992+
)
993+
994+
requests = [
995+
UpdateMany(
996+
{"name": "cafe"},
997+
{"$set": {"updated": True}},
998+
collation={"locale": "en", "strength": 2},
999+
)
1000+
]
1001+
result = await self.coll.bulk_write(requests)
1002+
# With case-insensitive collation, both docs should match
1003+
self.assertEqual(2, result.modified_count)
1004+
7841005

7851006
class AsyncBulkAuthorizationTestBase(AsyncBulkTestBase):
7861007
@async_client_context.require_auth

test/asynchronous/test_change_stream.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,5 +1152,122 @@ def asyncTearDown(self):
11521152
)
11531153

11541154

1155+
class AsyncTestChangeStreamCoverage(TestAsyncCollectionAsyncChangeStream):
1156+
"""Additional tests to improve code coverage for AsyncChangeStream."""
1157+
1158+
async def test_change_stream_alive_property(self):
1159+
"""Test alive property state transitions."""
1160+
async with await self.change_stream() as cs:
1161+
self.assertTrue(cs.alive)
1162+
# After context exit, should be closed
1163+
self.assertFalse(cs.alive)
1164+
1165+
async def test_change_stream_idempotent_close(self):
1166+
"""Test that close() can be called multiple times safely."""
1167+
cs = await self.change_stream()
1168+
await cs.close()
1169+
# Second close should not raise
1170+
await cs.close()
1171+
self.assertFalse(cs.alive)
1172+
1173+
async def test_change_stream_resume_token_deepcopy(self):
1174+
"""Test that resume_token returns a deep copy."""
1175+
coll = self.watched_collection()
1176+
async with await self.change_stream() as cs:
1177+
await coll.insert_one({"x": 1})
1178+
await anext(cs) # Consume the change event
1179+
token1 = cs.resume_token
1180+
token2 = cs.resume_token
1181+
# Should be equal but different objects
1182+
self.assertEqual(token1, token2)
1183+
self.assertIsNot(token1, token2)
1184+
1185+
async def test_change_stream_with_comment(self):
1186+
"""Test change stream with comment parameter."""
1187+
client, listener = await self.client_with_listener("aggregate")
1188+
try:
1189+
async with await self.change_stream_with_client(client, comment="test_comment"):
1190+
pass
1191+
finally:
1192+
await client.close()
1193+
1194+
# Check that comment was in the aggregate command
1195+
self.assertGreater(len(listener.started_events), 0)
1196+
cmd = listener.started_events[0].command
1197+
self.assertEqual("test_comment", cmd.get("comment"))
1198+
1199+
async def test_change_stream_with_show_expanded_events(self):
1200+
"""Test change stream with show_expanded_events parameter."""
1201+
if not async_client_context.version.at_least(6, 0):
1202+
self.skipTest("show_expanded_events requires MongoDB 6.0+")
1203+
1204+
async with await self.change_stream(show_expanded_events=True) as cs:
1205+
# Just verify it doesn't error
1206+
self.assertTrue(cs.alive)
1207+
1208+
@async_client_context.require_version_min(6, 0)
1209+
async def test_change_stream_with_full_document_before_change(self):
1210+
"""Test change stream with full_document_before_change parameter."""
1211+
coll = self.watched_collection()
1212+
# Need to ensure collection exists with changeStreamPreAndPostImages enabled
1213+
await coll.drop()
1214+
await self.db.create_collection(coll.name, changeStreamPreAndPostImages={"enabled": True})
1215+
await coll.insert_one({"x": 1})
1216+
1217+
async with await self.change_stream(full_document_before_change="whenAvailable") as cs:
1218+
await coll.update_one({"x": 1}, {"$set": {"x": 2}})
1219+
change = await anext(cs)
1220+
self.assertEqual("update", change["operationType"])
1221+
# fullDocumentBeforeChange should be present
1222+
self.assertIn("fullDocumentBeforeChange", change)
1223+
1224+
async def test_change_stream_next_after_close(self):
1225+
"""Test that next() on closed stream raises StopAsyncIteration."""
1226+
cs = await self.change_stream()
1227+
await cs.close()
1228+
with self.assertRaises(StopAsyncIteration):
1229+
await anext(cs)
1230+
1231+
async def test_change_stream_try_next_after_close(self):
1232+
"""Test that try_next() on closed stream raises StopAsyncIteration."""
1233+
cs = await self.change_stream()
1234+
await cs.close()
1235+
with self.assertRaises(StopAsyncIteration):
1236+
await cs.try_next()
1237+
1238+
async def test_change_stream_pipeline_construction(self):
1239+
"""Test change stream pipeline is properly constructed."""
1240+
pipeline = [{"$match": {"operationType": "insert"}}]
1241+
client, listener = await self.client_with_listener("aggregate")
1242+
try:
1243+
async with await self.change_stream_with_client(client, pipeline=pipeline):
1244+
pass
1245+
finally:
1246+
await client.close()
1247+
1248+
cmd = listener.started_events[0].command
1249+
agg_pipeline = cmd["pipeline"]
1250+
# First stage should be $changeStream
1251+
self.assertIn("$changeStream", agg_pipeline[0])
1252+
# Second stage should be our match
1253+
self.assertEqual({"$match": {"operationType": "insert"}}, agg_pipeline[1])
1254+
1255+
async def test_change_stream_empty_pipeline(self):
1256+
"""Test change stream with empty pipeline."""
1257+
async with await self.change_stream(pipeline=[]) as cs:
1258+
self.assertTrue(cs.alive)
1259+
1260+
async def test_change_stream_context_manager_exception(self):
1261+
"""Test change stream context manager closes on exception."""
1262+
cs = None
1263+
try:
1264+
async with await self.change_stream() as cs:
1265+
raise ValueError("test exception")
1266+
except ValueError:
1267+
pass
1268+
# Stream should be closed
1269+
self.assertFalse(cs.alive)
1270+
1271+
11551272
if __name__ == "__main__":
11561273
unittest.main()

0 commit comments

Comments
 (0)