wiki.techinc.nl/tests/phpunit/integration/includes/db/DatabaseSqliteTest.php

398 lines
11 KiB
PHP
Raw Normal View History

<?php
use Psr\Log\NullLogger;
use Wikimedia\Rdbms\Blob;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\DatabaseSqlite;
use Wikimedia\Rdbms\ResultWrapper;
use Wikimedia\Rdbms\TransactionProfiler;
/**
* @group sqlite
* @group Database
* @group medium
*/
Define unit and integration test suites Following discussion in Ibb8175981092d7f41864e641cc3c118af70a5c76, this patch proposes to further reduce the scope of what unit tests may access, by removing the loading of DefaultSettings and GlobalFunctions.php. This also has the implied effect of disabling the storage backend, as well as the global service locator. MediaWikiTestCase is renamed to MediaWikiIntegrationTestCase so it's scope and purpose is more clear. Whether we still need to keep `@group Database` annotation around is debatable, as it's unclear to me what the performance costs are of implying database access for all tests which extend IntegrationTestCase. As far as I can tell, `@group Database` is primarily used in CI to run faster tests before slower ones, and with the new UnitTestCase the annotation seems redundant. To run all testsuites, use `composer phpunit`. Other composer scripts: - `composer phpunit:unit` to run unit tests - `composer phpunit:integration` to run integration tests - `composer phpunit:coverage` to generate code coverage reports from unit tests (requires XDebug). Note that you can pass arguments to composer scripts with `--`, e.g. `composer phpunit:integration --exclude-group Dump`. Other changes: - Rename bootstrap.php to bootstrap.maintenance.php so it's clear it's part of the legacy PHPUnit-as-maintenance-class setup - Create new bootstrap.php which loads the minimal configuration necessary for the tests, and do additional setup in the run() method of the unit/integration test case classes - Move the unit-tests.xml file to phpunit.xml.dist in preparation for this being the default test configuration For a follow-up patch: - Find unit/integration tests for extensions/skins - Migrate other test suites from suite.xml - Support running all tests via vendor/bin/phpunit Bug: T84948 Bug: T89432 Bug: T87781 Change-Id: Ie717b0ecf4fcfd089d46248f14853c80b7ef4a76
2019-06-26 02:33:14 +00:00
class DatabaseSqliteTest extends \MediaWikiIntegrationTestCase {
/** @var DatabaseSqlite */
protected $db;
/** @var array|null */
protected $currentTableInfo;
protected function setUp(): void {
Clean and repair many phpunit tests (+ fix implied configuration) This commit depends on the introduction of MediaWikiTestCase::setMwGlobals in change Iccf6ea81f4. Various tests already set their globals, but forgot to restore them afterwards, or forgot to call the parent setUp, tearDown... Either way they won't have to anymore with setMwGlobals. Consistent use of function characteristics: * protected function setUp * protected function tearDown * public static function (provide..) (Matching the function signature with PHPUnit/Framework/TestCase.php) Replaces: * public function (setUp|tearDown)\( * protected function $1( * \tfunction (setUp|tearDown)\( * \tprotected function $1( * \tfunction (data|provide)\( * \tpublic static function $1\( Also renamed a few "data#", "provider#" and "provides#" functions to "provide#" for consistency. This also removes confusion where the /media tests had a few private methods called dataFile(), which were sometimes expected to be data providers. Fixes: TimestampTest often failed due to a previous test setting a different language (it tests "1 hour ago" so need to make sure it is set to English). MWNamespaceTest became a lot cleaner now that it executes with a known context. Though the now-redundant code that was removed didn't work anyway because wgContentNamespaces isn't keyed by namespace id, it had them was values... FileBackendTest: * Fixed: "PHP Fatal: Using $this when not in object context" HttpTest * Added comment about: "PHP Fatal: Call to protected MWHttpRequest::__construct()" (too much unrelated code to fix in this commit) ExternalStoreTest * Add an assertTrue as well, without it the test is useless because regardless of whether wgExternalStores is true or false it only uses it if it is an array. Change-Id: I9d2b148e57bada64afeb7d5a99bec0e58f8e1561
2012-10-08 10:56:20 +00:00
parent::setUp();
if ( !Sqlite::isPresent() ) {
$this->markTestSkipped( 'No SQLite support detected' );
}
$this->db = $this->newMockDb();
if ( version_compare( $this->db->getServerVersion(), '3.6.0', '<' ) ) {
$this->markTestSkipped( "SQLite at least 3.6 required, {$this->db->getServerVersion()} found" );
}
}
/**
* @param string|null $version
* @param string|null &$sqlDump
* @return \PHPUnit\Framework\MockObject\MockObject|DatabaseSqlite
*/
private function newMockDb( $version = null, &$sqlDump = null ) {
$mock = $this->getMockBuilder( DatabaseSqlite::class )
->setConstructorArgs( [ [
'dbFilePath' => ':memory:',
'dbname' => 'Foo',
'schema' => null,
'host' => false,
'user' => false,
'password' => false,
'tablePrefix' => '',
'cliMode' => true,
'agent' => 'unit-tests',
'serverName' => null,
'flags' => DBO_DEFAULT,
'variables' => [ 'synchronous' => 'NORMAL', 'temp_store' => 'MEMORY' ],
'profiler' => null,
'topologyRole' => Database::ROLE_STREAMING_MASTER,
'trxProfiler' => new TransactionProfiler(),
'errorLogger' => null,
'deprecationLogger' => new NullLogger(),
'srvCache' => new HashBagOStuff(),
] ] )->onlyMethods( array_merge(
[ 'query' ],
$version ? [ 'getServerVersion' ] : []
) )->getMock();
$mock->initConnection();
$sqlDump = '';
$mock->method( 'query' )->willReturnCallback( static function ( $sql ) use ( &$sqlDump ) {
$sqlDump .= "$sql;";
return true;
} );
if ( $version ) {
$mock->method( 'getServerVersion' )->willReturn( $version );
}
return $mock;
}
private function assertResultIs( $expected, $res ) {
$this->assertNotNull( $res );
$i = 0;
foreach ( $res as $row ) {
foreach ( $expected[$i] as $key => $value ) {
$this->assertTrue( isset( $row->$key ) );
$this->assertEquals( $value, $row->$key );
}
$i++;
}
$this->assertEquals( count( $expected ), $i, 'Unexpected number of rows' );
}
public static function provideAddQuotes() {
return [
[ // #0: empty
'', "''"
],
[ // #1: simple
'foo bar', "'foo bar'"
],
[ // #2: including quote
'foo\'bar', "'foo''bar'"
],
// #3: including \0 (must be represented as hex, per https://bugs.php.net/bug.php?id=63419)
[
"x\0y",
"x'780079'",
],
[ // #4: blob object (must be represented as hex)
new Blob( "hello" ),
"x'68656c6c6f'",
],
[ // #5: null
null,
"''",
],
];
}
/**
* @dataProvider provideAddQuotes()
* @covers \Wikimedia\Rdbms\DatabaseSqlite::addQuotes
*/
public function testAddQuotes( $value, $expected ) {
// check quoting
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
$this->assertEquals( $expected, $db->addQuotes( $value ), 'string not quoted as expected' );
// ok, quoting works as expected, now try a round trip.
$re = $db->query( 'select ' . $db->addQuotes( $value ) );
$this->assertTrue( $re !== false, 'query failed' );
$row = $re->fetchRow();
if ( $row ) {
if ( $value instanceof Blob ) {
$value = $value->fetch();
}
$this->assertEquals( $value, $row[0], 'string mangled by the database' );
} else {
$this->fail( 'query returned no result' );
}
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::duplicateTableStructure
*/
public function testDuplicateTableStructure() {
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
$db->query( 'CREATE TABLE foo(foo, barfoo)' );
$db->query( 'CREATE INDEX index1 ON foo(foo)' );
$db->query( 'CREATE UNIQUE INDEX index2 ON foo(barfoo)' );
$db->duplicateTableStructure( 'foo', 'bar' );
2011-04-12 20:48:19 +00:00
$this->assertEquals( 'CREATE TABLE "bar"(foo, barfoo)',
$db->selectField( 'sqlite_master', 'sql', [ 'name' => 'bar' ] ),
'Normal table duplication'
);
$indexList = $db->query( 'PRAGMA INDEX_LIST("bar")' );
$index = $indexList->fetchObject();
$this->assertEquals( 'bar_index1', $index->name );
$this->assertSame( '0', (string)$index->unique );
$index = $indexList->fetchObject();
$this->assertEquals( 'bar_index2', $index->name );
$this->assertSame( '1', (string)$index->unique );
$db->duplicateTableStructure( 'foo', 'baz', true );
2011-04-12 20:48:19 +00:00
$this->assertEquals( 'CREATE TABLE "baz"(foo, barfoo)',
$db->selectField( 'sqlite_temp_master', 'sql', [ 'name' => 'baz' ] ),
'Creation of temporary duplicate'
);
$indexList = $db->query( 'PRAGMA INDEX_LIST("baz")' );
$index = $indexList->fetchObject();
$this->assertEquals( 'baz_index1', $index->name );
$this->assertSame( '0', (string)$index->unique );
$index = $indexList->fetchObject();
$this->assertEquals( 'baz_index2', $index->name );
$this->assertSame( '1', (string)$index->unique );
$this->assertSame( '0',
(string)$db->selectField( 'sqlite_master', 'COUNT(*)', [ 'name' => 'baz' ] ),
'Create a temporary duplicate only'
);
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::duplicateTableStructure
*/
public function testDuplicateTableStructureVirtual() {
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
if ( $db->getFulltextSearchModule() != 'FTS3' ) {
$this->markTestSkipped( 'FTS3 not supported, cannot create virtual tables' );
}
2011-04-12 20:48:19 +00:00
$db->query( 'CREATE VIRTUAL TABLE "foo" USING FTS3(foobar)' );
$db->duplicateTableStructure( 'foo', 'bar' );
2011-04-12 20:48:19 +00:00
$this->assertEquals( 'CREATE VIRTUAL TABLE "bar" USING FTS3(foobar)',
$db->selectField( 'sqlite_master', 'sql', [ 'name' => 'bar' ] ),
'Duplication of virtual tables'
);
$db->duplicateTableStructure( 'foo', 'baz', true );
2011-04-12 20:48:19 +00:00
$this->assertEquals( 'CREATE VIRTUAL TABLE "baz" USING FTS3(foobar)',
$db->selectField( 'sqlite_master', 'sql', [ 'name' => 'baz' ] ),
"Can't create temporary virtual tables, should fall back to non-temporary duplication"
);
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::deleteJoin
*/
public function testDeleteJoin() {
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
$db->query( 'CREATE TABLE a (a_1)', __METHOD__ );
$db->query( 'CREATE TABLE b (b_1, b_2)', __METHOD__ );
$db->insert( 'a', [
[ 'a_1' => 1 ],
[ 'a_1' => 2 ],
[ 'a_1' => 3 ],
],
__METHOD__
);
$db->insert( 'b', [
[ 'b_1' => 2, 'b_2' => 'a' ],
[ 'b_1' => 3, 'b_2' => 'b' ],
],
__METHOD__
);
$db->deleteJoin( 'a', 'b', 'a_1', 'b_1', [ 'b_2' => 'a' ], __METHOD__ );
$res = $db->query( "SELECT * FROM a", __METHOD__ );
$this->assertResultIs( [
[ 'a_1' => 1 ],
[ 'a_1' => 3 ],
],
$res
);
}
/**
* @coversNothing
*/
public function testEntireSchema() {
global $IP;
$result = Sqlite::checkSqlSyntax( "$IP/maintenance/sqlite/tables-generated.sql" );
$this->assertTrue( $result, $result );
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::insertId
*/
public function testInsertIdType() {
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
$databaseCreation = $db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
$this->assertInstanceOf( ResultWrapper::class, $databaseCreation, "Database creation" );
$insertion = $db->insert( 'a', [ 'a_1' => 10 ], __METHOD__ );
$this->assertTrue( $insertion, "Insertion worked" );
$this->assertIsInt( $db->insertId(), "Actual typecheck" );
$this->assertTrue( $db->close(), "closing database" );
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::insert
*/
public function testInsertAffectedRows() {
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
$db->query( 'CREATE TABLE testInsertAffectedRows ( foo )', __METHOD__ );
$insertion = $db->insert(
'testInsertAffectedRows',
[
[ 'foo' => 10 ],
[ 'foo' => 12 ],
[ 'foo' => 1555 ],
],
__METHOD__
);
$this->assertTrue( $insertion, "Insertion worked" );
$this->assertSame( 3, $db->affectedRows() );
$this->assertTrue( $db->close(), "closing database" );
}
/**
* @coversNothing
*/
public function testCaseInsensitiveLike() {
// TODO: Test this for all databases
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
$res = $db->query( 'SELECT "a" LIKE "A" AS a' );
$row = $res->fetchRow();
$this->assertFalse( (bool)$row['a'] );
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::__toString
*/
public function testToString() {
$db = DatabaseSqlite::newStandaloneInstance( ':memory:' );
$toString = (string)$db;
$this->assertStringContainsString( 'sqlite object', $toString );
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::getAttributes()
*/
public function testsAttributes() {
$attributes = Database::attributesFromType( 'sqlite' );
$this->assertTrue( $attributes[Database::ATTR_DB_LEVEL_LOCKING] );
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::insert()
* @param string $version
* @param string $table
* @param array $rows
* @param string $expectedSql
* @dataProvider provideNativeInserts
*/
public function testNativeInsertSupport( $version, $table, $rows, $expectedSql ) {
$sqlDump = '';
$db = $this->newMockDb( $version, $sqlDump );
$db->query( 'CREATE TABLE a ( a_1 )', __METHOD__ );
$sqlDump = '';
$db->insert( $table, $rows, __METHOD__ );
$this->assertEquals( $expectedSql, $sqlDump );
}
public function provideNativeInserts() {
return [
[
'3.8.0',
'a',
[ 'a_1' => 1 ],
'INSERT INTO "a" (a_1) VALUES (1);'
],
[
'3.8.0',
'a',
[
[ 'a_1' => 2 ],
[ 'a_1' => 3 ]
],
'INSERT INTO "a" (a_1) VALUES (2),(3);'
],
];
}
/**
* @covers \Wikimedia\Rdbms\DatabaseSqlite::replace()
* @param string $version
* @param string $table
* @param array $ukeys
* @param array $rows
* @param string $expectedSql
* @dataProvider provideNativeReplaces
*/
public function testNativeReplaceSupport( $version, $table, $ukeys, $rows, $expectedSql ) {
$sqlDump = '';
$db = $this->newMockDb( $version, $sqlDump );
$db->query( 'CREATE TABLE a ( a_1 PRIMARY KEY, a_2 )', __METHOD__ );
$sqlDump = '';
$db->replace( $table, $ukeys, $rows, __METHOD__ );
$this->assertEquals( $expectedSql, $sqlDump );
}
public function provideNativeReplaces() {
return [
[
'3.8.0',
'a',
[ 'a_1' ],
[ 'a_1' => 1, 'a_2' => 'x' ],
'REPLACE INTO "a" (a_1,a_2) VALUES (1,\'x\');'
],
[
'3.8.0',
'a',
[ 'a_1' ],
[
[ 'a_1' => 2, 'a_2' => 'x' ],
[ 'a_1' => 3, 'a_2' => 'y' ]
],
'REPLACE INTO "a" (a_1,a_2) VALUES (2,\'x\'),(3,\'y\');'
],
];
}
2011-04-12 20:48:19 +00:00
}