<?php
 namespace MongoDB\Tests\GridFS;
 use DateTime;
 use Exception;
 use IteratorIterator;
 use LogicException;
 use MongoDB\BSON\Binary;
 use MongoDB\BSON\ObjectId;
 use MongoDB\BSON\UTCDateTime;
 use MongoDB\Collection;
 use MongoDB\GridFS\Exception\FileNotFoundException;
 use MongoDB\Operation\BulkWrite;
 use MultipleIterator;
 use PHPUnit\Framework\Error\Warning;
 use Symfony\Bridge\PhpUnit\SetUpTearDownTrait;
 use function array_walk;
 use function array_walk_recursive;
 use function file_get_contents;
 use function glob;
 use function hex2bin;
 use function in_array;
 use function is_array;
 use function is_string;
 use function json_decode;
 use function str_replace;
 use function stream_get_contents;
 use function strncmp;
 use function var_export;
 class SpecFunctionalTest extends FunctionalTestCase {
 use SetUpTearDownTrait;
 private $expectedChunksCollection;
 private $expectedFilesCollection;
 private function doSetUp() {
 parent::setUp();
 $this->expectedFilesCollection = new Collection($this->manager, $this->getDatabaseName(), 'expected.files');
 $this->expectedFilesCollection->drop();
 $this->expectedChunksCollection = new Collection($this->manager, $this->getDatabaseName(), 'expected.chunks');
 $this->expectedChunksCollection->drop();
 }
 public function testSpecification(array $initialData, array $test) {
 $this->initializeData($initialData);
 if (isset($test['arrange'])) {
 foreach ($test['arrange']['data'] as $dataModification) {
 $this->executeDataModification($dataModification);
 }
 }
 try {
 $result = $this->executeAct($test['act']);
 }
 catch (Exception $e) {
 $result = $e;
 }
 if ($test['act']['operation'] === 'delete' && $result instanceof FileNotFoundException) {
 $filesId = $this->convertTypes($test['act'])['arguments']['id'];
 $this->expectedChunksCollection->deleteMany(['files_id' => $filesId]);
 }
 if (isset($test['assert'])) {
 $this->executeAssert($test['assert'], $result);
 }
 }
 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];
 }
 }
 return $testArgs;
 }
 private function assertEquivalentCollections($expectedCollection, $actualCollection, $actualResult) {
 $mi = new MultipleIterator(MultipleIterator::MIT_NEED_ANY);
 $mi->attachIterator(new IteratorIterator($expectedCollection->find()));
 $mi->attachIterator(new IteratorIterator($actualCollection->find()));
 foreach ($mi as $documents) {
 list($expectedDocument, $actualDocument) = $documents;
 foreach ($expectedDocument as $key => $value) {
 if (! is_string($value)) {
 continue;
 }
 if ($value === '*result') {
 $expectedDocument[$key] = $actualResult;
 }
 if (! strncmp($value, '*actual_', 8)) {
 $expectedDocument[$key] = $actualDocument[$key];
 }
 }
 $this->assertSameDocument($expectedDocument, $actualDocument);
 }
 }
 private function convertTypes(array $data, $createBinary = true) {
 array_walk($data, function (&$value) use ($createBinary) {
 if (! is_array($value)) {
 return;
 }
 if (isset($value['$oid'])) {
 $value = new ObjectId($value['$oid']);
 return;
 }
 if (isset($value['$hex'])) {
 $value = $createBinary ? new Binary(hex2bin($value['$hex']), Binary::TYPE_GENERIC) : hex2bin($value['$hex']);
 return;
 }
 if (isset($value['$date'])) {
 $value = new UTCDateTime(new DateTime($value['$date']));
 return;
 }
 $value = $this->convertTypes($value, $createBinary);
 }
);
 return $data;
 }
 private function executeAct(array $act) {
 $act = $this->convertTypes($act, false);
 switch ($act['operation']) {
 case 'delete': return $this->bucket->delete($act['arguments']['id']);
 case 'download': return stream_get_contents($this->bucket->openDownloadStream($act['arguments']['id']));
 case 'download_by_name': return stream_get_contents($this->bucket->openDownloadStreamByName( $act['arguments']['filename'], isset($act['arguments']['options']) ? $act['arguments']['options'] : [] ));
 case 'upload': return $this->bucket->uploadFromStream( $act['arguments']['filename'], $this->createStream($act['arguments']['source']), isset($act['arguments']['options']) ? $act['arguments']['options'] : [] );
 default: throw new LogicException('Unsupported act: ' . $act['operation']);
 }
 }
 private function executeAssert(array $assert, $actualResult) {
 if (isset($assert['error'])) {
 $this->assertInstanceOf($this->getExceptionClassForError($assert['error']), $actualResult);
 }
 if (isset($assert['result'])) {
 $this->executeAssertResult($assert['result'], $actualResult);
 }
 if (! isset($assert['data'])) {
 return;
 }
 array_walk_recursive($assert['data'], function (&$value) {
 if ($value === '*actual') {
 $value .= '_' . new ObjectId();
 }
 }
);
 foreach ($assert['data'] as $dataModification) {
 $this->executeDataModification($dataModification);
 }
 $this->assertEquivalentCollections($this->expectedFilesCollection, $this->filesCollection, $actualResult);
 $this->assertEquivalentCollections($this->expectedChunksCollection, $this->chunksCollection, $actualResult);
 }
 private function executeAssertResult($expectedResult, $actualResult) {
 if ($expectedResult === 'void') {
 return $this->assertNull($actualResult);
 }
 if ($expectedResult === '&result') {
 return;
 }
 if (isset($expectedResult['$hex'])) {
 return $this->assertSame(hex2bin($expectedResult['$hex']), $actualResult);
 }
 throw new LogicException('Unsupported result assertion: ' . var_export($expectedResult, true));
 }
 private function executeDataModification(array $dataModification) {
 if (empty($dataModification)) {
 throw new LogicException('Command for data modification is empty');
 }
 foreach ($dataModification as $type => $collectionName) {
 break;
 }
 if (! in_array($collectionName, ['fs.files', 'fs.chunks', 'expected.files', 'expected.chunks'])) {
 throw new LogicException('Unsupported collection: ' . $collectionName);
 }
 $dataModification = $this->convertTypes($dataModification);
 $operations = [];
 switch ($type) {
 case 'delete': foreach ($dataModification['deletes'] as $delete) {
 $operations[] = [ ($delete['limit'] === 1 ? 'deleteOne' : 'deleteMany') => [ $delete['q'] ] ];
 }
 break;
 case 'insert': foreach ($dataModification['documents'] as $document) {
 $operations[] = [ 'insertOne' => [ $document ] ];
 }
 break;
 case 'update': foreach ($dataModification['updates'] as $update) {
 $operations[] = [ 'updateOne' => [ $update['q'], $update['u'] ] ];
 }
 break;
 default: throw new LogicException('Unsupported arrangement: ' . $type);
 }
 $bulk = new BulkWrite($this->getDatabaseName(), $collectionName, $operations);
 return $bulk->execute($this->getPrimaryServer());
 }
 private function getExceptionClassForError($error) {
 switch ($error) {
 case 'FileNotFound': case 'RevisionNotFound': return FileNotFoundException::class;
 case 'ChunkIsMissing': case 'ChunkIsWrongSize': return Warning::class;
 default: throw new LogicException('Unsupported error: ' . $error);
 }
 }
 private function initializeData(array $data) {
 $data = $this->convertTypes($data);
 if (! empty($data['files'])) {
 $this->filesCollection->insertMany($data['files']);
 $this->expectedFilesCollection->insertMany($data['files']);
 }
 if (! empty($data['chunks'])) {
 $this->chunksCollection->insertMany($data['chunks']);
 $this->expectedChunksCollection->insertMany($data['chunks']);
 }
 }
 }
 