Merge "registration: Only allow one extension to set a specific config setting"
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / rdbms / database / DatabaseTest.php
1 <?php
2
3 use Wikimedia\Rdbms\IDatabase;
4 use Wikimedia\Rdbms\LBFactorySingle;
5 use Wikimedia\Rdbms\TransactionProfiler;
6 use Wikimedia\TestingAccessWrapper;
7
8 class DatabaseTest extends PHPUnit_Framework_TestCase {
9
10 protected function setUp() {
11 $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
12 }
13
14 public static function provideAddQuotes() {
15 return [
16 [ null, 'NULL' ],
17 [ 1234, "'1234'" ],
18 [ 1234.5678, "'1234.5678'" ],
19 [ 'string', "'string'" ],
20 [ 'string\'s cause trouble', "'string\'s cause trouble'" ],
21 ];
22 }
23
24 /**
25 * @dataProvider provideAddQuotes
26 * @covers Wikimedia\Rdbms\Database::addQuotes
27 */
28 public function testAddQuotes( $input, $expected ) {
29 $this->assertEquals( $expected, $this->db->addQuotes( $input ) );
30 }
31
32 public static function provideTableName() {
33 // Formatting is mostly ignored since addIdentifierQuotes is abstract.
34 // For testing of addIdentifierQuotes, see actual Database subclas tests.
35 return [
36 'local' => [
37 'tablename',
38 'tablename',
39 'quoted',
40 ],
41 'local-raw' => [
42 'tablename',
43 'tablename',
44 'raw',
45 ],
46 'shared' => [
47 'sharedb.tablename',
48 'tablename',
49 'quoted',
50 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
51 ],
52 'shared-raw' => [
53 'sharedb.tablename',
54 'tablename',
55 'raw',
56 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
57 ],
58 'shared-prefix' => [
59 'sharedb.sh_tablename',
60 'tablename',
61 'quoted',
62 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
63 ],
64 'shared-prefix-raw' => [
65 'sharedb.sh_tablename',
66 'tablename',
67 'raw',
68 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
69 ],
70 'foreign' => [
71 'databasename.tablename',
72 'databasename.tablename',
73 'quoted',
74 ],
75 'foreign-raw' => [
76 'databasename.tablename',
77 'databasename.tablename',
78 'raw',
79 ],
80 ];
81 }
82
83 /**
84 * @dataProvider provideTableName
85 * @covers Wikimedia\Rdbms\Database::tableName
86 */
87 public function testTableName( $expected, $table, $format, array $alias = null ) {
88 if ( $alias ) {
89 $this->db->setTableAliases( [ $table => $alias ] );
90 }
91 $this->assertEquals(
92 $expected,
93 $this->db->tableName( $table, $format ?: 'quoted' )
94 );
95 }
96
97 public function provideTableNamesWithIndexClauseOrJOIN() {
98 return [
99 'one-element array' => [
100 [ 'table' ], [], 'table '
101 ],
102 'comma join' => [
103 [ 'table1', 'table2' ], [], 'table1,table2 '
104 ],
105 'real join' => [
106 [ 'table1', 'table2' ],
107 [ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
108 'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
109 ],
110 'real join with multiple conditionals' => [
111 [ 'table1', 'table2' ],
112 [ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
113 'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
114 ],
115 'join with parenthesized group' => [
116 [ 'table1', 'n' => [ 'table2', 'table3' ] ],
117 [
118 'table3' => [ 'JOIN', 't2_id = t3_id' ],
119 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
120 ],
121 'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
122 ],
123 ];
124 }
125
126 /**
127 * @dataProvider provideTableNamesWithIndexClauseOrJOIN
128 * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
129 */
130 public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
131 $clause = TestingAccessWrapper::newFromObject( $this->db )
132 ->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
133 $this->assertSame( $expect, $clause );
134 }
135
136 /**
137 * @covers Wikimedia\Rdbms\Database::onTransactionIdle
138 * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
139 */
140 public function testTransactionIdle() {
141 $db = $this->db;
142
143 $db->setFlag( DBO_TRX );
144 $called = false;
145 $flagSet = null;
146 $db->onTransactionIdle(
147 function () use ( $db, &$flagSet, &$called ) {
148 $called = true;
149 $flagSet = $db->getFlag( DBO_TRX );
150 },
151 __METHOD__
152 );
153 $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
154 $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
155 $this->assertTrue( $called, 'Callback reached' );
156
157 $db->clearFlag( DBO_TRX );
158 $flagSet = null;
159 $db->onTransactionIdle(
160 function () use ( $db, &$flagSet ) {
161 $flagSet = $db->getFlag( DBO_TRX );
162 },
163 __METHOD__
164 );
165 $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
166 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
167
168 $db->clearFlag( DBO_TRX );
169 $db->onTransactionIdle(
170 function () use ( $db ) {
171 $db->setFlag( DBO_TRX );
172 },
173 __METHOD__
174 );
175 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
176 }
177
178 /**
179 * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
180 * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
181 */
182 public function testTransactionPreCommitOrIdle() {
183 $db = $this->getMockDB( [ 'isOpen' ] );
184 $db->method( 'isOpen' )->willReturn( true );
185 $db->clearFlag( DBO_TRX );
186
187 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX is not set' );
188
189 $called = false;
190 $db->onTransactionPreCommitOrIdle(
191 function () use ( &$called ) {
192 $called = true;
193 },
194 __METHOD__
195 );
196 $this->assertTrue( $called, 'Called when idle' );
197
198 $db->begin( __METHOD__ );
199 $called = false;
200 $db->onTransactionPreCommitOrIdle(
201 function () use ( &$called ) {
202 $called = true;
203 },
204 __METHOD__
205 );
206 $this->assertFalse( $called, 'Not called when transaction is active' );
207 $db->commit( __METHOD__ );
208 $this->assertTrue( $called, 'Called when transaction is committed' );
209 }
210
211 /**
212 * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
213 * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
214 */
215 public function testTransactionPreCommitOrIdle_TRX() {
216 $db = $this->getMockDB( [ 'isOpen' ] );
217 $db->method( 'isOpen' )->willReturn( true );
218 $db->setFlag( DBO_TRX );
219
220 $lbFactory = LBFactorySingle::newFromConnection( $db );
221 // Ask for the connectin so that LB sets internal state
222 // about this connection being the master connection
223 $lb = $lbFactory->getMainLB();
224 $conn = $lb->openConnection( $lb->getWriterIndex() );
225 $this->assertSame( $db, $conn, 'Same DB instance' );
226 $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
227
228 $called = false;
229 $db->onTransactionPreCommitOrIdle(
230 function () use ( &$called ) {
231 $called = true;
232 }
233 );
234 $this->assertFalse( $called, 'Not called when idle if DBO_TRX is set' );
235
236 $lbFactory->beginMasterChanges( __METHOD__ );
237 $this->assertFalse( $called, 'Not called when lb-transaction is active' );
238
239 $lbFactory->commitMasterChanges( __METHOD__ );
240 $this->assertTrue( $called, 'Called when lb-transaction is committed' );
241 }
242
243 /**
244 * @covers Wikimedia\Rdbms\Database::onTransactionResolution
245 * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
246 */
247 public function testTransactionResolution() {
248 $db = $this->db;
249
250 $db->clearFlag( DBO_TRX );
251 $db->begin( __METHOD__ );
252 $called = false;
253 $db->onTransactionResolution( function () use ( $db, &$called ) {
254 $called = true;
255 $db->setFlag( DBO_TRX );
256 } );
257 $db->commit( __METHOD__ );
258 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
259 $this->assertTrue( $called, 'Callback reached' );
260
261 $db->clearFlag( DBO_TRX );
262 $db->begin( __METHOD__ );
263 $called = false;
264 $db->onTransactionResolution( function () use ( $db, &$called ) {
265 $called = true;
266 $db->setFlag( DBO_TRX );
267 } );
268 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
269 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
270 $this->assertTrue( $called, 'Callback reached' );
271 }
272
273 /**
274 * @covers Wikimedia\Rdbms\Database::setTransactionListener
275 */
276 public function testTransactionListener() {
277 $db = $this->db;
278
279 $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
280 $called = true;
281 } );
282
283 $called = false;
284 $db->begin( __METHOD__ );
285 $db->commit( __METHOD__ );
286 $this->assertTrue( $called, 'Callback reached' );
287
288 $called = false;
289 $db->begin( __METHOD__ );
290 $db->commit( __METHOD__ );
291 $this->assertTrue( $called, 'Callback still reached' );
292
293 $called = false;
294 $db->begin( __METHOD__ );
295 $db->rollback( __METHOD__ );
296 $this->assertTrue( $called, 'Callback reached' );
297
298 $db->setTransactionListener( 'ping', null );
299 $called = false;
300 $db->begin( __METHOD__ );
301 $db->commit( __METHOD__ );
302 $this->assertFalse( $called, 'Callback not reached' );
303 }
304
305 /**
306 * Use this mock instead of DatabaseTestHelper for cases where
307 * DatabaseTestHelper is too inflexibile due to mocking too much
308 * or being too restrictive about fname matching (e.g. for tests
309 * that assert behaviour when the name is a mismatch, we need to
310 * catch the error here instead of there).
311 *
312 * @return Database
313 */
314 private function getMockDB( $methods = [] ) {
315 static $abstractMethods = [
316 'affectedRows',
317 'closeConnection',
318 'dataSeek',
319 'doQuery',
320 'fetchObject', 'fetchRow',
321 'fieldInfo', 'fieldName',
322 'getSoftwareLink', 'getServerVersion',
323 'getType',
324 'indexInfo',
325 'insertId',
326 'lastError', 'lastErrno',
327 'numFields', 'numRows',
328 'open',
329 'strencode',
330 ];
331 $db = $this->getMockBuilder( Database::class )
332 ->disableOriginalConstructor()
333 ->setMethods( array_values( array_unique( array_merge(
334 $abstractMethods,
335 $methods
336 ) ) ) )
337 ->getMock();
338 $wdb = TestingAccessWrapper::newFromObject( $db );
339 $wdb->trxProfiler = new TransactionProfiler();
340 $wdb->connLogger = new \Psr\Log\NullLogger();
341 $wdb->queryLogger = new \Psr\Log\NullLogger();
342 return $db;
343 }
344
345 /**
346 * @covers Wikimedia\Rdbms\Database::flushSnapshot
347 */
348 public function testFlushSnapshot() {
349 $db = $this->getMockDB( [ 'isOpen' ] );
350 $db->method( 'isOpen' )->willReturn( true );
351
352 $db->flushSnapshot( __METHOD__ ); // ok
353 $db->flushSnapshot( __METHOD__ ); // ok
354
355 $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
356 $db->query( 'SELECT 1', __METHOD__ );
357 $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
358 $db->flushSnapshot( __METHOD__ ); // ok
359 $db->restoreFlags( $db::RESTORE_PRIOR );
360
361 $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
362 }
363
364 public function testGetScopedLock() {
365 $db = $this->getMockDB( [ 'isOpen' ] );
366 $db->method( 'isOpen' )->willReturn( true );
367
368 $db->setFlag( DBO_TRX );
369 try {
370 $this->badLockingMethodImplicit( $db );
371 } catch ( RunTimeException $e ) {
372 $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
373 }
374 $db->clearFlag( DBO_TRX );
375 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
376 $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
377
378 try {
379 $this->badLockingMethodExplicit( $db );
380 } catch ( RunTimeException $e ) {
381 $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
382 }
383 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
384 $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
385 }
386
387 private function badLockingMethodImplicit( IDatabase $db ) {
388 $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
389 $db->query( "SELECT 1" ); // trigger DBO_TRX
390 throw new RunTimeException( "Uh oh!" );
391 }
392
393 private function badLockingMethodExplicit( IDatabase $db ) {
394 $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
395 $db->begin( __METHOD__ );
396 throw new RunTimeException( "Uh oh!" );
397 }
398
399 /**
400 * @covers Wikimedia\Rdbms\Database::getFlag
401 * @covers Wikimedia\Rdbms\Database::setFlag
402 * @covers Wikimedia\Rdbms\Database::restoreFlags
403 */
404 public function testFlagSetting() {
405 $db = $this->db;
406 $origTrx = $db->getFlag( DBO_TRX );
407 $origSsl = $db->getFlag( DBO_SSL );
408
409 $origTrx
410 ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
411 : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
412 $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
413
414 $origSsl
415 ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
416 : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
417 $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
418
419 $db->restoreFlags( $db::RESTORE_INITIAL );
420 $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
421 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
422
423 $origTrx
424 ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
425 : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
426 $origSsl
427 ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
428 : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
429
430 $db->restoreFlags();
431 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
432 $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
433
434 $db->restoreFlags();
435 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
436 $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
437 }
438
439 /**
440 * @covers Wikimedia\Rdbms\Database::tablePrefix
441 * @covers Wikimedia\Rdbms\Database::dbSchema
442 */
443 public function testMutators() {
444 $old = $this->db->tablePrefix();
445 $this->assertInternalType( 'string', $old, 'Prefix is string' );
446 $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
447 $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
448 $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
449 $this->db->tablePrefix( $old );
450 $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
451
452 $old = $this->db->dbSchema();
453 $this->assertInternalType( 'string', $old, 'Schema is string' );
454 $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
455 $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
456 $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
457 $this->db->dbSchema( $old );
458 $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
459 }
460 }