Skip to content

Commit d45f806

Browse files
authored
PHPLIB-627: Comprehensive Atlas Tests (#806)
* PHPLIB-613: Extract UnifiedTestRunner and model class from UnifiedSpecTest This will make it easier to invoke the UnifiedTestRunner from other test files or contexts. * Fix typos in exception messages and reference link * Rethrow internal PHP errors in Operation::assert() Like PHPUnit's AssertionFailedError, internal PHP errors are not expected for "expectError" assertions. This ensure that errors within the test runner implementation (e.g. calling a nonexistent method) will not be suppressed by the test runner itself. * PHPLIB-613: Loop operation * PHPLIB-613: EventCollector for storeEventsAsEntities client entity option * PHPLIB-613: Bump UnifiedTestRunner supported schema to 1.2 This change was necessary to support new syntax for Atlas testing; however, versioned API syntax from schema 1.1 is still unsupported. * Update valid-fail and valid-pass tests Synced with mongodb/specifications@966bf96 * PHPLIB-613: Allow a callable to receive the EntityMap after each test * PHPLIB-640: Workarounds for killAllSessions Disable the command for Atlas and update the list of ignored error codes.
1 parent a54358b commit d45f806

13 files changed

+1240
-395
lines changed

tests/UnifiedSpecTests/Context.php

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use MongoDB\Driver\ReadPreference;
88
use MongoDB\Driver\Server;
99
use MongoDB\Driver\ServerApi;
10+
use MongoDB\Model\BSONArray;
11+
use MongoDB\Tests\FunctionalTestCase;
1012
use stdClass;
1113
use function array_key_exists;
1214
use function array_map;
@@ -26,6 +28,7 @@
2628
use function PHPUnit\Framework\assertIsString;
2729
use function PHPUnit\Framework\assertNotEmpty;
2830
use function PHPUnit\Framework\assertNotFalse;
31+
use function PHPUnit\Framework\assertNotSame;
2932
use function PHPUnit\Framework\assertStringContainsString;
3033
use function PHPUnit\Framework\assertStringStartsWith;
3134
use function strlen;
@@ -50,12 +53,18 @@ final class Context
5053
/** @var EntityMap */
5154
private $entityMap;
5255

56+
/** @var EventCollector[] */
57+
private $eventCollectors = [];
58+
5359
/** @var EventObserver[] */
5460
private $eventObserversByClient = [];
5561

5662
/** @var Client */
5763
private $internalClient;
5864

65+
/** @var boolean */
66+
private $inLoop = false;
67+
5968
/** @var string */
6069
private $uri;
6170

@@ -146,6 +155,16 @@ public function setActiveClient(string $clientId = null)
146155
$this->activeClient = $clientId;
147156
}
148157

158+
public function isInLoop() : bool
159+
{
160+
return $this->inLoop;
161+
}
162+
163+
public function setInLoop(bool $inLoop)
164+
{
165+
$this->inLoop = $inLoop;
166+
}
167+
149168
public function assertExpectedEventsForClients(array $expectedEventsForClients)
150169
{
151170
assertNotEmpty($expectedEventsForClients);
@@ -186,6 +205,20 @@ public function getEventObserverForClient(string $id) : EventObserver
186205
return $this->eventObserversByClient[$id];
187206
}
188207

208+
public function startEventCollectors()
209+
{
210+
foreach ($this->eventCollectors as $eventCollector) {
211+
$eventCollector->start();
212+
}
213+
}
214+
215+
public function stopEventCollectors()
216+
{
217+
foreach ($this->eventCollectors as $eventCollector) {
218+
$eventCollector->stop();
219+
}
220+
}
221+
189222
/** @param string|array $readPreferenceTags */
190223
private function convertReadPreferenceTags($readPreferenceTags) : array
191224
{
@@ -208,12 +241,13 @@ static function (string $tag) : array {
208241

209242
private function createClient(string $id, stdClass $o)
210243
{
211-
Util::assertHasOnlyKeys($o, ['id', 'uriOptions', 'useMultipleMongoses', 'observeEvents', 'ignoreCommandMonitoringEvents', 'serverApi']);
244+
Util::assertHasOnlyKeys($o, ['id', 'uriOptions', 'useMultipleMongoses', 'observeEvents', 'ignoreCommandMonitoringEvents', 'serverApi', 'storeEventsAsEntities']);
212245

213246
$useMultipleMongoses = $o->useMultipleMongoses ?? null;
214247
$observeEvents = $o->observeEvents ?? null;
215248
$ignoreCommandMonitoringEvents = $o->ignoreCommandMonitoringEvents ?? [];
216249
$serverApi = $o->serverApi ?? null;
250+
$storeEventsAsEntities = $o->storeEventsAsEntities ?? null;
217251

218252
$uri = $this->uri;
219253

@@ -252,6 +286,14 @@ private function createClient(string $id, stdClass $o)
252286
$this->eventObserversByClient[$id] = new EventObserver($observeEvents, $ignoreCommandMonitoringEvents, $id, $this);
253287
}
254288

289+
if (isset($storeEventsAsEntities)) {
290+
assertIsArray($storeEventsAsEntities);
291+
292+
foreach ($storeEventsAsEntities as $storeEventsAsEntity) {
293+
$this->createEntityCollector($id, $storeEventsAsEntity);
294+
}
295+
}
296+
255297
/* TODO: Remove this once PHPC-1645 is implemented. Each client needs
256298
* its own libmongoc client to facilitate txnNumber assertions. */
257299
static $i = 0;
@@ -266,7 +308,22 @@ private function createClient(string $id, stdClass $o)
266308
);
267309
}
268310

269-
$this->entityMap->set($id, UnifiedSpecTest::createTestClient($uri, $uriOptions, $driverOptions));
311+
$this->entityMap->set($id, FunctionalTestCase::createTestClient($uri, $uriOptions, $driverOptions));
312+
}
313+
314+
private function createEntityCollector(string $clientId, stdClass $o)
315+
{
316+
Util::assertHasOnlyKeys($o, ['id', 'events']);
317+
318+
$eventListId = $o->id ?? null;
319+
$events = $o->events ?? null;
320+
321+
assertNotSame($eventListId, $clientId);
322+
assertIsArray($events);
323+
324+
$eventList = new BSONArray();
325+
$this->entityMap->set($eventListId, $eventList);
326+
$this->eventCollectors[] = new EventCollector($eventList, $events, $clientId, $this);
270327
}
271328

272329
private function createCollection(string $id, stdClass $o)
@@ -410,7 +467,7 @@ private static function removeMultipleMongoses(string $uri) : string
410467
{
411468
assertStringStartsWith('mongodb://', $uri);
412469

413-
$manager = UnifiedSpecTest::createTestManager($uri);
470+
$manager = FunctionalTestCase::createTestManager($uri);
414471

415472
// Nothing to do if the URI does not refer to a sharded cluster
416473
if ($manager->selectServer(new ReadPreference(ReadPreference::PRIMARY))->getType() !== Server::TYPE_MONGOS) {
@@ -450,7 +507,7 @@ private static function requireMultipleMongoses(string $uri)
450507
{
451508
assertStringStartsWith('mongodb://', $uri);
452509

453-
$manager = UnifiedSpecTest::createTestManager($uri);
510+
$manager = FunctionalTestCase::createTestManager($uri);
454511

455512
// Nothing to do if the URI does not refer to a sharded cluster
456513
if ($manager->selectServer(new ReadPreference(ReadPreference::PRIMARY))->getType() !== Server::TYPE_MONGOS) {

tests/UnifiedSpecTests/EntityMap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function offsetGet($id)
7878
*/
7979
public function offsetSet($id, $value)
8080
{
81-
Assert::fail('Entities can only be set via register()');
81+
Assert::fail('Entities can only be set via set()');
8282
}
8383

8484
/**
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\UnifiedSpecTests;
4+
5+
use MongoDB\Driver\Monitoring\CommandFailedEvent;
6+
use MongoDB\Driver\Monitoring\CommandStartedEvent;
7+
use MongoDB\Driver\Monitoring\CommandSubscriber;
8+
use MongoDB\Driver\Monitoring\CommandSucceededEvent;
9+
use MongoDB\Model\BSONArray;
10+
use function array_filter;
11+
use function array_flip;
12+
use function get_class;
13+
use function microtime;
14+
use function MongoDB\Driver\Monitoring\addSubscriber;
15+
use function MongoDB\Driver\Monitoring\removeSubscriber;
16+
use function PHPUnit\Framework\assertArrayHasKey;
17+
use function PHPUnit\Framework\assertIsObject;
18+
use function PHPUnit\Framework\assertIsString;
19+
use function PHPUnit\Framework\assertNotEmpty;
20+
use function sprintf;
21+
22+
/**
23+
* EventCollector handles "storeEventsAsEntities" for client entities.
24+
*
25+
* Unlike EventObserver, this does not support ignoring command monitoring
26+
* events for specific commands. That said, internal/security commands that
27+
* bypass command monitoring will still be ignored.
28+
*/
29+
final class EventCollector implements CommandSubscriber
30+
{
31+
/** @var array */
32+
private static $supportedEvents = [
33+
'PoolCreatedEvent' => null,
34+
'PoolReadyEvent' => null,
35+
'PoolClearedEvent' => null,
36+
'PoolClosedEvent' => null,
37+
'ConnectionCreatedEvent' => null,
38+
'ConnectionReadyEvent' => null,
39+
'ConnectionClosedEvent' => null,
40+
'ConnectionCheckOutStartedEvent' => null,
41+
'ConnectionCheckOutFailedEvent' => null,
42+
'ConnectionCheckedOutEvent' => null,
43+
'ConnectionCheckedInEvent' => null,
44+
'CommandStartedEvent' => CommandStartedEvent::class,
45+
'CommandSucceededEvent' => CommandSucceededEvent::class,
46+
'CommandFailedEvent' => CommandFailedEvent::class,
47+
];
48+
49+
/** @var string */
50+
private $clientId;
51+
52+
/** @var Context */
53+
private $context;
54+
55+
/** @var array */
56+
private $collectEvents = [];
57+
58+
/** @var BSONArray */
59+
private $eventList;
60+
61+
public function __construct(BSONArray $eventList, array $collectEvents, string $clientId, Context $context)
62+
{
63+
assertNotEmpty($collectEvents);
64+
65+
foreach ($collectEvents as $event) {
66+
assertIsString($event);
67+
assertArrayHasKey($event, self::$supportedEvents);
68+
69+
/* CMAP events are "supported" only in the sense that we recognize
70+
* them in the test format; however, PHPC does not implement
71+
* connection pooling so these events cannot be collected. */
72+
if (self::$supportedEvents[$event] !== null) {
73+
$this->collectEvents[self::$supportedEvents[$event]] = 1;
74+
}
75+
}
76+
77+
$this->clientId = $clientId;
78+
$this->context = $context;
79+
$this->eventList = $eventList;
80+
}
81+
82+
/**
83+
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandfailed.php
84+
*/
85+
public function commandFailed(CommandFailedEvent $event)
86+
{
87+
$this->handleCommandMonitoringEvent($event);
88+
}
89+
90+
/**
91+
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandstarted.php
92+
*/
93+
public function commandStarted(CommandStartedEvent $event)
94+
{
95+
$this->handleCommandMonitoringEvent($event);
96+
}
97+
98+
/**
99+
* @see https://www.php.net/manual/en/mongodb-driver-monitoring-commandsubscriber.commandsucceeded.php
100+
*/
101+
public function commandSucceeded(CommandSucceededEvent $event)
102+
{
103+
$this->handleCommandMonitoringEvent($event);
104+
}
105+
106+
public function start()
107+
{
108+
addSubscriber($this);
109+
}
110+
111+
public function stop()
112+
{
113+
removeSubscriber($this);
114+
}
115+
116+
/** @param CommandStartedEvent|CommandSucceededEvent|CommandFailedEvent $event */
117+
private function handleCommandMonitoringEvent($event)
118+
{
119+
assertIsObject($event);
120+
121+
if (! $this->context->isActiveClient($this->clientId)) {
122+
return;
123+
}
124+
125+
if (! isset($this->collectEvents[get_class($event)])) {
126+
return;
127+
}
128+
129+
$log = [
130+
'name' => self::getEventName($event),
131+
'observedAt' => microtime(true),
132+
'commandName' => $event->getCommandName(),
133+
'connectionId' => self::getConnectionId($event),
134+
'requestId' => $event->getRequestId(),
135+
'operationId' => $event->getOperationId(),
136+
];
137+
138+
/* Note: CommandStartedEvent.command and CommandSucceededEvent.reply can
139+
* be omitted from logged events. */
140+
141+
if ($event instanceof CommandStartedEvent) {
142+
$log['databaseName'] = $event->getDatabaseName();
143+
}
144+
145+
if ($event instanceof CommandSucceededEvent) {
146+
$log['duration'] = $event->getDurationMicros();
147+
}
148+
149+
if ($event instanceof CommandFailedEvent) {
150+
$log['failure'] = $event->getError()->getMessage();
151+
$log['duration'] = $event->getDurationMicros();
152+
}
153+
154+
$this->eventList[] = $log;
155+
}
156+
157+
/** @param CommandStartedEvent|CommandSucceededEvent|CommandFailedEvent $event */
158+
private static function getConnectionId($event) : string
159+
{
160+
$server = $event->getServer();
161+
162+
return sprintf('%s:%d', $server->getHost(), $server->getPort());
163+
}
164+
165+
/** @param object $event */
166+
private static function getEventName($event) : string
167+
{
168+
static $eventNamesByClass = null;
169+
170+
if ($eventNamesByClass === null) {
171+
$eventNamesByClass = array_flip(array_filter(self::$supportedEvents));
172+
}
173+
174+
return $eventNamesByClass[get_class($event)];
175+
}
176+
}

tests/UnifiedSpecTests/EventObserver.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
use function PHPUnit\Framework\assertThat;
3232
use function sprintf;
3333

34-
class EventObserver implements CommandSubscriber
34+
/**
35+
* EventObserver handles "observeEvents" for client entities and assertions for
36+
* "expectEvents" and special operations (e.g. assertSameLsidOnLastTwoCommands).
37+
*/
38+
final class EventObserver implements CommandSubscriber
3539
{
3640
/** @var array */
3741
private static $defaultIgnoreCommands = [

0 commit comments

Comments
 (0)