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