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