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