wiki.techinc.nl/tests/phpunit/unit/includes/libs/rdbms/database/DBConnRefTest.php
Umherirrender fc9e42823b rdbms: Create IReadableDatabase::andExpr() / ::orExpr()
Avoid the call to internal constructor of AndExpressionGroup and
OrExpressionGroup by creating a factory function similiar as the
IReadableDatabase::expr function for Expression objects.

This is also a replacement for calls to ISQLPlatform::makeList with
LIST_AND or LIST_OR argument to reduce passing sql as string to the
query builders.

Created two functions to allow the return type to be set for both
expression group to allow further calls of ->and() or ->or() on the
returned object.
Depending on the length of the array argument to makeList() it is
sometimes hard to see if the list gets converted to AND or OR, having
the operator in the function name makes it easier to read, so two
functions are helpful in this case as well.

Bug: T358961
Change-Id: Ica29689cbd0b111b099bb09b20845f85ae4c3376
2024-07-11 15:29:20 +00:00

237 lines
7 KiB
PHP

<?php
namespace Wikimedia\Tests\Rdbms;
use InvalidArgumentException;
use LogicException;
use MediaWikiCoversValidator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Wikimedia\Rdbms\AndExpressionGroup;
use Wikimedia\Rdbms\DBConnRef;
use Wikimedia\Rdbms\DBReadOnlyRoleError;
use Wikimedia\Rdbms\DBUnexpectedError;
use Wikimedia\Rdbms\Expression;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILoadBalancer;
use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\OrExpressionGroup;
/**
* @covers \Wikimedia\Rdbms\DBConnRef
*/
class DBConnRefTest extends TestCase {
use MediaWikiCoversValidator;
/**
* @return ILoadBalancer|MockObject
*/
private function getLoadBalancerMock() {
// getConnection() and getConnectionInternal() should keep returning the same connection
// on every call, unless that connection was closed. Then they should return a new
// connection.
$conn = $this->getDatabaseMock();
$getDatabaseMock = function () use ( &$conn ) {
if ( !$conn->isOpen() ) {
$conn = $this->getDatabaseMock();
}
return $conn;
};
$lb = $this->createMock( ILoadBalancer::class );
$lb->method( 'getConnection' )->willReturnCallback( $getDatabaseMock );
$lb->method( 'getConnectionInternal' )->willReturnCallback( $getDatabaseMock );
$lb->method( 'getConnectionRef' )->willReturnCallback(
function () use ( $lb ) {
return $this->getDBConnRef( $lb );
}
);
return $lb;
}
/**
* @return IDatabase
*/
private function getDatabaseMock() {
$db = $this->createMock( IDatabase::class );
$open = true;
$db->method( 'select' )->willReturnCallback( static function () use ( &$open ) {
if ( !$open ) {
throw new LogicException( "Not open" );
}
return new FakeResultWrapper( [] );
} );
$db->method( 'close' )->willReturnCallback( static function () use ( &$open ) {
$open = false;
return true;
} );
$db->method( 'isOpen' )->willReturnCallback( static function () use ( &$open ) {
return $open;
} );
return $db;
}
/**
* @param ILoadBalancer|null $lb
* @return IDatabase
*/
private function getDBConnRef( ILoadBalancer $lb = null ) {
$lb = $lb ?: $this->getLoadBalancerMock();
return new DBConnRef( $lb, [ DB_PRIMARY, [], 'mywiki', 0 ], DB_PRIMARY );
}
/**
* Test that bumping the modification counter causes the wrapped connection
* to be discarded and re-aquired.
*/
public function testModCount() {
$lb = $this->getLoadBalancerMock();
$lb->expects( $this->exactly( 3 ) )->method( 'getConnectionInternal' );
$params = [ DB_PRIMARY, [], 'mywiki', 0 ];
$modcount = 0;
$ref = new DBConnRef( $lb, $params, DB_PRIMARY, $modcount );
$ref->select( 'test', '*' );
$ref->select( 'test', '*' );
$modcount++; // cause second call to getConnectionInternal
$ref->select( 'test', '*' );
$ref->select( 'test', '*' );
$modcount++; // cause third call to getConnectionInternal
$ref->select( 'test', '*' );
$ref->select( 'test', '*' );
}
public function testConstruct() {
$lb = $this->createMock( ILoadBalancer::class );
$lb->expects( $this->once() )
->method( 'getConnectionInternal' )
->with( DB_PRIMARY, [ 'test' ], 'dummy', ILoadBalancer::CONN_TRX_AUTOCOMMIT )
->willReturn( $this->getDatabaseMock() );
$ref = new DBConnRef(
$lb,
[ DB_PRIMARY, [ 'test' ], 'dummy', $lb::CONN_TRX_AUTOCOMMIT ],
DB_PRIMARY
);
$this->assertInstanceOf( IResultWrapper::class, $ref->select( 'whatever', '*' ) );
$this->assertEquals( DB_PRIMARY, $ref->getReferenceRole() );
$ref2 = new DBConnRef(
$lb,
[ DB_PRIMARY, [ 'test' ], 'dummy', $lb::CONN_TRX_AUTOCOMMIT ],
DB_REPLICA
);
$this->assertEquals( DB_REPLICA, $ref2->getReferenceRole() );
}
public function testDestruct() {
$lb = $this->getLoadBalancerMock();
$this->innerMethodForTestDestruct( $lb );
}
private function innerMethodForTestDestruct( ILoadBalancer $lb ) {
$ref = $lb->getConnection( DB_REPLICA );
$this->assertInstanceOf( IResultWrapper::class, $ref->select( 'whatever', '*' ) );
}
public function testConstruct_failure() {
$this->expectException( InvalidArgumentException::class );
$lb = $this->getLoadBalancerMock();
new DBConnRef( $lb, 17, DB_REPLICA ); // bad constructor argument
}
public function testGetDomainID() {
$lb = $this->createMock( ILoadBalancer::class );
// getDomainID is optimized to not create a connection
$lb->expects( $this->never() )
->method( 'getConnection' );
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
$this->assertSame( 'dummy', $ref->getDomainID() );
}
public function testSelect() {
// select should get passed through normally
$ref = $this->getDBConnRef();
$this->assertInstanceOf( IResultWrapper::class, $ref->select( 'whatever', '*' ) );
}
public function testExpr() {
$ref = $this->getDBConnRef();
$this->assertInstanceOf( Expression::class, $ref->expr( 'key', '=', null ) );
$this->assertInstanceOf( AndExpressionGroup::class, $ref->andExpr( [ 'key' => null, $ref->expr( 'key', '=', null ) ] ) );
$this->assertInstanceOf( OrExpressionGroup::class, $ref->orExpr( [ 'key' => null, $ref->expr( 'key', '=', null ) ] ) );
}
public function testToString() {
$ref = $this->getDBConnRef();
$this->assertIsString( $ref->__toString() );
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_PRIMARY, [], 'test', 0 ], DB_PRIMARY );
$this->assertIsString( $ref->__toString() );
}
public function testClose() {
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_PRIMARY );
$this->expectException( DBUnexpectedError::class );
$ref->close();
}
public function testGetReferenceRole() {
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
$this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
$ref = new DBConnRef( $lb, [ DB_PRIMARY, [], 'dummy', 0 ], DB_PRIMARY );
$this->assertSame( DB_PRIMARY, $ref->getReferenceRole() );
$ref = new DBConnRef( $lb, [ 1, [], 'dummy', 0 ], DB_REPLICA );
$this->assertSame( DB_REPLICA, $ref->getReferenceRole() );
$ref = new DBConnRef( $lb, [ 0, [], 'dummy', 0 ], DB_PRIMARY );
$this->assertSame( DB_PRIMARY, $ref->getReferenceRole() );
}
/**
* @dataProvider provideRoleExceptions
*/
public function testRoleExceptions( $method, $args ) {
$lb = $this->getLoadBalancerMock();
$ref = new DBConnRef( $lb, [ DB_REPLICA, [], 'dummy', 0 ], DB_REPLICA );
$this->expectException( DBReadOnlyRoleError::class );
$ref->$method( ...$args );
}
public static function provideRoleExceptions() {
return [
[ 'insert', [ 'table', [ 'a' => 1 ] ] ],
[ 'update', [ 'table', [ 'a' => 1 ], [ 'a' => 2 ] ] ],
[ 'delete', [ 'table', [ 'a' => 1 ] ] ],
[ 'replace', [ 'table', [ 'a' ], [ 'a' => 1 ] ] ],
[ 'upsert', [ 'table', [ 'a' => 1 ], [ 'a' ], [ 'a = a + 1' ] ] ],
[ 'lock', [ 'k', 'method' ] ],
[ 'unlock', [ 'k', 'method' ] ],
[ 'getScopedLockAndFlush', [ 'k', 'method', 1 ] ]
];
}
}