MongoCursorTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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. {
  158. return function ($value) use ($optionName) {
  159. return
  160. is_array($value) &&
  161. ! array_key_exists($optionName, $value);
  162. };
  163. }
  164. function getBasicCheckCallback($expected, $optionName)
  165. {
  166. return function ($value) use ($expected, $optionName) {
  167. return
  168. is_array($value) &&
  169. array_key_exists($optionName, $value) &&
  170. $value[$optionName] == $expected;
  171. };
  172. }
  173. function getModifierCheckCallback($expected, $modifierName)
  174. {
  175. return function ($value) use ($expected, $modifierName) {
  176. return
  177. is_array($value) &&
  178. is_array($value['modifiers']) &&
  179. array_key_exists($modifierName, $value['modifiers']) &&
  180. $value['modifiers'][$modifierName] == $expected;
  181. };
  182. }
  183. $tests = [
  184. 'allowPartialResults' => [
  185. getBasicCheckCallback(true, 'allowPartialResults'),
  186. function (\MongoCursor $cursor) {
  187. $cursor->partial(true);
  188. },
  189. ],
  190. 'batchSize' => [
  191. getBasicCheckCallback(10, 'batchSize'),
  192. function (\MongoCursor $cursor) {
  193. $cursor->batchSize(10);
  194. },
  195. ],
  196. 'cursorTypeNonTailable' => [
  197. getMissingOptionCallback('cursorType'),
  198. function (\MongoCursor $cursor) {
  199. $cursor
  200. ->tailable(false)
  201. ->awaitData(true);
  202. },
  203. ],
  204. 'cursorTypeTailable' => [
  205. getBasicCheckCallback(Find::TAILABLE, 'cursorType'),
  206. function (\MongoCursor $cursor) {
  207. $cursor->tailable(true);
  208. },
  209. ],
  210. 'cursorTypeTailableAwait' => [
  211. getBasicCheckCallback(Find::TAILABLE_AWAIT, 'cursorType'),
  212. function (\MongoCursor $cursor) {
  213. $cursor->tailable(true)->awaitData(true);
  214. },
  215. ],
  216. 'hint' => [
  217. getModifierCheckCallback('index_name', '$hint'),
  218. function (\MongoCursor $cursor) {
  219. $cursor->hint('index_name');
  220. },
  221. ],
  222. 'limit' => [
  223. getBasicCheckCallback(5, 'limit'),
  224. function (\MongoCursor $cursor) {
  225. $cursor->limit(5);
  226. }
  227. ],
  228. 'maxTimeMS' => [
  229. getBasicCheckCallback(100, 'maxTimeMS'),
  230. function (\MongoCursor $cursor) {
  231. $cursor->maxTimeMS(100);
  232. },
  233. ],
  234. 'noCursorTimeout' => [
  235. getBasicCheckCallback(true, 'noCursorTimeout'),
  236. function (\MongoCursor $cursor) {
  237. $cursor->immortal(true);
  238. },
  239. ],
  240. 'slaveOkay' => [
  241. getBasicCheckCallback(new ReadPreference(ReadPreference::RP_SECONDARY_PREFERRED), 'readPreference'),
  242. function (\MongoCursor $cursor) {
  243. $cursor->slaveOkay(true);
  244. },
  245. ],
  246. 'slaveOkayWithReadPreferenceSet' => [
  247. getBasicCheckCallback(new ReadPreference(ReadPreference::RP_SECONDARY), 'readPreference'),
  248. function (\MongoCursor $cursor) {
  249. $cursor
  250. ->setReadPreference(\MongoClient::RP_SECONDARY)
  251. ->slaveOkay(true);
  252. },
  253. ],
  254. 'projectionDefaultFields' => [
  255. getBasicCheckCallback(new BSONDocument(['_id' => false, 'foo' => true]), 'projection'),
  256. ],
  257. 'projectionDifferentFields' => [
  258. getBasicCheckCallback(new BSONDocument(['_id' => false, 'foo' => true, 'bar' => true]), 'projection'),
  259. function (\MongoCursor $cursor) {
  260. $cursor->fields(['_id' => false, 'foo' => true, 'bar' => true]);
  261. },
  262. ],
  263. 'readPreferencePrimary' => [
  264. getBasicCheckCallback(new ReadPreference(ReadPreference::RP_PRIMARY), 'readPreference'),
  265. function (\MongoCursor $cursor) {
  266. $cursor->setReadPreference(\MongoClient::RP_PRIMARY);
  267. },
  268. ],
  269. 'skip' => [
  270. getBasicCheckCallback(5, 'skip'),
  271. function (\MongoCursor $cursor) {
  272. $cursor->skip(5);
  273. },
  274. ],
  275. 'sort' => [
  276. getBasicCheckCallback(['foo' => -1], 'sort'),
  277. function (\MongoCursor $cursor) {
  278. $cursor->sort(['foo' => -1]);
  279. },
  280. ],
  281. ];
  282. return $tests;
  283. }
  284. public function testCursorInfo()
  285. {
  286. $this->prepareData();
  287. $collection = $this->getCollection();
  288. $cursor = $collection->find(['foo' => 'bar'], ['_id' => false])->skip(1)->limit(3);
  289. $expected = [
  290. 'ns' => 'mongo-php-adapter.test',
  291. 'limit' => 3,
  292. 'batchSize' => 0,
  293. 'skip' => 1,
  294. 'flags' => 0,
  295. 'query' => ['foo' => 'bar'],
  296. 'fields' => ['_id' => false],
  297. 'started_iterating' => false,
  298. ];
  299. $this->assertSame($expected, $cursor->info());
  300. // Ensure cursor started iterating
  301. iterator_to_array($cursor);
  302. $expected['started_iterating'] = true;
  303. $expected += [
  304. 'id' => 0,
  305. 'at' => 1,
  306. 'numReturned' => 1,
  307. 'server' => 'localhost:27017;-;.;' . getmypid(),
  308. 'host' => 'localhost',
  309. 'port' => 27017,
  310. 'connection_type_desc' => 'STANDALONE'
  311. ];
  312. $this->assertSame($expected, $cursor->info());
  313. }
  314. public function testCursorInfoWithBatchSize()
  315. {
  316. $this->prepareData();
  317. $collection = $this->getCollection();
  318. $cursor = $collection->find(['foo' => 'bar'], ['_id' => false])->skip(1)->limit(3);
  319. $cursor->batchSize(1);
  320. $expected = [
  321. 'ns' => 'mongo-php-adapter.test',
  322. 'limit' => 3,
  323. 'batchSize' => 1,
  324. 'skip' => 1,
  325. 'flags' => 0,
  326. 'query' => ['foo' => 'bar'],
  327. 'fields' => ['_id' => false],
  328. 'started_iterating' => false,
  329. ];
  330. $this->assertSame($expected, $cursor->info());
  331. // Ensure cursor started iterating
  332. iterator_to_array($cursor);
  333. $expected['started_iterating'] = true;
  334. $expected += [
  335. 'id' => 0,
  336. 'at' => 1,
  337. 'numReturned' => 1,
  338. 'server' => 'localhost:27017;-;.;' . getmypid(),
  339. 'host' => 'localhost',
  340. 'port' => 27017,
  341. 'connection_type_desc' => 'STANDALONE'
  342. ];
  343. $this->assertSame($expected, $cursor->info());
  344. }
  345. public function testReadPreferenceIsInherited()
  346. {
  347. $collection = $this->getCollection();
  348. $collection->setReadPreference(\MongoClient::RP_SECONDARY, [['a' => 'b']]);
  349. $cursor = $collection->find(['foo' => 'bar']);
  350. $this->assertSame(['type' => \MongoClient::RP_SECONDARY, 'tagsets' => [['a' => 'b']]], $cursor->getReadPreference());
  351. }
  352. public function testExplain()
  353. {
  354. $this->prepareData();
  355. $collection = $this->getCollection();
  356. $cursor = $collection->find(['foo' => 'bar'], ['_id' => false])->skip(1)->limit(3);
  357. $expected = [
  358. 'queryPlanner' => [
  359. 'plannerVersion' => 1,
  360. 'namespace' => 'mongo-php-adapter.test',
  361. 'indexFilterSet' => false,
  362. 'parsedQuery' => [
  363. 'foo' => ['$eq' => 'bar']
  364. ],
  365. 'winningPlan' => [],
  366. 'rejectedPlans' => [],
  367. ],
  368. 'executionStats' => [
  369. 'executionSuccess' => true,
  370. 'nReturned' => 1,
  371. 'totalKeysExamined' => 0,
  372. 'totalDocsExamined' => 3,
  373. 'executionStages' => [],
  374. 'allPlansExecution' => [],
  375. ],
  376. 'serverInfo' => [
  377. 'port' => 27017,
  378. ],
  379. ];
  380. $this->assertArraySubset($expected, $cursor->explain());
  381. }
  382. public function testExplainWithEmptyProjection()
  383. {
  384. $this->prepareData();
  385. $collection = $this->getCollection();
  386. $cursor = $collection->find(['foo' => 'bar']);
  387. $expected = [
  388. 'queryPlanner' => [
  389. 'plannerVersion' => 1,
  390. 'namespace' => 'mongo-php-adapter.test',
  391. 'indexFilterSet' => false,
  392. 'parsedQuery' => [
  393. 'foo' => ['$eq' => 'bar']
  394. ],
  395. 'winningPlan' => [],
  396. 'rejectedPlans' => [],
  397. ],
  398. 'executionStats' => [
  399. 'executionSuccess' => true,
  400. 'nReturned' => 2,
  401. 'totalKeysExamined' => 0,
  402. 'totalDocsExamined' => 3,
  403. 'executionStages' => [],
  404. 'allPlansExecution' => [],
  405. ],
  406. 'serverInfo' => [
  407. 'port' => 27017,
  408. ],
  409. ];
  410. $this->assertArraySubset($expected, $cursor->explain());
  411. }
  412. public function testExplainConvertsQuery()
  413. {
  414. $this->prepareData();
  415. $collection = $this->getCollection();
  416. $cursor = $collection->find(['foo' => new \MongoRegex('/^b/')]);
  417. $expected = [
  418. 'queryPlanner' => [
  419. 'plannerVersion' => 1,
  420. 'namespace' => 'mongo-php-adapter.test',
  421. 'indexFilterSet' => false,
  422. 'winningPlan' => [],
  423. 'rejectedPlans' => [],
  424. ],
  425. 'executionStats' => [
  426. 'executionSuccess' => true,
  427. 'nReturned' => 2,
  428. 'totalKeysExamined' => 0,
  429. 'totalDocsExamined' => 3,
  430. 'executionStages' => [],
  431. 'allPlansExecution' => [],
  432. ],
  433. 'serverInfo' => [
  434. 'port' => 27017,
  435. ],
  436. ];
  437. $this->assertArraySubset($expected, $cursor->explain());
  438. }
  439. /**
  440. * @return \PHPUnit_Framework_MockObject_MockObject
  441. */
  442. protected function getCollectionMock()
  443. {
  444. return $this->createMock('MongoDB\Collection', [], [], '', false);
  445. }
  446. }