Skip to content

Commit fd53a1a

Browse files
committed
Merge pull request #615
2 parents a4067ff + 1e5a99b commit fd53a1a

20 files changed

+2305
-88
lines changed

tests/SpecTests/CommandExpectations.php

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace MongoDB\Tests\SpecTests;
44

5-
use MongoDB\BSON\Timestamp;
65
use MongoDB\Driver\Monitoring\CommandFailedEvent;
76
use MongoDB\Driver\Monitoring\CommandStartedEvent;
87
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
@@ -17,20 +16,45 @@
1716
*/
1817
class CommandExpectations implements CommandSubscriber
1918
{
20-
private $commandStartedEvents = [];
21-
private $expectedCommandStartedEvents = [];
19+
private $actualEvents = [];
20+
private $expectedEvents = [];
21+
private $ignoreCommandFailed = false;
22+
private $ignoreCommandStarted = false;
23+
private $ignoreCommandSucceeded = false;
2224

23-
public static function fromTransactions(array $expectedEvents)
25+
private function __construct(array $events)
2426
{
25-
$o = new self;
26-
27-
foreach ($expectedEvents as $expectedEvent) {
28-
if (!isset($expectedEvent->command_started_event)) {
29-
throw new LogicException('$expectedEvent->command_started_event field is not set');
27+
foreach ($events as $event) {
28+
switch (key($event)) {
29+
case 'command_failed_event':
30+
$this->expectedEvents[] = [$event->command_failed_event, CommandFailedEvent::class];
31+
break;
32+
33+
case 'command_started_event':
34+
$this->expectedEvents[] = [$event->command_started_event, CommandStartedEvent::class];
35+
break;
36+
37+
case 'command_succeeded_event':
38+
$this->expectedEvents[] = [$event->command_succeeded_event, CommandSucceededEvent::class];
39+
break;
40+
41+
default:
42+
throw new LogicException('Unsupported event type: ' . key($event));
3043
}
31-
32-
$o->expectedCommandStartedEvents[] = $expectedEvent->command_started_event;
3344
}
45+
}
46+
47+
public static function fromCommandMonitoring(array $expectedEvents)
48+
{
49+
return new self($expectedEvents);
50+
}
51+
52+
public static function fromTransactions(array $expectedEvents)
53+
{
54+
$o = new self($expectedEvents);
55+
56+
$o->ignoreCommandFailed = true;
57+
$o->ignoreCommandSucceeded = true;
3458

3559
return $o;
3660
}
@@ -42,6 +66,11 @@ public static function fromTransactions(array $expectedEvents)
4266
*/
4367
public function commandFailed(CommandFailedEvent $event)
4468
{
69+
if ($this->ignoreCommandFailed) {
70+
return;
71+
}
72+
73+
$this->actualEvents[] = $event;
4574
}
4675

4776
/**
@@ -51,7 +80,11 @@ public function commandFailed(CommandFailedEvent $event)
5180
*/
5281
public function commandStarted(CommandStartedEvent $event)
5382
{
54-
$this->commandStartedEvents[] = $event;
83+
if ($this->ignoreCommandStarted) {
84+
return;
85+
}
86+
87+
$this->actualEvents[] = $event;
5588
}
5689

5790
/**
@@ -61,6 +94,11 @@ public function commandStarted(CommandStartedEvent $event)
6194
*/
6295
public function commandSucceeded(CommandSucceededEvent $event)
6396
{
97+
if ($this->ignoreCommandSucceeded) {
98+
return;
99+
}
100+
101+
$this->actualEvents[] = $event;
64102
}
65103

66104
/**
@@ -87,16 +125,17 @@ public function stopMonitoring()
87125
*/
88126
public function assert(FunctionalTestCase $test, Context $context)
89127
{
90-
$test->assertCount(count($this->expectedCommandStartedEvents), $this->commandStartedEvents);
128+
$test->assertCount(count($this->expectedEvents), $this->actualEvents);
91129

92130
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
93-
$mi->attachIterator(new ArrayIterator($this->expectedCommandStartedEvents));
94-
$mi->attachIterator(new ArrayIterator($this->commandStartedEvents));
131+
$mi->attachIterator(new ArrayIterator($this->expectedEvents));
132+
$mi->attachIterator(new ArrayIterator($this->actualEvents));
95133

96134
foreach ($mi as $events) {
97-
list($expectedEvent, $actualEvent) = $events;
98-
$test->assertInternalType('object', $expectedEvent);
99-
$test->assertInstanceOf(CommandStartedEvent::class, $actualEvent);
135+
list($expectedEventAndClass, $actualEvent) = $events;
136+
list($expectedEvent, $expectedClass) = $expectedEventAndClass;
137+
138+
$test->assertInstanceOf($expectedClass, $actualEvent);
100139

101140
if (isset($expectedEvent->command_name)) {
102141
$test->assertSame($expectedEvent->command_name, $actualEvent->getCommandName());
@@ -107,14 +146,16 @@ public function assert(FunctionalTestCase $test, Context $context)
107146
}
108147

109148
if (isset($expectedEvent->command)) {
149+
$test->assertInstanceOf(CommandStartedEvent::class, $actualEvent);
110150
$expectedCommand = $expectedEvent->command;
111151
$context->replaceCommandSessionPlaceholder($expectedCommand);
112-
$test->assertSameCommand($expectedCommand, $actualEvent->getCommand());
152+
$test->assertCommandMatches($expectedCommand, $actualEvent->getCommand());
113153
}
114-
}
115-
}
116154

117-
private function __construct()
118-
{
155+
if (isset($expectedEvent->reply)) {
156+
$test->assertInstanceOf(CommandSucceededEvent::class, $actualEvent);
157+
$test->assertCommandReplyMatches($expectedEvent->reply, $actualEvent->getReply());
158+
}
159+
}
119160
}
120161
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\SpecTests;
4+
5+
use stdClass;
6+
7+
/**
8+
* Command monitoring spec tests.
9+
*
10+
* @see https://github.com/mongodb/specifications/tree/master/source/command-monitoring
11+
*/
12+
class CommandMonitoringSpecTest extends FunctionalTestCase
13+
{
14+
/**
15+
* Assert that the expected and actual command documents match.
16+
*
17+
* Note: this method may modify the $expected object.
18+
*
19+
* @param stdClass $expected Expected command document
20+
* @param stdClass $actual Actual command document
21+
*/
22+
public static function assertCommandMatches(stdClass $expected, stdClass $actual)
23+
{
24+
if (isset($expected->getMore) && $expected->getMore === 42) {
25+
static::assertObjectHasAttribute('getMore', $actual);
26+
static::assertThat($actual->getMore, static::logicalOr(
27+
static::isInstanceOf(Int64::class),
28+
static::isType('integer')
29+
));
30+
unset($expected->getMore);
31+
}
32+
33+
if (isset($expected->killCursors) && isset($expected->cursors) && is_array($expected->cursors)) {
34+
static::assertObjectHasAttribute('cursors', $actual);
35+
static::assertInternalType('array', $actual->cursors);
36+
37+
foreach ($expected->cursors as $i => $cursorId) {
38+
static::assertArrayHasKey($i, $actual->cursors);
39+
40+
if ($cursorId === 42) {
41+
static::assertThat($actual->cursors[$i], static::logicalOr(
42+
static::isInstanceOf(Int64::class),
43+
static::isType('integer')
44+
));
45+
}
46+
}
47+
48+
unset($expected->cursors);
49+
}
50+
51+
static::assertDocumentsMatch($expected, $actual);
52+
}
53+
54+
/**
55+
* Assert that the expected and actual command reply documents match.
56+
*
57+
* Note: this method may modify the $expectedReply object.
58+
*
59+
* @param stdClass $expected Expected command reply document
60+
* @param stdClass $actual Actual command reply document
61+
*/
62+
public static function assertCommandReplyMatches(stdClass $expected, stdClass $actual)
63+
{
64+
if (isset($expected->cursor->id) && $expected->cursor->id === 42) {
65+
static::assertObjectHasAttribute('cursor', $actual);
66+
static::assertInternalType('object', $actual->cursor);
67+
static::assertObjectHasAttribute('id', $actual->cursor);
68+
static::assertThat($actual->cursor->id, static::logicalOr(
69+
static::isInstanceOf(Int64::class),
70+
static::isType('integer')
71+
));
72+
unset($expected->cursor->id);
73+
}
74+
75+
if (isset($expected->cursorsUnknown) && is_array($expected->cursorsUnknown)) {
76+
static::assertObjectHasAttribute('cursorsUnknown', $actual);
77+
static::assertInternalType('array', $actual->cursorsUnknown);
78+
79+
foreach ($expected->cursorsUnknown as $i => $cursorId) {
80+
static::assertArrayHasKey($i, $actual->cursorsUnknown);
81+
82+
if ($cursorId === 42) {
83+
static::assertThat($actual->cursorsUnknown[$i], static::logicalOr(
84+
static::isInstanceOf(Int64::class),
85+
static::isType('integer')
86+
));
87+
}
88+
}
89+
90+
unset($expected->cursorsUnknown);
91+
}
92+
93+
if (isset($expected->ok) && is_numeric($expected->ok)) {
94+
static::assertObjectHasAttribute('ok', $actual);
95+
static::assertInternalType('numeric', $actual->ok);
96+
static::assertEquals($expected->ok, $actual->ok);
97+
unset($expected->ok);
98+
}
99+
100+
if (isset($expected->writeErrors) && is_array($expected->writeErrors)) {
101+
static::assertObjectHasAttribute('writeErrors', $actual);
102+
static::assertInternalType('array', $actual->writeErrors);
103+
104+
foreach ($expected->writeErrors as $i => $expectedWriteError) {
105+
static::assertArrayHasKey($i, $actual->writeErrors);
106+
$actualWriteError = $actual->writeErrors[$i];
107+
108+
if (isset($expectedWriteError->code) && $expectedWriteError->code === 42) {
109+
static::assertObjectHasAttribute('code', $actualWriteError);
110+
static::assertThat($actualWriteError->code, static::logicalOr(
111+
static::isInstanceOf(Int64::class),
112+
static::isType('integer')
113+
));
114+
unset($expected->writeErrors[$i]->code);
115+
}
116+
117+
if (isset($expectedWriteError->errmsg) && $expectedWriteError->errmsg === '') {
118+
static::assertObjectHasAttribute('errmsg', $actualWriteError);
119+
static::assertInternalType('string', $actualWriteError->errmsg);
120+
static::assertNotEmpty($actualWriteError->errmsg);
121+
unset($expected->writeErrors[$i]->errmsg);
122+
}
123+
}
124+
}
125+
126+
static::assertDocumentsMatch($expected, $actual);
127+
}
128+
129+
/**
130+
* Execute an individual test case from the specification.
131+
*
132+
* @dataProvider provideTests
133+
* @param string $name Test name
134+
* @param stdClass $test Individual "tests[]" document
135+
* @param array $data Top-level "data" array to initialize collection
136+
* @param string $databaseName Name of database under test
137+
* @param string $collectionName Name of collection under test
138+
*/
139+
public function testCommandMonitoring($name, stdClass $test, array $data, $databaseName = null, $collectionName = null)
140+
{
141+
$this->setName($name);
142+
143+
$this->checkServerRequirements($this->createRunOn($test));
144+
145+
$databaseName = isset($databaseName) ? $databaseName : $this->getDatabaseName();
146+
$collectionName = isset($collectionName) ? $collectionName : $this->getCollectionName();
147+
148+
$context = Context::fromCommandMonitoring($test, $databaseName, $collectionName);
149+
$this->setContext($context);
150+
151+
$this->dropTestAndOutcomeCollections();
152+
$this->insertDataFixtures($data);
153+
154+
if (isset($test->expectations)) {
155+
$commandExpectations = CommandExpectations::fromCommandMonitoring($test->expectations);
156+
$commandExpectations->startMonitoring();
157+
}
158+
159+
Operation::fromCommandMonitoring($test->operation)->assert($this, $context);
160+
161+
if (isset($commandExpectations)) {
162+
$commandExpectations->stopMonitoring();
163+
$commandExpectations->assert($this, $context);
164+
}
165+
}
166+
167+
public function provideTests()
168+
{
169+
$testArgs = [];
170+
171+
foreach (glob(__DIR__ . '/command-monitoring/*.json') as $filename) {
172+
$json = $this->decodeJson(file_get_contents($filename));
173+
$group = basename($filename, '.json');
174+
$data = isset($json->data) ? $json->data : [];
175+
$databaseName = isset($json->database_name) ? $json->database_name : null;
176+
$collectionName = isset($json->collection_name) ? $json->collection_name : null;
177+
178+
foreach ($json->tests as $test) {
179+
$name = $group . ': ' . $test->description;
180+
$testArgs[] = [$name, $test, $data, $databaseName, $collectionName];
181+
}
182+
}
183+
184+
return $testArgs;
185+
}
186+
187+
/**
188+
* Convert the server and topology requirements to a standard "runOn" array
189+
* used by other specifications.
190+
*
191+
* @param stdClass $test
192+
* @return array
193+
*/
194+
private function createRunOn(stdClass $test)
195+
{
196+
$req = new stdClass;
197+
198+
$topologies = [
199+
self::TOPOLOGY_SINGLE,
200+
self::TOPOLOGY_REPLICASET,
201+
self::TOPOLOGY_SHARDED
202+
];
203+
204+
/* Append ".99" as patch version, since command monitoring tests expect
205+
* the minor version to be an inclusive upper bound. */
206+
if (isset($test->ignore_if_server_version_greater_than)) {
207+
$req->maxServerVersion = $test->ignore_if_server_version_greater_than . '.99';
208+
}
209+
210+
if (isset($test->ignore_if_server_version_less_than)) {
211+
$req->minServerVersion = $test->ignore_if_server_version_less_than;
212+
}
213+
214+
if (isset($test->ignore_if_topology_type)) {
215+
$req->topology = array_diff($topologies, $test->ignore_if_topology_type);
216+
}
217+
218+
return [$req];
219+
}
220+
}

tests/SpecTests/Context.php

Lines changed: 9 additions & 0 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 fromCommandMonitoring(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 fromRetryableWrites(stdClass $test, $databaseName, $collectionName)
3948
{
4049
$o = new self($databaseName, $collectionName);

0 commit comments

Comments
 (0)