Merge "Change ResultWrapper to IResultWrapper in pagers and special pages"
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / rdbms / database / DatabaseSQLTest.php
index b883c11..40e07d8 100644 (file)
@@ -1,7 +1,11 @@
 <?php
 
+use Wikimedia\Rdbms\IDatabase;
 use Wikimedia\Rdbms\LikeMatch;
 use Wikimedia\Rdbms\Database;
+use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Rdbms\DBTransactionStateError;
+use Wikimedia\Rdbms\DBUnexpectedError;
 
 /**
  * Test the parts of the Database abstract class that deal
@@ -1352,4 +1356,269 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
                $this->assertSame( 'CAST( fieldName AS INTEGER )', $output );
        }
 
+       /**
+        * @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 testAtomicSections() {
+               $this->database->startAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->cancelAtomic( __METHOD__ );
+               $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->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->startAtomic( __METHOD__, IDatabase::ATOMIC_CANCELABLE );
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+
+               $this->database->doAtomicSection( __METHOD__, function () {
+               } );
+               $this->assertLastSql( 'BEGIN; COMMIT' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->doAtomicSection( __METHOD__, function () {
+               } );
+               $this->database->rollback( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; RELEASE SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK' );
+
+               $this->database->begin( __METHOD__ );
+               try {
+                       $this->database->doAtomicSection( __METHOD__, function () {
+                               throw new RuntimeException( 'Test exception' );
+                       } );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( RuntimeException $ex ) {
+                       $this->assertSame( 'Test exception', $ex->getMessage() );
+               }
+               $this->database->commit( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; COMMIT' );
+       }
+
+       public static function provideAtomicSectionMethodsForErrors() {
+               return [
+                       [ 'endAtomic' ],
+                       [ 'cancelAtomic' ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideAtomicSectionMethodsForErrors
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        */
+       public function testNoAtomicSection( $method ) {
+               try {
+                       $this->database->$method( __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'No atomic transaction is open (got ' . __METHOD__ . ').',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       /**
+        * @dataProvider provideAtomicSectionMethodsForErrors
+        * @covers \Wikimedia\Rdbms\Database::endAtomic
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        */
+       public function testInvalidAtomicSectionEnded( $method ) {
+               $this->database->startAtomic( __METHOD__ . 'X' );
+               try {
+                       $this->database->$method( __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'Invalid atomic section ended (got ' . __METHOD__ . ').',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::cancelAtomic
+        */
+       public function testUncancellableAtomicSection() {
+               $this->database->startAtomic( __METHOD__ );
+               try {
+                       $this->database->cancelAtomic( __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'Uncancelable atomic section canceled (got ' . __METHOD__ . ').',
+                               $ex->getMessage()
+                       );
+               }
+       }
+
+       /**
+        * @expectedException \Wikimedia\Rdbms\DBTransactionStateError
+        */
+       public function testTransactionErrorState1() {
+               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+
+               $this->database->begin( __METHOD__ );
+               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+               $this->database->commit( __METHOD__ );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::query
+        */
+       public function testTransactionErrorState2() {
+               $wrapper = TestingAccessWrapper::newFromObject( $this->database );
+
+               $this->database->startAtomic( __METHOD__ );
+               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+               $this->database->rollback( __METHOD__ );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+               $this->assertLastSql( 'BEGIN; ROLLBACK' );
+
+               $this->database->startAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
+               $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
+
+               $this->database->begin( __METHOD__ );
+               $this->database->startAtomic( __METHOD__, Database::ATOMIC_CANCELABLE );
+               $this->database->update( 'y', [ 'a' => 1 ], [ 'field' => 1 ], __METHOD__ );
+               $wrapper->trxStatus = Database::STATUS_TRX_ERROR;
+               $this->database->cancelAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+               $this->database->startAtomic( __METHOD__ );
+               $this->database->delete( 'y', [ 'field' => 1 ], __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->database->commit( __METHOD__ );
+               // phpcs:ignore Generic.Files.LineLength
+               $this->assertLastSql( 'BEGIN; SAVEPOINT wikimedia_rdbms_atomic1; UPDATE y SET a = \'1\' WHERE field = \'1\'; ROLLBACK TO SAVEPOINT wikimedia_rdbms_atomic1; DELETE FROM y WHERE field = \'1\'; COMMIT' );
+               $this->assertEquals( 0, $this->database->trxLevel(), 'Use after rollback()' );
+
+               // Next transaction
+               $this->database->startAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_OK, $wrapper->trxStatus() );
+               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+               $this->database->endAtomic( __METHOD__ );
+               $this->assertEquals( Database::STATUS_TRX_NONE, $wrapper->trxStatus() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::query
+        */
+       public function testImplicitTransactionRollback() {
+               $doError = function ( $wasKnown = true ) {
+                       $this->database->forceNextQueryError( 666, 'Evilness' );
+                       try {
+                               $this->database->delete( 'error', '1', __CLASS__ . '::SomeCaller' );
+                               $this->fail( 'Expected exception not thrown' );
+                       } catch ( DBError $e ) {
+                               $this->assertSame( 666, $e->errno );
+                       }
+               };
+
+               $this->database->setFlag( Database::DBO_TRX );
+
+               // Implicit transaction gets silently rolled back
+               $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
+               call_user_func( $doError, false );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               $this->database->commit( __METHOD__, Database::FLUSHING_INTERNAL );
+               // phpcs:ignore
+               $this->assertLastSql( 'BEGIN; DELETE FROM error WHERE 1; ROLLBACK; BEGIN; DELETE FROM x WHERE field = \'1\'; COMMIT' );
+
+               // ... unless there were prior writes
+               $this->database->begin( __METHOD__, Database::TRANSACTION_INTERNAL );
+               $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+               call_user_func( $doError, false );
+               try {
+                       $this->database->delete( 'x', [ 'field' => 1 ], __METHOD__ );
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBTransactionStateError $e ) {
+               }
+               $this->database->rollback( __METHOD__, Database::FLUSHING_INTERNAL );
+               // phpcs:ignore
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'1\'; DELETE FROM error WHERE 1; ROLLBACK' );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::close
+        */
+       public function testPrematureClose1() {
+               $fname = __METHOD__;
+               $this->database->begin( __METHOD__ );
+               $this->database->onTransactionIdle( function () use ( $fname ) {
+                       $this->database->query( 'SELECT 1', $fname );
+               } );
+               $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+               $this->database->close();
+
+               $this->assertFalse( $this->database->isOpen() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; COMMIT; SELECT 1' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
+
+       /**
+        * @covers \Wikimedia\Rdbms\Database::close
+        */
+       public function testPrematureClose2() {
+               try {
+                       $fname = __METHOD__;
+                       $this->database->startAtomic( __METHOD__ );
+                       $this->database->onTransactionIdle( function () use ( $fname ) {
+                               $this->database->query( 'SELECT 1', $fname );
+                       } );
+                       $this->database->delete( 'x', [ 'field' => 3 ], __METHOD__ );
+                       $this->database->close();
+                       $this->fail( 'Expected exception not thrown' );
+               } catch ( DBUnexpectedError $ex ) {
+                       $this->assertSame(
+                               'Wikimedia\Rdbms\Database::close: atomic sections ' .
+                               'DatabaseSQLTest::testPrematureClose2 are still open.',
+                               $ex->getMessage()
+                       );
+               }
+
+               $this->assertFalse( $this->database->isOpen() );
+               $this->assertLastSql( 'BEGIN; DELETE FROM x WHERE field = \'3\'; ROLLBACK' );
+               $this->assertEquals( 0, $this->database->trxLevel() );
+       }
 }