SPEC 1153: Adding readConcern support to aggregation with $out (#400)

This commit is contained in:
Prashant Mital 2019-02-20 16:21:39 -08:00 committed by Shane Harvey
parent 3030a5a094
commit 2b1fdb091d
46 changed files with 2223 additions and 258 deletions

View File

@ -2276,11 +2276,13 @@ class Collection(common.BaseObject):
kwargs["cursor"]["batchSize"] = first_batch_size
cmd.update(kwargs)
# Apply this Collection's read concern if $out is not in the
# pipeline.
if (sock_info.max_wire_version >= 4
and 'readConcern' not in cmd
and not dollar_out):
# Apply this Collection's read concern if
# readConcern has not been specified as a kwarg and either
# - server version is >= 4.2 or
# - server version is >= 3.2 and pipeline doesn't use $out
if (('readConcern' not in cmd) and
((sock_info.max_wire_version >= 4 and not dollar_out) or
(sock_info.max_wire_version >= 8))):
read_concern = self.read_concern
else:
read_concern = None

View File

@ -0,0 +1,39 @@
{
"data": [],
"tests": [
{
"description": "Estimated document count with empty collection",
"operation": {
"name": "estimatedDocumentCount",
"arguments": {}
},
"outcome": {
"result": 0
}
},
{
"description": "Count documents with empty collection",
"operation": {
"name": "countDocuments",
"arguments": {
"filter": {}
}
},
"outcome": {
"result": 0
}
},
{
"description": "Deprecated count with empty collection",
"operation": {
"name": "count",
"arguments": {
"filter": {}
}
},
"outcome": {
"result": 0
}
}
]
}

View File

@ -97,7 +97,7 @@
{
"description": "Deprecated count with skip and limit",
"operation": {
"name": "countDocuments",
"name": "count",
"arguments": {
"filter": {},
"skip": 1,

View File

@ -28,48 +28,57 @@
{
"description": "BulkWrite with arrayFilters",
"operation": {
"name": "bulkWrite",
"arguments": {
"options": {
"ordered": true
},
"requests": [
{
"name": "updateOne",
"arguments": {
"filter": {},
"update": {
"$set": {
"y.$[i].b": 2
}
},
"arrayFilters": [
{
"i.b": 3
}
],
]
}
},
{
"name": "updateMany",
"arguments": {
"filter": {},
"update": {
"$set": {
"y.$[i].b": 2
}
}
},
"name": "updateOne"
},
{
"arguments": {
},
"arrayFilters": [
{
"i.b": 1
}
],
"filter": {},
"update": {
"$set": {
"y.$[i].b": 2
}
}
},
"name": "updateMany"
]
}
}
]
},
"name": "bulkWrite"
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 0,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 3,
"modifiedCount": 3,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
@ -95,16 +104,8 @@
]
}
]
},
"result": {
"deletedCount": 0,
"insertedIds": {},
"matchedCount": 3,
"modifiedCount": 3,
"upsertedCount": 0,
"upsertedIds": {}
}
}
}
]
}
}

View File

@ -0,0 +1,217 @@
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": "ping"
},
{
"_id": 3,
"x": "pINg"
},
{
"_id": 4,
"x": "pong"
},
{
"_id": 5,
"x": "pONg"
}
],
"minServerVersion": "3.4",
"tests": [
{
"description": "BulkWrite with delete operations and collation",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "deleteOne",
"arguments": {
"filter": {
"x": "PING"
},
"collation": {
"locale": "en_US",
"strength": 2
}
}
},
{
"name": "deleteOne",
"arguments": {
"filter": {
"x": "PING"
},
"collation": {
"locale": "en_US",
"strength": 2
}
}
},
{
"name": "deleteMany",
"arguments": {
"filter": {
"x": "PONG"
},
"collation": {
"locale": "en_US",
"strength": 2
}
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 4,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
}
]
}
}
},
{
"description": "BulkWrite with update operations and collation",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "updateMany",
"arguments": {
"filter": {
"x": "ping"
},
"update": {
"$set": {
"x": "PONG"
}
},
"collation": {
"locale": "en_US",
"strength": 3
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"x": "ping"
},
"update": {
"$set": {
"x": "PONG"
}
},
"collation": {
"locale": "en_US",
"strength": 2
}
}
},
{
"name": "replaceOne",
"arguments": {
"filter": {
"x": "ping"
},
"replacement": {
"_id": 6,
"x": "ping"
},
"upsert": true,
"collation": {
"locale": "en_US",
"strength": 3
}
}
},
{
"name": "updateMany",
"arguments": {
"filter": {
"x": "pong"
},
"update": {
"$set": {
"x": "PONG"
}
},
"collation": {
"locale": "en_US",
"strength": 2
}
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 0,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 6,
"modifiedCount": 4,
"upsertedCount": 1,
"upsertedIds": {
"2": 6
}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": "PONG"
},
{
"_id": 3,
"x": "PONG"
},
{
"_id": 4,
"x": "PONG"
},
{
"_id": 5,
"x": "PONG"
},
{
"_id": 6,
"x": "ping"
}
]
}
}
}
]
}

View File

@ -0,0 +1,778 @@
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"minServerVersion": "2.6",
"tests": [
{
"description": "BulkWrite with deleteOne operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "deleteOne",
"arguments": {
"filter": {
"_id": 3
}
}
},
{
"name": "deleteOne",
"arguments": {
"filter": {
"_id": 2
}
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 1,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
}
]
}
}
},
{
"description": "BulkWrite with deleteMany operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "deleteMany",
"arguments": {
"filter": {
"x": {
"$lt": 11
}
}
}
},
{
"name": "deleteMany",
"arguments": {
"filter": {
"x": {
"$lte": 22
}
}
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 2,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": []
}
}
},
{
"description": "BulkWrite with insertOne operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 3,
"x": 33
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 4,
"x": 44
}
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 0,
"insertedCount": 2,
"insertedIds": {
"0": 3,
"1": 4
},
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
}
]
}
}
},
{
"description": "BulkWrite with replaceOne operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "replaceOne",
"arguments": {
"filter": {
"_id": 3
},
"replacement": {
"x": 33
}
}
},
{
"name": "replaceOne",
"arguments": {
"filter": {
"_id": 1
},
"replacement": {
"x": 12
}
}
},
{
"name": "replaceOne",
"arguments": {
"filter": {
"_id": 3
},
"replacement": {
"x": 33
},
"upsert": true
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 0,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 1,
"modifiedCount": 1,
"upsertedCount": 1,
"upsertedIds": {
"2": 3
}
},
"collection": {
"data": [
{
"_id": 1,
"x": 12
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "BulkWrite with updateOne operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 0
},
"update": {
"$set": {
"x": 0
}
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"x": 11
}
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 2
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 3
},
"update": {
"$set": {
"x": 33
}
},
"upsert": true
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 0,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 2,
"modifiedCount": 1,
"upsertedCount": 1,
"upsertedIds": {
"3": 3
}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 23
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "BulkWrite with updateMany operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "updateMany",
"arguments": {
"filter": {
"x": {
"$lt": 11
}
},
"update": {
"$set": {
"x": 0
}
}
}
},
{
"name": "updateMany",
"arguments": {
"filter": {
"x": {
"$lte": 22
}
},
"update": {
"$unset": {
"y": 1
}
}
}
},
{
"name": "updateMany",
"arguments": {
"filter": {
"x": {
"$lte": 22
}
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"name": "updateMany",
"arguments": {
"filter": {
"_id": 3
},
"update": {
"$set": {
"x": 33
}
},
"upsert": true
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 0,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 4,
"modifiedCount": 2,
"upsertedCount": 1,
"upsertedIds": {
"3": 3
}
},
"collection": {
"data": [
{
"_id": 1,
"x": 12
},
{
"_id": 2,
"x": 23
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "BulkWrite with mixed ordered operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 3,
"x": 33
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 2
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"name": "updateMany",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
},
"update": {
"$inc": {
"x": 1
}
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 4,
"x": 44
}
}
},
{
"name": "deleteMany",
"arguments": {
"filter": {
"x": {
"$nin": [
24,
34
]
}
}
}
},
{
"name": "replaceOne",
"arguments": {
"filter": {
"_id": 4
},
"replacement": {
"_id": 4,
"x": 44
},
"upsert": true
}
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"deletedCount": 2,
"insertedCount": 2,
"insertedIds": {
"0": 3,
"3": 4
},
"matchedCount": 3,
"modifiedCount": 3,
"upsertedCount": 1,
"upsertedIds": {
"5": 4
}
},
"collection": {
"data": [
{
"_id": 2,
"x": 24
},
{
"_id": 3,
"x": 34
},
{
"_id": 4,
"x": 44
}
]
}
}
},
{
"description": "BulkWrite with mixed unordered operations",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "replaceOne",
"arguments": {
"filter": {
"_id": 3
},
"replacement": {
"_id": 3,
"x": 33
},
"upsert": true
}
},
{
"name": "deleteOne",
"arguments": {
"filter": {
"_id": 1
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 2
},
"update": {
"$inc": {
"x": 1
}
}
}
}
],
"options": {
"ordered": false
}
}
},
"outcome": {
"result": {
"deletedCount": 1,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 1,
"modifiedCount": 1,
"upsertedCount": 1,
"upsertedIds": {
"0": 3
}
},
"collection": {
"data": [
{
"_id": 2,
"x": 23
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "BulkWrite continue-on-error behavior with unordered (preexisting duplicate key)",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 2,
"x": 22
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 3,
"x": 33
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 4,
"x": 44
}
}
}
],
"options": {
"ordered": false
}
}
},
"outcome": {
"error": true,
"result": {
"deletedCount": 0,
"insertedCount": 2,
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
}
]
}
}
},
{
"description": "BulkWrite continue-on-error behavior with unordered (duplicate key in requests)",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 3,
"x": 33
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 3,
"x": 33
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 4,
"x": 44
}
}
}
],
"options": {
"ordered": false
}
}
},
"outcome": {
"error": true,
"result": {
"deletedCount": 0,
"insertedCount": 2,
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
}
]
}
}
}
]
}

View File

@ -0,0 +1,159 @@
{
"data": [
{
"_id": 1,
"x": 11
}
],
"tests": [
{
"description": "InsertMany with non-existing documents",
"operation": {
"name": "insertMany",
"arguments": {
"documents": [
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"options": {
"ordered": true
}
}
},
"outcome": {
"result": {
"insertedIds": {
"0": 2,
"1": 3
}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "InsertMany continue-on-error behavior with unordered (preexisting duplicate key)",
"operation": {
"name": "insertMany",
"arguments": {
"documents": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"options": {
"ordered": false
}
}
},
"outcome": {
"error": true,
"result": {
"deletedCount": 0,
"insertedCount": 2,
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "InsertMany continue-on-error behavior with unordered (duplicate key in requests)",
"operation": {
"name": "insertMany",
"arguments": {
"documents": [
{
"_id": 2,
"x": 22
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"options": {
"ordered": false
}
}
},
"outcome": {
"error": true,
"result": {
"deletedCount": 0,
"insertedCount": 2,
"matchedCount": 0,
"modifiedCount": 0,
"upsertedCount": 0,
"upsertedIds": {}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
}
]
}

View File

@ -46,7 +46,8 @@
"outcome": {
"result": {
"matchedCount": 2,
"modifiedCount": 0
"modifiedCount": 0,
"upsertedCount": 0
},
"collection": {
"data": [
@ -97,7 +98,8 @@
"outcome": {
"result": {
"matchedCount": 2,
"modifiedCount": 1
"modifiedCount": 1,
"upsertedCount": 0
},
"collection": {
"data": [
@ -148,7 +150,8 @@
"outcome": {
"result": {
"matchedCount": 2,
"modifiedCount": 2
"modifiedCount": 2,
"upsertedCount": 0
},
"collection": {
"data": [

View File

@ -62,7 +62,8 @@
"outcome": {
"result": {
"matchedCount": 1,
"modifiedCount": 0
"modifiedCount": 0,
"upsertedCount": 0
},
"collection": {
"data": [
@ -129,7 +130,8 @@
"outcome": {
"result": {
"matchedCount": 1,
"modifiedCount": 1
"modifiedCount": 1,
"upsertedCount": 0
},
"collection": {
"data": [
@ -196,7 +198,8 @@
"outcome": {
"result": {
"matchedCount": 1,
"modifiedCount": 1
"modifiedCount": 1,
"upsertedCount": 0
},
"collection": {
"data": [
@ -268,7 +271,8 @@
"outcome": {
"result": {
"matchedCount": 1,
"modifiedCount": 0
"modifiedCount": 0,
"upsertedCount": 0
},
"collection": {
"data": [
@ -340,7 +344,8 @@
"outcome": {
"result": {
"matchedCount": 1,
"modifiedCount": 1
"modifiedCount": 1,
"upsertedCount": 0
},
"collection": {
"data": [

View File

@ -0,0 +1,377 @@
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"minServerVersion": "4.1",
"collection_name": "test_aggregate_out_readconcern",
"tests": [
{
"description": "readConcern majority with out stage",
"operations": [
{
"object": "collection",
"name": "aggregate",
"collectionOptions": {
"readConcern": {
"level": "majority"
}
},
"arguments": {
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
]
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "test_aggregate_out_readconcern",
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
],
"readConcern": {
"level": "majority"
}
}
}
}
],
"outcome": {
"collection": {
"name": "other_test_collection",
"data": [
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "readConcern local with out stage",
"operations": [
{
"object": "collection",
"name": "aggregate",
"collectionOptions": {
"readConcern": {
"level": "local"
}
},
"arguments": {
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
]
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "test_aggregate_out_readconcern",
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
],
"readConcern": {
"level": "local"
}
}
}
}
],
"outcome": {
"collection": {
"name": "other_test_collection",
"data": [
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "readConcern available with out stage",
"operations": [
{
"object": "collection",
"name": "aggregate",
"collectionOptions": {
"readConcern": {
"level": "available"
}
},
"arguments": {
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
]
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "test_aggregate_out_readconcern",
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
],
"readConcern": {
"level": "available"
}
}
}
}
],
"outcome": {
"collection": {
"name": "other_test_collection",
"data": [
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
},
{
"description": "readConcern linearizable with out stage",
"operations": [
{
"object": "collection",
"name": "aggregate",
"collectionOptions": {
"readConcern": {
"level": "linearizable"
}
},
"arguments": {
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
]
},
"error": true
}
],
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "test_aggregate_out_readconcern",
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
],
"readConcern": {
"level": "linearizable"
}
}
}
}
]
},
{
"description": "invalid readConcern with out stage",
"operations": [
{
"object": "collection",
"name": "aggregate",
"collectionOptions": {
"readConcern": {
"level": "!invalid123"
}
},
"arguments": {
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
]
},
"error": true
}
],
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "test_aggregate_out_readconcern",
"pipeline": [
{
"$sort": {
"x": 1
}
},
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_test_collection"
}
],
"readConcern": {
"level": "!invalid123"
}
}
}
}
]
}
]
}

View File

@ -0,0 +1,156 @@
{
"data": [
{
"_id": 1,
"y": [
{
"b": 3
},
{
"b": 1
}
]
},
{
"_id": 2,
"y": [
{
"b": 0
},
{
"b": 1
}
]
}
],
"minServerVersion": "3.5.6",
"collection_name": "test",
"database_name": "crud-tests",
"tests": [
{
"description": "BulkWrite with arrayFilters",
"operations": [
{
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "updateOne",
"arguments": {
"filter": {},
"update": {
"$set": {
"y.$[i].b": 2
}
},
"arrayFilters": [
{
"i.b": 3
}
]
}
},
{
"name": "updateMany",
"arguments": {
"filter": {},
"update": {
"$set": {
"y.$[i].b": 2
}
},
"arrayFilters": [
{
"i.b": 1
}
]
}
}
],
"options": {
"ordered": true
}
},
"result": {
"deletedCount": 0,
"insertedCount": 0,
"insertedIds": {},
"matchedCount": 3,
"modifiedCount": 3,
"upsertedCount": 0,
"upsertedIds": {}
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"updates": [
{
"q": {},
"u": {
"$set": {
"y.$[i].b": 2
}
},
"arrayFilters": [
{
"i.b": 3
}
]
},
{
"q": {},
"u": {
"$set": {
"y.$[i].b": 2
}
},
"multi": true,
"arrayFilters": [
{
"i.b": 1
}
]
}
],
"ordered": true
},
"command_name": "update",
"database_name": "crud-tests"
}
}
],
"outcome": {
"collection": {
"data": [
{
"_id": 1,
"y": [
{
"b": 2
},
{
"b": 2
}
]
},
{
"_id": 2,
"y": [
{
"b": 0
},
{
"b": 2
}
]
}
]
}
}
}
]
}

View File

@ -1,52 +0,0 @@
{
"data": [
{
"_id": 1,
"x": 11
}
],
"tests": [
{
"description": "InsertMany with non-existing documents",
"operation": {
"name": "insertMany",
"arguments": {
"documents": [
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
},
"outcome": {
"result": {
"insertedIds": {
"0": 2,
"1": 3
}
},
"collection": {
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
}
}
]
}

View File

@ -22,9 +22,11 @@ import sys
sys.path[0:0] = [""]
from bson.py3compat import iteritems
from pymongo import operations
from pymongo import operations, WriteConcern
from pymongo.command_cursor import CommandCursor
from pymongo.cursor import Cursor
from pymongo.errors import PyMongoError
from pymongo.read_concern import ReadConcern
from pymongo.results import _WriteResult, BulkWriteResult
from pymongo.operations import (InsertOne,
DeleteOne,
@ -34,27 +36,18 @@ from pymongo.operations import (InsertOne,
UpdateMany)
from test import unittest, client_context, IntegrationTest
from test.utils import drop_collections
from test.utils import (camel_to_snake, camel_to_upper_camel,
camel_to_snake_args, drop_collections, TestCreator)
# Location of JSON test specifications.
_TEST_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'crud')
os.path.dirname(os.path.realpath(__file__)), 'crud', 'v1')
class TestAllScenarios(IntegrationTest):
pass
def camel_to_snake(camel):
# Regex to convert CamelCase to snake_case.
snake = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake).lower()
def camel_to_upper_camel(camel):
return camel[0].upper() + camel[1:]
def check_result(expected_result, result):
if isinstance(result, Cursor) or isinstance(result, CommandCursor):
return list(result) == expected_result
@ -96,19 +89,12 @@ def check_result(expected_result, result):
return False
return True
else:
if not expected_result:
if expected_result is None:
return result is None
else:
return result == expected_result
def camel_to_snake_args(arguments):
for arg_name in list(arguments):
c2s = camel_to_snake(arg_name)
arguments[c2s] = arguments.pop(arg_name)
return arguments
def run_operation(collection, test):
# Convert command from CamelCase to pymongo.collection method.
operation = camel_to_snake(test['operation']['name'])
@ -156,14 +142,25 @@ def run_operation(collection, test):
return result
def create_test(scenario_def, test):
def create_test(scenario_def, test, name):
def run_scenario(self):
# Load data.
assert scenario_def['data'], "tests must have non-empty data"
# Cleanup state and load data (if provided).
drop_collections(self.db)
self.db.test.insert_many(scenario_def['data'])
data = scenario_def.get('data')
if data:
self.db.test.with_options(
write_concern=WriteConcern(w="majority")).insert_many(
scenario_def['data'])
result = run_operation(self.db.test, test)
# Run operations and check results or errors.
expected_result = test.get('outcome', {}).get('result')
expected_error = test.get('outcome', {}).get('error')
if expected_error is True:
with self.assertRaises(PyMongoError):
run_operation(self.db.test, test)
else:
result = run_operation(self.db.test, test)
self.assertTrue(check_result(expected_result, result))
# Assert final state is expected.
expected_c = test['outcome'].get('collection')
@ -173,53 +170,15 @@ def create_test(scenario_def, test):
db_coll = self.db[expected_name]
else:
db_coll = self.db.test
db_coll = db_coll.with_options(
read_concern=ReadConcern(level="local"))
self.assertEqual(list(db_coll.find()), expected_c['data'])
expected_result = test['outcome'].get('result')
self.assertTrue(check_result(expected_result, result))
return run_scenario
def create_tests():
for dirpath, _, filenames in os.walk(_TEST_PATH):
dirname = os.path.split(dirpath)[-1]
for filename in filenames:
with open(os.path.join(dirpath, filename)) as scenario_stream:
scenario_def = json.load(scenario_stream)
test_type = os.path.splitext(filename)[0]
min_ver, max_ver = None, None
if 'minServerVersion' in scenario_def:
min_ver = tuple(
int(elt) for
elt in scenario_def['minServerVersion'].split('.'))
if 'maxServerVersion' in scenario_def:
max_ver = tuple(
int(elt) for
elt in scenario_def['maxServerVersion'].split('.'))
# Construct test from scenario.
for test in scenario_def['tests']:
new_test = create_test(scenario_def, test)
if min_ver is not None:
new_test = client_context.require_version_min(*min_ver)(
new_test)
if max_ver is not None:
new_test = client_context.require_version_max(*max_ver)(
new_test)
test_name = 'test_%s_%s_%s' % (
dirname,
test_type,
str(test['description'].replace(" ", "_")))
new_test.__name__ = test_name
setattr(TestAllScenarios, new_test.__name__, new_test)
create_tests()
test_creator = TestCreator(create_test, TestAllScenarios, _TEST_PATH)
test_creator.create_tests()
class TestWriteOpsComparison(unittest.TestCase):

258
test/test_crud_v2.py Normal file
View File

@ -0,0 +1,258 @@
# Copyright 2019-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test the collection module."""
import json
import os
import sys
sys.path[0:0] = [""]
from bson.py3compat import iteritems
from pymongo import operations, WriteConcern
from pymongo.command_cursor import CommandCursor
from pymongo.cursor import Cursor
from pymongo.errors import PyMongoError
from pymongo.read_concern import ReadConcern
from pymongo.results import _WriteResult, BulkWriteResult
from test import unittest, client_context, IntegrationTest
from test.utils import (camel_to_snake, camel_to_upper_camel,
camel_to_snake_args, drop_collections,
parse_collection_options, rs_client,
OvertCommandListener, TestCreator)
# Location of JSON test specifications.
_TEST_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'crud', 'v2')
# Default test database and collection names.
TEST_DB = 'testdb'
TEST_COLLECTION = 'testcollection'
class TestAllScenarios(IntegrationTest):
def run_operation(self, collection, test):
# Iterate over all operations.
for opdef in test['operations']:
# Convert command from CamelCase to pymongo.collection method.
operation = camel_to_snake(opdef['name'])
# Get command handle on target entity (collection/database).
target_object = opdef.get('object', 'collection')
if target_object == 'database':
cmd = getattr(collection.database, operation)
elif target_object == 'collection':
collection = collection.with_options(**dict(
parse_collection_options(opdef.get(
'collectionOptions', {}))))
cmd = getattr(collection, operation)
else:
self.fail("Unknown object name %s" % (target_object,))
# Convert arguments to snake_case and handle special cases.
arguments = opdef['arguments']
options = arguments.pop("options", {})
for option_name in options:
arguments[camel_to_snake(option_name)] = options[option_name]
if operation == "bulk_write":
# Parse each request into a bulk write model.
requests = []
for request in arguments["requests"]:
bulk_model = camel_to_upper_camel(request["name"])
bulk_class = getattr(operations, bulk_model)
bulk_arguments = camel_to_snake_args(request["arguments"])
requests.append(bulk_class(**bulk_arguments))
arguments["requests"] = requests
else:
for arg_name in list(arguments):
c2s = camel_to_snake(arg_name)
# PyMongo accepts sort as list of tuples.
if arg_name == "sort":
sort_dict = arguments[arg_name]
arguments[arg_name] = list(iteritems(sort_dict))
# Named "key" instead not fieldName.
if arg_name == "fieldName":
arguments["key"] = arguments.pop(arg_name)
# Aggregate uses "batchSize", while find uses batch_size.
elif arg_name == "batchSize" and operation == "aggregate":
continue
# Requires boolean returnDocument.
elif arg_name == "returnDocument":
arguments[c2s] = arguments[arg_name] == "After"
else:
arguments[c2s] = arguments.pop(arg_name)
if opdef.get('error') is True:
with self.assertRaises(PyMongoError):
cmd(**arguments)
else:
result = cmd(**arguments)
self.check_result(opdef.get('result'), result)
def check_result(self, expected_result, result):
if expected_result is None:
return True
if isinstance(result, Cursor) or isinstance(result, CommandCursor):
return list(result) == expected_result
elif isinstance(result, _WriteResult):
for res in expected_result:
prop = camel_to_snake(res)
# SPEC-869: Only BulkWriteResult has upserted_count.
if (prop == "upserted_count" and
not isinstance(result, BulkWriteResult)):
if result.upserted_id is not None:
upserted_count = 1
else:
upserted_count = 0
if upserted_count != expected_result[res]:
return False
elif prop == "inserted_ids":
# BulkWriteResult does not have inserted_ids.
if isinstance(result, BulkWriteResult):
if len(expected_result[res]) != result.inserted_count:
return False
else:
# InsertManyResult may be compared to [id1] from the
# crud spec or {"0": id1} from the retryable write spec.
ids = expected_result[res]
if isinstance(ids, dict):
ids = [ids[str(i)] for i in range(len(ids))]
if ids != result.inserted_ids:
return False
elif prop == "upserted_ids":
# Convert indexes from strings to integers.
ids = expected_result[res]
expected_ids = {}
for str_index in ids:
expected_ids[int(str_index)] = ids[str_index]
if expected_ids != result.upserted_ids:
return False
elif getattr(result, prop) != expected_result[res]:
return False
return True
else:
if not expected_result:
return result is None
else:
return result == expected_result
def check_events(self, expected_events, listener):
res = listener.results
if not len(expected_events):
return
# Expectations only have CommandStartedEvents.
self.assertEqual(len(res['started']), len(expected_events))
for i, expectation in enumerate(expected_events):
event_type = next(iter(expectation))
event = res['started'][i]
# The tests substitute 42 for any number other than 0.
if (event.command_name == 'getMore'
and event.command['getMore']):
event.command['getMore'] = 42
elif event.command_name == 'killCursors':
event.command['cursors'] = [42]
# Add upsert and multi fields back into expectations.
elif event.command_name == 'update':
updates = expectation[event_type]['command']['updates']
for update in updates:
update.setdefault('upsert', False)
update.setdefault('multi', False)
# Replace afterClusterTime: 42 with actual afterClusterTime.
expected_cmd = expectation[event_type]['command']
expected_read_concern = expected_cmd.get('readConcern')
if expected_read_concern is not None:
time = expected_read_concern.get('afterClusterTime')
if time == 42:
actual_time = event.command.get(
'readConcern', {}).get('afterClusterTime')
if actual_time is not None:
expected_read_concern['afterClusterTime'] = actual_time
for attr, expected in expectation[event_type].items():
actual = getattr(event, attr)
if isinstance(expected, dict):
for key, val in expected.items():
if val is None:
if key in actual:
self.fail("Unexpected key [%s] in %r" % (
key, actual))
elif key not in actual:
self.fail("Expected key [%s] in %r" % (
key, actual))
else:
self.assertEqual(val, actual[key],
"Key [%s] in %s" % (key, actual))
else:
self.assertEqual(actual, expected)
def create_test(scenario_def, test, name):
def run_scenario(self):
listener = OvertCommandListener()
# New client, to avoid interference from pooled sessions.
# Convert test['clientOptions'] to dict to avoid a Jython bug using "**"
# with ScenarioDict.
client = rs_client(event_listeners=[listener],
**dict(test.get('clientOptions', {})))
# Close the client explicitly to avoid having too many threads open.
self.addCleanup(client.close)
# Get database and collection objects.
database = getattr(
client, scenario_def.get('database_name', TEST_DB))
drop_collections(database)
collection = getattr(
database, scenario_def.get('collection_name', TEST_COLLECTION))
# Populate collection with data and run test.
collection.with_options(
write_concern=WriteConcern(w="majority")).insert_many(
scenario_def.get('data', []))
listener.results.clear()
self.run_operation(collection, test)
# Assert expected events.
self.check_events(test.get('expectations', {}), listener)
# Assert final state is expected.
expected_outcome = test.get('outcome', {}).get('collection')
if expected_outcome is not None:
collname = expected_outcome.get('name')
if collname is not None:
o_collection = getattr(database, collname)
else:
o_collection = collection
o_collection = o_collection.with_options(
read_concern=ReadConcern(level="local"))
self.assertEqual(list(o_collection.find()),
expected_outcome['data'])
return run_scenario
test_creator = TestCreator(create_test, TestAllScenarios, _TEST_PATH)
test_creator.create_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -20,7 +20,7 @@ from pymongo.errors import ConfigurationError, OperationFailure
from pymongo.read_concern import ReadConcern
from test import client_context, PyMongoTestCase
from test.utils import single_client, rs_or_single_client, EventListener
from test.utils import single_client, rs_or_single_client, OvertCommandListener
class TestReadConcern(PyMongoTestCase):
@ -28,7 +28,7 @@ class TestReadConcern(PyMongoTestCase):
@classmethod
@client_context.require_connection
def setUpClass(cls):
cls.listener = EventListener()
cls.listener = OvertCommandListener()
cls.saved_listeners = monitoring._LISTENERS
# Don't use any global subscribers.
monitoring._LISTENERS = monitoring._Listeners([], [], [], [])
@ -118,8 +118,14 @@ class TestReadConcern(PyMongoTestCase):
except OperationFailure:
# "ns doesn't exist"
pass
self.assertNotIn('readConcern',
self.listener.results['started'][0].command)
# Aggregate with $out supports readConcern MongoDB 4.2 onwards.
if client_context.version >= (4, 1):
self.assertIn('readConcern',
self.listener.results['started'][0].command)
else:
self.assertNotIn('readConcern',
self.listener.results['started'][0].command)
def test_map_reduce_out(self):
coll = self.db.get_collection('coll', read_concern=ReadConcern('local'))

View File

@ -43,7 +43,7 @@ from test import unittest, client_context, IntegrationTest, SkipTest, client_kno
from test.utils import (rs_or_single_client,
DeprecationFilter,
OvertCommandListener)
from test.test_crud import check_result, run_operation
from test.test_crud_v1 import check_result, run_operation
# Location of JSON test specifications.
_TEST_PATH = os.path.join(

View File

@ -981,9 +981,6 @@ class TestCausalConsistency(unittest.TestCase):
lambda coll, session: coll.drop_index("foo_1", session=session))
self._test_no_read_concern(
lambda coll, session: coll.drop_indexes(session=session))
self._test_no_read_concern(
lambda coll, session: list(
coll.aggregate([{"$out": "aggout"}], session=session)))
self._test_no_read_concern(
lambda coll, session: coll.map_reduce(
'function() {}', 'function() {}', 'mrout', session=session))
@ -999,6 +996,13 @@ class TestCausalConsistency(unittest.TestCase):
self._test_no_read_concern(
lambda coll, session: coll.reindex(session=session))
@client_context.require_no_standalone
@client_context.require_version_max(4, 1, 0)
def test_aggregate_out_does_not_include_read_concern(self):
self._test_no_read_concern(
lambda coll, session: list(
coll.aggregate([{"$out": "aggout"}], session=session)))
@client_context.require_no_standalone
def test_get_more_does_not_include_read_concern(self):
coll = self.client.pymongo_test.test

View File

@ -14,15 +14,11 @@
"""Execute Transactions Spec tests."""
import collections
from functools import partial
import os
import re
import sys
sys.path[0:0] = [""]
from bson import json_util, py3compat
from bson.py3compat import iteritems
from bson.son import SON
from pymongo import client_session, operations, WriteConcern
@ -38,10 +34,9 @@ from pymongo.read_preferences import ReadPreference
from pymongo.results import _WriteResult, BulkWriteResult
from test import unittest, client_context, IntegrationTest
from test.utils import (OvertCommandListener,
rs_client,
single_client,
wait_until)
from test.utils import (camel_to_snake, camel_to_upper_camel,
camel_to_snake_args, rs_client, single_client,
wait_until, OvertCommandListener, TestCreator)
from test.utils_selection_tests import parse_read_preference
# Location of JSON test specifications.
@ -57,26 +52,7 @@ _TXN_TESTS_DEBUG = os.environ.get('TRANSACTION_TESTS_DEBUG')
UNPIN_TEST_MAX_ATTEMPTS = 50
# TODO: factor the following functions with test_crud.py.
def camel_to_snake(camel):
# Regex to convert CamelCase to snake_case.
snake = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake).lower()
def camel_to_upper_camel(camel):
return camel[0].upper() + camel[1:]
def camel_to_snake_args(arguments):
for arg_name in list(arguments):
c2s = camel_to_snake(arg_name)
arguments[c2s] = arguments.pop(arg_name)
return arguments
class TestTransactions(IntegrationTest):
@classmethod
def setUpClass(cls):
super(TestTransactions, cls).setUpClass()
@ -255,7 +231,7 @@ class TestTransactions(IntegrationTest):
pass
self.assertEqual(filtered_result, expected_result)
# TODO: factor the following function with test_crud.py.
# TODO: factor the following function with CRUD v2 test runner.
def check_result(self, expected_result, result):
if isinstance(result, _WriteResult):
for res in expected_result:
@ -476,7 +452,8 @@ def end_sessions(sessions):
s.end_session()
def create_test(scenario_def, test):
def create_test(scenario_def, test, name):
@client_context.require_transactions
def run_scenario(self):
if test.get('skipReason'):
raise unittest.SkipTest(test.get('skipReason'))
@ -608,65 +585,17 @@ def create_test(scenario_def, test):
read_concern=ReadConcern('local'))
self.assertEqual(list(primary_coll.find()), expected_c['data'])
if 'secondary' in name:
run_scenario = client_context._require(
lambda: client_context.has_secondaries, 'No secondaries',
run_scenario)
return run_scenario
class ScenarioDict(dict):
"""Dict that returns {} for any unknown key, recursively."""
def __init__(self, data):
def convert(v):
if isinstance(v, collections.Mapping):
return ScenarioDict(v)
if isinstance(v, py3compat.string_type):
return v
if isinstance(v, collections.Sequence):
return [convert(item) for item in v]
return v
test_creator = TestCreator(create_test, TestTransactions, _TEST_PATH)
test_creator.create_tests()
dict.__init__(self, [(k, convert(v)) for k, v in data.items()])
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
# Unlike a defaultdict, don't set the key, just return a dict.
return ScenarioDict({})
def create_tests():
for dirpath, _, filenames in os.walk(_TEST_PATH):
dirname = os.path.split(dirpath)[-1]
for filename in filenames:
test_type, ext = os.path.splitext(filename)
if ext != '.json':
continue
with open(os.path.join(dirpath, filename)) as scenario_stream:
scenario_def = ScenarioDict(
json_util.loads(scenario_stream.read()))
# Construct test from scenario.
for test in scenario_def['tests']:
test_name = 'test_%s_%s_%s' % (
dirname,
test_type.replace("-", "_"),
str(test['description'].replace(" ", "_")))
new_test = create_test(scenario_def, test)
new_test = client_context.require_transactions(new_test)
if 'secondary' in test_name:
new_test = client_context._require(
lambda: client_context.has_secondaries,
'No secondaries',
new_test)
new_test.__name__ = test_name
setattr(TestTransactions, new_test.__name__, new_test)
create_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -15,8 +15,11 @@
"""Utilities for testing pymongo
"""
import collections
import contextlib
import functools
import os
import re
import sys
import threading
import time
@ -25,14 +28,16 @@ import warnings
from collections import defaultdict
from functools import partial
from bson import json_util, py3compat
from bson.objectid import ObjectId
from pymongo import MongoClient, monitoring
from pymongo.errors import OperationFailure
from pymongo.monitoring import _SENSITIVE_COMMANDS
from pymongo.read_concern import ReadConcern
from pymongo.server_selectors import (any_server_selector,
writable_server_selector)
from pymongo.write_concern import WriteConcern
from test import (client_context, db_user, db_pwd)
from test import client_context, db_user, db_pwd
IMPOSSIBLE_WRITE_CONCERN = WriteConcern(w=1000)
@ -120,6 +125,93 @@ class HeartbeatEventListener(monitoring.ServerHeartbeatListener):
self.results.append(event)
class ScenarioDict(dict):
"""Dict that returns {} for any unknown key, recursively."""
def __init__(self, data):
def convert(v):
if isinstance(v, collections.Mapping):
return ScenarioDict(v)
if isinstance(v, py3compat.string_type):
return v
if isinstance(v, collections.Sequence):
return [convert(item) for item in v]
return v
dict.__init__(self, [(k, convert(v)) for k, v in data.items()])
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
# Unlike a defaultdict, don't set the key, just return a dict.
return ScenarioDict({})
class TestCreator(object):
"""Class to create test cases from specifications."""
def __init__(self, create_test, test_class, test_path):
"""Create a TestCreator object.
:Parameters:
- `create_test`: callback that returns a test case. The callback
must accept the following arguments - a dictionary containing the
entire test specification (the `scenario_def`), a dictionary
containing the specification for which the test case will be
generated (the `test_def`).
- `test_class`: the unittest.TestCase class in which to create the
test case.
- `test_path`: path to the directory containing the JSON files with
the test specifications.
"""
self._create_test = create_test
self._test_class= test_class
self.test_path = test_path
def _ensure_min_max_server_version(self, scenario_def, method):
"""Test modifier that enforces a version range for the server on a
test case."""
if 'minServerVersion' in scenario_def:
min_ver = tuple(
int(elt) for
elt in scenario_def['minServerVersion'].split('.'))
if min_ver is not None:
method = client_context.require_version_min(*min_ver)(method)
if 'maxServerVersion' in scenario_def:
max_ver = tuple(
int(elt) for
elt in scenario_def['maxServerVersion'].split('.'))
if max_ver is not None:
method = client_context.require_version_max(*max_ver)(method)
return method
def create_tests(self):
for dirpath, _, filenames in os.walk(self.test_path):
dirname = os.path.split(dirpath)[-1]
for filename in filenames:
with open(os.path.join(dirpath, filename)) as scenario_stream:
scenario_def = ScenarioDict(
json_util.loads(scenario_stream.read()))
test_type = os.path.splitext(filename)[0]
# Construct test from scenario.
for test_def in scenario_def['tests']:
test_name = 'test_%s_%s_%s' % (
dirname, test_type,
str(test_def['description'].replace(" ", "_")))
new_test = self._create_test(
scenario_def, test_def, test_name)
new_test = self._ensure_min_max_server_version(
scenario_def, new_test)
new_test.__name__ = test_name
setattr(self._test_class, new_test.__name__, new_test)
def _connection_string(h, authenticate):
if h.startswith("mongodb://"):
return h
@ -202,6 +294,38 @@ def get_command_line(client):
return command_line
def camel_to_snake(camel):
# Regex to convert CamelCase to snake_case.
snake = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', snake).lower()
def camel_to_upper_camel(camel):
return camel[0].upper() + camel[1:]
def camel_to_snake_args(arguments):
for arg_name in list(arguments):
c2s = camel_to_snake(arg_name)
arguments[c2s] = arguments.pop(arg_name)
return arguments
def parse_collection_options(opts):
if 'readPreference' in opts:
opts['read_preference'] = parse_read_preference(
opts.pop('readPreference'))
if 'writeConcern' in opts:
opts['write_concern'] = WriteConcern(
**dict(opts.pop('writeConcern')))
if 'readConcern' in opts:
opts['read_concern'] = ReadConcern(
**dict(opts.pop('readConcern')))
return opts
def server_started_with_option(client, cmdline_opt, config_opt):
"""Check if the server was started with a particular option.