Merge pull request #16783 from civicrm/5.24
[civicrm-core.git] / tests / phpunit / CRM / Core / TransactionTest.php
1 <?php
2
3 /**
4 * Class CRM_Core_TransactionTest
5 * @group headless
6 */
7 class CRM_Core_TransactionTest extends CiviUnitTestCase {
8
9 /**
10 * @var array
11 */
12 private $callbackLog;
13
14 /**
15 * @var array
16 * (int $idx => int $contactId) list of contact IDs that have been created (in order of creation)
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
22 public function setUp() {
23 parent::setUp();
24 $this->quickCleanup(['civicrm_contact', 'civicrm_activity']);
25 $this->callbackLog = [];
26 $this->cids = [];
27 }
28
29 /**
30 * @return array
31 */
32 public function dataCreateStyle() {
33 return [
34 ['sql-insert'],
35 ['bao-create'],
36 ];
37 }
38
39 /**
40 * @return array
41 */
42 public function dataCreateAndCommitStyles() {
43 return [
44 ['sql-insert', 'implicit-commit'],
45 ['sql-insert', 'explicit-commit'],
46 ['bao-create', 'implicit-commit'],
47 ['bao-create', 'explicit-commit'],
48 ];
49 }
50
51 /**
52 * @param string $createStyle
53 * 'sql-insert'|'bao-create'.
54 * @param string $commitStyle
55 * 'implicit-commit'|'explicit-commit'.
56 * @dataProvider dataCreateAndCommitStyles
57 */
58 public function testBasicCommit($createStyle, $commitStyle) {
59 $this->createContactWithTransaction('reuse-tx', $createStyle, $commitStyle);
60 $this->assertCount(1, $this->cids);
61 $this->assertContactsExistByOffset([0 => TRUE]);
62 }
63
64 /**
65 * @dataProvider dataCreateStyle
66 * @param $createStyle
67 */
68 public function testBasicRollback($createStyle) {
69 $this->createContactWithTransaction('reuse-tx', $createStyle, 'rollback');
70 $this->assertCount(1, $this->cids);
71 $this->assertContactsExistByOffset([0 => FALSE]);
72 }
73
74 /**
75 * Test in which an outer function makes multiple calls to inner functions.
76 * but then rolls back the entire set.
77 *
78 * @param string $createStyle
79 * 'sql-insert'|'bao-create'.
80 * @param string $commitStyle
81 * 'implicit-commit'|'explicit-commit'.
82 * @dataProvider dataCreateAndCommitStyles
83 */
84 public function testBatchRollback($createStyle, $commitStyle) {
85 $this->runBatch(
86 'reuse-tx',
87 [
88 // cid 0
89 ['reuse-tx', $createStyle, $commitStyle],
90 // cid 1
91 ['reuse-tx', $createStyle, $commitStyle],
92 ],
93 [0 => TRUE, 1 => TRUE],
94 'rollback'
95 );
96 $this->assertCount(2, $this->cids);
97 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE]);
98 }
99
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 *
106 * @param string $createStyle
107 * 'sql-insert'|'bao-create'.
108 * @param string $commitStyle
109 * 'implicit-commit'|'explicit-commit'.
110 * @dataProvider dataCreateAndCommitStyles
111 */
112 public function testMixedBatchCommit_nesting($createStyle, $commitStyle) {
113 $this->runBatch(
114 'reuse-tx',
115 [
116 // cid 0
117 ['nest-tx', $createStyle, $commitStyle],
118 // cid 1
119 ['nest-tx', $createStyle, 'rollback'],
120 // cid 2
121 ['nest-tx', $createStyle, $commitStyle],
122 ],
123 [0 => TRUE, 1 => FALSE, 2 => TRUE],
124 $commitStyle
125 );
126 $this->assertCount(3, $this->cids);
127 $this->assertContactsExistByOffset([0 => TRUE, 1 => FALSE, 2 => TRUE]);
128 }
129
130 /**
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 *
136 * @param string $createStyle
137 * 'sql-insert'|'bao-create'.
138 * @param string $commitStyle
139 * 'implicit-commit'|'explicit-commit'.
140 * @dataProvider dataCreateAndCommitStyles
141 */
142 public function testMixedBatchCommit_reuse($createStyle, $commitStyle) {
143 $this->runBatch(
144 'reuse-tx',
145 [
146 // cid 0
147 ['reuse-tx', $createStyle, $commitStyle],
148 // cid 1
149 ['reuse-tx', $createStyle, 'rollback'],
150 // cid 2
151 ['reuse-tx', $createStyle, $commitStyle],
152 ],
153 [0 => TRUE, 1 => TRUE, 2 => TRUE],
154 $commitStyle
155 );
156 $this->assertCount(3, $this->cids);
157 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]);
158 }
159
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 *
165 * @param string $createStyle
166 * 'sql-insert'|'bao-create'.
167 * @param string $commitStyle
168 * 'implicit-commit'|'explicit-commit'.
169 * @dataProvider dataCreateAndCommitStyles
170 */
171 public function testMixedBatchRollback_nesting($createStyle, $commitStyle) {
172 $this->assertFalse(CRM_Core_Transaction::isActive());
173 $this->runBatch(
174 'reuse-tx',
175 [
176 // cid 0
177 ['nest-tx', $createStyle, $commitStyle],
178 // cid 1
179 ['nest-tx', $createStyle, 'rollback'],
180 // cid 2
181 ['nest-tx', $createStyle, $commitStyle],
182 ],
183 [0 => TRUE, 1 => FALSE, 2 => TRUE],
184 'rollback'
185 );
186 $this->assertFalse(CRM_Core_Transaction::isActive());
187 $this->assertCount(3, $this->cids);
188 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]);
189 }
190
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 }
201
202 public function testIsActive_rollback() {
203 $this->assertEquals(FALSE, CRM_Core_Transaction::isActive());
204 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
205
206 $tx = new CRM_Core_Transaction();
207 $this->assertEquals(TRUE, CRM_Core_Transaction::isActive());
208 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
209
210 $tx->rollback();
211 $this->assertEquals(TRUE, CRM_Core_Transaction::isActive());
212 $this->assertEquals(FALSE, CRM_Core_Transaction::willCommit());
213
214 $tx = NULL;
215 $this->assertEquals(FALSE, CRM_Core_Transaction::isActive());
216 $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit());
217 }
218
219 public function testCallback_commit() {
220 $tx = new CRM_Core_Transaction();
221
222 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_COMMIT, [$this, '_preCommit'], [
223 'qwe',
224 'rty',
225 ]);
226 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_COMMIT, [$this, '_postCommit'], [
227 'uio',
228 'p[]',
229 ]);
230 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_ROLLBACK, [
231 $this,
232 '_preRollback',
233 ], ['asd', 'fgh']);
234 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_ROLLBACK, [
235 $this,
236 '_postRollback',
237 ], ['jkl', ';']);
238
239 CRM_Core_DAO::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
240
241 $this->assertEquals([], $this->callbackLog);
242 $tx = NULL;
243 $this->assertEquals(['_preCommit', 'qwe', 'rty'], $this->callbackLog[0]);
244 $this->assertEquals(['_postCommit', 'uio', 'p[]'], $this->callbackLog[1]);
245 }
246
247 public function testCallback_rollback() {
248 $tx = new CRM_Core_Transaction();
249
250 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_COMMIT, [$this, '_preCommit'], [
251 'ewq',
252 'ytr',
253 ]);
254 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_COMMIT, [$this, '_postCommit'], [
255 'oiu',
256 '][p',
257 ]);
258 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_ROLLBACK, [
259 $this,
260 '_preRollback',
261 ], ['dsa', 'hgf']);
262 CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_ROLLBACK, [
263 $this,
264 '_postRollback',
265 ], ['lkj', ';']);
266
267 CRM_Core_DAO::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
268 $tx->rollback();
269
270 $this->assertEquals([], $this->callbackLog);
271 $tx = NULL;
272 $this->assertEquals(['_preRollback', 'dsa', 'hgf'], $this->callbackLog[0]);
273 $this->assertEquals(['_postRollback', 'lkj', ';'], $this->callbackLog[1]);
274 }
275
276 /**
277 * @param string $createStyle
278 * 'sql-insert'|'bao-create'.
279 * @param string $commitStyle
280 * 'implicit-commit'|'explicit-commit'.
281 * @dataProvider dataCreateAndCommitStyles
282 */
283 public function testRun_ok($createStyle, $commitStyle) {
284 $test = $this;
285 CRM_Core_Transaction::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) {
286 $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle);
287 $test->assertContactsExistByOffset([0 => TRUE]);
288 });
289 $this->assertContactsExistByOffset([0 => TRUE]);
290 }
291
292 /**
293 * @param string $createStyle
294 * 'sql-insert'|'bao-create'.
295 * @param string $commitStyle
296 * 'implicit-commit'|'explicit-commit'.
297 * @dataProvider dataCreateAndCommitStyles
298 */
299 public function testRun_exception($createStyle, $commitStyle) {
300 $tx = new CRM_Core_Transaction();
301 $test = $this;
302 // Exception
303 $e = NULL;
304 try {
305 CRM_Core_Transaction::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) {
306 $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle);
307 $test->assertContactsExistByOffset([0 => TRUE]);
308 throw new Exception("Ruh-roh");
309 });
310 }
311 catch (Exception $ex) {
312 $e = $ex;
313 if (get_class($e) != 'Exception' || $e->getMessage() != 'Ruh-roh') {
314 throw $e;
315 }
316 }
317 $this->assertTrue($e instanceof Exception);
318 $this->assertContactsExistByOffset([0 => FALSE]);
319 }
320
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));
328 $this->assertDBQuery($exist ? 1 : 0, 'SELECT count(*) FROM civicrm_contact WHERE id = %1', [
329 1 => [$cid, 'Integer'],
330 ]);
331 }
332 }
333
334 /**
335 * @param array $existsByOffset
336 * Array(int $cidOffset => bool $expectExists).
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));
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)");
347 }
348 }
349
350 /**
351 * Use SQL to INSERT a contact and assert success. Perform
352 * work within a transaction.
353 *
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.
360 * @return int
361 * cid
362 */
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 }
373
374 $tx = new CRM_Core_Transaction($nesting === 'nest-tx');
375
376 if ($insert == 'sql-insert') {
377 $r = CRM_Core_DAO::executeQuery("INSERT INTO civicrm_contact(first_name,last_name) VALUES ('ff', 'll')");
378 $cid = $r->getConnection()->lastInsertId();
379 }
380 elseif ($insert == 'bao-create') {
381 $params = [
382 'contact_type' => 'Individual',
383 'first_name' => 'FF',
384 'last_name' => 'LL',
385 ];
386 $r = CRM_Contact_BAO_Contact::create($params);
387 $cid = $r->id;
388 }
389
390 $this->cids[] = $cid;
391
392 $this->assertContactsExist([$cid], TRUE);
393
394 if ($outcome == 'rollback') {
395 $tx->rollback();
396 }
397 elseif ($outcome == 'explicit-commit') {
398 $tx->commit();
399 } // else: implicit-commit
400
401 return $cid;
402 }
403
404 /**
405 * Perform a series of operations within smaller transactions.
406 *
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.
415 * @return void
416 */
417 public function runBatch($nesting, $callbacks, $existsByOffset, $outcome) {
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 }
424
425 $tx = new CRM_Core_Transaction($nesting === 'reuse-tx');
426
427 $generalOffset = count($this->cids);
428 foreach ($callbacks as $callback) {
429 list ($cbNesting, $cbInsert, $cbOutcome) = $callback;
430 $this->createContactWithTransaction($cbNesting, $cbInsert, $cbOutcome);
431 }
432
433 $this->assertContactsExistByOffset($existsByOffset, $generalOffset);
434
435 if ($outcome == 'rollback') {
436 $tx->rollback();
437 }
438 elseif ($outcome == 'explicit-commit') {
439 $tx->commit();
440 } // else: implicit-commit
441 }
442
443 /**
444 * @param $arg1
445 * @param $arg2
446 */
447 public function _preCommit($arg1, $arg2) {
448 $this->callbackLog[] = ['_preCommit', $arg1, $arg2];
449 }
450
451 /**
452 * @param $arg1
453 * @param $arg2
454 */
455 public function _postCommit($arg1, $arg2) {
456 $this->callbackLog[] = ['_postCommit', $arg1, $arg2];
457 }
458
459 /**
460 * @param $arg1
461 * @param $arg2
462 */
463 public function _preRollback($arg1, $arg2) {
464 $this->callbackLog[] = ['_preRollback', $arg1, $arg2];
465 }
466
467 /**
468 * @param $arg1
469 * @param $arg2
470 */
471 public function _postRollback($arg1, $arg2) {
472 $this->callbackLog[] = ['_postRollback', $arg1, $arg2];
473 }
474
475 }