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