Skip to content

Commit 7313f12

Browse files
committed
Merge pull request #616
2 parents 66ef30d + 1dc5478 commit 7313f12

16 files changed

+2201
-40
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\SpecTests;
4+
5+
use MongoDB\ChangeStream;
6+
use MongoDB\Client;
7+
use MongoDB\Driver\Exception\Exception;
8+
use MongoDB\Model\BSONDocument;
9+
use ArrayIterator;
10+
use LogicException;
11+
use MultipleIterator;
12+
use stdClass;
13+
14+
/**
15+
* Change Streams spec tests.
16+
*
17+
* @see https://github.com/mongodb/specifications/tree/master/source/change-streams
18+
*/
19+
class ChangeStreamsSpecTest extends FunctionalTestCase
20+
{
21+
/* These should all pass before the driver can be considered compatible with
22+
* MongoDB 4.2. */
23+
private static $incompleteTests = [
24+
'change-streams: Test consecutive resume' => 'PHPLIB-442, PHPLIB-416',
25+
];
26+
27+
/**
28+
* Assert that the expected and actual command documents match.
29+
*
30+
* Note: this method may modify the $expected object.
31+
*
32+
* @param stdClass $expected Expected command document
33+
* @param stdClass $actual Actual command document
34+
*/
35+
public static function assertCommandMatches(stdClass $expected, stdClass $actual)
36+
{
37+
static::assertDocumentsMatch($expected, $actual);
38+
}
39+
40+
/**
41+
* Assert that the expected and actual documents match.
42+
*
43+
* @param array $expectedDocuments Expected documents
44+
* @param array $actualDocuments Actual documents
45+
*/
46+
public static function assertResult(array $expectedDocuments, array $actualDocuments)
47+
{
48+
static::assertCount(count($expectedDocuments), $actualDocuments);
49+
50+
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
51+
$mi->attachIterator(new ArrayIterator($expectedDocuments));
52+
$mi->attachIterator(new ArrayIterator($actualDocuments));
53+
54+
foreach ($mi as $documents) {
55+
list($expectedDocument, $actualDocument) = $documents;
56+
57+
$constraint = new DocumentsMatchConstraint($expectedDocument, true, true, ['42']);
58+
59+
static::assertThat($actualDocument, $constraint);
60+
}
61+
}
62+
63+
/**
64+
* Execute an individual test case from the specification.
65+
*
66+
* @dataProvider provideTests
67+
* @param string $name Test name
68+
* @param stdClass $test Individual "tests[]" document
69+
* @param string $databaseName Name of database under test
70+
* @param string $collectionName Name of collection under test
71+
* @param string $database2Name Name of alternate database under test
72+
* @param string $collection2Name Name of alternate collection under test
73+
*/
74+
public function testChangeStreams($name, stdClass $test, $databaseName = null, $collectionName = null, $database2Name = null, $collection2Name = null)
75+
{
76+
$this->setName($name);
77+
78+
if (isset(self::$incompleteTests[$name])) {
79+
$this->markTestIncomplete(self::$incompleteTests[$name]);
80+
}
81+
82+
$this->checkServerRequirements($this->createRunOn($test));
83+
84+
if (!isset($databaseName, $collectionName, $database2Name, $collection2Name)) {
85+
$this->fail('Required database and collection names are unset');
86+
}
87+
88+
$context = Context::fromChangeStreams($test, $databaseName, $collectionName);
89+
$this->setContext($context);
90+
91+
$this->dropDatabasesAndCreateCollection($databaseName, $collectionName);
92+
$this->dropDatabasesAndCreateCollection($database2Name, $collection2Name);
93+
94+
if (isset($test->failPoint)) {
95+
$this->configureFailPoint($test->failPoint);
96+
}
97+
98+
if (isset($test->expectations)) {
99+
$commandExpectations = CommandExpectations::fromChangeStreams($test->expectations);
100+
$commandExpectations->startMonitoring();
101+
}
102+
103+
$errorExpectation = ErrorExpectation::fromChangeStreams($test->result);
104+
$resultExpectation = ResultExpectation::fromChangeStreams($test->result, [$this, 'assertResult']);
105+
106+
$result = null;
107+
$exception = null;
108+
109+
try {
110+
$changeStream = $this->createChangeStream($test);
111+
} catch (Exception $e) {
112+
$exception = $e;
113+
}
114+
115+
if (isset($commandExpectations)) {
116+
$commandExpectations->stopMonitoring();
117+
}
118+
119+
foreach ($test->operations as $operation) {
120+
Operation::fromChangeStreams($operation)->assert($this, $context);
121+
}
122+
123+
if (isset($commandExpectations)) {
124+
$commandExpectations->startMonitoring();
125+
}
126+
127+
/* If the change stream was successfully created (i.e. $exception is
128+
* null), attempt to iterate up to the expected number of results. It's
129+
* possible that some errors (e.g. projecting out _id) will only be
130+
* thrown during iteration, so we must also try/catch here. */
131+
try {
132+
if (isset($changeStream)) {
133+
$limit = isset($test->result->success) ? count($test->result->success) : 0;
134+
$result = $this->iterateChangeStream($changeStream, $limit);
135+
}
136+
} catch (Exception $e) {
137+
$this->assertNull($exception);
138+
$exception = $e;
139+
}
140+
141+
$errorExpectation->assert($this, $exception);
142+
$resultExpectation->assert($this, $result);
143+
144+
if (isset($commandExpectations)) {
145+
$commandExpectations->stopMonitoring();
146+
$commandExpectations->assert($this, $context);
147+
}
148+
}
149+
150+
public function provideTests()
151+
{
152+
$testArgs = [];
153+
154+
foreach (glob(__DIR__ . '/change-streams/*.json') as $filename) {
155+
$json = $this->decodeJson(file_get_contents($filename));
156+
$group = basename($filename, '.json');
157+
$databaseName = isset($json->database_name) ? $json->database_name : null;
158+
$database2Name = isset($json->database2_name) ? $json->database2_name : null;
159+
$collectionName = isset($json->collection_name) ? $json->collection_name : null;
160+
$collection2Name = isset($json->collection2_name) ? $json->collection2_name : null;
161+
162+
foreach ($json->tests as $test) {
163+
$name = $group . ': ' . $test->description;
164+
$testArgs[] = [$name, $test, $databaseName, $collectionName, $database2Name, $collection2Name];
165+
}
166+
}
167+
168+
return $testArgs;
169+
}
170+
171+
/**
172+
* Create a change stream.
173+
*
174+
* @param stdClass $test
175+
* @return ChangeStream
176+
* @throws LogicException if the target is unsupported
177+
*/
178+
private function createChangeStream(stdClass $test)
179+
{
180+
$context = $this->getContext();
181+
$pipeline = isset($test->changeStreamPipeline) ? $test->changeStreamPipeline : [];
182+
$options = isset($test->changeStreamOptions) ? (array) $test->changeStreamOptions : [];
183+
184+
switch ($test->target) {
185+
case 'client':
186+
return $context->client->watch($pipeline, $options);
187+
188+
case 'database':
189+
return $context->getDatabase()->watch($pipeline, $options);
190+
191+
case 'collection':
192+
return $context->getCollection()->watch($pipeline, $options);
193+
194+
default:
195+
throw new LogicException('Unsupported target: ' . $test->target);
196+
}
197+
}
198+
199+
/**
200+
* Convert the server requirements to a standard "runOn" array used by other
201+
* specifications.
202+
*
203+
* @param stdClass $test
204+
* @return array
205+
*/
206+
private function createRunOn(stdClass $test)
207+
{
208+
$req = new stdClass;
209+
210+
/* Append ".99" as patch version, since command monitoring tests expect
211+
* the minor version to be an inclusive upper bound. */
212+
if (isset($test->maxServerVersion)) {
213+
$req->maxServerVersion = $test->maxServerVersion;
214+
}
215+
216+
if (isset($test->minServerVersion)) {
217+
$req->minServerVersion = $test->minServerVersion;
218+
}
219+
220+
if (isset($test->topology)) {
221+
$req->topology = $test->topology;
222+
}
223+
224+
return [$req];
225+
}
226+
227+
/**
228+
* Drop the database and create the collection.
229+
*
230+
* @param string $databaseName
231+
* @param string $collectionName
232+
*/
233+
private function dropDatabasesAndCreateCollection($databaseName, $collectionName)
234+
{
235+
$context = $this->getContext();
236+
237+
$database = $context->client->selectDatabase($databaseName);
238+
$database->drop($context->defaultWriteOptions);
239+
$database->createCollection($collectionName, $context->defaultWriteOptions);
240+
}
241+
242+
/**
243+
* Iterate a change stream.
244+
*
245+
* @param ChangeStream $changeStream
246+
* @return BSONDocument[]
247+
*/
248+
private function iterateChangeStream(ChangeStream $changeStream, $limit = 0)
249+
{
250+
$events = [];
251+
252+
for ($changeStream->rewind(); count($events) < $limit; $changeStream->next()) {
253+
if ( ! $changeStream->valid()) {
254+
continue;
255+
}
256+
257+
$event = $changeStream->current();
258+
259+
$this->assertInstanceOf(BSONDocument::class, $event);
260+
261+
$events[] = $event;
262+
}
263+
264+
return $events;
265+
}
266+
}

tests/SpecTests/CommandExpectations.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class CommandExpectations implements CommandSubscriber
2121
private $ignoreCommandFailed = false;
2222
private $ignoreCommandStarted = false;
2323
private $ignoreCommandSucceeded = false;
24+
private $ignoreExtraEvents = false;
2425

2526
private function __construct(array $events)
2627
{
@@ -44,6 +45,20 @@ private function __construct(array $events)
4445
}
4546
}
4647

48+
public static function fromChangeStreams(array $expectedEvents)
49+
{
50+
$o = new self($expectedEvents);
51+
52+
$o->ignoreCommandFailed = true;
53+
$o->ignoreCommandSucceeded = true;
54+
/* Change Streams spec tests do not include getMore commands in the
55+
* list of expected events, so ignore any observed events beyond the
56+
* number that are expected. */
57+
$o->ignoreExtraEvents = true;;
58+
59+
return $o;
60+
}
61+
4762
public static function fromCommandMonitoring(array $expectedEvents)
4863
{
4964
return new self($expectedEvents);
@@ -125,11 +140,15 @@ public function stopMonitoring()
125140
*/
126141
public function assert(FunctionalTestCase $test, Context $context)
127142
{
128-
$test->assertCount(count($this->expectedEvents), $this->actualEvents);
143+
$actualEvents = $this->ignoreExtraEvents
144+
? array_slice($this->actualEvents, 0, count($this->expectedEvents))
145+
: $this->actualEvents;
146+
147+
$test->assertCount(count($this->expectedEvents), $actualEvents);
129148

130149
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
131150
$mi->attachIterator(new ArrayIterator($this->expectedEvents));
132-
$mi->attachIterator(new ArrayIterator($this->actualEvents));
151+
$mi->attachIterator(new ArrayIterator($actualEvents));
133152

134153
foreach ($mi as $events) {
135154
list($expectedEventAndClass, $actualEvent) = $events;

tests/SpecTests/Context.php

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ private function __construct($databaseName, $collectionName)
3535
$this->outcomeCollectionName = $collectionName;
3636
}
3737

38+
public static function fromChangeStreams(stdClass $test, $databaseName, $collectionName)
39+
{
40+
$o = new self($databaseName, $collectionName);
41+
42+
$o->client = new Client(FunctionalTestCase::getUri());
43+
44+
return $o;
45+
}
46+
3847
public static function fromCommandMonitoring(stdClass $test, $databaseName, $collectionName)
3948
{
4049
$o = new self($databaseName, $collectionName);
@@ -98,7 +107,7 @@ public static function fromTransactions(stdClass $test, $databaseName, $collecti
98107

99108
public function getCollection(array $collectionOptions = [])
100109
{
101-
return $this->client->selectCollection(
110+
return $this->selectCollection(
102111
$this->databaseName,
103112
$this->collectionName,
104113
$this->prepareOptions($collectionOptions)
@@ -107,10 +116,7 @@ public function getCollection(array $collectionOptions = [])
107116

108117
public function getDatabase(array $databaseOptions = [])
109118
{
110-
return $this->client->selectDatabase(
111-
$this->databaseName,
112-
$this->prepareOptions($databaseOptions)
113-
);
119+
return $this->selectDatabase($this->databaseName, $databaseOptions);
114120
}
115121

116122
/**
@@ -221,6 +227,23 @@ public function replaceCommandSessionPlaceholder(stdClass $command)
221227
}
222228
}
223229

230+
public function selectCollection($databaseName, $collectionName, array $collectionOptions = [])
231+
{
232+
return $this->client->selectCollection(
233+
$databaseName,
234+
$collectionName,
235+
$this->prepareOptions($collectionOptions)
236+
);
237+
}
238+
239+
public function selectDatabase($databaseName, array $databaseOptions = [])
240+
{
241+
return $this->client->selectDatabase(
242+
$databaseName,
243+
$this->prepareOptions($databaseOptions)
244+
);
245+
}
246+
224247
private function prepareSessionOptions(array $options)
225248
{
226249
if (isset($options['defaultTransactionOptions'])) {

0 commit comments

Comments
 (0)