Commit c142fe01 authored by Jeremy Mikola's avatar Jeremy Mikola

Merge pull request #614

parents cb595b6d 4b4a7d99
...@@ -14,7 +14,7 @@ cache: ...@@ -14,7 +14,7 @@ cache:
env: env:
global: global:
- DRIVER_VERSION=1.6.0alpha1 - DRIVER_VERSION=1.6.0alpha2
- SERVER_DISTRO=ubuntu1604 - SERVER_DISTRO=ubuntu1604
- SERVER_VERSION=4.0.9 - SERVER_VERSION=4.0.9
- DEPLOYMENT=STANDALONE - DEPLOYMENT=STANDALONE
......
...@@ -18,7 +18,7 @@ class ClientFunctionalTest extends FunctionalTestCase ...@@ -18,7 +18,7 @@ class ClientFunctionalTest extends FunctionalTestCase
{ {
parent::setUp(); parent::setUp();
$this->client = new Client($this->getUri()); $this->client = new Client(static::getUri());
$this->client->dropDatabase($this->getDatabaseName()); $this->client->dropDatabase($this->getDatabaseName());
} }
......
...@@ -26,7 +26,7 @@ class ClientTest extends TestCase ...@@ -26,7 +26,7 @@ class ClientTest extends TestCase
public function testConstructorDriverOptionTypeChecks(array $driverOptions) public function testConstructorDriverOptionTypeChecks(array $driverOptions)
{ {
$this->expectException(InvalidArgumentException::class); $this->expectException(InvalidArgumentException::class);
new Client($this->getUri(), [], $driverOptions); new Client(static::getUri(), [], $driverOptions);
} }
public function provideInvalidConstructorDriverOptions() public function provideInvalidConstructorDriverOptions()
...@@ -42,9 +42,9 @@ class ClientTest extends TestCase ...@@ -42,9 +42,9 @@ class ClientTest extends TestCase
public function testToString() public function testToString()
{ {
$client = new Client($this->getUri()); $client = new Client(static::getUri());
$this->assertSame($this->getUri(), (string) $client); $this->assertSame(static::getUri(), (string) $client);
} }
public function testSelectCollectionInheritsOptions() public function testSelectCollectionInheritsOptions()
...@@ -59,7 +59,7 @@ class ClientTest extends TestCase ...@@ -59,7 +59,7 @@ class ClientTest extends TestCase
'typeMap' => ['root' => 'array'], 'typeMap' => ['root' => 'array'],
]; ];
$client = new Client($this->getUri(), $uriOptions, $driverOptions); $client = new Client(static::getUri(), $uriOptions, $driverOptions);
$collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName()); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName());
$debug = $collection->__debugInfo(); $debug = $collection->__debugInfo();
...@@ -82,7 +82,7 @@ class ClientTest extends TestCase ...@@ -82,7 +82,7 @@ class ClientTest extends TestCase
'writeConcern' => new WriteConcern(WriteConcern::MAJORITY), 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY),
]; ];
$client = new Client($this->getUri()); $client = new Client(static::getUri());
$collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName(), $collectionOptions); $collection = $client->selectCollection($this->getDatabaseName(), $this->getCollectionName(), $collectionOptions);
$debug = $collection->__debugInfo(); $debug = $collection->__debugInfo();
...@@ -100,7 +100,7 @@ class ClientTest extends TestCase ...@@ -100,7 +100,7 @@ class ClientTest extends TestCase
{ {
$uriOptions = ['w' => WriteConcern::MAJORITY]; $uriOptions = ['w' => WriteConcern::MAJORITY];
$client = new Client($this->getUri(), $uriOptions); $client = new Client(static::getUri(), $uriOptions);
$database = $client->{$this->getDatabaseName()}; $database = $client->{$this->getDatabaseName()};
$debug = $database->__debugInfo(); $debug = $database->__debugInfo();
...@@ -121,7 +121,7 @@ class ClientTest extends TestCase ...@@ -121,7 +121,7 @@ class ClientTest extends TestCase
'typeMap' => ['root' => 'array'], 'typeMap' => ['root' => 'array'],
]; ];
$client = new Client($this->getUri(), $uriOptions, $driverOptions); $client = new Client(static::getUri(), $uriOptions, $driverOptions);
$database = $client->selectDatabase($this->getDatabaseName()); $database = $client->selectDatabase($this->getDatabaseName());
$debug = $database->__debugInfo(); $debug = $database->__debugInfo();
...@@ -144,7 +144,7 @@ class ClientTest extends TestCase ...@@ -144,7 +144,7 @@ class ClientTest extends TestCase
'writeConcern' => new WriteConcern(WriteConcern::MAJORITY), 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY),
]; ];
$client = new Client($this->getUri()); $client = new Client(static::getUri());
$database = $client->selectDatabase($this->getDatabaseName(), $databaseOptions); $database = $client->selectDatabase($this->getDatabaseName(), $databaseOptions);
$debug = $database->__debugInfo(); $debug = $database->__debugInfo();
......
...@@ -1227,7 +1227,7 @@ class DocumentationExamplesTest extends FunctionalTestCase ...@@ -1227,7 +1227,7 @@ class DocumentationExamplesTest extends FunctionalTestCase
{ {
$this->skipIfTransactionsAreNotSupported(); $this->skipIfTransactionsAreNotSupported();
$client = new Client($this->getUri()); $client = new Client(static::getUri());
/* The WC is required: https://docs.mongodb.com/manual/core/transactions/#transactions-and-locks */ /* The WC is required: https://docs.mongodb.com/manual/core/transactions/#transactions-and-locks */
$client->hr->dropCollection('employees', ['writeConcern' => new \MongoDB\Driver\WriteConcern('majority')]); $client->hr->dropCollection('employees', ['writeConcern' => new \MongoDB\Driver\WriteConcern('majority')]);
...@@ -1387,7 +1387,7 @@ class DocumentationExamplesTest extends FunctionalTestCase ...@@ -1387,7 +1387,7 @@ class DocumentationExamplesTest extends FunctionalTestCase
{ {
$this->skipIfTransactionsAreNotSupported(); $this->skipIfTransactionsAreNotSupported();
$client = new Client($this->getUri()); $client = new Client(static::getUri());
/* The WC is required: https://docs.mongodb.com/manual/core/transactions/#transactions-and-locks */ /* The WC is required: https://docs.mongodb.com/manual/core/transactions/#transactions-and-locks */
$client->hr->dropCollection('employees', ['writeConcern' => new \MongoDB\Driver\WriteConcern('majority')]); $client->hr->dropCollection('employees', ['writeConcern' => new \MongoDB\Driver\WriteConcern('majority')]);
...@@ -1410,7 +1410,7 @@ class DocumentationExamplesTest extends FunctionalTestCase ...@@ -1410,7 +1410,7 @@ class DocumentationExamplesTest extends FunctionalTestCase
$this->skipIfCausalConsistencyIsNotSupported(); $this->skipIfCausalConsistencyIsNotSupported();
// Prep // Prep
$client = new Client($this->getUri()); $client = new Client(static::getUri());
$items = $client->selectDatabase( $items = $client->selectDatabase(
'test', 'test',
[ 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY) ] [ 'writeConcern' => new WriteConcern(WriteConcern::MAJORITY) ]
......
...@@ -17,11 +17,9 @@ use UnexpectedValueException; ...@@ -17,11 +17,9 @@ use UnexpectedValueException;
abstract class FunctionalTestCase extends TestCase abstract class FunctionalTestCase extends TestCase
{ {
protected $manager;
public function setUp() public function setUp()
{ {
$this->manager = new Manager($this->getUri()); $this->manager = new Manager(static::getUri());
} }
protected function assertCollectionCount($namespace, $count) protected function assertCollectionCount($namespace, $count)
......
...@@ -17,7 +17,7 @@ class FindAndModifyFunctionalTest extends FunctionalTestCase ...@@ -17,7 +17,7 @@ class FindAndModifyFunctionalTest extends FunctionalTestCase
*/ */
public function testManagerReadConcernIsOmitted() public function testManagerReadConcernIsOmitted()
{ {
$manager = new Manager($this->getUri(), ['readConcernLevel' => 'majority']); $manager = new Manager(static::getUri(), ['readConcernLevel' => 'majority']);
$server = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); $server = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
(new CommandObserver)->observe( (new CommandObserver)->observe(
......
...@@ -75,7 +75,7 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -75,7 +75,7 @@ class WatchFunctionalTest extends FunctionalTestCase
/* In order to trigger a dropped connection, we'll use a new client with /* In order to trigger a dropped connection, we'll use a new client with
* a socket timeout that is less than the change stream's maxAwaitTimeMS * a socket timeout that is less than the change stream's maxAwaitTimeMS
* option. */ * option. */
$manager = new Manager($this->getUri(), ['socketTimeoutMS' => 50]); $manager = new Manager(static::getUri(), ['socketTimeoutMS' => 50]);
$primaryServer = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); $primaryServer = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
$operation = new Watch($manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions); $operation = new Watch($manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
...@@ -222,7 +222,7 @@ class WatchFunctionalTest extends FunctionalTestCase ...@@ -222,7 +222,7 @@ class WatchFunctionalTest extends FunctionalTestCase
/* In order to trigger a dropped connection, we'll use a new client with /* In order to trigger a dropped connection, we'll use a new client with
* a socket timeout that is less than the change stream's maxAwaitTimeMS * a socket timeout that is less than the change stream's maxAwaitTimeMS
* option. */ * option. */
$manager = new Manager($this->getUri(), ['socketTimeoutMS' => 50]); $manager = new Manager(static::getUri(), ['socketTimeoutMS' => 50]);
$primaryServer = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY)); $primaryServer = $manager->selectServer(new ReadPreference(ReadPreference::RP_PRIMARY));
$operation = new Watch($manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions); $operation = new Watch($manager, $this->getDatabaseName(), $this->getCollectionName(), [], $this->defaultOptions);
......
<?php
namespace MongoDB\Tests\SpecTests;
use MongoDB\BSON\Timestamp;
use MongoDB\Driver\Monitoring\CommandFailedEvent;
use MongoDB\Driver\Monitoring\CommandStartedEvent;
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
use MongoDB\Driver\Monitoring\CommandSubscriber;
use ArrayIterator;
use LogicException;
use MultipleIterator;
use stdClass;
/**
* Spec test CommandStartedEvent expectations.
*/
class CommandExpectations implements CommandSubscriber
{
private $commandStartedEvents = [];
private $expectedCommandStartedEvents = [];
public static function fromTransactions(array $expectedEvents)
{
$o = new self;
foreach ($expectedEvents as $expectedEvent) {
if (!isset($expectedEvent->command_started_event)) {
throw new LogicException('$expectedEvent->command_started_event field is not set');
}
$o->expectedCommandStartedEvents[] = $expectedEvent->command_started_event;
}
return $o;
}
/**
* Not used.
*
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandfailed.php
*/
public function commandFailed(CommandFailedEvent $event)
{
}
/**
* Tracks outgoing commands for spec test APM assertions.
*
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandstarted.php
*/
public function commandStarted(CommandStartedEvent $event)
{
$this->commandStartedEvents[] = $event;
}
/**
* Not used.
*
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandsucceeded.php
*/
public function commandSucceeded(CommandSucceededEvent $event)
{
}
/**
* Start command monitoring.
*/
public function startMonitoring()
{
\MongoDB\Driver\Monitoring\addSubscriber($this);
}
/**
* Stop command monitoring.
*/
public function stopMonitoring()
{
\MongoDB\Driver\Monitoring\removeSubscriber($this);
}
/**
* Assert that the command expectations match the monitored events.
*
* @param FunctionalTestCase $test Test instance
* @param Context $context Execution context
*/
public function assert(FunctionalTestCase $test, Context $context)
{
$test->assertCount(count($this->expectedCommandStartedEvents), $this->commandStartedEvents);
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
$mi->attachIterator(new ArrayIterator($this->expectedCommandStartedEvents));
$mi->attachIterator(new ArrayIterator($this->commandStartedEvents));
foreach ($mi as $events) {
list($expectedEvent, $actualEvent) = $events;
$test->assertInternalType('object', $expectedEvent);
$test->assertInstanceOf(CommandStartedEvent::class, $actualEvent);
if (isset($expectedEvent->command_name)) {
$test->assertSame($expectedEvent->command_name, $actualEvent->getCommandName());
}
if (isset($expectedEvent->database_name)) {
$test->assertSame($expectedEvent->database_name, $actualEvent->getDatabaseName());
}
if (isset($expectedEvent->command)) {
$expectedCommand = $expectedEvent->command;
$context->replaceCommandSessionPlaceholder($expectedCommand);
$test->assertSameCommand($expectedCommand, $actualEvent->getCommand());
}
}
}
private function __construct()
{
}
}
<?php
namespace MongoDB\Tests\SpecTests;
use MongoDB\Client;
use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
use LogicException;
use stdClass;
/**
* Execution context for spec tests.
*
* This object tracks state that would be difficult to store on the test itself
* due to the design of PHPUnit's data providers and setUp/tearDown methods.
*/
final class Context
{
public $client;
public $collectionName;
public $databaseName;
public $defaultWriteOptions = [];
public $outcomeFindOptions = [];
public $outcomeCollectionName;
public $session0;
public $session0Lsid;
public $session1;
public $session1Lsid;
private function __construct($databaseName, $collectionName)
{
$this->databaseName = $databaseName;
$this->collectionName = $collectionName;
$this->outcomeCollectionName = $collectionName;
}
public static function fromRetryableWrites(stdClass $test, $databaseName, $collectionName)
{
$o = new self($databaseName, $collectionName);
$clientOptions = isset($test->clientOptions) ? (array) $test->clientOptions : [];
// TODO: Remove this once retryWrites=true by default (see: PHPC-1324)
$clientOptions['retryWrites'] = true;
if (isset($test->outcome->collection->name)) {
$o->outcomeCollectionName = $test->outcome->collection->name;
}
$o->client = new Client(FunctionalTestCase::getUri(), $clientOptions);
return $o;
}
public static function fromTransactions(stdClass $test, $databaseName, $collectionName)
{
$o = new self($databaseName, $collectionName);
$o->defaultWriteOptions = [
'writeConcern' => new WriteConcern(WriteConcern::MAJORITY),
];
$o->outcomeFindOptions = [
'readConcern' => new ReadConcern('local'),
'readPreference' => new ReadPreference('primary'),
];
$clientOptions = isset($test->clientOptions) ? (array) $test->clientOptions : [];
/* Transaction spec tests expect a new client for each test so that
* txnNumber values are deterministic. Append a random option to avoid
* re-using a previously persisted libmongoc client object. */
$clientOptions += ['p' => mt_rand()];
$o->client = new Client(FunctionalTestCase::getUri(), $clientOptions);
$session0Options = isset($test->sessionOptions->session0) ? (array) $test->sessionOptions->session0 : [];
$session1Options = isset($test->sessionOptions->session1) ? (array) $test->sessionOptions->session1 : [];
$o->session0 = $o->client->startSession($o->prepareSessionOptions($session0Options));
$o->session1 = $o->client->startSession($o->prepareSessionOptions($session1Options));
$o->session0Lsid = $o->session0->getLogicalSessionId();
$o->session1Lsid = $o->session1->getLogicalSessionId();
return $o;
}
public function getCollection(array $collectionOptions = [])
{
return $this->client->selectCollection(
$this->databaseName,
$this->collectionName,
$this->prepareOptions($collectionOptions)
);
}
public function getDatabase(array $databaseOptions = [])
{
return $this->client->selectDatabase(
$this->databaseName,
$this->prepareOptions($databaseOptions)
);
}
/**
* Prepare options readConcern, readPreference, and writeConcern options by
* creating value objects.
*
* @param array $options
* @return array
* @throws LogicException if any option keys are unsupported
*/
public function prepareOptions(array $options)
{
if (isset($options['readConcern']) && !($options['readConcern'] instanceof ReadConcern)) {
$readConcern = (array) $options['readConcern'];
$diff = array_diff_key($readConcern, ['level' => 1]);
if (!empty($diff)) {
throw new LogicException('Unsupported readConcern args: ' . implode(',', array_keys($diff)));
}
$options['readConcern'] = new ReadConcern($readConcern['level']);
}
if (isset($options['readPreference']) && !($options['readPreference'] instanceof ReadPreference)) {
$readPreference = (array) $options['readPreference'];
$diff = array_diff_key($readPreference, ['mode' => 1]);
if (!empty($diff)) {
throw new LogicException('Unsupported readPreference args: ' . implode(',', array_keys($diff)));
}
$options['readPreference'] = new ReadPreference($readPreference['mode']);
}
if (isset($options['writeConcern']) && !($options['writeConcern'] instanceof writeConcern)) {
$writeConcern = (array) $options['writeConcern'];
$diff = array_diff_key($writeConcern, ['w' => 1, 'wtimeout' => 1, 'j' => 1]);
if (!empty($diff)) {
throw new LogicException('Unsupported writeConcern args: ' . implode(',', array_keys($diff)));
}
$w = $writeConcern['w'];
$wtimeout = isset($writeConcern['wtimeout']) ? $writeConcern['wtimeout'] : 0;
$j = isset($writeConcern['j']) ? $writeConcern['j'] : null;
$options['writeConcern'] = isset($j)
? new WriteConcern($w, $wtimeout, $j)
: new WriteConcern($w, $wtimeout);
}
return $options;
}
/**
* Replace a session placeholder in an operation arguments array.
*
* Note: this method will modify the $args parameter.
*
* @param array $args Operation arguments
* @throws LogicException if the session placeholder is unsupported
*/
public function replaceArgumentSessionPlaceholder(array &$args)
{
if (!isset($args['session'])) {
return;
}
switch ($args['session']) {
case 'session0':
$args['session'] = $this->session0;
break;
case 'session1':
$args['session'] = $this->session1;
break;
default:
throw new LogicException('Unsupported session placeholder: ' . $args['session']);
}
}
/**
* Replace a logical session ID placeholder in a command document.
*
* Note: this method will modify the $command parameter.
*
* @param stdClass $command Command document
* @throws LogicException if the session placeholder is unsupported
*/
public function replaceCommandSessionPlaceholder(stdClass $command)
{
if (!isset($command->lsid)) {
return;
}
switch ($command->lsid) {
case 'session0':
$command->lsid = $this->session0Lsid;
break;
case 'session1':
$command->lsid = $this->session1Lsid;
break;
default:
throw new LogicException('Unsupported session placeholder: ' . $command->lsid);
}
}
private function prepareSessionOptions(array $options)
{
if (isset($options['defaultTransactionOptions'])) {
$options['defaultTransactionOptions'] = $this->prepareOptions((array) $options['defaultTransactionOptions']);
}
return $options;
}
}
<?php
namespace MongoDB\Tests\SpecTests;
use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\CommandException;
use MongoDB\Driver\Exception\RuntimeException;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\Tests\TestCase;
use Exception;
use stdClass;
/**
* Spec test operation error expectation.
*/
final class ErrorExpectation
{
/**
* @see https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.err
*/
private static $codeNameMap = [
'Interrupted' => 11601,
'WriteConflict' => 112,
'NoSuchTransaction' => 251,
'OperationNotSupportedInTransaction' => 263,
];
private $codeName;
private $isExpected = false;
private $excludedLabels = [];
private $includedLabels = [];
private $messageContains;
private function __construct()
{
}
public static function fromRetryableWrites(stdClass $outcome)
{
$o = new self;
if (isset($outcome->error)) {
$o->isExpected = $outcome->error;
}
return $o;
}
/**
* @throws InvalidArgumentException
*/
public static function fromTransactions(stdClass $operation)
{
$o = new self;
if (isset($operation->error)) {
$o->isExpected = $operation->error;
}
$result = isset($operation->result) ? $operation->result : null;
if (isset($result->errorContains)) {
$o->messageContains = $result->errorContains;
$o->isExpected = true;
}
if (isset($result->errorCodeName)) {
$o->codeName = $result->errorCodeName;
$o->isExpected = true;
}
if (isset($result->errorLabelsContain)) {
if (!self::isArrayOfStrings($result->errorLabelsContain)) {
throw InvalidArgumentException::invalidType('errorLabelsContain', $result->errorLabelsContain, 'string[]');
}
$o->includedLabels = $result->errorLabelsContain;
$o->isExpected = true;
}
if (isset($result->errorLabelsOmit)) {
if (!self::isArrayOfStrings($result->errorLabelsOmit)) {
throw InvalidArgumentException::invalidType('errorLabelsOmit', $result->errorLabelsOmit, 'string[]');
}
$o->excludedLabels = $result->errorLabelsOmit;
$o->isExpected = true;
}
return $o;
}
/**
* Assert that the error expectation matches the actual outcome.
*
* @param TestCase $test Test instance for performing assertions
* @param Exception|null $actual Exception (if any) from the actual outcome
*/
public function assert(TestCase $test, Exception $actual = null)
{
if (!$this->isExpected) {
if ($actual !== null) {
$test->fail(sprintf("Operation threw unexpected %s: %s\n%s", get_class($actual), $actual->getMessage(), $actual->getTraceAsString()));
}
return;
}
$test->assertNotNull($actual);
if (isset($this->messageContains)) {
$test->assertContains($this->messageContains, $actual->getMessage(), '', true /* case-insensitive */);
}
if (isset($this->codeName)) {
$this->assertCodeName($test, $actual);
}
if (!empty($this->excludedLabels) or !empty($this->includedLabels)) {
$test->assertInstanceOf(RuntimeException::class, $actual);
foreach ($this->excludedLabels as $label) {
$test->assertFalse($actual->hasErrorLabel($label), 'Exception should not have error label: ' . $label);
}
foreach ($this->includedLabels as $label) {
$test->assertTrue($actual->hasErrorLabel($label), 'Exception should have error label: ' . $label);
}
}
}
public function isExpected()
{
return $this->isExpected;
}
/**
* Assert that the error code name expectation matches the actual outcome.
*
* @param TestCase $test Test instance for performing assertions
* @param Exception|null $actual Exception (if any) from the actual outcome
*/
private function assertCodeName(TestCase $test, Exception $actual = null)
{
/* BulkWriteException does not expose codeName for server errors. Work
* around this be comparing the error code against a map.
*
* TODO: Remove this once PHPC-1386 is resolved. */
if ($actual instanceof BulkWriteException) {
$test->assertArrayHasKey($this->codeName, self::$codeNameMap);
$test->assertSame(self::$codeNameMap[$this->codeName], $actual->getCode());
return;
}
$test->assertInstanceOf(CommandException::class, $actual);
$result = $actual->getResultDocument();
$test->assertObjectHasAttribute('codeName', $result);
$test->assertAttributeSame($this->codeName, 'codeName', $result);
}
private static function isArrayOfStrings($array)
{
if (!is_array($array)) {
return false;
}
foreach ($array as $string) {
if (!is_string($string)) {
return false;
}
}
return true;
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
namespace MongoDB\Tests\SpecTests; namespace MongoDB\Tests\SpecTests;
use LogicException;
use stdClass;
/** /**
* Retryable writes spec tests. * Retryable writes spec tests.
* *
...@@ -9,38 +12,47 @@ namespace MongoDB\Tests\SpecTests; ...@@ -9,38 +12,47 @@ namespace MongoDB\Tests\SpecTests;
*/ */
class RetryableWritesSpecTest extends FunctionalTestCase class RetryableWritesSpecTest extends FunctionalTestCase
{ {
public function assertSameCommand(stdClass $expectedCommand, stdClass $actualCommand)
{
throw new LogicException('Retryable writes spec tests do not assert CommandStartedEvents');
}
/** /**
* Execute an individual test case from the specification. * Execute an individual test case from the specification.
* *
* @dataProvider provideTests * @dataProvider provideTests
* @param string $name Test name * @param string $name Test name
* @param array $test Individual "tests[]" document * @param stdClass $test Individual "tests[]" document
* @param array $runOn Top-level "runOn" document * @param array $runOn Top-level "runOn" array with server requirements
* @param array $data Top-level "data" array to initialize collection * @param array $data Top-level "data" array to initialize collection
*/ */
public function testRetryableWrites($name, array $test, array $runOn = null, array $data) public function testRetryableWrites($name, stdClass $test, array $runOn = null, array $data)
{ {
$this->setName($name); $this->setName($name);
// TODO: Revise this once a test environment with multiple mongos nodes is available (see: PHPLIB-430)
if (isset($test->useMultipleMongoses) && $test->useMultipleMongoses && $this->isShardedCluster()) {
$this->markTestSkipped('"useMultipleMongoses" is not supported');
}
if (isset($runOn)) { if (isset($runOn)) {
$this->checkServerRequirements($runOn); $this->checkServerRequirements($runOn);
} }
// TODO: Remove this once retryWrites=true by default (see: PHPC-1324) $context = Context::fromRetryableWrites($test, $this->getDatabaseName(), $this->getCollectionName());
$test['clientOptions']['retryWrites'] = true; $this->setContext($context);
$this->initTestSubjects($test); $this->dropTestAndOutcomeCollections();
$this->initOutcomeCollection($test); $this->insertDataFixtures($data);
$this->initDataFixtures($data);
if (isset($test['failPoint'])) { if (isset($test->failPoint)) {
$this->configureFailPoint($test['failPoint']); $this->configureFailPoint($test->failPoint);
} }
$this->assertOperation($test['operation'], $test['outcome']); Operation::fromRetryableWrites($test->operation, $test->outcome)->assert($this, $context);
if (isset($test['outcome']['collection']['data'])) { if (isset($test->outcome->collection->data)) {
$this->assertOutcomeCollectionData($test['outcome']['collection']['data']); $this->assertOutcomeCollectionData($test->outcome->collection->data);
} }
} }
...@@ -49,13 +61,13 @@ class RetryableWritesSpecTest extends FunctionalTestCase ...@@ -49,13 +61,13 @@ class RetryableWritesSpecTest extends FunctionalTestCase
$testArgs = []; $testArgs = [];
foreach (glob(__DIR__ . '/retryable-writes/*.json') as $filename) { foreach (glob(__DIR__ . '/retryable-writes/*.json') as $filename) {
$json = json_decode(file_get_contents($filename), true); $json = $this->decodeJson(file_get_contents($filename));
$group = basename($filename, '.json'); $group = basename($filename, '.json');
$runOn = isset($json['runOn']) ? $json['runOn'] : null; $runOn = isset($json->runOn) ? $json->runOn : null;
$data = isset($json['data']) ? $json['data'] : []; $data = isset($json->data) ? $json->data : [];
foreach ($json['tests'] as $test) { foreach ($json->tests as $test) {
$name = $group . ': ' . $test['description']; $name = $group . ': ' . $test->description;
$testArgs[] = [$name, $test, $runOn, $data]; $testArgs[] = [$name, $test, $runOn, $data];
} }
} }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"runOn": [
{
"minServerVersion": "4.0",
"topology": [
"replicaset"
]
},
{
"minServerVersion": "4.1.8",
"topology": [
"sharded"
]
}
],
"database_name": "transaction-tests",
"collection_name": "test",
"data": [
{
"_id": 1,
"count": 0
}
],
"tests": [
{
"description": "causal consistency",
"clientOptions": {
"retryWrites": false
},
"operations": [
{
"name": "updateOne",
"object": "collection",
"arguments": {
"session": "session0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"count": 1
}
},
"upsert": false
},
"result": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedCount": 0
}
},
{
"name": "startTransaction",
"object": "session0"
},
{
"name": "updateOne",
"object": "collection",
"arguments": {
"session": "session0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"count": 1
}
},
"upsert": false
},
"result": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedCount": 0
}
},
{
"name": "commitTransaction",
"object": "session0"
}
],
"expectations": [
{
"command_started_event": {
"command": {
"update": "test",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$inc": {
"count": 1
}
},
"multi": false,
"upsert": false
}
],
"ordered": true,
"lsid": "session0",
"readConcern": null,
"txnNumber": null,
"startTransaction": null,
"autocommit": null,
"writeConcern": null
},
"command_name": "update",
"database_name": "transaction-tests"
}
},
{
"command_started_event": {
"command": {
"update": "test",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$inc": {
"count": 1
}
},
"multi": false,
"upsert": false
}
],
"ordered": true,
"readConcern": {
"afterClusterTime": 42
},
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"startTransaction": true,
"autocommit": false,
"writeConcern": null
},
"command_name": "update",
"database_name": "transaction-tests"
}
},
{
"command_started_event": {
"command": {
"commitTransaction": 1,
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"startTransaction": null,
"autocommit": false,
"writeConcern": null
},
"command_name": "commitTransaction",
"database_name": "admin"
}
}
],
"outcome": {
"collection": {
"data": [
{
"_id": 1,
"count": 2
}
]
}
}
},
{
"description": "causal consistency disabled",
"clientOptions": {
"retryWrites": false
},
"sessionOptions": {
"session0": {
"causalConsistency": false
}
},
"operations": [
{
"name": "insertOne",
"object": "collection",
"arguments": {
"session": "session0",
"document": {
"_id": 2
}
},
"result": {
"insertedId": 2
}
},
{
"name": "startTransaction",
"object": "session0"
},
{
"name": "updateOne",
"object": "collection",
"arguments": {
"session": "session0",
"filter": {
"_id": 1
},
"update": {
"$inc": {
"count": 1
}
},
"upsert": false
},
"result": {
"matchedCount": 1,
"modifiedCount": 1,
"upsertedCount": 0
}
},
{
"name": "commitTransaction",
"object": "session0"
}
],
"expectations": [
{
"command_started_event": {
"command": {
"insert": "test",
"documents": [
{
"_id": 2
}
],
"ordered": true,
"readConcern": null,
"lsid": "session0",
"txnNumber": null,
"autocommit": null,
"writeConcern": null
},
"command_name": "insert",
"database_name": "transaction-tests"
}
},
{
"command_started_event": {
"command": {
"update": "test",
"updates": [
{
"q": {
"_id": 1
},
"u": {
"$inc": {
"count": 1
}
},
"multi": false,
"upsert": false
}
],
"ordered": true,
"readConcern": null,
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"startTransaction": true,
"autocommit": false,
"writeConcern": null
},
"command_name": "update",
"database_name": "transaction-tests"
}
},
{
"command_started_event": {
"command": {
"commitTransaction": 1,
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"startTransaction": null,
"autocommit": false,
"writeConcern": null
},
"command_name": "commitTransaction",
"database_name": "admin"
}
}
],
"outcome": {
"collection": {
"data": [
{
"_id": 1,
"count": 1
},
{
"_id": 2
}
]
}
}
}
]
}
This diff is collapsed.
{
"runOn": [
{
"minServerVersion": "4.0.2",
"topology": [
"replicaset"
]
},
{
"minServerVersion": "4.1.8",
"topology": [
"sharded"
]
}
],
"database_name": "transaction-tests",
"collection_name": "test",
"data": [
{
"_id": 1
},
{
"_id": 2
},
{
"_id": 3
},
{
"_id": 4
}
],
"tests": [
{
"description": "count",
"operations": [
{
"name": "startTransaction",
"object": "session0"
},
{
"name": "count",
"object": "collection",
"arguments": {
"session": "session0",
"filter": {
"_id": 1
}
},
"result": {
"errorCodeName": "OperationNotSupportedInTransaction",
"errorLabelsOmit": [
"TransientTransactionError",
"UnknownTransactionCommitResult"
]
}
},
{
"name": "abortTransaction",
"object": "session0"
}
],
"expectations": [
{
"command_started_event": {
"command": {
"count": "test",
"query": {
"_id": 1
},
"readConcern": null,
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"startTransaction": true,
"autocommit": false,
"writeConcern": null
},
"command_name": "count",
"database_name": "transaction-tests"
}
},
{
"command_started_event": {
"command": {
"abortTransaction": 1,
"lsid": "session0",
"txnNumber": {
"$numberLong": "1"
},
"startTransaction": null,
"autocommit": false,
"writeConcern": null
},
"command_name": "abortTransaction",
"database_name": "admin"
}
}
],
"outcome": {
"collection": {
"data": [
{
"_id": 1
},
{
"_id": 2
},
{
"_id": 3
},
{
"_id": 4
}
]
}
}
}
]
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -15,58 +15,14 @@ use Traversable; ...@@ -15,58 +15,14 @@ use Traversable;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
public function expectException($exception) /**
{ * Return the connection URI.
if (method_exists(BaseTestCase::class, 'expectException')) { *
parent::expectException($exception); * @return string
return; */
} public static function getUri()
parent::setExpectedException($exception);
}
public function expectExceptionMessage($exceptionMessage)
{
if (method_exists(BaseTestCase::class, 'expectExceptionMessage')) {
parent::expectExceptionMessage($exceptionMessage);
return;
}
parent::setExpectedException($this->getExpectedException(), $exceptionMessage);
}
public function expectExceptionMessageRegExp($exceptionMessageRegExp)
{
if (method_exists(BaseTestCase::class, 'expectExceptionMessageRegExp')) {
parent::expectExceptionMessageRegExp($exceptionMessageRegExp);
return;
}
parent::setExpectedExceptionRegExp($this->getExpectedException(), $exceptionMessageRegExp);
}
public function provideInvalidArrayValues()
{
return $this->wrapValuesForDataProvider($this->getInvalidArrayValues());
}
public function provideInvalidDocumentValues()
{
return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues());
}
protected function assertDeprecated(callable $execution)
{ {
$errors = []; return getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1:27017';
set_error_handler(function($errno, $errstr) use (&$errors) {
$errors[] = $errstr;
}, E_USER_DEPRECATED);
try {
call_user_func($execution);
} finally {
restore_error_handler();
}
$this->assertCount(1, $errors);
} }
/** /**
...@@ -78,7 +34,7 @@ abstract class TestCase extends BaseTestCase ...@@ -78,7 +34,7 @@ abstract class TestCase extends BaseTestCase
* @param array|object $expectedDocument * @param array|object $expectedDocument
* @param array|object $actualDocument * @param array|object $actualDocument
*/ */
protected function assertMatchesDocument($expectedDocument, $actualDocument) public function assertMatchesDocument($expectedDocument, $actualDocument)
{ {
$normalizedExpectedDocument = $this->normalizeBSON($expectedDocument); $normalizedExpectedDocument = $this->normalizeBSON($expectedDocument);
$normalizedActualDocument = $this->normalizeBSON($actualDocument); $normalizedActualDocument = $this->normalizeBSON($actualDocument);
...@@ -112,7 +68,7 @@ abstract class TestCase extends BaseTestCase ...@@ -112,7 +68,7 @@ abstract class TestCase extends BaseTestCase
* @param array|object $expectedDocument * @param array|object $expectedDocument
* @param array|object $actualDocument * @param array|object $actualDocument
*/ */
protected function assertSameDocument($expectedDocument, $actualDocument) public function assertSameDocument($expectedDocument, $actualDocument)
{ {
$this->assertEquals( $this->assertEquals(
\MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($expectedDocument))), \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($expectedDocument))),
...@@ -120,7 +76,7 @@ abstract class TestCase extends BaseTestCase ...@@ -120,7 +76,7 @@ abstract class TestCase extends BaseTestCase
); );
} }
protected function assertSameDocuments(array $expectedDocuments, $actualDocuments) public function assertSameDocuments(array $expectedDocuments, $actualDocuments)
{ {
if ($actualDocuments instanceof Traversable) { if ($actualDocuments instanceof Traversable) {
$actualDocuments = iterator_to_array($actualDocuments); $actualDocuments = iterator_to_array($actualDocuments);
...@@ -140,6 +96,60 @@ abstract class TestCase extends BaseTestCase ...@@ -140,6 +96,60 @@ abstract class TestCase extends BaseTestCase
); );
} }
public function expectException($exception)
{
if (method_exists(BaseTestCase::class, 'expectException')) {
parent::expectException($exception);
return;
}
parent::setExpectedException($exception);
}
public function expectExceptionMessage($exceptionMessage)
{
if (method_exists(BaseTestCase::class, 'expectExceptionMessage')) {
parent::expectExceptionMessage($exceptionMessage);
return;
}
parent::setExpectedException($this->getExpectedException(), $exceptionMessage);
}
public function expectExceptionMessageRegExp($exceptionMessageRegExp)
{
if (method_exists(BaseTestCase::class, 'expectExceptionMessageRegExp')) {
parent::expectExceptionMessageRegExp($exceptionMessageRegExp);
return;
}
parent::setExpectedExceptionRegExp($this->getExpectedException(), $exceptionMessageRegExp);
}
public function provideInvalidArrayValues()
{
return $this->wrapValuesForDataProvider($this->getInvalidArrayValues());
}
public function provideInvalidDocumentValues()
{
return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues());
}
protected function assertDeprecated(callable $execution)
{
$errors = [];
set_error_handler(function($errno, $errstr) use (&$errors) {
$errors[] = $errstr;
}, E_USER_DEPRECATED);
try {
call_user_func($execution);
} finally {
restore_error_handler();
}
$this->assertCount(1, $errors);
}
/** /**
* Return the test collection name. * Return the test collection name.
* *
...@@ -262,16 +272,6 @@ abstract class TestCase extends BaseTestCase ...@@ -262,16 +272,6 @@ abstract class TestCase extends BaseTestCase
return sprintf('%s.%s', $this->getDatabaseName(), $this->getCollectionName()); return sprintf('%s.%s', $this->getDatabaseName(), $this->getCollectionName());
} }
/**
* Return the connection URI.
*
* @return string
*/
protected function getUri()
{
return getenv('MONGODB_URI') ?: 'mongodb://127.0.0.1:27017';
}
/** /**
* Wrap a list of values for use as a single-argument data provider. * Wrap a list of values for use as a single-argument data provider.
* *
......
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