Merge "Schema change for reading ct_tag_id instead of ct_tag"
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / DerivedPageDataUpdaterTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use CommentStoreComment;
6 use Content;
7 use LinksUpdate;
8 use MediaWiki\MediaWikiServices;
9 use MediaWiki\Storage\DerivedPageDataUpdater;
10 use MediaWiki\Storage\MutableRevisionRecord;
11 use MediaWiki\Storage\MutableRevisionSlots;
12 use MediaWiki\Storage\RevisionRecord;
13 use MediaWiki\Storage\RevisionSlotsUpdate;
14 use MediaWiki\Storage\SlotRecord;
15 use MediaWikiTestCase;
16 use Title;
17 use User;
18 use Wikimedia\TestingAccessWrapper;
19 use WikiPage;
20 use WikitextContent;
21
22 /**
23 * @group Database
24 *
25 * @covers \MediaWiki\Storage\DerivedPageDataUpdater
26 */
27 class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
28
29 /**
30 * @param string $title
31 *
32 * @return Title
33 */
34 private function getTitle( $title ) {
35 return Title::makeTitleSafe( $this->getDefaultWikitextNS(), $title );
36 }
37
38 /**
39 * @param string|Title $title
40 *
41 * @return WikiPage
42 */
43 private function getPage( $title ) {
44 $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title );
45
46 return WikiPage::factory( $title );
47 }
48
49 /**
50 * @param string|Title|WikiPage $page
51 *
52 * @return DerivedPageDataUpdater
53 */
54 private function getDerivedPageDataUpdater( $page, RevisionRecord $rec = null ) {
55 if ( is_string( $page ) || $page instanceof Title ) {
56 $page = $this->getPage( $page );
57 }
58
59 $page = TestingAccessWrapper::newFromObject( $page );
60 return $page->getDerivedDataUpdater( null, $rec );
61 }
62
63 /**
64 * Creates a revision in the database.
65 *
66 * @param WikiPage $page
67 * @param $summary
68 * @param null|string|Content $content
69 *
70 * @return RevisionRecord|null
71 */
72 private function createRevision( WikiPage $page, $summary, $content = null ) {
73 $user = $this->getTestUser()->getUser();
74 $comment = CommentStoreComment::newUnsavedComment( $summary );
75
76 if ( $content === null || is_string( $content ) ) {
77 $content = new WikitextContent( $content ?? $summary );
78 }
79
80 if ( !is_array( $content ) ) {
81 $content = [ 'main' => $content ];
82 }
83
84 $this->getDerivedPageDataUpdater( $page ); // flush cached instance before.
85
86 $updater = $page->newPageUpdater( $user );
87
88 foreach ( $content as $role => $c ) {
89 $updater->setContent( $role, $c );
90 }
91
92 $rev = $updater->saveRevision( $comment );
93
94 $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
95 return $rev;
96 }
97
98 // TODO: test setArticleCountMethod() and isCountable();
99 // TODO: test isRedirect() and wasRedirect()
100
101 /**
102 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions()
103 */
104 public function testGetCanonicalParserOptions() {
105 $user = $this->getTestUser()->getUser();
106 $page = $this->getPage( __METHOD__ );
107
108 $parentRev = $this->createRevision( $page, 'first' );
109
110 $mainContent = new WikitextContent( 'Lorem ipsum' );
111
112 $update = new RevisionSlotsUpdate();
113 $update->modifyContent( 'main', $mainContent );
114 $updater = $this->getDerivedPageDataUpdater( $page );
115 $updater->prepareContent( $user, $update, false );
116
117 $options1 = $updater->getCanonicalParserOptions();
118 $this->assertSame( MediaWikiServices::getInstance()->getContentLanguage(),
119 $options1->getUserLangObj() );
120
121 $speculativeId = $options1->getSpeculativeRevId();
122 $this->assertSame( $parentRev->getId() + 1, $speculativeId );
123
124 $rev = $this->makeRevision(
125 $page->getTitle(),
126 $update,
127 $user,
128 $parentRev->getId() + 7,
129 $parentRev->getId()
130 );
131 $updater->prepareUpdate( $rev );
132
133 $options2 = $updater->getCanonicalParserOptions();
134
135 $currentRev = call_user_func( $options2->getCurrentRevisionCallback(), $page->getTitle() );
136 $this->assertSame( $rev->getId(), $currentRev->getId() );
137 }
138
139 /**
140 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision()
141 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
142 */
143 public function testGrabCurrentRevision() {
144 $page = $this->getPage( __METHOD__ );
145
146 $updater0 = $this->getDerivedPageDataUpdater( $page );
147 $this->assertNull( $updater0->grabCurrentRevision() );
148 $this->assertFalse( $updater0->pageExisted() );
149
150 $rev1 = $this->createRevision( $page, 'first' );
151 $updater1 = $this->getDerivedPageDataUpdater( $page );
152 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
153 $this->assertFalse( $updater0->pageExisted() );
154 $this->assertTrue( $updater1->pageExisted() );
155
156 $rev2 = $this->createRevision( $page, 'second' );
157 $updater2 = $this->getDerivedPageDataUpdater( $page );
158 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
159 $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() );
160 }
161
162 /**
163 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
164 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isContentPrepared()
165 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
166 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
167 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
168 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
169 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
170 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
171 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
172 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
173 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
174 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
175 */
176 public function testPrepareContent() {
177 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
178 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
179
180 $this->assertFalse( $updater->isContentPrepared() );
181
182 // TODO: test stash
183 // TODO: MCR: Test multiple slots. Test slot removal.
184 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
185 $auxContent = new WikitextContent( 'inherited ~~~ content' );
186 $auxSlot = SlotRecord::newSaved(
187 10, 7, 'tt:7',
188 SlotRecord::newUnsaved( 'aux', $auxContent )
189 );
190
191 $update = new RevisionSlotsUpdate();
192 $update->modifyContent( 'main', $mainContent );
193 $update->modifySlot( SlotRecord::newInherited( $auxSlot ) );
194 // TODO: MCR: test removing slots!
195
196 $updater->prepareContent( $sysop, $update, false );
197
198 // second be ok to call again with the same params
199 $updater->prepareContent( $sysop, $update, false );
200
201 $this->assertNull( $updater->grabCurrentRevision() );
202 $this->assertTrue( $updater->isContentPrepared() );
203 $this->assertFalse( $updater->isUpdatePrepared() );
204 $this->assertFalse( $updater->pageExisted() );
205 $this->assertTrue( $updater->isCreation() );
206 $this->assertTrue( $updater->isChange() );
207 $this->assertFalse( $updater->isContentDeleted() );
208
209 $this->assertNotNull( $updater->getRevision() );
210 $this->assertNotNull( $updater->getRenderedRevision() );
211
212 $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() );
213 $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) );
214 $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) );
215 $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() );
216 $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() );
217
218 $mainSlot = $updater->getRawSlot( 'main' );
219 $this->assertInstanceOf( SlotRecord::class, $mainSlot );
220 $this->assertNotContains( '~~~', $mainSlot->getContent()->serialize(), 'PST should apply.' );
221 $this->assertContains( $sysop->getName(), $mainSlot->getContent()->serialize() );
222
223 $auxSlot = $updater->getRawSlot( 'aux' );
224 $this->assertInstanceOf( SlotRecord::class, $auxSlot );
225 $this->assertContains( '~~~', $auxSlot->getContent()->serialize(), 'No PST should apply.' );
226
227 $mainOutput = $updater->getCanonicalParserOutput();
228 $this->assertContains( 'first', $mainOutput->getText() );
229 $this->assertContains( '<a ', $mainOutput->getText() );
230 $this->assertNotEmpty( $mainOutput->getLinks() );
231
232 $canonicalOutput = $updater->getCanonicalParserOutput();
233 $this->assertContains( 'first', $canonicalOutput->getText() );
234 $this->assertContains( '<a ', $canonicalOutput->getText() );
235 $this->assertContains( 'inherited ', $canonicalOutput->getText() );
236 $this->assertNotEmpty( $canonicalOutput->getLinks() );
237 }
238
239 /**
240 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
241 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
242 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
243 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
244 */
245 public function testPrepareContentInherit() {
246 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
247 $page = $this->getPage( __METHOD__ );
248
249 $mainContent1 = new WikitextContent( 'first [[main]] ({{REVISIONUSER}}) #~~~#' );
250 $mainContent2 = new WikitextContent( 'second ({{subst:REVISIONUSER}}) #~~~#' );
251
252 $rev = $this->createRevision( $page, 'first', $mainContent1 );
253 $mainContent1 = $rev->getContent( 'main' ); // get post-pst content
254 $userName = $rev->getUser()->getName();
255 $sysopName = $sysop->getName();
256
257 $update = new RevisionSlotsUpdate();
258 $update->modifyContent( 'main', $mainContent1 );
259 $updater1 = $this->getDerivedPageDataUpdater( $page );
260 $updater1->prepareContent( $sysop, $update, false );
261
262 $this->assertNotNull( $updater1->grabCurrentRevision() );
263 $this->assertTrue( $updater1->isContentPrepared() );
264 $this->assertTrue( $updater1->pageExisted() );
265 $this->assertFalse( $updater1->isCreation() );
266 $this->assertFalse( $updater1->isChange() );
267
268 $this->assertNotNull( $updater1->getRevision() );
269 $this->assertNotNull( $updater1->getRenderedRevision() );
270
271 // parser-output for null-edit uses the original author's name
272 $html = $updater1->getRenderedRevision()->getRevisionParserOutput()->getText();
273 $this->assertNotContains( $sysopName, $html, '{{REVISIONUSER}}' );
274 $this->assertNotContains( '{{REVISIONUSER}}', $html, '{{REVISIONUSER}}' );
275 $this->assertNotContains( '~~~', $html, 'signature ~~~' );
276 $this->assertContains( '(' . $userName . ')', $html, '{{REVISIONUSER}}' );
277 $this->assertContains( '>' . $userName . '<', $html, 'signature ~~~' );
278
279 // TODO: MCR: test inheritance from parent
280 $update = new RevisionSlotsUpdate();
281 $update->modifyContent( 'main', $mainContent2 );
282 $updater2 = $this->getDerivedPageDataUpdater( $page );
283 $updater2->prepareContent( $sysop, $update, false );
284
285 // non-null edit use the new user name in PST
286 $pstText = $updater2->getSlots()->getContent( 'main' )->serialize();
287 $this->assertNotContains( '{{subst:REVISIONUSER}}', $pstText, '{{subst:REVISIONUSER}}' );
288 $this->assertNotContains( '~~~', $pstText, 'signature ~~~' );
289 $this->assertContains( '(' . $sysopName . ')', $pstText, '{{subst:REVISIONUSER}}' );
290 $this->assertContains( ':' . $sysopName . '|', $pstText, 'signature ~~~' );
291
292 $this->assertFalse( $updater2->isCreation() );
293 $this->assertTrue( $updater2->isChange() );
294 }
295
296 // TODO: test failure of prepareContent() when called again...
297 // - with different user
298 // - with different update
299 // - after calling prepareUpdate()
300
301 /**
302 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
303 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isUpdatePrepared()
304 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
305 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
306 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
307 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
308 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
309 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
310 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
311 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
312 */
313 public function testPrepareUpdate() {
314 $page = $this->getPage( __METHOD__ );
315
316 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
317 $rev1 = $this->createRevision( $page, 'first', $mainContent1 );
318 $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 );
319
320 $options = []; // TODO: test *all* the options...
321 $updater1->prepareUpdate( $rev1, $options );
322
323 $this->assertTrue( $updater1->isUpdatePrepared() );
324 $this->assertTrue( $updater1->isContentPrepared() );
325 $this->assertTrue( $updater1->isCreation() );
326 $this->assertTrue( $updater1->isChange() );
327 $this->assertFalse( $updater1->isContentDeleted() );
328
329 $this->assertNotNull( $updater1->getRevision() );
330 $this->assertNotNull( $updater1->getRenderedRevision() );
331
332 $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() );
333 $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) );
334 $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) );
335 $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() );
336 $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() );
337
338 // TODO: MCR: test multiple slots, test slot removal!
339
340 $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( 'main' ) );
341 $this->assertNotContains( '~~~~', $updater1->getRawContent( 'main' )->serialize() );
342
343 $mainOutput = $updater1->getCanonicalParserOutput();
344 $this->assertContains( 'first', $mainOutput->getText() );
345 $this->assertContains( '<a ', $mainOutput->getText() );
346 $this->assertNotEmpty( $mainOutput->getLinks() );
347
348 $canonicalOutput = $updater1->getCanonicalParserOutput();
349 $this->assertContains( 'first', $canonicalOutput->getText() );
350 $this->assertContains( '<a ', $canonicalOutput->getText() );
351 $this->assertNotEmpty( $canonicalOutput->getLinks() );
352
353 $mainContent2 = new WikitextContent( 'second' );
354 $rev2 = $this->createRevision( $page, 'second', $mainContent2 );
355 $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 );
356
357 $options = []; // TODO: test *all* the options...
358 $updater2->prepareUpdate( $rev2, $options );
359
360 $this->assertFalse( $updater2->isCreation() );
361 $this->assertTrue( $updater2->isChange() );
362
363 $canonicalOutput = $updater2->getCanonicalParserOutput();
364 $this->assertContains( 'second', $canonicalOutput->getText() );
365 }
366
367 /**
368 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
369 */
370 public function testPrepareUpdateReusesParserOutput() {
371 $user = $this->getTestUser()->getUser();
372 $page = $this->getPage( __METHOD__ );
373
374 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
375
376 $update = new RevisionSlotsUpdate();
377 $update->modifyContent( 'main', $mainContent1 );
378 $updater = $this->getDerivedPageDataUpdater( $page );
379 $updater->prepareContent( $user, $update, false );
380
381 $mainOutput = $updater->getSlotParserOutput( 'main' );
382 $canonicalOutput = $updater->getCanonicalParserOutput();
383
384 $rev = $this->createRevision( $page, 'first', $mainContent1 );
385
386 $options = []; // TODO: test *all* the options...
387 $updater->prepareUpdate( $rev, $options );
388
389 $this->assertTrue( $updater->isUpdatePrepared() );
390 $this->assertTrue( $updater->isContentPrepared() );
391
392 $this->assertSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
393 $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
394 }
395
396 /**
397 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
398 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
399 */
400 public function testPrepareUpdateOutputReset() {
401 $user = $this->getTestUser()->getUser();
402 $page = $this->getPage( __METHOD__ );
403
404 $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' );
405
406 $update = new RevisionSlotsUpdate();
407 $update->modifyContent( 'main', $mainContent1 );
408 $updater = $this->getDerivedPageDataUpdater( $page );
409 $updater->prepareContent( $user, $update, false );
410
411 $mainOutput = $updater->getSlotParserOutput( 'main' );
412 $canonicalOutput = $updater->getCanonicalParserOutput();
413
414 // prevent optimization on matching speculative ID
415 $mainOutput->setSpeculativeRevIdUsed( 0 );
416 $canonicalOutput->setSpeculativeRevIdUsed( 0 );
417
418 $rev = $this->createRevision( $page, 'first', $mainContent1 );
419
420 $options = []; // TODO: test *all* the options...
421 $updater->prepareUpdate( $rev, $options );
422
423 $this->assertTrue( $updater->isUpdatePrepared() );
424 $this->assertTrue( $updater->isContentPrepared() );
425
426 // ParserOutput objects should have been flushed.
427 $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
428 $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
429
430 $html = $updater->getCanonicalParserOutput()->getText();
431 $this->assertContains( '--' . $rev->getId() . '--', $html );
432
433 // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is
434 // updated, the main slot is still re-rendered!
435 }
436
437 // TODO: test failure of prepareUpdate() when called again with a different revision
438 // TODO: test failure of prepareUpdate() on inconsistency with prepareContent.
439
440 /**
441 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
442 */
443 public function testGetPreparedEditAfterPrepareContent() {
444 $user = $this->getTestUser()->getUser();
445
446 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
447 $update = new RevisionSlotsUpdate();
448 $update->modifyContent( 'main', $mainContent );
449
450 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
451 $updater->prepareContent( $user, $update, false );
452
453 $canonicalOutput = $updater->getCanonicalParserOutput();
454
455 $preparedEdit = $updater->getPreparedEdit();
456 $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
457 $this->assertSame( $canonicalOutput, $preparedEdit->output );
458 $this->assertSame( $mainContent, $preparedEdit->newContent );
459 $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
460 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
461 $this->assertSame( null, $preparedEdit->revid );
462 }
463
464 /**
465 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
466 */
467 public function testGetPreparedEditAfterPrepareUpdate() {
468 $page = $this->getPage( __METHOD__ );
469
470 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
471 $update = new MutableRevisionSlots();
472 $update->setContent( 'main', $mainContent );
473
474 $rev = $this->createRevision( $page, __METHOD__ );
475
476 $updater = $this->getDerivedPageDataUpdater( $page );
477 $updater->prepareUpdate( $rev );
478
479 $canonicalOutput = $updater->getCanonicalParserOutput();
480
481 $preparedEdit = $updater->getPreparedEdit();
482 $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
483 $this->assertSame( $canonicalOutput, $preparedEdit->output );
484 $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
485 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
486 $this->assertSame( $rev->getId(), $preparedEdit->revid );
487 }
488
489 public function testGetSecondaryDataUpdatesAfterPrepareContent() {
490 $user = $this->getTestUser()->getUser();
491 $page = $this->getPage( __METHOD__ );
492 $this->createRevision( $page, __METHOD__ );
493
494 $mainContent1 = new WikitextContent( 'first' );
495
496 $update = new RevisionSlotsUpdate();
497 $update->modifyContent( 'main', $mainContent1 );
498 $updater = $this->getDerivedPageDataUpdater( $page );
499 $updater->prepareContent( $user, $update, false );
500
501 $dataUpdates = $updater->getSecondaryDataUpdates();
502
503 // TODO: MCR: assert updates from all slots!
504 $this->assertNotEmpty( $dataUpdates );
505
506 $linksUpdates = array_filter( $dataUpdates, function ( $du ) {
507 return $du instanceof LinksUpdate;
508 } );
509 $this->assertCount( 1, $linksUpdates );
510 }
511
512 /**
513 * Creates a dummy revision object without touching the database.
514 *
515 * @param Title $title
516 * @param RevisionSlotsUpdate $update
517 * @param User $user
518 * @param string $comment
519 * @param int $id
520 * @param int $parentId
521 *
522 * @return MutableRevisionRecord
523 */
524 private function makeRevision(
525 Title $title,
526 RevisionSlotsUpdate $update,
527 User $user,
528 $comment,
529 $id,
530 $parentId = 0
531 ) {
532 $rev = new MutableRevisionRecord( $title );
533
534 $rev->applyUpdate( $update );
535 $rev->setUser( $user );
536 $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) );
537 $rev->setId( $id );
538 $rev->setPageId( $title->getArticleID() );
539 $rev->setParentId( $parentId );
540
541 return $rev;
542 }
543
544 /**
545 * @param int $id
546 * @return Title
547 */
548 private function getMockTitle( $id = 23 ) {
549 $mock = $this->getMockBuilder( Title::class )
550 ->disableOriginalConstructor()
551 ->getMock();
552 $mock->expects( $this->any() )
553 ->method( 'getDBkey' )
554 ->will( $this->returnValue( __CLASS__ ) );
555 $mock->expects( $this->any() )
556 ->method( 'getArticleID' )
557 ->will( $this->returnValue( $id ) );
558
559 return $mock;
560 }
561
562 public function provideIsReusableFor() {
563 $title = $this->getMockTitle();
564
565 $user1 = User::newFromName( 'Alice' );
566 $user2 = User::newFromName( 'Bob' );
567
568 $content1 = new WikitextContent( 'one' );
569 $content2 = new WikitextContent( 'two' );
570
571 $update1 = new RevisionSlotsUpdate();
572 $update1->modifyContent( 'main', $content1 );
573
574 $update1b = new RevisionSlotsUpdate();
575 $update1b->modifyContent( 'xyz', $content1 );
576
577 $update2 = new RevisionSlotsUpdate();
578 $update2->modifyContent( 'main', $content2 );
579
580 $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 );
581 $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 );
582
583 $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 );
584 $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 );
585 $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 );
586
587 yield 'any' => [
588 '$prepUser' => null,
589 '$prepRevision' => null,
590 '$prepUpdate' => null,
591 '$forUser' => null,
592 '$forRevision' => null,
593 '$forUpdate' => null,
594 '$forParent' => null,
595 '$isReusable' => true,
596 ];
597 yield 'for any' => [
598 '$prepUser' => $user1,
599 '$prepRevision' => $rev1,
600 '$prepUpdate' => $update1,
601 '$forUser' => null,
602 '$forRevision' => null,
603 '$forUpdate' => null,
604 '$forParent' => null,
605 '$isReusable' => true,
606 ];
607 yield 'unprepared' => [
608 '$prepUser' => null,
609 '$prepRevision' => null,
610 '$prepUpdate' => null,
611 '$forUser' => $user1,
612 '$forRevision' => $rev1,
613 '$forUpdate' => $update1,
614 '$forParent' => 0,
615 '$isReusable' => true,
616 ];
617 yield 'match prepareContent' => [
618 '$prepUser' => $user1,
619 '$prepRevision' => null,
620 '$prepUpdate' => $update1,
621 '$forUser' => $user1,
622 '$forRevision' => null,
623 '$forUpdate' => $update1,
624 '$forParent' => 0,
625 '$isReusable' => true,
626 ];
627 yield 'match prepareUpdate' => [
628 '$prepUser' => null,
629 '$prepRevision' => $rev1,
630 '$prepUpdate' => null,
631 '$forUser' => $user1,
632 '$forRevision' => $rev1,
633 '$forUpdate' => null,
634 '$forParent' => 0,
635 '$isReusable' => true,
636 ];
637 yield 'match all' => [
638 '$prepUser' => $user1,
639 '$prepRevision' => $rev1,
640 '$prepUpdate' => $update1,
641 '$forUser' => $user1,
642 '$forRevision' => $rev1,
643 '$forUpdate' => $update1,
644 '$forParent' => 0,
645 '$isReusable' => true,
646 ];
647 yield 'mismatch prepareContent update' => [
648 '$prepUser' => $user1,
649 '$prepRevision' => null,
650 '$prepUpdate' => $update1,
651 '$forUser' => $user1,
652 '$forRevision' => null,
653 '$forUpdate' => $update1b,
654 '$forParent' => 0,
655 '$isReusable' => false,
656 ];
657 yield 'mismatch prepareContent user' => [
658 '$prepUser' => $user1,
659 '$prepRevision' => null,
660 '$prepUpdate' => $update1,
661 '$forUser' => $user2,
662 '$forRevision' => null,
663 '$forUpdate' => $update1,
664 '$forParent' => 0,
665 '$isReusable' => false,
666 ];
667 yield 'mismatch prepareContent parent' => [
668 '$prepUser' => $user1,
669 '$prepRevision' => null,
670 '$prepUpdate' => $update1,
671 '$forUser' => $user1,
672 '$forRevision' => null,
673 '$forUpdate' => $update1,
674 '$forParent' => 7,
675 '$isReusable' => false,
676 ];
677 yield 'mismatch prepareUpdate revision update' => [
678 '$prepUser' => null,
679 '$prepRevision' => $rev1,
680 '$prepUpdate' => null,
681 '$forUser' => null,
682 '$forRevision' => $rev1b,
683 '$forUpdate' => null,
684 '$forParent' => 0,
685 '$isReusable' => false,
686 ];
687 yield 'mismatch prepareUpdate revision user' => [
688 '$prepUser' => null,
689 '$prepRevision' => $rev2,
690 '$prepUpdate' => null,
691 '$forUser' => null,
692 '$forRevision' => $rev2x,
693 '$forUpdate' => null,
694 '$forParent' => 0,
695 '$isReusable' => false,
696 ];
697 yield 'mismatch prepareUpdate revision id' => [
698 '$prepUser' => null,
699 '$prepRevision' => $rev2,
700 '$prepUpdate' => null,
701 '$forUser' => null,
702 '$forRevision' => $rev2y,
703 '$forUpdate' => null,
704 '$forParent' => 0,
705 '$isReusable' => false,
706 ];
707 }
708
709 /**
710 * @dataProvider provideIsReusableFor
711 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isReusableFor()
712 *
713 * @param User|null $prepUser
714 * @param RevisionRecord|null $prepRevision
715 * @param RevisionSlotsUpdate|null $prepUpdate
716 * @param User|null $forUser
717 * @param RevisionRecord|null $forRevision
718 * @param RevisionSlotsUpdate|null $forUpdate
719 * @param int|null $forParent
720 * @param bool $isReusable
721 */
722 public function testIsReusableFor(
723 User $prepUser = null,
724 RevisionRecord $prepRevision = null,
725 RevisionSlotsUpdate $prepUpdate = null,
726 User $forUser = null,
727 RevisionRecord $forRevision = null,
728 RevisionSlotsUpdate $forUpdate = null,
729 $forParent = null,
730 $isReusable = null
731 ) {
732 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
733
734 if ( $prepUpdate ) {
735 $updater->prepareContent( $prepUser, $prepUpdate, false );
736 }
737
738 if ( $prepRevision ) {
739 $updater->prepareUpdate( $prepRevision );
740 }
741
742 $this->assertSame(
743 $isReusable,
744 $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent )
745 );
746 }
747
748 /**
749 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
750 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
751 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
752 */
753 public function testDoUpdates() {
754 $page = $this->getPage( __METHOD__ );
755
756 $content = [ 'main' => new WikitextContent( 'first [[main]]' ) ];
757
758 if ( $this->hasMultiSlotSupport() ) {
759 $content['aux'] = new WikitextContent( 'Aux [[Nix]]' );
760 }
761
762 $rev = $this->createRevision( $page, 'first', $content );
763 $pageId = $page->getId();
764
765 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
766 $this->db->delete( 'pagelinks', '*' );
767
768 $pcache = MediaWikiServices::getInstance()->getParserCache();
769 $pcache->deleteOptionsKey( $page );
770
771 $updater = $this->getDerivedPageDataUpdater( $page, $rev );
772 $updater->setArticleCountMethod( 'link' );
773
774 $options = []; // TODO: test *all* the options...
775 $updater->prepareUpdate( $rev, $options );
776
777 $updater->doUpdates();
778
779 // links table update
780 $pageLinks = $this->db->select(
781 'pagelinks',
782 '*',
783 [ 'pl_from' => $pageId ],
784 __METHOD__,
785 [ 'ORDER BY' => 'pl_namespace, pl_title' ]
786 );
787
788 $pageLinksRow = $pageLinks->fetchObject();
789 $this->assertInternalType( 'object', $pageLinksRow );
790 $this->assertSame( 'Main', $pageLinksRow->pl_title );
791
792 if ( $this->hasMultiSlotSupport() ) {
793 $pageLinksRow = $pageLinks->fetchObject();
794 $this->assertInternalType( 'object', $pageLinksRow );
795 $this->assertSame( 'Nix', $pageLinksRow->pl_title );
796 }
797
798 // parser cache update
799 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() );
800 $this->assertInternalType( 'object', $cached );
801 $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
802
803 // site stats
804 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
805 $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
806 $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
807 $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles );
808
809 // TODO: MCR: test data updates for additional slots!
810 // TODO: test update for edit without page creation
811 // TODO: test message cache purge
812 // TODO: test module cache purge
813 // TODO: test CDN purge
814 // TODO: test newtalk update
815 // TODO: test search update
816 // TODO: test site stats good_articles while turning the page into (or back from) a redir.
817 // TODO: test category membership update (with setRcWatchCategoryMembership())
818 }
819
820 private function hasMultiSlotSupport() {
821 global $wgMultiContentRevisionSchemaMigrationStage;
822
823 return ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW )
824 && ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW );
825 }
826
827 }