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