MultiExecTest.php 29 KB

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