Merge "FauxRequest: don’t override getValues()"
[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\Revision\MutableRevisionRecord;
11 use MediaWiki\Revision\MutableRevisionSlots;
12 use MediaWiki\Revision\RevisionRecord;
13 use MediaWiki\Revision\SlotRecord;
14 use MediaWiki\Storage\DerivedPageDataUpdater;
15 use MediaWiki\Storage\RevisionSlotsUpdate;
16 use MediaWikiTestCase;
17 use MWCallableUpdate;
18 use MWTimestamp;
19 use PHPUnit\Framework\MockObject\MockObject;
20 use TextContent;
21 use TextContentHandler;
22 use Title;
23 use User;
24 use Wikimedia\TestingAccessWrapper;
25 use WikiPage;
26 use WikitextContent;
27 use DeferredUpdates;
28
29 /**
30 * @group Database
31 *
32 * @covers \MediaWiki\Storage\DerivedPageDataUpdater
33 */
34 class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
35
36 public function tearDown() {
37 MWTimestamp::setFakeTime( false );
38
39 parent::tearDown();
40 }
41
42 /**
43 * @param string $title
44 *
45 * @return Title
46 */
47 private function getTitle( $title ) {
48 return Title::makeTitleSafe( $this->getDefaultWikitextNS(), $title );
49 }
50
51 /**
52 * @param string|Title $title
53 *
54 * @return WikiPage
55 */
56 private function getPage( $title ) {
57 $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title );
58
59 return WikiPage::factory( $title );
60 }
61
62 /**
63 * @param string|Title|WikiPage $page
64 * @param RevisionRecord|null $rec
65 * @param User|null $user
66 *
67 * @return DerivedPageDataUpdater
68 */
69 private function getDerivedPageDataUpdater(
70 $page, RevisionRecord $rec = null, User $user = null
71 ) {
72 if ( is_string( $page ) || $page instanceof Title ) {
73 $page = $this->getPage( $page );
74 }
75
76 $page = TestingAccessWrapper::newFromObject( $page );
77 return $page->getDerivedDataUpdater( $user, $rec );
78 }
79
80 /**
81 * Creates a revision in the database.
82 *
83 * @param WikiPage $page
84 * @param string|Message|CommentStoreComment $summary
85 * @param null|string|Content $content
86 * @param User|null $user
87 *
88 * @return RevisionRecord|null
89 */
90 private function createRevision( WikiPage $page, $summary, $content = null, $user = null ) {
91 $user = $user ?: $this->getTestUser()->getUser();
92 $comment = CommentStoreComment::newUnsavedComment( $summary );
93
94 if ( $content === null || is_string( $content ) ) {
95 $content = new WikitextContent( $content ?? $summary );
96 }
97
98 if ( !is_array( $content ) ) {
99 $content = [ 'main' => $content ];
100 }
101
102 $this->getDerivedPageDataUpdater( $page ); // flush cached instance before.
103
104 $updater = $page->newPageUpdater( $user );
105
106 foreach ( $content as $role => $c ) {
107 $updater->setContent( $role, $c );
108 }
109
110 $rev = $updater->saveRevision( $comment );
111 if ( !$updater->wasSuccessful() ) {
112 $this->fail( $updater->getStatus()->getWikiText() );
113 }
114
115 $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
116 return $rev;
117 }
118
119 // TODO: test setArticleCountMethod() and isCountable();
120 // TODO: test isRedirect() and wasRedirect()
121
122 /**
123 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions()
124 */
125 public function testGetCanonicalParserOptions() {
126 $user = $this->getTestUser()->getUser();
127 $page = $this->getPage( __METHOD__ );
128
129 $parentRev = $this->createRevision( $page, 'first' );
130
131 $mainContent = new WikitextContent( 'Lorem ipsum' );
132
133 $update = new RevisionSlotsUpdate();
134 $update->modifyContent( SlotRecord::MAIN, $mainContent );
135 $updater = $this->getDerivedPageDataUpdater( $page );
136 $updater->prepareContent( $user, $update, false );
137
138 $options1 = $updater->getCanonicalParserOptions();
139 $this->assertSame( MediaWikiServices::getInstance()->getContentLanguage(),
140 $options1->getUserLangObj() );
141
142 $speculativeId = $options1->getSpeculativeRevId();
143 $this->assertSame( $parentRev->getId() + 1, $speculativeId );
144
145 $rev = $this->makeRevision(
146 $page->getTitle(),
147 $update,
148 $user,
149 $parentRev->getId() + 7,
150 $parentRev->getId()
151 );
152 $updater->prepareUpdate( $rev );
153
154 $options2 = $updater->getCanonicalParserOptions();
155
156 $currentRev = call_user_func( $options2->getCurrentRevisionCallback(), $page->getTitle() );
157 $this->assertSame( $rev->getId(), $currentRev->getId() );
158 }
159
160 /**
161 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision()
162 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
163 */
164 public function testGrabCurrentRevision() {
165 $page = $this->getPage( __METHOD__ );
166
167 $updater0 = $this->getDerivedPageDataUpdater( $page );
168 $this->assertNull( $updater0->grabCurrentRevision() );
169 $this->assertFalse( $updater0->pageExisted() );
170
171 $rev1 = $this->createRevision( $page, 'first' );
172 $updater1 = $this->getDerivedPageDataUpdater( $page );
173 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
174 $this->assertFalse( $updater0->pageExisted() );
175 $this->assertTrue( $updater1->pageExisted() );
176
177 $rev2 = $this->createRevision( $page, 'second' );
178 $updater2 = $this->getDerivedPageDataUpdater( $page );
179 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
180 $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() );
181 }
182
183 /**
184 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
185 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isContentPrepared()
186 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
187 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
188 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
189 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
190 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
191 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
192 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
193 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
194 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
195 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
196 */
197 public function testPrepareContent() {
198 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
199 'aux',
200 CONTENT_MODEL_WIKITEXT
201 );
202
203 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
204 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
205
206 $this->assertFalse( $updater->isContentPrepared() );
207
208 // TODO: test stash
209 // TODO: MCR: Test multiple slots. Test slot removal.
210 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
211 $auxContent = new WikitextContent( 'inherited ~~~ content' );
212 $auxSlot = SlotRecord::newSaved(
213 10, 7, 'tt:7',
214 SlotRecord::newUnsaved( 'aux', $auxContent )
215 );
216
217 $update = new RevisionSlotsUpdate();
218 $update->modifyContent( SlotRecord::MAIN, $mainContent );
219 $update->modifySlot( SlotRecord::newInherited( $auxSlot ) );
220 // TODO: MCR: test removing slots!
221
222 $updater->prepareContent( $sysop, $update, false );
223
224 // second be ok to call again with the same params
225 $updater->prepareContent( $sysop, $update, false );
226
227 $this->assertNull( $updater->grabCurrentRevision() );
228 $this->assertTrue( $updater->isContentPrepared() );
229 $this->assertFalse( $updater->isUpdatePrepared() );
230 $this->assertFalse( $updater->pageExisted() );
231 $this->assertTrue( $updater->isCreation() );
232 $this->assertTrue( $updater->isChange() );
233 $this->assertFalse( $updater->isContentDeleted() );
234
235 $this->assertNotNull( $updater->getRevision() );
236 $this->assertNotNull( $updater->getRenderedRevision() );
237
238 $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() );
239 $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) );
240 $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) );
241 $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() );
242 $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() );
243
244 $mainSlot = $updater->getRawSlot( SlotRecord::MAIN );
245 $this->assertInstanceOf( SlotRecord::class, $mainSlot );
246 $this->assertNotContains( '~~~', $mainSlot->getContent()->serialize(), 'PST should apply.' );
247 $this->assertContains( $sysop->getName(), $mainSlot->getContent()->serialize() );
248
249 $auxSlot = $updater->getRawSlot( 'aux' );
250 $this->assertInstanceOf( SlotRecord::class, $auxSlot );
251 $this->assertContains( '~~~', $auxSlot->getContent()->serialize(), 'No PST should apply.' );
252
253 $mainOutput = $updater->getCanonicalParserOutput();
254 $this->assertContains( 'first', $mainOutput->getText() );
255 $this->assertContains( '<a ', $mainOutput->getText() );
256 $this->assertNotEmpty( $mainOutput->getLinks() );
257
258 $canonicalOutput = $updater->getCanonicalParserOutput();
259 $this->assertContains( 'first', $canonicalOutput->getText() );
260 $this->assertContains( '<a ', $canonicalOutput->getText() );
261 $this->assertContains( 'inherited ', $canonicalOutput->getText() );
262 $this->assertNotEmpty( $canonicalOutput->getLinks() );
263 }
264
265 /**
266 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
267 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
268 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
269 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
270 */
271 public function testPrepareContentInherit() {
272 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
273 $page = $this->getPage( __METHOD__ );
274
275 $mainContent1 = new WikitextContent( 'first [[main]] ({{REVISIONUSER}}) #~~~#' );
276 $mainContent2 = new WikitextContent( 'second ({{subst:REVISIONUSER}}) #~~~#' );
277
278 $rev = $this->createRevision( $page, 'first', $mainContent1 );
279 $mainContent1 = $rev->getContent( SlotRecord::MAIN ); // get post-pst content
280 $userName = $rev->getUser()->getName();
281 $sysopName = $sysop->getName();
282
283 $update = new RevisionSlotsUpdate();
284 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
285 $updater1 = $this->getDerivedPageDataUpdater( $page );
286 $updater1->prepareContent( $sysop, $update, false );
287
288 $this->assertNotNull( $updater1->grabCurrentRevision() );
289 $this->assertTrue( $updater1->isContentPrepared() );
290 $this->assertTrue( $updater1->pageExisted() );
291 $this->assertFalse( $updater1->isCreation() );
292 $this->assertFalse( $updater1->isChange() );
293
294 $this->assertNotNull( $updater1->getRevision() );
295 $this->assertNotNull( $updater1->getRenderedRevision() );
296
297 // parser-output for null-edit uses the original author's name
298 $html = $updater1->getRenderedRevision()->getRevisionParserOutput()->getText();
299 $this->assertNotContains( $sysopName, $html, '{{REVISIONUSER}}' );
300 $this->assertNotContains( '{{REVISIONUSER}}', $html, '{{REVISIONUSER}}' );
301 $this->assertNotContains( '~~~', $html, 'signature ~~~' );
302 $this->assertContains( '(' . $userName . ')', $html, '{{REVISIONUSER}}' );
303 $this->assertContains( '>' . $userName . '<', $html, 'signature ~~~' );
304
305 // TODO: MCR: test inheritance from parent
306 $update = new RevisionSlotsUpdate();
307 $update->modifyContent( SlotRecord::MAIN, $mainContent2 );
308 $updater2 = $this->getDerivedPageDataUpdater( $page );
309 $updater2->prepareContent( $sysop, $update, false );
310
311 // non-null edit use the new user name in PST
312 $pstText = $updater2->getSlots()->getContent( SlotRecord::MAIN )->serialize();
313 $this->assertNotContains( '{{subst:REVISIONUSER}}', $pstText, '{{subst:REVISIONUSER}}' );
314 $this->assertNotContains( '~~~', $pstText, 'signature ~~~' );
315 $this->assertContains( '(' . $sysopName . ')', $pstText, '{{subst:REVISIONUSER}}' );
316 $this->assertContains( ':' . $sysopName . '|', $pstText, 'signature ~~~' );
317
318 $this->assertFalse( $updater2->isCreation() );
319 $this->assertTrue( $updater2->isChange() );
320 }
321
322 // TODO: test failure of prepareContent() when called again...
323 // - with different user
324 // - with different update
325 // - after calling prepareUpdate()
326
327 /**
328 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
329 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isUpdatePrepared()
330 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
331 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
332 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
333 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
334 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
335 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
336 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
337 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
338 */
339 public function testPrepareUpdate() {
340 $page = $this->getPage( __METHOD__ );
341
342 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
343 $rev1 = $this->createRevision( $page, 'first', $mainContent1 );
344 $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 );
345
346 $options = []; // TODO: test *all* the options...
347 $updater1->prepareUpdate( $rev1, $options );
348
349 $this->assertTrue( $updater1->isUpdatePrepared() );
350 $this->assertTrue( $updater1->isContentPrepared() );
351 $this->assertTrue( $updater1->isCreation() );
352 $this->assertTrue( $updater1->isChange() );
353 $this->assertFalse( $updater1->isContentDeleted() );
354
355 $this->assertNotNull( $updater1->getRevision() );
356 $this->assertNotNull( $updater1->getRenderedRevision() );
357
358 $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() );
359 $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) );
360 $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) );
361 $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() );
362 $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() );
363
364 // TODO: MCR: test multiple slots, test slot removal!
365
366 $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( SlotRecord::MAIN ) );
367 $this->assertNotContains( '~~~~', $updater1->getRawContent( SlotRecord::MAIN )->serialize() );
368
369 $mainOutput = $updater1->getCanonicalParserOutput();
370 $this->assertContains( 'first', $mainOutput->getText() );
371 $this->assertContains( '<a ', $mainOutput->getText() );
372 $this->assertNotEmpty( $mainOutput->getLinks() );
373
374 $canonicalOutput = $updater1->getCanonicalParserOutput();
375 $this->assertContains( 'first', $canonicalOutput->getText() );
376 $this->assertContains( '<a ', $canonicalOutput->getText() );
377 $this->assertNotEmpty( $canonicalOutput->getLinks() );
378
379 $mainContent2 = new WikitextContent( 'second' );
380 $rev2 = $this->createRevision( $page, 'second', $mainContent2 );
381 $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 );
382
383 $options = []; // TODO: test *all* the options...
384 $updater2->prepareUpdate( $rev2, $options );
385
386 $this->assertFalse( $updater2->isCreation() );
387 $this->assertTrue( $updater2->isChange() );
388
389 $canonicalOutput = $updater2->getCanonicalParserOutput();
390 $this->assertContains( 'second', $canonicalOutput->getText() );
391 }
392
393 /**
394 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
395 */
396 public function testPrepareUpdateReusesParserOutput() {
397 $user = $this->getTestUser()->getUser();
398 $page = $this->getPage( __METHOD__ );
399
400 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
401
402 $update = new RevisionSlotsUpdate();
403 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
404 $updater = $this->getDerivedPageDataUpdater( $page );
405 $updater->prepareContent( $user, $update, false );
406
407 $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN );
408 $canonicalOutput = $updater->getCanonicalParserOutput();
409
410 $rev = $this->createRevision( $page, 'first', $mainContent1 );
411
412 $options = []; // TODO: test *all* the options...
413 $updater->prepareUpdate( $rev, $options );
414
415 $this->assertTrue( $updater->isUpdatePrepared() );
416 $this->assertTrue( $updater->isContentPrepared() );
417
418 $this->assertSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) );
419 $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
420 }
421
422 /**
423 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
424 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
425 */
426 public function testPrepareUpdateOutputReset() {
427 $user = $this->getTestUser()->getUser();
428 $page = $this->getPage( __METHOD__ );
429
430 $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' );
431
432 $update = new RevisionSlotsUpdate();
433 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
434 $updater = $this->getDerivedPageDataUpdater( $page );
435 $updater->prepareContent( $user, $update, false );
436
437 $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN );
438 $canonicalOutput = $updater->getCanonicalParserOutput();
439
440 // prevent optimization on matching speculative ID
441 $mainOutput->setSpeculativeRevIdUsed( 0 );
442 $canonicalOutput->setSpeculativeRevIdUsed( 0 );
443
444 $rev = $this->createRevision( $page, 'first', $mainContent1 );
445
446 $options = []; // TODO: test *all* the options...
447 $updater->prepareUpdate( $rev, $options );
448
449 $this->assertTrue( $updater->isUpdatePrepared() );
450 $this->assertTrue( $updater->isContentPrepared() );
451
452 // ParserOutput objects should have been flushed.
453 $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) );
454 $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
455
456 $html = $updater->getCanonicalParserOutput()->getText();
457 $this->assertContains( '--' . $rev->getId() . '--', $html );
458
459 // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is
460 // updated, the main slot is still re-rendered!
461 }
462
463 // TODO: test failure of prepareUpdate() when called again with a different revision
464 // TODO: test failure of prepareUpdate() on inconsistency with prepareContent.
465
466 /**
467 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
468 */
469 public function testGetPreparedEditAfterPrepareContent() {
470 $user = $this->getTestUser()->getUser();
471
472 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
473 $update = new RevisionSlotsUpdate();
474 $update->modifyContent( SlotRecord::MAIN, $mainContent );
475
476 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
477 $updater->prepareContent( $user, $update, false );
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( $mainContent, $preparedEdit->newContent );
485 $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent );
486 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
487 $this->assertSame( null, $preparedEdit->revid );
488 }
489
490 /**
491 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
492 */
493 public function testGetPreparedEditAfterPrepareUpdate() {
494 $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
495 MWTimestamp::setFakeTime( function () use ( &$clock ) {
496 return $clock++;
497 } );
498
499 $page = $this->getPage( __METHOD__ );
500
501 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
502 $update = new MutableRevisionSlots();
503 $update->setContent( SlotRecord::MAIN, $mainContent );
504
505 $rev = $this->createRevision( $page, __METHOD__ );
506
507 $updater = $this->getDerivedPageDataUpdater( $page );
508 $updater->prepareUpdate( $rev );
509
510 $canonicalOutput = $updater->getCanonicalParserOutput();
511
512 $preparedEdit = $updater->getPreparedEdit();
513 $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
514 $this->assertSame( $canonicalOutput, $preparedEdit->output );
515 $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent );
516 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
517 $this->assertSame( $rev->getId(), $preparedEdit->revid );
518 }
519
520 public function testGetSecondaryDataUpdatesAfterPrepareContent() {
521 $user = $this->getTestUser()->getUser();
522 $page = $this->getPage( __METHOD__ );
523 $this->createRevision( $page, __METHOD__ );
524
525 $mainContent1 = new WikitextContent( 'first' );
526
527 $update = new RevisionSlotsUpdate();
528 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
529 $updater = $this->getDerivedPageDataUpdater( $page );
530 $updater->prepareContent( $user, $update, false );
531
532 $dataUpdates = $updater->getSecondaryDataUpdates();
533
534 $this->assertNotEmpty( $dataUpdates );
535
536 $linksUpdates = array_filter( $dataUpdates, function ( $du ) {
537 return $du instanceof LinksUpdate;
538 } );
539 $this->assertCount( 1, $linksUpdates );
540 }
541
542 /**
543 * @param string $name
544 *
545 * @return ContentHandler
546 */
547 private function defineMockContentModelForUpdateTesting( $name ) {
548 /** @var ContentHandler|MockObject $handler */
549 $handler = $this->getMockBuilder( TextContentHandler::class )
550 ->setConstructorArgs( [ $name ] )
551 ->setMethods(
552 [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ]
553 )
554 ->getMock();
555
556 $dataUpdate = new MWCallableUpdate( 'time' );
557 $dataUpdate->_name = "$name data update";
558
559 $deletionUpdate = new MWCallableUpdate( 'time' );
560 $deletionUpdate->_name = "$name deletion update";
561
562 $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
563 $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
564 $handler->method( 'unserializeContent' )->willReturnCallback(
565 function ( $text ) use ( $handler ) {
566 return $this->createMockContent( $handler, $text );
567 }
568 );
569
570 $this->mergeMwGlobalArrayValue(
571 'wgContentHandlers', [
572 $name => function () use ( $handler ){
573 return $handler;
574 }
575 ]
576 );
577
578 return $handler;
579 }
580
581 /**
582 * @param ContentHandler $handler
583 * @param string $text
584 *
585 * @return Content
586 */
587 private function createMockContent( ContentHandler $handler, $text ) {
588 /** @var Content|MockObject $content */
589 $content = $this->getMockBuilder( TextContent::class )
590 ->setConstructorArgs( [ $text ] )
591 ->setMethods( [ 'getModel', 'getContentHandler' ] )
592 ->getMock();
593
594 $content->method( 'getModel' )->willReturn( $handler->getModelID() );
595 $content->method( 'getContentHandler' )->willReturn( $handler );
596
597 return $content;
598 }
599
600 public function testGetSecondaryDataUpdatesWithSlotRemoval() {
601 if ( !$this->hasMultiSlotSupport() ) {
602 $this->markTestSkipped( 'Slot removal cannot happen with MCR being enabled' );
603 }
604
605 $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
606 $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' );
607 $m2 = $this->defineMockContentModelForUpdateTesting( 'M2' );
608
609 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
610 'aux',
611 $a1->getModelID()
612 );
613
614 $mainContent1 = $this->createMockContent( $m1, 'main 1' );
615 $auxContent1 = $this->createMockContent( $a1, 'aux 1' );
616 $mainContent2 = $this->createMockContent( $m2, 'main 2' );
617
618 $user = $this->getTestUser()->getUser();
619 $page = $this->getPage( __METHOD__ );
620 $this->createRevision(
621 $page,
622 __METHOD__,
623 [ 'main' => $mainContent1, 'aux' => $auxContent1 ]
624 );
625
626 $update = new RevisionSlotsUpdate();
627 $update->modifyContent( SlotRecord::MAIN, $mainContent2 );
628 $update->removeSlot( 'aux' );
629
630 $page = $this->getPage( __METHOD__ );
631 $updater = $this->getDerivedPageDataUpdater( $page );
632 $updater->prepareContent( $user, $update, false );
633
634 $dataUpdates = $updater->getSecondaryDataUpdates();
635
636 $this->assertNotEmpty( $dataUpdates );
637
638 $updateNames = array_map( function ( $du ) {
639 return isset( $du->_name ) ? $du->_name : get_class( $du );
640 }, $dataUpdates );
641
642 $this->assertContains( LinksUpdate::class, $updateNames );
643 $this->assertContains( 'A1 deletion update', $updateNames );
644 $this->assertContains( 'M2 data update', $updateNames );
645 $this->assertNotContains( 'M1 data update', $updateNames );
646 }
647
648 /**
649 * Creates a dummy revision object without touching the database.
650 *
651 * @param Title $title
652 * @param RevisionSlotsUpdate $update
653 * @param User $user
654 * @param string $comment
655 * @param int $id
656 * @param int $parentId
657 *
658 * @return MutableRevisionRecord
659 */
660 private function makeRevision(
661 Title $title,
662 RevisionSlotsUpdate $update,
663 User $user,
664 $comment,
665 $id = 0,
666 $parentId = 0
667 ) {
668 $rev = new MutableRevisionRecord( $title );
669
670 $rev->applyUpdate( $update );
671 $rev->setUser( $user );
672 $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) );
673 $rev->setPageId( $title->getArticleID() );
674 $rev->setParentId( $parentId );
675
676 if ( $id ) {
677 $rev->setId( $id );
678 }
679
680 return $rev;
681 }
682
683 /**
684 * @param int $id
685 * @return Title
686 */
687 private function getMockTitle( $id = 23 ) {
688 $mock = $this->getMockBuilder( Title::class )
689 ->disableOriginalConstructor()
690 ->getMock();
691 $mock->expects( $this->any() )
692 ->method( 'getDBkey' )
693 ->will( $this->returnValue( __CLASS__ ) );
694 $mock->expects( $this->any() )
695 ->method( 'getArticleID' )
696 ->will( $this->returnValue( $id ) );
697
698 return $mock;
699 }
700
701 public function provideIsReusableFor() {
702 $title = $this->getMockTitle();
703
704 $user1 = User::newFromName( 'Alice' );
705 $user2 = User::newFromName( 'Bob' );
706
707 $content1 = new WikitextContent( 'one' );
708 $content2 = new WikitextContent( 'two' );
709
710 $update1 = new RevisionSlotsUpdate();
711 $update1->modifyContent( SlotRecord::MAIN, $content1 );
712
713 $update1b = new RevisionSlotsUpdate();
714 $update1b->modifyContent( 'xyz', $content1 );
715
716 $update2 = new RevisionSlotsUpdate();
717 $update2->modifyContent( SlotRecord::MAIN, $content2 );
718
719 $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 );
720 $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 );
721
722 $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 );
723 $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 );
724 $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 );
725
726 yield 'any' => [
727 '$prepUser' => null,
728 '$prepRevision' => null,
729 '$prepUpdate' => null,
730 '$forUser' => null,
731 '$forRevision' => null,
732 '$forUpdate' => null,
733 '$forParent' => null,
734 '$isReusable' => true,
735 ];
736 yield 'for any' => [
737 '$prepUser' => $user1,
738 '$prepRevision' => $rev1,
739 '$prepUpdate' => $update1,
740 '$forUser' => null,
741 '$forRevision' => null,
742 '$forUpdate' => null,
743 '$forParent' => null,
744 '$isReusable' => true,
745 ];
746 yield 'unprepared' => [
747 '$prepUser' => null,
748 '$prepRevision' => null,
749 '$prepUpdate' => null,
750 '$forUser' => $user1,
751 '$forRevision' => $rev1,
752 '$forUpdate' => $update1,
753 '$forParent' => 0,
754 '$isReusable' => true,
755 ];
756 yield 'match prepareContent' => [
757 '$prepUser' => $user1,
758 '$prepRevision' => null,
759 '$prepUpdate' => $update1,
760 '$forUser' => $user1,
761 '$forRevision' => null,
762 '$forUpdate' => $update1,
763 '$forParent' => 0,
764 '$isReusable' => true,
765 ];
766 yield 'match prepareUpdate' => [
767 '$prepUser' => null,
768 '$prepRevision' => $rev1,
769 '$prepUpdate' => null,
770 '$forUser' => $user1,
771 '$forRevision' => $rev1,
772 '$forUpdate' => null,
773 '$forParent' => 0,
774 '$isReusable' => true,
775 ];
776 yield 'match all' => [
777 '$prepUser' => $user1,
778 '$prepRevision' => $rev1,
779 '$prepUpdate' => $update1,
780 '$forUser' => $user1,
781 '$forRevision' => $rev1,
782 '$forUpdate' => $update1,
783 '$forParent' => 0,
784 '$isReusable' => true,
785 ];
786 yield 'mismatch prepareContent update' => [
787 '$prepUser' => $user1,
788 '$prepRevision' => null,
789 '$prepUpdate' => $update1,
790 '$forUser' => $user1,
791 '$forRevision' => null,
792 '$forUpdate' => $update1b,
793 '$forParent' => 0,
794 '$isReusable' => false,
795 ];
796 yield 'mismatch prepareContent user' => [
797 '$prepUser' => $user1,
798 '$prepRevision' => null,
799 '$prepUpdate' => $update1,
800 '$forUser' => $user2,
801 '$forRevision' => null,
802 '$forUpdate' => $update1,
803 '$forParent' => 0,
804 '$isReusable' => false,
805 ];
806 yield 'mismatch prepareContent parent' => [
807 '$prepUser' => $user1,
808 '$prepRevision' => null,
809 '$prepUpdate' => $update1,
810 '$forUser' => $user1,
811 '$forRevision' => null,
812 '$forUpdate' => $update1,
813 '$forParent' => 7,
814 '$isReusable' => false,
815 ];
816 yield 'mismatch prepareUpdate revision update' => [
817 '$prepUser' => null,
818 '$prepRevision' => $rev1,
819 '$prepUpdate' => null,
820 '$forUser' => null,
821 '$forRevision' => $rev1b,
822 '$forUpdate' => null,
823 '$forParent' => 0,
824 '$isReusable' => false,
825 ];
826 yield 'mismatch prepareUpdate revision id' => [
827 '$prepUser' => null,
828 '$prepRevision' => $rev2,
829 '$prepUpdate' => null,
830 '$forUser' => null,
831 '$forRevision' => $rev2y,
832 '$forUpdate' => null,
833 '$forParent' => 0,
834 '$isReusable' => false,
835 ];
836 }
837
838 /**
839 * @dataProvider provideIsReusableFor
840 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isReusableFor()
841 *
842 * @param User|null $prepUser
843 * @param RevisionRecord|null $prepRevision
844 * @param RevisionSlotsUpdate|null $prepUpdate
845 * @param User|null $forUser
846 * @param RevisionRecord|null $forRevision
847 * @param RevisionSlotsUpdate|null $forUpdate
848 * @param int|null $forParent
849 * @param bool $isReusable
850 */
851 public function testIsReusableFor(
852 User $prepUser = null,
853 RevisionRecord $prepRevision = null,
854 RevisionSlotsUpdate $prepUpdate = null,
855 User $forUser = null,
856 RevisionRecord $forRevision = null,
857 RevisionSlotsUpdate $forUpdate = null,
858 $forParent = null,
859 $isReusable = null
860 ) {
861 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
862
863 if ( $prepUpdate ) {
864 $updater->prepareContent( $prepUser, $prepUpdate, false );
865 }
866
867 if ( $prepRevision ) {
868 $updater->prepareUpdate( $prepRevision );
869 }
870
871 $this->assertSame(
872 $isReusable,
873 $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent )
874 );
875 }
876
877 /**
878 * * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCountable
879 */
880 public function testIsCountableNotContentPage() {
881 $updater = $this->getDerivedPageDataUpdater(
882 Title::newFromText( 'Main_Page', NS_TALK )
883 );
884 self::assertFalse( $updater->isCountable() );
885 }
886
887 public function provideIsCountable() {
888 yield 'deleted revision' => [
889 '$articleCountMethod' => 'any',
890 '$wikitextContent' => 'Test',
891 '$revisionVisibility' => RevisionRecord::SUPPRESSED_ALL,
892 '$isCountable' => false
893 ];
894 yield 'redirect' => [
895 '$articleCountMethod' => 'any',
896 '$wikitextContent' => '#REDIRECT [[Main_Page]]',
897 '$revisionVisibility' => 0,
898 '$isCountable' => false
899 ];
900 yield 'no links count method any' => [
901 '$articleCountMethod' => 'any',
902 '$wikitextContent' => 'Test',
903 '$revisionVisibility' => 0,
904 '$isCountable' => true
905 ];
906 yield 'no links count method link' => [
907 '$articleCountMethod' => 'link',
908 '$wikitextContent' => 'Test',
909 '$revisionVisibility' => 0,
910 '$isCountable' => false
911 ];
912 yield 'with links count method link' => [
913 '$articleCountMethod' => 'link',
914 '$wikitextContent' => '[[Test]]',
915 '$revisionVisibility' => 0,
916 '$isCountable' => true
917 ];
918 }
919
920 /**
921 * @dataProvider provideIsCountable
922 *
923 * @param string $articleCountMethod
924 * @param string $wikitextContent
925 * @param int $revisionVisibility
926 * @param bool $isCountable
927 * @throws \MWException
928 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCountable
929 */
930 public function testIsCountable(
931 $articleCountMethod,
932 $wikitextContent,
933 $revisionVisibility,
934 $isCountable
935 ) {
936 $this->setMwGlobals( [ 'wgArticleCountMethod' => $articleCountMethod ] );
937 $title = $this->getTitle( 'Main_Page' );
938 $content = new WikitextContent( $wikitextContent );
939 $update = new RevisionSlotsUpdate();
940 $update->modifyContent( SlotRecord::MAIN, $content );
941 $revision = $this->makeRevision( $title, $update, User::newFromName( 'Alice' ), 'rev1', 13 );
942 $revision->setVisibility( $revisionVisibility );
943 $updater = $this->getDerivedPageDataUpdater( $title );
944 $updater->prepareUpdate( $revision );
945 self::assertSame( $isCountable, $updater->isCountable() );
946 }
947
948 /**
949 * @throws \MWException
950 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCountable
951 */
952 public function testIsCountableNoModifiedSlots() {
953 $page = $this->getPage( __METHOD__ );
954 $content = [ 'main' => new WikitextContent( '[[Test]]' ) ];
955 $rev = $this->createRevision( $page, 'first', $content );
956 $nullRevision = MutableRevisionRecord::newFromParentRevision( $rev );
957 $nullRevision->setId( 14 );
958 $updater = $this->getDerivedPageDataUpdater( $page, $nullRevision );
959 $updater->prepareUpdate( $nullRevision );
960 $this->assertTrue( $updater->isCountable() );
961 }
962
963 /**
964 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
965 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
966 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
967 */
968 public function testDoUpdates() {
969 $page = $this->getPage( __METHOD__ );
970
971 $content = [ 'main' => new WikitextContent( 'first [[main]]' ) ];
972
973 if ( $this->hasMultiSlotSupport() ) {
974 $content['aux'] = new WikitextContent( 'Aux [[Nix]]' );
975
976 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
977 'aux',
978 CONTENT_MODEL_WIKITEXT
979 );
980 }
981
982 $rev = $this->createRevision( $page, 'first', $content );
983 $pageId = $page->getId();
984
985 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
986 $this->db->delete( 'pagelinks', '*' );
987
988 $pcache = MediaWikiServices::getInstance()->getParserCache();
989 $pcache->deleteOptionsKey( $page );
990
991 $updater = $this->getDerivedPageDataUpdater( $page, $rev );
992 $updater->setArticleCountMethod( 'link' );
993
994 $options = []; // TODO: test *all* the options...
995 $updater->prepareUpdate( $rev, $options );
996
997 $updater->doUpdates();
998
999 // links table update
1000 $pageLinks = $this->db->select(
1001 'pagelinks',
1002 '*',
1003 [ 'pl_from' => $pageId ],
1004 __METHOD__,
1005 [ 'ORDER BY' => 'pl_namespace, pl_title' ]
1006 );
1007
1008 $pageLinksRow = $pageLinks->fetchObject();
1009 $this->assertInternalType( 'object', $pageLinksRow );
1010 $this->assertSame( 'Main', $pageLinksRow->pl_title );
1011
1012 if ( $this->hasMultiSlotSupport() ) {
1013 $pageLinksRow = $pageLinks->fetchObject();
1014 $this->assertInternalType( 'object', $pageLinksRow );
1015 $this->assertSame( 'Nix', $pageLinksRow->pl_title );
1016 }
1017
1018 // parser cache update
1019 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() );
1020 $this->assertInternalType( 'object', $cached );
1021 $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
1022
1023 // site stats
1024 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
1025 $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
1026 $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
1027 $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles );
1028
1029 // TODO: MCR: test data updates for additional slots!
1030 // TODO: test update for edit without page creation
1031 // TODO: test message cache purge
1032 // TODO: test module cache purge
1033 // TODO: test CDN purge
1034 // TODO: test newtalk update
1035 // TODO: test search update
1036 // TODO: test site stats good_articles while turning the page into (or back from) a redir.
1037 // TODO: test category membership update (with setRcWatchCategoryMembership())
1038 }
1039
1040 /**
1041 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
1042 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
1043 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
1044 */
1045 public function testDoUpdatesCacheSaveDeferral_canonical() {
1046 $page = $this->getPage( __METHOD__ );
1047
1048 // Case where user has canonical parser options
1049 $content = [ 'main' => new WikitextContent( 'rev ID ver #1: {{REVISIONID}}' ) ];
1050 $rev = $this->createRevision( $page, 'first', $content );
1051 $pcache = MediaWikiServices::getInstance()->getParserCache();
1052 $pcache->deleteOptionsKey( $page );
1053
1054 $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up
1055
1056 $updater = $this->getDerivedPageDataUpdater( $page, $rev );
1057 $updater->prepareUpdate( $rev, [] );
1058 $updater->doUpdates();
1059
1060 $this->assertGreaterThan( 0, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' );
1061 $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
1062
1063 $this->db->endAtomic( __METHOD__ ); // run deferred updates
1064
1065 $this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' );
1066 }
1067
1068 /**
1069 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
1070 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
1071 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
1072 */
1073 public function testDoUpdatesCacheSaveDeferral_noncanonical() {
1074 $page = $this->getPage( __METHOD__ );
1075
1076 // Case where user does not have canonical parser options
1077 $user = $this->getMutableTestUser()->getUser();
1078 $user->setOption(
1079 'thumbsize',
1080 $user->getOption( 'thumbsize' ) + 1
1081 );
1082 $content = [ 'main' => new WikitextContent( 'rev ID ver #2: {{REVISIONID}}' ) ];
1083 $rev = $this->createRevision( $page, 'first', $content, $user );
1084 $pcache = MediaWikiServices::getInstance()->getParserCache();
1085 $pcache->deleteOptionsKey( $page );
1086
1087 $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up
1088
1089 $updater = $this->getDerivedPageDataUpdater( $page, $rev, $user );
1090 $updater->prepareUpdate( $rev, [] );
1091 $updater->doUpdates();
1092
1093 $this->assertGreaterThan( 1, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' );
1094 $this->assertFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
1095
1096 $this->db->endAtomic( __METHOD__ ); // run deferred updates
1097
1098 $this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' );
1099 $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
1100 }
1101
1102 /**
1103 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
1104 */
1105 public function testDoParserCacheUpdate() {
1106 if ( $this->hasMultiSlotSupport() ) {
1107 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
1108 'aux',
1109 CONTENT_MODEL_WIKITEXT
1110 );
1111 }
1112
1113 $page = $this->getPage( __METHOD__ );
1114 $this->createRevision( $page, 'Dummy' );
1115
1116 $user = $this->getTestUser()->getUser();
1117
1118 $update = new RevisionSlotsUpdate();
1119 $update->modifyContent( 'main', new WikitextContent( 'first [[Main]]' ) );
1120
1121 if ( $this->hasMultiSlotSupport() ) {
1122 $update->modifyContent( 'aux', new WikitextContent( 'Aux [[Nix]]' ) );
1123 }
1124
1125 // Emulate update after edit ----------
1126 $pcache = MediaWikiServices::getInstance()->getParserCache();
1127 $pcache->deleteOptionsKey( $page );
1128
1129 $rev = $this->makeRevision( $page->getTitle(), $update, $user, 'rev', null );
1130 $rev->setTimestamp( '20100101000000' );
1131 $rev->setParentId( $page->getLatest() );
1132
1133 $updater = $this->getDerivedPageDataUpdater( $page );
1134 $updater->prepareContent( $user, $update, false );
1135
1136 $rev->setId( 11 );
1137 $updater->prepareUpdate( $rev );
1138
1139 // Force the page timestamp, so we notice whether ParserOutput::getTimestamp
1140 // or ParserOutput::getCacheTime are used.
1141 $page->setTimestamp( $rev->getTimestamp() );
1142 $updater->doParserCacheUpdate();
1143
1144 // The cached ParserOutput should not use the revision timestamp
1145 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true );
1146 $this->assertInternalType( 'object', $cached );
1147 $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
1148
1149 $this->assertSame( $rev->getTimestamp(), $cached->getCacheTime() );
1150 $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() );
1151
1152 // Emulate forced update of an old revision ----------
1153 $pcache->deleteOptionsKey( $page );
1154
1155 $updater = $this->getDerivedPageDataUpdater( $page );
1156 $updater->prepareUpdate( $rev );
1157
1158 // Force the page timestamp, so we notice whether ParserOutput::getTimestamp
1159 // or ParserOutput::getCacheTime are used.
1160 $page->setTimestamp( $rev->getTimestamp() );
1161 $updater->doParserCacheUpdate();
1162
1163 // The cached ParserOutput should not use the revision timestamp
1164 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true );
1165 $this->assertInternalType( 'object', $cached );
1166 $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
1167
1168 $this->assertGreaterThan( $rev->getTimestamp(), $cached->getCacheTime() );
1169 $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() );
1170 }
1171
1172 /**
1173 * @return bool
1174 */
1175 private function hasMultiSlotSupport() {
1176 global $wgMultiContentRevisionSchemaMigrationStage;
1177
1178 return ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW )
1179 && ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW );
1180 }
1181
1182 }