int $contactId) list of contact IDs that have been created (in order of creation) * * Note that ID this is all IDs created by the test-case -- even if the creation was subsequently rolled back */ private $cids; public function setUp() { parent::setUp(); $this->quickCleanup(['civicrm_contact', 'civicrm_activity']); $this->callbackLog = []; $this->cids = []; } /** * @return array */ public function dataCreateStyle() { return [ ['sql-insert'], ['bao-create'], ]; } /** * @return array */ public function dataCreateAndCommitStyles() { return [ ['sql-insert', 'implicit-commit'], ['sql-insert', 'explicit-commit'], ['bao-create', 'implicit-commit'], ['bao-create', 'explicit-commit'], ]; } /** * @param string $createStyle * 'sql-insert'|'bao-create'. * @param string $commitStyle * 'implicit-commit'|'explicit-commit'. * @dataProvider dataCreateAndCommitStyles */ public function testBasicCommit($createStyle, $commitStyle) { $this->createContactWithTransaction('reuse-tx', $createStyle, $commitStyle); $this->assertCount(1, $this->cids); $this->assertContactsExistByOffset([0 => TRUE]); } /** * @dataProvider dataCreateStyle * @param $createStyle */ public function testBasicRollback($createStyle) { $this->createContactWithTransaction('reuse-tx', $createStyle, 'rollback'); $this->assertCount(1, $this->cids); $this->assertContactsExistByOffset([0 => FALSE]); } /** * Test in which an outer function makes multiple calls to inner functions. * but then rolls back the entire set. * * @param string $createStyle * 'sql-insert'|'bao-create'. * @param string $commitStyle * 'implicit-commit'|'explicit-commit'. * @dataProvider dataCreateAndCommitStyles */ public function testBatchRollback($createStyle, $commitStyle) { $this->runBatch( 'reuse-tx', [ // cid 0 ['reuse-tx', $createStyle, $commitStyle], // cid 1 ['reuse-tx', $createStyle, $commitStyle], ], [0 => TRUE, 1 => TRUE], 'rollback' ); $this->assertCount(2, $this->cids); $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE]); } /** * Test in which runBatch makes multiple calls to * createContactWithTransaction using a mix of rollback/commit. * createContactWithTransaction use nesting (savepoints), so the * batch is able to commit. * * @param string $createStyle * 'sql-insert'|'bao-create'. * @param string $commitStyle * 'implicit-commit'|'explicit-commit'. * @dataProvider dataCreateAndCommitStyles */ public function testMixedBatchCommit_nesting($createStyle, $commitStyle) { $this->runBatch( 'reuse-tx', [ // cid 0 ['nest-tx', $createStyle, $commitStyle], // cid 1 ['nest-tx', $createStyle, 'rollback'], // cid 2 ['nest-tx', $createStyle, $commitStyle], ], [0 => TRUE, 1 => FALSE, 2 => TRUE], $commitStyle ); $this->assertCount(3, $this->cids); $this->assertContactsExistByOffset([0 => TRUE, 1 => FALSE, 2 => TRUE]); } /** * Test in which runBatch makes multiple calls to * createContactWithTransaction using a mix of rollback/commit. * createContactWithTransaction reuses the main transaction, * so the overall batch must rollback. * * @param string $createStyle * 'sql-insert'|'bao-create'. * @param string $commitStyle * 'implicit-commit'|'explicit-commit'. * @dataProvider dataCreateAndCommitStyles */ public function testMixedBatchCommit_reuse($createStyle, $commitStyle) { $this->runBatch( 'reuse-tx', [ // cid 0 ['reuse-tx', $createStyle, $commitStyle], // cid 1 ['reuse-tx', $createStyle, 'rollback'], // cid 2 ['reuse-tx', $createStyle, $commitStyle], ], [0 => TRUE, 1 => TRUE, 2 => TRUE], $commitStyle ); $this->assertCount(3, $this->cids); $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]); } /** * Test in which runBatch makes multiple calls to * createContactWithTransaction using a mix of rollback/commit. * The overall batch is rolled back. * * @param string $createStyle * 'sql-insert'|'bao-create'. * @param string $commitStyle * 'implicit-commit'|'explicit-commit'. * @dataProvider dataCreateAndCommitStyles */ public function testMixedBatchRollback_nesting($createStyle, $commitStyle) { $this->assertFalse(CRM_Core_Transaction::isActive()); $this->runBatch( 'reuse-tx', [ // cid 0 ['nest-tx', $createStyle, $commitStyle], // cid 1 ['nest-tx', $createStyle, 'rollback'], // cid 2 ['nest-tx', $createStyle, $commitStyle], ], [0 => TRUE, 1 => FALSE, 2 => TRUE], 'rollback' ); $this->assertFalse(CRM_Core_Transaction::isActive()); $this->assertCount(3, $this->cids); $this->assertContactsExistByOffset([0 => FALSE, 1 => FALSE, 2 => FALSE]); } public function testIsActive() { $this->assertEquals(FALSE, CRM_Core_Transaction::isActive()); $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit()); $tx = new CRM_Core_Transaction(); $this->assertEquals(TRUE, CRM_Core_Transaction::isActive()); $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit()); $tx = NULL; $this->assertEquals(FALSE, CRM_Core_Transaction::isActive()); $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit()); } public function testIsActive_rollback() { $this->assertEquals(FALSE, CRM_Core_Transaction::isActive()); $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit()); $tx = new CRM_Core_Transaction(); $this->assertEquals(TRUE, CRM_Core_Transaction::isActive()); $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit()); $tx->rollback(); $this->assertEquals(TRUE, CRM_Core_Transaction::isActive()); $this->assertEquals(FALSE, CRM_Core_Transaction::willCommit()); $tx = NULL; $this->assertEquals(FALSE, CRM_Core_Transaction::isActive()); $this->assertEquals(TRUE, CRM_Core_Transaction::willCommit()); } public function testCallback_commit() { $tx = new CRM_Core_Transaction(); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_COMMIT, [$this, '_preCommit'], [ 'qwe', 'rty', ]); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_COMMIT, [$this, '_postCommit'], [ 'uio', 'p[]', ]); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_ROLLBACK, [ $this, '_preRollback', ], ['asd', 'fgh']); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_ROLLBACK, [ $this, '_postRollback', ], ['jkl', ';']); CRM_Core_DAO::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100'); $this->assertEquals([], $this->callbackLog); $tx = NULL; $this->assertEquals(['_preCommit', 'qwe', 'rty'], $this->callbackLog[0]); $this->assertEquals(['_postCommit', 'uio', 'p[]'], $this->callbackLog[1]); } public function testCallback_rollback() { $tx = new CRM_Core_Transaction(); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_COMMIT, [$this, '_preCommit'], [ 'ewq', 'ytr', ]); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_COMMIT, [$this, '_postCommit'], [ 'oiu', '][p', ]); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_PRE_ROLLBACK, [ $this, '_preRollback', ], ['dsa', 'hgf']); CRM_Core_Transaction::addCallback(CRM_Core_Transaction::PHASE_POST_ROLLBACK, [ $this, '_postRollback', ], ['lkj', ';']); CRM_Core_DAO::executeQuery('UPDATE civicrm_contact SET id = 100 WHERE id = 100'); $tx->rollback(); $this->assertEquals([], $this->callbackLog); $tx = NULL; $this->assertEquals(['_preRollback', 'dsa', 'hgf'], $this->callbackLog[0]); $this->assertEquals(['_postRollback', 'lkj', ';'], $this->callbackLog[1]); } /** * @param string $createStyle * 'sql-insert'|'bao-create'. * @param string $commitStyle * 'implicit-commit'|'explicit-commit'. * @dataProvider dataCreateAndCommitStyles */ public function testRun_ok($createStyle, $commitStyle) { $test = $this; CRM_Core_Transaction::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) { $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle); $test->assertContactsExistByOffset([0 => TRUE]); }); $this->assertContactsExistByOffset([0 => TRUE]); } /** * @param string $createStyle * 'sql-insert'|'bao-create'. * @param string $commitStyle * 'implicit-commit'|'explicit-commit'. * @dataProvider dataCreateAndCommitStyles */ public function testRun_exception($createStyle, $commitStyle) { $tx = new CRM_Core_Transaction(); $test = $this; // Exception $e = NULL; try { CRM_Core_Transaction::create(TRUE)->run(function ($tx) use (&$test, $createStyle, $commitStyle) { $test->createContactWithTransaction('nest-tx', $createStyle, $commitStyle); $test->assertContactsExistByOffset([0 => TRUE]); throw new Exception("Ruh-roh"); }); } catch (Exception $ex) { $e = $ex; if (get_class($e) != 'Exception' || $e->getMessage() != 'Ruh-roh') { throw $e; } } $this->assertTrue($e instanceof Exception); $this->assertContactsExistByOffset([0 => FALSE]); } /** * @param $cids * @param bool $exist */ public function assertContactsExist($cids, $exist = TRUE) { foreach ($cids as $cid) { $this->assertTrue(is_numeric($cid)); $this->assertDBQuery($exist ? 1 : 0, 'SELECT count(*) FROM civicrm_contact WHERE id = %1', [ 1 => [$cid, 'Integer'], ]); } } /** * @param array $existsByOffset * Array(int $cidOffset => bool $expectExists). * @param int $generalOffset */ public function assertContactsExistByOffset($existsByOffset, $generalOffset = 0) { foreach ($existsByOffset as $offset => $expectExists) { $this->assertTrue(isset($this->cids[$generalOffset + $offset]), "Find cid at offset($generalOffset + $offset)"); $cid = $this->cids[$generalOffset + $offset]; $this->assertTrue(is_numeric($cid)); $this->assertDBQuery($expectExists ? 1 : 0, 'SELECT count(*) FROM civicrm_contact WHERE id = %1', [ 1 => [$cid, 'Integer'], ], "Check contact at offset($generalOffset + $offset)"); } } /** * Use SQL to INSERT a contact and assert success. Perform * work within a transaction. * * @param string $nesting * 'reuse-tx'|'nest-tx' how to construct transaction. * @param string $insert * 'sql-insert'|'bao-create' how to add the example record. * @param string $outcome * 'rollback'|'implicit-commit'|'explicit-commit' how to finish transaction. * @return int * cid */ public function createContactWithTransaction($nesting, $insert, $outcome) { if ($nesting != 'reuse-tx' && $nesting != 'nest-tx') { throw new RuntimeException('Bad test data: reuse=' . $nesting); } if ($insert != 'sql-insert' && $insert != 'bao-create') { throw new RuntimeException('Bad test data: insert=' . $insert); } if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') { throw new RuntimeException('Bad test data: outcome=' . $outcome); } $tx = new CRM_Core_Transaction($nesting === 'nest-tx'); if ($insert == 'sql-insert') { $r = CRM_Core_DAO::executeQuery("INSERT INTO civicrm_contact(first_name,last_name) VALUES ('ff', 'll')"); $cid = $r->getConnection()->lastInsertId(); } elseif ($insert == 'bao-create') { $params = [ 'contact_type' => 'Individual', 'first_name' => 'FF', 'last_name' => 'LL', ]; $r = CRM_Contact_BAO_Contact::create($params); $cid = $r->id; } $this->cids[] = $cid; $this->assertContactsExist([$cid], TRUE); if ($outcome == 'rollback') { $tx->rollback(); } elseif ($outcome == 'explicit-commit') { $tx->commit(); } // else: implicit-commit return $cid; } /** * Perform a series of operations within smaller transactions. * * @param string $nesting * 'reuse-tx'|'nest-tx' how to construct transaction. * @param array $callbacks * See createContactWithTransaction. * @param array $existsByOffset * See assertContactsMix. * @param string $outcome * 'rollback'|'implicit-commit'|'explicit-commit' how to finish transaction. * @return void */ public function runBatch($nesting, $callbacks, $existsByOffset, $outcome) { if ($nesting != 'reuse-tx' && $nesting != 'nest-tx') { throw new RuntimeException('Bad test data: nesting=' . $nesting); } if ($outcome != 'rollback' && $outcome != 'implicit-commit' && $outcome != 'explicit-commit') { throw new RuntimeException('Bad test data: outcome=' . $nesting); } $tx = new CRM_Core_Transaction($nesting === 'reuse-tx'); $generalOffset = count($this->cids); foreach ($callbacks as $callback) { list ($cbNesting, $cbInsert, $cbOutcome) = $callback; $this->createContactWithTransaction($cbNesting, $cbInsert, $cbOutcome); } $this->assertContactsExistByOffset($existsByOffset, $generalOffset); if ($outcome == 'rollback') { $tx->rollback(); } elseif ($outcome == 'explicit-commit') { $tx->commit(); } // else: implicit-commit } /** * @param $arg1 * @param $arg2 */ public function _preCommit($arg1, $arg2) { $this->callbackLog[] = ['_preCommit', $arg1, $arg2]; } /** * @param $arg1 * @param $arg2 */ public function _postCommit($arg1, $arg2) { $this->callbackLog[] = ['_postCommit', $arg1, $arg2]; } /** * @param $arg1 * @param $arg2 */ public function _preRollback($arg1, $arg2) { $this->callbackLog[] = ['_preRollback', $arg1, $arg2]; } /** * @param $arg1 * @param $arg2 */ public function _postRollback($arg1, $arg2) { $this->callbackLog[] = ['_postRollback', $arg1, $arg2]; } }