Merge pull request #6212 from colemanw/CRM-16354
[civicrm-core.git] / tests / phpunit / CRM / Core / TransactionTest.php
CommitLineData
792ad554
TO
1<?php
2require_once 'CiviTest/CiviUnitTestCase.php';
3require_once 'CiviTest/Contact.php';
4require_once 'CiviTest/Custom.php';
aba1cd8b
EM
5
6/**
7 * Class CRM_Core_TransactionTest
8 */
792ad554
TO
9class CRM_Core_TransactionTest extends CiviUnitTestCase {
10
5bb8417a
TO
11 /**
12 * @var array
13 */
14 private $callbackLog;
15
16 /**
17 * @var array (int $idx => int $contactId) list of contact IDs that have been created (in order of creation)
18 *
19 * Note that ID this is all IDs created by the test-case -- even if the creation was subsequently rolled back
20 */
21 private $cids;
22
00be9182 23 public function setUp() {
792ad554
TO
24 parent::setUp();
25 $this->quickCleanup(array('civicrm_contact', 'civicrm_activity'));
5bb8417a
TO
26 $this->callbackLog = array();
27 $this->cids = array();
792ad554
TO
28 }
29
7fe37828
EM
30 /**
31 * @return array
32 */
00be9182 33 public function dataCreateStyle() {
5bb8417a
TO
34 return array(
35 array('sql-insert'),
36 array('bao-create'),
37 );
792ad554
TO
38 }
39
7fe37828
EM
40 /**
41 * @return array
42 */
00be9182 43 public function dataCreateAndCommitStyles() {
5bb8417a
TO
44 return array(
45 array('sql-insert', 'implicit-commit'),
46 array('sql-insert', 'explicit-commit'),
47 array('bao-create', 'implicit-commit'),
48 array('bao-create', 'explicit-commit'),
49 );
792ad554
TO
50 }
51
5bb8417a 52 /**
e16033b4
TO
53 * @param string $createStyle
54 * 'sql-insert'|'bao-create'.
55 * @param string $commitStyle
56 * 'implicit-commit'|'explicit-commit'.
5bb8417a
TO
57 * @dataProvider dataCreateAndCommitStyles
58 */
00be9182 59 public function testBasicCommit($createStyle, $commitStyle) {
5bb8417a
TO
60 $this->createContactWithTransaction('reuse-tx', $createStyle, $commitStyle);
61 $this->assertCount(1, $this->cids);
62 $this->assertContactsExistByOffset(array(0 => TRUE));
63 }
792ad554 64
5bb8417a
TO
65 /**
66 * @dataProvider dataCreateStyle
1e1fdcf6 67 * @param $createStyle
5bb8417a 68 */
00be9182 69 public function testBasicRollback($createStyle) {
5bb8417a
TO
70 $this->createContactWithTransaction('reuse-tx', $createStyle, 'rollback');
71 $this->assertCount(1, $this->cids);
72 $this->assertContactsExistByOffset(array(0 => FALSE));
73 }
792ad554 74
5bb8417a 75 /**
eceb18cc 76 * Test in which an outer function makes multiple calls to inner functions.
5bb8417a
TO
77 * but then rolls back the entire set.
78 *
e16033b4
TO
79 * @param string $createStyle
80 * 'sql-insert'|'bao-create'.
81 * @param string $commitStyle
82 * 'implicit-commit'|'explicit-commit'.
5bb8417a
TO
83 * @dataProvider dataCreateAndCommitStyles
84 */
00be9182 85 public function testBatchRollback($createStyle, $commitStyle) {
5bb8417a
TO
86 $this->runBatch(
87 'reuse-tx',
88 array(
89 array('reuse-tx', $createStyle, $commitStyle), // cid 0
90 array('reuse-tx', $createStyle, $commitStyle), // cid 1
91 ),
92 array(0 => TRUE, 1 => TRUE),
93 'rollback'
94 );
95 $this->assertCount(2, $this->cids);
96 $this->assertContactsExistByOffset(array(0 => FALSE, 1 => FALSE));
97 }
792ad554 98
5bb8417a
TO
99 /**
100 * Test in which runBatch makes multiple calls to
101 * createContactWithTransaction using a mix of rollback/commit.
102 * createContactWithTransaction use nesting (savepoints), so the
103 * batch is able to commit.
104 *
e16033b4
TO
105 * @param string $createStyle
106 * 'sql-insert'|'bao-create'.
107 * @param string $commitStyle
108 * 'implicit-commit'|'explicit-commit'.
5bb8417a
TO
109 * @dataProvider dataCreateAndCommitStyles
110 */
00be9182 111 public function testMixedBatchCommit_nesting($createStyle, $commitStyle) {
5bb8417a
TO
112 $this->runBatch(
113 'reuse-tx',
114 array(
115 array('nest-tx', $createStyle, $commitStyle), // cid 0
116 array('nest-tx', $createStyle, 'rollback'), // cid 1
117 array('nest-tx', $createStyle, $commitStyle), // cid 2
118 ),
119 array(0 => TRUE, 1 => FALSE, 2 => TRUE),
120 $commitStyle
121 );
122 $this->assertCount(3, $this->cids);
123 $this->assertContactsExistByOffset(array(0 => TRUE, 1 => FALSE, 2 => TRUE));
792ad554
TO
124 }
125
126 /**
5bb8417a
TO
127 * Test in which runBatch makes multiple calls to
128 * createContactWithTransaction using a mix of rollback/commit.
129 * createContactWithTransaction reuses the main transaction,
130 * so the overall batch must rollback.
131 *
e16033b4
TO
132 * @param string $createStyle
133 * 'sql-insert'|'bao-create'.
134 * @param string $commitStyle
135 * 'implicit-commit'|'explicit-commit'.
5bb8417a 136 * @dataProvider dataCreateAndCommitStyles
792ad554 137 */
00be9182 138 public function testMixedBatchCommit_reuse($createStyle, $commitStyle) {
5bb8417a
TO
139 $this->runBatch(
140 'reuse-tx',
141 array(
142 array('reuse-tx', $createStyle, $commitStyle), // cid 0
143 array('reuse-tx', $createStyle, 'rollback'), // cid 1
144 array('reuse-tx', $createStyle, $commitStyle), // cid 2
145 ),
146 array(0 => TRUE, 1 => TRUE, 2 => TRUE),
147 $commitStyle
148 );
149 $this->assertCount(3, $this->cids);
150 $this->assertContactsExistByOffset(array(0 => FALSE, 1 => FALSE, 2 => FALSE));
151 }
792ad554 152
5bb8417a
TO
153 /**
154 * Test in which runBatch makes multiple calls to
155 * createContactWithTransaction using a mix of rollback/commit.
156 * The overall batch is rolled back.
157 *
e16033b4
TO
158 * @param string $createStyle
159 * 'sql-insert'|'bao-create'.
160 * @param string $commitStyle
161 * 'implicit-commit'|'explicit-commit'.
5bb8417a
TO
162 * @dataProvider dataCreateAndCommitStyles
163 */
00be9182 164 public function testMixedBatchRollback_nesting($createStyle, $commitStyle) {
5bb8417a
TO
165 $this->assertFalse(CRM_Core_Transaction::isActive());
166 $this->runBatch(
167 'reuse-tx',
168 array(
169 array('nest-tx', $createStyle, $commitStyle), // cid 0
170 array('nest-tx', $createStyle, 'rollback'), // cid 1
171 array('nest-tx', $createStyle, $commitStyle), // cid 2
172 ),
173 array(0 => TRUE, 1 => FALSE, 2 => TRUE),
174 'rollback'
175 );
176 $this->assertFalse(CRM_Core_Transaction::isActive());
177 $this->assertCount(3, $this->cids);
178 $this->assertContactsExistByOffset(array(0 => FALSE, 1 => FALSE, 2 => FALSE));
179 }
792ad554 180
5bb8417a
TO
181 public function testIsActive() {
182 $this->assertEquals(FALSE, CRM_Core_Transaction::isActive());
183 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
184 $tx = new CRM_Core_Transaction();
185 $this->assertEquals(TRUE, CRM_Core_Transaction::isActive());
186 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
187 $tx = NULL;
188 $this->assertEquals(FALSE, CRM_Core_Transaction::isActive());
189 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
190 }
792ad554 191
5bb8417a
TO
192 public function testIsActive_rollback() {
193 $this->assertEquals(FALSE, CRM_Core_Transaction::isActive());
194 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
792ad554 195
5bb8417a
TO
196 $tx = new CRM_Core_Transaction();
197 $this->assertEquals(TRUE, CRM_Core_Transaction::isActive());
198 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
792ad554 199
5bb8417a
TO
200 $tx->rollback();
201 $this->assertEquals(TRUE, CRM_Core_Transaction::isActive());
202 $this->assertEquals(FALSE, CRM_Core_Transaction::willCommit());
792ad554 203
5bb8417a
TO
204 $tx = NULL;
205 $this->assertEquals(FALSE, CRM_Core_Transaction::isActive());
206 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
207 }
792ad554 208
5bb8417a
TO
209 public function testCallback_commit() {
210 $tx = new CRM_Core_Transaction();
792ad554 211
92915c55
TO
212 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_COMMIT, array($this, '_preCommit'), array(
213 'qwe',
608e6658 214 'rty',
92915c55
TO
215 ));
216 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_COMMIT, array($this, '_postCommit'), array(
217 'uio',
608e6658 218 'p[]',
92915c55
TO
219 ));
220 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_ROLLBACK, array(
221 $this,
608e6658 222 '_preRollback',
92915c55
TO
223 ), array('asd', 'fgh'));
224 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_ROLLBACK, array(
225 $this,
608e6658 226 '_postRollback',
92915c55 227 ), array('jkl', ';'));
792ad554 228
5bb8417a 229 CRM_Core_DAO::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
792ad554 230
5bb8417a
TO
231 $this->assertEquals(array(), $this->callbackLog);
232 $tx = NULL;
233 $this->assertEquals(array('_preCommit', 'qwe', 'rty'), $this->callbackLog[0]);
234 $this->assertEquals(array('_postCommit', 'uio', 'p[]'), $this->callbackLog[1]);
792ad554
TO
235 }
236
5bb8417a
TO
237 public function testCallback_rollback() {
238 $tx = new CRM_Core_Transaction();
792ad554 239
92915c55
TO
240 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_COMMIT, array($this, '_preCommit'), array(
241 'ewq',
608e6658 242 'ytr',
92915c55
TO
243 ));
244 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_COMMIT, array($this, '_postCommit'), array(
245 'oiu',
608e6658 246 '][p',
92915c55
TO
247 ));
248 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_ROLLBACK, array(
249 $this,
608e6658 250 '_preRollback',
92915c55
TO
251 ), array('dsa', 'hgf'));
252 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_ROLLBACK, array(
253 $this,
608e6658 254 '_postRollback',
92915c55 255 ), array('lkj', ';'));
792ad554 256
5bb8417a
TO
257 CRM_Core_DAO::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
258 $tx->rollback();
792ad554 259
5bb8417a
TO
260 $this->assertEquals(array(), $this->callbackLog);
261 $tx = NULL;
262 $this->assertEquals(array('_preRollback', 'dsa', 'hgf'), $this->callbackLog[0]);
263 $this->assertEquals(array('_postRollback', 'lkj', ';'), $this->callbackLog[1]);
264 }
792ad554 265
5bb8417a 266 /**
e16033b4
TO
267 * @param string $createStyle
268 * 'sql-insert'|'bao-create'.
269 * @param string $commitStyle
270 * 'implicit-commit'|'explicit-commit'.
5bb8417a
TO
271 * @dataProvider dataCreateAndCommitStyles
272 */
273 public function testRun_ok($createStyle, $commitStyle) {
274 $test = $this;
92915c55 275 CRM_Core_Transaction::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) {
5bb8417a
TO
276 $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle);
277 $test->assertContactsExistByOffset(array(0 => TRUE));
278 });
279 $this->assertContactsExistByOffset(array(0 => TRUE));
280 }
792ad554 281
5bb8417a 282 /**
e16033b4
TO
283 * @param string $createStyle
284 * 'sql-insert'|'bao-create'.
285 * @param string $commitStyle
286 * 'implicit-commit'|'explicit-commit'.
5bb8417a
TO
287 * @dataProvider dataCreateAndCommitStyles
288 */
289 public function testRun_exception($createStyle, $commitStyle) {
290 $tx = new CRM_Core_Transaction();
291 $test = $this;
292 $e = NULL; // Exception
293 try {
92915c55 294 CRM_Core_Transaction::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) {
5bb8417a
TO
295 $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle);
296 $test->assertContactsExistByOffset(array(0 => TRUE));
297 throw new Exception("Ruh-roh");
298 });
0db6c3e1
TO
299 }
300 catch (Exception $ex) {
5bb8417a 301 $e = $ex;
c0aa95b2
TO
302 if (get_class($e) != 'Exception' || $e->getMessage() != 'Ruh-roh') {
303 throw $e;
304 }
5bb8417a
TO
305 }
306 $this->assertTrue($e instanceof Exception);
5bb8417a
TO
307 $this->assertContactsExistByOffset(array(0 => FALSE));
308 }
792ad554 309
5bb8417a
TO
310 /**
311 * @param $cids
312 * @param bool $exist
313 */
314 public function assertContactsExist($cids, $exist = TRUE) {
315 foreach ($cids as $cid) {
316 $this->assertTrue(is_numeric($cid));
317 $this->assertDBQuery($exist ? 1 : 0, 'SELECT count(*) FROM civicrm_contact WHERE id = %1', array(
318 1 => array($cid, 'Integer'),
319 ));
320 }
321 }
792ad554 322
5bb8417a 323 /**
e16033b4
TO
324 * @param array $existsByOffset
325 * Array(int $cidOffset => bool $expectExists).
5bb8417a
TO
326 * @param int $generalOffset
327 */
328 public function assertContactsExistByOffset($existsByOffset, $generalOffset = 0) {
329 foreach ($existsByOffset as $offset => $expectExists) {
330 $this->assertTrue(isset($this->cids[$generalOffset + $offset]), "Find cid at offset($generalOffset + $offset)");
331 $cid = $this->cids[$generalOffset + $offset];
332 $this->assertTrue(is_numeric($cid));
333 $this->assertDBQuery($expectExists ? 1 : 0, 'SELECT count(*) FROM civicrm_contact WHERE id = %1', array(
334 1 => array($cid, 'Integer'),
335 ), "Check contact at offset($generalOffset + $offset)");
336 }
792ad554
TO
337 }
338
339 /**
5bb8417a
TO
340 * Use SQL to INSERT a contact and assert success. Perform
341 * work within a transaction.
342 *
e16033b4
TO
343 * @param string $nesting
344 * 'reuse-tx'|'nest-tx' how to construct transaction.
345 * @param string $insert
346 * 'sql-insert'|'bao-create' how to add the example record.
347 * @param string $outcome
348 * 'rollback'|'implicit-commit'|'explicit-commit' how to finish transaction.
a6c01b45
CW
349 * @return int
350 * cid
792ad554 351 */
5bb8417a
TO
352 public function createContactWithTransaction($nesting, $insert, $outcome) {
353 if ($nesting != 'reuse-tx' && $nesting != 'nest-tx') {
354 throw new RuntimeException('Bad test data: reuse=' . $nesting);
355 }
356 if ($insert != 'sql-insert' && $insert != 'bao-create') {
357 throw new RuntimeException('Bad test data: insert=' . $insert);
358 }
359 if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') {
360 throw new RuntimeException('Bad test data: outcome=' . $outcome);
361 }
792ad554 362
5bb8417a 363 $tx = new CRM_Core_Transaction($nesting === 'nest-tx');
792ad554 364
5bb8417a
TO
365 if ($insert == 'sql-insert') {
366 $r = CRM_Core_DAO::executeQuery("INSERT INTO civicrm_contact(first_name,last_name) VALUES ('ff', 'll')");
367 $cid = mysql_insert_id();
0db6c3e1
TO
368 }
369 elseif ($insert == 'bao-create') {
792ad554
TO
370 $params = array(
371 'contact_type' => 'Individual',
5bb8417a
TO
372 'first_name' => 'FF',
373 'last_name' => 'LL',
792ad554
TO
374 );
375 $r = CRM_Contact_BAO_Contact::create($params);
376 $cid = $r->id;
5bb8417a 377 }
792ad554 378
5bb8417a 379 $this->cids[] = $cid;
792ad554 380
5bb8417a 381 $this->assertContactsExist(array($cid), TRUE);
792ad554 382
5bb8417a
TO
383 if ($outcome == 'rollback') {
384 $tx->rollback();
0db6c3e1
TO
385 }
386 elseif ($outcome == 'explicit-commit') {
5bb8417a
TO
387 $tx->commit();
388 } // else: implicit-commit
792ad554 389
5bb8417a
TO
390 return $cid;
391 }
792ad554 392
5bb8417a 393 /**
eceb18cc 394 * Perform a series of operations within smaller transactions.
5bb8417a 395 *
e16033b4
TO
396 * @param string $nesting
397 * 'reuse-tx'|'nest-tx' how to construct transaction.
398 * @param array $callbacks
399 * See createContactWithTransaction.
400 * @param array $existsByOffset
401 * See assertContactsMix.
402 * @param string $outcome
403 * 'rollback'|'implicit-commit'|'explicit-commit' how to finish transaction.
608e6658 404 * @return void
5bb8417a 405 */
2ffda55d 406 public function runBatch($nesting, $callbacks, $existsByOffset, $outcome) {
5bb8417a
TO
407 if ($nesting != 'reuse-tx' && $nesting != 'nest-tx') {
408 throw new RuntimeException('Bad test data: nesting=' . $nesting);
409 }
410 if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') {
411 throw new RuntimeException('Bad test data: outcome=' . $nesting);
412 }
792ad554 413
5bb8417a 414 $tx = new CRM_Core_Transaction($nesting === 'reuse-tx');
792ad554 415
5bb8417a
TO
416 $generalOffset = count($this->cids);
417 foreach ($callbacks as $callback) {
418 list ($cbNesting, $cbInsert, $cbOutcome) = $callback;
419 $this->createContactWithTransaction($cbNesting, $cbInsert, $cbOutcome);
420 }
792ad554 421
5bb8417a 422 $this->assertContactsExistByOffset($existsByOffset, $generalOffset);
792ad554 423
5bb8417a
TO
424 if ($outcome == 'rollback') {
425 $tx->rollback();
0db6c3e1
TO
426 }
427 elseif ($outcome == 'explicit-commit') {
5bb8417a
TO
428 $tx->commit();
429 } // else: implicit-commit
792ad554
TO
430 }
431
7fe37828
EM
432 /**
433 * @param $arg1
434 * @param $arg2
435 */
00be9182 436 public function _preCommit($arg1, $arg2) {
5bb8417a
TO
437 $this->callbackLog[] = array('_preCommit', $arg1, $arg2);
438 }
439
7fe37828
EM
440 /**
441 * @param $arg1
442 * @param $arg2
443 */
00be9182 444 public function _postCommit($arg1, $arg2) {
5bb8417a
TO
445 $this->callbackLog[] = array('_postCommit', $arg1, $arg2);
446 }
447
7fe37828
EM
448 /**
449 * @param $arg1
450 * @param $arg2
451 */
00be9182 452 public function _preRollback($arg1, $arg2) {
5bb8417a
TO
453 $this->callbackLog[] = array('_preRollback', $arg1, $arg2);
454 }
455
7fe37828
EM
456 /**
457 * @param $arg1
458 * @param $arg2
459 */
00be9182 460 public function _postRollback($arg1, $arg2) {
5bb8417a 461 $this->callbackLog[] = array('_postRollback', $arg1, $arg2);
792ad554 462 }
96025800 463
4cbe18b8 464}