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