MINI Sh3ll
<?php
namespace MongoDB\Tests\Collection;
use LogicException;
use MongoDB\BulkWriteResult;
use MongoDB\Collection;
use MongoDB\DeleteResult;
use MongoDB\Driver\Exception\BulkWriteException;
use MongoDB\Driver\Exception\RuntimeException;
use MongoDB\InsertManyResult;
use MongoDB\InsertOneResult;
use MongoDB\Operation\FindOneAndReplace;
use MongoDB\UpdateResult;
use MultipleIterator;
use PHPUnit_Framework_SkippedTestError;
use function array_diff_key;
use function array_key_exists;
use function array_map;
use function file_get_contents;
use function glob;
use function json_decode;
use function MongoDB\is_last_pipeline_operator_write;
use function sprintf;
use function str_replace;
use function strtolower;
use function version_compare;
/**
* CRUD spec functional tests.
*
* @see https://github.com/mongodb/specifications/tree/master/source/crud/tests
*
* @group serverless
* @group matrix-testing-exclude-server-5.0-driver-4.0
*/
class CrudSpecFunctionalTest extends FunctionalTestCase
{
public const SERVERLESS_ALLOW = 'allow';
public const SERVERLESS_FORBID = 'forbid';
public const SERVERLESS_REQUIRE = 'require';
/** @var Collection */
private $expectedCollection;
public function setUp(): void
{
parent::setUp();
$this->expectedCollection = new Collection($this->manager, $this->getDatabaseName(), $this->getCollectionName() . '.expected');
$this->expectedCollection->drop();
}
/**
* @dataProvider provideSpecificationTests
*/
public function testSpecification(array $initialData, array $test, $minServerVersion, $maxServerVersion, $serverless): void
{
if (isset($minServerVersion) || isset($maxServerVersion)) {
$this->checkServerVersion($minServerVersion, $maxServerVersion);
}
$this->checkServerlessRequirement($serverless);
$expectedData = $test['outcome']['collection']['data'] ?? null;
$this->initializeData($initialData, $expectedData);
if (isset($test['outcome']['collection']['name'])) {
$outputCollection = new Collection($this->manager, $this->getDatabaseName(), $test['outcome']['collection']['name']);
$outputCollection->drop();
}
$result = null;
$exception = null;
try {
$result = $this->executeOperation($test['operation']);
} catch (RuntimeException $e) {
$exception = $e;
}
$this->executeOutcome($test['operation'], $test['outcome'], $result, $exception);
}
public function provideSpecificationTests()
{
$testArgs = [];
foreach (glob(__DIR__ . '/spec-tests/*/*.json') as $filename) {
$json = json_decode(file_get_contents($filename), true);
foreach ($json['tests'] as $test) {
$name = str_replace(' ', '_', $test['description']);
$testArgs[$name] = [
$json['data'],
$test,
$json['minServerVersion'] ?? null,
$json['maxServerVersion'] ?? null,
$json['serverless'] ?? null,
];
}
}
return $testArgs;
}
/**
* Assert that the collections contain equivalent documents.
*
* @param Collection $expectedCollection
* @param Collection $actualCollection
*/
private function assertEquivalentCollections(Collection $expectedCollection, Collection $actualCollection): void
{
$mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
$mi->attachIterator($expectedCollection->find());
$mi->attachIterator($actualCollection->find());
foreach ($mi as $documents) {
[$expectedDocument, $actualDocument] = $documents;
$this->assertSameDocument($expectedDocument, $actualDocument);
}
}
private function checkServerlessRequirement(?string $serverless): void
{
switch ($serverless) {
case null:
case self::SERVERLESS_ALLOW:
return;
case self::SERVERLESS_FORBID:
if ($this->isServerless()) {
$this->markTestSkipped('Test does not apply on serverless');
}
return;
case self::SERVERLESS_REQUIRE:
if (! $this->isServerless()) {
$this->markTestSkipped('Test requires serverless');
}
return;
default:
$this->fail(sprintf('Unknown serverless requirement "%s".', $serverless));
}
}
/**
* Checks that the server version is within the allowed bounds (if any).
*
* @param string|null $minServerVersion
* @param string|null $maxServerVersion
* @throws PHPUnit_Framework_SkippedTestError
*/
private function checkServerVersion(?string $minServerVersion, ?string $maxServerVersion): void
{
$serverVersion = $this->getServerVersion();
if (isset($minServerVersion) && version_compare($serverVersion, $minServerVersion, '<')) {
$this->markTestSkipped(sprintf('Server version "%s" < minServerVersion "%s"', $serverVersion, $minServerVersion));
}
if (isset($maxServerVersion) && version_compare($serverVersion, $maxServerVersion, '>=')) {
$this->markTestSkipped(sprintf('Server version "%s" >= maxServerVersion "%s"', $serverVersion, $maxServerVersion));
}
}
/**
* Executes an "operation" block.
*
* @param array $operation
* @return mixed
* @throws LogicException if the operation is unsupported
*/
private function executeOperation(array $operation)
{
switch ($operation['name']) {
case 'aggregate':
return $this->collection->aggregate(
$operation['arguments']['pipeline'],
array_diff_key($operation['arguments'], ['pipeline' => 1])
);
case 'bulkWrite':
return $this->collection->bulkWrite(
array_map([$this, 'prepareBulkWriteRequest'], $operation['arguments']['requests']),
$operation['arguments']['options'] ?? []
);
case 'count':
case 'countDocuments':
case 'find':
return $this->collection->{$operation['name']}(
$operation['arguments']['filter'] ?? [],
array_diff_key($operation['arguments'], ['filter' => 1])
);
case 'estimatedDocumentCount':
return $this->collection->estimatedDocumentCount($operation['arguments']);
case 'deleteMany':
case 'deleteOne':
case 'findOneAndDelete':
return $this->collection->{$operation['name']}(
$operation['arguments']['filter'],
array_diff_key($operation['arguments'], ['filter' => 1])
);
case 'distinct':
return $this->collection->distinct(
$operation['arguments']['fieldName'],
$operation['arguments']['filter'] ?? [],
array_diff_key($operation['arguments'], ['fieldName' => 1, 'filter' => 1])
);
case 'findOneAndReplace':
$operation['arguments'] = $this->prepareFindAndModifyArguments($operation['arguments']);
// Fall through
case 'replaceOne':
return $this->collection->{$operation['name']}(
$operation['arguments']['filter'],
$operation['arguments']['replacement'],
array_diff_key($operation['arguments'], ['filter' => 1, 'replacement' => 1])
);
case 'findOneAndUpdate':
$operation['arguments'] = $this->prepareFindAndModifyArguments($operation['arguments']);
// Fall through
case 'updateMany':
case 'updateOne':
return $this->collection->{$operation['name']}(
$operation['arguments']['filter'],
$operation['arguments']['update'],
array_diff_key($operation['arguments'], ['filter' => 1, 'update' => 1])
);
case 'insertMany':
return $this->collection->insertMany(
$operation['arguments']['documents'],
$operation['arguments']['options'] ?? []
);
case 'insertOne':
return $this->collection->insertOne(
$operation['arguments']['document'],
array_diff_key($operation['arguments'], ['document' => 1])
);
default:
throw new LogicException('Unsupported operation: ' . $operation['name']);
}
}
/**
* Executes an "outcome" block.
*
* @param array $operation
* @param array $outcome
* @param mixed $result
* @param RuntimeException $exception
* @return mixed
* @throws LogicException if the operation is unsupported
*/
private function executeOutcome(array $operation, array $outcome, $result, ?RuntimeException $exception = null)
{
$expectedError = array_key_exists('error', $outcome) ? $outcome['error'] : false;
if ($expectedError) {
$this->assertNull($result);
$this->assertNotNull($exception);
$result = $this->extractResultFromException($operation, $outcome, $exception);
}
if (array_key_exists('result', $outcome)) {
$this->executeAssertResult($operation, $outcome['result'], $result);
}
if (isset($outcome['collection'])) {
$actualCollection = isset($outcome['collection']['name'])
? new Collection($this->manager, $this->getDatabaseName(), $outcome['collection']['name'])
: $this->collection;
$this->assertEquivalentCollections($this->expectedCollection, $actualCollection);
}
}
/**
* Extracts a result from an exception.
*
* Errors for bulkWrite and insertMany operations may still report a write
* result. This method will attempt to extract such a result so that it can
* be used in executeAssertResult().
*
* If no result can be extracted, null will be returned.
*
* @param array $operation
* @param RuntimeException $exception
* @return mixed
*/
private function extractResultFromException(array $operation, array $outcome, RuntimeException $exception)
{
switch ($operation['name']) {
case 'bulkWrite':
$insertedIds = $outcome['result']['insertedIds'] ?? [];
if ($exception instanceof BulkWriteException) {
return new BulkWriteResult($exception->getWriteResult(), $insertedIds);
}
break;
case 'insertMany':
$insertedIds = $outcome['result']['insertedIds'] ?? [];
if ($exception instanceof BulkWriteException) {
return new InsertManyResult($exception->getWriteResult(), $insertedIds);
}
break;
}
return null;
}
/**
* Executes the "result" section of an "outcome" block.
*
* @param array $operation
* @param mixed $expectedResult
* @param mixed $actualResult
* @throws LogicException if the operation is unsupported
*/
private function executeAssertResult(array $operation, $expectedResult, $actualResult): void
{
switch ($operation['name']) {
case 'aggregate':
/* Returning a cursor for the $out collection is optional per
* the CRUD specification and is not implemented in the library
* since we have no concept of lazy cursors. We will not assert
* the result here; however, assertEquivalentCollections() will
* assert the output collection's contents later.
*/
if (! is_last_pipeline_operator_write($operation['arguments']['pipeline'])) {
$this->assertSameDocuments($expectedResult, $actualResult);
}
break;
case 'bulkWrite':
$this->assertIsArray($expectedResult);
$this->assertInstanceOf(BulkWriteResult::class, $actualResult);
if (isset($expectedResult['deletedCount'])) {
$this->assertSame($expectedResult['deletedCount'], $actualResult->getDeletedCount());
}
if (isset($expectedResult['insertedCount'])) {
$this->assertSame($expectedResult['insertedCount'], $actualResult->getInsertedCount());
}
if (isset($expectedResult['insertedIds'])) {
$this->assertSameDocument(
['insertedIds' => $expectedResult['insertedIds']],
['insertedIds' => $actualResult->getInsertedIds()]
);
}
if (isset($expectedResult['matchedCount'])) {
$this->assertSame($expectedResult['matchedCount'], $actualResult->getMatchedCount());
}
if (isset($expectedResult['modifiedCount'])) {
$this->assertSame($expectedResult['modifiedCount'], $actualResult->getModifiedCount());
}
if (isset($expectedResult['upsertedCount'])) {
$this->assertSame($expectedResult['upsertedCount'], $actualResult->getUpsertedCount());
}
if (isset($expectedResult['upsertedIds'])) {
$this->assertSameDocument(
['upsertedIds' => $expectedResult['upsertedIds']],
['upsertedIds' => $actualResult->getUpsertedIds()]
);
}
break;
case 'count':
case 'countDocuments':
case 'estimatedDocumentCount':
$this->assertSame($expectedResult, $actualResult);
break;
case 'distinct':
$this->assertSameDocument(
['values' => $expectedResult],
['values' => $actualResult]
);
break;
case 'find':
$this->assertSameDocuments($expectedResult, $actualResult);
break;
case 'deleteMany':
case 'deleteOne':
$this->assertIsArray($expectedResult);
$this->assertInstanceOf(DeleteResult::class, $actualResult);
if (isset($expectedResult['deletedCount'])) {
$this->assertSame($expectedResult['deletedCount'], $actualResult->getDeletedCount());
}
break;
case 'findOneAndDelete':
case 'findOneAndReplace':
case 'findOneAndUpdate':
$this->assertSameDocument(
['result' => $expectedResult],
['result' => $actualResult]
);
break;
case 'insertMany':
$this->assertIsArray($expectedResult);
$this->assertInstanceOf(InsertManyResult::class, $actualResult);
if (isset($expectedResult['insertedCount'])) {
$this->assertSame($expectedResult['insertedCount'], $actualResult->getInsertedCount());
}
if (isset($expectedResult['insertedIds'])) {
$this->assertSameDocument(
['insertedIds' => $expectedResult['insertedIds']],
['insertedIds' => $actualResult->getInsertedIds()]
);
}
break;
case 'insertOne':
$this->assertIsArray($expectedResult);
$this->assertInstanceOf(InsertOneResult::class, $actualResult);
if (isset($expectedResult['insertedCount'])) {
$this->assertSame($expectedResult['insertedCount'], $actualResult->getInsertedCount());
}
if (isset($expectedResult['insertedId'])) {
$this->assertSameDocument(
['insertedId' => $expectedResult['insertedId']],
['insertedId' => $actualResult->getInsertedId()]
);
}
break;
case 'replaceOne':
case 'updateMany':
case 'updateOne':
$this->assertIsArray($expectedResult);
$this->assertInstanceOf(UpdateResult::class, $actualResult);
if (isset($expectedResult['matchedCount'])) {
$this->assertSame($expectedResult['matchedCount'], $actualResult->getMatchedCount());
}
if (isset($expectedResult['modifiedCount'])) {
$this->assertSame($expectedResult['modifiedCount'], $actualResult->getModifiedCount());
}
if (isset($expectedResult['upsertedCount'])) {
$this->assertSame($expectedResult['upsertedCount'], $actualResult->getUpsertedCount());
}
if (array_key_exists('upsertedId', $expectedResult)) {
$this->assertSameDocument(
['upsertedId' => $expectedResult['upsertedId']],
['upsertedId' => $actualResult->getUpsertedId()]
);
}
break;
default:
throw new LogicException('Unsupported operation: ' . $operation['name']);
}
}
/**
* Initializes data in the test collections.
*
* @param array $initialData
* @param array $expectedData
*/
private function initializeData(array $initialData, ?array $expectedData = null): void
{
if (! empty($initialData)) {
$this->collection->insertMany($initialData);
}
if (! empty($expectedData)) {
$this->expectedCollection->insertMany($expectedData);
}
}
/**
* Prepares a request element for a bulkWrite operation.
*
* @param array $request
* @return array
*/
private function prepareBulkWriteRequest(array $request): array
{
switch ($request['name']) {
case 'deleteMany':
case 'deleteOne':
return [
$request['name'] => [
$request['arguments']['filter'],
array_diff_key($request['arguments'], ['filter' => 1]),
],
];
case 'insertOne':
return ['insertOne' => [$request['arguments']['document']]];
case 'replaceOne':
return [
'replaceOne' => [
$request['arguments']['filter'],
$request['arguments']['replacement'],
array_diff_key($request['arguments'], ['filter' => 1, 'replacement' => 1]),
],
];
case 'updateMany':
case 'updateOne':
return [
$request['name'] => [
$request['arguments']['filter'],
$request['arguments']['update'],
array_diff_key($request['arguments'], ['filter' => 1, 'update' => 1]),
],
];
default:
throw new LogicException('Unsupported bulk write request: ' . $request['name']);
}
}
/**
* Prepares arguments for findOneAndReplace and findOneAndUpdate operations.
*
* @param array $arguments
* @return array
*/
private function prepareFindAndModifyArguments(array $arguments): array
{
if (isset($arguments['returnDocument'])) {
$arguments['returnDocument'] = 'after' === strtolower($arguments['returnDocument'])
? FindOneAndReplace::RETURN_DOCUMENT_AFTER
: FindOneAndReplace::RETURN_DOCUMENT_BEFORE;
}
return $arguments;
}
}
OHA YOOOO