X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=tests%2Fphpunit%2Fincludes%2Flibs%2Frdbms%2Fdatabase%2FDatabaseSQLTest.php;h=ab2f11b5ea06879326c72ee4b3982c238a95aace;hp=665e953e9d6ef24080cc7811c3cd22c367b0e369;hb=7065200b036d4bbf0c46f4b236d761a79b57215e;hpb=579319d960107d1d61b6778b11e359e7046ce10d diff --git a/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php b/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php index 665e953e9d..ab2f11b5ea 100644 --- a/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php +++ b/tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php @@ -6,6 +6,7 @@ use Wikimedia\Rdbms\Database; use Wikimedia\TestingAccessWrapper; use Wikimedia\Rdbms\DBTransactionStateError; use Wikimedia\Rdbms\DBUnexpectedError; +use Wikimedia\Rdbms\DBTransactionError; /** * Test the parts of the Database abstract class that deal @@ -1402,22 +1403,167 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { // phpcs:ignore Generic.Files.LineLength $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); - $this->database->doAtomicSection( __METHOD__, function () { - } ); + $noOpCallack = function () { + }; + + $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE ); + $this->assertLastSql( 'BEGIN; COMMIT' ); + + $this->database->doAtomicSection( __METHOD__, $noOpCallack ); $this->assertLastSql( 'BEGIN; COMMIT' ); $this->database->begin( __METHOD__ ); - $this->database->doAtomicSection( __METHOD__, function () { - } ); + $this->database->doAtomicSection( __METHOD__, $noOpCallack, IDatabase::ATOMIC_CANCELABLE ); $this->database->rollback( __METHOD__ ); // phpcs:ignore Generic.Files.LineLength $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' ); + $fname = __METHOD__; + $triggerMap = [ + '-' => '-', + IDatabase::TRIGGER_COMMIT => 'tCommit', + IDatabase::TRIGGER_ROLLBACK => 'tRollback' + ]; + $callback1 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) { + $this->database->query( "SELECT 1, {$triggerMap[$trigger]} AS t", $fname ); + }; + $callback2 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) { + $this->database->query( "SELECT 2, {$triggerMap[$trigger]} AS t", $fname ); + }; + $callback3 = function ( $trigger = '-' ) use ( $fname, $triggerMap ) { + $this->database->query( "SELECT 3, {$triggerMap[$trigger]} AS t", $fname ); + }; + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionPreCommitOrIdle( $callback1, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $callback1, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK; SELECT 1, tRollback AS t' ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->onTransactionPreCommitOrIdle( $callback1, __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'SELECT 1, - AS t', + 'SELECT 3, - AS t', + 'COMMIT' + ] ) ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionIdle( $callback2, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->onTransactionIdle( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT', + 'SELECT 1, tCommit AS t', + 'SELECT 3, tCommit AS t' + ] ) ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->onTransactionResolution( $callback1, __METHOD__ ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $callback2, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT', + 'SELECT 1, tCommit AS t', + 'SELECT 2, tRollback AS t', + 'SELECT 3, tCommit AS t' + ] ) ); + + $makeCallback = function ( $id ) use ( $fname, $triggerMap ) { + return function ( $trigger = '-' ) use ( $id, $fname, $triggerMap ) { + $this->database->query( "SELECT $id, {$triggerMap[$trigger]} AS t", $fname ); + }; + }; + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT', + 'SELECT 1, tRollback AS t' + ] ) ); + + $this->database->startAtomic( __METHOD__ . '_level1', IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $makeCallback( 1 ), __METHOD__ ); + $this->database->startAtomic( __METHOD__ . '_level2' ); + $this->database->startAtomic( __METHOD__ . '_level3', IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionResolution( $makeCallback( 2 ), __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->database->onTransactionResolution( $makeCallback( 3 ), __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ . '_level3' ); + $this->database->endAtomic( __METHOD__ . '_level2' ); + $this->database->onTransactionResolution( $makeCallback( 4 ), __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_level1' ); + $this->assertLastSql( implode( "; ", [ + 'BEGIN', + 'SAVEPOINT wikimedia_rdbms_atomic1', + 'SAVEPOINT wikimedia_rdbms_atomic2', + 'RELEASE SAVEPOINT wikimedia_rdbms_atomic2', + 'ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1', + 'COMMIT; SELECT 1, tCommit AS t', + 'SELECT 2, tRollback AS t', + 'SELECT 3, tRollback AS t', + 'SELECT 4, tCommit AS t' + ] ) ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSectionsRecovery() { $this->database->begin( __METHOD__ ); try { - $this->database->doAtomicSection( __METHOD__, function () { - throw new RuntimeException( 'Test exception' ); - } ); + $this->database->doAtomicSection( + __METHOD__, + function () { + $this->database->startAtomic( 'inner_func1' ); + $this->database->startAtomic( 'inner_func2' ); + + throw new RuntimeException( 'Test exception' ); + }, + IDatabase::ATOMIC_CANCELABLE + ); $this->fail( 'Expected exception not thrown' ); } catch ( RuntimeException $ex ) { $this->assertSame( 'Test exception', $ex->getMessage() ); @@ -1425,6 +1571,180 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { $this->database->commit( __METHOD__ ); // phpcs:ignore Generic.Files.LineLength $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); + + $this->database->begin( __METHOD__ ); + try { + $this->database->doAtomicSection( + __METHOD__, + function () { + throw new RuntimeException( 'Test exception' ); + } + ); + $this->fail( 'Test exception not thrown' ); + } catch ( RuntimeException $ex ) { + $this->assertSame( 'Test exception', $ex->getMessage() ); + } + try { + $this->database->commit( __METHOD__ ); + $this->fail( 'Test exception not thrown' ); + } catch ( DBTransactionError $ex ) { + $this->assertSame( + 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.', + $ex->getMessage() + ); + } + $this->database->rollback( __METHOD__ ); + $this->assertLastSql( 'BEGIN; ROLLBACK' ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSectionsCallbackCancellation() { + $fname = __METHOD__; + $callback1Called = null; + $callback1 = function ( $trigger = '-' ) use ( $fname, &$callback1Called ) { + $callback1Called = $trigger; + $this->database->query( "SELECT 1", $fname ); + }; + $callback2Called = null; + $callback2 = function ( $trigger = '-' ) use ( $fname, &$callback2Called ) { + $callback2Called = $trigger; + $this->database->query( "SELECT 2", $fname ); + }; + $callback3Called = null; + $callback3 = function ( $trigger = '-' ) use ( $fname, &$callback3Called ) { + $callback3Called = $trigger; + $this->database->query( "SELECT 3", $fname ); + }; + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' ); + + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner', IDatabase::ATOMIC_CANCELABLE ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SAVEPOINT wikimedia_rdbms_atomic2; RELEASE SAVEPOINT wikimedia_rdbms_atomic2; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT; SELECT 3' ); + + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__, $atomicId ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $atomicId = $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + try { + $this->database->cancelAtomic( __METHOD__ . '_X', $atomicId ); + } catch ( DBUnexpectedError $e ) { + $m = __METHOD__; + $this->assertSame( + "Invalid atomic section ended (got {$m}_X but expected {$m}).", + $e->getMessage() + ); + } + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $this->database->cancelAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + + $wrapper = TestingAccessWrapper::newFromObject( $this->database ); + $callback1Called = null; + $callback2Called = null; + $callback3Called = null; + $this->database->startAtomic( __METHOD__ . '_outer' ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->startAtomic( __METHOD__ . '_inner' ); + $this->database->onTransactionIdle( $callback1, __METHOD__ ); + $this->database->onTransactionPreCommitOrIdle( $callback2, __METHOD__ ); + $this->database->onTransactionResolution( $callback3, __METHOD__ ); + $wrapper->trxStatus = Database::STATUS_TRX_ERROR; + $this->database->cancelAtomic( __METHOD__ . '_inner' ); + $this->database->cancelAtomic( __METHOD__ ); + $this->database->endAtomic( __METHOD__ . '_outer' ); + $this->assertNull( $callback1Called ); + $this->assertNull( $callback2Called ); + $this->assertEquals( IDatabase::TRIGGER_ROLLBACK, $callback3Called ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::doSavepoint + * @covers \Wikimedia\Rdbms\Database::doReleaseSavepoint + * @covers \Wikimedia\Rdbms\Database::doRollbackToSavepoint + * @covers \Wikimedia\Rdbms\Database::startAtomic + * @covers \Wikimedia\Rdbms\Database::endAtomic + * @covers \Wikimedia\Rdbms\Database::cancelAtomic + * @covers \Wikimedia\Rdbms\Database::doAtomicSection + */ + public function testAtomicSectionsTrxRound() { + $this->database->setFlag( IDatabase::DBO_TRX ); + $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE ); + $this->database->query( 'SELECT 1', __METHOD__ ); + $this->database->endAtomic( __METHOD__ ); + $this->database->commit( __METHOD__, IDatabase::FLUSHING_ALL_PEERS ); + // phpcs:ignore Generic.Files.LineLength + $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; SELECT 1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' ); } public static function provideAtomicSectionMethodsForErrors() { @@ -1445,7 +1765,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { $this->fail( 'Expected exception not thrown' ); } catch ( DBUnexpectedError $ex ) { $this->assertSame( - 'No atomic transaction is open (got ' . __METHOD__ . ').', + 'No atomic section is open (got ' . __METHOD__ . ').', $ex->getMessage() ); } @@ -1463,7 +1783,8 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { $this->fail( 'Expected exception not thrown' ); } catch ( DBUnexpectedError $ex ) { $this->assertSame( - 'Invalid atomic section ended (got ' . __METHOD__ . ').', + 'Invalid atomic section ended (got ' . __METHOD__ . ' but expected ' . + __METHOD__ . 'X' . ').', $ex->getMessage() ); } @@ -1476,10 +1797,11 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { $this->database->startAtomic( __METHOD__ ); try { $this->database->cancelAtomic( __METHOD__ ); + $this->database->select( 'test', '1', [], __METHOD__ ); $this->fail( 'Expected exception not thrown' ); - } catch ( DBUnexpectedError $ex ) { + } catch ( DBTransactionError $ex ) { $this->assertSame( - 'Uncancelable atomic section canceled (got ' . __METHOD__ . ').', + 'Cannot execute query from ' . __METHOD__ . ' while transaction status is ERROR.', $ex->getMessage() ); } @@ -1687,4 +2009,59 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase { $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' ); $this->assertEquals( 0, $this->database->trxLevel() ); } + + /** + * @covers \Wikimedia\Rdbms\Database::close + */ + public function testPrematureClose3() { + try { + $this->database->setFlag( IDatabase::DBO_TRX ); + $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ ); + $this->assertEquals( 1, $this->database->trxLevel() ); + $this->database->close(); + $this->fail( 'Expected exception not thrown' ); + } catch ( DBUnexpectedError $ex ) { + $this->assertSame( + 'Wikimedia\Rdbms\Database::close: ' . + 'mass commit/rollback of peer transaction required (DBO_TRX set).', + $ex->getMessage() + ); + } + + $this->assertFalse( $this->database->isOpen() ); + $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' ); + $this->assertEquals( 0, $this->database->trxLevel() ); + } + + /** + * @covers \Wikimedia\Rdbms\Database::close + */ + public function testPrematureClose4() { + $this->database->setFlag( IDatabase::DBO_TRX ); + $this->database->query( 'SELECT 1', __METHOD__ ); + $this->assertEquals( 1, $this->database->trxLevel() ); + $this->database->close(); + $this->database->clearFlag( IDatabase::DBO_TRX ); + + $this->assertFalse( $this->database->isOpen() ); + $this->assertLastSql( 'BEGIN; SELECT 1; COMMIT' ); + $this->assertEquals( 0, $this->database->trxLevel() ); + } + + /** + * @covers Wikimedia\Rdbms\Database::selectFieldValues() + */ + public function testSelectFieldValues() { + $this->database->forceNextResult( [ + (object)[ 'value' => 'row1' ], + (object)[ 'value' => 'row2' ], + (object)[ 'value' => 'row3' ], + ] ); + + $this->assertSame( + [ 'row1', 'row2', 'row3' ], + $this->database->selectFieldValues( 'table', 'table.field', 'conds', __METHOD__ ) + ); + $this->assertLastSql( 'SELECT table.field AS value FROM table WHERE conds' ); + } }