Commit 1e5a99bf authored by Jeremy Mikola's avatar Jeremy Mikola

PHPLIB-410: Command Monitoring test runner

Spec tests synced with mongodb/specifications@4820c7bad9b06b71f169c0d4b99aba6fbb319243
parent a4067ff4
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
namespace MongoDB\Tests\SpecTests; namespace MongoDB\Tests\SpecTests;
use MongoDB\BSON\Timestamp;
use MongoDB\Driver\Monitoring\CommandFailedEvent; use MongoDB\Driver\Monitoring\CommandFailedEvent;
use MongoDB\Driver\Monitoring\CommandStartedEvent; use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSucceededEvent; use MongoDB\Driver\Monitoring\CommandSucceededEvent;
...@@ -17,20 +16,45 @@ use stdClass; ...@@ -17,20 +16,45 @@ use stdClass;
*/ */
class CommandExpectations implements CommandSubscriber class CommandExpectations implements CommandSubscriber
{ {
private $commandStartedEvents = []; private $actualEvents = [];
private $expectedCommandStartedEvents = []; private $expectedEvents = [];
private $ignoreCommandFailed = false;
private $ignoreCommandStarted = false;
private $ignoreCommandSucceeded = false;
public static function fromTransactions(array $expectedEvents) private function __construct(array $events)
{ {
$o = new self; foreach ($events as $event) {
switch (key($event)) {
foreach ($expectedEvents as $expectedEvent) { case 'command_failed_event':
if (!isset($expectedEvent->command_started_event)) { $this->expectedEvents[] = [$event->command_failed_event, CommandFailedEvent::class];
throw new LogicException('$expectedEvent->command_started_event field is not set'); break;
case 'command_started_event':
$this->expectedEvents[] = [$event->command_started_event, CommandStartedEvent::class];
break;
case 'command_succeeded_event':
$this->expectedEvents[] = [$event->command_succeeded_event, CommandSucceededEvent::class];
break;
default:
throw new LogicException('Unsupported event type: ' . key($event));
} }
$o->expectedCommandStartedEvents[] = $expectedEvent->command_started_event;
} }
}
public static function fromCommandMonitoring(array $expectedEvents)
{
return new self($expectedEvents);
}
public static function fromTransactions(array $expectedEvents)
{
$o = new self($expectedEvents);
$o->ignoreCommandFailed = true;
$o->ignoreCommandSucceeded = true;
return $o; return $o;
} }
...@@ -42,6 +66,11 @@ class CommandExpectations implements CommandSubscriber ...@@ -42,6 +66,11 @@ class CommandExpectations implements CommandSubscriber
*/ */
public function commandFailed(CommandFailedEvent $event) public function commandFailed(CommandFailedEvent $event)
{ {
if ($this->ignoreCommandFailed) {
return;
}
$this->actualEvents[] = $event;
} }
/** /**
...@@ -51,7 +80,11 @@ class CommandExpectations implements CommandSubscriber ...@@ -51,7 +80,11 @@ class CommandExpectations implements CommandSubscriber
*/ */
public function commandStarted(CommandStartedEvent $event) public function commandStarted(CommandStartedEvent $event)
{ {
$this->commandStartedEvents[] = $event; if ($this->ignoreCommandStarted) {
return;
}
$this->actualEvents[] = $event;
} }
/** /**
...@@ -61,6 +94,11 @@ class CommandExpectations implements CommandSubscriber ...@@ -61,6 +94,11 @@ class CommandExpectations implements CommandSubscriber
*/ */
public function commandSucceeded(CommandSucceededEvent $event) public function commandSucceeded(CommandSucceededEvent $event)
{ {
if ($this->ignoreCommandSucceeded) {
return;
}
$this->actualEvents[] = $event;
} }
/** /**
...@@ -87,16 +125,17 @@ class CommandExpectations implements CommandSubscriber ...@@ -87,16 +125,17 @@ class CommandExpectations implements CommandSubscriber
*/ */
public function assert(FunctionalTestCase $test, Context $context) public function assert(FunctionalTestCase $test, Context $context)
{ {
$test->assertCount(count($this->expectedCommandStartedEvents), $this->commandStartedEvents); $test->assertCount(count($this->expectedEvents), $this->actualEvents);
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY); $mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
$mi->attachIterator(new ArrayIterator($this->expectedCommandStartedEvents)); $mi->attachIterator(new ArrayIterator($this->expectedEvents));
$mi->attachIterator(new ArrayIterator($this->commandStartedEvents)); $mi->attachIterator(new ArrayIterator($this->actualEvents));
foreach ($mi as $events) { foreach ($mi as $events) {
list($expectedEvent, $actualEvent) = $events; list($expectedEventAndClass, $actualEvent) = $events;
$test->assertInternalType('object', $expectedEvent); list($expectedEvent, $expectedClass) = $expectedEventAndClass;
$test->assertInstanceOf(CommandStartedEvent::class, $actualEvent);
$test->assertInstanceOf($expectedClass, $actualEvent);
if (isset($expectedEvent->command_name)) { if (isset($expectedEvent->command_name)) {
$test->assertSame($expectedEvent->command_name, $actualEvent->getCommandName()); $test->assertSame($expectedEvent->command_name, $actualEvent->getCommandName());
...@@ -107,14 +146,16 @@ class CommandExpectations implements CommandSubscriber ...@@ -107,14 +146,16 @@ class CommandExpectations implements CommandSubscriber
} }
if (isset($expectedEvent->command)) { if (isset($expectedEvent->command)) {
$test->assertInstanceOf(CommandStartedEvent::class, $actualEvent);
$expectedCommand = $expectedEvent->command; $expectedCommand = $expectedEvent->command;
$context->replaceCommandSessionPlaceholder($expectedCommand); $context->replaceCommandSessionPlaceholder($expectedCommand);
$test->assertSameCommand($expectedCommand, $actualEvent->getCommand()); $test->assertCommandMatches($expectedCommand, $actualEvent->getCommand());
} }
}
}
private function __construct() if (isset($expectedEvent->reply)) {
{ $test->assertInstanceOf(CommandSucceededEvent::class, $actualEvent);
$test->assertCommandReplyMatches($expectedEvent->reply, $actualEvent->getReply());
}
}
} }
} }
<?php
namespace MongoDB\Tests\SpecTests;
use stdClass;
/**
* Command monitoring spec tests.
*
* @see https://github.com/mongodb/specifications/tree/master/source/command-monitoring
*/
class CommandMonitoringSpecTest extends FunctionalTestCase
{
/**
* Assert that the expected and actual command documents match.
*
* Note: this method may modify the $expected object.
*
* @param stdClass $expected Expected command document
* @param stdClass $actual Actual command document
*/
public static function assertCommandMatches(stdClass $expected, stdClass $actual)
{
if (isset($expected->getMore) && $expected->getMore === 42) {
static::assertObjectHasAttribute('getMore', $actual);
static::assertThat($actual->getMore, static::logicalOr(
static::isInstanceOf(Int64::class),
static::isType('integer')
));
unset($expected->getMore);
}
if (isset($expected->killCursors) && isset($expected->cursors) && is_array($expected->cursors)) {
static::assertObjectHasAttribute('cursors', $actual);
static::assertInternalType('array', $actual->cursors);
foreach ($expected->cursors as $i => $cursorId) {
static::assertArrayHasKey($i, $actual->cursors);
if ($cursorId === 42) {
static::assertThat($actual->cursors[$i], static::logicalOr(
static::isInstanceOf(Int64::class),
static::isType('integer')
));
}
}
unset($expected->cursors);
}
static::assertDocumentsMatch($expected, $actual);
}
/**
* Assert that the expected and actual command reply documents match.
*
* Note: this method may modify the $expectedReply object.
*
* @param stdClass $expected Expected command reply document
* @param stdClass $actual Actual command reply document
*/
public static function assertCommandReplyMatches(stdClass $expected, stdClass $actual)
{
if (isset($expected->cursor->id) && $expected->cursor->id === 42) {
static::assertObjectHasAttribute('cursor', $actual);
static::assertInternalType('object', $actual->cursor);
static::assertObjectHasAttribute('id', $actual->cursor);
static::assertThat($actual->cursor->id, static::logicalOr(
static::isInstanceOf(Int64::class),
static::isType('integer')
));
unset($expected->cursor->id);
}
if (isset($expected->cursorsUnknown) && is_array($expected->cursorsUnknown)) {
static::assertObjectHasAttribute('cursorsUnknown', $actual);
static::assertInternalType('array', $actual->cursorsUnknown);
foreach ($expected->cursorsUnknown as $i => $cursorId) {
static::assertArrayHasKey($i, $actual->cursorsUnknown);
if ($cursorId === 42) {
static::assertThat($actual->cursorsUnknown[$i], static::logicalOr(
static::isInstanceOf(Int64::class),
static::isType('integer')
));
}
}
unset($expected->cursorsUnknown);
}
if (isset($expected->ok) && is_numeric($expected->ok)) {
static::assertObjectHasAttribute('ok', $actual);
static::assertInternalType('numeric', $actual->ok);
static::assertEquals($expected->ok, $actual->ok);
unset($expected->ok);
}
if (isset($expected->writeErrors) && is_array($expected->writeErrors)) {
static::assertObjectHasAttribute('writeErrors', $actual);
static::assertInternalType('array', $actual->writeErrors);
foreach ($expected->writeErrors as $i => $expectedWriteError) {
static::assertArrayHasKey($i, $actual->writeErrors);
$actualWriteError = $actual->writeErrors[$i];
if (isset($expectedWriteError->code) && $expectedWriteError->code === 42) {
static::assertObjectHasAttribute('code', $actualWriteError);
static::assertThat($actualWriteError->code, static::logicalOr(
static::isInstanceOf(Int64::class),
static::isType('integer')
));
unset($expected->writeErrors[$i]->code);
}
if (isset($expectedWriteError->errmsg) && $expectedWriteError->errmsg === '') {
static::assertObjectHasAttribute('errmsg', $actualWriteError);
static::assertInternalType('string', $actualWriteError->errmsg);
static::assertNotEmpty($actualWriteError->errmsg);
unset($expected->writeErrors[$i]->errmsg);
}
}
}
static::assertDocumentsMatch($expected, $actual);
}
/**
* Execute an individual test case from the specification.
*
* @dataProvider provideTests
* @param string $name Test name
* @param stdClass $test Individual "tests[]" document
* @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 testCommandMonitoring($name, stdClass $test, array $data, $databaseName = null, $collectionName = null)
{
$this->setName($name);
$this->checkServerRequirements($this->createRunOn($test));
$databaseName = isset($databaseName) ? $databaseName : $this->getDatabaseName();
$collectionName = isset($collectionName) ? $collectionName : $this->getCollectionName();
$context = Context::fromCommandMonitoring($test, $databaseName, $collectionName);
$this->setContext($context);
$this->dropTestAndOutcomeCollections();
$this->insertDataFixtures($data);
if (isset($test->expectations)) {
$commandExpectations = CommandExpectations::fromCommandMonitoring($test->expectations);
$commandExpectations->startMonitoring();
}
Operation::fromCommandMonitoring($test->operation)->assert($this, $context);
if (isset($commandExpectations)) {
$commandExpectations->stopMonitoring();
$commandExpectations->assert($this, $context);
}
}
public function provideTests()
{
$testArgs = [];
foreach (glob(__DIR__ . '/command-monitoring/*.json') as $filename) {
$json = $this->decodeJson(file_get_contents($filename));
$group = basename($filename, '.json');
$data = isset($json->data) ? $json->data : [];
$databaseName = isset($json->database_name) ? $json->database_name : null;
$collectionName = isset($json->collection_name) ? $json->collection_name : null;
foreach ($json->tests as $test) {
$name = $group . ': ' . $test->description;
$testArgs[] = [$name, $test, $data, $databaseName, $collectionName];
}
}
return $testArgs;
}
/**
* Convert the server and topology requirements to a standard "runOn" array
* used by other specifications.
*
* @param stdClass $test
* @return array
*/
private function createRunOn(stdClass $test)
{
$req = new stdClass;
$topologies = [
self::TOPOLOGY_SINGLE,
self::TOPOLOGY_REPLICASET,
self::TOPOLOGY_SHARDED
];
/* Append ".99" as patch version, since command monitoring tests expect
* the minor version to be an inclusive upper bound. */
if (isset($test->ignore_if_server_version_greater_than)) {
$req->maxServerVersion = $test->ignore_if_server_version_greater_than . '.99';
}
if (isset($test->ignore_if_server_version_less_than)) {
$req->minServerVersion = $test->ignore_if_server_version_less_than;
}
if (isset($test->ignore_if_topology_type)) {
$req->topology = array_diff($topologies, $test->ignore_if_topology_type);
}
return [$req];
}
}
...@@ -35,6 +35,15 @@ final class Context ...@@ -35,6 +35,15 @@ final class Context
$this->outcomeCollectionName = $collectionName; $this->outcomeCollectionName = $collectionName;
} }
public static function fromCommandMonitoring(stdClass $test, $databaseName, $collectionName)
{
$o = new self($databaseName, $collectionName);
$o->client = new Client(FunctionalTestCase::getUri());
return $o;
}
public static function fromRetryableWrites(stdClass $test, $databaseName, $collectionName) public static function fromRetryableWrites(stdClass $test, $databaseName, $collectionName)
{ {
$o = new self($databaseName, $collectionName); $o = new self($databaseName, $collectionName);
......
<?php
namespace MongoDB\Tests\SpecTests;
use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument;
use PHPUnit\Framework\Constraint\Constraint;
use ArrayObject;
use InvalidArgumentException;
use RuntimeException;
use stdClass;
/**
* Constraint that checks if one document matches another.
*
* The expected value is passed in the constructor.
*/
class DocumentsMatchConstraint extends Constraint
{
private $ignoreExtraKeysInRoot = false;
private $ignoreExtraKeysInEmbedded = false;
/* TODO: This is not currently used, but was preserved from the design of
* TestCase::assertMatchesDocument(), which would sort keys and then compare
* documents as JSON strings. If the TODO item in matches() is implemented
* to make document comparisons more efficient, we may consider supporting
* this option. */
private $sortKeys = false;
private $value;
/**
* Creates a new constraint.
*
* @param array|object $value
* @param boolean $ignoreExtraKeysInRoot If true, ignore extra keys within the root document
* @param boolean $ignoreExtraKeysInEmbedded If true, ignore extra keys within embedded documents
*
*/
public function __construct($value, $ignoreExtraKeysInRoot = false, $ignoreExtraKeysInEmbedded = false)
{
parent::__construct();
$this->value = $this->prepareBSON($value, true, $this->sortKeys);
$this->ignoreExtraKeysInRoot = $ignoreExtraKeysInRoot;
$this->ignoreExtraKeysInEmbedded = $ignoreExtraKeysInEmbedded;
}
/**
* Returns a string representation of the constraint.
*
* @return string
*/
public function toString()
{
return 'matches ' . json_encode($this->value);
}
/**
* Evaluates the constraint for parameter $other. Returns true if the
* constraint is met, false otherwise.
*
* @param mixed $other
* @return boolean
*/
protected function matches($other)
{
/* TODO: If ignoreExtraKeys and sortKeys are both false, then we may be
* able to skip preparation, convert both documents to extended JSON,
* and compare strings.
*
* If ignoreExtraKeys is false and sortKeys is true, we still be able to
* compare JSON strings but will still require preparation to sort keys
* in all documents and sub-documents. */
$other = $this->prepareBSON($other, true, $this->sortKeys);
try {
$this->assertEquals($this->value, $other, $this->ignoreExtraKeysInRoot);
} catch (RuntimeException $e) {
return false;
}
return true;
}
/**
* Compares two documents recursively.
*
* @param ArrayObject $expected
* @param ArrayObject $actual
* @param boolean $ignoreExtraKeys
* @throws RuntimeException if the documents do not match
*/
private function assertEquals(ArrayObject $expected, ArrayObject $actual, $ignoreExtraKeys)
{
if (get_class($expected) !== get_class($actual)) {
throw new RuntimeException(sprintf('$expected is %s but $actual is %s', get_class($expected), get_class($actual)));
}
foreach ($expected as $key => $expectedValue) {
$actualHasKey = $actual->offsetExists($key);
if (!$actualHasKey) {
throw new RuntimeException('$actual is missing key: ' . $key);
}
$actualValue = $actual[$key];
if (($expectedValue instanceof BSONArray && $actualValue instanceof BSONArray) ||
($expectedValue instanceof BSONDocument && $actualValue instanceof BSONDocument)) {
$this->assertEquals($expectedValue, $actualValue, $this->ignoreExtraKeysInEmbedded);
continue;
}
if (gettype($expectedValue) != gettype($actualValue) || $expectedValue != $actualValue) {
throw new RuntimeException('$expectedValue != $actualValue for key: ' . $key);
}
}
if ($ignoreExtraKeys) {
return;
}
foreach ($actual as $key => $value) {
if (!$expected->offsetExists($key)) {
throw new RuntimeException('$actual has extra key: ' . $key);
}
}
}
/**
* Prepare a BSON document or array for comparison.
*
* The argument will be converted to a BSONArray or BSONDocument based on
* its type and keys. Keys within documents will optionally be sorted. Each
* value within the array or document will then be prepared recursively.
*
* @param array|object $bson
* @param boolean $isRoot If true, ensure an array value is converted to a document
* @param boolean $sortKeys
* @return BSONDocument|BSONArray
* @throws InvalidArgumentException if $bson is not an array or object
*/
private function prepareBSON($bson, $isRoot, $sortKeys = false)
{
if ( ! is_array($bson) && ! is_object($bson)) {
throw new InvalidArgumentException('$bson is not an array or object');
}
if ($isRoot && is_array($bson)) {
$bson = (object) $bson;
}
if ($bson instanceof BSONArray || (is_array($bson) && $bson === array_values($bson))) {
if ( ! $bson instanceof BSONArray) {
$bson = new BSONArray($bson);
}
} else {
if ( ! $bson instanceof BSONDocument) {
$bson = new BSONDocument((array) $bson);
}
if ($sortKeys) {
$bson->ksort();
}
}
foreach ($bson as $key => $value) {
if ($value instanceof BSONArray || (is_array($value) && $value === array_values($value))) {
$bson[$key] = $this->prepareBSON($value, false, $sortKeys);
continue;
}
if ($value instanceof BSONDocument || $value instanceof stdClass || is_array($value)) {
$bson[$key] = $this->prepareBSON($value, false, $sortKeys);
continue;
}
}
return $bson;
}
}
<?php
namespace MongoDB\Tests\SpecTests;
use PHPUnit\Framework\TestCase;
class DocumentsMatchConstraintTest extends TestCase
{
public function testIgnoreExtraKeysInRoot()
{
$c = new DocumentsMatchConstraint(['x' => 1, 'y' => ['a' => 1, 'b' => 2]], true, false);
$this->assertResult(false, $c, ['x' => 1, 'y' => 2], 'Incorrect value');
$this->assertResult(true, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2]], 'Exact match');
$this->assertResult(true, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2], 'z' => 3], 'Extra keys in root are permitted');
$this->assertResult(false, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are not permitted');
$this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded key order is not significant');
// Arrays are always intepretted as root documents
$c = new DocumentsMatchConstraint([1, ['a' => 1]], true, false);
$this->assertResult(false, $c, [1, 2], 'Incorrect value');
$this->assertResult(true, $c, [1, ['a' => 1]], 'Exact match');
$this->assertResult(true, $c, [1, ['a' => 1], 3], 'Extra keys in root are permitted');
$this->assertResult(false, $c, [1, ['a' => 1, 'b' => 2]], 'Extra keys in embedded are not permitted');
}
public function testIgnoreExtraKeysInEmbedded()
{
$c = new DocumentsMatchConstraint(['x' => 1, 'y' => ['a' => 1, 'b' => 2]], false, true);
$this->assertResult(false, $c, ['x' => 1, 'y' => 2], 'Incorrect value');
$this->assertResult(false, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 3]], 'Incorrect value');
$this->assertResult(true, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2]], 'Exact match');
$this->assertResult(false, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2], 'z' => 3], 'Extra keys in root are not permitted');
$this->assertResult(true, $c, ['x' => 1, 'y' => ['a' => 1, 'b' => 2, 'c' => 3]], 'Extra keys in embedded are permitted');
$this->assertResult(true, $c, ['y' => ['b' => 2, 'a' => 1], 'x' => 1], 'Root and embedded Key order is not significant');
// Arrays are always intepretted as root documents
$c = new DocumentsMatchConstraint([1, ['a' => 1]], false, true);
$this->assertResult(false, $c, [1, 2], 'Incorrect value');
$this->assertResult(true, $c, [1, ['a' => 1]], 'Exact match');
$this->assertResult(false, $c, [1, ['a' => 1], 3], 'Extra keys in root are not permitted');
$this->assertResult(true, $c, [1, ['a' => 1, 'b' => 2]], 'Extra keys in embedded are permitted');
$this->assertResult(false, $c, [1, ['a' => 2]], 'Keys must have the correct value');
}
private function assertResult($expectedResult, DocumentsMatchConstraint $constraint, $value, $message)
{
$this->assertSame($expectedResult, $constraint->evaluate($value, '', true), $message);
}
}
...@@ -60,7 +60,34 @@ abstract class FunctionalTestCase extends BaseFunctionalTestCase ...@@ -60,7 +60,34 @@ abstract class FunctionalTestCase extends BaseFunctionalTestCase
* @param stdClass $expectedCommand Expected command document * @param stdClass $expectedCommand Expected command document
* @param stdClass $actualCommand Actual command document * @param stdClass $actualCommand Actual command document
*/ */
abstract public function assertSameCommand(stdClass $expectedCommand, stdClass $actualCommand); abstract public static function assertCommandMatches(stdClass $expected, stdClass $actual);
/**
* Assert that the expected and actual command reply documents match.
*
* Note: Spec tests that do not assert command started events may throw an
* exception in lieu of implementing this method.
*
* @param stdClass $expected Expected command reply document
* @param stdClass $actual Actual command reply document
*/
abstract public static function assertCommandReplyMatches(stdClass $expected, stdClass $actual);
/**
* Asserts that two given documents match.
*
* Extra keys in the actual value's document(s) will be ignored.
*
* @param array|object $expectedDocument
* @param array|object $actualDocument
* @param string $message
*/
protected static function assertDocumentsMatch($expectedDocument, $actualDocument, $message = '')
{
$constraint = new DocumentsMatchConstraint($expectedDocument, true, true);
static::assertThat($actualDocument, $constraint, $message);
}
/** /**
* Assert data within the outcome collection. * Assert data within the outcome collection.
......
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
namespace MongoDB\Tests\SpecTests; namespace MongoDB\Tests\SpecTests;
use MongoDB\Collection; use MongoDB\Collection;
use MongoDB\Operation\FindOneAndReplace; use MongoDB\Driver\Cursor;
use MongoDB\Operation\FindOneAndUpdate;
use MongoDB\Driver\Session; use MongoDB\Driver\Session;
use MongoDB\Driver\Exception\BulkWriteException; use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\Exception; use MongoDB\Driver\Exception\Exception;
use MongoDB\Operation\FindOneAndReplace;
use MongoDB\Operation\FindOneAndUpdate;
use LogicException; use LogicException;
use stdClass; use stdClass;
...@@ -43,6 +44,17 @@ final class Operation ...@@ -43,6 +44,17 @@ final class Operation
} }
} }
public static function fromCommandMonitoring(stdClass $operation)
{
$o = new self($operation);
if (isset($operation->collectionOptions)) {
$o->collectionOptions = (array) $operation->collectionOptions;
}
return $o;
}
public static function fromRetryableWrites(stdClass $operation, stdClass $outcome) public static function fromRetryableWrites(stdClass $operation, stdClass $outcome)
{ {
$o = new self($operation); $o = new self($operation);
...@@ -84,6 +96,14 @@ final class Operation ...@@ -84,6 +96,14 @@ final class Operation
try { try {
$result = $this->execute($context); $result = $this->execute($context);
/* Eagerly iterate the results of a cursor. This both allows an
* exception to be thrown sooner and ensures that any expected
* getMore command(s) can be observed even if a ResultExpectation
* is not used (e.g. Command Monitoring spec). */
if ($result instanceof Cursor) {
$result = $result->toArray();
}
} catch (Exception $e) { } catch (Exception $e) {
$exception = $e; $exception = $e;
} }
...@@ -93,8 +113,13 @@ final class Operation ...@@ -93,8 +113,13 @@ final class Operation
$result = $exception->getWriteResult(); $result = $exception->getWriteResult();
} }
$this->errorExpectation->assert($test, $exception); if (isset($this->errorExpectation)) {
$this->resultExpectation->assert($test, $result); $this->errorExpectation->assert($test, $exception);
}
if (isset($this->resultExpectation)) {
$this->resultExpectation->assert($test, $result);
}
} }
/** /**
......
...@@ -12,11 +12,16 @@ use stdClass; ...@@ -12,11 +12,16 @@ use stdClass;
*/ */
class RetryableWritesSpecTest extends FunctionalTestCase class RetryableWritesSpecTest extends FunctionalTestCase
{ {
public function assertSameCommand(stdClass $expectedCommand, stdClass $actualCommand) public static function assertCommandMatches(stdClass $expected, stdClass $actual)
{ {
throw new LogicException('Retryable writes spec tests do not assert CommandStartedEvents'); throw new LogicException('Retryable writes spec tests do not assert CommandStartedEvents');
} }
public static function assertCommandReplyMatches(stdClass $expected, stdClass $actual)
{
throw new LogicException('Retryable writes spec tests do not assert CommandSucceededEvents');
}
/** /**
* Execute an individual test case from the specification. * Execute an individual test case from the specification.
* *
......
...@@ -56,89 +56,62 @@ class TransactionsSpecTest extends FunctionalTestCase ...@@ -56,89 +56,62 @@ class TransactionsSpecTest extends FunctionalTestCase
parent::tearDown(); parent::tearDown();
} }
/**
* Kill all sessions on the cluster.
*
* This will clean up any open transactions that may remain from a
* previously failed test. For sharded clusters, this command will be run
* on all mongos nodes.
*/
private static function killAllSessions()
{
$manager = new Manager(static::getUri());
$primary = $manager->selectServer(new ReadPreference('primary'));
$servers = ($primary->getType() === Server::TYPE_MONGOS)
? $manager->getServers()
: [$primary];
foreach ($servers as $server) {
try {
// Skip servers that do not support sessions
if (!isset($server->getInfo()['logicalSessionTimeoutMinutes'])) {
continue;
}
$server->executeCommand('admin', new Command(['killAllSessions' => []]));
} catch (ServerException $e) {
// Interrupted error is safe to ignore (see: SERVER-38335)
if ($e->getCode() != self::INTERRUPTED) {
throw $e;
}
}
}
}
/** /**
* Assert that the expected and actual command documents match. * Assert that the expected and actual command documents match.
* *
* Note: this method may modify the $expectedCommand object. * Note: this method may modify the $expected object.
* *
* @param stdClass $expectedCommand Expected command document * @param stdClass $expected Expected command document
* @param stdClass $actualCommand Actual command document * @param stdClass $actual Actual command document
*/ */
public function assertSameCommand(stdClass $expectedCommand, stdClass $actualCommand) public static function assertCommandMatches(stdClass $expected, stdClass $actual)
{ {
if (isset($expectedCommand->getMore) && $expectedCommand->getMore === 42) { if (isset($expected->getMore) && $expected->getMore === 42) {
$this->assertObjectHasAttribute('getMore', $actualCommand); static::assertObjectHasAttribute('getMore', $actual);
$this->assertThat($actualCommand->getMore, $this->logicalOr( static::assertThat($actual->getMore, static::logicalOr(
$this->isInstanceOf(Int64::class), static::isInstanceOf(Int64::class),
$this->isType('integer') static::isType('integer')
)); ));
unset($expectedCommand->getMore); unset($expected->getMore);
} }
if (isset($expectedCommand->recoveryToken) && $expectedCommand->recoveryToken === 42) { if (isset($expected->recoveryToken) && $expected->recoveryToken === 42) {
$this->assertObjectHasAttribute('recoveryToken', $actualCommand); static::assertObjectHasAttribute('recoveryToken', $actual);
$this->assertInternalType('object', $actualCommand->recoveryToken); static::assertInternalType('object', $actual->recoveryToken);
unset($expectedCommand->recoveryToken); unset($expected->recoveryToken);
} }
if (isset($expectedCommand->readConcern->afterClusterTime) && $expectedCommand->readConcern->afterClusterTime === 42) { if (isset($expected->readConcern->afterClusterTime) && $expected->readConcern->afterClusterTime === 42) {
$this->assertObjectHasAttribute('readConcern', $actualCommand); static::assertObjectHasAttribute('readConcern', $actual);
$this->assertInternalType('object', $actualCommand->readConcern); static::assertInternalType('object', $actual->readConcern);
$this->assertObjectHasAttribute('afterClusterTime', $actualCommand->readConcern); static::assertObjectHasAttribute('afterClusterTime', $actual->readConcern);
$this->assertInstanceOf(Timestamp::class, $actualCommand->readConcern->afterClusterTime); static::assertInstanceOf(Timestamp::class, $actual->readConcern->afterClusterTime);
unset($expectedCommand->readConcern->afterClusterTime); unset($expected->readConcern->afterClusterTime);
/* If "afterClusterTime" was the only assertion for "readConcern", /* If "afterClusterTime" was the only assertion for "readConcern",
* unset the field to avoid expecting an empty document later. */ * unset the field to avoid expecting an empty document later. */
if (get_object_vars($expectedCommand->readConcern) === []) { if (get_object_vars($expected->readConcern) === []) {
unset($expectedCommand->readConcern); unset($expected->readConcern);
} }
} }
/* TODO: Determine if forcing a new libmongoc client in Context is /* TODO: Determine if forcing a new libmongoc client in Context is
* preferable to skipping the txnNumber assertion. */ * preferable to skipping the txnNumber assertion. */
//unset($expectedCommand['txnNumber']); //unset($expected['txnNumber']);
foreach ($expectedCommand as $key => $value) { foreach ($expected as $key => $value) {
if ($value === null) { if ($value === null) {
$this->assertObjectNotHasAttribute($key, $actualCommand); static::assertObjectNotHasAttribute($key, $actual);
unset($expectedCommand->{$key}); unset($expected->{$key});
} }
} }
$this->assertMatchesDocument($expectedCommand, $actualCommand); static::assertDocumentsMatch($expected, $actual);
}
public static function assertCommandReplyMatches(stdClass $expected, stdClass $actual)
{
throw new LogicException('Transactions spec tests do not assert CommandSucceededEvents');
} }
/** /**
...@@ -242,6 +215,38 @@ class TransactionsSpecTest extends FunctionalTestCase ...@@ -242,6 +215,38 @@ class TransactionsSpecTest extends FunctionalTestCase
$database->createCollection($context->collectionName, $context->defaultWriteOptions); $database->createCollection($context->collectionName, $context->defaultWriteOptions);
} }
/**
* Kill all sessions on the cluster.
*
* This will clean up any open transactions that may remain from a
* previously failed test. For sharded clusters, this command will be run
* on all mongos nodes.
*/
private static function killAllSessions()
{
$manager = new Manager(static::getUri());
$primary = $manager->selectServer(new ReadPreference('primary'));
$servers = ($primary->getType() === Server::TYPE_MONGOS)
? $manager->getServers()
: [$primary];
foreach ($servers as $server) {
try {
// Skip servers that do not support sessions
if (!isset($server->getInfo()['logicalSessionTimeoutMinutes'])) {
continue;
}
$server->executeCommand('admin', new Command(['killAllSessions' => []]));
} catch (ServerException $e) {
// Interrupted error is safe to ignore (see: SERVER-38335)
if ($e->getCode() != self::INTERRUPTED) {
throw $e;
}
}
}
}
/** /**
* Work around potential error executing distinct on sharded clusters. * Work around potential error executing distinct on sharded clusters.
* *
......
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful mixed bulk write",
"operation": {
"name": "bulkWrite",
"arguments": {
"requests": [
{
"name": "insertOne",
"arguments": {
"document": {
"_id": 4,
"x": 44
}
}
},
{
"name": "updateOne",
"arguments": {
"filter": {
"_id": 3
},
"update": {
"$set": {
"x": 333
}
}
}
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 4,
"x": 44
}
],
"ordered": true
},
"command_name": "insert",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "insert"
}
},
{
"command_started_event": {
"command": {
"update": "test",
"updates": [
{
"q": {
"_id": 3
},
"u": {
"$set": {
"x": 333
}
}
}
],
"ordered": true
},
"command_name": "update",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "update"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful command",
"operation": {
"name": "count",
"arguments": {
"filter": {
"_id": 1
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"count": "test",
"query": {
"_id": 1
}
},
"command_name": "count",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "count"
}
}
]
},
{
"description": "A failed command event",
"operation": {
"name": "count",
"arguments": {
"filter": {
"$or": true
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"count": "test",
"query": {
"$or": true
}
},
"command_name": "count",
"database_name": "command-monitoring-tests"
}
},
{
"command_failed_event": {
"command_name": "count"
}
}
]
},
{
"description": "A successful command with a non-primary read preference",
"operation": {
"name": "count",
"arguments": {
"filter": {
"_id": 1
}
},
"read_preference": {
"mode": "primaryPreferred"
}
},
"expectations": [
{
"command_started_event": {
"command": {
"count": "test",
"query": {
"_id": 1
}
},
"command_name": "count",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "count"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful delete many",
"operation": {
"name": "deleteMany",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"delete": "test",
"deletes": [
{
"q": {
"_id": {
"$gt": 1
}
},
"limit": 0
}
],
"ordered": true
},
"command_name": "delete",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 2
},
"command_name": "delete"
}
}
]
},
{
"description": "A successful delete many command with write errors",
"operation": {
"name": "deleteMany",
"arguments": {
"filter": {
"_id": {
"$nothing": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"delete": "test",
"deletes": [
{
"q": {
"_id": {
"$nothing": 1
}
},
"limit": 0
}
],
"ordered": true
},
"command_name": "delete",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 0,
"writeErrors": [
{
"index": 0,
"code": 42,
"errmsg": ""
}
]
},
"command_name": "delete"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful delete one",
"operation": {
"name": "deleteOne",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"delete": "test",
"deletes": [
{
"q": {
"_id": {
"$gt": 1
}
},
"limit": 1
}
],
"ordered": true
},
"command_name": "delete",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "delete"
}
}
]
},
{
"description": "A successful delete one command with write errors",
"operation": {
"name": "deleteOne",
"arguments": {
"filter": {
"_id": {
"$nothing": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"delete": "test",
"deletes": [
{
"q": {
"_id": {
"$nothing": 1
}
},
"limit": 1
}
],
"ordered": true
},
"command_name": "delete",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 0,
"writeErrors": [
{
"index": 0,
"code": 42,
"errmsg": ""
}
]
},
"command_name": "delete"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
},
{
"_id": 4,
"x": 44
},
{
"_id": 5,
"x": 55
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"namespace": "command-monitoring-tests.test",
"tests": [
{
"description": "A successful find event with no options",
"operation": {
"name": "find",
"arguments": {
"filter": {
"_id": 1
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"find": "test",
"filter": {
"_id": 1
}
},
"command_name": "find",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "0"
},
"ns": "command-monitoring-tests.test",
"firstBatch": [
{
"_id": 1,
"x": 11
}
]
}
},
"command_name": "find"
}
}
]
},
{
"description": "A successful find event with options",
"operation": {
"name": "find",
"read_preference": {
"mode": "primaryPreferred"
},
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
},
"sort": {
"_id": 1
},
"skip": {
"$numberLong": "2"
},
"modifiers": {
"$comment": "test",
"$hint": {
"_id": 1
},
"$max": {
"_id": 6
},
"$maxTimeMS": 6000,
"$min": {
"_id": 0
},
"$returnKey": false,
"$showDiskLoc": false
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"find": "test",
"filter": {
"_id": {
"$gt": 1
}
},
"sort": {
"_id": 1
},
"skip": {
"$numberLong": "2"
},
"comment": "test",
"hint": {
"_id": 1
},
"max": {
"_id": 6
},
"maxTimeMS": 6000,
"min": {
"_id": 0
},
"returnKey": false,
"showRecordId": false
},
"command_name": "find",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "0"
},
"ns": "command-monitoring-tests.test",
"firstBatch": [
{
"_id": 4,
"x": 44
},
{
"_id": 5,
"x": 55
}
]
}
},
"command_name": "find"
}
}
]
},
{
"description": "A successful find event with a getmore",
"operation": {
"name": "find",
"arguments": {
"filter": {
"_id": {
"$gte": 1
}
},
"sort": {
"_id": 1
},
"batchSize": {
"$numberLong": "3"
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"find": "test",
"filter": {
"_id": {
"$gte": 1
}
},
"sort": {
"_id": 1
},
"batchSize": {
"$numberLong": "3"
}
},
"command_name": "find",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "42"
},
"ns": "command-monitoring-tests.test",
"firstBatch": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
},
"command_name": "find"
}
},
{
"command_started_event": {
"command": {
"getMore": {
"$numberLong": "42"
},
"collection": "test",
"batchSize": {
"$numberLong": "3"
}
},
"command_name": "getMore",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "0"
},
"ns": "command-monitoring-tests.test",
"nextBatch": [
{
"_id": 4,
"x": 44
},
{
"_id": 5,
"x": 55
}
]
}
},
"command_name": "getMore"
}
}
]
},
{
"description": "A successful find event with a getmore and killcursors",
"ignore_if_server_version_greater_than": "3.0",
"operation": {
"name": "find",
"arguments": {
"filter": {
"_id": {
"$gte": 1
}
},
"sort": {
"_id": 1
},
"batchSize": {
"$numberLong": "3"
},
"limit": {
"$numberLong": "4"
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"find": "test",
"filter": {
"_id": {
"$gte": 1
}
},
"sort": {
"_id": 1
},
"batchSize": {
"$numberLong": "3"
},
"limit": {
"$numberLong": "4"
}
},
"command_name": "find",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "42"
},
"ns": "command-monitoring-tests.test",
"firstBatch": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
},
"command_name": "find"
}
},
{
"command_started_event": {
"command": {
"getMore": {
"$numberLong": "42"
},
"collection": "test",
"batchSize": {
"$numberLong": "1"
}
},
"command_name": "getMore",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "42"
},
"ns": "command-monitoring-tests.test",
"nextBatch": [
{
"_id": 4,
"x": 44
}
]
}
},
"command_name": "getMore"
}
},
{
"command_started_event": {
"command": {
"killCursors": "test",
"cursors": [
{
"$numberLong": "42"
}
]
},
"command_name": "killCursors",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursorsUnknown": [
{
"$numberLong": "42"
}
]
},
"command_name": "killCursors"
}
}
]
},
{
"description": "A successful find event with a getmore and the server kills the cursor",
"ignore_if_server_version_less_than": "3.1",
"ignore_if_topology_type": [
"sharded"
],
"operation": {
"name": "find",
"arguments": {
"filter": {
"_id": {
"$gte": 1
}
},
"sort": {
"_id": 1
},
"batchSize": {
"$numberLong": "3"
},
"limit": {
"$numberLong": "4"
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"find": "test",
"filter": {
"_id": {
"$gte": 1
}
},
"sort": {
"_id": 1
},
"batchSize": {
"$numberLong": "3"
},
"limit": {
"$numberLong": "4"
}
},
"command_name": "find",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "42"
},
"ns": "command-monitoring-tests.test",
"firstBatch": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
]
}
},
"command_name": "find"
}
},
{
"command_started_event": {
"command": {
"getMore": {
"$numberLong": "42"
},
"collection": "test",
"batchSize": {
"$numberLong": "1"
}
},
"command_name": "getMore",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"cursor": {
"id": {
"$numberLong": "0"
},
"ns": "command-monitoring-tests.test",
"nextBatch": [
{
"_id": 4,
"x": 44
}
]
}
},
"command_name": "getMore"
}
}
]
},
{
"description": "A failed find event",
"operation": {
"name": "find",
"arguments": {
"filter": {
"$or": true
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"find": "test",
"filter": {
"$or": true
}
},
"command_name": "find",
"database_name": "command-monitoring-tests"
}
},
{
"command_failed_event": {
"command_name": "find"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful insert many",
"operation": {
"name": "insertMany",
"arguments": {
"documents": [
{
"_id": 2,
"x": 22
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 2,
"x": 22
}
],
"ordered": true
},
"command_name": "insert",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "insert"
}
}
]
},
{
"description": "A successful insert many command with write errors",
"operation": {
"name": "insertMany",
"arguments": {
"documents": [
{
"_id": 1,
"x": 11
}
]
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 1,
"x": 11
}
],
"ordered": true
},
"command_name": "insert",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 0,
"writeErrors": [
{
"index": 0,
"code": 42,
"errmsg": ""
}
]
},
"command_name": "insert"
}
}
]
},
{
"description": "A successful unordered insert many",
"operation": {
"name": "insertMany",
"arguments": {
"documents": [
{
"_id": 2,
"x": 22
}
],
"options": {
"ordered": false
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 2,
"x": 22
}
],
"ordered": false
},
"command_name": "insert",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "insert"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful insert one",
"operation": {
"name": "insertOne",
"arguments": {
"document": {
"_id": 2,
"x": 22
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 2,
"x": 22
}
],
"ordered": true
},
"command_name": "insert",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "insert"
}
}
]
},
{
"description": "A successful insert one command with write errors",
"operation": {
"name": "insertOne",
"arguments": {
"document": {
"_id": 1,
"x": 11
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 1,
"x": 11
}
],
"ordered": true
},
"command_name": "insert",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 0,
"writeErrors": [
{
"index": 0,
"code": 42,
"errmsg": ""
}
]
},
"command_name": "insert"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
}
],
"collection_name": "test-unacknowledged-bulk-write",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful unordered bulk write with an unacknowledged write concern",
"comment": "On a 2.4 server, no GLE is sent and requires a client-side manufactured reply",
"operation": {
"name": "bulkWrite",
"collectionOptions": {
"writeConcern": {
"w": 0
}
},
"arguments": {
"requests": [
{
"name": "insertOne",
"arguments": {
"document": {
"_id": "unorderedBulkWriteInsertW0",
"x": 44
}
}
}
],
"options": {
"ordered": false
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test-unacknowledged-bulk-write",
"documents": [
{
"_id": "unorderedBulkWriteInsertW0",
"x": 44
}
],
"ordered": false,
"writeConcern": {
"w": 0
}
},
"command_name": "insert",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1
},
"command_name": "insert"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful update many",
"operation": {
"name": "updateMany",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
},
"update": {
"$inc": {
"x": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"ordered": true,
"updates": [
{
"q": {
"_id": {
"$gt": 1
}
},
"u": {
"$inc": {
"x": 1
}
},
"multi": true
}
]
},
"command_name": "update",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 2
},
"command_name": "update"
}
}
]
},
{
"description": "A successful update many command with write errors",
"operation": {
"name": "updateMany",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
},
"update": {
"$nothing": {
"x": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"ordered": true,
"updates": [
{
"q": {
"_id": {
"$gt": 1
}
},
"u": {
"$nothing": {
"x": 1
}
},
"multi": true
}
]
},
"command_name": "update",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 0,
"writeErrors": [
{
"index": 0,
"code": 42,
"errmsg": ""
}
]
},
"command_name": "update"
}
}
]
}
]
}
{
"data": [
{
"_id": 1,
"x": 11
},
{
"_id": 2,
"x": 22
},
{
"_id": 3,
"x": 33
}
],
"collection_name": "test",
"database_name": "command-monitoring-tests",
"tests": [
{
"description": "A successful update one",
"operation": {
"name": "updateOne",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
},
"update": {
"$inc": {
"x": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"ordered": true,
"updates": [
{
"q": {
"_id": {
"$gt": 1
}
},
"u": {
"$inc": {
"x": 1
}
}
}
]
},
"command_name": "update",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1
},
"command_name": "update"
}
}
]
},
{
"description": "A successful update one with upsert when the upserted id is not an object id",
"operation": {
"name": "updateOne",
"arguments": {
"filter": {
"_id": 4
},
"update": {
"$inc": {
"x": 1
}
},
"upsert": true
}
},
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"ordered": true,
"updates": [
{
"q": {
"_id": 4
},
"u": {
"$inc": {
"x": 1
}
},
"upsert": true
}
]
},
"command_name": "update",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 1,
"upserted": [
{
"index": 0,
"_id": 4
}
]
},
"command_name": "update"
}
}
]
},
{
"description": "A successful update one command with write errors",
"operation": {
"name": "updateOne",
"arguments": {
"filter": {
"_id": {
"$gt": 1
}
},
"update": {
"$nothing": {
"x": 1
}
}
}
},
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"ordered": true,
"updates": [
{
"q": {
"_id": {
"$gt": 1
}
},
"u": {
"$nothing": {
"x": 1
}
}
}
]
},
"command_name": "update",
"database_name": "command-monitoring-tests"
}
},
{
"command_succeeded_event": {
"reply": {
"ok": 1,
"n": 0,
"writeErrors": [
{
"index": 0,
"code": 42,
"errmsg": ""
}
]
},
"command_name": "update"
}
}
]
}
]
}
...@@ -13,3 +13,7 @@ if (file_exists(__DIR__ . '/../vendor/autoload.php')) { ...@@ -13,3 +13,7 @@ if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
if ( ! class_exists('PHPUnit\Framework\Error\Warning')) { if ( ! class_exists('PHPUnit\Framework\Error\Warning')) {
class_alias('PHPUnit_Framework_Error_Warning', 'PHPUnit\Framework\Error\Warning'); class_alias('PHPUnit_Framework_Error_Warning', 'PHPUnit\Framework\Error\Warning');
} }
if ( ! class_exists('PHPUnit\Framework\Constraint\Constraint')) {
class_alias('PHPUnit_Framework_Constraint', 'PHPUnit\Framework\Constraint\Constraint');
}
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