4 * Class CRM_Core_TransactionTest
7 class CRM_Core_TransactionTest
extends CiviUnitTestCase
{
16 * (int $idx => int $contactId) list of contact IDs that have been created (in order of creation)
18 * Note that ID this is all IDs created by the test-case -- even if the creation was subsequently rolled back
22 public function setUp() {
24 $this->quickCleanup(['civicrm_contact', 'civicrm_activity']);
25 $this->callbackLog
= [];
32 public function dataCreateStyle() {
42 public function dataCreateAndCommitStyles() {
44 ['sql-insert', 'implicit-commit'],
45 ['sql-insert', 'explicit-commit'],
46 ['bao-create', 'implicit-commit'],
47 ['bao-create', 'explicit-commit'],
52 * @param string $createStyle
53 * 'sql-insert'|'bao-create'.
54 * @param string $commitStyle
55 * 'implicit-commit'|'explicit-commit'.
56 * @dataProvider dataCreateAndCommitStyles
58 public function testBasicCommit($createStyle, $commitStyle) {
59 $this->createContactWithTransaction('reuse-tx', $createStyle, $commitStyle);
60 $this->assertCount(1, $this->cids
);
61 $this->assertContactsExistByOffset([0 => TRUE]);
65 * @dataProvider dataCreateStyle
68 public function testBasicRollback($createStyle) {
69 $this->createContactWithTransaction('reuse-tx', $createStyle, 'rollback');
70 $this->assertCount(1, $this->cids
);
71 $this->assertContactsExistByOffset([0 => FALSE]);
75 * Test in which an outer function makes multiple calls to inner functions.
76 * but then rolls back the entire set.
78 * @param string $createStyle
79 * 'sql-insert'|'bao-create'.
80 * @param string $commitStyle
81 * 'implicit-commit'|'explicit-commit'.
82 * @dataProvider dataCreateAndCommitStyles
84 public function testBatchRollback($createStyle, $commitStyle) {
89 ['reuse-tx', $createStyle, $commitStyle],
91 ['reuse-tx', $createStyle, $commitStyle],
93 [0 => TRUE, 1 => TRUE],
96 $this->assertCount(2, $this->cids
);
97 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE]);
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.
106 * @param string $createStyle
107 * 'sql-insert'|'bao-create'.
108 * @param string $commitStyle
109 * 'implicit-commit'|'explicit-commit'.
110 * @dataProvider dataCreateAndCommitStyles
112 public function testMixedBatchCommit_nesting($createStyle, $commitStyle) {
117 ['nest-tx', $createStyle, $commitStyle],
119 ['nest-tx', $createStyle, 'rollback'],
121 ['nest-tx', $createStyle, $commitStyle],
123 [0 => TRUE, 1 => FALSE, 2 => TRUE],
126 $this->assertCount(3, $this->cids
);
127 $this->assertContactsExistByOffset([0 => TRUE, 1 => FALSE, 2 => TRUE]);
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.
136 * @param string $createStyle
137 * 'sql-insert'|'bao-create'.
138 * @param string $commitStyle
139 * 'implicit-commit'|'explicit-commit'.
140 * @dataProvider dataCreateAndCommitStyles
142 public function testMixedBatchCommit_reuse($createStyle, $commitStyle) {
147 ['reuse-tx', $createStyle, $commitStyle],
149 ['reuse-tx', $createStyle, 'rollback'],
151 ['reuse-tx', $createStyle, $commitStyle],
153 [0 => TRUE, 1 => TRUE, 2 => TRUE],
156 $this->assertCount(3, $this->cids
);
157 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]);
161 * Test in which runBatch makes multiple calls to
162 * createContactWithTransaction using a mix of rollback/commit.
163 * The overall batch is rolled back.
165 * @param string $createStyle
166 * 'sql-insert'|'bao-create'.
167 * @param string $commitStyle
168 * 'implicit-commit'|'explicit-commit'.
169 * @dataProvider dataCreateAndCommitStyles
171 public function testMixedBatchRollback_nesting($createStyle, $commitStyle) {
172 $this->assertFalse(CRM_Core_Transaction
::isActive());
177 ['nest-tx', $createStyle, $commitStyle],
179 ['nest-tx', $createStyle, 'rollback'],
181 ['nest-tx', $createStyle, $commitStyle],
183 [0 => TRUE, 1 => FALSE, 2 => TRUE],
186 $this->assertFalse(CRM_Core_Transaction
::isActive());
187 $this->assertCount(3, $this->cids
);
188 $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]);
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());
198 $this->assertEquals(FALSE, CRM_Core_Transaction
::isActive());
199 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
202 public function testIsActive_rollback() {
203 $this->assertEquals(FALSE, CRM_Core_Transaction
::isActive());
204 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
206 $tx = new CRM_Core_Transaction();
207 $this->assertEquals(TRUE, CRM_Core_Transaction
::isActive());
208 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
211 $this->assertEquals(TRUE, CRM_Core_Transaction
::isActive());
212 $this->assertEquals(FALSE, CRM_Core_Transaction
::willCommit());
215 $this->assertEquals(FALSE, CRM_Core_Transaction
::isActive());
216 $this->assertEquals(TRUE, CRM_Core_Transaction
::willCommit());
219 public function testCallback_commit() {
220 $tx = new CRM_Core_Transaction();
222 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_COMMIT
, [$this, '_preCommit'], [
226 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_COMMIT
, [$this, '_postCommit'], [
230 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_ROLLBACK
, [
234 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_ROLLBACK
, [
239 CRM_Core_DAO
::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
241 $this->assertEquals([], $this->callbackLog
);
243 $this->assertEquals(['_preCommit', 'qwe', 'rty'], $this->callbackLog
[0]);
244 $this->assertEquals(['_postCommit', 'uio', 'p[]'], $this->callbackLog
[1]);
247 public function testCallback_rollback() {
248 $tx = new CRM_Core_Transaction();
250 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_COMMIT
, [$this, '_preCommit'], [
254 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_COMMIT
, [$this, '_postCommit'], [
258 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_PRE_ROLLBACK
, [
262 CRM_Core_Transaction
::addCallback(CRM_Core_Transaction
::PHASE_POST_ROLLBACK
, [
267 CRM_Core_DAO
::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100');
270 $this->assertEquals([], $this->callbackLog
);
272 $this->assertEquals(['_preRollback', 'dsa', 'hgf'], $this->callbackLog
[0]);
273 $this->assertEquals(['_postRollback', 'lkj', ';'], $this->callbackLog
[1]);
277 * @param string $createStyle
278 * 'sql-insert'|'bao-create'.
279 * @param string $commitStyle
280 * 'implicit-commit'|'explicit-commit'.
281 * @dataProvider dataCreateAndCommitStyles
283 public function testRun_ok($createStyle, $commitStyle) {
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]);
289 $this->assertContactsExistByOffset([0 => TRUE]);
293 * @param string $createStyle
294 * 'sql-insert'|'bao-create'.
295 * @param string $commitStyle
296 * 'implicit-commit'|'explicit-commit'.
297 * @dataProvider dataCreateAndCommitStyles
299 public function testRun_exception($createStyle, $commitStyle) {
300 $tx = new CRM_Core_Transaction();
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");
311 catch (Exception
$ex) {
313 if (get_class($e) != 'Exception' ||
$e->getMessage() != 'Ruh-roh') {
317 $this->assertTrue($e instanceof Exception
);
318 $this->assertContactsExistByOffset([0 => FALSE]);
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'],
335 * @param array $existsByOffset
336 * Array(int $cidOffset => bool $expectExists).
337 * @param int $generalOffset
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)");
351 * Use SQL to INSERT a contact and assert success. Perform
352 * work within a transaction.
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.
363 public function createContactWithTransaction($nesting, $insert, $outcome) {
364 if ($nesting != 'reuse-tx' && $nesting != 'nest-tx') {
365 throw new RuntimeException('Bad test data: reuse=' . $nesting);
367 if ($insert != 'sql-insert' && $insert != 'bao-create') {
368 throw new RuntimeException('Bad test data: insert=' . $insert);
370 if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') {
371 throw new RuntimeException('Bad test data: outcome=' . $outcome);
374 $tx = new CRM_Core_Transaction($nesting === 'nest-tx');
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();
380 elseif ($insert == 'bao-create') {
382 'contact_type' => 'Individual',
383 'first_name' => 'FF',
386 $r = CRM_Contact_BAO_Contact
::create($params);
390 $this->cids
[] = $cid;
392 $this->assertContactsExist([$cid], TRUE);
394 if ($outcome == 'rollback') {
397 elseif ($outcome == 'explicit-commit') {
399 } // else: implicit-commit
405 * Perform a series of operations within smaller transactions.
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.
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);
421 if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') {
422 throw new RuntimeException('Bad test data: outcome=' . $nesting);
425 $tx = new CRM_Core_Transaction($nesting === 'reuse-tx');
427 $generalOffset = count($this->cids
);
428 foreach ($callbacks as $callback) {
429 list ($cbNesting, $cbInsert, $cbOutcome) = $callback;
430 $this->createContactWithTransaction($cbNesting, $cbInsert, $cbOutcome);
433 $this->assertContactsExistByOffset($existsByOffset, $generalOffset);
435 if ($outcome == 'rollback') {
438 elseif ($outcome == 'explicit-commit') {
440 } // else: implicit-commit
447 public function _preCommit($arg1, $arg2) {
448 $this->callbackLog
[] = ['_preCommit', $arg1, $arg2];
455 public function _postCommit($arg1, $arg2) {
456 $this->callbackLog
[] = ['_postCommit', $arg1, $arg2];
463 public function _preRollback($arg1, $arg2) {
464 $this->callbackLog
[] = ['_preRollback', $arg1, $arg2];
471 public function _postRollback($arg1, $arg2) {
472 $this->callbackLog
[] = ['_postRollback', $arg1, $arg2];