PHPLIB-536: Add spec tests for default write concern

parent 61528bcf
......@@ -102,6 +102,16 @@ class CommandExpectations implements CommandSubscriber
return $o;
}
public static function fromReadWriteConcern(array $expectedEvents)
{
$o = new self($expectedEvents);
$o->ignoreCommandFailed = true;
$o->ignoreCommandSucceeded = true;
return $o;
}
public static function fromRetryableReads(array $expectedEvents)
{
$o = new self($expectedEvents);
......
......@@ -166,6 +166,21 @@ final class Context
return $o;
}
public static function fromReadWriteConcern(stdClass $test, $databaseName, $collectionName)
{
$o = new self($databaseName, $collectionName);
if (isset($test->outcome->collection->name)) {
$o->outcomeCollectionName = $test->outcome->collection->name;
}
$clientOptions = isset($test->clientOptions) ? (array) $test->clientOptions : [];
$o->client = new Client(FunctionalTestCase::getUri(), $clientOptions);
return $o;
}
public static function fromRetryableReads(stdClass $test, $databaseName, $collectionName, $bucketName)
{
$o = new self($databaseName, $collectionName);
......@@ -253,12 +268,13 @@ final class Context
return $this->useEncryptedClient && $this->encryptedClient ? $this->encryptedClient : $this->client;
}
public function getCollection(array $collectionOptions = [])
public function getCollection(array $collectionOptions = [], array $databaseOptions = [])
{
return $this->selectCollection(
$this->databaseName,
$this->collectionName,
$this->prepareOptions($collectionOptions)
$collectionOptions,
$databaseOptions
);
}
......@@ -312,6 +328,7 @@ final class Context
throw new LogicException('Unsupported writeConcern args: ' . implode(',', array_keys($diff)));
}
if (! empty($writeConcern)) {
$w = $writeConcern['w'];
$wtimeout = $writeConcern['wtimeout'] ?? 0;
$j = $writeConcern['j'] ?? null;
......@@ -319,6 +336,9 @@ final class Context
$options['writeConcern'] = isset($j)
? new WriteConcern($w, $wtimeout, $j)
: new WriteConcern($w, $wtimeout);
} else {
unset($options['writeConcern']);
}
}
return $options;
......@@ -380,13 +400,11 @@ final class Context
}
}
public function selectCollection($databaseName, $collectionName, array $collectionOptions = [])
public function selectCollection($databaseName, $collectionName, array $collectionOptions = [], array $databaseOptions = [])
{
return $this->getClient()->selectCollection(
$databaseName,
$collectionName,
$this->prepareOptions($collectionOptions)
);
return $this
->selectDatabase($databaseName, $databaseOptions)
->selectCollection($collectionName, $this->prepareOptions($collectionOptions));
}
public function selectDatabase($databaseName, array $databaseOptions = [])
......
......@@ -92,6 +92,11 @@ final class ErrorExpectation
return $o;
}
public static function fromReadWriteConcern(stdClass $operation)
{
return self::fromGenericOperation($operation);
}
public static function fromRetryableReads(stdClass $operation)
{
$o = new self();
......
......@@ -61,6 +61,9 @@ final class Operation
/** @var string|null */
private $databaseName;
/** @var array */
private $databaseOptions = [];
/** @var string */
private $name;
......@@ -184,6 +187,24 @@ final class Operation
return $o;
}
public static function fromReadWriteConcern(stdClass $operation)
{
$o = new self($operation);
$o->errorExpectation = ErrorExpectation::fromReadWriteConcern($operation);
$o->resultExpectation = ResultExpectation::fromReadWriteConcern($operation, $o->getResultAssertionType());
if (isset($operation->databaseOptions)) {
$o->databaseOptions = (array) $operation->databaseOptions;
}
if (isset($operation->collectionOptions)) {
$o->collectionOptions = (array) $operation->collectionOptions;
}
return $o;
}
public static function fromRetryableReads(stdClass $operation)
{
$o = new self($operation);
......@@ -277,7 +298,7 @@ final class Operation
return $this->executeForClient($client, $context);
case self::OBJECT_COLLECTION:
$collection = $context->getCollection($this->collectionOptions);
$collection = $context->getCollection($this->collectionOptions, $this->databaseOptions);
return $this->executeForCollection($collection, $context);
case self::OBJECT_DATABASE:
......@@ -289,7 +310,7 @@ final class Operation
return $this->executeForGridFSBucket($bucket, $context);
case self::OBJECT_SELECT_COLLECTION:
$collection = $context->selectCollection($this->databaseName, $this->collectionName, $this->collectionOptions);
$collection = $context->selectCollection($this->databaseName, $this->collectionName, $this->collectionOptions, $this->databaseOptions);
return $this->executeForCollection($collection, $context);
case self::OBJECT_SELECT_DATABASE:
......@@ -367,6 +388,11 @@ final class Operation
$args['keys'],
array_diff_key($args, ['keys' => 1])
);
case 'dropIndex':
return $collection->dropIndex(
$args['name'],
array_diff_key($args, ['name' => 1])
);
case 'count':
case 'countDocuments':
case 'find':
......@@ -738,6 +764,7 @@ final class Operation
case 'countDocuments':
return ResultExpectation::ASSERT_SAME;
case 'createIndex':
case 'dropIndex':
return ResultExpectation::ASSERT_MATCHES_DOCUMENT;
case 'distinct':
case 'estimatedDocumentCount':
......
<?php
namespace MongoDB\Tests\SpecTests;
use stdClass;
use function basename;
use function dirname;
use function file_get_contents;
use function glob;
/**
* @see https://github.com/mongodb/specifications/tree/master/source/read-write-concern
*/
class ReadWriteConcernSpecTest extends FunctionalTestCase
{
/** @var array */
private static $incompleteTests = [];
/**
* Assert that the expected and actual command documents match.
*
* @param stdClass $expected Expected command document
* @param stdClass $actual Actual command document
*/
public static function assertCommandMatches(stdClass $expected, stdClass $actual)
{
foreach ($expected as $key => $value) {
if ($value === null) {
static::assertObjectNotHasAttribute($key, $actual);
unset($expected->{$key});
}
}
static::assertDocumentsMatch($expected, $actual);
}
/**
* Execute an individual test case from the specification.
*
* @dataProvider provideTests
* @param stdClass $test Individual "tests[]" document
* @param array $runOn Top-level "runOn" array with server requirements
* @param array $data Top-level "data" array to initialize collection
* @param string $databaseName Name of database under test
* @param string $collectionName Name of collection under test
*/
public function testReadWriteConcern(stdClass $test, array $runOn = null, array $data, $databaseName = null, $collectionName = null)
{
if (isset(self::$incompleteTests[$this->dataDescription()])) {
$this->markTestIncomplete(self::$incompleteTests[$this->dataDescription()]);
}
if (isset($runOn)) {
$this->checkServerRequirements($runOn);
}
if (isset($test->skipReason)) {
$this->markTestSkipped($test->skipReason);
}
$databaseName = $databaseName ?? $this->getDatabaseName();
$collectionName = $collectionName ?? $this->getCollectionName();
$context = Context::fromReadWriteConcern($test, $databaseName, $collectionName);
$this->setContext($context);
$this->dropTestAndOutcomeCollections();
$this->insertDataFixtures($data);
if (isset($test->failPoint)) {
$this->configureFailPoint($test->failPoint);
}
if (isset($test->expectations)) {
$commandExpectations = CommandExpectations::fromReadWriteConcern($test->expectations);
$commandExpectations->startMonitoring();
}
foreach ($test->operations as $operation) {
Operation::fromReadWriteConcern($operation)->assert($this, $context);
}
if (isset($commandExpectations)) {
$commandExpectations->stopMonitoring();
$commandExpectations->assert($this, $context);
}
if (isset($test->outcome->collection->data)) {
$this->assertOutcomeCollectionData($test->outcome->collection->data);
}
}
public function provideTests()
{
$testArgs = [];
foreach (glob(__DIR__ . '/read-write-concern/operation/*.json') as $filename) {
$json = $this->decodeJson(file_get_contents($filename));
$group = basename(dirname($filename)) . '/' . basename($filename, '.json');
$runOn = $json->runOn ?? null;
$data = $json->data ?? [];
$databaseName = $json->database_name ?? null;
$collectionName = $json->collection_name ?? null;
foreach ($json->tests as $test) {
$name = $group . ': ' . $test->description;
$testArgs[$name] = [$test, $runOn, $data, $databaseName, $collectionName];
}
}
return $testArgs;
}
}
......@@ -111,6 +111,19 @@ final class ResultExpectation
return new self($assertionType, $expectedValue);
}
public static function fromReadWriteConcern(stdClass $operation, $defaultAssertionType)
{
if (property_exists($operation, 'result') && ! self::isErrorResult($operation->result)) {
$assertionType = $operation->result === null ? self::ASSERT_NULL : $defaultAssertionType;
$expectedValue = $operation->result;
} else {
$assertionType = self::ASSERT_NOTHING;
$expectedValue = null;
}
return new self($assertionType, $expectedValue);
}
public static function fromRetryableReads(stdClass $operation, $defaultAssertionType)
{
if (property_exists($operation, 'result') && ! self::isErrorResult($operation->result)) {
......
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
{
"minServerVersion": "2.6"
}
],
"tests": [
{
"description": "DeleteOne omits default write concern",
"operations": [
{
"name": "deleteOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {}
},
"result": {
"deletedCount": 1
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 1
}
],
"writeConcern": null
}
}
}
]
},
{
"description": "DeleteMany omits default write concern",
"operations": [
{
"name": "deleteMany",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {}
},
"result": {
"deletedCount": 2
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 0
}
],
"writeConcern": null
}
}
}
]
},
{
"description": "BulkWrite with all models omits default write concern",
"operations": [
{
"name": "bulkWrite",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"ordered": true,
"requests": [
{
"name": "deleteMany",
"arguments": {
"filter": {}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 1
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"x": 1
}
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 2
}
}
},
{
"name": "replaceOne",
"arguments": {
"filter": {
"_id": 1
},
"replacement": {
"x": 2
}
}
},
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 3
}
}
},
{
"name": "updateMany",
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"x": 3
}
}
}
},
{
"name": "deleteOne",
"arguments": {
"filter": {
"_id": 3
}
}
}
]
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
{
"_id": 1,
"x": 3
},
{
"_id": 2
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 0
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 1
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 1
}
}
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 2
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"x": 2
}
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 3
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 3
}
},
"multi": true
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {
"_id": 3
},
"limit": 1
}
],
"writeConcern": null
}
}
}
]
},
{
"description": "InsertOne and InsertMany omit default write concern",
"operations": [
{
"name": "insertOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"document": {
"_id": 3
}
}
},
{
"name": "insertMany",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"documents": [
{
"_id": 4
},
{
"_id": 5
}
]
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3
},
{
"_id": 4
},
{
"_id": 5
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 3
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"insert": "default_write_concern_coll",
"documents": [
{
"_id": 4
},
{
"_id": 5
}
],
"writeConcern": null
}
}
}
]
},
{
"description": "UpdateOne, UpdateMany, and ReplaceOne omit default write concern",
"operations": [
{
"name": "updateOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"x": 1
}
}
}
},
{
"name": "updateMany",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {
"_id": 2
},
"update": {
"$set": {
"x": 2
}
}
}
},
{
"name": "replaceOne",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {
"_id": 2
},
"replacement": {
"x": 3
}
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
{
"_id": 1,
"x": 1
},
{
"_id": 2,
"x": 3
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$set": {
"x": 1
}
}
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 2
},
"u": {
"$set": {
"x": 2
}
},
"multi": true
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"update": "default_write_concern_coll",
"updates": [
{
"q": {
"_id": 2
},
"u": {
"x": 3
}
}
],
"writeConcern": null
}
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
{
"minServerVersion": "3.2"
}
],
"tests": [
{
"description": "findAndModify operations omit default write concern",
"operations": [
{
"name": "findOneAndUpdate",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"x": 1
}
}
}
},
{
"name": "findOneAndReplace",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {
"_id": 2
},
"replacement": {
"x": 2
}
}
},
{
"name": "findOneAndDelete",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"filter": {
"_id": 2
}
}
}
],
"outcome": {
"collection": {
"name": "default_write_concern_coll",
"data": [
{
"_id": 1,
"x": 1
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"findAndModify": "default_write_concern_coll",
"query": {
"_id": 1
},
"update": {
"$set": {
"x": 1
}
},
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"findAndModify": "default_write_concern_coll",
"query": {
"_id": 2
},
"update": {
"x": 2
},
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"findAndModify": "default_write_concern_coll",
"query": {
"_id": 2
},
"remove": true,
"writeConcern": null
}
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
{
"minServerVersion": "3.4"
}
],
"tests": [
{
"description": "Aggregate with $out omits default write concern",
"operations": [
{
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"name": "aggregate",
"arguments": {
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_collection_name"
}
]
}
}
],
"outcome": {
"collection": {
"name": "other_collection_name",
"data": [
{
"_id": 2,
"x": 22
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "default_write_concern_coll",
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$out": "other_collection_name"
}
],
"writeConcern": null
}
}
}
]
},
{
"description": "RunCommand with a write command omits default write concern (runCommand should never inherit write concern)",
"operations": [
{
"object": "database",
"databaseOptions": {
"writeConcern": {}
},
"name": "runCommand",
"command_name": "delete",
"arguments": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 1
}
]
}
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"delete": "default_write_concern_coll",
"deletes": [
{
"q": {},
"limit": 1
}
],
"writeConcern": null
}
}
}
]
},
{
"description": "CreateIndex and dropIndex omits default write concern",
"operations": [
{
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"name": "createIndex",
"arguments": {
"keys": {
"x": 1
}
}
},
{
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"name": "dropIndex",
"arguments": {
"name": "x_1"
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"createIndexes": "default_write_concern_coll",
"indexes": [
{
"name": "x_1",
"key": {
"x": 1
}
}
],
"writeConcern": null
}
}
},
{
"command_started_event": {
"command": {
"dropIndexes": "default_write_concern_coll",
"index": "x_1",
"writeConcern": null
}
}
}
]
},
{
"description": "MapReduce omits default write concern",
"operations": [
{
"name": "mapReduce",
"object": "collection",
"collectionOptions": {
"writeConcern": {}
},
"arguments": {
"map": {
"$code": "function inc() { return emit(0, this.x + 1) }"
},
"reduce": {
"$code": "function sum(key, values) { return values.reduce((acc, x) => acc + x); }"
},
"out": {
"inline": 1
}
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"mapReduce": "default_write_concern_coll",
"map": {
"$code": "function inc() { return emit(0, this.x + 1) }"
},
"reduce": {
"$code": "function sum(key, values) { return values.reduce((acc, x) => acc + x); }"
},
"out": {
"inline": 1
},
"writeConcern": null
}
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
}
],
"collection_name": "default_write_concern_coll",
"database_name": "default_write_concern_db",
"runOn": [
{
"minServerVersion": "4.2"
}
],
"tests": [
{
"description": "Aggregate with $merge omits default write concern",
"operations": [
{
"object": "collection",
"databaseOptions": {
"writeConcern": {}
},
"collectionOptions": {
"writeConcern": {}
},
"name": "aggregate",
"arguments": {
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$merge": {
"into": "other_collection_name"
}
}
]
}
}
],
"expectations": [
{
"command_started_event": {
"command": {
"aggregate": "default_write_concern_coll",
"pipeline": [
{
"$match": {
"_id": {
"$gt": 1
}
}
},
{
"$merge": {
"into": "other_collection_name"
}
}
],
"writeConcern": null
}
}
}
],
"outcome": {
"collection": {
"name": "other_collection_name",
"data": [
{
"_id": 2,
"x": 22
}
]
}
}
}
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment