MultiExecContextTest.php 25 KB

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