MongoCursorTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <?php
  2. namespace Alcaeus\MongoDbAdapter\Tests\Mongo;
  3. use Alcaeus\MongoDbAdapter\Tests\TestCase;
  4. use Alcaeus\MongoDbAdapter\TypeConverter;
  5. use MongoDB\Driver\ReadPreference;
  6. use MongoDB\Model\BSONDocument;
  7. use MongoDB\Operation\Find;
  8. /**
  9. * @author alcaeus <alcaeus@alcaeus.org>
  10. */
  11. class MongoCursorTest extends TestCase
  12. {
  13. public function testSerialize()
  14. {
  15. $this->prepareData();
  16. $cursor = $this->getCollection()->find(['foo' => 'bar']);
  17. $this->assertInternalType('string', serialize($cursor));
  18. }
  19. public function testCursorConvertsTypes()
  20. {
  21. $this->prepareData();
  22. $collection = $this->getCollection();
  23. $cursor = $collection->find(['foo' => 'bar']);
  24. $this->assertCount(2, $cursor);
  25. $this->assertCursorIteration($cursor);
  26. }
  27. public function testCursorHandlesHasNextBeforeIteration()
  28. {
  29. $this->prepareData();
  30. $collection = $this->getCollection();
  31. $cursor = $collection->find(['foo' => 'bar']);
  32. $this->assertTrue($cursor->hasNext());
  33. $this->assertCursorIteration($cursor);
  34. }
  35. private function assertCursorIteration($cursor)
  36. {
  37. $iterated = 0;
  38. foreach ($cursor as $key => $item) {
  39. $this->assertSame($iterated, $cursor->info()['at']);
  40. $this->assertInstanceOf('MongoId', $item['_id']);
  41. $this->assertEquals($key, (string) $item['_id']);
  42. $this->assertSame('bar', $item['foo']);
  43. $iterated++;
  44. }
  45. $this->assertSame(2, $iterated);
  46. }
  47. public function testCount()
  48. {
  49. $this->prepareData();
  50. $collection = $this->getCollection();
  51. $cursor = $collection->find(['foo' => 'bar'])->limit(1);
  52. $this->assertSame(2, $cursor->count());
  53. $this->assertSame(1, $cursor->count(true));
  54. }
  55. public function testCountCannotConnect()
  56. {
  57. $client = $this->getClient(['connect' => false], 'mongodb://localhost:28888');
  58. $cursor = $client->selectCollection('mongo-php-adapter', 'test')->find();
  59. $this->expectException(\MongoConnectionException::class);
  60. $cursor->count();
  61. }
  62. public function testCountAfterIteration()
  63. {
  64. $this->prepareData();
  65. $collection = $this->getCollection();
  66. $cursor = $collection->find(['foo' => 'bar']);
  67. // Ensure the generator is consumed and thus closed
  68. iterator_to_array($cursor);
  69. $this->assertSame(2, $cursor->count(true));
  70. }
  71. public function testNextStartsWithFirstItem()
  72. {
  73. $this->prepareData();
  74. $collection = $this->getCollection();
  75. $cursor = $collection->find(['foo' => 'bar']);
  76. $this->assertTrue($cursor->hasNext());
  77. $item = $cursor->getNext();
  78. $this->assertNotNull($item);
  79. $this->assertInstanceOf('MongoId', $item['_id']);
  80. $this->assertSame('bar', $item['foo']);
  81. $this->assertTrue($cursor->hasNext());
  82. $item = $cursor->getNext();
  83. $this->assertNotNull($item);
  84. $this->assertInstanceOf('MongoId', $item['_id']);
  85. $this->assertSame('bar', $item['foo']);
  86. $this->assertFalse($cursor->hasNext());
  87. $item = $cursor->getNext();
  88. $this->assertNull($item);
  89. $cursor->reset();
  90. $this->assertTrue($cursor->hasNext());
  91. $item = $cursor->getNext();
  92. $this->assertNotNull($item);
  93. $this->assertInstanceOf('MongoId', $item['_id']);
  94. $this->assertSame('bar', $item['foo']);
  95. $item = $cursor->getNext();
  96. $this->assertNotNull($item);
  97. $this->assertInstanceOf('MongoId', $item['_id']);
  98. $this->assertSame('bar', $item['foo']);
  99. }
  100. public function testIteratorInterface()
  101. {
  102. $this->prepareData();
  103. $collection = $this->getCollection();
  104. $cursor = $collection->find(['foo' => 'bar']);
  105. $this->assertFalse($cursor->valid(), 'Cursor should be invalid to start with');
  106. $this->assertNull($cursor->current(), 'Cursor should be invalid to start with');
  107. $this->assertNull($cursor->key(), 'Cursor should be invalid to start with');
  108. $cursor->next();
  109. $this->assertTrue($cursor->valid(), 'Cursor should be valid');
  110. $item = $cursor->current();
  111. $this->assertNotNull($item);
  112. $this->assertInstanceOf('MongoId', $item['_id']);
  113. $this->assertSame('bar', $item['foo']);
  114. $cursor->next();
  115. $item = $cursor->current();
  116. $this->assertNotNull($item);
  117. $this->assertInstanceOf('MongoId', $item['_id']);
  118. $this->assertSame('bar', $item['foo']);
  119. $cursor->next();
  120. $this->assertNull($cursor->current(), 'Cursor should return null at the end');
  121. $this->assertFalse($cursor->valid(), 'Cursor should be invalid');
  122. $cursor->rewind();
  123. $item = $cursor->current();
  124. $this->assertNotNull($item);
  125. $this->assertInstanceOf('MongoId', $item['_id']);
  126. $this->assertSame('bar', $item['foo']);
  127. }
  128. /**
  129. * @dataProvider getCursorOptions
  130. */
  131. public function testCursorAppliesOptions($checkOptionCallback, \Closure $applyOptionCallback = null)
  132. {
  133. $this->skipTestIf(extension_loaded('mongo'));
  134. $query = ['foo' => 'bar'];
  135. $projection = ['_id' => false, 'foo' => true];
  136. $collectionMock = $this->getCollectionMock();
  137. $collectionMock
  138. ->expects($this->once())
  139. ->method('find')
  140. ->with($this->equalTo(TypeConverter::fromLegacy($query)), $this->callback($checkOptionCallback))
  141. ->will($this->returnValue(new \ArrayIterator([])));
  142. $collection = $this->getCollection('test');
  143. $cursor = $collection->find($query, $projection);
  144. // Replace the original MongoDB collection with our mock
  145. $reflectionProperty = new \ReflectionProperty($cursor, 'collection');
  146. $reflectionProperty->setAccessible(true);
  147. $reflectionProperty->setValue($cursor, $collectionMock);
  148. if ($applyOptionCallback !== null) {
  149. $applyOptionCallback($cursor);
  150. }
  151. // Force query by converting to array
  152. iterator_to_array($cursor);
  153. }
  154. public static function getCursorOptions()
  155. {
  156. function getMissingOptionCallback($optionName) {
  157. return function ($value) use ($optionName) {
  158. return
  159. is_array($value) &&
  160. ! array_key_exists($optionName, $value);
  161. };
  162. }
  163. function getBasicCheckCallback($expected, $optionName) {
  164. return function ($value) use ($expected, $optionName) {
  165. return
  166. is_array($value) &&
  167. array_key_exists($optionName, $value) &&
  168. $value[$optionName] == $expected;
  169. };
  170. }
  171. function getModifierCheckCallback($expected, $modifierName) {
  172. return function ($value) use ($expected, $modifierName) {
  173. return
  174. is_array($value) &&
  175. is_array($value['modifiers']) &&
  176. array_key_exists($modifierName, $value['modifiers']) &&
  177. $value['modifiers'][$modifierName] == $expected;
  178. };
  179. }
  180. $tests = [
  181. 'allowPartialResults' => [
  182. getBasicCheckCallback(true, 'allowPartialResults'),
  183. function (\MongoCursor $cursor) {
  184. $cursor->partial(true);
  185. },
  186. ],
  187. 'batchSize' => [
  188. getBasicCheckCallback(10, 'batchSize'),
  189. function (\MongoCursor $cursor) {
  190. $cursor->batchSize(10);
  191. },
  192. ],
  193. 'cursorTypeNonTailable' => [
  194. getMissingOptionCallback('cursorType'),
  195. function (\MongoCursor $cursor) {
  196. $cursor
  197. ->tailable(false)
  198. ->awaitData(true);
  199. },
  200. ],
  201. 'cursorTypeTailable' => [
  202. getBasicCheckCallback(Find::TAILABLE, 'cursorType'),
  203. function (\MongoCursor $cursor) {
  204. $cursor->tailable(true);
  205. },
  206. ],
  207. 'cursorTypeTailableAwait' => [
  208. getBasicCheckCallback(Find::TAILABLE_AWAIT, 'cursorType'),
  209. function (\MongoCursor $cursor) {
  210. $cursor->tailable(true)->awaitData(true);
  211. },
  212. ],
  213. 'hint' => [
  214. getModifierCheckCallback('index_name', '$hint'),
  215. function (\MongoCursor $cursor) {
  216. $cursor->hint('index_name');
  217. },
  218. ],
  219. 'limit' => [
  220. getBasicCheckCallback(5, 'limit'),
  221. function (\MongoCursor $cursor) {
  222. $cursor->limit(5);
  223. }
  224. ],
  225. 'maxTimeMS' => [
  226. getBasicCheckCallback(100, 'maxTimeMS'),
  227. function (\MongoCursor $cursor) {
  228. $cursor->maxTimeMS(100);
  229. },
  230. ],
  231. 'noCursorTimeout' => [
  232. getBasicCheckCallback(true, 'noCursorTimeout'),
  233. function (\MongoCursor $cursor) {
  234. $cursor->immortal(true);
  235. },
  236. ],
  237. 'slaveOkay' => [
  238. getBasicCheckCallback(new ReadPreference(ReadPreference::RP_SECONDARY_PREFERRED), 'readPreference'),
  239. function (\MongoCursor $cursor) {
  240. $cursor->slaveOkay(true);
  241. },
  242. ],
  243. 'slaveOkayWithReadPreferenceSet' => [
  244. getBasicCheckCallback(new ReadPreference(ReadPreference::RP_SECONDARY), 'readPreference'),
  245. function (\MongoCursor $cursor) {
  246. $cursor
  247. ->setReadPreference(\MongoClient::RP_SECONDARY)
  248. ->slaveOkay(true);
  249. },
  250. ],
  251. 'projectionDefaultFields' => [
  252. getBasicCheckCallback(new BSONDocument(['_id' => false, 'foo' => true]), 'projection'),
  253. ],
  254. 'projectionDifferentFields' => [
  255. getBasicCheckCallback(new BSONDocument(['_id' => false, 'foo' => true, 'bar' => true]), 'projection'),
  256. function (\MongoCursor $cursor) {
  257. $cursor->fields(['_id' => false, 'foo' => true, 'bar' => true]);
  258. },
  259. ],
  260. 'readPreferencePrimary' => [
  261. getBasicCheckCallback(new ReadPreference(ReadPreference::RP_PRIMARY), 'readPreference'),
  262. function (\MongoCursor $cursor) {
  263. $cursor->setReadPreference(\MongoClient::RP_PRIMARY);
  264. },
  265. ],
  266. 'skip' => [
  267. getBasicCheckCallback(5, 'skip'),
  268. function (\MongoCursor $cursor) {
  269. $cursor->skip(5);
  270. },
  271. ],
  272. 'sort' => [
  273. getBasicCheckCallback(['foo' => -1], 'sort'),
  274. function (\MongoCursor $cursor) {
  275. $cursor->sort(['foo' => -1]);
  276. },
  277. ],
  278. ];
  279. return $tests;
  280. }
  281. public function testCursorInfo()
  282. {
  283. $this->prepareData();
  284. $collection = $this->getCollection();
  285. $cursor = $collection->find(['foo' => 'bar'], ['_id' => false])->skip(1)->limit(3);
  286. $expected = [
  287. 'ns' => 'mongo-php-adapter.test',
  288. 'limit' => 3,
  289. 'batchSize' => 0,
  290. 'skip' => 1,
  291. 'flags' => 0,
  292. 'query' => ['foo' => 'bar'],
  293. 'fields' => ['_id' => false],
  294. 'started_iterating' => false,
  295. ];
  296. $this->assertSame($expected, $cursor->info());
  297. // Ensure cursor started iterating
  298. iterator_to_array($cursor);
  299. $expected['started_iterating'] = true;
  300. $expected += [
  301. 'id' => 0,
  302. 'at' => 1,
  303. 'numReturned' => 1,
  304. 'server' => 'localhost:27017;-;.;' . getmypid(),
  305. 'host' => 'localhost',
  306. 'port' => 27017,
  307. 'connection_type_desc' => 'STANDALONE'
  308. ];
  309. $this->assertSame($expected, $cursor->info());
  310. }
  311. public function testCursorInfoWithBatchSize()
  312. {
  313. $this->prepareData();
  314. $collection = $this->getCollection();
  315. $cursor = $collection->find(['foo' => 'bar'], ['_id' => false])->skip(1)->limit(3);
  316. $cursor->batchSize(1);
  317. $expected = [
  318. 'ns' => 'mongo-php-adapter.test',
  319. 'limit' => 3,
  320. 'batchSize' => 1,
  321. 'skip' => 1,
  322. 'flags' => 0,
  323. 'query' => ['foo' => 'bar'],
  324. 'fields' => ['_id' => false],
  325. 'started_iterating' => false,
  326. ];
  327. $this->assertSame($expected, $cursor->info());
  328. // Ensure cursor started iterating
  329. iterator_to_array($cursor);
  330. $expected['started_iterating'] = true;
  331. $expected += [
  332. 'id' => 0,
  333. 'at' => 1,
  334. 'numReturned' => 1,
  335. 'server' => 'localhost:27017;-;.;' . getmypid(),
  336. 'host' => 'localhost',
  337. 'port' => 27017,
  338. 'connection_type_desc' => 'STANDALONE'
  339. ];
  340. $this->assertSame($expected, $cursor->info());
  341. }
  342. public function testReadPreferenceIsInherited()
  343. {
  344. $collection = $this->getCollection();
  345. $collection->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]);
  346. $cursor = $collection->find(['foo' => 'bar']);
  347. $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $cursor->getReadPreference());
  348. }
  349. public function testExplain()
  350. {
  351. $this->prepareData();
  352. $collection = $this->getCollection();
  353. $cursor = $collection->find(['foo' => 'bar'], ['_id' => false])->skip(1)->limit(3);
  354. $expected = [
  355. 'queryPlanner' => [
  356. 'plannerVersion' => 1,
  357. 'namespace' => 'mongo-php-adapter.test',
  358. 'indexFilterSet' => false,
  359. 'parsedQuery' => [
  360. 'foo' => ['$eq' => 'bar']
  361. ],
  362. 'winningPlan' => [],
  363. 'rejectedPlans' => [],
  364. ],
  365. 'executionStats' => [
  366. 'executionSuccess' => true,
  367. 'nReturned' => 1,
  368. 'totalKeysExamined' => 0,
  369. 'totalDocsExamined' => 3,
  370. 'executionStages' => [],
  371. 'allPlansExecution' => [],
  372. ],
  373. 'serverInfo' => [
  374. 'port' => 27017,
  375. ],
  376. ];
  377. $this->assertArraySubset($expected, $cursor->explain());
  378. }
  379. /**
  380. * @return \PHPUnit_Framework_MockObject_MockObject
  381. */
  382. protected function getCollectionMock()
  383. {
  384. return $this->createMock('MongoDB\Collection', [], [], '', false);
  385. }
  386. }