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