MultiExecContextTest.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  1. <?php
  2. /*
  3. * This file is part of the Predis package.
  4. *
  5. * (c) Daniele Alessandri <suppakilla@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Predis\Transaction;
  11. use PredisTestCase;
  12. use Predis\Client;
  13. use Predis\ResponseQueued;
  14. use Predis\ResponseError;
  15. use Predis\ServerException;
  16. use Predis\Command\CommandInterface;
  17. /**
  18. * @group realm-transaction
  19. */
  20. class MultiExecContextTest extends PredisTestCase
  21. {
  22. /**
  23. * @group disconnected
  24. * @expectedException Predis\NotSupportedException
  25. * @expectedExceptionMessage The current profile does not support MULTI, EXEC and DISCARD
  26. */
  27. public function testThrowsExceptionOnUnsupportedMultiExecInProfile()
  28. {
  29. $profile = $this->getMock('Predis\Profile\ServerProfileInterface');
  30. $profile->expects($this->once())
  31. ->method('supportsCommands')
  32. ->with(array('MULTI', 'EXEC', 'DISCARD'))
  33. ->will($this->returnValue(false));
  34. $connection = $this->getMock('Predis\Connection\SingleConnectionInterface');
  35. $client = new Client($connection, array('profile' => $profile));
  36. $tx = new MultiExecContext($client);
  37. }
  38. /**
  39. * @group disconnected
  40. * @expectedException Predis\NotSupportedException
  41. * @expectedExceptionMessage The current profile does not support WATCH and UNWATCH
  42. */
  43. public function testThrowsExceptionOnUnsupportedWatchUnwatchInProfile()
  44. {
  45. $profile = $this->getMock('Predis\Profile\ServerProfileInterface');
  46. $profile->expects($this->at(0))
  47. ->method('supportsCommands')
  48. ->with(array('MULTI', 'EXEC', 'DISCARD'))
  49. ->will($this->returnValue(true));
  50. $profile->expects($this->at(1))
  51. ->method('supportsCommands')
  52. ->with(array('WATCH', 'UNWATCH'))
  53. ->will($this->returnValue(false));
  54. $connection = $this->getMock('Predis\Connection\SingleConnectionInterface');
  55. $client = new Client($connection, array('profile' => $profile));
  56. $tx = new MultiExecContext($client, array('options' => 'cas'));
  57. $tx->watch('foo');
  58. }
  59. /**
  60. * @group disconnected
  61. */
  62. public function testExecutionWithFluentInterface()
  63. {
  64. $commands = array();
  65. $expected = array('one', 'two', 'three');
  66. $callback = $this->getExecuteCallback($expected, $commands);
  67. $tx = $this->getMockedTransaction($callback);
  68. $this->assertSame($expected, $tx->echo('one')->echo('two')->echo('three')->execute());
  69. $this->assertSame(array('MULTI', 'ECHO', 'ECHO', 'ECHO', 'EXEC'), self::commandsToIDs($commands));
  70. }
  71. /**
  72. * @group disconnected
  73. */
  74. public function testExecutionWithCallable()
  75. {
  76. $commands = array();
  77. $expected = array('one', 'two', 'three');
  78. $callback = $this->getExecuteCallback($expected, $commands);
  79. $tx = $this->getMockedTransaction($callback);
  80. $replies = $tx->execute(function ($tx) {
  81. $tx->echo('one');
  82. $tx->echo('two');
  83. $tx->echo('three');
  84. });
  85. $this->assertSame($expected, $replies);
  86. $this->assertSame(array('MULTI', 'ECHO', 'ECHO', 'ECHO', 'EXEC'), self::commandsToIDs($commands));
  87. }
  88. /**
  89. * @group disconnected
  90. */
  91. public function testCannotMixExecutionWithFluentInterfaceAndCallable()
  92. {
  93. $exception = null;
  94. $commands = array();
  95. $callback = $this->getExecuteCallback(null, $commands);
  96. $tx = $this->getMockedTransaction($callback);
  97. $exception = null;
  98. try {
  99. $tx->echo('foo')->execute(function ($tx) {
  100. $tx->echo('bar');
  101. });
  102. } catch (\Exception $exception) {
  103. // NOOP
  104. }
  105. $this->assertInstanceOf('Predis\ClientException', $exception);
  106. $this->assertSame(array('MULTI', 'ECHO', 'DISCARD'), self::commandsToIDs($commands));
  107. }
  108. /**
  109. * @group disconnected
  110. */
  111. public function testEmptyTransactionDoesNotSendMultiExecCommands()
  112. {
  113. $commands = array();
  114. $callback = $this->getExecuteCallback(null, $commands);
  115. $tx = $this->getMockedTransaction($callback);
  116. $replies = $tx->execute(function ($tx) {
  117. // NOOP
  118. });
  119. $this->assertNull($replies);
  120. $this->assertSame(array(), self::commandsToIDs($commands));
  121. }
  122. /**
  123. * @group disconnected
  124. * @expectedException Predis\ClientException
  125. * @expectedExceptionMessage Cannot invoke 'execute' or 'exec' inside an active client transaction block
  126. */
  127. public function testThrowsExceptionOnExecInsideTransactionBlock()
  128. {
  129. $commands = array();
  130. $callback = $this->getExecuteCallback(null, $commands);
  131. $tx = $this->getMockedTransaction($callback);
  132. $replies = $tx->execute(function ($tx) {
  133. $tx->exec();
  134. });
  135. $this->assertNull($replies);
  136. $this->assertSame(array(), self::commandsToIDs($commands));
  137. }
  138. /**
  139. * @group disconnected
  140. */
  141. public function testEmptyTransactionIgnoresDiscard()
  142. {
  143. $commands = array();
  144. $callback = $this->getExecuteCallback(null, $commands);
  145. $tx = $this->getMockedTransaction($callback);
  146. $replies = $tx->execute(function ($tx) {
  147. $tx->discard();
  148. });
  149. $this->assertNull($replies);
  150. $this->assertSame(array(), self::commandsToIDs($commands));
  151. }
  152. /**
  153. * @group disconnected
  154. */
  155. public function testTransactionWithCommandsSendsDiscard()
  156. {
  157. $commands = array();
  158. $callback = $this->getExecuteCallback(null, $commands);
  159. $tx = $this->getMockedTransaction($callback);
  160. $replies = $tx->execute(function ($tx) {
  161. $tx->set('foo', 'bar');
  162. $tx->get('foo');
  163. $tx->discard();
  164. });
  165. $this->assertNull($replies);
  166. $this->assertSame(array('MULTI', 'SET', 'GET', 'DISCARD'), self::commandsToIDs($commands));
  167. }
  168. /**
  169. * @group disconnected
  170. */
  171. public function testSendMultiOnCommandsFollowingDiscard()
  172. {
  173. $commands = array();
  174. $expected = array('after DISCARD');
  175. $callback = $this->getExecuteCallback($expected, $commands);
  176. $tx = $this->getMockedTransaction($callback);
  177. $replies = $tx->execute(function ($tx) {
  178. $tx->echo('before DISCARD');
  179. $tx->discard();
  180. $tx->echo('after DISCARD');
  181. });
  182. $this->assertSame($replies, $expected);
  183. $this->assertSame(array('MULTI', 'ECHO', 'DISCARD', 'MULTI', 'ECHO', 'EXEC'), self::commandsToIDs($commands));
  184. }
  185. /**
  186. * @group disconnected
  187. * @expectedException Predis\ClientException
  188. */
  189. public function testThrowsExceptionOnWatchInsideMulti()
  190. {
  191. $callback = $this->getExecuteCallback();
  192. $tx = $this->getMockedTransaction($callback);
  193. $tx->echo('foobar')->watch('foo')->execute();
  194. }
  195. /**
  196. * @group disconnected
  197. */
  198. public function testUnwatchInsideMulti()
  199. {
  200. $commands = array();
  201. $expected = array('foobar', true);
  202. $callback = $this->getExecuteCallback($expected, $commands);
  203. $tx = $this->getMockedTransaction($callback);
  204. $replies = $tx->echo('foobar')->unwatch('foo')->execute();
  205. $this->assertSame($replies, $expected);
  206. $this->assertSame(array('MULTI', 'ECHO', 'UNWATCH', 'EXEC'), self::commandsToIDs($commands));
  207. }
  208. /**
  209. * @group disconnected
  210. */
  211. public function testAutomaticWatchInOptions()
  212. {
  213. $txCommands = $casCommands = array();
  214. $expected = array('bar', 'piyo');
  215. $options = array('watch' => array('foo', 'hoge'));
  216. $callback = $this->getExecuteCallback($expected, $txCommands, $casCommands);
  217. $tx = $this->getMockedTransaction($callback, $options);
  218. $replies = $tx->execute(function ($tx) {
  219. $tx->get('foo');
  220. $tx->get('hoge');
  221. });
  222. $this->assertSame($replies, $expected);
  223. $this->assertSame(array('WATCH'), self::commandsToIDs($casCommands));
  224. $this->assertSame(array('foo', 'hoge'), $casCommands[0]->getArguments());
  225. $this->assertSame(array('MULTI', 'GET', 'GET', 'EXEC'), self::commandsToIDs($txCommands));
  226. }
  227. /**
  228. * @group disconnected
  229. */
  230. public function testCheckAndSetWithFluentInterface()
  231. {
  232. $txCommands = $casCommands = array();
  233. $expected = array('bar', 'piyo');
  234. $options = array('cas' => true, 'watch' => array('foo', 'hoge'));
  235. $callback = $this->getExecuteCallback($expected, $txCommands, $casCommands);
  236. $tx = $this->getMockedTransaction($callback, $options);
  237. $tx->watch('foobar');
  238. $this->assertSame('DUMMY_REPLY', $tx->get('foo'));
  239. $this->assertSame('DUMMY_REPLY', $tx->get('hoge'));
  240. $replies = $tx->multi()
  241. ->get('foo')
  242. ->get('hoge')
  243. ->execute();
  244. $this->assertSame($replies, $expected);
  245. $this->assertSame(array('WATCH', 'WATCH', 'GET', 'GET'), self::commandsToIDs($casCommands));
  246. $this->assertSame(array('MULTI', 'GET', 'GET', 'EXEC'), self::commandsToIDs($txCommands));
  247. }
  248. /**
  249. * @group disconnected
  250. */
  251. public function testCheckAndSetWithBlock()
  252. {
  253. $txCommands = $casCommands = array();
  254. $expected = array('bar', 'piyo');
  255. $options = array('cas' => true, 'watch' => array('foo', 'hoge'));
  256. $callback = $this->getExecuteCallback($expected, $txCommands, $casCommands);
  257. $tx = $this->getMockedTransaction($callback, $options);
  258. $test = $this;
  259. $replies = $tx->execute(function ($tx) use ($test) {
  260. $tx->watch('foobar');
  261. $reply1 = $tx->get('foo');
  262. $reply2 = $tx->get('hoge');
  263. $test->assertSame('DUMMY_REPLY', $reply1);
  264. $test->assertSame('DUMMY_REPLY', $reply2);
  265. $tx->multi();
  266. $tx->get('foo');
  267. $tx->get('hoge');
  268. });
  269. $this->assertSame($replies, $expected);
  270. $this->assertSame(array('WATCH', 'WATCH', 'GET', 'GET'), self::commandsToIDs($casCommands));
  271. $this->assertSame(array('MULTI', 'GET', 'GET', 'EXEC'), self::commandsToIDs($txCommands));
  272. }
  273. /**
  274. * @group disconnected
  275. */
  276. public function testCheckAndSetWithEmptyBlock()
  277. {
  278. $txCommands = $casCommands = array();
  279. $options = array('cas' => true);
  280. $callback = $this->getExecuteCallback(array(), $txCommands, $casCommands);
  281. $tx = $this->getMockedTransaction($callback, $options);
  282. $tx->execute(function ($tx) {
  283. $tx->multi();
  284. });
  285. $this->assertSame(array(), self::commandsToIDs($casCommands));
  286. $this->assertSame(array(), self::commandsToIDs($txCommands));
  287. }
  288. /**
  289. * @group disconnected
  290. */
  291. public function testCheckAndSetWithoutExec()
  292. {
  293. $txCommands = $casCommands = array();
  294. $options = array('cas' => true);
  295. $callback = $this->getExecuteCallback(array(), $txCommands, $casCommands);
  296. $tx = $this->getMockedTransaction($callback, $options);
  297. $tx->execute(function ($tx) {
  298. $bar = $tx->get('foo');
  299. $tx->set('hoge', 'piyo');
  300. });
  301. $this->assertSame(array('GET', 'SET'), self::commandsToIDs($casCommands));
  302. $this->assertSame(array(), self::commandsToIDs($txCommands));
  303. }
  304. /**
  305. * @group disconnected
  306. * @expectedException InvalidArgumentException
  307. * @expectedExceptionMessage Automatic retries can be used only when a transaction block is provided
  308. */
  309. public function testThrowsExceptionOnAutomaticRetriesWithFluentInterface()
  310. {
  311. $options = array('retry' => 1);
  312. $callback = $this->getExecuteCallback();
  313. $tx = $this->getMockedTransaction($callback, $options);
  314. $tx->echo('message')->execute();
  315. }
  316. /**
  317. * @group disconnected
  318. */
  319. public function testAutomaticRetryOnServerSideTransactionAbort()
  320. {
  321. $casCommands = $txCommands = array();
  322. $expected = array('bar');
  323. $options = array('watch' => array('foo', 'bar'), 'retry' => ($attempts = 2) + 1);
  324. $sentinel = $this->getMock('stdClass', array('signal'));
  325. $sentinel->expects($this->exactly($attempts))->method('signal');
  326. $callback = $this->getExecuteCallback($expected, $txCommands, $casCommands);
  327. $tx = $this->getMockedTransaction($callback, $options);
  328. $replies = $tx->execute(function ($tx) use ($sentinel, &$attempts) {
  329. $tx->get('foo');
  330. if ($attempts > 0) {
  331. $attempts -= 1;
  332. $sentinel->signal();
  333. $tx->echo('!!ABORT!!');
  334. }
  335. });
  336. $this->assertSame($replies, $expected);
  337. $this->assertSame(array('WATCH'), self::commandsToIDs($casCommands));
  338. $this->assertSame(array('foo', 'bar'), $casCommands[0]->getArguments());
  339. $this->assertSame(array('MULTI', 'GET', 'EXEC'), self::commandsToIDs($txCommands));
  340. }
  341. /**
  342. * @group disconnected
  343. * @expectedException Predis\Transaction\AbortedMultiExecException
  344. */
  345. public function testThrowsExceptionOnServerSideTransactionAbort()
  346. {
  347. $callback = $this->getExecuteCallback();
  348. $tx = $this->getMockedTransaction($callback);
  349. $replies = $tx->execute(function ($tx) {
  350. $tx->echo('!!ABORT!!');
  351. });
  352. }
  353. /**
  354. * @group disconnected
  355. */
  356. public function testHandlesStandardExceptionsInBlock()
  357. {
  358. $exception = null;
  359. $commands = array();
  360. $expected = array('foobar', true);
  361. $callback = $this->getExecuteCallback($expected, $commands);
  362. $tx = $this->getMockedTransaction($callback);
  363. $replies = null;
  364. try {
  365. $replies = $tx->execute(function ($tx) {
  366. $tx->set('foo', 'bar');
  367. $tx->get('foo');
  368. throw new \RuntimeException('TEST');
  369. });
  370. } catch (\Exception $exception) {
  371. // NOOP
  372. }
  373. $this->assertNull($replies, $expected);
  374. $this->assertSame(array('MULTI', 'SET', 'GET', 'DISCARD'), self::commandsToIDs($commands));
  375. }
  376. /**
  377. * @group disconnected
  378. */
  379. public function testHandlesServerExceptionsInBlock()
  380. {
  381. $commands = array();
  382. $expected = array('foobar', true);
  383. $callback = $this->getExecuteCallback($expected, $commands);
  384. $tx = $this->getMockedTransaction($callback);
  385. $replies = null;
  386. try {
  387. $replies = $tx->execute(function ($tx) {
  388. $tx->set('foo', 'bar');
  389. $tx->echo('ERR Invalid operation');
  390. $tx->get('foo');
  391. });
  392. } catch (ServerException $exception) {
  393. $tx->discard();
  394. }
  395. $this->assertNull($replies);
  396. $this->assertSame(array('MULTI', 'SET', 'ECHO', 'DISCARD'), self::commandsToIDs($commands));
  397. }
  398. /**
  399. * @group disconnected
  400. */
  401. public function testProperlyDiscardsTransactionAfterServerExceptionInBlock()
  402. {
  403. $connection = $this->getMockedConnection(function (CommandInterface $command) {
  404. switch ($command->getId()) {
  405. case 'MULTI':
  406. return true;
  407. case 'ECHO':
  408. return new ResponseError('ERR simulated failure on ECHO');
  409. case 'EXEC':
  410. return new ResponseError('EXECABORT Transaction discarded because of previous errors.');
  411. default:
  412. return new ResponseQueued();
  413. }
  414. });
  415. $client = new Client($connection);
  416. // First attempt
  417. $tx = new MultiExecContext($client);
  418. try {
  419. $tx->multi()->set('foo', 'bar')->echo('simulated failure')->exec();
  420. } catch (\Exception $exception) {
  421. $this->assertInstanceOf('Predis\Transaction\AbortedMultiExecException', $exception);
  422. $this->assertSame('ERR simulated failure on ECHO', $exception->getMessage());
  423. }
  424. // Second attempt
  425. $tx = new MultiExecContext($client);
  426. try {
  427. $tx->multi()->set('foo', 'bar')->echo('simulated failure')->exec();
  428. } catch (\Exception $exception) {
  429. $this->assertInstanceOf('Predis\Transaction\AbortedMultiExecException', $exception);
  430. $this->assertSame('ERR simulated failure on ECHO', $exception->getMessage());
  431. }
  432. }
  433. // ******************************************************************** //
  434. // ---- INTEGRATION TESTS --------------------------------------------- //
  435. // ******************************************************************** //
  436. /**
  437. * @group connected
  438. */
  439. public function testIntegrationHandlesStandardExceptionsInBlock()
  440. {
  441. $exception = null;
  442. $client = $this->getClient();
  443. try {
  444. $client->multiExec(function ($tx) {
  445. $tx->set('foo', 'bar');
  446. throw new \RuntimeException("TEST");
  447. });
  448. } catch (\Exception $exception) {
  449. // NOOP
  450. }
  451. $this->assertInstanceOf('RuntimeException', $exception);
  452. $this->assertFalse($client->exists('foo'));
  453. }
  454. /**
  455. * @group connected
  456. */
  457. public function testIntegrationThrowsExceptionOnRedisErrorInBlock()
  458. {
  459. $exception = null;
  460. $client = $this->getClient();
  461. $value = (string) rand();
  462. try {
  463. $client->multiExec(function ($tx) use ($value) {
  464. $tx->set('foo', 'bar');
  465. $tx->lpush('foo', 'bar');
  466. $tx->set('foo', $value);
  467. });
  468. } catch (ServerException $exception) {
  469. // NOOP
  470. }
  471. $this->assertInstanceOf('Predis\ResponseErrorInterface', $exception);
  472. $this->assertSame($value, $client->get('foo'));
  473. }
  474. /**
  475. * @group connected
  476. */
  477. public function testIntegrationReturnsErrorObjectOnRedisErrorInBlock()
  478. {
  479. $client = $this->getClient(array(), array('exceptions' => false));
  480. $replies = $client->multiExec(function ($tx) {
  481. $tx->set('foo', 'bar');
  482. $tx->lpush('foo', 'bar');
  483. $tx->echo('foobar');
  484. });
  485. $this->assertTrue($replies[0]);
  486. $this->assertInstanceOf('Predis\ResponseErrorInterface', $replies[1]);
  487. $this->assertSame('foobar', $replies[2]);
  488. }
  489. /**
  490. * @group connected
  491. */
  492. public function testIntegrationSendMultiOnCommandsAfterDiscard()
  493. {
  494. $client = $this->getClient();
  495. $replies = $client->multiExec(function ($tx) {
  496. $tx->set('foo', 'bar');
  497. $tx->discard();
  498. $tx->set('hoge', 'piyo');
  499. });
  500. $this->assertSame(1, count($replies));
  501. $this->assertFalse($client->exists('foo'));
  502. $this->assertTrue($client->exists('hoge'));
  503. }
  504. /**
  505. * @group connected
  506. */
  507. public function testIntegrationWritesOnWatchedKeysAbortTransaction()
  508. {
  509. $exception = null;
  510. $client1 = $this->getClient();
  511. $client2 = $this->getClient();
  512. try {
  513. $client1->multiExec(array('watch' => 'sentinel'), function ($tx) use ($client2) {
  514. $tx->set('sentinel', 'client1');
  515. $tx->get('sentinel');
  516. $client2->set('sentinel', 'client2');
  517. });
  518. } catch (AbortedMultiExecException $exception) {
  519. // NOOP
  520. }
  521. $this->assertInstanceOf('Predis\Transaction\AbortedMultiExecException', $exception);
  522. $this->assertSame('client2', $client1->get('sentinel'));
  523. }
  524. /**
  525. * @group connected
  526. */
  527. public function testIntegrationCheckAndSetWithDiscardAndRetry()
  528. {
  529. $client = $this->getClient();
  530. $client->set('foo', 'bar');
  531. $options = array('watch' => 'foo', 'cas' => true);
  532. $replies = $client->multiExec($options, function ($tx) {
  533. $tx->watch('foobar');
  534. $foo = $tx->get('foo');
  535. $tx->multi();
  536. $tx->set('foobar', $foo);
  537. $tx->discard();
  538. $tx->mget('foo', 'foobar');
  539. });
  540. $this->assertInternalType('array', $replies);
  541. $this->assertSame(array(array('bar', null)), $replies);
  542. $hijack = true;
  543. $client2 = $this->getClient();
  544. $client->set('foo', 'bar');
  545. $options = array('watch' => 'foo', 'cas' => true, 'retry' => 1);
  546. $replies = $client->multiExec($options, function ($tx) use ($client2, &$hijack) {
  547. $foo = $tx->get('foo');
  548. $tx->multi();
  549. $tx->set('foobar', $foo);
  550. $tx->discard();
  551. if ($hijack) {
  552. $hijack = false;
  553. $client2->set('foo', 'hijacked!');
  554. }
  555. $tx->mget('foo', 'foobar');
  556. });
  557. $this->assertInternalType('array', $replies);
  558. $this->assertSame(array(array('hijacked!', null)), $replies);
  559. }
  560. // ******************************************************************** //
  561. // ---- HELPER METHODS ------------------------------------------------ //
  562. // ******************************************************************** //
  563. /**
  564. * Returns a mocked instance of Predis\Connection\SingleConnectionInterface
  565. * usingthe specified callback to return values from executeCommand().
  566. *
  567. * @param \Closure $executeCallback
  568. * @return \Predis\Connection\SingleConnectionInterface
  569. */
  570. protected function getMockedConnection($executeCallback)
  571. {
  572. $connection = $this->getMock('Predis\Connection\SingleConnectionInterface');
  573. $connection->expects($this->any())
  574. ->method('executeCommand')
  575. ->will($this->returnCallback($executeCallback));
  576. return $connection;
  577. }
  578. /**
  579. * Returns a mocked instance of Predis\Transaction\MultiExecContext using
  580. * the specified callback to return values from the executeCommand method
  581. * of the underlying connection.
  582. *
  583. * @param \Closure $executeCallback
  584. * @param array $options
  585. * @return MultiExecContext
  586. */
  587. protected function getMockedTransaction($executeCallback, $options = array())
  588. {
  589. $connection = $this->getMockedConnection($executeCallback);
  590. $client = new Client($connection);
  591. $transaction = new MultiExecContext($client, $options);
  592. return $transaction;
  593. }
  594. /**
  595. * Returns a callback that emulates a server-side MULTI/EXEC transaction context.
  596. *
  597. * @param array $expected Expected responses.
  598. * @param array $commands Reference to an array storing the whole flow of commands.
  599. * @param array $cas Check and set operations performed by the transaction.
  600. * @return \Closure
  601. */
  602. protected function getExecuteCallback($expected = array(), &$commands = array(), &$cas = array())
  603. {
  604. $multi = $watch = $abort = false;
  605. return function (CommandInterface $command) use (&$expected, &$commands, &$cas, &$multi, &$watch, &$abort) {
  606. $cmd = $command->getId();
  607. if ($multi || $cmd === 'MULTI') {
  608. $commands[] = $command;
  609. } else {
  610. $cas[] = $command;
  611. }
  612. switch ($cmd) {
  613. case 'WATCH':
  614. if ($multi) {
  615. throw new ServerException("ERR $cmd inside MULTI is not allowed");
  616. }
  617. return $watch = true;
  618. case 'MULTI':
  619. if ($multi) {
  620. throw new ServerException("ERR MULTI calls can not be nested");
  621. }
  622. return $multi = true;
  623. case 'EXEC':
  624. if (!$multi) {
  625. throw new ServerException("ERR $cmd without MULTI");
  626. }
  627. $watch = $multi = false;
  628. if ($abort) {
  629. $commands = $cas = array();
  630. $abort = false;
  631. return null;
  632. }
  633. return $expected;
  634. case 'DISCARD':
  635. if (!$multi) {
  636. throw new ServerException("ERR $cmd without MULTI");
  637. }
  638. $watch = $multi = false;
  639. return true;
  640. case 'ECHO':
  641. @list($trigger) = $command->getArguments();
  642. if (strpos($trigger, 'ERR ') === 0) {
  643. throw new ServerException($trigger);
  644. }
  645. if ($trigger === '!!ABORT!!' && $multi) {
  646. $abort = true;
  647. }
  648. return new ResponseQueued();
  649. case 'UNWATCH':
  650. $watch = false;
  651. default:
  652. return $multi ? new ResponseQueued() : 'DUMMY_REPLY';
  653. }
  654. };
  655. }
  656. /**
  657. * Converts an array of instances of Predis\Command\CommandInterface and
  658. * returns an array containing their IDs.
  659. *
  660. * @param array $commands List of commands instances.
  661. * @return array
  662. */
  663. protected static function commandsToIDs($commands)
  664. {
  665. return array_map(function ($cmd) { return $cmd->getId(); }, $commands);
  666. }
  667. /**
  668. * Returns a client instance connected to the specified Redis
  669. * server instance to perform integration tests.
  670. *
  671. * @param array Additional connection parameters.
  672. * @param array Additional client options.
  673. * @return Client client instance.
  674. */
  675. protected function getClient(array $parameters = array(), array $options = array())
  676. {
  677. return $this->createClient($parameters, $options);
  678. }
  679. }