Skip to content

Commit 6d29779

Browse files
committed
PHPLIB-569: Atlas Data Lake spec tests
Tests synced with mongodb/specifications@bdc283c
1 parent faf167f commit 6d29779

File tree

8 files changed

+530
-0
lines changed

8 files changed

+530
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
<?php
2+
3+
namespace MongoDB\Tests\SpecTests;
4+
5+
use MongoDB\Client;
6+
use MongoDB\Driver\Command;
7+
use MongoDB\Driver\Cursor;
8+
use MongoDB\Tests\CommandObserver;
9+
use stdClass;
10+
use Symfony\Bridge\PhpUnit\SetUpTearDownTrait;
11+
use function basename;
12+
use function current;
13+
use function explode;
14+
use function file_get_contents;
15+
use function glob;
16+
use function parse_url;
17+
18+
/**
19+
* Atlas Data Lake spec tests.
20+
*
21+
* @see https://github.com/mongodb/specifications/tree/master/source/atlas-data-lake-testing/tests
22+
*/
23+
class AtlasDataLakeSpecTest extends FunctionalTestCase
24+
{
25+
use SetUpTearDownTrait;
26+
27+
private function doSetUp()
28+
{
29+
parent::setUp();
30+
31+
if (! $this->isAtlasDataLake()) {
32+
$this->markTestSkipped('Server is not Atlas Data Lake');
33+
}
34+
}
35+
36+
/**
37+
* Assert that the expected and actual command documents match.
38+
*
39+
* @param stdClass $expected Expected command document
40+
* @param stdClass $actual Actual command document
41+
*/
42+
public static function assertCommandMatches(stdClass $expected, stdClass $actual)
43+
{
44+
foreach ($expected as $key => $value) {
45+
if ($value === null) {
46+
static::assertObjectNotHasAttribute($key, $actual);
47+
unset($expected->{$key});
48+
}
49+
}
50+
51+
static::assertDocumentsMatch($expected, $actual);
52+
}
53+
54+
/**
55+
* Execute an individual test case from the specification.
56+
*
57+
* @dataProvider provideTests
58+
* @param stdClass $test Individual "tests[]" document
59+
* @param array $runOn Top-level "runOn" array with server requirements
60+
* @param array $data Top-level "data" array to initialize collection
61+
* @param string $databaseName Name of database under test
62+
* @param string $collectionName Name of collection under test
63+
*/
64+
public function testAtlasDataLake(stdClass $test, array $runOn = null, array $data, $databaseName = null, $collectionName = null)
65+
{
66+
if (isset($runOn)) {
67+
$this->checkServerRequirements($runOn);
68+
}
69+
70+
if (isset($test->skipReason)) {
71+
$this->markTestSkipped($test->skipReason);
72+
}
73+
74+
$databaseName = $databaseName ?? $this->getDatabaseName();
75+
$collectionName = $collectionName ?? $this->getCollectionName();
76+
77+
$context = Context::fromCrud($test, $databaseName, $collectionName);
78+
$this->setContext($context);
79+
80+
/* Note: Atlas Data Lake is read-only, so do not attempt to drop the
81+
* collection under test or insert data fixtures. Necesarry data
82+
* fixtures are already specified in the mongohoused configuration. */
83+
84+
if (isset($test->failPoint)) {
85+
throw new LogicException('ADL tests are not expected to configure fail points');
86+
}
87+
88+
if (isset($test->expectations)) {
89+
$commandExpectations = CommandExpectations::fromCrud((array) $test->expectations);
90+
$commandExpectations->startMonitoring();
91+
}
92+
93+
foreach ($test->operations as $operation) {
94+
Operation::fromCrud($operation)->assert($this, $context);
95+
}
96+
97+
if (isset($commandExpectations)) {
98+
$commandExpectations->stopMonitoring();
99+
$commandExpectations->assert($this, $context);
100+
}
101+
102+
if (isset($test->outcome->collection->data)) {
103+
throw new LogicException('ADL tests are not expected to assert collection data');
104+
}
105+
}
106+
107+
public function provideTests()
108+
{
109+
$testArgs = [];
110+
111+
foreach (glob(__DIR__ . '/atlas_data_lake/*.json') as $filename) {
112+
$json = $this->decodeJson(file_get_contents($filename));
113+
$group = basename($filename, '.json');
114+
$runOn = $json->runOn ?? null;
115+
$data = $json->data ?? [];
116+
$databaseName = $json->database_name ?? null;
117+
$collectionName = $json->collection_name ?? null;
118+
119+
foreach ($json->tests as $test) {
120+
$name = $group . ': ' . $test->description;
121+
$testArgs[$name] = [$test, $runOn, $data, $databaseName, $collectionName];
122+
}
123+
}
124+
125+
return $testArgs;
126+
}
127+
128+
/**
129+
* Prose test 1: Connect without authentication
130+
*/
131+
public function testKillCursors()
132+
{
133+
$cursorId = null;
134+
$cursorNamespace = null;
135+
136+
(new CommandObserver())->observe(
137+
function () {
138+
$client = new Client(static::getUri());
139+
$client->test->driverdata->find([], ['batchSize' => 2, 'limit' => 3]);
140+
},
141+
function (array $event) use (&$cursorId, &$cursorNamespace) {
142+
if ($event['started']->getCommandName() === 'find') {
143+
$this->assertArrayHasKey('succeeded', $event);
144+
145+
$reply = $event['succeeded']->getReply();
146+
$this->assertObjectHasAttribute('cursor', $reply);
147+
$this->assertIsObject($reply->cursor);
148+
$this->assertObjectHasAttribute('id', $reply->cursor);
149+
$this->assertIsInt($reply->cursor->id);
150+
$this->assertObjectHasAttribute('ns', $reply->cursor);
151+
$this->assertIsString($reply->cursor->ns);
152+
153+
/* Note: MongoDB\Driver\CursorId is not used here; however,
154+
* we shouldn't have to worry about encoutnering a 64-bit
155+
* cursor IDs on a 32-bit platform mongohoused allocates IDs
156+
* sequentially (starting from 1). */
157+
$cursorId = $reply->cursor->id;
158+
$cursorNamespace = $reply->cursor->ns;
159+
160+
return;
161+
}
162+
163+
/* After the initial find command, expect that killCursors is
164+
* next and that a cursor ID and namespace were collected. */
165+
$this->assertSame('killCursors', $event['started']->getCommandName());
166+
$this->assertIsInt($cursorId);
167+
$this->assertIsString($cursorNamespace);
168+
169+
list($databaseName, $collectionName) = explode('.', $cursorNamespace, 2);
170+
$command = $event['started']->getCommand();
171+
172+
/* Assert that the killCursors command uses the namespace and
173+
* cursor ID from the find command reply. */
174+
$this->assertSame($databaseName, $event['started']->getDatabaseName());
175+
$this->assertSame($databaseName, $command->{'$db'});
176+
$this->assertObjectHasAttribute('killCursors', $command);
177+
$this->assertSame($collectionName, $command->killCursors);
178+
$this->assertObjectHasAttribute('cursors', $command);
179+
$this->assertIsArray($command->cursors);
180+
$this->assertArrayHasKey(0, $command->cursors);
181+
$this->assertSame($cursorId, $command->cursors[0]);
182+
183+
/* Assert that the killCursors command reply indicates that the
184+
* expected cursor ID was killed. */
185+
$reply = $event['succeeded']->getReply();
186+
$this->assertObjectHasAttribute('cursorsKilled', $reply);
187+
$this->assertIsArray($reply->cursorsKilled);
188+
$this->assertArrayHasKey(0, $reply->cursorsKilled);
189+
$this->assertSame($cursorId, $reply->cursorsKilled[0]);
190+
}
191+
);
192+
}
193+
194+
/**
195+
* Prose test 2: Connect without authentication
196+
*/
197+
public function testConnectWithoutAuth()
198+
{
199+
/* Parse URI to remove userinfo component. The query string is left
200+
* as-is and must not include authMechanism or credentials. */
201+
$parts = parse_url(static::getUri());
202+
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
203+
$path = $parts['path'] ?? '/';
204+
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
205+
206+
$uri = $parts['scheme'] . '://' . $parts['host'] . $port . $path . $query;
207+
208+
$client = new Client($uri);
209+
$cursor = $client->selectDatabase($this->getDatabaseName())->command(['ping' => 1]);
210+
211+
$this->assertInstanceOf(Cursor::class, $cursor);
212+
$this->assertCommandSucceeded(current($cursor->toArray()));
213+
}
214+
215+
/**
216+
* Prose test 3: Connect with SCRAM-SHA-1 authentication
217+
*/
218+
public function testConnectwithSCRAMSHA1()
219+
{
220+
$client = new Client(static::getUri(), ['authMechanism' => 'SCRAM-SHA-1']);
221+
$cursor = $client->selectDatabase($this->getDatabaseName())->command(['ping' => 1]);
222+
223+
$this->assertInstanceOf(Cursor::class, $cursor);
224+
$this->assertCommandSucceeded(current($cursor->toArray()));
225+
}
226+
227+
/**
228+
* Prose test 4: Connect with SCRAM-SHA-256 authentication
229+
*/
230+
public function testConnectwithSCRAMSHA256()
231+
{
232+
$client = new Client(static::getUri(), ['authMechanism' => 'SCRAM-SHA-256']);
233+
$cursor = $client->selectDatabase($this->getDatabaseName())->command(['ping' => 1]);
234+
235+
$this->assertInstanceOf(Cursor::class, $cursor);
236+
$this->assertCommandSucceeded(current($cursor->toArray()));
237+
}
238+
239+
private function isAtlasDataLake() : bool
240+
{
241+
$cursor = $this->manager->executeCommand(
242+
$this->getDatabaseName(),
243+
new Command(['buildInfo' => 1])
244+
);
245+
246+
$document = current($cursor->toArray());
247+
248+
return ! empty($document->dataLake);
249+
}
250+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"collection_name": "driverdata",
3+
"database_name": "test",
4+
"tests": [
5+
{
6+
"description": "Aggregate with pipeline (project, sort, limit)",
7+
"operations": [
8+
{
9+
"object": "collection",
10+
"name": "aggregate",
11+
"arguments": {
12+
"pipeline": [
13+
{
14+
"$project": {
15+
"_id": 0
16+
}
17+
},
18+
{
19+
"$sort": {
20+
"a": 1
21+
}
22+
},
23+
{
24+
"$limit": 2
25+
}
26+
]
27+
},
28+
"result": [
29+
{
30+
"a": 1,
31+
"b": 2,
32+
"c": 3
33+
},
34+
{
35+
"a": 2,
36+
"b": 3,
37+
"c": 4
38+
}
39+
]
40+
}
41+
],
42+
"expectations": [
43+
{
44+
"command_started_event": {
45+
"command": {
46+
"aggregate": "driverdata"
47+
}
48+
}
49+
}
50+
]
51+
}
52+
]
53+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"collection_name": "driverdata",
3+
"database_name": "test",
4+
"tests": [
5+
{
6+
"description": "estimatedDocumentCount succeeds",
7+
"operations": [
8+
{
9+
"object": "collection",
10+
"name": "estimatedDocumentCount",
11+
"result": 15
12+
}
13+
],
14+
"expectations": [
15+
{
16+
"command_started_event": {
17+
"command": {
18+
"count": "driverdata"
19+
}
20+
}
21+
}
22+
]
23+
}
24+
]
25+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"collection_name": "driverdata",
3+
"database_name": "test",
4+
"tests": [
5+
{
6+
"description": "Find with projection and sort",
7+
"operations": [
8+
{
9+
"object": "collection",
10+
"name": "find",
11+
"arguments": {
12+
"filter": {
13+
"b": {
14+
"$gt": 5
15+
}
16+
},
17+
"projection": {
18+
"_id": 0
19+
},
20+
"sort": {
21+
"a": 1
22+
},
23+
"limit": 5
24+
},
25+
"result": [
26+
{
27+
"a": 5,
28+
"b": 6,
29+
"c": 7
30+
},
31+
{
32+
"a": 6,
33+
"b": 7,
34+
"c": 8
35+
},
36+
{
37+
"a": 7,
38+
"b": 8,
39+
"c": 9
40+
},
41+
{
42+
"a": 8,
43+
"b": 9,
44+
"c": 10
45+
},
46+
{
47+
"a": 9,
48+
"b": 10,
49+
"c": 11
50+
}
51+
]
52+
}
53+
],
54+
"expectations": [
55+
{
56+
"command_started_event": {
57+
"command": {
58+
"find": "driverdata"
59+
}
60+
}
61+
}
62+
]
63+
}
64+
]
65+
}

0 commit comments

Comments
 (0)