Commit cd9394c2 authored by Jeremy Mikola's avatar Jeremy Mikola

Merge pull request #192

parents 3dbbd798 a916f8f6
......@@ -265,9 +265,14 @@ class Bucket
*
* Supported options:
*
* * _id (mixed): File document identifier. Defaults to a new ObjectId.
*
* * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
* bucket's chunk size.
*
* * metadata (document): User data for the "metadata" field of the files
* collection document.
*
* @param string $filename Filename
* @param array $options Upload options
* @return resource
......@@ -322,9 +327,14 @@ class Bucket
*
* Supported options:
*
* * _id (mixed): File document identifier. Defaults to a new ObjectId.
*
* * chunkSizeBytes (integer): The chunk size in bytes. Defaults to the
* bucket's chunk size.
*
* * metadata (document): User data for the "metadata" field of the files
* collection document.
*
* @param string $filename Filename
* @param resource $source Readable stream
* @param array $options Stream options
......
......@@ -17,7 +17,9 @@ use stdClass;
*/
class CollectionWrapper
{
private $bucketName;
private $chunksCollection;
private $databaseName;
private $checkedIndexes = false;
private $filesCollection;
......@@ -33,6 +35,9 @@ class CollectionWrapper
*/
public function __construct(Manager $manager, $databaseName, $bucketName, array $collectionOptions = [])
{
$this->databaseName = (string) $databaseName;
$this->bucketName = (string) $bucketName;
$this->filesCollection = new Collection($manager, $databaseName, sprintf('%s.files', $bucketName), $collectionOptions);
$this->chunksCollection = new Collection($manager, $databaseName, sprintf('%s.chunks', $bucketName), $collectionOptions);
}
......@@ -135,10 +140,14 @@ class CollectionWrapper
return $this->filesCollection->find($filter, $options);
}
// TODO: Remove this
public function getChunksCollection()
/**
* Return the bucket name.
*
* @return string
*/
public function getBucketName()
{
return $this->chunksCollection;
return $this->bucketName;
}
/**
......@@ -160,10 +169,14 @@ class CollectionWrapper
return new IteratorIterator($cursor);
}
// TODO: Remove this
public function getFilesCollection()
/**
* Return the database name.
*
* @return string
*/
public function getDatabaseName()
{
return $this->filesCollection;
return $this->databaseName;
}
/**
......
......@@ -2,7 +2,7 @@
namespace MongoDB\GridFS;
use MongoDB\Driver\Exception\Exception;
use MongoDB\Exception\InvalidArgumentException;
use MongoDB\GridFS\Exception\CorruptFileException;
use stdClass;
......@@ -17,11 +17,14 @@ class ReadableStream
private $bufferEmpty;
private $bufferFresh;
private $bytesSeen = 0;
private $chunkSize;
private $chunkOffset = 0;
private $chunksIterator;
private $file;
private $collectionWrapper;
private $firstCheck = true;
private $id;
private $iteratorEmpty = false;
private $length;
private $numChunks;
/**
......@@ -33,13 +36,45 @@ class ReadableStream
*/
public function __construct(CollectionWrapper $collectionWrapper, stdClass $file)
{
$this->file = $file;
if ( ! isset($file->chunkSize) || ! is_integer($file->chunkSize) || $file->chunkSize < 1) {
throw new CorruptFileException('file.chunkSize is not an integer >= 1');
}
if ( ! isset($file->length) || ! is_integer($file->length) || $file->length < 0) {
throw new CorruptFileException('file.length is not an integer > 0');
}
$this->chunksIterator = $collectionWrapper->getChunksIteratorByFilesId($this->file->_id);
$this->numChunks = ($file->length >= 0) ? ceil($file->length / $file->chunkSize) : 0;
if ( ! isset($file->_id) && ! array_key_exists('_id', (array) $file)) {
throw new CorruptFileException('file._id does not exist');
}
$this->id = $file->_id;
$this->chunkSize = $file->chunkSize;
$this->length = $file->length;
$this->chunksIterator = $collectionWrapper->getChunksIteratorByFilesId($this->id);
$this->collectionWrapper = $collectionWrapper;
$this->numChunks = ceil($this->length / $this->chunkSize);
$this->initEmptyBuffer();
}
/**
* Return internal properties for debugging purposes.
*
* @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
* @return array
*/
public function __debugInfo()
{
return [
'bucketName' => $this->collectionWrapper->getBucketName(),
'databaseName' => $this->collectionWrapper->getDatabaseName(),
'id' => $this->id,
'chunkSize' => $this->chunkSize,
'length' => $this->length,
];
}
public function close()
{
fclose($this->buffer);
......@@ -53,9 +88,18 @@ class ReadableStream
*
* @param integer $numBytes Number of bytes to read
* @return string
* @throws InvalidArgumentException if $numBytes is negative
*/
public function downloadNumBytes($numBytes)
{
if ($numBytes < 0) {
throw new InvalidArgumentException(sprintf('$numBytes must be >= zero; given: %d', $numBytes));
}
if ($numBytes == 0) {
return '';
}
if ($this->bufferFresh) {
rewind($this->buffer);
$this->bufferFresh = false;
......@@ -77,7 +121,7 @@ class ReadableStream
$output .= substr($this->chunksIterator->current()->data->getData(), 0, $bytesLeft);
}
if ( ! $this->iteratorEmpty && $this->file->length > 0 && $bytesLeft < strlen($this->chunksIterator->current()->data->getData())) {
if ( ! $this->iteratorEmpty && $this->length > 0 && $bytesLeft < strlen($this->chunksIterator->current()->data->getData())) {
fwrite($this->buffer, substr($this->chunksIterator->current()->data->getData(), $bytesLeft));
$this->bufferEmpty = false;
}
......@@ -103,19 +147,24 @@ class ReadableStream
}
}
public function getFile()
{
return $this->file;
}
/**
* Return the stream's ID (i.e. file document identifier).
*
* @return integer
*/
public function getId()
{
return $this->file->_id;
return $this->id;
}
/**
* Return the stream's size in bytes.
*
* @return integer
*/
public function getSize()
{
return $this->file->length;
return $this->length;
}
public function isEOF()
......@@ -149,8 +198,8 @@ class ReadableStream
$actualChunkSize = strlen($this->chunksIterator->current()->data->getData());
$expectedChunkSize = ($this->chunkOffset == $this->numChunks - 1)
? ($this->file->length - $this->bytesSeen)
: $this->file->chunkSize;
? ($this->length - $this->bytesSeen)
: $this->chunkSize;
if ($actualChunkSize != $expectedChunkSize) {
throw CorruptFileException::unexpectedSize($actualChunkSize, $expectedChunkSize);
......
......@@ -89,6 +89,21 @@ class WritableStream
] + array_intersect_key($options, ['aliases' => 1, 'contentType' => 1, 'metadata' => 1]);
}
/**
* Return internal properties for debugging purposes.
*
* @see http://php.net/manual/en/language.oop5.magic.php#language.oop5.magic.debuginfo
* @return array
*/
public function __debugInfo()
{
return [
'bucketName' => $this->collectionWrapper->getBucketName(),
'databaseName' => $this->collectionWrapper->getDatabaseName(),
'file' => $this->file,
];
}
/**
* Closes an active stream and flushes all buffered data to GridFS.
*/
......@@ -111,26 +126,23 @@ class WritableStream
$this->isClosed = true;
}
public function getChunkSize()
{
return $this->chunkSize;
}
public function getFile()
{
return $this->file;
}
/**
* Return the stream's ID (i.e. file document identifier).
*
* @return integer
*/
public function getId()
{
return $this->file['_id'];
}
public function getLength()
{
return $this->length;
}
/**
* Return the stream's size in bytes.
*
* Note: this value will increase as more data is written to the stream.
*
* @return integer
*/
public function getSize()
{
return $this->length;
......
......@@ -77,9 +77,10 @@ function is_first_key_operator($document)
throw InvalidArgumentException::invalidType('$document', $document, 'array or object');
}
reset($document);
$firstKey = (string) key($document);
return (isset($firstKey[0]) && $firstKey[0] == '$');
return (isset($firstKey[0]) && $firstKey[0] === '$');
}
/**
......
......@@ -2,9 +2,14 @@
namespace MongoDB\Tests\GridFS;
use MongoDB\BSON\Binary;
use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern;
use MongoDB\GridFS\Bucket;
use MongoDB\GridFS\Exception\FileNotFoundException;
use MongoDB\Model\IndexInfo;
use MongoDB\Operation\ListCollections;
use MongoDB\Operation\ListIndexes;
/**
* Functional tests for the Bucket class.
......@@ -53,271 +58,445 @@ class BucketFunctionalTest extends FunctionalTestCase
return $options;
}
public function testGetDatabaseName()
/**
* @dataProvider provideInputDataAndExpectedChunks
*/
public function testDelete($input, $expectedChunks)
{
$this->assertEquals($this->getDatabaseName(), $this->bucket->getDatabaseName());
$id = $this->bucket->uploadFromStream('filename', $this->createStream($input));
$this->assertCollectionCount($this->filesCollection, 1);
$this->assertCollectionCount($this->chunksCollection, $expectedChunks);
$this->bucket->delete($id);
$this->assertCollectionCount($this->filesCollection, 0);
$this->assertCollectionCount($this->chunksCollection, 0);
}
public function testBasicOperations()
public function provideInputDataAndExpectedChunks()
{
$id = $this->bucket->uploadFromStream("test_filename", $this->generateStream("hello world"));
$contents = stream_get_contents($this->bucket->openDownloadStream($id));
$this->assertEquals("hello world", $contents);
$this->assertEquals(1, $this->bucket->getCollectionWrapper()->getFilesCollection()->count());
$this->assertEquals(1, $this->bucket->getCollectionWrapper()->getChunksCollection()->count());
return [
['', 0],
['foobar', 1],
[str_repeat('a', 261120), 1],
[str_repeat('a', 261121), 2],
[str_repeat('a', 522240), 2],
[str_repeat('a', 522241), 3],
[str_repeat('foobar', 43520), 1],
[str_repeat('foobar', 43521), 2],
[str_repeat('foobar', 87040), 2],
[str_repeat('foobar', 87041), 3],
];
}
/**
* @expectedException MongoDB\GridFS\Exception\FileNotFoundException
*/
public function testDeleteShouldRequireFileToExist()
{
$this->bucket->delete('nonexistent-id');
}
/**
* @dataProvider provideInputDataAndExpectedChunks
*/
public function testDeleteStillRemovesChunksIfFileDoesNotExist($input, $expectedChunks)
{
$id = $this->bucket->uploadFromStream('filename', $this->createStream($input));
$this->assertCollectionCount($this->filesCollection, 1);
$this->assertCollectionCount($this->chunksCollection, $expectedChunks);
$this->filesCollection->deleteOne(['_id' => $id]);
try {
$this->bucket->delete($id);
$error=null;
try{
$this->bucket->openDownloadStream($id);
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
}
$fileNotFound = '\MongoDB\GridFS\Exception\FileNotFoundException';
$this->assertTrue($error instanceof $fileNotFound);
$this->assertEquals(0, $this->bucket->getCollectionWrapper()->getFilesCollection()->count());
$this->assertEquals(0, $this->bucket->getCollectionWrapper()->getChunksCollection()->count());
}
public function testMultiChunkDelete()
{
$id = $this->bucket->uploadFromStream("test_filename", $this->generateStream("hello"), ['chunkSizeBytes'=>1]);
$this->assertEquals(1, $this->bucket->getCollectionWrapper()->getFilesCollection()->count());
$this->assertEquals(5, $this->bucket->getCollectionWrapper()->getChunksCollection()->count());
$this->bucket->delete($id);
$this->assertEquals(0, $this->bucket->getCollectionWrapper()->getFilesCollection()->count());
$this->assertEquals(0, $this->bucket->getCollectionWrapper()->getChunksCollection()->count());
$this->fail('FileNotFoundException was not thrown');
} catch (FileNotFoundException $e) {}
$this->assertCollectionCount($this->chunksCollection, 0);
}
public function testEmptyFile()
/**
* @expectedException MongoDB\GridFS\Exception\CorruptFileException
*/
public function testDownloadingFileWithMissingChunk()
{
$id = $this->bucket->uploadFromStream("test_filename",$this->generateStream(""));
$contents = stream_get_contents($this->bucket->openDownloadStream($id));
$this->assertEquals("", $contents);
$this->assertEquals(1, $this->bucket->getCollectionWrapper()->getFilesCollection()->count());
$this->assertEquals(0, $this->bucket->getCollectionWrapper()->getChunksCollection()->count());
$raw = $this->bucket->getCollectionWrapper()->getFilesCollection()->findOne();
$this->assertEquals(0, $raw->length);
$this->assertEquals($id, $raw->_id);
$this->assertTrue($raw->uploadDate instanceof \MongoDB\BSON\UTCDateTime);
$this->assertEquals(255 * 1024, $raw->chunkSize);
$this->assertTrue(is_string($raw->md5));
}
public function testCorruptChunk()
{
$id = $this->bucket->uploadFromStream("test_filename", $this->generateStream("foobar"));
$this->collectionWrapper->getChunksCollection()->updateOne(['files_id' => $id],
['$set' => ['data' => new \MongoDB\BSON\Binary('foo', \MongoDB\BSON\Binary::TYPE_GENERIC)]]);
$error = null;
try{
$download = $this->bucket->openDownloadStream($id);
stream_get_contents($download);
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
}
$corruptFileError = '\MongoDB\GridFS\Exception\CorruptFileException';
$this->assertTrue($error instanceof $corruptFileError);
}
public function testErrorsOnMissingChunk()
{
$id = $this->bucket->uploadFromStream("test_filename", $this->generateStream("hello world,abcdefghijklmnopqrstuv123456789"), ["chunkSizeBytes" => 1]);
$this->collectionWrapper->getChunksCollection()->deleteOne(['files_id' => $id, 'n' => 7]);
$error = null;
try{
$download = $this->bucket->openDownloadStream($id);
stream_get_contents($download);
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
}
$corruptFileError = '\MongoDB\GridFS\Exception\CorruptFileException';
$this->assertTrue($error instanceof $corruptFileError);
}
public function testUploadEnsureIndexes()
{
$chunks = $this->bucket->getCollectionWrapper()->getChunksCollection();
$files = $this->bucket->getCollectionWrapper()->getFilesCollection();
$this->bucket->uploadFromStream("filename", $this->generateStream("junk"));
$chunksIndexed = false;
foreach($chunks->listIndexes() as $index) {
$chunksIndexed = $chunksIndexed || ($index->isUnique() && $index->getKey() === ['files_id' => 1, 'n' => 1]);
}
$this->assertTrue($chunksIndexed);
$filesIndexed = false;
foreach($files->listIndexes() as $index) {
$filesIndexed = $filesIndexed || ($index->getKey() === ['filename' => 1, 'uploadDate' => 1]);
}
$this->assertTrue($filesIndexed);
}
public function testGetLastVersion()
{
$idOne = $this->bucket->uploadFromStream("test",$this->generateStream("foo"));
$streamTwo = $this->bucket->openUploadStream("test");
fwrite($streamTwo, "bar");
//echo "Calling FSTAT\n";
//$stat = fstat($streamTwo);
$idTwo = $this->bucket->getIdFromStream($streamTwo);
//var_dump
//var_dump($idTwo);
fclose($streamTwo);
$idThree = $this->bucket->uploadFromStream("test",$this->generateStream("baz"));
$this->assertEquals("baz", stream_get_contents($this->bucket->openDownloadStreamByName("test")));
$this->bucket->delete($idThree);
$this->assertEquals("bar", stream_get_contents($this->bucket->openDownloadStreamByName("test")));
$this->bucket->delete($idTwo);
$this->assertEquals("foo", stream_get_contents($this->bucket->openDownloadStreamByName("test")));
$this->bucket->delete($idOne);
$error = null;
try{
$this->bucket->openDownloadStreamByName("test");
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
}
$fileNotFound = '\MongoDB\GridFS\Exception\FileNotFoundException';
$this->assertTrue($error instanceof $fileNotFound);
}
public function testGetVersion()
{
$this->bucket->uploadFromStream("test",$this->generateStream("foo"));
$this->bucket->uploadFromStream("test",$this->generateStream("bar"));
$this->bucket->uploadFromStream("test",$this->generateStream("baz"));
$this->assertEquals("foo", stream_get_contents($this->bucket->openDownloadStreamByName("test", ['revision' => 0])));
$this->assertEquals("bar", stream_get_contents($this->bucket->openDownloadStreamByName("test", ['revision' => 1])));
$this->assertEquals("baz", stream_get_contents($this->bucket->openDownloadStreamByName("test", ['revision' => 2])));
$this->assertEquals("baz", stream_get_contents($this->bucket->openDownloadStreamByName("test", ['revision' => -1])));
$this->assertEquals("bar", stream_get_contents($this->bucket->openDownloadStreamByName("test", ['revision' => -2])));
$this->assertEquals("foo", stream_get_contents($this->bucket->openDownloadStreamByName("test", ['revision' => -3])));
$fileNotFound = '\MongoDB\GridFS\Exception\FileNotFoundException';
$error = null;
try{
$this->bucket->openDownloadStreamByName("test", ['revision' => 3]);
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
}
$this->assertTrue($error instanceof $fileNotFound);
$error = null;
try{
$this->bucket->openDownloadStreamByName("test", ['revision' => -4]);
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
}
$this->assertTrue($error instanceof $fileNotFound);
}
public function testGridfsFind()
{
$this->bucket->uploadFromStream("two",$this->generateStream("test2"));
usleep(5000);
$this->bucket->uploadFromStream("two",$this->generateStream("test2+"));
usleep(5000);
$this->bucket->uploadFromStream("one",$this->generateStream("test1"));
usleep(5000);
$this->bucket->uploadFromStream("two",$this->generateStream("test2++"));
$cursor = $this->bucket->find(["filename" => "two"]);
$count = count($cursor->toArray());
$this->assertEquals(3, $count);
$cursor = $this->bucket->find([]);
$count = count($cursor->toArray());
$this->assertEquals(4, $count);
$cursor = $this->bucket->find([], ["noCursorTimeout"=>false, "sort"=>["uploadDate"=> -1], "skip"=>1, "limit"=>2]);
$outputs = ["test1", "test2+"];
$i=0;
foreach($cursor as $file){
$contents = stream_get_contents($this->bucket->openDownloadStream($file->_id));
$this->assertEquals($outputs[$i], $contents);
$i++;
}
}
public function testGridInNonIntChunksize()
{
$id = $this->bucket->uploadFromStream("f",$this->generateStream("data"));
$this->bucket->getCollectionWrapper()->getFilesCollection()->updateOne(["filename"=>"f"],
['$set'=> ['chunkSize' => 100.00]]);
$this->assertEquals("data", stream_get_contents($this->bucket->openDownloadStream($id)));
}
public function testBigInsert()
{
for ($tmpStream = tmpfile(), $i = 0; $i < 20; $i++) {
fwrite($tmpStream, str_repeat('a', 1048576));
}
fseek($tmpStream, 0);
$this->bucket->uploadFromStream("BigInsertTest", $tmpStream);
fclose($tmpStream);
$id = $this->bucket->uploadFromStream("filename", $this->createStream("foobar"));
$this->chunksCollection->deleteOne(['files_id' => $id, 'n' => 0]);
stream_get_contents($this->bucket->openDownloadStream($id));
}
public function testGetIdFromStream()
/**
* @expectedException MongoDB\GridFS\Exception\CorruptFileException
*/
public function testDownloadingFileWithUnexpectedChunkIndex()
{
$upload = $this->bucket->openUploadStream("test");
$id = $this->bucket->getIdFromStream($upload);
fclose($upload);
$this->assertTrue($id instanceof \MongoDB\BSON\ObjectId);
$id = $this->bucket->uploadFromStream("filename", $this->createStream("foobar"));
$download = $this->bucket->openDownloadStream($id);
$id=null;
$id = $this->bucket->getIdFromStream($download);
fclose($download);
$this->assertTrue($id instanceof \MongoDB\BSON\ObjectId);
$this->chunksCollection->updateOne(
['files_id' => $id, 'n' => 0],
['$set' => ['n' => 1]]
);
stream_get_contents($this->bucket->openDownloadStream($id));
}
public function testRename()
/**
* @expectedException MongoDB\GridFS\Exception\CorruptFileException
*/
public function testDownloadingFileWithUnexpectedChunkSize()
{
$id = $this->bucket->uploadFromStream("first_name", $this->generateStream("testing"));
$this->assertEquals("testing", stream_get_contents($this->bucket->openDownloadStream($id)));
$id = $this->bucket->uploadFromStream("filename", $this->createStream("foobar"));
$this->chunksCollection->updateOne(
['files_id' => $id, 'n' => 0],
['$set' => ['data' => new Binary('fooba', Binary::TYPE_GENERIC)]]
);
stream_get_contents($this->bucket->openDownloadStream($id));
}
$this->bucket->rename($id, "second_name");
/**
* @dataProvider provideInputDataAndExpectedChunks
*/
public function testDownloadToStream($input)
{
$id = $this->bucket->uploadFromStream('filename', $this->createStream($input));
$destination = $this->createStream();
$this->bucket->downloadToStream($id, $destination);
$error = null;
try{
$this->bucket->openDownloadStreamByName("first_name");
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
$this->assertStreamContents($input, $destination);
}
$fileNotFound = '\MongoDB\GridFS\Exception\FileNotFoundException';
$this->assertTrue($error instanceof $fileNotFound);
$this->assertEquals("testing", stream_get_contents($this->bucket->openDownloadStreamByName("second_name")));
/**
* @expectedException MongoDB\GridFS\Exception\FileNotFoundException
*/
public function testDownloadToStreamShouldRequireFileToExist()
{
$this->bucket->downloadToStream('nonexistent-id', $this->createStream());
}
public function testDrop()
{
$id = $this->bucket->uploadFromStream("test_filename", $this->generateStream("hello world"));
$this->bucket->uploadFromStream('filename', $this->createStream('foobar'));
$this->assertCollectionCount($this->filesCollection, 1);
$this->assertCollectionCount($this->chunksCollection, 1);
$this->bucket->drop();
$id = $this->bucket->uploadFromStream("test_filename", $this->generateStream("hello world"));
$this->assertEquals(1, $this->collectionWrapper->getFilesCollection()->count());
$this->assertCollectionDoesNotExist($this->filesCollection->getCollectionName());
$this->assertCollectionDoesNotExist($this->chunksCollection->getCollectionName());
}
public function testFind()
{
$this->bucket->uploadFromStream('a', $this->createStream('foo'));
$this->bucket->uploadFromStream('b', $this->createStream('foobar'));
$this->bucket->uploadFromStream('c', $this->createStream('foobarbaz'));
$cursor = $this->bucket->find(
['length' => ['$lte' => 6]],
[
'projection' => [
'filename' => 1,
'length' => 1,
'_id' => 0,
],
'sort' => ['length' => -1],
]
);
$expected = [
['filename' => 'b', 'length' => 6],
['filename' => 'a', 'length' => 3],
];
$this->assertSameDocuments($expected, $cursor);
}
public function testGetDatabaseName()
{
$this->assertEquals($this->getDatabaseName(), $this->bucket->getDatabaseName());
}
public function testGetIdFromStream()
{
$id = $this->bucket->uploadFromStream('filename', $this->createStream('foobar'));
$stream = $this->bucket->openDownloadStream($id);
$this->assertEquals($id, $this->bucket->getIdFromStream($stream));
}
/**
* @dataProvider provideInputDataAndExpectedChunks
*/
public function testOpenDownloadStream($input)
{
$id = $this->bucket->uploadFromStream('filename', $this->createStream($input));
$this->assertStreamContents($input, $this->bucket->openDownloadStream($id));
}
/**
* @dataProvider provideInputDataAndExpectedChunks
*/
public function testOpenDownloadStreamAndMultipleReadOperations($input)
{
$id = $this->bucket->uploadFromStream('filename', $this->createStream($input));
$stream = $this->bucket->openDownloadStream($id);
$buffer = '';
while (strlen($buffer) < strlen($input)) {
$expectedReadLength = min(4096, strlen($input) - strlen($buffer));
$buffer .= $read = fread($stream, 4096);
$this->assertInternalType('string', $read);
$this->assertEquals($expectedReadLength, strlen($read));
}
$this->assertTrue(fclose($stream));
$this->assertEquals($input, $buffer);
}
/**
* @expectedException MongoDB\GridFS\Exception\FileNotFoundException
*/
public function testOpenDownloadStreamShouldRequireFileToExist()
{
$this->bucket->openDownloadStream('nonexistent-id');
}
/**
* @expectedException MongoDB\GridFS\Exception\FileNotFoundException
*/
public function testOpenDownloadStreamByNameShouldRequireFilenameToExist()
{
$this->bucket->openDownloadStream('nonexistent-filename');
}
public function testOpenDownloadStreamByName()
{
$this->bucket->uploadFromStream('filename', $this->createStream('foo'));
$this->bucket->uploadFromStream('filename', $this->createStream('bar'));
$this->bucket->uploadFromStream('filename', $this->createStream('baz'));
$this->assertStreamContents('baz', $this->bucket->openDownloadStreamByName('filename'));
$this->assertStreamContents('foo', $this->bucket->openDownloadStreamByName('filename', ['revision' => -3]));
$this->assertStreamContents('bar', $this->bucket->openDownloadStreamByName('filename', ['revision' => -2]));
$this->assertStreamContents('baz', $this->bucket->openDownloadStreamByName('filename', ['revision' => -1]));
$this->assertStreamContents('foo', $this->bucket->openDownloadStreamByName('filename', ['revision' => 0]));
$this->assertStreamContents('bar', $this->bucket->openDownloadStreamByName('filename', ['revision' => 1]));
$this->assertStreamContents('baz', $this->bucket->openDownloadStreamByName('filename', ['revision' => 2]));
}
/**
* @expectedException MongoDB\GridFS\Exception\FileNotFoundException
* @dataProvider provideNonexistentFilenameAndRevision
*/
public function testOpenDownloadStreamByNameShouldRequireFilenameAndRevisionToExist($filename, $revision)
{
$this->bucket->uploadFromStream('filename', $this->createStream('foo'));
$this->bucket->uploadFromStream('filename', $this->createStream('bar'));
$this->bucket->openDownloadStream($filename, ['revision' => $revision]);
}
public function provideNonexistentFilenameAndRevision()
{
return [
['filename', 2],
['filename', -3],
['nonexistent-filename', 0],
['nonexistent-filename', -1],
];
}
public function testOpenUploadStream()
{
$stream = $this->bucket->openUploadStream('filename');
fwrite($stream, 'foobar');
fclose($stream);
$this->assertStreamContents('foobar', $this->bucket->openDownloadStreamByName('filename'));
}
/**
* @dataProvider provideInputDataAndExpectedChunks
*/
public function testOpenUploadStreamAndMultipleWriteOperations($input)
{
$stream = $this->bucket->openUploadStream('filename');
$offset = 0;
while ($offset < strlen($input)) {
$expectedWriteLength = min(4096, strlen($input) - $offset);
$writeLength = fwrite($stream, substr($input, $offset, 4096));
$offset += $writeLength;
$this->assertEquals($expectedWriteLength, $writeLength);
}
$this->assertTrue(fclose($stream));
$this->assertStreamContents($input, $this->bucket->openDownloadStreamByName('filename'));
}
public function testRename()
{
$id = $this->bucket->uploadFromStream('a', $this->createStream('foo'));
$this->bucket->rename($id, 'b');
$fileDocument = $this->filesCollection->findOne(
['_id' => $id],
['projection' => ['filename' => 1, '_id' => 0]]
);
$this->assertSameDocument(['filename' => 'b'], $fileDocument);
$this->assertStreamContents('foo', $this->bucket->openDownloadStreamByName('b'));
}
public function testRenameShouldNotRequireFileToBeModified()
{
$id = $this->bucket->uploadFromStream('a', $this->createStream('foo'));
$this->bucket->rename($id, 'a');
$fileDocument = $this->filesCollection->findOne(
['_id' => $id],
['projection' => ['filename' => 1, '_id' => 0]]
);
$this->assertSameDocument(['filename' => 'a'], $fileDocument);
$this->assertStreamContents('foo', $this->bucket->openDownloadStreamByName('a'));
}
/**
* @expectedException MongoDB\GridFS\Exception\FileNotFoundException
*/
public function testRenameShouldRequireFileToExist()
{
$this->bucket->rename('nonexistent-id', 'b');
}
public function testUploadFromStream()
{
$options = [
'_id' => 'custom-id',
'chunkSizeBytes' => 2,
'metadata' => ['foo' => 'bar'],
];
$id = $this->bucket->uploadFromStream('filename', $this->createStream('foobar'), $options);
$this->assertCollectionCount($this->filesCollection, 1);
$this->assertCollectionCount($this->chunksCollection, 3);
$this->assertSame('custom-id', $id);
$fileDocument = $this->filesCollection->findOne(['_id' => $id]);
$this->assertSameDocument(['foo' => 'bar'], $fileDocument['metadata']);
}
public function testUploadingAnEmptyFile()
{
$id = $this->bucket->uploadFromStream('filename', $this->createStream(''));
$destination = $this->createStream();
$this->bucket->downloadToStream($id, $destination);
$this->assertStreamContents('', $destination);
$this->assertCollectionCount($this->filesCollection, 1);
$this->assertCollectionCount($this->chunksCollection, 0);
$fileDocument = $this->filesCollection->findOne(
['_id' => $id],
[
'projection' => [
'length' => 1,
'md5' => 1,
'_id' => 0,
],
]
);
$expected = [
'length' => 0,
'md5' => 'd41d8cd98f00b204e9800998ecf8427e',
];
$this->assertSameDocument($expected, $fileDocument);
}
public function testUploadingFirstFileCreatesIndexes()
{
$this->bucket->uploadFromStream('filename', $this->createStream('foo'));
$this->assertIndexExists($this->filesCollection->getCollectionName(), 'filename_1_uploadDate_1');
$this->assertIndexExists($this->chunksCollection->getCollectionName(), 'files_id_1_n_1', function(IndexInfo $info) {
$this->assertTrue($info->isUnique());
});
}
/**
* Asserts that a collection with the given name does not exist on the
* server.
*
* @param string $collectionName
*/
private function assertCollectionDoesNotExist($collectionName)
{
$operation = new ListCollections($this->getDatabaseName());
$collections = $operation->execute($this->getPrimaryServer());
$foundCollection = null;
foreach ($collections as $collection) {
if ($collection->getName() === $collectionName) {
$foundCollection = $collection;
break;
}
}
$this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName));
}
/**
*@dataProvider provideInsertChunks
* Asserts that an index with the given name exists for the collection.
*
* An optional $callback may be provided, which should take an IndexInfo
* argument as its first and only parameter. If an IndexInfo matching the
* given name is found, it will be passed to the callback, which may perform
* additional assertions.
*
* @param string $collectionName
* @param string $indexName
* @param callable $callback
*/
public function testProvidedMultipleReads($data)
{
$upload = $this->bucket->openUploadStream("test", ["chunkSizeBytes"=>rand(1, 5)]);
fwrite($upload,$data);
$id = $this->bucket->getIdFromStream($upload);
fclose($upload);
$download = $this->bucket->openDownloadStream($id);
$readPos = 0;
while($readPos < strlen($data)){
$numToRead = rand(1, strlen($data) - $readPos);
$expected = substr($data, $readPos, $numToRead);
$actual = fread($download, $numToRead);
$this->assertEquals($expected,$actual);
$readPos+= $numToRead;
}
$actual = fread($download, 5);
$expected = "";
$this->assertEquals($expected,$actual);
fclose($download);
}
private function generateStream($input)
{
$stream = fopen('php://temp', 'w+');
fwrite($stream, $input);
rewind($stream);
return $stream;
private function assertIndexExists($collectionName, $indexName, $callback = null)
{
if ($callback !== null && ! is_callable($callback)) {
throw new InvalidArgumentException('$callback is not a callable');
}
$operation = new ListIndexes($this->getDatabaseName(), $collectionName);
$indexes = $operation->execute($this->getPrimaryServer());
$foundIndex = null;
foreach ($indexes as $index) {
if ($index->getName() === $indexName) {
$foundIndex = $index;
break;
}
}
$this->assertNotNull($foundIndex, sprintf('Index %s does not exist', $indexName));
if ($callback !== null) {
call_user_func($callback, $foundIndex);
}
}
}
......@@ -2,8 +2,8 @@
namespace MongoDB\Tests\GridFS;
use MongoDB\GridFS;
use MongoDB\Collection;
use MongoDB\GridFS\Bucket;
use MongoDB\Tests\FunctionalTestCase as BaseFunctionalTestCase;
/**
......@@ -12,49 +12,47 @@ use MongoDB\Tests\FunctionalTestCase as BaseFunctionalTestCase;
abstract class FunctionalTestCase extends BaseFunctionalTestCase
{
protected $bucket;
protected $collectionWrapper;
protected $chunksCollection;
protected $filesCollection;
public function setUp()
{
parent::setUp();
foreach(['fs.files', 'fs.chunks'] as $collection){
$col = new Collection($this->manager, $this->getDatabaseName(), $collection);
$col->drop();
}
$this->bucket = new \MongoDB\GridFS\Bucket($this->manager, $this->getDatabaseName());
$this->collectionWrapper = $this->bucket->getCollectionWrapper();
$this->bucket = new Bucket($this->manager, $this->getDatabaseName());
$this->bucket->drop();
$this->chunksCollection = new Collection($this->manager, $this->getDatabaseName(), 'fs.chunks');
$this->filesCollection = new Collection($this->manager, $this->getDatabaseName(), 'fs.files');
}
public function tearDown()
/**
* Asserts that a variable is a stream containing the expected data.
*
* Note: this will seek to the beginning of the stream before reading.
*
* @param string $expectedContents
* @param resource $stream
*/
protected function assertStreamContents($expectedContents, $stream)
{
foreach(['fs.files', 'fs.chunks'] as $collection){
$col = new Collection($this->manager, $this->getDatabaseName(), $collection);
$col->drop();
}
if ($this->hasFailed()) {
return;
}
$this->assertInternalType('resource', $stream);
$this->assertSame('stream', get_resource_type($stream));
$this->assertEquals($expectedContents, stream_get_contents($stream, -1,.0));
}
public function provideInsertChunks()
/**
* Creates an in-memory stream with the given data.
*
* @param string $data
* @return resource
*/
protected function createStream($data = '')
{
$dataVals = [];
$testArgs[][] = "hello world";
$testArgs[][] = "1234567890";
$testArgs[][] = "~!@#$%^&*()_+";
for($j=0; $j<30; $j++){
$randomTest = "";
for($i=0; $i<100; $i++){
$randomTest .= chr(rand(0, 256));
}
$testArgs[][] = $randomTest;
}
$utf8="";
for($i=0; $i<256; $i++){
$utf8 .= chr($i);
}
$testArgs[][]=$utf8;
return $testArgs;
}
$stream = fopen('php://temp', 'w+b');
fwrite($stream, $data);
rewind($stream);
return $stream;
}
}
<?php
namespace MongoDB\Tests\GridFS;
use MongoDB\GridFS;
/**
* Functional tests for the Bucket class.
*/
class GridFSStreamTest extends FunctionalTestCase
{
public function testBasic()
{
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test");
$upload->insertChunks("hello world");
$id = $upload->getId();
$upload->close();
$this->assertEquals(1, $this->collectionWrapper->getFilesCollection()->count());
$this->assertEquals(1, $this->collectionWrapper->getChunksCollection()->count());
$file = $this->collectionWrapper->findFileById($id);
$download = new \MongoDB\GridFS\ReadableStream($this->collectionWrapper, $file);
$stream = fopen('php://temp', 'w+');
$download->downloadToStream($stream);
rewind($stream);
$contents = stream_get_contents($stream);
$this->assertEquals("hello world", $contents);
fclose($stream);
#make sure it's still there!
$download = new \MongoDB\GridFS\ReadableStream($this->collectionWrapper, $file);
$stream = fopen('php://temp', 'w+');
$download->downloadToStream($stream);
rewind($stream);
$contents = stream_get_contents($stream);
$this->assertEquals("hello world", $contents);
fclose($stream);
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test");
$id = $upload->getId();
$upload->close();
$this->assertEquals(2, $this->collectionWrapper->getFilesCollection()->count());
$this->assertEquals(1, $this->collectionWrapper->getChunksCollection()->count());
$file = $this->collectionWrapper->findFileById($id);
$download = new \MongoDB\GridFS\ReadableStream($this->collectionWrapper, $file);
$stream = fopen('php://temp', 'w+');
$download->downloadToStream($stream);
rewind($stream);
$contents = stream_get_contents($stream);
$this->assertEquals("", $contents);
}
public function testMd5()
{
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test");
$upload->insertChunks("hello world\n");
$id = $upload->getId();
$upload->close();
$file = $this->collectionWrapper->findFileById($id);
$this->assertEquals("6f5902ac237024bdd0c176cb93063dc4", $file->md5);
}
public function testUploadDefaultOpts()
{
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test");
$this->assertTrue($upload->getId() instanceof \MongoDB\BSON\ObjectId);
$this->assertTrue($upload->getFile()["uploadDate"] instanceof \MongoDB\BSON\UTCDateTime);
$this->assertEquals($upload->getFile()["filename"], "test");
$this->assertEquals($upload->getLength(),0);
$this->assertTrue(!isset($upload->getFile()["contentType"]));
$this->assertTrue(!isset($upload->getFile()["aliases"]));
$this->assertTrue(!isset($upload->getFile()["metadata"]));
$this->assertEquals(255 * 1024, $upload->getChunkSize());
}
public function testUploadCustomOpts()
{
$options = ["chunkSizeBytes" => 1,
"contentType" => "text/html",
"aliases" => ["foo", "bar"],
"metadata" => ["foo" => 1, "bar" => 2]
];
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test", $options);
$this->assertEquals($upload->getChunkSize(), 1);
$this->assertEquals($upload->getFile()["contentType"], "text/html");
$this->assertEquals($upload->getFile()["aliases"], ["foo", "bar"]);
$this->assertEquals($upload->getFile()["metadata"], ["foo" => 1, "bar" => 2]);
}
public function testDownloadDefaultOpts()
{
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test");
$upload->close();
$file = $this->collectionWrapper->findFileById($upload->getId());
$download = new \MongoDB\GridFS\ReadableStream($this->collectionWrapper, $file);
$download->close();
$this->assertEquals($upload->getId(), $download->getId());
$this->assertEquals(0, $download->getFile()->length);
$this->assertTrue(!isset($download->getFile()->contentType));
$this->assertTrue(!isset($download->getFile()->aliases));
$this->assertTrue(!isset($download->getFile()->metadata));
$this->assertTrue($download->getFile()->uploadDate instanceof \MongoDB\BSON\UTCDateTime);
$this->assertEquals(255 * 1024, $download->getFile()->chunkSize);
$this->assertEquals("d41d8cd98f00b204e9800998ecf8427e", $download->getFile()->md5);
}
public function testDownloadCustomOpts()
{
$options = ["chunkSizeBytes" => 1000,
"contentType" => "text/html",
"aliases" => ["foo", "bar"],
"metadata" => ["foo" => 1, "bar" => 2]
];
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test", $options);
$upload->insertChunks("hello world");
$upload->close();
$file = $this->collectionWrapper->findFileById($upload->getId());
$download = new \MongoDB\GridFS\ReadableStream($this->collectionWrapper, $file);
$this->assertEquals("test", $download->getFile()->filename);
$this->assertEquals($upload->getId(), $download->getId());
$this->assertEquals(11, $download->getFile()->length);
$this->assertEquals("text/html", $download->getFile()->contentType);
$this->assertEquals(1000, $download->getFile()->chunkSize);
$this->assertEquals(["foo", "bar"], $download->getFile()->aliases);
$this->assertEquals(["foo"=> 1, "bar"=> 2], (array) $download->getFile()->metadata);
$this->assertEquals("5eb63bbbe01eeed093cb22bb8f5acdc3", $download->getFile()->md5);
}
/**
*@dataProvider provideInsertChunks
*/
public function testInsertChunks($data)
{
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test");
$upload->insertChunks($data);
$upload->close();
$stream = $this->bucket->openDownloadStream($upload->getId());
$this->assertEquals($data, stream_get_contents($stream));
}
public function testMultiChunkFile()
{
$toUpload="";
for($i=0; $i<255*1024+1000; $i++){
$toUpload .= "a";
}
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test");
$upload->insertChunks($toUpload);
$upload->close();
$this->assertEquals(1, $this->collectionWrapper->getFilesCollection()->count());
$this->assertEquals(2, $this->collectionWrapper->getChunksCollection()->count());
$download = $this->bucket->openDownloadStream($upload->getId());
$this->assertEquals($toUpload, stream_get_contents($download));
}
/**
*@dataProvider provideInsertChunks
*/
public function testSmallChunks($data)
{
$options = ["chunkSizeBytes"=>1];
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test", $options);
$upload->insertChunks($data);
$upload->close();
$this->assertEquals(strlen($data), $this->collectionWrapper->getChunksCollection()->count());
$this->assertEquals(1, $this->collectionWrapper->getFilesCollection()->count());
$stream = $this->bucket->openDownloadStream($upload->getId());
$this->assertEquals($data, stream_get_contents($stream));
}
public function testMultipleReads()
{
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test", ["chunkSizeBytes"=>3]);
$upload->insertChunks("hello world");
$upload->close();
$file = $this->collectionWrapper->findFileById($upload->getId());
$download = new \MongoDB\GridFS\ReadableStream($this->collectionWrapper, $file);
$this->assertEquals("he", $download->downloadNumBytes(2));
$this->assertEquals("ll", $download->downloadNumBytes(2));
$this->assertEquals("o ", $download->downloadNumBytes(2));
$this->assertEquals("wo", $download->downloadNumBytes(2));
$this->assertEquals("rl", $download->downloadNumBytes(2));
$this->assertEquals("d", $download->downloadNumBytes(2));
$this->assertEquals("", $download->downloadNumBytes(2));
$this->assertEquals("", $download->downloadNumBytes(2));
$download->close();
}
/**
*@dataProvider provideInsertChunks
*/
public function testProvidedMultipleReads($data)
{
$upload = new \MongoDB\GridFS\WritableStream($this->collectionWrapper, "test", ["chunkSizeBytes"=>rand(1, 5)]);
$upload->insertChunks($data);
$upload->close();
$file = $this->collectionWrapper->findFileById($upload->getId());
$download = new \MongoDB\GridFS\ReadableStream($this->collectionWrapper, $file);
$readPos = 0;
while($readPos < strlen($data)){
$numToRead = rand(1, strlen($data) - $readPos);
$expected = substr($data, $readPos, $numToRead);
$actual = $download->downloadNumBytes($numToRead);
$this->assertEquals($expected,$actual);
$readPos+= $numToRead;
}
$actual = $download->downloadNumBytes(5);
$expected = "";
$this->assertEquals($expected,$actual);
$download->close();
}
/**
* @expectedException \MongoDB\Exception\InvalidArgumentException
* @dataProvider provideInvalidUploadConstructorOptions
*/
public function testUploadConstructorOptionTypeChecks(array $options)
{
new \MongoDB\GridFS\WritableStream($this->collectionWrapper,"test", $options);
}
public function provideInvalidUploadConstructorOptions()
{
$options = [];
$invalidContentType = [123, 3.14, true, [], new \stdClass];
$invalidAliases = ['foo', 3.14, true, [12, 34], new \stdClass];
$invalidMetadata = ['foo', 3.14, true];
foreach ($invalidContentType as $value) {
$options[][] = ['contentType' => $value];
}
foreach ($invalidAliases as $value) {
$options[][] = ['aliases' => $value];
}
foreach ($invalidMetadata as $value) {
$options[][] = ['metadata' => $value];
}
return $options;
}
}
<?php
namespace MongoDB\Tests\GridFS;
use MongoDB\BSON\Binary;
use MongoDB\GridFS\CollectionWrapper;
use MongoDB\GridFS\ReadableStream;
/**
* Functional tests for the internal ReadableStream class.
*/
class ReadableStreamFunctionalTest extends FunctionalTestCase
{
private $collectionWrapper;
public function setUp()
{
parent::setUp();
$this->collectionWrapper = new CollectionWrapper($this->manager, $this->getDatabaseName(), 'fs');
$this->filesCollection->insertMany([
['_id' => 'length-0', 'length' => 0, 'chunkSize' => 4],
['_id' => 'length-0-with-empty-chunk', 'length' => 0, 'chunkSize' => 4],
['_id' => 'length-2', 'length' => 2, 'chunkSize' => 4],
['_id' => 'length-8', 'length' => 8, 'chunkSize' => 4],
['_id' => 'length-10', 'length' => 10, 'chunkSize' => 4],
]);
$this->chunksCollection->insertMany([
['_id' => 1, 'files_id' => 'length-0-with-empty-chunk', 'n' => 0, 'data' => new Binary('', Binary::TYPE_GENERIC)],
['_id' => 2, 'files_id' => 'length-2', 'n' => 0, 'data' => new Binary('ab', Binary::TYPE_GENERIC)],
['_id' => 3, 'files_id' => 'length-8', 'n' => 0, 'data' => new Binary('abcd', Binary::TYPE_GENERIC)],
['_id' => 4, 'files_id' => 'length-8', 'n' => 1, 'data' => new Binary('efgh', Binary::TYPE_GENERIC)],
['_id' => 5, 'files_id' => 'length-10', 'n' => 0, 'data' => new Binary('abcd', Binary::TYPE_GENERIC)],
['_id' => 6, 'files_id' => 'length-10', 'n' => 1, 'data' => new Binary('efgh', Binary::TYPE_GENERIC)],
['_id' => 7, 'files_id' => 'length-10', 'n' => 2, 'data' => new Binary('ij', Binary::TYPE_GENERIC)],
]);
}
public function testValidConstructorFileDocument()
{
new ReadableStream($this->collectionWrapper, (object) ['_id' => null, 'chunkSize' => 1, 'length' => 0]);
}
/**
* @expectedException MongoDB\GridFS\Exception\CorruptFileException
* @dataProvider provideInvalidConstructorFileDocuments
*/
public function testConstructorFileDocumentChecks($file)
{
new ReadableStream($this->collectionWrapper, $file);
}
public function provideInvalidConstructorFileDocuments()
{
$options = [];
foreach ($this->getInvalidIntegerValues() as $value) {
$options[][] = (object) ['_id' => 1, 'chunkSize' => $value, 'length' => 0];
}
foreach ($this->getInvalidIntegerValues() as $value) {
$options[][] = (object) ['_id' => 1, 'chunkSize' => 1, 'length' => $value];
}
$options[][] = (object) ['_id' => 1, 'chunkSize' => 0, 'length' => 0];
$options[][] = (object) ['_id' => 1, 'chunkSize' => 1, 'length' => -1];
$options[][] = (object) ['chunkSize' => 1, 'length' => 0];
return $options;
}
/**
* @dataProvider provideFileIdAndExpectedBytes
*/
public function testDownloadNumBytes($fileId, $numBytes, $expectedBytes)
{
$fileDocument = $this->collectionWrapper->findFileById($fileId);
$stream = new ReadableStream($this->collectionWrapper, $fileDocument);
$this->assertSame($expectedBytes, $stream->downloadNumBytes($numBytes));
}
public function provideFileIdAndExpectedBytes()
{
return [
['length-0', 0, ''],
['length-0', 2, ''],
['length-0-with-empty-chunk', 0, ''],
['length-0-with-empty-chunk', 2, ''],
['length-2', 0, ''],
['length-2', 2, 'ab'],
['length-2', 4, 'ab'],
['length-8', 0, ''],
['length-8', 2, 'ab'],
['length-8', 4, 'abcd'],
['length-8', 6, 'abcdef'],
['length-8', 8, 'abcdefgh'],
['length-8', 10, 'abcdefgh'],
['length-10', 0, ''],
['length-10', 2, 'ab'],
['length-10', 4, 'abcd'],
['length-10', 6, 'abcdef'],
['length-10', 8, 'abcdefgh'],
['length-10', 10, 'abcdefghij'],
['length-10', 12, 'abcdefghij'],
];
}
/**
* @dataProvider provideFileIdAndExpectedBytes
*/
public function testDownloadNumBytesCalledMultipleTimes($fileId, $numBytes, $expectedBytes)
{
$fileDocument = $this->collectionWrapper->findFileById($fileId);
$stream = new ReadableStream($this->collectionWrapper, $fileDocument);
for ($i = 0; $i < $numBytes; $i++) {
$expectedByte = isset($expectedBytes[$i]) ? $expectedBytes[$i] : '';
$this->assertSame($expectedByte, $stream->downloadNumBytes(1));
}
}
/**
* @expectedException MongoDB\GridFS\Exception\CorruptFileException
* @expectedExceptionMessage Chunk not found for index "2"
*/
public function testDownloadNumBytesWithMissingChunk()
{
$this->chunksCollection->deleteOne(['files_id' => 'length-10', 'n' => 2]);
$fileDocument = $this->collectionWrapper->findFileById('length-10');
$stream = new ReadableStream($this->collectionWrapper, $fileDocument);
$stream->downloadNumBytes(10);
}
/**
* @expectedException MongoDB\GridFS\Exception\CorruptFileException
* @expectedExceptionMessage Expected chunk to have index "1" but found "2"
*/
public function testDownloadNumBytesWithUnexpectedChunkIndex()
{
$this->chunksCollection->deleteOne(['files_id' => 'length-10', 'n' => 1]);
$fileDocument = $this->collectionWrapper->findFileById('length-10');
$stream = new ReadableStream($this->collectionWrapper, $fileDocument);
$stream->downloadNumBytes(10);
}
/**
* @expectedException MongoDB\GridFS\Exception\CorruptFileException
* @expectedExceptionMessage Expected chunk to have size "2" but found "1"
*/
public function testDownloadNumBytesWithUnexpectedChunkSize()
{
$this->chunksCollection->updateOne(
['files_id' => 'length-10', 'n' => 2],
['$set' => ['data' => new Binary('i', Binary::TYPE_GENERIC)]]
);
$fileDocument = $this->collectionWrapper->findFileById('length-10');
$stream = new ReadableStream($this->collectionWrapper, $fileDocument);
$stream->downloadNumBytes(10);
}
/**
* @expectedException MongoDB\Exception\InvalidArgumentException
*/
public function testDownloadNumBytesWithNegativeReadSize()
{
$fileDocument = $this->collectionWrapper->findFileById('length-0');
$stream = new ReadableStream($this->collectionWrapper, $fileDocument);
$stream->downloadNumBytes(-1);
}
}
<?php
namespace MongoDB\Tests\GridFS;
use MongoDB\Collection;
use MongoDB\BSON\Binary;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Exception\RuntimeException;
use MongoDB\Operation\BulkWrite;
use DateTime;
use IteratorIterator;
use LogicException;
use MultipleIterator;
/**
* GridFS spec functional tests.
*
* @see https://github.com/mongodb/specifications/tree/master/source/gridfs/tests
*/
class SpecFunctionalTest extends FunctionalTestCase
{
private $expectedChunksCollection;
private $expectedFilesCollection;
public function setUp()
{
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();
}
/**
* @dataProvider provideSpecificationTests
*/
public function testSpecification(array $initialData, array $test)
{
$this->setName(str_replace(' ', '_', $test['description']));
$this->initializeData($initialData);
if (isset($test['arrange'])) {
foreach ($test['arrange']['data'] as $dataModification) {
$this->executeDataModification($dataModification);
}
}
try {
$result = $this->executeAct($test['act']);
} catch (RuntimeException $e) {
$result = $e;
}
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) {
$testArgs[] = [$json['data'], $test];
}
}
return $testArgs;
}
/**
* Assert that the collections contain equivalent documents.
*
* This method will resolve references within the expected collection's
* documents before comparing documents. Occurrences of "*result" in the
* expected collection's documents will be replaced with the actual result.
* Occurrences of "*actual" in the expected collection's documents will be
* replaced with the corresponding value in the actual collection's document
* being compared.
*
* @param Collection $expectedCollection
* @param Collection $actualCollection
* @param mixed $actualResult
*/
private function assertEquivalentCollections($expectedCollection, $actualCollection, $actualResult)
{
$mi = new MultipleIterator;
$mi->attachIterator(new IteratorIterator($expectedCollection->find()));
$mi->attachIterator(new IteratorIterator($actualCollection->find()));
foreach ($mi as $documents) {
list($expectedDocument, $actualDocument) = $documents;
array_walk($expectedDocument, function(&$value) use ($actualResult) {
if ($value === '*result') {
$value = $actualResult;
}
});
array_walk($expectedDocument, function(&$value, $key) use ($actualDocument) {
if ( ! is_string($value)) {
return;
}
if ( ! strncmp($value, '*actual_', 8)) {
$value = $actualDocument[$key];
}
});
$this->assertSameDocument($expectedDocument, $actualDocument);
}
}
/**
* Convert encoded types in the array and return the modified array.
*
* Nested arrays with "$oid" and "$date" keys will be converted to ObjectID
* and UTCDateTime instances, respectively. Nested arrays with "$hex" keys
* will be converted to a string or Binary object.
*
* @param param $data
* @param boolean $createBinary If true, convert "$hex" values to a Binary
* @return array
*/
private function convertTypes(array $data, $createBinary = true)
{
/* array_walk_recursive() only visits leaf nodes within the array, so we
* need to manually recurse.
*/
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'])) {
// TODO: This is necessary until PHPC-536 is implemented
$milliseconds = floor((new DateTime($value['$date']))->format('U.u') * 1000);
$value = new UTCDateTime($milliseconds);
return;
}
$value = $this->convertTypes($value, $createBinary);
});
return $data;
}
/**
* Executes an "act" block.
*
* @param array $act
* @return mixed
* @throws LogicException if the operation is unsupported
*/
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']);
}
}
/**
* Executes an "assert" block.
*
* @param array $assert
* @param mixed $actualResult
* @return mixed
* @throws FileNotFoundException
* @throws LogicException if the operation is unsupported
*/
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;
}
/* Since "*actual" may be used for an expected document's "_id", append
* a unique value to avoid duplicate key exceptions.
*/
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);
}
/**
* Executes the "result" section of an "assert" block.
*
* @param mixed $expectedResult
* @param mixed $actualResult
* @param array $data
* @throws LogicException if the result assertion is unsupported
*/
private function executeAssertResult($expectedResult, $actualResult)
{
if ($expectedResult === 'void') {
return $this->assertNull($actualResult);
}
if ($expectedResult === '&result') {
// Do nothing; assertEquivalentCollections() will handle this
return;
}
if (isset($expectedResult['$hex'])) {
return $this->assertSame(hex2bin($expectedResult['$hex']), $actualResult);
}
throw new LogicException('Unsupported result assertion: ' . var_export($expectedResult, true));
}
/**
* Executes a data modification from an "arrange" or "assert" block.
*
* @param array $dataModification
* @return mixed
* @throws LogicException if the operation or collection is unsupported
*/
private function executeDataModification(array $dataModification)
{
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());
}
/**
* Returns the exception class for the "error" section of an "assert" block.
*
* @param string $error
* @return string
* @throws LogicException if the error is unsupported
*/
private function getExceptionClassForError($error)
{
switch ($error) {
case 'FileNotFound':
case 'RevisionNotFound':
return 'MongoDB\GridFS\Exception\FileNotFoundException';
case 'ChunkIsMissing':
case 'ChunkIsWrongSize':
return 'MongoDB\GridFS\Exception\CorruptFileException';
default:
throw new LogicException('Unsupported error: ' . $error);
}
}
/**
* Initializes data in the files and chunks collections.
*
* @param array $data
*/
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']);
}
}
}
GridFS Tests
============
The YAML and JSON files in this directory are platform-independent tests
meant to exercise a driver's implementation of GridFS.
Converting to JSON
==================
The tests are written in YAML because it is easier for humans to write
and read, and because YAML supports a standard comment format. Each test
is also provided in JSON format because in some languages it is easier
to parse JSON than YAML.
If you modify any test, you should modify the YAML file and then
regenerate the JSON file from it.
One way to convert the files is using an online web page. I used:
http://www.json2yaml.com/
It's advertised as a JSON to YAML converter but it can be used in either direction.
Note: the yaml2json utility from npm is not capable of converting these YAML tests
because it doesn't implement the full YAML spec.
Format
======
Each test file has two top level sections:
1. data
2. tests
The data section defines the initial contents of the files and chunks
collections for all tests in that file.
The tests section defines the tests to be run. The format of the tests
section will vary slightly depending on what tests are being defined.
In general, they will have the following sections:
1. description
2. arrange
3. act
4. assert
The arrange section, if present, defines changes to be made to the
initial contents of the files and chunks collections (as defined by
the data section) before this particular test is run. These changes
are described in the form of write commands that can be sent directly
to MongoDB.
The act section defines what operation (with which arguments) should
be performed.
The assert section defines what should be true at the end of the test.
This includes checking the return value of the operation, as well as
checking the expected contents of the files and chunks collections. The
expected contents of the files and chunks collections are described
in the form of write commands that modify collections named
expected.files and expected.chunks. Before running these commands,
load the initial files and chunks documents into the expected.files
and expected.chunks collections and then run the commands. At that point
you can assert that fs.files and expected.files are the same, and that
expected.chunks and fs.chunks are the same.
For operations that are expected to succeed the assert section contains
a "result" element describing the expected result. For operations
that are expected to fail the assert section contains an "error"
element describing the expected failure.
The "result" element is either the expected result when it is possible to
know the result in advance, or it is the special value "&result"
which means that we expect a result (not a failure) but the actual
value of the result could be anything. The notation "&result" is
modeled after YAML syntax for defining an anchor, and the
result value may be referenced later in the assert section as
"*result".
Another special notation in the assert section is "*actual", which
is used when the value of a field cannot be known in advance of the
test, so the assert logic should accept whatever the actual value
ended up being.
data:
files:
-
_id: { "$oid" : "000000000000000000000001" }
length: 0
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "d41d8cd98f00b204e9800998ecf8427e"
filename: "length-0"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000002" }
length: 0
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "d41d8cd98f00b204e9800998ecf8427e"
filename: "length-0-with-empty-chunk"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000003" }
length: 2
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "c700ed4fdb1d27055aa3faa2c2432283"
filename: "length-2"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000004" }
length: 8
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "dd254cdc958e53abaa67da9f797125f5"
filename: "length-8"
contentType: "application/octet-stream"
aliases: []
metadata: {}
chunks:
- { _id : { "$oid" : "000000000000000000000001" }, files_id : { "$oid" : "000000000000000000000002" }, n : 0, data : { $hex : "" } }
- { _id : { "$oid" : "000000000000000000000002" }, files_id : { "$oid" : "000000000000000000000003" }, n : 0, data : { $hex : "1122" } }
- { _id : { "$oid" : "000000000000000000000003" }, files_id : { "$oid" : "000000000000000000000004" }, n : 0, data : { $hex : "11223344" } }
- { _id : { "$oid" : "000000000000000000000004" }, files_id : { "$oid" : "000000000000000000000004" }, n : 1, data : { $hex : "55667788" } }
tests:
-
description: "Delete when length is 0"
act:
operation: delete
arguments:
id: { "$oid" : "000000000000000000000001" }
assert:
result: void
data:
-
{ delete : "expected.files", deletes : [
{ q : { _id : { "$oid" : "000000000000000000000001" } }, limit : 1 }
] }
-
description: "Delete when length is 0 and there is one extra empty chunk"
act:
operation: delete
arguments:
id: { "$oid" : "000000000000000000000002" }
assert:
result: void
data:
-
{ delete : "expected.files", deletes : [
{ q : { _id : { "$oid" : "000000000000000000000002" } }, limit : 1 }
] }
-
{ delete : "expected.chunks", deletes : [
{ q : { files_id : { "$oid" : "000000000000000000000002" } }, limit : 0 }
] }
-
description: "Delete when length is 8"
act:
operation: delete
arguments:
id: { "$oid" : "000000000000000000000004" }
assert:
result: void
data:
-
{ delete : "expected.files", deletes : [
{ q : { _id : { "$oid" : "000000000000000000000004" } }, limit : 1 }
] }
-
{ delete : "expected.chunks", deletes : [
{ q : { files_id : { "$oid" : "000000000000000000000004" } }, limit : 0 }
] }
-
description: "Delete when files entry does not exist"
act:
operation: delete
arguments:
id: { "$oid" : "000000000000000000000000" }
assert:
error: "FileNotFound"
-
description: "Delete when files entry does not exist and there are orphaned chunks"
arrange:
data:
-
{ delete : "fs.files", deletes : [
{ q : { _id : { "$oid" : "000000000000000000000004" } }, limit : 1 }
] }
act:
operation: delete
arguments:
id: { "$oid" : "000000000000000000000004" }
assert:
error: "FileNotFound"
data:
-
{ delete : "expected.files", deletes : [
{ q : { _id : { "$oid" : "000000000000000000000004" } }, limit : 1 }
] }
-
{ delete : "expected.chunks", deletes : [
{ q : { files_id : { "$oid" : "000000000000000000000004" } }, limit : 0 }
] }
data:
files:
-
_id: { "$oid" : "000000000000000000000001" }
length: 0
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "d41d8cd98f00b204e9800998ecf8427e"
filename: "length-0"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000002" }
length: 0
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "d41d8cd98f00b204e9800998ecf8427e"
filename: "length-0-with-empty-chunk"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000003" }
length: 2
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "c700ed4fdb1d27055aa3faa2c2432283"
filename: "length-2"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000004" }
length: 8
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "dd254cdc958e53abaa67da9f797125f5"
filename: "length-8"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000005" }
length: 10
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "57d83cd477bfb1ccd975ab33d827a92b"
filename: "length-10"
contentType: "application/octet-stream"
aliases: []
metadata: {}
chunks:
- { _id : { "$oid" : "000000000000000000000001" }, files_id : { "$oid" : "000000000000000000000002" }, n : 0, data : { $hex : "" } }
- { _id : { "$oid" : "000000000000000000000002" }, files_id : { "$oid" : "000000000000000000000003" }, n : 0, data : { $hex : "1122" } }
- { _id : { "$oid" : "000000000000000000000003" }, files_id : { "$oid" : "000000000000000000000004" }, n : 0, data : { $hex : "11223344" } }
- { _id : { "$oid" : "000000000000000000000004" }, files_id : { "$oid" : "000000000000000000000004" }, n : 1, data : { $hex : "55667788" } }
- { _id : { "$oid" : "000000000000000000000005" }, files_id : { "$oid" : "000000000000000000000005" }, n : 0, data : { $hex : "11223344" } }
- { _id : { "$oid" : "000000000000000000000006" }, files_id : { "$oid" : "000000000000000000000005" }, n : 1, data : { $hex : "55667788" } }
- { _id : { "$oid" : "000000000000000000000007" }, files_id : { "$oid" : "000000000000000000000005" }, n : 2, data : { $hex : "99aa" } }
tests:
-
description: "Download when length is zero"
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000001" }
options: { }
assert:
result: { $hex : "" }
-
description: "Download when length is zero and there is one empty chunk"
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000002" }
options: { }
assert:
result: { $hex : "" }
-
description: "Download when there is one chunk"
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000003" }
options: { }
assert:
result: { $hex : "1122" }
-
description: "Download when there are two chunks"
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000004" }
options: { }
assert:
result: { $hex : "1122334455667788" }
-
description: "Download when there are three chunks"
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000005" }
options: { }
assert:
result: { $hex : "112233445566778899aa" }
-
description: "Download when files entry does not exist"
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000000" }
options: { }
assert:
error: "FileNotFound"
-
description: "Download when an intermediate chunk is missing"
arrange:
data:
-
{ delete : "fs.chunks", deletes : [
{ q : { files_id : { "$oid" : "000000000000000000000005" }, n : 1 }, limit : 1 }
] }
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000005" }
assert:
error: "ChunkIsMissing"
-
description: "Download when final chunk is missing"
arrange:
data:
-
{ delete : "fs.chunks", deletes : [
{ q : { files_id : { "$oid" : "000000000000000000000005" }, n : 1 }, limit : 1 }
] }
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000005" }
assert:
error: "ChunkIsMissing"
-
description: "Download when an intermediate chunk is the wrong size"
arrange:
data:
-
{ update : "fs.chunks", updates : [
{ q : { files_id : { "$oid" : "000000000000000000000005" }, n : 1 }, u : { $set : { data : { $hex : "556677" } } } },
{ q : { files_id : { "$oid" : "000000000000000000000005" }, n : 2 }, u : { $set : { data : { $hex : "8899aa" } } } }
] }
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000005" }
assert:
error: "ChunkIsWrongSize"
-
description: "Download when final chunk is the wrong size"
arrange:
data:
-
{ update : "fs.chunks", updates : [
{ q : { files_id : { "$oid" : "000000000000000000000005" }, n : 2 }, u : { $set : { data : { $hex : "99" } } } }
] }
act:
operation: download
arguments:
id: { "$oid" : "000000000000000000000005" }
assert:
error: "ChunkIsWrongSize"
data:
files:
-
_id: { "$oid" : "000000000000000000000001" }
length: 1
chunkSize: 4
uploadDate: { "$date" : "1970-01-01T00:00:00.000Z" }
md5: "47ed733b8d10be225eceba344d533586"
filename: "abc"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000002" }
length: 1
chunkSize: 4
uploadDate: { "$date" : "1970-01-02T00:00:00.000Z" }
md5: "b15835f133ff2e27c7cb28117bfae8f4"
filename: "abc"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000003" }
length: 1
chunkSize: 4
uploadDate: { "$date" : "1970-01-03T00:00:00.000Z" }
md5: "eccbc87e4b5ce2fe28308fd9f2a7baf3"
filename: "abc"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000004" }
length: 1
chunkSize: 4
uploadDate: { "$date" : "1970-01-04T00:00:00.000Z" }
md5: "f623e75af30e62bbd73d6df5b50bb7b5"
filename: "abc"
contentType: "application/octet-stream"
aliases: []
metadata: {}
-
_id: { "$oid" : "000000000000000000000005" }
length: 1
chunkSize: 4
uploadDate: { "$date" : "1970-01-05T00:00:00.000Z" }
md5: "4c614360da93c0a041b22e537de151eb"
filename: "abc"
contentType: "application/octet-stream"
aliases: []
metadata: {}
chunks:
- { _id : { "$oid" : "000000000000000000000001" }, files_id : { "$oid" : "000000000000000000000001" }, n : 0, data : { $hex : "11" } }
- { _id : { "$oid" : "000000000000000000000002" }, files_id : { "$oid" : "000000000000000000000002" }, n : 0, data : { $hex : "22" } }
- { _id : { "$oid" : "000000000000000000000003" }, files_id : { "$oid" : "000000000000000000000003" }, n : 0, data : { $hex : "33" } }
- { _id : { "$oid" : "000000000000000000000004" }, files_id : { "$oid" : "000000000000000000000004" }, n : 0, data : { $hex : "44" } }
- { _id : { "$oid" : "000000000000000000000005" }, files_id : { "$oid" : "000000000000000000000005" }, n : 0, data : { $hex : "55" } }
tests:
-
description: "Download_by_name when revision is 0"
act:
operation: download_by_name
arguments:
filename: "abc"
options: { revision : 0 }
assert:
result: { $hex : "11" }
-
description: "Download_by_name when revision is 1"
act:
operation: download_by_name
arguments:
filename: "abc"
options: { revision : 1 }
assert:
result: { $hex : "22" }
-
description: "Download_by_name when revision is -2"
act:
operation: download_by_name
arguments:
filename: "abc"
options: { revision : -2 }
assert:
result: { $hex : "44" }
-
description: "Download_by_name when revision is -1"
act:
operation: download_by_name
arguments:
filename: "abc"
options: { revision : -1 }
assert:
result: { $hex : "55" }
-
description: "Download_by_name when files entry does not exist"
act:
operation: download_by_name
arguments:
filename: "xyz"
assert:
error: "FileNotFound"
-
description: "Download_by_name when revision does not exist"
act:
operation: download_by_name
arguments:
filename: "abc"
options: { revision : 999 }
assert:
error: "RevisionNotFound"
data:
files: []
chunks: []
tests:
-
description: "Upload when length is 0"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "" }
options: { chunkSizeBytes : 4 }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 0, chunkSize : 4, uploadDate : "*actual", md5 : "d41d8cd98f00b204e9800998ecf8427e", filename : "filename" }
] }
-
description: "Upload when length is 1"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "11" }
options: { chunkSizeBytes : 4 }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 1, chunkSize : 4, uploadDate : "*actual", md5 : "47ed733b8d10be225eceba344d533586", filename : "filename" }
] }
-
{ insert : "expected.chunks", documents : [
{ _id : "*actual", files_id : "*result", n : 0, data : { $hex : "11" } }
] }
-
description: "Upload when length is 3"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "112233" }
options: { chunkSizeBytes : 4 }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 3, chunkSize : 4, uploadDate : "*actual", md5 : "bafae3a174ab91fc70db7a6aa50f4f52", filename : "filename" }
] }
-
{ insert : "expected.chunks", documents : [
{ _id : "*actual", files_id : "*result", n : 0, data : { $hex : "112233" } }
] }
-
description: "Upload when length is 4"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "11223344" }
options: { chunkSizeBytes : 4 }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 4, chunkSize : 4, uploadDate : "*actual", md5 : "7e7c77cff5705d1f7574a25ef6662117", filename : "filename" }
] }
-
{ insert : "expected.chunks", documents : [
{ _id : "*actual", files_id : "*result", n : 0, data : { $hex : "11223344" } }
] }
-
description: "Upload when length is 5"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "1122334455" }
options: { chunkSizeBytes : 4 }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 5, chunkSize : 4, uploadDate : "*actual", md5 : "283d4fea5dded59cf837d3047328f5af", filename : "filename" }
] }
-
{ insert : "expected.chunks", documents : [
{ _id : "*actual", files_id : "*result", n : 0, data : { $hex : "11223344" } },
{ _id : "*actual", files_id : "*result", n : 1, data : { $hex : "55" } }
] }
-
description: "Upload when length is 8"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "1122334455667788" }
options: { chunkSizeBytes : 4 }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 8, chunkSize : 4, uploadDate : "*actual", md5 : "dd254cdc958e53abaa67da9f797125f5", filename : "filename" }
] }
-
{ insert : "expected.chunks", documents : [
{ _id : "*actual", files_id : "*result", n : 0, data : { $hex : "11223344" } },
{ _id : "*actual", files_id : "*result", n : 1, data : { $hex : "55667788" } }
] }
-
description: "Upload when contentType is provided"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "11" }
options: { chunkSizeBytes : 4, contentType : "image/jpeg" }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 1, chunkSize : 4, uploadDate : "*actual", md5 : "47ed733b8d10be225eceba344d533586", filename : "filename", contentType : "image/jpeg" }
] }
-
{ insert : "expected.chunks", documents : [
{ _id : "*actual", files_id : "*result", n : 0, data : { $hex : "11" } }
] }
-
description: "Upload when metadata is provided"
act:
operation: upload
arguments:
filename: "filename"
source: { $hex : "11" }
options:
chunkSizeBytes: 4
metadata: { x : 1 }
assert:
result: "&result"
data:
-
{ insert : "expected.files", documents : [
{ _id : "*result", length : 1, chunkSize : 4, uploadDate : "*actual", md5 : "47ed733b8d10be225eceba344d533586", filename : "filename", metadata : { x : 1 } }
] }
-
{ insert : "expected.chunks", documents : [
{ _id : "*actual", files_id : "*result", n : 0, data : { $hex : "11" } }
] }
<?php
namespace MongoDB\Tests\GridFS;
use \MongoDB\GridFS;
use \MongoDB\Collection;
use \MongoDB\BSON\ObjectId;
use \MongoDB\BSON\Binary;
use \MongoDB\Exception;
class SpecificationTests extends FunctionalTestCase
{
private $commands;
private $collections;
public function setUp()
{
parent::setUp();
$this->commands = array(
'insert' => function($col, $docs) {
$col->insertMany($docs['documents']);},
'update' => function($col, $docs) {
foreach($docs['updates'] as $update) {
$col->updateMany($update['q'], $update['u']);
}
},
'delete' => function($col, $docs){
foreach($docs['deletes'] as $delete){
$col->deleteMany($delete['q']);
}
}
);
}
/**
*@dataProvider provideSpecificationTests
*/
public function testSpecificationTests($testJson)
{
foreach ($testJson['tests'] as $test) {
$this->initializeDatabases($testJson['data'], $test);
if(isset($test['act']['arguments']['options'])){
$options = $test['act']['arguments']['options'];
} else {
$options =[];
}
$this->bucket = new \MongoDB\GridFS\Bucket($this->manager, $this->getDatabaseName(), $this->fixTypes($options,false));
$func = $test['act']['operation'] . "Command";
$error = null;
try {
$result = $this->$func($test['act']['arguments']);
} catch(\MongoDB\Exception\Exception $e) {
$error = $e;
}
$errors = ['FileNotFound' => '\MongoDB\GridFS\Exception\FileNotFoundException',
'ChunkIsMissing' => '\MongoDB\GridFS\Exception\CorruptFileException',
'ExtraChunk' => '\MongoDB\GridFS\Exception\CorruptFileException',
'ChunkIsWrongSize' => '\MongoDB\GridFS\Exception\CorruptFileException',
'RevisionNotFound' => '\MongoDB\GridFS\Exception\FileNotFoundException'
];
if (!isset($test['assert']['error'])) {
$this->assertNull($error);
} else {
$shouldError = $test['assert']['error'];
$this->assertTrue($error instanceof $errors[$shouldError]);
}
if (isset($test['assert']['result'])) {
$testResult = $test['assert']['result'];
if ($testResult == '&result') {
$test['assert']['result'] = $result;
}
if ($testResult == "void") {
$test['assert']['result'] = null;
}
$fixedAssertFalse = $this->fixTypes($test['assert'], false);
$this->assertEquals($result, $fixedAssertFalse['result']);
}
$fixedAssertTrue = $this->fixTypes($test['assert'], true);
if (isset($test['assert']['data'])) {
$this->runCommands($fixedAssertTrue['data'], $result);
$this->collectionsEqual($this->collections['expected.files'],$this->bucket->getCollectionWrapper()->getFilesCollection());
if(isset($this->collections['expected.chunks'])) {
$this->collectionsEqual($this->collections['expected.chunks'],$this->bucket->getCollectionWrapper()->getChunksCollection());
}
}
}
}
public function provideSpecificationTests()
{
$testPath= __DIR__.'/Specification/tests/*.json';
$testArgs = [];
foreach(glob($testPath) as $filename) {
$fileContents = file_get_contents($filename);
$testJson = json_decode($fileContents, true);
$testArgs[][] = $testJson;
}
return $testArgs;
}
public function fixTypes($testJson, $makeBinary)
{
$result = $testJson;
foreach($result as $key =>$value) {
if (is_array($value) && isset($value['$hex'])) {
$result[$key] = hex2bin($value['$hex']);
if($makeBinary) {
$result[$key] = new \MongoDB\BSON\Binary($result[$key], \MongoDB\BSON\Binary::TYPE_GENERIC);
}
} else if (is_array($value) && isset($value['$oid'])) {
$result[$key] = new \MongoDB\BSON\ObjectId("".$value['$oid']);
} else if (is_array($value)) {
$result[$key] = $this->fixTypes($result[$key], $makeBinary);
} else if(is_string($value) && $value == '*actual') {
unset($result[$key]);
}
}
return $result;
}
public function collectionsEqual($col1, $col2)
{
$docs1 = $this->filterDoc($col1, true);
$docs2 = $this->filterDoc($col2, true);
$this->assertSameDocuments($docs1, $docs2);
}
public function filterDoc($collection, $ignoreId)
{
$output = [];
$documents = $collection->find();
foreach($documents as $doc){
if ($ignoreId) {
unset($doc->_id);
}
if(isset($doc->uploadDate)) {
// $this->assertTrue($doc->uploadDate instanceof DateTime);
unset($doc->uploadDate);
}
$output [] = $doc;
}
return $output;
}
public function runCommands($cmds, $result)
{
foreach($cmds as $cmd){
foreach($cmd as $key => $value) {
if(isset($this->commands[$key])) {
$cmdName = $key;
$collectionName = $value;
if(isset($cmd['documents'])){
foreach($cmd['documents'] as $docIndex => $doc) {
foreach($doc as $docKey => $docVal){
if(is_string($docVal)) {
if($docVal == '*result') {
$doc[$docKey] = $result;
}
}
}
$cmd['documents'][$docIndex] = $doc;
}
}
$collection = new Collection($this->manager, $this->getDatabaseName(), $collectionName);
$this->commands[$key]($collection, $this->fixTypes($cmd, true));
$this->collections[$collectionName] = $collection;
}
}
}
}
public function initializeDatabases($data, $test)
{
$collectionsToDrop = ['fs.files','fs.chunks','expected.files','expected.chunks'];
$data = $this->fixTypes($data, true);
foreach ($collectionsToDrop as $collectionName) {
$collection = new Collection($this->manager, $this->getDatabaseName(), $collectionName);
$collection->drop();
}
if (isset($data['files']) && count($data['files']) > 0) {
$filesCollection = new Collection($this->manager, $this->getDatabaseName(), "fs.files");
$filesCollection->insertMany($data['files']);
$expectedFilesCollection = new Collection($this->manager, $this->getDatabaseName(), "expected.files");
$expectedFilesCollection->insertMany($data['files']);
$this->collections['expected.files'] = $expectedFilesCollection;
}
if (isset($data['chunks']) && count($data['chunks']) > 0) {
$chunksCollection = new Collection($this->manager, $this->getDatabaseName(), "fs.chunks");
$chunksCollection->insertMany($data['chunks']);
$expectedChunksCollection = new Collection($this->manager, $this->getDatabaseName(), "expected.chunks");
$expectedChunksCollection->insertMany($data['chunks']);
$this->collections['expected.chunks'] = $expectedChunksCollection;
}
if(isset($test['arrange'])) {
foreach($test['arrange']['data'] as $cmd) {
foreach($cmd as $key => $value) {
if(isset($this->commands[$key])) {
$collection = new Collection($this->manager, $this->getDatabaseName(), $cmd[$key]);
$this->commands[$key]($collection,$this->fixTypes($cmd, true));
}
}
}
}
}
public function uploadCommand($args)
{
$args = $this->fixTypes($args, false);
$stream = fopen('php://temp', 'w+');
fwrite($stream, $args['source']);
rewind($stream);
$result = $this->bucket->uploadFromStream($args['filename'], $stream, $args['options']);
fclose($stream);
return $result;
}
function downloadCommand($args)
{
$args = $this->fixTypes($args, false);
$stream = fopen('php://temp', 'w+');
$this->bucket->downloadToStream($args['id'], $stream);
rewind($stream);
$result = stream_get_contents($stream);
fclose($stream);
return $result;
}
function deleteCommand($args)
{
$args = $this->fixTypes($args, false);
$this->bucket->delete($args['id']);
}
function download_by_nameCommand($args)
{
$args = $this->fixTypes($args, false);
$stream = fopen('php://temp', 'w+');
if(isset($args['options'])) {
$this->bucket->downloadToStreamByName($args['filename'], $stream, $args['options']);
} else {
$this->bucket->downloadToStreamByName($args['filename'], $stream);
}
rewind($stream);
$result = stream_get_contents($stream);
fclose($stream);
return $result;
}
}
<?php
namespace MongoDB\Tests\GridFS;
use MongoDB\GridFS\CollectionWrapper;
use MongoDB\GridFS\WritableStream;
/**
* Functional tests for the internal WritableStream class.
*/
class WritableStreamFunctionalTest extends FunctionalTestCase
{
private $collectionWrapper;
public function setUp()
{
parent::setUp();
$this->collectionWrapper = new CollectionWrapper($this->manager, $this->getDatabaseName(), 'fs');
}
public function testValidConstructorOptions()
{
new WritableStream($this->collectionWrapper, 'filename', [
'_id' => 'custom-id',
'chunkSizeBytes' => 2,
'metadata' => ['foo' => 'bar'],
]);
}
/**
* @expectedException MongoDB\Exception\InvalidArgumentException
* @dataProvider provideInvalidConstructorOptions
*/
public function testConstructorOptionTypeChecks(array $options)
{
new WritableStream($this->collectionWrapper, 'filename', $options);
}
public function provideInvalidConstructorOptions()
{
$options = [];
foreach ($this->getInvalidIntegerValues() as $value) {
$options[][] = ['chunkSizeBytes' => $value];
}
foreach ($this->getInvalidDocumentValues() as $value) {
$options[][] = ['metadata' => $value];
}
return $options;
}
/**
* @dataProvider provideInputDataAndExpectedMD5
*/
public function testInsertChunksCalculatesMD5($input, $expectedMD5)
{
$stream = new WritableStream($this->collectionWrapper, 'filename');
$stream->insertChunks($input);
$stream->close();
$fileDocument = $this->filesCollection->findOne(
['_id' => $stream->getId()],
['projection' => ['md5' => 1, '_id' => 0]]
);
$this->assertSameDocument(['md5' => $expectedMD5], $fileDocument);
}
public function provideInputDataAndExpectedMD5()
{
return [
['', 'd41d8cd98f00b204e9800998ecf8427e'],
['foobar', '3858f62230ac3c915f300c664312c63f'],
[str_repeat('foobar', 43520), '88ff0e5fcb0acb27947d736b5d69cb73'],
[str_repeat('foobar', 43521), '8ff86511c95a06a611842ceb555d8454'],
[str_repeat('foobar', 87040), '45bfa1a9ec36728ee7338d15c5a30c13'],
[str_repeat('foobar', 87041), '95e78f624f8e745bcfd2d11691fa601e'],
];
}
/**
* @dataProvider provideInputDataAndExpectedMD5
*/
public function testUploadFromStreamCalculatesMD5($input, $expectedMD5)
{
$stream = new WritableStream($this->collectionWrapper, 'filename');
$stream->uploadFromStream($this->createStream($input));
//$stream->close();
$fileDocument = $this->filesCollection->findOne(
['_id' => $stream->getId()],
['projection' => ['md5' => 1, '_id' => 0]]
);
$this->assertSameDocument(['md5' => $expectedMD5], $fileDocument);
}
}
......@@ -165,6 +165,7 @@ class CreateIndexesFunctionalTest extends FunctionalTestCase
* given name is found, it will be passed to the callback, which may perform
* additional assertions.
*
* @param string $indexName
* @param callable $callback
*/
private function assertIndexExists($indexName, $callback = null)
......@@ -185,7 +186,7 @@ class CreateIndexesFunctionalTest extends FunctionalTestCase
}
}
$this->assertNotNull($foundIndex, sprintf('Found %s index for the collection', $indexName));
$this->assertNotNull($foundIndex, sprintf('Index %s does not exist', $indexName));
if ($callback !== null) {
call_user_func($callback, $foundIndex);
......
......@@ -2,7 +2,6 @@
namespace MongoDB\Tests\Operation;
use MongoDB\Driver\Server;
use MongoDB\Operation\DropCollection;
use MongoDB\Operation\InsertOne;
use MongoDB\Operation\ListCollections;
......@@ -20,7 +19,7 @@ class DropCollectionFunctionalTest extends FunctionalTestCase
$operation = new DropCollection($this->getDatabaseName(), $this->getCollectionName());
$operation->execute($server);
$this->assertCollectionDoesNotExist($server, $this->getDatabaseName(), $this->getCollectionName());
$this->assertCollectionDoesNotExist($this->getCollectionName());
}
/**
......@@ -28,26 +27,22 @@ class DropCollectionFunctionalTest extends FunctionalTestCase
*/
public function testDropNonexistentCollection()
{
$server = $this->getPrimaryServer();
$this->assertCollectionDoesNotExist($server, $this->getDatabaseName(), $this->getCollectionName());
$this->assertCollectionDoesNotExist($this->getCollectionName());
$operation = new DropCollection($this->getDatabaseName(), $this->getCollectionName());
$operation->execute($server);
$operation->execute($this->getPrimaryServer());
}
/**
* Asserts that a collection with the given name does not exist on the
* server.
*
* @param Server $server
* @param string $databaseName
* @param string $collectionName
*/
private function assertCollectionDoesNotExist(Server $server, $databaseName, $collectionName)
private function assertCollectionDoesNotExist($collectionName)
{
$operation = new ListCollections($databaseName);
$collections = $operation->execute($server);
$operation = new ListCollections($this->getDatabaseName());
$collections = $operation->execute($this->getPrimaryServer());
$foundCollection = null;
......@@ -58,6 +53,6 @@ class DropCollectionFunctionalTest extends FunctionalTestCase
}
}
$this->assertNull($foundCollection, sprintf('Collection %s exists on the server', $collectionName));
$this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment