Merge "Print logs generated during unit test when test fails"
[lhc/web/wiklou.git] / tests / phpunit / includes / Revision / RevisionStoreTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Revision;
4
5 use CommentStore;
6 use HashBagOStuff;
7 use InvalidArgumentException;
8 use Language;
9 use MediaWiki\MediaWikiServices;
10 use MediaWiki\Revision\RevisionAccessException;
11 use MediaWiki\Revision\RevisionStore;
12 use MediaWiki\Revision\SlotRoleRegistry;
13 use MediaWiki\Revision\SlotRecord;
14 use MediaWiki\Storage\SqlBlobStore;
15 use MediaWikiTestCase;
16 use MWException;
17 use Title;
18 use WANObjectCache;
19 use Wikimedia\Rdbms\Database;
20 use Wikimedia\Rdbms\LoadBalancer;
21 use Wikimedia\TestingAccessWrapper;
22 use WikitextContent;
23
24 class RevisionStoreTest extends MediaWikiTestCase {
25
26 private function useTextId() {
27 global $wgMultiContentRevisionSchemaMigrationStage;
28
29 return (bool)( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_OLD );
30 }
31
32 /**
33 * @param LoadBalancer $loadBalancer
34 * @param SqlBlobStore $blobStore
35 * @param WANObjectCache $WANObjectCache
36 *
37 * @return RevisionStore
38 */
39 private function getRevisionStore(
40 $loadBalancer = null,
41 $blobStore = null,
42 $WANObjectCache = null
43 ) {
44 global $wgMultiContentRevisionSchemaMigrationStage;
45 // the migration stage should be irrelevant, since all the tests that interact with
46 // the database are in RevisionStoreDbTest, not here.
47
48 return new RevisionStore(
49 $loadBalancer ?: $this->getMockLoadBalancer(),
50 $blobStore ?: $this->getMockSqlBlobStore(),
51 $WANObjectCache ?: $this->getHashWANObjectCache(),
52 MediaWikiServices::getInstance()->getCommentStore(),
53 MediaWikiServices::getInstance()->getContentModelStore(),
54 MediaWikiServices::getInstance()->getSlotRoleStore(),
55 MediaWikiServices::getInstance()->getSlotRoleRegistry(),
56 $wgMultiContentRevisionSchemaMigrationStage,
57 MediaWikiServices::getInstance()->getActorMigration()
58 );
59 }
60
61 /**
62 * @return \PHPUnit_Framework_MockObject_MockObject|LoadBalancer
63 */
64 private function getMockLoadBalancer() {
65 return $this->getMockBuilder( LoadBalancer::class )
66 ->disableOriginalConstructor()->getMock();
67 }
68
69 /**
70 * @return \PHPUnit_Framework_MockObject_MockObject|Database
71 */
72 private function getMockDatabase() {
73 return $this->getMockBuilder( Database::class )
74 ->disableOriginalConstructor()->getMock();
75 }
76
77 /**
78 * @return \PHPUnit_Framework_MockObject_MockObject|SqlBlobStore
79 */
80 private function getMockSqlBlobStore() {
81 return $this->getMockBuilder( SqlBlobStore::class )
82 ->disableOriginalConstructor()->getMock();
83 }
84
85 /**
86 * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
87 */
88 private function getMockCommentStore() {
89 return $this->getMockBuilder( CommentStore::class )
90 ->disableOriginalConstructor()->getMock();
91 }
92
93 /**
94 * @return \PHPUnit_Framework_MockObject_MockObject|SlotRoleRegistry
95 */
96 private function getMockSlotRoleRegistry() {
97 return $this->getMockBuilder( SlotRoleRegistry::class )
98 ->disableOriginalConstructor()->getMock();
99 }
100
101 private function getHashWANObjectCache() {
102 return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
103 }
104
105 public function provideSetContentHandlerUseDB() {
106 return [
107 // ContentHandlerUseDB can be true of false pre migration.
108 [ false, SCHEMA_COMPAT_OLD, false ],
109 [ true, SCHEMA_COMPAT_OLD, false ],
110 // During and after migration it can not be false...
111 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, true ],
112 [ false, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, true ],
113 [ false, SCHEMA_COMPAT_NEW, true ],
114 // ...but it can be true.
115 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
116 [ true, SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
117 [ true, SCHEMA_COMPAT_NEW, false ],
118 ];
119 }
120
121 /**
122 * @dataProvider provideSetContentHandlerUseDB
123 * @covers \MediaWiki\Revision\RevisionStore::getContentHandlerUseDB
124 * @covers \MediaWiki\Revision\RevisionStore::setContentHandlerUseDB
125 */
126 public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
127 if ( $expectedFail ) {
128 $this->setExpectedException( MWException::class );
129 }
130
131 $nameTables = MediaWikiServices::getInstance()->getNameTableStoreFactory();
132
133 $store = new RevisionStore(
134 $this->getMockLoadBalancer(),
135 $this->getMockSqlBlobStore(),
136 $this->getHashWANObjectCache(),
137 $this->getMockCommentStore(),
138 $nameTables->getContentModels(),
139 $nameTables->getSlotRoles(),
140 $this->getMockSlotRoleRegistry(),
141 $migrationMode,
142 MediaWikiServices::getInstance()->getActorMigration()
143 );
144
145 $store->setContentHandlerUseDB( $contentHandlerDb );
146 $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
147 }
148
149 /**
150 * @covers \MediaWiki\Revision\RevisionStore::getTitle
151 */
152 public function testGetTitle_successFromPageId() {
153 $mockLoadBalancer = $this->getMockLoadBalancer();
154 // Title calls wfGetDB() so we have to set the main service
155 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
156
157 $db = $this->getMockDatabase();
158 // Title calls wfGetDB() which uses a regular Connection
159 $mockLoadBalancer->expects( $this->atLeastOnce() )
160 ->method( 'getConnection' )
161 ->willReturn( $db );
162
163 // First call to Title::newFromID, faking no result (db lag?)
164 $db->expects( $this->at( 0 ) )
165 ->method( 'selectRow' )
166 ->with(
167 'page',
168 $this->anything(),
169 [ 'page_id' => 1 ]
170 )
171 ->willReturn( (object)[
172 'page_namespace' => '1',
173 'page_title' => 'Food',
174 ] );
175
176 $store = $this->getRevisionStore( $mockLoadBalancer );
177 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
178
179 $this->assertSame( 1, $title->getNamespace() );
180 $this->assertSame( 'Food', $title->getDBkey() );
181 }
182
183 /**
184 * @covers \MediaWiki\Revision\RevisionStore::getTitle
185 */
186 public function testGetTitle_successFromPageIdOnFallback() {
187 $mockLoadBalancer = $this->getMockLoadBalancer();
188 // Title calls wfGetDB() so we have to set the main service
189 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
190
191 $db = $this->getMockDatabase();
192 // Title calls wfGetDB() which uses a regular Connection
193 // Assert that the first call uses a REPLICA and the second falls back to master
194 $mockLoadBalancer->expects( $this->exactly( 2 ) )
195 ->method( 'getConnection' )
196 ->willReturn( $db );
197 // RevisionStore getTitle uses a ConnectionRef
198 $mockLoadBalancer->expects( $this->atLeastOnce() )
199 ->method( 'getConnectionRef' )
200 ->willReturn( $db );
201
202 // First call to Title::newFromID, faking no result (db lag?)
203 $db->expects( $this->at( 0 ) )
204 ->method( 'selectRow' )
205 ->with(
206 'page',
207 $this->anything(),
208 [ 'page_id' => 1 ]
209 )
210 ->willReturn( false );
211
212 // First select using rev_id, faking no result (db lag?)
213 $db->expects( $this->at( 1 ) )
214 ->method( 'selectRow' )
215 ->with(
216 [ 'revision', 'page' ],
217 $this->anything(),
218 [ 'rev_id' => 2 ]
219 )
220 ->willReturn( false );
221
222 // Second call to Title::newFromID, no result
223 $db->expects( $this->at( 2 ) )
224 ->method( 'selectRow' )
225 ->with(
226 'page',
227 $this->anything(),
228 [ 'page_id' => 1 ]
229 )
230 ->willReturn( (object)[
231 'page_namespace' => '2',
232 'page_title' => 'Foodey',
233 ] );
234
235 $store = $this->getRevisionStore( $mockLoadBalancer );
236 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
237
238 $this->assertSame( 2, $title->getNamespace() );
239 $this->assertSame( 'Foodey', $title->getDBkey() );
240 }
241
242 /**
243 * @covers \MediaWiki\Revision\RevisionStore::getTitle
244 */
245 public function testGetTitle_successFromRevId() {
246 $mockLoadBalancer = $this->getMockLoadBalancer();
247 // Title calls wfGetDB() so we have to set the main service
248 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
249
250 $db = $this->getMockDatabase();
251 // Title calls wfGetDB() which uses a regular Connection
252 $mockLoadBalancer->expects( $this->atLeastOnce() )
253 ->method( 'getConnection' )
254 ->willReturn( $db );
255 // RevisionStore getTitle uses a ConnectionRef
256 $mockLoadBalancer->expects( $this->atLeastOnce() )
257 ->method( 'getConnectionRef' )
258 ->willReturn( $db );
259
260 // First call to Title::newFromID, faking no result (db lag?)
261 $db->expects( $this->at( 0 ) )
262 ->method( 'selectRow' )
263 ->with(
264 'page',
265 $this->anything(),
266 [ 'page_id' => 1 ]
267 )
268 ->willReturn( false );
269
270 // First select using rev_id, faking no result (db lag?)
271 $db->expects( $this->at( 1 ) )
272 ->method( 'selectRow' )
273 ->with(
274 [ 'revision', 'page' ],
275 $this->anything(),
276 [ 'rev_id' => 2 ]
277 )
278 ->willReturn( (object)[
279 'page_namespace' => '1',
280 'page_title' => 'Food2',
281 ] );
282
283 $store = $this->getRevisionStore( $mockLoadBalancer );
284 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
285
286 $this->assertSame( 1, $title->getNamespace() );
287 $this->assertSame( 'Food2', $title->getDBkey() );
288 }
289
290 /**
291 * @covers \MediaWiki\Revision\RevisionStore::getTitle
292 */
293 public function testGetTitle_successFromRevIdOnFallback() {
294 $mockLoadBalancer = $this->getMockLoadBalancer();
295 // Title calls wfGetDB() so we have to set the main service
296 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
297
298 $db = $this->getMockDatabase();
299 // Title calls wfGetDB() which uses a regular Connection
300 // Assert that the first call uses a REPLICA and the second falls back to master
301 $mockLoadBalancer->expects( $this->exactly( 2 ) )
302 ->method( 'getConnection' )
303 ->willReturn( $db );
304 // RevisionStore getTitle uses a ConnectionRef
305 $mockLoadBalancer->expects( $this->atLeastOnce() )
306 ->method( 'getConnectionRef' )
307 ->willReturn( $db );
308
309 // First call to Title::newFromID, faking no result (db lag?)
310 $db->expects( $this->at( 0 ) )
311 ->method( 'selectRow' )
312 ->with(
313 'page',
314 $this->anything(),
315 [ 'page_id' => 1 ]
316 )
317 ->willReturn( false );
318
319 // First select using rev_id, faking no result (db lag?)
320 $db->expects( $this->at( 1 ) )
321 ->method( 'selectRow' )
322 ->with(
323 [ 'revision', 'page' ],
324 $this->anything(),
325 [ 'rev_id' => 2 ]
326 )
327 ->willReturn( false );
328
329 // Second call to Title::newFromID, no result
330 $db->expects( $this->at( 2 ) )
331 ->method( 'selectRow' )
332 ->with(
333 'page',
334 $this->anything(),
335 [ 'page_id' => 1 ]
336 )
337 ->willReturn( false );
338
339 // Second select using rev_id, result
340 $db->expects( $this->at( 3 ) )
341 ->method( 'selectRow' )
342 ->with(
343 [ 'revision', 'page' ],
344 $this->anything(),
345 [ 'rev_id' => 2 ]
346 )
347 ->willReturn( (object)[
348 'page_namespace' => '2',
349 'page_title' => 'Foodey',
350 ] );
351
352 $store = $this->getRevisionStore( $mockLoadBalancer );
353 $title = $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
354
355 $this->assertSame( 2, $title->getNamespace() );
356 $this->assertSame( 'Foodey', $title->getDBkey() );
357 }
358
359 /**
360 * @covers \MediaWiki\Revision\RevisionStore::getTitle
361 */
362 public function testGetTitle_correctFallbackAndthrowsExceptionAfterFallbacks() {
363 $mockLoadBalancer = $this->getMockLoadBalancer();
364 // Title calls wfGetDB() so we have to set the main service
365 $this->setService( 'DBLoadBalancer', $mockLoadBalancer );
366
367 $db = $this->getMockDatabase();
368 // Title calls wfGetDB() which uses a regular Connection
369 // Assert that the first call uses a REPLICA and the second falls back to master
370
371 // RevisionStore getTitle uses getConnectionRef
372 // Title::newFromID uses getConnection
373 foreach ( [ 'getConnection', 'getConnectionRef' ] as $method ) {
374 $mockLoadBalancer->expects( $this->exactly( 2 ) )
375 ->method( $method )
376 ->willReturnCallback( function ( $masterOrReplica ) use ( $db ) {
377 static $callCounter = 0;
378 $callCounter++;
379 // The first call should be to a REPLICA, and the second a MASTER.
380 if ( $callCounter === 1 ) {
381 $this->assertSame( DB_REPLICA, $masterOrReplica );
382 } elseif ( $callCounter === 2 ) {
383 $this->assertSame( DB_MASTER, $masterOrReplica );
384 }
385 return $db;
386 } );
387 }
388 // First and third call to Title::newFromID, faking no result
389 foreach ( [ 0, 2 ] as $counter ) {
390 $db->expects( $this->at( $counter ) )
391 ->method( 'selectRow' )
392 ->with(
393 'page',
394 $this->anything(),
395 [ 'page_id' => 1 ]
396 )
397 ->willReturn( false );
398 }
399
400 foreach ( [ 1, 3 ] as $counter ) {
401 $db->expects( $this->at( $counter ) )
402 ->method( 'selectRow' )
403 ->with(
404 [ 'revision', 'page' ],
405 $this->anything(),
406 [ 'rev_id' => 2 ]
407 )
408 ->willReturn( false );
409 }
410
411 $store = $this->getRevisionStore( $mockLoadBalancer );
412
413 $this->setExpectedException( RevisionAccessException::class );
414 $store->getTitle( 1, 2, RevisionStore::READ_NORMAL );
415 }
416
417 public function provideNewRevisionFromRow_legacyEncoding_applied() {
418 yield 'windows-1252, old_flags is empty' => [
419 'windows-1252',
420 'en',
421 [
422 'old_flags' => '',
423 'old_text' => "S\xF6me Content",
424 ],
425 'Söme Content'
426 ];
427
428 yield 'windows-1252, old_flags is null' => [
429 'windows-1252',
430 'en',
431 [
432 'old_flags' => null,
433 'old_text' => "S\xF6me Content",
434 ],
435 'Söme Content'
436 ];
437 }
438
439 /**
440 * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
441 *
442 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
443 */
444 public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
445 if ( !$this->useTextId() ) {
446 $this->markTestSkipped( 'No longer applicable with MCR schema' );
447 }
448
449 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
450 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
451
452 $blobStore = new SqlBlobStore( $lb, $cache );
453 $blobStore->setLegacyEncoding( $encoding, Language::factory( $locale ) );
454
455 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
456
457 $record = $store->newRevisionFromRow(
458 $this->makeRow( $row ),
459 0,
460 Title::newFromText( __METHOD__ . '-UTPage' )
461 );
462
463 $this->assertSame( $text, $record->getContent( SlotRecord::MAIN )->serialize() );
464 }
465
466 /**
467 * @covers \MediaWiki\Revision\RevisionStore::newRevisionFromRow
468 */
469 public function testNewRevisionFromRow_legacyEncoding_ignored() {
470 if ( !$this->useTextId() ) {
471 $this->markTestSkipped( 'No longer applicable with MCR schema' );
472 }
473
474 $row = [
475 'old_flags' => 'utf-8',
476 'old_text' => 'Söme Content',
477 ];
478
479 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
480 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
481
482 $blobStore = new SqlBlobStore( $lb, $cache );
483 $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
484
485 $store = $this->getRevisionStore( $lb, $blobStore, $cache );
486
487 $record = $store->newRevisionFromRow(
488 $this->makeRow( $row ),
489 0,
490 Title::newFromText( __METHOD__ . '-UTPage' )
491 );
492 $this->assertSame( 'Söme Content', $record->getContent( SlotRecord::MAIN )->serialize() );
493 }
494
495 private function makeRow( array $array ) {
496 $row = $array + [
497 'rev_id' => 7,
498 'rev_page' => 5,
499 'rev_timestamp' => '20110101000000',
500 'rev_user_text' => 'Tester',
501 'rev_user' => 17,
502 'rev_minor_edit' => 0,
503 'rev_deleted' => 0,
504 'rev_len' => 100,
505 'rev_parent_id' => 0,
506 'rev_sha1' => 'deadbeef',
507 'rev_comment_text' => 'Testing',
508 'rev_comment_data' => '{}',
509 'rev_comment_cid' => 111,
510 'page_namespace' => 0,
511 'page_title' => 'TEST',
512 'page_id' => 5,
513 'page_latest' => 7,
514 'page_is_redirect' => 0,
515 'page_len' => 100,
516 'user_name' => 'Tester',
517 ];
518
519 if ( $this->useTextId() ) {
520 $row += [
521 'rev_content_format' => CONTENT_FORMAT_TEXT,
522 'rev_content_model' => CONTENT_MODEL_TEXT,
523 'rev_text_id' => 11,
524 'old_id' => 11,
525 'old_text' => 'Hello World',
526 'old_flags' => 'utf-8',
527 ];
528 } else {
529 if ( !isset( $row['content'] ) && isset( $array['old_text'] ) ) {
530 $row['content'] = [
531 'main' => new WikitextContent( $array['old_text'] ),
532 ];
533 }
534 }
535
536 return (object)$row;
537 }
538
539 public function provideMigrationConstruction() {
540 return [
541 [ SCHEMA_COMPAT_OLD, false ],
542 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD, false ],
543 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW, false ],
544 [ SCHEMA_COMPAT_NEW, false ],
545 [ SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_BOTH, true ],
546 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, true ],
547 [ SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_BOTH, true ],
548 ];
549 }
550
551 /**
552 * @covers \MediaWiki\Revision\RevisionStore::__construct
553 * @dataProvider provideMigrationConstruction
554 */
555 public function testMigrationConstruction( $migration, $expectException ) {
556 if ( $expectException ) {
557 $this->setExpectedException( InvalidArgumentException::class );
558 }
559 $loadBalancer = $this->getMockLoadBalancer();
560 $blobStore = $this->getMockSqlBlobStore();
561 $cache = $this->getHashWANObjectCache();
562 $commentStore = $this->getMockCommentStore();
563 $services = MediaWikiServices::getInstance();
564 $nameTables = $services->getNameTableStoreFactory();
565 $contentModelStore = $nameTables->getContentModels();
566 $slotRoleStore = $nameTables->getSlotRoles();
567 $slotRoleRegistry = $services->getSlotRoleRegistry();
568 $store = new RevisionStore(
569 $loadBalancer,
570 $blobStore,
571 $cache,
572 $commentStore,
573 $nameTables->getContentModels(),
574 $nameTables->getSlotRoles(),
575 $slotRoleRegistry,
576 $migration,
577 $services->getActorMigration()
578 );
579 if ( !$expectException ) {
580 $store = TestingAccessWrapper::newFromObject( $store );
581 $this->assertSame( $loadBalancer, $store->loadBalancer );
582 $this->assertSame( $blobStore, $store->blobStore );
583 $this->assertSame( $cache, $store->cache );
584 $this->assertSame( $commentStore, $store->commentStore );
585 $this->assertSame( $contentModelStore, $store->contentModelStore );
586 $this->assertSame( $slotRoleStore, $store->slotRoleStore );
587 $this->assertSame( $migration, $store->mcrMigrationStage );
588 }
589 }
590
591 }