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