Parcourir la source

Merge pull request #28 from alcaeus/run-ext-mongo-tests

Run testsuite against ext-mongo
Andreas il y a 10 ans
Parent
commit
e8bb929d76
35 fichiers modifiés avec 660 ajouts et 392 suppressions
  1. 7 2
      .travis.yml
  2. 12 2
      README.md
  3. 26 7
      lib/Alcaeus/MongoDbAdapter/AbstractCursor.php
  4. 29 12
      lib/Alcaeus/MongoDbAdapter/ExceptionConverter.php
  5. 8 4
      lib/Mongo/MongoClient.php
  6. 84 74
      lib/Mongo/MongoCollection.php
  7. 19 0
      lib/Mongo/MongoCommandCursor.php
  8. 2 2
      lib/Mongo/MongoCursor.php
  9. 13 14
      lib/Mongo/MongoDB.php
  10. 10 10
      lib/Mongo/MongoGridFS.php
  11. 48 15
      lib/Mongo/MongoWriteBatch.php
  12. 22 18
      lib/Mongo/functions.php
  13. 6 0
      phpunit.xml.dist
  14. 1 7
      tests/Alcaeus/MongoDbAdapter/ExceptionConverterTest.php
  15. 14 1
      tests/Alcaeus/MongoDbAdapter/MongoBinDataTest.php
  16. 18 21
      tests/Alcaeus/MongoDbAdapter/MongoClientTest.php
  17. 14 1
      tests/Alcaeus/MongoDbAdapter/MongoCodeTest.php
  18. 127 96
      tests/Alcaeus/MongoDbAdapter/MongoCollectionTest.php
  19. 10 9
      tests/Alcaeus/MongoDbAdapter/MongoCommandCursorTest.php
  20. 19 11
      tests/Alcaeus/MongoDbAdapter/MongoCursorTest.php
  21. 3 2
      tests/Alcaeus/MongoDbAdapter/MongoDBRefTest.php
  22. 41 27
      tests/Alcaeus/MongoDbAdapter/MongoDBTest.php
  23. 15 0
      tests/Alcaeus/MongoDbAdapter/MongoDateTest.php
  24. 3 11
      tests/Alcaeus/MongoDbAdapter/MongoDeleteBatchTest.php
  25. 1 1
      tests/Alcaeus/MongoDbAdapter/MongoGridFSCursorTest.php
  26. 10 9
      tests/Alcaeus/MongoDbAdapter/MongoGridFSFileTest.php
  27. 6 5
      tests/Alcaeus/MongoDbAdapter/MongoGridFSTest.php
  28. 7 3
      tests/Alcaeus/MongoDbAdapter/MongoIdTest.php
  29. 14 11
      tests/Alcaeus/MongoDbAdapter/MongoInsertBatchTest.php
  30. 4 2
      tests/Alcaeus/MongoDbAdapter/MongoMaxKeyTest.php
  31. 2 0
      tests/Alcaeus/MongoDbAdapter/MongoMinKeyTest.php
  32. 13 0
      tests/Alcaeus/MongoDbAdapter/MongoRegexTest.php
  33. 13 0
      tests/Alcaeus/MongoDbAdapter/MongoTimestampTest.php
  34. 21 15
      tests/Alcaeus/MongoDbAdapter/MongoUpdateBatchTest.php
  35. 18 0
      tests/Alcaeus/MongoDbAdapter/TestCase.php

+ 7 - 2
.travis.yml

@@ -7,8 +7,12 @@ php:
   - 7.0
 
 env:
-  matrix:
-    - DRIVER_VERSION=1.1.1
+  - DRIVER_VERSION=1.1.1
+
+matrix:
+  include:
+    - php: 5.6
+      env: DRIVER_VERSION=1.1.1 LEGACY_DRIVER_VERSION=1.6.12
 
 addons:
   apt:
@@ -20,6 +24,7 @@ addons:
 before_script:
   - pecl install -f mongodb-${DRIVER_VERSION}
   - composer install
+  - if [ "x$LEGACY_DRIVER_VERSION" != "x" ]; then yes '' | pecl -q install -f mongo-${LEGACY_DRIVER_VERSION}; fi
 
 script:
     - ./vendor/bin/phpunit --coverage-clover=coverage.clover

+ 12 - 2
README.md

@@ -91,6 +91,8 @@ return `0` as connection ID.
 
 ## MongoCollection
 
+ - The [aggregate](https://secure.php.net/manual/en/mongocollection.aggregate.php)
+ method is not working because of a bug in the underlying library.
  - The [createIndex](https://secure.php.net/manual/en/mongocollection.createindex.php)
  method does not yet return the same result as the original method. Instead, it
  always returns the name of the index created.
@@ -109,14 +111,22 @@ return `0` as connection ID.
  - The [hasNext](https://php.net/manual/en/mongocursor.hasnext.php)
  method is not yet implemented.
  - The [info](https://php.net/manual/en/mongocursor.info.php) method does not
- reliably fill all fields in the cursor information. This includes the `at`, `numReturned`,
- and `server` keys once the cursor has started iterating.
+ reliably fill all fields in the cursor information. This includes the `numReturned`
+ and `server` keys once the cursor has started iterating. The `numReturned` field
+ will always show the same value as the `at` field. The `server` field is lacking
+ authentication information.
  - The [setFlag](https://php.net/manual/en/mongocursor.setflag.php)
  method is not yet implemented.
 
 ## MongoCommandCursor
  - The [createFromDocument](https://php.net/manual/en/mongocommandcursor.createfromdocument.php)
  method is not yet implemented.
+ - The [info](https://php.net/manual/en/mongocommandcursor.info.php) method does not
+ reliably fill all fields in the cursor information. This includes the `at`, `numReturned`,
+ `firstBatchAt` and `firstBatchNumReturned` fields. The `at` and `numReturned`
+ fields always return 0 for compatibility to MongoCursor. The `firstBatchAt` and
+ `firstBatchNumReturned` fields will contain the same value, which is the internal
+ position of the iterator.
 
 ## Types
 

+ 26 - 7
lib/Alcaeus/MongoDbAdapter/AbstractCursor.php

@@ -29,7 +29,7 @@ abstract class AbstractCursor
     /**
      * @var int
      */
-    protected $batchSize;
+    protected $batchSize = 0;
 
     /**
      * @var Collection
@@ -67,6 +67,11 @@ abstract class AbstractCursor
     protected $startedIterating = false;
 
     /**
+     * @var int
+     */
+    protected $position = 0;
+
+    /**
      * @var array
      */
     protected $optionNames = [
@@ -113,7 +118,10 @@ abstract class AbstractCursor
      */
     public function current()
     {
-        $this->startedIterating = true;
+        if (! $this->startedIterating) {
+            return null;
+        }
+
         $document = $this->ensureIterator()->current();
         if ($document !== null) {
             $document = TypeConverter::toLegacy($document);
@@ -129,6 +137,10 @@ abstract class AbstractCursor
      */
     public function key()
     {
+        if (! $this->startedIterating) {
+            return null;
+        }
+
         return $this->ensureIterator()->key();
     }
 
@@ -141,11 +153,12 @@ abstract class AbstractCursor
      */
     public function next()
     {
-        if (!$this->startedIterating) {
+        if (! $this->startedIterating) {
             $this->ensureIterator();
             $this->startedIterating = true;
         } else {
             $this->ensureIterator()->next();
+            $this->position++;
         }
 
         return $this->current();
@@ -162,6 +175,7 @@ abstract class AbstractCursor
         // We can recreate the cursor to allow it to be rewound
         $this->reset();
         $this->startedIterating = true;
+        $this->position = 0;
         $this->ensureIterator()->rewind();
     }
 
@@ -172,6 +186,10 @@ abstract class AbstractCursor
      */
     public function valid()
     {
+        if (! $this->startedIterating) {
+            return false;
+        }
+
         return $this->ensureIterator()->valid();
     }
 
@@ -323,11 +341,12 @@ abstract class AbstractCursor
                     $typeString = 'STANDALONE';
             }
 
+            $cursorId = (string) $this->cursor->getId();
             $iterationInfo += [
-                'id' => (string) $this->cursor->getId(),
-                'at' => null, // @todo Complete info for cursor that is iterating
-                'numReturned' => null, // @todo Complete info for cursor that is iterating
-                'server' => null, // @todo Complete info for cursor that is iterating
+                'id' => (int) $cursorId,
+                'at' => $this->position,
+                'numReturned' => $this->position, // This can't be obtained from the new cursor
+                'server' => sprintf('%s:%d;-;.;%d', $this->cursor->getServer()->getHost(), $this->cursor->getServer()->getPort(), getmypid()),
                 'host' => $this->cursor->getServer()->getHost(),
                 'port' => $this->cursor->getServer()->getPort(),
                 'connection_type_desc' => $typeString,

+ 29 - 12
lib/Alcaeus/MongoDbAdapter/ExceptionConverter.php

@@ -28,8 +28,11 @@ class ExceptionConverter
      *
      * @return \MongoException
      */
-    public static function convertException(Exception\Exception $e, $fallbackClass = 'MongoException')
+    public static function toLegacy(Exception\Exception $e, $fallbackClass = 'MongoException')
     {
+        $message = $e->getMessage();
+        $code = $e->getCode();
+
         switch (get_class($e)) {
             case Exception\AuthenticationException::class:
             case Exception\ConnectionException::class:
@@ -40,7 +43,25 @@ class ExceptionConverter
 
             case Exception\BulkWriteException::class:
             case Exception\WriteException::class:
-                $class = 'MongoCursorException';
+                $writeResult = $e->getWriteResult();
+
+                if ($writeResult) {
+                    $writeError = $writeResult->getWriteErrors()[0];
+
+                    $message = $writeError->getMessage();
+                    $code = $writeError->getCode();
+                }
+
+                switch ($code) {
+                    // see https://github.com/mongodb/mongo-php-driver-legacy/blob/ad3ed45739e9702ae48e53ddfadc482d9c4c7e1c/cursor_shared.c#L540
+                    case 11000:
+                    case 11001:
+                    case 12582:
+                        $class = 'MongoDuplicateKeyException';
+                        break;
+                    default:
+                        $class = 'MongoCursorException';
+                }
                 break;
 
             case Exception\ExecutionTimeoutException::class:
@@ -51,18 +72,14 @@ class ExceptionConverter
                 $class = $fallbackClass;
         }
 
-        if (strpos($e->getMessage(), 'No suitable servers found') !== false) {
-            return new \MongoConnectionException($e->getMessage(), $e->getCode(), $e);
+        if (strpos($message, 'No suitable servers found') !== false) {
+            return new \MongoConnectionException($message, $code, $e);
         }
 
-        return new $class($e->getMessage(), $e->getCode(), $e);
-    }
+        if ($message === "cannot use 'w' > 1 when a host is not replicated") {
+            return new \MongoWriteConcernException($message, $code, $e);
+        }
 
-    /**
-     * @throws \MongoException
-     */
-    public static function toLegacy(Exception\Exception $e, $fallbackClass = 'MongoException')
-    {
-        throw self::convertException($e, $fallbackClass);
+        return new $class($message, $code, $e);
     }
 }

+ 8 - 4
lib/Mongo/MongoClient.php

@@ -191,11 +191,11 @@ class MongoClient
         try {
             $servers = $this->manager->getServers();
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         foreach ($servers as $server) {
-            $key = sprintf('%s:%d', $server->getHost(), $server->getPort());
+            $key = sprintf('%s:%d;-;.;%d', $server->getHost(), $server->getPort(), getmypid());
             $info = $server->getInfo();
 
             switch ($server->getType()) {
@@ -246,7 +246,7 @@ class MongoClient
         try {
             $databaseInfoIterator = $this->client->listDatabases();
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         $databases = [
@@ -256,7 +256,11 @@ class MongoClient
         ];
 
         foreach ($databaseInfoIterator as $databaseInfo) {
-            $databases['databases'][] = $databaseInfo->getName();
+            $databases['databases'][] = [
+                'name' => $databaseInfo->getName(),
+                'empty' => $databaseInfo->isEmpty(),
+                'sizeOnDisk' => $databaseInfo->getSizeOnDisk(),
+            ];
             $databases['totalSize'] += $databaseInfo->getSizeOnDisk();
         }
 

+ 84 - 74
lib/Mongo/MongoCollection.php

@@ -101,7 +101,7 @@ class MongoCollection
             return $this->getWriteConcern()[$name];
         }
 
-        return $this->db->selectCollection($this->name . '.' . $name);
+        return $this->db->selectCollection($this->name . '.' . str_replace(chr(0), '', $name));
     }
 
     /**
@@ -144,19 +144,23 @@ class MongoCollection
             $options = $op;
         }
 
-        $command = [
-            'aggregate' => $this->name,
-            'pipeline' => $pipeline
-        ];
+        if (isset($options['cursor'])) {
+            $options['useCursor'] = true;
 
-        $command += $options;
+            if (isset($options['cursor']['batchSize'])) {
+                $options['batchSize'] = $options['cursor']['batchSize'];
+            }
 
-        try {
-            return $this->db->command($command);
-        } catch (MongoCursorTimeoutException $e) {
-            throw new MongoExecutionTimeoutException($e->getMessage(), $e->getCode(), $e);
+            unset($options['cursor']);
+        } else {
+            $options['useCursor'] = false;
         }
 
+        try {
+            return $this->collection->aggregate(TypeConverter::fromLegacy($pipeline), $options);
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            throw ExceptionConverter::toLegacy($e);
+        }
     }
 
     /**
@@ -177,7 +181,7 @@ class MongoCollection
 
         // Convert cursor option
         if (! isset($options['cursor'])) {
-            $options['cursor'] = true;
+            $options['cursor'] = new \stdClass();
         }
 
         $command += $options;
@@ -263,7 +267,7 @@ class MongoCollection
     public function insert(&$a, array $options = [])
     {
         if (! $this->ensureDocumentHasMongoId($a)) {
-            trigger_error(sprintf('%s expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
+            trigger_error(sprintf('%s(): expects parameter %d to be an array or object, %s given', __METHOD__, 1, gettype($a)), E_USER_WARNING);
             return;
         }
 
@@ -276,17 +280,8 @@ class MongoCollection
                 TypeConverter::fromLegacy($a),
                 $this->convertWriteConcernOptions($options)
             );
-        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
-            $writeResult = $e->getWriteResult();
-            $writeError = $writeResult->getWriteErrors()[0];
-            return [
-                'ok' => 0.0,
-                'n' => 0,
-                'err' => $writeError->getCode(),
-                'errmsg' => $writeError->getMessage(),
-            ];
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         if (! $result->isAcknowledged()) {
@@ -341,7 +336,7 @@ class MongoCollection
                 $this->convertWriteConcernOptions($options)
             );
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
         }
 
         if (! $result->isAcknowledged()) {
@@ -349,12 +344,12 @@ class MongoCollection
         }
 
         return [
+            'ok' => 1.0,
             'connectionId' => 0,
             'n' => 0,
             'syncMillis' => 0,
             'writtenTo' => null,
             'err' => null,
-            'errmsg' => null,
         ];
     }
 
@@ -381,19 +376,8 @@ class MongoCollection
                 TypeConverter::fromLegacy($newobj),
                 $this->convertWriteConcernOptions($options)
             );
-        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
-            $writeResult = $e->getWriteResult();
-            $writeError = $writeResult->getWriteErrors()[0];
-            return [
-                'ok' => 0.0,
-                'nModified' => $writeResult->getModifiedCount(),
-                'n' => $writeResult->getMatchedCount(),
-                'err' => $writeError->getCode(),
-                'errmsg' => $writeError->getMessage(),
-                'updatedExisting' => $writeResult->getUpsertedCount() == 0,
-            ];
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         if (! $result->isAcknowledged()) {
@@ -433,7 +417,7 @@ class MongoCollection
                 $this->convertWriteConcernOptions($options)
             );
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         if (! $result->isAcknowledged()) {
@@ -508,12 +492,16 @@ class MongoCollection
 
                 $options['projection'] = is_array($fields) ? TypeConverter::fromLegacy($fields) : [];
 
-                $document = $this->collection->findOneAndUpdate($query, $update, $options);
+                if (! \MongoDB\is_first_key_operator($update)) {
+                    $document = $this->collection->findOneAndReplace($query, $update, $options);
+                } else {
+                    $document = $this->collection->findOneAndUpdate($query, $update, $options);
+                }
             }
         } catch (\MongoDB\Driver\Exception\ConnectionException $e) {
             throw new MongoResultException($e->getMessage(), $e->getCode(), $e);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e, 'MongoResultException');
+            throw ExceptionConverter::toLegacy($e, 'MongoResultException');
         }
 
         if ($document) {
@@ -538,7 +526,7 @@ class MongoCollection
         try {
             $document = $this->collection->findOne(TypeConverter::fromLegacy($query), $options);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         if ($document !== null) {
@@ -572,15 +560,21 @@ class MongoCollection
         }
 
         if (! is_array($keys) || ! count($keys)) {
-            throw new MongoException('keys cannot be empty');
+            throw new MongoException('index specification has no elements');
         }
 
         // duplicate
         $neededOptions = ['unique' => 1, 'sparse' => 1, 'expireAfterSeconds' => 1, 'background' => 1, 'dropDups' => 1];
         $indexOptions = array_intersect_key($options, $neededOptions);
-        $indexes = $this->collection->listIndexes();
-        foreach ($indexes as $index) {
+        $indexes = iterator_to_array($this->collection->listIndexes());
+        $indexCount = count($indexes);
 
+        // listIndexes returns 0 for non-existing collections while the legacy driver returns 1
+        if ($indexCount === 0) {
+            $indexCount = 1;
+        }
+
+        foreach ($indexes as $index) {
             if (! empty($options['name']) && $index->getName() === $options['name']) {
                 throw new \MongoResultException(sprintf('index with name: %s already exists', $index->getName()));
             }
@@ -595,8 +589,8 @@ class MongoCollection
 
                 return [
                     'createdCollectionAutomatically' => false,
-                    'numIndexesBefore' => count($indexes),
-                    'numIndexesAfter' => count($indexes),
+                    'numIndexesBefore' => $indexCount,
+                    'numIndexesAfter' => $indexCount,
                     'note' => 'all indexes already exist',
                     'ok' => 1.0
                 ];
@@ -606,13 +600,13 @@ class MongoCollection
         try {
             $this->collection->createIndex($keys, $this->convertWriteConcernOptions($options));
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         return [
             'createdCollectionAutomatically' => true,
-            'numIndexesBefore' => count($indexes),
-            'numIndexesAfter' => count($indexes) + 1,
+            'numIndexesBefore' => $indexCount,
+            'numIndexesAfter' => $indexCount + 1,
             'ok' => 1.0
         ];
     }
@@ -623,14 +617,12 @@ class MongoCollection
      * @link http://www.php.net/manual/en/mongocollection.ensureindex.php
      * @param array $keys Field or fields to use as index.
      * @param array $options [optional] This parameter is an associative array of the form array("optionname" => <boolean>, ...).
-     * @return boolean always true
+     * @return array Returns the database response.
      * @deprecated Use MongoCollection::createIndex() instead.
      */
     public function ensureIndex(array $keys, array $options = [])
     {
-        $this->createIndex($keys, $options);
-
-        return true;
+        return $this->createIndex($keys, $options);
     }
 
     /**
@@ -644,13 +636,25 @@ class MongoCollection
     {
         if (is_string($keys)) {
             $indexName = $keys;
+            if (! preg_match('#_-?1$#', $indexName)) {
+                $indexName .= '_1';
+            }
         } elseif (is_array($keys)) {
             $indexName = \MongoDB\generate_index_name($keys);
         } else {
             throw new \InvalidArgumentException();
         }
 
-        return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
+        try {
+            return TypeConverter::toLegacy($this->collection->dropIndex($indexName));
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            return [
+                'nIndexesWas' => count($this->getIndexInfo()),
+                'errmsg' => $e->getMessage(),
+                'ok' => 0.0,
+                'code' => $e->getCode(),
+            ];
+        }
     }
 
     /**
@@ -697,7 +701,7 @@ class MongoCollection
         try {
             return $this->collection->count(TypeConverter::fromLegacy($query), $options);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
     }
 
@@ -728,22 +732,27 @@ class MongoCollection
                 TypeConverter::fromLegacy($document),
                 $this->convertWriteConcernOptions($options)
             );
-        } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
-        }
 
-        if (!$result->isAcknowledged()) {
-            return true;
-        }
+            if (! $result->isAcknowledged()) {
+                return true;
+            }
 
-        return [
-            'ok' => 1.0,
-            'nModified' => $result->getModifiedCount(),
-            'n' => $result->getMatchedCount(),
-            'err' => null,
-            'errmsg' => null,
-            'updatedExisting' => $result->getUpsertedCount() == 0,
-        ];
+            $resultArray = [
+                'ok' => 1.0,
+                'nModified' => $result->getModifiedCount(),
+                'n' => $result->getUpsertedCount() + $result->getModifiedCount(),
+                'err' => null,
+                'errmsg' => null,
+                'updatedExisting' => $result->getUpsertedCount() == 0,
+            ];
+            if ($result->getUpsertedId() !== null) {
+                $resultArray['upserted'] = TypeConverter::toLegacy($result->getUpsertedId());
+            }
+
+            return $resultArray;
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
+            throw ExceptionConverter::toLegacy($e);
+        }
     }
 
     /**
@@ -907,16 +916,13 @@ class MongoCollection
     {
         $checkKeys = function($array) {
             foreach (array_keys($array) as $key) {
-                if (is_int($key) || empty($key) || strpos($key, '*') === 1) {
+                if (empty($key) || strpos($key, '*') === 1) {
                     throw new \MongoException('document contain invalid key');
                 }
             }
         };
 
         if (is_array($document)) {
-            if (empty($document)) {
-                throw new \MongoException('document cannot be empty');
-            }
             if (! isset($document['_id'])) {
                 $document['_id'] = new \MongoId();
             }
@@ -925,9 +931,13 @@ class MongoCollection
 
             return $document['_id'];
         } elseif (is_object($document)) {
-            if (empty((array) $document)) {
-                throw new \MongoException('document cannot be empty');
+            $reflectionObject = new \ReflectionObject($document);
+            foreach ($reflectionObject->getProperties() as $property) {
+                if (! $property->isPublic()) {
+                    throw new \MongoException('zero-length keys are not allowed, did you use $ with double quotes?');
+                }
             }
+
             if (! isset($document->_id)) {
                 $document->_id = new \MongoId();
             }

+ 19 - 0
lib/Mongo/MongoCommandCursor.php

@@ -81,4 +81,23 @@ class MongoCommandCursor extends AbstractCursor implements MongoCursorInterface
             'fields' => null,
         ];
     }
+
+    /**
+     * @return array
+     */
+    protected function getIterationInfo()
+    {
+        $iterationInfo = parent::getIterationInfo();
+
+        if ($iterationInfo['started_iterating']) {
+            $iterationInfo += [
+                'firstBatchAt' => $iterationInfo['at'],
+                'firstBatchNumReturned' => $iterationInfo['numReturned'],
+            ];
+            $iterationInfo['at'] = 0;
+            $iterationInfo['numReturned'] = 0;
+        }
+
+        return $iterationInfo;
+    }
 }

+ 2 - 2
lib/Mongo/MongoCursor.php

@@ -146,7 +146,7 @@ class MongoCursor extends AbstractCursor implements Iterator
         } catch (\MongoDB\Driver\Exception\ExecutionTimeoutException $e) {
             throw new MongoCursorTimeoutException($e->getMessage(), $e->getCode(), $e);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         return $count;
@@ -167,7 +167,7 @@ class MongoCursor extends AbstractCursor implements Iterator
         } catch (\MongoDB\Driver\Exception\ExecutionTimeoutException $e) {
             throw new MongoCursorTimeoutException($e->getMessage(), $e->getCode(), $e);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
     }
 

+ 13 - 14
lib/Mongo/MongoDB.php

@@ -112,8 +112,7 @@ class MongoDB
     public function __set($name, $value)
     {
         if ($name === 'w' || $name === 'wtimeout') {
-            $this->setWriteConcernFromArray([$name => $value] + $this->getWriteConcern());
-            $this->createDatabaseObject();
+            trigger_error("The '{$name}' property is read-only", E_DEPRECATED);
         }
     }
 
@@ -134,7 +133,7 @@ class MongoDB
         try {
             $collections = $this->db->listCollections($options);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         $getCollectionInfo = function (CollectionInfo $collectionInfo) {
@@ -164,7 +163,7 @@ class MongoDB
         try {
             $collections = $this->db->listCollections($options);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
-            ExceptionConverter::toLegacy($e);
+            throw ExceptionConverter::toLegacy($e);
         }
 
         $getCollectionName = function (CollectionInfo $collectionInfo) {
@@ -270,6 +269,10 @@ class MongoDB
     public function createCollection($name, $options)
     {
         try {
+            if (isset($options['capped'])) {
+                $options['capped'] = (bool) $options['capped'];
+            }
+
             $this->db->createCollection($name, $options);
         } catch (\MongoDB\Driver\Exception\Exception $e) {
             return false;
@@ -322,10 +325,10 @@ class MongoDB
             $id = $document_or_id;
         } elseif (is_object($document_or_id)) {
             if (! isset($document_or_id->_id)) {
-                return null;
+                $id = $document_or_id;
+            } else {
+                $id = $document_or_id->_id;
             }
-
-            $id = $document_or_id->_id;
         } elseif (is_array($document_or_id)) {
             if (! isset($document_or_id['_id'])) {
                 return null;
@@ -336,7 +339,7 @@ class MongoDB
             $id = $document_or_id;
         }
 
-        return MongoDBRef::create($collection, $id, $this->name);
+        return MongoDBRef::create($collection, $id);
     }
 
 
@@ -380,16 +383,12 @@ class MongoDB
             $cursor->setReadPreference($this->getReadPreference());
 
             return iterator_to_array($cursor)[0];
-        } catch (\MongoDB\Driver\Exception\ExecutionTimeoutException $e) {
-            throw new MongoCursorTimeoutException($e->getMessage(), $e->getCode(), $e);
-        } catch (\MongoDB\Driver\Exception\RuntimeException $e) {
+        } catch (\MongoDB\Driver\Exception\Exception $e) {
             return [
-                'ok' => 0,
+                'ok' => 0.0,
                 'errmsg' => $e->getMessage(),
                 'code' => $e->getCode(),
             ];
-        } catch (\MongoDB\Driver\Exception\Excepiton $e) {
-            ExceptionConverter::toLegacy($e);
         }
     }
 

+ 10 - 10
lib/Mongo/MongoGridFS.php

@@ -15,8 +15,6 @@
 
 class MongoGridFS extends MongoCollection
 {
-    const DEFAULT_CHUNK_SIZE = 262144; // 256 kb
-
     const ASCENDING = 1;
     const DESCENDING = -1;
 
@@ -45,6 +43,8 @@ class MongoGridFS extends MongoCollection
 
     private $prefix;
 
+    private $defaultChunkSize = 261120;
+
     /**
      * Files as stored across two collections, the first containing file meta
      * information, the second containing chunks of the actual file. By default,
@@ -203,14 +203,14 @@ class MongoGridFS extends MongoCollection
         try {
             $file = $this->insertFile($record, $options);
         } catch (MongoException $e) {
-            throw new MongoGridFSException('Cannot insert file record', 0, $e);
+            throw new MongoGridFSException('Could not store file: '. $e->getMessage(), 0, $e);
         }
 
         try {
             $this->insertChunksFromBytes($bytes, $file);
         } catch (MongoException $e) {
             $this->delete($file['_id']);
-            throw new MongoGridFSException('Error while inserting chunks', 0, $e);
+            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
         }
 
         return $file['_id'];
@@ -253,14 +253,14 @@ class MongoGridFS extends MongoCollection
         try {
             $file = $this->insertFile($record, $options);
         } catch (MongoException $e) {
-            throw new MongoGridFSException('Cannot insert file record', 0, $e);
+            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
         }
 
         try {
             $length = $this->insertChunksFromFile($handle, $file, $md5);
         } catch (MongoException $e) {
             $this->delete($file['_id']);
-            throw new MongoGridFSException('Error while inserting chunks', 0, $e);
+            throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
         }
 
 
@@ -273,7 +273,7 @@ class MongoGridFS extends MongoCollection
             try {
                 $update['md5'] = $md5;
             } catch (MongoException $e) {
-                throw new MongoGridFSException('Error computing MD5 checksum', 0, $e);
+                throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
             }
         }
 
@@ -281,11 +281,11 @@ class MongoGridFS extends MongoCollection
             try {
                 $result = $this->update(['_id' => $file['_id']], ['$set' => $update]);
                 if (! $this->isOKResult($result)) {
-                    throw new MongoGridFSException('Error updating file record');
+                    throw new MongoGridFSException('Could not store file');
                 }
             } catch (MongoException $e) {
                 $this->delete($file['_id']);
-                throw new MongoGridFSException('Error updating file record', 0, $e);
+                throw new MongoGridFSException('Could not store file: ' . $e->getMessage(), 0, $e);
             }
 
         }
@@ -427,7 +427,7 @@ class MongoGridFS extends MongoCollection
         $record += [
             '_id' => new MongoId(),
             'uploadDate' => new MongoDate(),
-            'chunkSize' => self::DEFAULT_CHUNK_SIZE,
+            'chunkSize' => $this->defaultChunkSize,
         ];
 
         $result = $this->insert($record, $options);

+ 48 - 15
lib/Mongo/MongoWriteBatch.php

@@ -115,38 +115,71 @@ class MongoWriteBatch
 
         try {
             $result = $collection->BulkWrite($this->items, $options);
-            $ok = 1.0;
+            $ok = true;
         } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
             $result = $e->getWriteResult();
-            $ok = 0.0;
+            $ok = false;
         }
 
-        if ($ok === 1.0) {
+        if ($ok === true) {
             $this->items = [];
         }
 
-        return [
-            'ok' => $ok,
-            'nInserted' => $result->getInsertedCount(),
-            'nMatched' => $result->getMatchedCount(),
-            'nModified' => $result->getModifiedCount(),
-            'nUpserted' => $result->getUpsertedCount(),
-            'nRemoved' => $result->getDeletedCount(),
-        ];
+        switch ($this->batchType) {
+            case self::COMMAND_UPDATE:
+                $upsertedIds = [];
+                foreach ($result->getUpsertedIds() as $index => $id) {
+                    $upsertedIds[] = [
+                        'index' => $index,
+                        '_id' => TypeConverter::toLegacy($id)
+                    ];
+                }
+
+                $result = [
+                    'nMatched' => $result->getMatchedCount(),
+                    'nModified' => $result->getModifiedCount(),
+                    'nUpserted' => $result->getUpsertedCount(),
+                    'ok' => $ok,
+                ];
+
+                if (count($upsertedIds)) {
+                    $result['upserted'] = $upsertedIds;
+                }
+
+                return $result;
+
+            case self::COMMAND_DELETE:
+                return [
+                    'nRemoved' => $result->getDeletedCount(),
+                    'ok' => $ok,
+                ];
+
+            case self::COMMAND_INSERT:
+                return [
+                    'nInserted' => $result->getInsertedCount(),
+                    'ok' => $ok,
+                ];
+        }
     }
 
     private function validate(array $item)
     {
         switch ($this->batchType) {
             case self::COMMAND_UPDATE:
-                if (! isset($item['q']) || ! isset($item['u'])) {
-                    throw new Exception('invalid item');
+                if (! isset($item['q'])) {
+                    throw new Exception("Expected \$item to contain 'q' key");
+                }
+                if (! isset($item['u'])) {
+                    throw new Exception("Expected \$item to contain 'u' key");
                 }
                 break;
 
             case self::COMMAND_DELETE:
-                if (! isset($item['q']) || ! isset($item['limit'])) {
-                    throw new Exception('invalid item');
+                if (! isset($item['q'])) {
+                    throw new Exception("Expected \$item to contain 'q' key");
+                }
+                if (! isset($item['limit'])) {
+                    throw new Exception("Expected \$item to contain 'limit' key");
                 }
                 break;
         }

+ 22 - 18
lib/Mongo/functions.php

@@ -15,24 +15,28 @@
 
 use Alcaeus\MongoDbAdapter\TypeConverter;
 
-/**
- * Deserializes a BSON object into a PHP array
- *
- * @param string $bson The BSON to be deserialized.
- * @return array Returns the deserialized BSON object.
- */
-function bson_decode($bson)
-{
-    return TypeConverter::toLegacy(\MongoDB\BSON\toPHP($bson));
+if (! function_exists('bson_decode')) {
+    /**
+     * Deserializes a BSON object into a PHP array
+     *
+     * @param string $bson The BSON to be deserialized.
+     * @return array Returns the deserialized BSON object.
+     */
+    function bson_decode($bson)
+    {
+        return TypeConverter::toLegacy(\MongoDB\BSON\toPHP($bson));
+    }
 }
 
-/**
- * Serializes a PHP variable into a BSON string
- *
- * @param mixed $anything The variable to be serialized.
- * @return string Returns the serialized string.
- */
-function bson_encode($anything)
-{
-    return \MongoDB\BSON\fromPHP(TypeConverter::fromLegacy($anything));
+if (! function_exists('bson_encode')) {
+    /**
+     * Serializes a PHP variable into a BSON string
+     *
+     * @param mixed $anything The variable to be serialized.
+     * @return string Returns the serialized string.
+     */
+    function bson_encode($anything)
+    {
+        return \MongoDB\BSON\fromPHP(TypeConverter::fromLegacy($anything));
+    }
 }

+ 6 - 0
phpunit.xml.dist

@@ -9,6 +9,12 @@
          stopOnFailure="false"
          syntaxCheck="false"
 >
+    <php>
+        <!-- Disable deprecation warnings -->
+        <!-- php -r 'echo -1 & ~E_USER_DEPRECATED & ~E_DEPRECATED;' -->
+        <ini name="error_reporting" value="-24577"/>
+    </php>
+
     <testsuites>
         <testsuite name="Mongo driver adapter test suite">
             <directory>./tests/Alcaeus/MongoDbAdapter/</directory>

+ 1 - 7
tests/Alcaeus/MongoDbAdapter/ExceptionConverterTest.php

@@ -7,18 +7,12 @@ use Alcaeus\MongoDbAdapter\ExceptionConverter;
 
 class ExceptionConverterTest extends \PHPUnit_Framework_TestCase
 {
-    public function testThrowException()
-    {
-        $this->setExpectedException('MongoException');
-        ExceptionConverter::toLegacy(new Exception\InvalidArgumentException());
-    }
-
     /**
      * @dataProvider exceptionProvider
      */
     public function testConvertException($e, $expectedClass)
     {
-        $exception = ExceptionConverter::convertException($e);
+        $exception = ExceptionConverter::toLegacy($e);
         $this->assertInstanceOf($expectedClass, $exception);
         $this->assertSame($e->getMessage(), $exception->getMessage());
         $this->assertSame($e->getCode(), $exception->getCode());

+ 14 - 1
tests/Alcaeus/MongoDbAdapter/MongoBinDataTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use Alcaeus\MongoDbAdapter\TypeInterface;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -13,7 +14,17 @@ class MongoBinDataTest extends TestCase
         $this->assertAttributeSame('foo', 'bin', $bin);
         $this->assertAttributeSame(\MongoBinData::FUNC, 'type', $bin);
 
-        $this->assertSame('<Mongo Binary Data>', (string) $bin);
+        $this->assertSame('<Mongo Binary Data>', (string)$bin);
+
+        return $bin;
+    }
+
+    /**
+     * @depends testCreate
+     */
+    public function testConvertToBson(\MongoBinData $bin)
+    {
+        $this->skipTestUnless($bin instanceof TypeInterface);
 
         $bsonBinary = $bin->toBSONType();
         $this->assertInstanceOf('MongoDB\BSON\Binary', $bsonBinary);
@@ -24,6 +35,8 @@ class MongoBinDataTest extends TestCase
 
     public function testCreateWithBsonBinary()
     {
+        $this->skipTestUnless(in_array(TypeInterface::class, class_implements('MongoBinData')));
+
         $bsonBinary = new \MongoDB\BSON\Binary('foo', \MongoDB\BSON\Binary::TYPE_UUID);
         $bin = new \MongoBinData($bsonBinary);
 

+ 18 - 21
tests/Alcaeus/MongoDbAdapter/MongoClientTest.php

@@ -7,21 +7,6 @@ namespace Alcaeus\MongoDbAdapter\Tests;
  */
 class MongoClientTest extends TestCase
 {
-    public function testConnectAndDisconnect()
-    {
-        $client = $this->getClient();
-        $this->assertTrue($client->connected);
-
-        $client->close();
-        $this->assertFalse($client->connected);
-    }
-
-    public function testClientWithoutAutomaticConnect()
-    {
-        $client = $this->getClient([]);
-        $this->assertFalse($client->connected);
-    }
-
     public function testGetDb()
     {
         $client = $this->getClient();
@@ -63,16 +48,17 @@ class MongoClientTest extends TestCase
     public function testGetHosts()
     {
         $client = $this->getClient();
+        $hosts = $client->getHosts();
         $this->assertArraySubset(
             [
-                'localhost:27017' => [
+                'localhost:27017;-;.;' . getmypid() => [
                     'host' => 'localhost',
                     'port' => 27017,
                     'health' => 1,
                     'state' => 0,
                 ],
             ],
-            $client->getHosts()
+            $hosts
         );
     }
 
@@ -81,14 +67,13 @@ class MongoClientTest extends TestCase
         $client = $this->getClient();
         $this->assertSame(['type' => \MongoClient::RP_PRIMARY], $client->getReadPreference());
 
-        $this->assertTrue($client->setReadPreference(\MongoClient::RP_SECONDARY, ['a' => 'b']));
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => ['a' => 'b']], $client->getReadPreference());
+        $this->assertTrue($client->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]));
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $client->getReadPreference());
     }
 
     public function testWriteConcern()
     {
         $client = $this->getClient();
-        $this->assertSame(['w' => 1, 'wtimeout' => 0], $client->getWriteConcern());
 
         $this->assertTrue($client->setWriteConcern('majority', 100));
         $this->assertSame(['w' => 'majority', 'wtimeout' => 100], $client->getWriteConcern());
@@ -103,7 +88,19 @@ class MongoClientTest extends TestCase
         $this->assertSame(1.0, $databases['ok']);
         $this->assertArrayHasKey('totalSize', $databases);
         $this->assertArrayHasKey('databases', $databases);
-        $this->assertContains('mongo-php-adapter', $databases['databases']);
+
+        foreach ($databases['databases'] as $database) {
+            $this->assertArrayHasKey('name', $database);
+            $this->assertArrayHasKey('empty', $database);
+            $this->assertArrayHasKey('sizeOnDisk', $database);
+
+            if ($database['name'] == 'mongo-php-adapter') {
+                $this->assertFalse($database['empty']);
+                return;
+            }
+        }
+
+        $this->fail('Could not find mongo-php-adapter database in list');
     }
 
     public function testNoPrefixUri()

+ 14 - 1
tests/Alcaeus/MongoDbAdapter/MongoCodeTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use Alcaeus\MongoDbAdapter\TypeInterface;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -13,7 +14,17 @@ class MongoCodeTest extends TestCase
         $this->assertAttributeSame('code', 'code', $code);
         $this->assertAttributeSame(['scope' => 'bleh'], 'scope', $code);
 
-        $this->assertSame('code', (string) $code);
+        $this->assertSame('code', (string)$code);
+
+        return $code;
+    }
+
+    /**
+     * @depends testCreate
+     */
+    public function testConvertToBson(\MongoCode $code)
+    {
+        $this->skipTestUnless($code instanceof TypeInterface);
 
         $bsonCode = $code->toBSONType();
         $this->assertInstanceOf('MongoDB\BSON\Javascript', $bsonCode);
@@ -21,6 +32,8 @@ class MongoCodeTest extends TestCase
 
     public function testCreateWithBsonObject()
     {
+        $this->skipTestUnless(in_array(TypeInterface::class, class_implements('MongoCode')));
+
         $bsonCode = new \MongoDB\BSON\Javascript('code', ['scope' => 'bleh']);
         $code = new \MongoCode($bsonCode);
 

+ 127 - 96
tests/Alcaeus/MongoDbAdapter/MongoCollectionTest.php

@@ -43,7 +43,7 @@ class MongoCollectionTest extends TestCase
 
     public function testInsertInvalidData()
     {
-        $this->setExpectedException('PHPUnit_Framework_Error_Warning', 'ongoCollection::insert expects parameter 1 to be an array or object, integer given');
+        $this->setExpectedException('PHPUnit_Framework_Error_Warning', 'MongoCollection::insert(): expects parameter 1 to be an array or object, integer given');
 
         $document = 8;
         $this->getCollection()->insert($document);
@@ -51,33 +51,33 @@ class MongoCollectionTest extends TestCase
 
     public function testInsertEmptyArray()
     {
-        $this->setExpectedException('MongoException', 'document cannot be empty');
-
         $document = [];
         $this->getCollection()->insert($document);
+
+        $this->assertSame(1, $this->getCollection()->count());
     }
 
     public function testInsertArrayWithNumericKeys()
     {
-        $this->setExpectedException('MongoException', 'document contain invalid key');
-
         $document = [1 => 'foo'];
         $this->getCollection()->insert($document);
+
+        $this->assertSame(1, $this->getCollection()->count(['_id' => $document['_id']]));
     }
 
     public function testInsertEmptyObject()
     {
-        $this->setExpectedException('MongoException', 'document cannot be empty');
-
         $document = (object) [];
         $this->getCollection()->insert($document);
+
+        $this->assertSame(1, $this->getCollection()->count());
     }
 
     public function testInsertObjectWithPrivateProperties()
     {
-        $this->setExpectedException('MongoException', 'document contain invalid key');
+        $this->setExpectedException('MongoException', 'zero-length keys are not allowed, did you use $ with double quotes?');
 
-        $document = $this->getCollection();
+        $document = new PrivatePropertiesStub();
         $this->getCollection()->insert($document);
     }
 
@@ -91,14 +91,8 @@ class MongoCollectionTest extends TestCase
         $collection->insert($document);
 
         unset($document['_id']);
-        $this->assertArraySubset(
-            [
-                'ok' => 0.0,
-                'n' => 0,
-                'err' => 11000,
-            ],
-            $collection->insert($document)
-        );
+        $this->setExpectedExceptionRegExp('MongoDuplicateKeyException', '/E11000 duplicate key error .* mongo-php-adapter\.test/');
+        $collection->insert($document);
     }
 
     public function testUnacknowledgedWrite()
@@ -109,7 +103,10 @@ class MongoCollectionTest extends TestCase
 
     public function testInsertWriteConcernException()
     {
-        $this->setExpectedException('MongoConnectionException');
+        $this->setExpectedException(
+            'MongoWriteConcernException',
+            "cannot use 'w' > 1 when a host is not replicated"
+        );
 
         $document = ['foo' => 'bar'];
         $this->getCollection()->insert($document, ['w' => 2]);
@@ -118,19 +115,18 @@ class MongoCollectionTest extends TestCase
     public function testInsertMany()
     {
         $expected = [
-            'connectionId' => 0,
+            'ok' => 1.0,
             'n' => 0,
             'syncMillis' => 0,
             'writtenTo' => null,
             'err' => null,
-            'errmsg' => null
         ];
 
         $documents = [
             ['foo' => 'bar'],
             ['bar' => 'foo']
         ];
-        $this->assertSame($expected, $this->getCollection()->batchInsert($documents));
+        $this->assertArraySubset($expected, $this->getCollection()->batchInsert($documents));
 
         foreach ($documents as $document) {
             $this->assertInstanceOf('MongoId', $document['_id']);
@@ -141,19 +137,18 @@ class MongoCollectionTest extends TestCase
     public function testInsertManyWithNonNumericKeys()
     {
         $expected = [
-            'connectionId' => 0,
+            'ok' => 1.0,
             'n' => 0,
             'syncMillis' => 0,
             'writtenTo' => null,
             'err' => null,
-            'errmsg' => null
         ];
 
         $documents = [
             'a' => ['foo' => 'bar'],
             'b' => ['bar' => 'foo']
         ];
-        $this->assertSame($expected, $this->getCollection()->batchInsert($documents));
+        $this->assertArraySubset($expected, $this->getCollection()->batchInsert($documents));
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(2, $newCollection->count());
@@ -162,19 +157,18 @@ class MongoCollectionTest extends TestCase
     public function testBatchInsertContinuesOnError()
     {
         $expected = [
-            'connectionId' => 0,
+            'ok' => 1.0,
             'n' => 0,
             'syncMillis' => 0,
             'writtenTo' => null,
             'err' => null,
-            'errmsg' => null
         ];
 
         $documents = [
             8,
             'b' => ['bar' => 'foo']
         ];
-        $this->assertSame($expected, $this->getCollection()->batchInsert($documents, ['continueOnError' => true]));
+        $this->assertArraySubset($expected, $this->getCollection()->batchInsert($documents, ['continueOnError' => true]));
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(1, $newCollection->count());
@@ -182,10 +176,11 @@ class MongoCollectionTest extends TestCase
 
     public function testBatchInsertException()
     {
-        $this->setExpectedException('MongoConnectionException');
+        $this->setExpectedException('MongoDuplicateKeyException', 'E11000 duplicate key error index: mongo-php-adapter.test.$_id_');
 
-        $documents = [['foo' => 'bar']];
-        $this->getCollection()->batchInsert($documents, ['w' => 2]);
+        $id = new \MongoId();
+        $documents = [['_id' => $id, 'foo' => 'bar'], ['_id' => $id, 'foo' => 'bleh']];
+        $this->getCollection()->batchInsert($documents);
     }
 
     public function testBatchInsertEmptyBatchException()
@@ -198,7 +193,7 @@ class MongoCollectionTest extends TestCase
 
     public function testUpdateWriteConcern()
     {
-        $this->setExpectedException('MongoConnectionException'); // does not match driver
+        $this->setExpectedException('MongoWriteConcernException', "cannot use 'w' > 1 when a host is not replicated");
 
         $this->getCollection()->update([], ['$set' => ['foo' => 'bar']], ['w' => 2]);
     }
@@ -227,7 +222,7 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(1, $this->getCheckDatabase()->selectCollection('test')->count(['foo' => 'foo']));
     }
 
-    public function testUpdateFail()
+    public function testUpdateDuplicate()
     {
         $collection = $this->getCollection();
         $collection->createIndex(['foo' => 1], ['unique' => 1]);
@@ -237,16 +232,8 @@ class MongoCollectionTest extends TestCase
         $document = ['foo' => 'foo'];
         $collection->insert($document);
 
-        $this->assertArraySubset(
-            [
-                'ok' => 0.0,
-                'nModified' => 0,
-                'n' => 0,
-                'err' => 11000,
-                'updatedExisting' => true,
-            ],
-            $collection->update(['foo' => 'bar'], ['$set' => ['foo' => 'foo']])
-        );
+        $this->setExpectedException('MongoDuplicateKeyException');
+        $collection->update(['foo' => 'bar'], ['$set' => ['foo' => 'foo']]);
     }
 
     public function testUpdateMany()
@@ -377,11 +364,11 @@ class MongoCollectionTest extends TestCase
 
     public function testFindOneConnectionIssue()
     {
+        $this->setExpectedException('MongoConnectionException');
+
         $client = $this->getClient([], 'mongodb://localhost:28888?connectTimeoutMS=1');
         $collection = $client->selectCollection('mongo-php-adapter', 'test');
 
-        $this->setExpectedException('MongoConnectionException');
-
         $collection->findOne();
     }
 
@@ -407,6 +394,7 @@ class MongoCollectionTest extends TestCase
 
     public function testAggregate()
     {
+        $this->skipTestUnless(extension_loaded('mongo'));
         $collection = $this->getCollection();
 
         $this->prepareData();
@@ -435,6 +423,7 @@ class MongoCollectionTest extends TestCase
 
     public function testAggregateTimeoutException()
     {
+        $this->skipTestUnless(extension_loaded('mongo'));
         $collection = $this->getCollection();
 
         $this->failMaxTimeMS();
@@ -489,53 +478,61 @@ class MongoCollectionTest extends TestCase
         $this->assertSame(['type' => \MongoClient::RP_PRIMARY], $collection->getReadPreference());
         $this->assertFalse($collection->getSlaveOkay());
 
-        $this->assertTrue($collection->setReadPreference(\MongoClient::RP_SECONDARY, ['a' => 'b']));
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => ['a' => 'b']], $collection->getReadPreference());
+        $this->assertTrue($collection->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]));
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $collection->getReadPreference());
         $this->assertTrue($collection->getSlaveOkay());
 
-        // Only way to check whether options are passed down is through debugInfo
-        $writeConcern = $collection->getCollection()->__debugInfo()['readPreference'];
-
-        $this->assertSame(ReadPreference::RP_SECONDARY, $writeConcern->getMode());
-        $this->assertSame(['a' => 'b'], $writeConcern->getTagSets());
-
         $this->assertTrue($collection->setSlaveOkay(true));
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY_PREFERRED, 'tagsets' => ['a' => 'b']], $collection->getReadPreference());
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY_PREFERRED, 'tagsets' => [['a' => 'b']]], $collection->getReadPreference());
 
         $this->assertTrue($collection->setSlaveOkay(false));
-        $this->assertSame(['type' => \MongoClient::RP_PRIMARY], $collection->getReadPreference());
+        $this->assertArraySubset(['type' => \MongoClient::RP_PRIMARY], $collection->getReadPreference());
+    }
+
+    public function testReadPreferenceIsSetInDriver()
+    {
+        $this->skipTestIf(extension_loaded('mongo'));
+
+        $collection = $this->getCollection();
+
+        $this->assertTrue($collection->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]));
+
+        // Only way to check whether options are passed down is through debugInfo
+        $readPreference = $collection->getCollection()->__debugInfo()['readPreference'];
+
+        $this->assertSame(ReadPreference::RP_SECONDARY, $readPreference->getMode());
+        $this->assertSame([['a' => 'b']], $readPreference->getTagSets());
     }
 
     public function testReadPreferenceIsInherited()
     {
         $database = $this->getDatabase();
-        $database->setReadPreference(\MongoClient::RP_SECONDARY, ['a' => 'b']);
+        $database->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]);
 
         $collection = $database->selectCollection('test');
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => ['a' => 'b']], $collection->getReadPreference());
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $collection->getReadPreference());
     }
 
     public function testWriteConcern()
     {
         $collection = $this->getCollection();
-        $this->assertSame(['w' => 1, 'wtimeout' => 0], $collection->getWriteConcern());
-        $this->assertSame(1, $collection->w);
-        $this->assertSame(0, $collection->wtimeout);
 
         $this->assertTrue($collection->setWriteConcern('majority', 100));
         $this->assertSame(['w' => 'majority', 'wtimeout' => 100], $collection->getWriteConcern());
+    }
 
-        $collection->w = 2;
-        $this->assertSame(['w' => 2, 'wtimeout' => 100], $collection->getWriteConcern());
+    public function testWriteConcernIsSetInDriver()
+    {
+        $this->skipTestIf(extension_loaded('mongo'));
 
-        $collection->wtimeout = -1;
-        $this->assertSame(['w' => 2, 'wtimeout' => 0], $collection->getWriteConcern());
+        $collection = $this->getCollection();
+        $this->assertTrue($collection->setWriteConcern(2, 100));
 
         // Only way to check whether options are passed down is through debugInfo
         $writeConcern = $collection->getCollection()->__debugInfo()['writeConcern'];
 
         $this->assertSame(2, $writeConcern->getW());
-        $this->assertSame(0, $writeConcern->getWtimeout());
+        $this->assertSame(100, $writeConcern->getWtimeout());
     }
 
     public function testWriteConcernIsInherited()
@@ -552,17 +549,19 @@ class MongoCollectionTest extends TestCase
         $id = '54203e08d51d4a1f868b456e';
         $collection = $this->getCollection();
 
+        $objectId = new \MongoId($id);
         $expected = [
             'ok' => 1.0,
             'nModified' => 0,
-            'n' => 0,
+            'n' => 1,
             'err' => null,
             'errmsg' => null,
+            'upserted' => $objectId,
             'updatedExisting' => false,
         ];
 
-        $document = ['_id' => new \MongoId($id), 'foo' => 'bar'];
-        $this->assertSame($expected, $collection->save($document));
+        $document = ['_id' => $objectId, 'foo' => 'bar'];
+        $this->assertEquals($expected, $collection->save($document));
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $this->assertSame(1, $newCollection->count());
@@ -645,43 +644,26 @@ class MongoCollectionTest extends TestCase
         $document = ['foo' => 'bar'];
         $collection->save($document);
 
-        $this->setExpectedException('MongoCursorException');
+        $this->setExpectedException('MongoDuplicateKeyException');
 
         unset($document['_id']);
-        $this->assertArraySubset(
-            [
-                'ok' => 0.0,
-                'nModified' => 0,
-                'n' => 0,
-                'err' => 11000,
-                'updatedExisting' => true,
-            ],
-            $collection->save($document)
-        );
+        $collection->save($document);
     }
 
     public function testSaveEmptyKeys()
     {
-        $this->setExpectedException('MongoException');
-
         $document = [];
         $this->getCollection()->save($document);
+
+        $this->assertSame(1, $this->getCollection()->count());
     }
 
     public function testSaveEmptyObject()
     {
-        $this->setExpectedException('MongoException');
-
         $document = (object) [];
         $this->getCollection()->save($document);
-    }
 
-    public function testSaveWrite()
-    {
-        $this->setExpectedException('MongoConnectionException'); // should be MongoCursorException
-
-        $document = ['foo' => 'bar'];
-        $this->getCollection()->save($document, ['w' => 2, 'wtimeout' => 1000]);
+        $this->assertSame(1, $this->getCollection()->count());
     }
 
     public function testGetDBRef()
@@ -734,7 +716,7 @@ class MongoCollectionTest extends TestCase
 
     public function testCreateIndexInvalid()
     {
-        $this->setExpectedException('MongoException', 'keys cannot be empty');
+        $this->setExpectedException('MongoException', 'index specification has no elements');
 
         $this->getCollection()->createIndex([]);
     }
@@ -745,8 +727,8 @@ class MongoCollectionTest extends TestCase
 
         $expected = [
             'createdCollectionAutomatically' => false,
-            'numIndexesBefore' => 1,
-            'numIndexesAfter' => 1,
+            'numIndexesBefore' => 2,
+            'numIndexesAfter' => 2,
             'note' => 'all indexes already exist',
             'ok' => 1.0
         ];
@@ -773,8 +755,15 @@ class MongoCollectionTest extends TestCase
 
     public function testEnsureIndex()
     {
+        $expected = [
+            'createdCollectionAutomatically' => true,
+            'numIndexesBefore' => 1,
+            'numIndexesAfter' => 2,
+            'ok' => 1.0
+        ];
+
         $collection = $this->getCollection();
-        $this->assertTrue($collection->ensureIndex(['bar' => 1], ['unique' => true]));
+        $this->assertEquals($expected, $collection->ensureIndex(['bar' => 1], ['unique' => true]));
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
         $indexes = iterator_to_array($newCollection->listIndexes());
@@ -792,6 +781,22 @@ class MongoCollectionTest extends TestCase
 
         $expected = [
             'nIndexesWas' => 2,
+            'errmsg' => 'index not found with name [bar_1]',
+            'ok' => 0.0,
+            'code' => 27,
+        ];
+        $this->assertEquals($expected, $this->getCollection()->deleteIndex('bar'));
+
+        $this->assertCount(2, iterator_to_array($newCollection->listIndexes()));
+    }
+
+    public function testDeleteIndexUsingField()
+    {
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $newCollection->createIndex(['bar' => 1]);
+
+        $expected = [
+            'nIndexesWas' => 2,
             'ok' => 1.0,
         ];
         $this->assertSame($expected, $this->getCollection()->deleteIndex('bar'));
@@ -875,6 +880,28 @@ class MongoCollectionTest extends TestCase
         $this->assertAttributeSame('foo', 'foo', $object);
     }
 
+    public function testFindAndModifyUpdateReplace()
+    {
+        $id = '54203e08d51d4a1f868b456e';
+        $collection = $this->getCollection();
+
+        $document = ['_id' => new \MongoId($id), 'foo' => 'bar'];
+        $collection->insert($document);
+        $document = $collection->findAndModify(
+            ['_id' => new \MongoId($id)],
+            ['_id' => new \MongoId($id), 'foo' => 'boo']
+        );
+        $this->assertSame('bar', $document['foo']);
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(1, $newCollection->count());
+        $object = $newCollection->findOne();
+
+        $this->assertNotNull($object);
+        $this->assertAttributeSame('boo', 'foo', $object);
+        $this->assertObjectNotHasAttribute('bar', $object);
+    }
+
     public function testFindAndModifyUpdateReturnNew()
     {
         $id = '54203e08d51d4a1f868b456e';
@@ -1043,8 +1070,12 @@ class MongoCollectionTest extends TestCase
     {
         $collection = $this->getCollection();
 
-        $this->setExpectedException('Exception', 'Collection name cannot contain null bytes');
-
-        $collection->{'foo' . chr(0)};
+        $this->assertInstanceOf('MongoCollection', $collection->{'foo' . chr(0)});
+        $this->assertSame('test', $collection->getName());
     }
 }
+
+class PrivatePropertiesStub
+{
+    private $foo = 'bar';
+}

+ 10 - 9
tests/Alcaeus/MongoDbAdapter/MongoCommandCursorTest.php

@@ -17,7 +17,7 @@ class MongoCommandCursorTest extends TestCase
         $expected = [
             'ns' => 'mongo-php-adapter.test',
             'limit' => 0,
-            'batchSize' => null,
+            'batchSize' => 0,
             'skip' => 0,
             'flags' => 0,
             'query' => [
@@ -27,28 +27,29 @@ class MongoCommandCursorTest extends TestCase
                         '$match' => ['foo' => 'bar']
                     ]
                 ],
-                'cursor' => true,
+                'cursor' => new \stdClass(),
             ],
             'fields' => null,
             'started_iterating' => false,
         ];
-        $this->assertEquals($expected, $cursor->info());
+        $info = $cursor->info();
+        $this->assertEquals($expected, $info);
 
         // Ensure cursor started iterating
         $array = iterator_to_array($cursor);
 
         $expected['started_iterating'] = true;
         $expected += [
-            'id' => '0',
-            'at' => null,
-            'numReturned' => null,
-            'server' => null,
+            'id' => 0,
+            'at' => 0,
+            'numReturned' => 0,
+            'server' => 'localhost:27017;-;.;' . getmypid(),
             'host' => 'localhost',
             'port' => 27017,
-            'connection_type_desc' => 'STANDALONE'
+            'connection_type_desc' => 'STANDALONE',
         ];
 
-        $this->assertEquals($expected, $cursor->info());
+        $this->assertArraySubset($expected, $cursor->info());
 
         $i = 0;
         foreach ($array as $key => $value) {

+ 19 - 11
tests/Alcaeus/MongoDbAdapter/MongoCursorTest.php

@@ -20,10 +20,11 @@ class MongoCursorTest extends TestCase
 
         $iterated = 0;
         foreach ($cursor as $key => $item) {
-            $iterated++;
+            $this->assertSame($iterated, $cursor->info()['at']);
             $this->assertInstanceOf('MongoId', $item['_id']);
             $this->assertEquals($key, (string) $item['_id']);
             $this->assertSame('bar', $item['foo']);
+            $iterated++;
         }
 
         $this->assertSame(2, $iterated);
@@ -42,7 +43,7 @@ class MongoCursorTest extends TestCase
 
     public function testCountCannotConnect()
     {
-        $client = $this->getClient([], 'mongodb://localhost:28888');
+        $client = $this->getClient(['connect' => false], 'mongodb://localhost:28888');
         $cursor = $client->selectCollection('mongo-php-adapter', 'test')->find();
 
         $this->setExpectedException('MongoConnectionException');
@@ -90,6 +91,11 @@ class MongoCursorTest extends TestCase
         $collection = $this->getCollection();
         $cursor = $collection->find(['foo' => 'bar']);
 
+        $this->assertFalse($cursor->valid(), 'Cursor should be invalid to start with');
+        $this->assertNull($cursor->current(), 'Cursor should be invalid to start with');
+        $this->assertNull($cursor->key(), 'Cursor should be invalid to start with');
+
+        $cursor->next();
         $this->assertTrue($cursor->valid(), 'Cursor should be valid');
 
         $item = $cursor->current();
@@ -122,6 +128,8 @@ class MongoCursorTest extends TestCase
      */
     public function testCursorAppliesOptions($checkOptionCallback, \Closure $applyOptionCallback = null)
     {
+        $this->skipTestIf(extension_loaded('mongo'));
+
         $query = ['foo' => 'bar'];
         $projection = ['_id' => false, 'foo' => true];
 
@@ -290,7 +298,7 @@ class MongoCursorTest extends TestCase
         $expected = [
             'ns' => 'mongo-php-adapter.test',
             'limit' => 3,
-            'batchSize' => null,
+            'batchSize' => 0,
             'skip' => 1,
             'flags' => 0,
             'query' => ['foo' => 'bar'],
@@ -298,32 +306,32 @@ class MongoCursorTest extends TestCase
             'started_iterating' => false,
         ];
 
-        $this->assertSame($expected, $cursor->info());
+        $this->assertEquals($expected, $cursor->info());
 
         // Ensure cursor started iterating
         iterator_to_array($cursor);
 
         $expected['started_iterating'] = true;
         $expected += [
-            'id' => '0',
-            'at' => null,
-            'numReturned' => null,
-            'server' => null,
+            'id' => 0,
+            'at' => 1,
+            'numReturned' => 1,
+            'server' => 'localhost:27017;-;.;' . getmypid(),
             'host' => 'localhost',
             'port' => 27017,
             'connection_type_desc' => 'STANDALONE'
         ];
 
-        $this->assertSame($expected, $cursor->info());
+        $this->assertEquals($expected, $cursor->info());
     }
 
     public function testReadPreferenceIsInherited()
     {
         $collection = $this->getCollection();
-        $collection->setReadPreference(\MongoClient::RP_SECONDARY, ['a' => 'b']);
+        $collection->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]);
 
         $cursor = $collection->find(['foo' => 'bar']);
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => ['a' => 'b']], $cursor->getReadPreference());
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $cursor->getReadPreference());
     }
 
     /**

+ 3 - 2
tests/Alcaeus/MongoDbAdapter/MongoDBRefTest.php

@@ -34,17 +34,18 @@ class MongoDBRefTest extends TestCase
     public static function dataCreateThroughMongoDB()
     {
         $id = new \MongoId();
-        $validRef = ['$ref' => 'test', '$id' => $id, '$db' => 'mongo-php-adapter'];
+        $validRef = ['$ref' => 'test', '$id' => $id];
 
         $object = new \stdClass();
         $object->_id = $id;
 
+        $objectWithoutId = new \stdClass();
         return [
             'simpleId' => [$validRef, $id],
             'arrayWithIdProperty' => [$validRef, ['_id' => $id]],
             'objectWithIdProperty' => [$validRef, $object],
             'arrayWithoutId' => [null, []],
-            'objectWithoutId' => [null, new \stdClass()],
+            'objectWithoutId' => [['$ref' => 'test', '$id' => $objectWithoutId], $objectWithoutId],
         ];
     }
 

+ 41 - 27
tests/Alcaeus/MongoDbAdapter/MongoDBTest.php

@@ -71,7 +71,7 @@ class MongoDBTest extends TestCase
     {
         $database = $this->getDatabase();
 
-        $this->assertFalse($database->createCollection('test', ['capped' => 2, 'size' => 100]));
+        $this->assertInstanceOf('MongoCollection', $database->createCollection('test', ['capped' => 2, 'size' => 100]));
     }
 
     public function testGetCollectionProperty()
@@ -106,13 +106,17 @@ class MongoDBTest extends TestCase
 
         $this->failMaxTimeMS();
 
-        $this->setExpectedException('MongoCursorTimeoutException');
-
-        $database->command([
+        $result = $database->command([
             "count" => "test",
             "query" => array("a" => 1),
             "maxTimeMS" => 100,
         ]);
+
+        $this->assertSame([
+            'ok' => 0.0,
+            'errmsg' => 'operation exceeded time limit',
+            'code' => 50,
+        ], $result);
     }
 
     public function testReadPreference()
@@ -121,62 +125,72 @@ class MongoDBTest extends TestCase
         $this->assertSame(['type' => \MongoClient::RP_PRIMARY], $database->getReadPreference());
         $this->assertFalse($database->getSlaveOkay());
 
-        $this->assertTrue($database->setReadPreference(\MongoClient::RP_SECONDARY, ['a' => 'b']));
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => ['a' => 'b']], $database->getReadPreference());
+        $this->assertTrue($database->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]));
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $database->getReadPreference());
         $this->assertTrue($database->getSlaveOkay());
 
-        // Only way to check whether options are passed down is through debugInfo
-        $writeConcern = $database->getDb()->__debugInfo()['readPreference'];
-
-        $this->assertSame(ReadPreference::RP_SECONDARY, $writeConcern->getMode());
-        $this->assertSame(['a' => 'b'], $writeConcern->getTagSets());
-
         $this->assertTrue($database->setSlaveOkay(true));
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY_PREFERRED, 'tagsets' => ['a' => 'b']], $database->getReadPreference());
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY_PREFERRED, 'tagsets' => [['a' => 'b']]], $database->getReadPreference());
 
         $this->assertTrue($database->setSlaveOkay(false));
-        $this->assertSame(['type' => \MongoClient::RP_PRIMARY], $database->getReadPreference());
+        // Only test a subset since we don't keep tagsets around for RP_PRIMARY
+        $this->assertArraySubset(['type' => \MongoClient::RP_PRIMARY], $database->getReadPreference());
+    }
+
+    public function testReadPreferenceIsSetInDriver()
+    {
+        $this->skipTestIf(extension_loaded('mongo'));
+
+        $database = $this->getDatabase();
+
+        $this->assertTrue($database->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]));
+
+        // Only way to check whether options are passed down is through debugInfo
+        $readPreference = $database->getDb()->__debugInfo()['readPreference'];
+
+        $this->assertSame(ReadPreference::RP_SECONDARY, $readPreference->getMode());
+        $this->assertSame([['a' => 'b']], $readPreference->getTagSets());
+
     }
 
     public function testReadPreferenceIsInherited()
     {
         $client = $this->getClient();
-        $client->setReadPreference(\MongoClient::RP_SECONDARY, ['a' => 'b']);
+        $client->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]);
 
         $database = $client->selectDB('test');
-        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => ['a' => 'b']], $database->getReadPreference());
+        $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $database->getReadPreference());
     }
 
     public function testWriteConcern()
     {
         $database = $this->getDatabase();
-        $this->assertSame(['w' => 1, 'wtimeout' => 0], $database->getWriteConcern());
-        $this->assertSame(1, $database->w);
-        $this->assertSame(0, $database->wtimeout);
 
         $this->assertTrue($database->setWriteConcern('majority', 100));
         $this->assertSame(['w' => 'majority', 'wtimeout' => 100], $database->getWriteConcern());
+    }
 
-        $database->w = 2;
-        $this->assertSame(['w' => 2, 'wtimeout' => 100], $database->getWriteConcern());
+    public function testWriteConcernIsSetInDriver()
+    {
+        $this->skipTestIf(extension_loaded('mongo'));
 
-        $database->wtimeout = -1;
-        $this->assertSame(['w' => 2, 'wtimeout' => 0], $database->getWriteConcern());
+        $database = $this->getDatabase();
+        $this->assertTrue($database->setWriteConcern(2, 100));
 
         // Only way to check whether options are passed down is through debugInfo
         $writeConcern = $database->getDb()->__debugInfo()['writeConcern'];
 
         $this->assertSame(2, $writeConcern->getW());
-        $this->assertSame(0, $writeConcern->getWtimeout());
+        $this->assertSame(100, $writeConcern->getWtimeout());
     }
 
     public function testWriteConcernIsInherited()
     {
         $client = $this->getClient();
-        $client->setWriteConcern('majority', 100);
+        $client->setWriteConcern(2, 100);
 
         $database = $client->selectDB('test');
-        $this->assertSame(['w' => 'majority', 'wtimeout' => 100], $database->getWriteConcern());
+        $this->assertSame(['w' => 2, 'wtimeout' => 100], $database->getWriteConcern());
     }
 
     public function testProfilingLevel()
@@ -192,7 +206,7 @@ class MongoDBTest extends TestCase
     public function testForceError()
     {
         $result = $this->getDatabase()->forceError();
-        $this->assertSame(0, $result['ok']);
+        $this->assertSame(0.0, $result['ok']);
     }
 
     public function testExecute()

+ 15 - 0
tests/Alcaeus/MongoDbAdapter/MongoDateTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use Alcaeus\MongoDbAdapter\TypeInterface;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -19,6 +20,18 @@ class MongoDateTest extends TestCase
         $this->assertSame(1234567890, $dateTime->getTimestamp());
         $this->assertSame('123000', $dateTime->format('u'));
 
+        return $date;
+    }
+
+    /**
+     * @depends testCreate
+     */
+    public function testConvertToBson(\MongoDate $date)
+    {
+        $this->skipTestUnless($date instanceof TypeInterface);
+
+        $dateTime = $date->toDateTime();
+
         $bsonDate = $date->toBSONType();
         $this->assertInstanceOf('MongoDB\BSON\UTCDateTime', $bsonDate);
         $this->assertSame('1234567890123', (string) $bsonDate);
@@ -28,6 +41,8 @@ class MongoDateTest extends TestCase
 
     public function testCreateWithBsonDate()
     {
+        $this->skipTestUnless(in_array(TypeInterface::class, class_implements('MongoDate')));
+
         $bsonDate = new \MongoDB\BSON\UTCDateTime(1234567890123);
         $date = new \MongoDate($bsonDate);
 

+ 3 - 11
tests/Alcaeus/MongoDbAdapter/MongoDeleteBatchTest.php

@@ -17,12 +17,8 @@ class MongoDeleteBatchTest extends TestCase
         $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'limit' => 1]));
 
         $expected = [
-            'ok' => 1.0,
-            'nInserted' => 0,
-            'nMatched' => 0,
-            'nModified' => 0,
-            'nUpserted' => 0,
             'nRemoved' => 1,
+            'ok' => true,
         ];
 
         $this->assertSame($expected, $batch->execute());
@@ -44,12 +40,8 @@ class MongoDeleteBatchTest extends TestCase
         $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'limit' => 0]));
 
         $expected = [
-            'ok' => 1.0,
-            'nInserted' => 0,
-            'nMatched' => 0,
-            'nModified' => 0,
-            'nUpserted' => 0,
             'nRemoved' => 2,
+            'ok' => true,
         ];
 
         $this->assertSame($expected, $batch->execute());
@@ -64,7 +56,7 @@ class MongoDeleteBatchTest extends TestCase
         $collection = $this->getCollection();
         $batch = new \MongoDeleteBatch($collection);
 
-        $this->setExpectedException('Exception', 'invalid item');
+        $this->setExpectedException('Exception', "Expected \$item to contain 'q' key");
 
         $batch->add([]);
     }

+ 1 - 1
tests/Alcaeus/MongoDbAdapter/MongoGridFSCursorTest.php

@@ -19,7 +19,7 @@ class MongoGridFSCursorTest extends TestCase
 
             $this->assertArraySubset([
                 'filename' => 'foo.txt',
-                'chunkSize' => \MongoGridFS::DEFAULT_CHUNK_SIZE,
+                'chunkSize' => 261120,
                 'length' => 3,
                 'md5' => 'acbd18db4cc2f85cedef654fccc4a4d8'
             ], $value->file);

+ 10 - 9
tests/Alcaeus/MongoDbAdapter/MongoGridFSFileTest.php

@@ -32,10 +32,11 @@ class MongoGridFSFileTest extends TestCase
 
     public function testWrite()
     {
-        $id = $this->prepareFile();
         $filename = '/tmp/test-mongo-grid-fs-file';
+        $id = $this->prepareFile('abcd', ['filename' => $filename]);
         @unlink($filename);
-        $file = $this->getFile(['_id' => $id, 'length' => 4, 'filename' => $filename]);
+        $file = $this->getGridFS()->findOne(['_id' => $id]);
+        $this->assertInstanceOf(\MongoGridFSFile::class, $file);
 
         $file->write();
 
@@ -49,7 +50,8 @@ class MongoGridFSFileTest extends TestCase
         $id = $this->prepareFile();
         $filename = '/tmp/test-mongo-grid-fs-file';
         @unlink($filename);
-        $file = $this->getFile(['_id' => $id, 'length' => 4]);
+        $file = $this->getGridFS()->findOne(['_id' => $id]);
+        $this->assertInstanceOf(\MongoGridFSFile::class, $file);
 
         $file->write($filename);
 
@@ -70,13 +72,15 @@ class MongoGridFSFileTest extends TestCase
 
     public function testGetResource()
     {
-        $id = $this->prepareFile();
-        $file = $this->getFile(['_id' => $id, 'length' => 4]);
+        $data = str_repeat('a', 500 * 1024);
+        $id = $this->prepareFile($data);
+        $file = $this->getGridFS()->findOne(['_id' => $id]);
+        $this->assertInstanceOf(\MongoGridFSFile::class, $file);
 
         $result = $file->getResource();
 
         $this->assertTrue(is_resource($result));
-        $this->assertSame('abcd', stream_get_contents($result));
+        $this->assertSame($data, stream_get_contents($result));
     }
 
     /**
@@ -101,9 +105,6 @@ class MongoGridFSFileTest extends TestCase
     {
         $collection = $this->getGridFS();
 
-        // to make sure we have multiple chunks
-        $extra += ['chunkSize' => 2];
-
         return $collection->storeBytes($data, $extra);
     }
 

+ 6 - 5
tests/Alcaeus/MongoDbAdapter/MongoGridFSTest.php

@@ -230,6 +230,7 @@ class MongoGridFSTest extends TestCase
 
     public function testStoreUpload()
     {
+        $this->skipTestIf(extension_loaded('mongo'));
         $collection = $this->getGridFS();
 
         $_FILES['foo'] = [
@@ -303,7 +304,7 @@ class MongoGridFSTest extends TestCase
         $document = ['_id' => $id];
         $collection->insert($document);
 
-        $this->setExpectedException('MongoGridFSException', 'Cannot insert file record');
+        $this->setExpectedExceptionRegExp('MongoGridFSException', '/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/');
 
         $collection->storeBytes('foo', ['_id' => $id]);
     }
@@ -316,7 +317,7 @@ class MongoGridFSTest extends TestCase
         $document = ['n' => 0];
         $collection->chunks->insert($document);
 
-        $this->setExpectedException('MongoGridFSException', 'Error while inserting chunks');
+        $this->setExpectedExceptionRegExp('MongoGridFSException', '/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.chunks/');
 
         $collection->storeBytes('foo');
     }
@@ -329,7 +330,7 @@ class MongoGridFSTest extends TestCase
         $document = ['_id' => $id];
         $collection->insert($document);
 
-        $this->setExpectedException('MongoGridFSException', 'Cannot insert file record');
+        $this->setExpectedExceptionRegExp('MongoGridFSException', '/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/');
 
         $collection->storeFile(__FILE__, ['_id' => $id]);
     }
@@ -342,7 +343,7 @@ class MongoGridFSTest extends TestCase
         $document = ['n' => 0];
         $collection->chunks->insert($document);
 
-        $this->setExpectedException('MongoGridFSException', 'Error while inserting chunks');
+        $this->setExpectedExceptionRegExp('MongoGridFSException', '/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.chunks/');
 
         $collection->storeFile(__FILE__);
     }
@@ -355,7 +356,7 @@ class MongoGridFSTest extends TestCase
         $document = ['length' => filesize(__FILE__)];
         $collection->insert($document);
 
-        $this->setExpectedException('MongoGridFSException', 'Error updating file record');
+        $this->setExpectedExceptionRegExp('MongoGridFSException', '/Could not store file:.* E11000 duplicate key error .* mongo-php-adapter\.fs\.files/');
 
         $collection->storeFile(fopen(__FILE__, 'r'));
     }

+ 7 - 3
tests/Alcaeus/MongoDbAdapter/MongoIdTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use MongoDB\BSON\ObjectID;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -43,10 +44,12 @@ class MongoIdTest extends TestCase
         new \MongoId('invalid');
     }
 
-    public function testCreateWithObjetId()
+    public function testCreateWithObjectId()
     {
+        $this->skipTestIf(extension_loaded('mongo'));
+
         $original = '54203e08d51d4a1f868b456e';
-        $objectId = new \MongoDB\BSON\ObjectID($original);
+        $objectId = new ObjectID($original);
 
         $id = new \MongoId($objectId);
         $this->assertSame($original, (string) $id);
@@ -59,6 +62,7 @@ class MongoIdTest extends TestCase
      */
     public function testIsValid($expected, $value)
     {
+        $this->skipTestIf($value instanceof ObjectID && extension_loaded('mongo'));
         $this->assertSame($expected, \MongoId::isValid($value));
     }
 
@@ -69,7 +73,7 @@ class MongoIdTest extends TestCase
         return [
             'validId' => [true, '' . $original . ''],
             'MongoId' => [true, new \MongoId($original)],
-            'ObjectID' => [true, new \MongoDB\BSON\ObjectID($original)],
+            'ObjectID' => [true, new ObjectID($original)],
             'invalidString' => [false, 'abc'],
         ];
     }

+ 14 - 11
tests/Alcaeus/MongoDbAdapter/MongoInsertBatchTest.php

@@ -12,12 +12,8 @@ class MongoInsertBatchTest extends TestCase
         $this->assertTrue($batch->add(['bar' => 'foo']));
 
         $expected = [
-            'ok' => 1.0,
             'nInserted' => 2,
-            'nMatched' => 0,
-            'nModified' => 0,
-            'nUpserted' => 0,
-            'nRemoved' => 0,
+            'ok' => true,
         ];
 
         $this->assertSame($expected, $batch->execute());
@@ -40,14 +36,21 @@ class MongoInsertBatchTest extends TestCase
         $this->assertTrue($batch->add(['foo' => 'bar']));
 
         $expected = [
-            'ok' => 0.0,
+            'writeErrors' => [
+                [
+                    'index' => 1,
+                    'code' => 11000,
+                ]
+            ],
             'nInserted' => 1,
-            'nMatched' => 0,
-            'nModified' => 0,
-            'nUpserted' => 0,
-            'nRemoved' => 0,
+            'ok' => true,
         ];
 
-        $this->assertSame($expected, $batch->execute());
+        try {
+            $batch->execute();
+        } catch (\MongoWriteConcernException $e) {
+            $this->assertSame('Failed write', $e->getMessage());
+            $this->assertArraySubset($expected, $e->getDocument());
+        }
     }
 }

+ 4 - 2
tests/Alcaeus/MongoDbAdapter/MongoMaxKeyTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use Alcaeus\MongoDbAdapter\TypeInterface;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -9,7 +10,8 @@ class MongoMaxKeyTest extends TestCase
 {
     public function testConvert()
     {
-        $MaxKey = new \MongoMaxKey();
-        $this->assertInstanceOf('MongoDB\BSON\MaxKey', $MaxKey->toBSONType());
+        $maxKey = new \MongoMaxKey();
+        $this->skipTestUnless($maxKey instanceof TypeInterface);
+        $this->assertInstanceOf('MongoDB\BSON\MaxKey', $maxKey->toBSONType());
     }
 }

+ 2 - 0
tests/Alcaeus/MongoDbAdapter/MongoMinKeyTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use Alcaeus\MongoDbAdapter\TypeInterface;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -10,6 +11,7 @@ class MongoMinKeyTest extends TestCase
     public function testConvert()
     {
         $minKey = new \MongoMinKey();
+        $this->skipTestUnless($minKey instanceof TypeInterface);
         $this->assertInstanceOf('MongoDB\BSON\MinKey', $minKey->toBSONType());
     }
 }

+ 13 - 0
tests/Alcaeus/MongoDbAdapter/MongoRegexTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use Alcaeus\MongoDbAdapter\TypeInterface;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -15,6 +16,16 @@ class MongoRegexTest extends TestCase
 
         $this->assertSame('/abc/i', (string) $regex);
 
+        return $regex;
+    }
+
+    /**
+     * @depends testCreate
+     */
+    public function testConvertToBson(\MongoRegex $regex)
+    {
+        $this->skipTestUnless($regex instanceof TypeInterface);
+
         $bsonRegex = $regex->toBSONType();
         $this->assertInstanceOf('MongoDB\BSON\Regex', $bsonRegex);
         $this->assertSame('abc', $bsonRegex->getPattern());
@@ -23,6 +34,8 @@ class MongoRegexTest extends TestCase
 
     public function testCreateWithBsonType()
     {
+        $this->skipTestUnless(in_array(TypeInterface::class, class_implements('MongoRegex')));
+
         $bsonRegex = new \MongoDB\BSON\Regex('abc', 'i');
         $regex = new \MongoRegex($bsonRegex);
 

+ 13 - 0
tests/Alcaeus/MongoDbAdapter/MongoTimestampTest.php

@@ -1,6 +1,7 @@
 <?php
 
 namespace Alcaeus\MongoDbAdapter\Tests;
+use Alcaeus\MongoDbAdapter\TypeInterface;
 
 /**
  * @author alcaeus <alcaeus@alcaeus.org>
@@ -15,6 +16,16 @@ class MongoTimestampTest extends TestCase
 
         $this->assertSame('1234567890', (string) $timestamp);
 
+        return $timestamp;
+    }
+
+    /**
+     * @depends testCreate
+     */
+    public function testConvertToBson(\MongoTimestamp $timestamp)
+    {
+        $this->skipTestUnless($timestamp instanceof TypeInterface);
+
         $bsonTimestamp = $timestamp->toBSONType();
         $this->assertInstanceOf('MongoDB\BSON\Timestamp', $bsonTimestamp);
         $this->assertSame('[1234567890:987654321]', (string) $bsonTimestamp);
@@ -31,6 +42,8 @@ class MongoTimestampTest extends TestCase
 
     public function testCreateWithBsonTimestamp()
     {
+        $this->skipTestUnless(in_array(TypeInterface::class, class_implements('MongoTimestamp')));
+
         $bsonTimestamp = new \MongoDB\BSON\Timestamp(1234567890, 987654321);
         $timestamp = new \MongoTimestamp($bsonTimestamp);
 

+ 21 - 15
tests/Alcaeus/MongoDbAdapter/MongoUpdateBatchTest.php

@@ -17,12 +17,10 @@ class MongoUpdateBatchTest extends TestCase
         $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'u' => ['$set' => ['foo' => 'foo']]]));
 
         $expected = [
-            'ok' => 1.0,
-            'nInserted' => 0,
             'nMatched' => 1,
             'nModified' => 1,
             'nUpserted' => 0,
-            'nRemoved' => 0,
+            'ok' => true,
         ];
 
         $this->assertSame($expected, $batch->execute());
@@ -49,12 +47,10 @@ class MongoUpdateBatchTest extends TestCase
         $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'u' => ['$set' => ['foo' => 'foo']], 'multi' => true]));
 
         $expected = [
-            'ok' => 1.0,
-            'nInserted' => 0,
             'nMatched' => 2,
             'nModified' => 2,
             'nUpserted' => 0,
-            'nRemoved' => 0,
+            'ok' => true,
         ];
 
         $this->assertSame($expected, $batch->execute());
@@ -69,23 +65,33 @@ class MongoUpdateBatchTest extends TestCase
 
     public function testUpsert()
     {
+        $document = ['foo' => 'foo'];
+        $this->getCollection()->insert($document);
         $batch = new \MongoUpdateBatch($this->getCollection());
 
-        $this->assertTrue($batch->add(['q' => [], 'u' => ['$set' => ['foo' => 'bar']], 'upsert' => true]));
+        $this->assertTrue($batch->add(['q' => ['foo' => 'foo'], 'u' => ['$set' => ['foo' => 'bar']], 'upsert' => true]));
+        $this->assertTrue($batch->add(['q' => ['bar' => 'foo'], 'u' => ['$set' => ['foo' => 'bar']], 'upsert' => true]));
 
         $expected = [
-            'ok' => 1.0,
-            'nInserted' => 0,
-            'nMatched' => 0,
-            'nModified' => 0,
+            'upserted' => [
+                [
+                    'index' => 1,
+                ]
+            ],
+            'nMatched' => 1,
+            'nModified' => 1,
             'nUpserted' => 1,
-            'nRemoved' => 0,
+            'ok' => true,
         ];
 
-        $this->assertSame($expected, $batch->execute());
+        $result = $batch->execute();
+        $this->assertArraySubset($expected, $result);
+
+        $this->assertInstanceOf('MongoId', $result['upserted'][0]['_id']);
 
         $newCollection = $this->getCheckDatabase()->selectCollection('test');
-        $this->assertSame(1, $newCollection->count());
+        $this->assertSame(0, $newCollection->count(['foo' => 'foo']));
+        $this->assertSame(2, $newCollection->count());
         $record = $newCollection->findOne();
         $this->assertNotNull($record);
         $this->assertObjectHasAttribute('foo', $record);
@@ -97,7 +103,7 @@ class MongoUpdateBatchTest extends TestCase
         $collection = $this->getCollection();
         $batch = new \MongoUpdateBatch($collection);
 
-        $this->setExpectedException('Exception', 'invalid item');
+        $this->setExpectedException('Exception', "Expected \$item to contain 'q' key");
 
         $batch->add([]);
     }

+ 18 - 0
tests/Alcaeus/MongoDbAdapter/TestCase.php

@@ -145,4 +145,22 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
     {
         return $this->configureFailPoint("maxTimeAlwaysTimeOut", array("times" => 1));
     }
+
+    /**
+     * @param bool $condition
+     */
+    protected function skipTestUnless($condition)
+    {
+        $this->skipTestIf(! $condition);
+    }
+
+    /**
+     * @param bool $condition
+     */
+    protected function skipTestIf($condition)
+    {
+        if ($condition) {
+            $this->markTestSkipped('Test only applies when running against mongo-php-adapter');
+        }
+    }
 }