4 * Class CRM_Core_TransactionTest
7 class CRM_Core_TransactionTest
extends CiviUnitTestCase
{
15 * @var array (int $idx => int $contactId) list of contact IDs that have been created (in order of creation)
17 * Note that ID this is all IDs created by the test-case -- even if the creation was subsequently rolled back
21 public function setUp() {
23 $this->quickCleanup(['civicrm_contact', 'civicrm_activity']);
24 $this->callbackLog
= [];
31 public function dataCreateStyle() {
41 public function dataCreateAndCommitStyles() {
43 ['sql-insert', 'implicit-commit'],
44 ['sql-insert', 'explicit-commit'],
45 ['bao-create', 'implicit-commit'],
46 ['bao-create', 'explicit-commit'],
51 * @param string $createStyle
52 * 'sql-insert'|'bao-create'.
53 * @param string $commitStyle
54 * 'implicit-commit'|'explicit-commit'.
55 * @dataProvider dataCreateAndCommitStyles
57 public function testBasicCommit($createStyle, $commitStyle) {
58 $this->createContactWithTransaction('reuse-tx', $createStyle, $commitStyle);
59 $this->assertCount(1, $this->cids
);
60 $this->assertContactsExistByOffset([0 => TRUE]);
64 * @dataProvider dataCreateStyle
67 public function testBasicRollback($createStyle) {
68 $this->createContactWithTransaction('reuse-tx', $createStyle, 'rollback');
69 $this->assertCount(1, $this->cids
);
70 $this->assertContactsExistByOffset([0 => FALSE]);
74 * Test in which an outer function makes multiple calls to inner functions.
75 * but then rolls back the entire set.
77 * @param string $createStyle
78 * 'sql-insert'|'bao-create'.
79 * @param string $commitStyle
80 * 'implicit-commit'|'explicit-commit'.
81 * @dataProvider dataCreateAndCommitStyles
83 public function testBatchRollback($createStyle, $commitStyle) {
88 ['reuse-tx', $createStyle, $commitStyle],
90 ['reuse-tx', $createStyle, $commitStyle],
92 [0 => TRUE, 1 => TRUE],
95 $this->assertCount(2, $this->cids
);
96 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE]);
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.
105 * @param string $createStyle
106 * 'sql-insert'|'bao-create'.
107 * @param string $commitStyle
108 * 'implicit-commit'|'explicit-commit'.
109 * @dataProvider dataCreateAndCommitStyles
111 public function testMixedBatchCommit_nesting($createStyle, $commitStyle) {
116 ['nest-tx', $createStyle, $commitStyle],
118 ['nest-tx', $createStyle, 'rollback'],
120 ['nest-tx', $createStyle, $commitStyle],
122 [0 => TRUE, 1 => FALSE, 2 => TRUE],
125 $this->assertCount(3, $this->cids
);
126 $this->assertContactsExistByOffset([0 => TRUE, 1 => FALSE, 2 => TRUE]);
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.
135 * @param string $createStyle
136 * 'sql-insert'|'bao-create'.
137 * @param string $commitStyle
138 * 'implicit-commit'|'explicit-commit'.
139 * @dataProvider dataCreateAndCommitStyles
141 public function testMixedBatchCommit_reuse($createStyle, $commitStyle) {
146 ['reuse-tx', $createStyle, $commitStyle],
148 ['reuse-tx', $createStyle, 'rollback'],
150 ['reuse-tx', $createStyle, $commitStyle],
152 [0 => TRUE, 1 => TRUE, 2 => TRUE],
155 $this->assertCount(3, $this->cids
);
156 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]);
160 * Test in which runBatch makes multiple calls to
161 * createContactWithTransaction using a mix of rollback/commit.
162 * The overall batch is rolled back.
164 * @param string $createStyle
165 * 'sql-insert'|'bao-create'.
166 * @param string $commitStyle
167 * 'implicit-commit'|'explicit-commit'.
168 * @dataProvider dataCreateAndCommitStyles
170 public function testMixedBatchRollback_nesting($createStyle, $commitStyle) {
171 $this->assertFalse(CRM_Core_Transaction
::isActive());
176 ['nest-tx', $createStyle, $commitStyle],
178 ['nest-tx', $createStyle, 'rollback'],
180 ['nest-tx', $createStyle, $commitStyle],
182 [0 => TRUE, 1 => FALSE, 2 => TRUE],
185 $this->assertFalse(CRM_Core_Transaction
::isActive());
186 $this->assertCount(3, $this->cids
);
187 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]);
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());
197 $this->assertEquals(FALSE, CRM_Core_Transaction
::isActive());
198 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
201 public function testIsActive_rollback() {
202 $this->assertEquals(FALSE, CRM_Core_Transaction
::isActive());
203 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
205 $tx = new CRM_Core_Transaction();
206 $this->assertEquals(TRUE, CRM_Core_Transaction
::isActive());
207 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
210 $this->assertEquals(TRUE, CRM_Core_Transaction
::isActive());
211 $this->assertEquals(FALSE, CRM_Core_Transaction
::willCommit());
214 $this->assertEquals(FALSE, CRM_Core_Transaction
::isActive());
215 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
218 public function testCallback_commit() {
219 $tx = new CRM_Core_Transaction();
221 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_COMMIT
, [$this, '_preCommit'], [
225 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_COMMIT
, [$this, '_postCommit'], [
229 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_ROLLBACK
, [
233 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_ROLLBACK
, [
238 CRM_Core_DAO
::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
240 $this->assertEquals([], $this->callbackLog
);
242 $this->assertEquals(['_preCommit', 'qwe', 'rty'], $this->callbackLog
[0]);
243 $this->assertEquals(['_postCommit', 'uio', 'p[]'], $this->callbackLog
[1]);
246 public function testCallback_rollback() {
247 $tx = new CRM_Core_Transaction();
249 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_COMMIT
, [$this, '_preCommit'], [
253 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_COMMIT
, [$this, '_postCommit'], [
257 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_ROLLBACK
, [
261 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_ROLLBACK
, [
266 CRM_Core_DAO
::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
269 $this->assertEquals([], $this->callbackLog
);
271 $this->assertEquals(['_preRollback', 'dsa', 'hgf'], $this->callbackLog
[0]);
272 $this->assertEquals(['_postRollback', 'lkj', ';'], $this->callbackLog
[1]);
276 * @param string $createStyle
277 * 'sql-insert'|'bao-create'.
278 * @param string $commitStyle
279 * 'implicit-commit'|'explicit-commit'.
280 * @dataProvider dataCreateAndCommitStyles
282 public function testRun_ok($createStyle, $commitStyle) {
284 CRM_Core_Transaction
::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) {
285 $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle);
286 $test->assertContactsExistByOffset([0 => TRUE]);
288 $this->assertContactsExistByOffset([0 => TRUE]);
292 * @param string $createStyle
293 * 'sql-insert'|'bao-create'.
294 * @param string $commitStyle
295 * 'implicit-commit'|'explicit-commit'.
296 * @dataProvider dataCreateAndCommitStyles
298 public function testRun_exception($createStyle, $commitStyle) {
299 $tx = new CRM_Core_Transaction();
304 CRM_Core_Transaction
::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) {
305 $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle);
306 $test->assertContactsExistByOffset([0 => TRUE]);
307 throw new Exception("Ruh-roh");
310 catch (Exception
$ex) {
312 if (get_class($e) != 'Exception' ||
$e->getMessage() != 'Ruh-roh') {
316 $this->assertTrue($e instanceof Exception
);
317 $this->assertContactsExistByOffset([0 => FALSE]);
324 public function assertContactsExist($cids, $exist = TRUE) {
325 foreach ($cids as $cid) {
326 $this->assertTrue(is_numeric($cid));
327 $this->assertDBQuery($exist ?
1 : 0, 'SELECT count(*) FROM civicrm_contact WHERE id = %1', [
328 1 => [$cid, 'Integer'],
334 * @param array $existsByOffset
335 * Array(int $cidOffset => bool $expectExists).
336 * @param int $generalOffset
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));
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)");
350 * Use SQL to INSERT a contact and assert success. Perform
351 * work within a transaction.
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.
362 public function createContactWithTransaction($nesting, $insert, $outcome) {
363 if ($nesting != 'reuse-tx' && $nesting != 'nest-tx') {
364 throw new RuntimeException('Bad test data: reuse=' . $nesting);
366 if ($insert != 'sql-insert' && $insert != 'bao-create') {
367 throw new RuntimeException('Bad test data: insert=' . $insert);
369 if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') {
370 throw new RuntimeException('Bad test data: outcome=' . $outcome);
373 $tx = new CRM_Core_Transaction($nesting === 'nest-tx');
375 if ($insert == 'sql-insert') {
376 $r = CRM_Core_DAO
::executeQuery("INSERT INTO civicrm_contact(first_name,last_name) VALUES ('ff', 'll')");
377 $cid = $r->getConnection()->lastInsertId();
379 elseif ($insert == 'bao-create') {
381 'contact_type' => 'Individual',
382 'first_name' => 'FF',
385 $r = CRM_Contact_BAO_Contact
::create($params);
389 $this->cids
[] = $cid;
391 $this->assertContactsExist([$cid], TRUE);
393 if ($outcome == 'rollback') {
396 elseif ($outcome == 'explicit-commit') {
398 } // else: implicit-commit
404 * Perform a series of operations within smaller transactions.
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.
416 public function runBatch($nesting, $callbacks, $existsByOffset, $outcome) {
417 if ($nesting != 'reuse-tx' && $nesting != 'nest-tx') {
418 throw new RuntimeException('Bad test data: nesting=' . $nesting);
420 if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') {
421 throw new RuntimeException('Bad test data: outcome=' . $nesting);
424 $tx = new CRM_Core_Transaction($nesting === 'reuse-tx');
426 $generalOffset = count($this->cids
);
427 foreach ($callbacks as $callback) {
428 list ($cbNesting, $cbInsert, $cbOutcome) = $callback;
429 $this->createContactWithTransaction($cbNesting, $cbInsert, $cbOutcome);
432 $this->assertContactsExistByOffset($existsByOffset, $generalOffset);
434 if ($outcome == 'rollback') {
437 elseif ($outcome == 'explicit-commit') {
439 } // else: implicit-commit
446 public function _preCommit($arg1, $arg2) {
447 $this->callbackLog
[] = ['_preCommit', $arg1, $arg2];
454 public function _postCommit($arg1, $arg2) {
455 $this->callbackLog
[] = ['_postCommit', $arg1, $arg2];
462 public function _preRollback($arg1, $arg2) {
463 $this->callbackLog
[] = ['_preRollback', $arg1, $arg2];
470 public function _postRollback($arg1, $arg2) {
471 $this->callbackLog
[] = ['_postRollback', $arg1, $arg2];