2020-09-30 19:25:29 +00:00
|
|
|
<?php
|
2020-11-18 00:26:53 +00:00
|
|
|
use MediaWiki\Json\JsonCodec;
|
2020-11-17 20:34:43 +00:00
|
|
|
use MediaWiki\Logger\Spi as LoggerSpi;
|
2020-09-30 19:25:29 +00:00
|
|
|
use MediaWiki\Page\ParserOutputAccess;
|
2020-12-04 15:28:25 +00:00
|
|
|
use MediaWiki\Parser\RevisionOutputCache;
|
2020-09-30 19:25:29 +00:00
|
|
|
use MediaWiki\Revision\MutableRevisionRecord;
|
|
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
2020-11-13 12:04:35 +00:00
|
|
|
use MediaWiki\Revision\RevisionRenderer;
|
2020-09-30 19:25:29 +00:00
|
|
|
use MediaWiki\Revision\SlotRecord;
|
|
|
|
|
use MediaWiki\Storage\RevisionStore;
|
|
|
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
2020-11-17 20:34:43 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
2020-09-30 19:25:29 +00:00
|
|
|
use Psr\Log\NullLogger;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @covers \MediaWiki\Page\ParserOutputAccess
|
|
|
|
|
* @group Database
|
|
|
|
|
*/
|
|
|
|
|
class ParserOutputAccessTest extends MediaWikiIntegrationTestCase {
|
|
|
|
|
|
|
|
|
|
private function getHtml( $value ) {
|
|
|
|
|
if ( $value instanceof StatusValue ) {
|
|
|
|
|
$value = $value->getValue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $value instanceof ParserOutput ) {
|
|
|
|
|
$value = $value->getText();
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-13 12:04:35 +00:00
|
|
|
$html = preg_replace( '/<!--.*?-->/s', '', $value );
|
|
|
|
|
$html = trim( preg_replace( '/[\r\n]{2,}/s', "\n", $html ) );
|
|
|
|
|
$html = trim( preg_replace( '/\s{2,}/s', ' ', $html ) );
|
2020-09-30 19:25:29 +00:00
|
|
|
return $html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function assertContainsHtml( $needle, $actual, $msg = '' ) {
|
|
|
|
|
$this->assertNotNull( $actual );
|
|
|
|
|
|
|
|
|
|
if ( $actual instanceof StatusValue ) {
|
|
|
|
|
$this->assertTrue( $actual->isOK(), 'isOK' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertStringContainsString( $needle, $this->getHtml( $actual ), $msg );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function assertSameHtml( $expected, $actual, $msg = '' ) {
|
|
|
|
|
$this->assertNotNull( $actual );
|
|
|
|
|
|
|
|
|
|
if ( $actual instanceof StatusValue ) {
|
|
|
|
|
$this->assertTrue( $actual->isOK(), 'isOK' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertSame( $this->getHtml( $expected ), $this->getHtml( $actual ), $msg );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function assertNotSameHtml( $expected, $actual, $msg = '' ) {
|
|
|
|
|
$this->assertNotNull( $actual );
|
|
|
|
|
|
|
|
|
|
if ( $actual instanceof StatusValue ) {
|
|
|
|
|
$this->assertTrue( $actual->isOK(), 'isOK' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->assertNotSame( $this->getHtml( $expected ), $this->getHtml( $actual ), $msg );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function getParserCache( $bag = null ) {
|
2020-10-23 00:17:31 +00:00
|
|
|
$parserCache = new ParserCache(
|
|
|
|
|
'test',
|
|
|
|
|
$bag ?: new HashBagOStuff(),
|
2021-02-03 22:08:25 +00:00
|
|
|
'19900220000000',
|
2020-10-23 00:17:31 +00:00
|
|
|
$this->getServiceContainer()->getHookContainer(),
|
2020-11-18 00:26:53 +00:00
|
|
|
new JsonCodec(),
|
2020-12-04 15:28:25 +00:00
|
|
|
new NullStatsdDataFactory(),
|
2021-03-24 14:05:44 +00:00
|
|
|
new NullLogger(),
|
|
|
|
|
$this->getServiceContainer()->getTitleFactory(),
|
|
|
|
|
$this->getServiceContainer()->getWikiPageFactory()
|
2020-10-23 00:17:31 +00:00
|
|
|
);
|
2020-09-30 19:25:29 +00:00
|
|
|
|
|
|
|
|
return $parserCache;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-04 15:28:25 +00:00
|
|
|
private function getRevisionOutputCache( $bag = null, $expiry = 3600 ) {
|
|
|
|
|
$wanCache = new WANObjectCache( [ 'cache' => $bag ?: new HashBagOStuff() ] );
|
|
|
|
|
$revisionOutputCache = new RevisionOutputCache(
|
|
|
|
|
'test',
|
|
|
|
|
$wanCache,
|
|
|
|
|
$expiry,
|
2021-02-03 22:08:25 +00:00
|
|
|
'19900220000000',
|
2020-12-04 15:28:25 +00:00
|
|
|
new JsonCodec(),
|
|
|
|
|
new NullStatsdDataFactory(),
|
|
|
|
|
new NullLogger()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return $revisionOutputCache;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-17 20:34:43 +00:00
|
|
|
/**
|
|
|
|
|
* @param LoggerInterface|null $logger
|
|
|
|
|
*
|
|
|
|
|
* @return LoggerSpi
|
|
|
|
|
*/
|
|
|
|
|
protected function getLoggerSpi( $logger = null ) {
|
|
|
|
|
$logger = $logger ?: new NullLogger();
|
|
|
|
|
$spi = $this->createNoOpMock( LoggerSpi::class, [ 'getLogger' ] );
|
|
|
|
|
$spi->method( 'getLogger' )->willReturn( $logger );
|
|
|
|
|
return $spi;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-30 19:25:29 +00:00
|
|
|
/**
|
|
|
|
|
* @param ParserCache|null $parserCache
|
2020-12-04 15:28:25 +00:00
|
|
|
* @param RevisionOutputCache|null $revisionOutputCache
|
2020-11-13 12:04:35 +00:00
|
|
|
* @param int|bool $maxRenderCalls
|
2020-09-30 19:25:29 +00:00
|
|
|
*
|
|
|
|
|
* @return ParserOutputAccess
|
2020-11-13 12:04:35 +00:00
|
|
|
* @throws Exception
|
2020-09-30 19:25:29 +00:00
|
|
|
*/
|
2020-11-13 12:04:35 +00:00
|
|
|
private function getParserOutputAccessWithCache(
|
|
|
|
|
$parserCache = null,
|
2020-12-04 15:28:25 +00:00
|
|
|
$revisionOutputCache = null,
|
2020-11-13 12:04:35 +00:00
|
|
|
$maxRenderCalls = false
|
|
|
|
|
) {
|
2020-09-30 19:25:29 +00:00
|
|
|
if ( !$parserCache ) {
|
|
|
|
|
$parserCache = $this->getParserCache( new HashBagOStuff() );
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-04 15:28:25 +00:00
|
|
|
if ( !$revisionOutputCache ) {
|
|
|
|
|
$revisionOutputCache = $this->getRevisionOutputCache( new HashBagOStuff() );
|
2020-11-13 12:04:35 +00:00
|
|
|
}
|
|
|
|
|
|
2020-11-13 11:46:05 +00:00
|
|
|
$revRenderer = $this->getServiceContainer()->getRevisionRenderer();
|
2020-11-16 20:05:03 +00:00
|
|
|
$lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory();
|
2020-09-30 19:25:29 +00:00
|
|
|
$stats = new NullStatsdDataFactory();
|
2020-11-13 12:04:35 +00:00
|
|
|
|
|
|
|
|
if ( $maxRenderCalls ) {
|
|
|
|
|
$realRevRenderer = $revRenderer;
|
|
|
|
|
$revRenderer =
|
|
|
|
|
$this->createNoOpMock( RevisionRenderer::class, [ 'getRenderedRevision' ] );
|
|
|
|
|
|
|
|
|
|
$revRenderer->expects( $this->atMost( $maxRenderCalls ) )
|
|
|
|
|
->method( 'getRenderedRevision' )
|
|
|
|
|
->willReturnCallback( [ $realRevRenderer, 'getRenderedRevision' ] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new ParserOutputAccess(
|
|
|
|
|
$parserCache,
|
2020-12-04 15:28:25 +00:00
|
|
|
$revisionOutputCache,
|
2021-04-03 02:12:43 +00:00
|
|
|
$this->getServiceContainer()->getRevisionLookup(),
|
2020-11-13 12:04:35 +00:00
|
|
|
$revRenderer,
|
|
|
|
|
$stats,
|
|
|
|
|
$lbFactory,
|
2021-04-03 02:12:43 +00:00
|
|
|
$this->getLoggerSpi(),
|
|
|
|
|
$this->getServiceContainer()->getWikiPageFactory(),
|
|
|
|
|
$this->getServiceContainer()->getTitleFormatter()
|
2020-11-13 12:04:35 +00:00
|
|
|
);
|
2020-09-30 19:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return ParserOutputAccess
|
|
|
|
|
*/
|
|
|
|
|
private function getParserOutputAccessNoCache() {
|
2020-12-04 15:28:25 +00:00
|
|
|
return $this->getParserOutputAccessWithCache(
|
|
|
|
|
$this->getParserCache( new EmptyBagOStuff() ),
|
|
|
|
|
$this->getRevisionOutputCache( new EmptyBagOStuff() )
|
|
|
|
|
);
|
2020-09-30 19:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param WikiPage $page
|
|
|
|
|
* @param string $text
|
|
|
|
|
*
|
|
|
|
|
* @return RevisionRecord
|
|
|
|
|
*/
|
|
|
|
|
private function makeFakeRevision( WikiPage $page, $text ) {
|
|
|
|
|
// construct fake revision with no ID
|
|
|
|
|
$content = new WikitextContent( $text );
|
|
|
|
|
$rev = new MutableRevisionRecord( $page->getTitle() );
|
|
|
|
|
$rev->setPageId( $page->getId() );
|
|
|
|
|
$rev->setContent( SlotRecord::MAIN, $content );
|
|
|
|
|
|
|
|
|
|
return $rev;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return ParserOptions
|
|
|
|
|
*/
|
|
|
|
|
private function getParserOptions() {
|
|
|
|
|
return ParserOptions::newCanonical( 'canonical' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that we can get rendered output for the latest revision.
|
|
|
|
|
*/
|
|
|
|
|
public function testOutputForLatestRevision() {
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$status = $access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertContainsHtml( 'Hello <i>World</i>!', $status );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that cached output in the ParserCache will be used for the latest revision.
|
|
|
|
|
*/
|
|
|
|
|
public function testLatestRevisionUseCached() {
|
2020-11-13 12:04:35 +00:00
|
|
|
// Allow only one render call, use default caches
|
2020-12-04 15:28:25 +00:00
|
|
|
$access = $this->getParserOutputAccessWithCache( null, null, 1 );
|
2020-09-30 19:25:29 +00:00
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
2020-11-13 12:04:35 +00:00
|
|
|
$access->getParserOutput( $page, $parserOptions );
|
2020-09-30 19:25:29 +00:00
|
|
|
|
2020-11-13 12:04:35 +00:00
|
|
|
// The second call should use cached output
|
2020-09-30 19:25:29 +00:00
|
|
|
$status = $access->getParserOutput( $page, $parserOptions );
|
2020-11-13 12:04:35 +00:00
|
|
|
$this->assertContainsHtml( 'Hello <i>World</i>!', $status );
|
2020-09-30 19:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that cached output in the ParserCache will not be used
|
|
|
|
|
* for the latest revision if the FORCE_PARSE option is given.
|
|
|
|
|
*/
|
|
|
|
|
public function testLatestRevisionForceParse() {
|
|
|
|
|
$parserCache = $this->getParserCache( new HashBagOStuff() );
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache( $parserCache );
|
|
|
|
|
|
|
|
|
|
$parserOptions = ParserOptions::newCanonical( 'canonical' );
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
// Put something else into the cache, so we'd notice if it got used
|
|
|
|
|
$cachedOutput = new ParserOutput( 'Cached Text' );
|
|
|
|
|
$parserCache->save( $cachedOutput, $page, $parserOptions );
|
|
|
|
|
|
|
|
|
|
$status = $access->getParserOutput(
|
|
|
|
|
$page,
|
|
|
|
|
$parserOptions,
|
|
|
|
|
null,
|
|
|
|
|
ParserOutputAccess::OPT_FORCE_PARSE
|
|
|
|
|
);
|
|
|
|
|
$this->assertNotSameHtml( $cachedOutput, $status );
|
|
|
|
|
$this->assertContainsHtml( 'Hello <i>World</i>!', $status );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that an error is reported if the latest revision cannot be loaded.
|
|
|
|
|
*/
|
|
|
|
|
public function testLatestRevisionCantLoad() {
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
$revisionStore = $this->createNoOpMock(
|
|
|
|
|
RevisionStore::class,
|
2021-04-03 02:12:43 +00:00
|
|
|
[ 'getRevisionByTitle', 'getKnownCurrentRevision', 'getRevisionById' ]
|
2020-09-30 19:25:29 +00:00
|
|
|
);
|
|
|
|
|
$revisionStore->method( 'getRevisionById' )->willReturn( null );
|
|
|
|
|
$revisionStore->method( 'getRevisionByTitle' )->willReturn( null );
|
|
|
|
|
$revisionStore->method( 'getKnownCurrentRevision' )->willReturn( false );
|
|
|
|
|
$this->setService( 'RevisionStore', $revisionStore );
|
|
|
|
|
$this->setService( 'RevisionLookup', $revisionStore );
|
|
|
|
|
|
|
|
|
|
$page->clear();
|
|
|
|
|
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$status = $access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertFalse( $status->isOK() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that getCachedParserOutput() will return previously generated output.
|
|
|
|
|
*/
|
|
|
|
|
public function testGetCachedParserOutput() {
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$output = $access->getCachedParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertNull( $output );
|
|
|
|
|
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
$access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
|
|
|
|
|
$output = $access->getCachedParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertNotNull( $output );
|
|
|
|
|
$this->assertContainsHtml( 'Hello <i>World</i>!', $output );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that getCachedParserOutput() will not return output for current revision when
|
|
|
|
|
* a fake revision with no ID is supplied.
|
|
|
|
|
*/
|
|
|
|
|
public function testGetCachedParserOutputForFakeRevision() {
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
|
|
|
|
|
$rev = $this->makeFakeRevision( $page, 'fake text' );
|
|
|
|
|
|
|
|
|
|
$output = $access->getCachedParserOutput( $page, $parserOptions, $rev );
|
|
|
|
|
$this->assertNull( $output );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that getPageOutput() will place the generated output for the latest revision
|
|
|
|
|
* in the parser cache.
|
|
|
|
|
*/
|
|
|
|
|
public function testLatestRevisionIsCached() {
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
|
|
|
|
|
$cachedOutput = $access->getCachedParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertContainsHtml( 'World', $cachedOutput );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that the cache for the current revision is split on parser options.
|
|
|
|
|
*/
|
|
|
|
|
public function testLatestRevisionCacheSplit() {
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$frenchOptions = ParserOptions::newCanonical( 'canonical' );
|
|
|
|
|
$frenchOptions->setUserLang( 'fr' );
|
|
|
|
|
|
|
|
|
|
$tongaOptions = ParserOptions::newCanonical( 'canonical' );
|
|
|
|
|
$tongaOptions->setUserLang( 'to' );
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Test {{int:ok}}!' );
|
|
|
|
|
|
|
|
|
|
$frenchResult = $access->getParserOutput( $page, $frenchOptions );
|
|
|
|
|
$this->assertContainsHtml( 'Test', $frenchResult );
|
|
|
|
|
|
|
|
|
|
// sanity check that French output was cached
|
|
|
|
|
$cachedFrenchOutput =
|
|
|
|
|
$access->getCachedParserOutput( $page, $frenchOptions );
|
|
|
|
|
$this->assertNotNull( $cachedFrenchOutput, 'French output should be in the cache' );
|
|
|
|
|
|
|
|
|
|
// check that we don't get the French output when asking for Tonga
|
|
|
|
|
$cachedTongaOutput =
|
|
|
|
|
$access->getCachedParserOutput( $page, $tongaOptions );
|
|
|
|
|
$this->assertNull( $cachedTongaOutput, 'Tonga output should not be in the cache yet' );
|
|
|
|
|
|
|
|
|
|
// check that we can generate the Tonga output, and it's different from French
|
|
|
|
|
$tongaResult = $access->getParserOutput( $page, $tongaOptions );
|
|
|
|
|
$this->assertContainsHtml( 'Test', $tongaResult );
|
|
|
|
|
$this->assertNotSameHtml(
|
|
|
|
|
$frenchResult,
|
|
|
|
|
$tongaResult,
|
|
|
|
|
'Tonga output should be different from French'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// check that the Tonga output is cached
|
|
|
|
|
$cachedTongaOutput =
|
|
|
|
|
$access->getCachedParserOutput( $page, $tongaOptions );
|
|
|
|
|
$this->assertNotNull( $cachedTongaOutput, 'Tonga output should be in the cache' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that getPageOutput() will place the generated output in the parser cache if the
|
|
|
|
|
* latest revision is passed explicitly. In other words, thins ensures that the current
|
|
|
|
|
* revision won't get treated like an old revision.
|
|
|
|
|
*/
|
|
|
|
|
public function testLatestRevisionIsDetectedAndCached() {
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$rev = $this->editPage( $page, 'Hello \'\'World\'\'!' )->getValue()['revision-record'];
|
|
|
|
|
|
|
|
|
|
// When $rev is passed, it should be detected to be the latest revision.
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$access->getParserOutput( $page, $parserOptions, $rev );
|
|
|
|
|
|
|
|
|
|
$cachedOutput = $access->getCachedParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertContainsHtml( 'World', $cachedOutput );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that getPageOutput() will generate output for an old revision, and
|
|
|
|
|
* that we still have the output for the current revision cached afterwards.
|
|
|
|
|
*/
|
|
|
|
|
public function testOutputForOldRevision() {
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$firstRev = $this->editPage( $page, 'First' )->getValue()['revision-record'];
|
|
|
|
|
$secondRev = $this->editPage( $page, 'Second' )->getValue()['revision-record'];
|
|
|
|
|
|
|
|
|
|
// output is for the second revision (write to ParserCache)
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$status = $access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertContainsHtml( 'Second', $status );
|
|
|
|
|
|
|
|
|
|
// output is for the first revision (not written to ParserCache)
|
|
|
|
|
$status = $access->getParserOutput( $page, $parserOptions, $firstRev );
|
|
|
|
|
$this->assertContainsHtml( 'First', $status );
|
|
|
|
|
|
|
|
|
|
// Latest revision is still the one in the ParserCache
|
|
|
|
|
$output = $access->getCachedParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertContainsHtml( 'Second', $output );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that trying to get output for a suppressed old revision is denied.
|
|
|
|
|
*/
|
|
|
|
|
public function testOldRevisionSuppressedDenied() {
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$firstRev = $this->editPage( $page, 'First' )->getValue()['revision-record'];
|
|
|
|
|
$secondRev = $this->editPage( $page, 'Second' )->getValue()['revision-record'];
|
|
|
|
|
|
|
|
|
|
$this->revisionDelete( $firstRev );
|
|
|
|
|
$firstRev =
|
|
|
|
|
$this->getServiceContainer()->getRevisionStore()->getRevisionById( $firstRev->getId() );
|
|
|
|
|
|
|
|
|
|
// output is for the first revision denied
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$status = $access->getParserOutput( $page, $parserOptions, $firstRev );
|
|
|
|
|
$this->assertFalse( $status->isOK() );
|
|
|
|
|
// TODO: Once PoolWorkArticleView properly reports errors, check that the correct error
|
|
|
|
|
// is propagated.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that getting output for a suppressed old revision is possible when NO_AUDIENCE_CHECK
|
|
|
|
|
* is set.
|
|
|
|
|
*/
|
|
|
|
|
public function testOldRevisionSuppressedAllowed() {
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$firstRev = $this->editPage( $page, 'First' )->getValue()['revision-record'];
|
|
|
|
|
$secondRev = $this->editPage( $page, 'Second' )->getValue()['revision-record'];
|
|
|
|
|
|
|
|
|
|
$this->revisionDelete( $firstRev );
|
|
|
|
|
$firstRev =
|
|
|
|
|
$this->getServiceContainer()->getRevisionStore()->getRevisionById( $firstRev->getId() );
|
|
|
|
|
|
|
|
|
|
// output is for the first revision (even though it's suppressed)
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$status = $access->getParserOutput(
|
|
|
|
|
$page,
|
|
|
|
|
$parserOptions,
|
|
|
|
|
$firstRev,
|
|
|
|
|
ParserOutputAccess::OPT_NO_AUDIENCE_CHECK
|
|
|
|
|
);
|
|
|
|
|
$this->assertContainsHtml( 'First', $status );
|
2020-12-04 15:28:25 +00:00
|
|
|
|
|
|
|
|
// even though the output was generated, it wasn't cached, since it's not public
|
|
|
|
|
$cachedOutput = $access->getCachedParserOutput( $page, $parserOptions, $firstRev );
|
|
|
|
|
$this->assertNull( $cachedOutput );
|
2020-09-30 19:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that output for an old revision is fetched from the secondary parser cache if possible.
|
|
|
|
|
*/
|
|
|
|
|
public function testOldRevisionUseCached() {
|
2020-11-13 12:04:35 +00:00
|
|
|
// Allow only one render call, use default caches
|
2020-12-04 15:28:25 +00:00
|
|
|
$access = $this->getParserOutputAccessWithCache( null, null, 1 );
|
2020-11-13 12:04:35 +00:00
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'First' );
|
|
|
|
|
$oldRev = $page->getRevisionRecord();
|
|
|
|
|
|
|
|
|
|
$this->editPage( $page, 'Second' );
|
|
|
|
|
|
|
|
|
|
$firstStatus = $access->getParserOutput( $page, $parserOptions, $oldRev );
|
|
|
|
|
|
|
|
|
|
// The second call should use cached output
|
|
|
|
|
$secondStatus = $access->getParserOutput( $page, $parserOptions, $oldRev );
|
|
|
|
|
$this->assertSameHtml( $firstStatus, $secondStatus );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that output for an old revision is fetched from the secondary parser cache if possible.
|
|
|
|
|
*/
|
|
|
|
|
public function testOldRevisionDisableCached() {
|
|
|
|
|
// Use default caches, but expiry 0 for the secondary cache
|
2020-12-04 15:28:25 +00:00
|
|
|
$access = $this->getParserOutputAccessWithCache(
|
|
|
|
|
null,
|
|
|
|
|
$this->getRevisionOutputCache( null, 0 )
|
|
|
|
|
);
|
2020-11-13 12:04:35 +00:00
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'First' );
|
|
|
|
|
$oldRev = $page->getRevisionRecord();
|
|
|
|
|
|
|
|
|
|
$this->editPage( $page, 'Second' );
|
|
|
|
|
$access->getParserOutput( $page, $parserOptions, $oldRev );
|
|
|
|
|
|
|
|
|
|
// Should not be cached!
|
|
|
|
|
$cachedOutput = $access->getCachedParserOutput( $page, $parserOptions, $oldRev );
|
|
|
|
|
$this->assertNull( $cachedOutput );
|
2020-09-30 19:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that the secondary cache for output for old revisions is split on parser options.
|
|
|
|
|
*/
|
|
|
|
|
public function testOldRevisionCacheSplit() {
|
2020-11-13 12:04:35 +00:00
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$frenchOptions = ParserOptions::newCanonical( 'canonical' );
|
|
|
|
|
$frenchOptions->setUserLang( 'fr' );
|
|
|
|
|
|
|
|
|
|
$tongaOptions = ParserOptions::newCanonical( 'canonical' );
|
|
|
|
|
$tongaOptions->setUserLang( 'to' );
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Test {{int:ok}}!' );
|
|
|
|
|
$oldRev = $page->getRevisionRecord();
|
|
|
|
|
|
|
|
|
|
$this->editPage( $page, 'Latest Test' );
|
|
|
|
|
|
|
|
|
|
$frenchResult = $access->getParserOutput( $page, $frenchOptions, $oldRev );
|
|
|
|
|
$this->assertContainsHtml( 'Test', $frenchResult );
|
|
|
|
|
|
|
|
|
|
// sanity check that French output was cached
|
|
|
|
|
$cachedFrenchOutput =
|
|
|
|
|
$access->getCachedParserOutput( $page, $frenchOptions, $oldRev );
|
|
|
|
|
$this->assertNotNull( $cachedFrenchOutput, 'French output should be in the cache' );
|
|
|
|
|
|
|
|
|
|
// check that we don't get the French output when asking for Tonga
|
|
|
|
|
$cachedTongaOutput =
|
|
|
|
|
$access->getCachedParserOutput( $page, $tongaOptions, $oldRev );
|
|
|
|
|
$this->assertNull( $cachedTongaOutput, 'Tonga output should not be in the cache yet' );
|
|
|
|
|
|
|
|
|
|
// check that we can generate the Tonga output, and it's different from French
|
|
|
|
|
$tongaResult = $access->getParserOutput( $page, $tongaOptions, $oldRev );
|
|
|
|
|
$this->assertContainsHtml( 'Test', $tongaResult );
|
|
|
|
|
$this->assertNotSameHtml(
|
|
|
|
|
$frenchResult,
|
|
|
|
|
$tongaResult,
|
|
|
|
|
'Tonga output should be different from French'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// check that the Tonga output is cached
|
|
|
|
|
$cachedTongaOutput =
|
|
|
|
|
$access->getCachedParserOutput( $page, $tongaOptions, $oldRev );
|
|
|
|
|
$this->assertNotNull( $cachedTongaOutput, 'Tonga output should be in the cache' );
|
2020-09-30 19:25:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that a RevisionRecord with no ID can be rendered if NO_CACHE is set.
|
|
|
|
|
*/
|
|
|
|
|
public function testFakeRevisionNoCache() {
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
$rev = $this->makeFakeRevision( $page, 'fake text' );
|
|
|
|
|
|
|
|
|
|
// Render fake
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$fakeResult = $access->getParserOutput(
|
|
|
|
|
$page,
|
|
|
|
|
$parserOptions,
|
|
|
|
|
$rev,
|
|
|
|
|
ParserOutputAccess::OPT_NO_CACHE
|
|
|
|
|
);
|
|
|
|
|
$this->assertContainsHtml( 'fake text', $fakeResult );
|
|
|
|
|
|
|
|
|
|
// check that fake output isn't cached
|
|
|
|
|
$cachedOutput = $access->getCachedParserOutput( $page, $parserOptions );
|
|
|
|
|
if ( $cachedOutput ) {
|
|
|
|
|
// we may have a cache entry for original edit
|
|
|
|
|
$this->assertNotSameHtml( $fakeResult, $cachedOutput );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that a RevisionRecord with no ID can not be rendered if NO_CACHE is not set.
|
|
|
|
|
*/
|
|
|
|
|
public function testFakeRevisionError() {
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
|
|
|
|
|
$page = $this->getExistingTestPage( __METHOD__ );
|
|
|
|
|
$rev = $this->makeFakeRevision( $page, 'fake text' );
|
|
|
|
|
|
|
|
|
|
// Render should fail
|
|
|
|
|
$this->expectException( InvalidArgumentException::class );
|
|
|
|
|
$access->getParserOutput( $page, $parserOptions, $rev );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that trying to render a RevisionRecord for another page will throw an exception.
|
|
|
|
|
*/
|
|
|
|
|
public function testPageIdMismatchError() {
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
|
|
|
|
|
$page1 = $this->getExistingTestPage( __METHOD__ . '-1' );
|
|
|
|
|
$page2 = $this->getExistingTestPage( __METHOD__ . '-2' );
|
|
|
|
|
|
|
|
|
|
$this->expectException( InvalidArgumentException::class );
|
|
|
|
|
$access->getParserOutput( $page1, $parserOptions, $page2->getRevisionRecord() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that trying to render a non-existing page will be reported as an error.
|
|
|
|
|
*/
|
|
|
|
|
public function testNonExistingPage() {
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$status = $access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertFalse( $status->isOK() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param Status $status
|
|
|
|
|
* @param bool $fastStale
|
|
|
|
|
*
|
|
|
|
|
* @return PoolCounter
|
|
|
|
|
*/
|
|
|
|
|
private function makePoolCounter( $status, $fastStale = false ) {
|
|
|
|
|
/** @var MockObject|PoolCounter $poolCounter */
|
|
|
|
|
$poolCounter = $this->getMockBuilder( PoolCounter::class )
|
|
|
|
|
->disableOriginalConstructor()
|
|
|
|
|
->onlyMethods( [ 'acquireForMe', 'acquireForAnyone', 'release', 'isFastStaleEnabled' ] )
|
|
|
|
|
->getMock();
|
|
|
|
|
|
|
|
|
|
$poolCounter->method( 'acquireForMe' )->willReturn( $status );
|
|
|
|
|
$poolCounter->method( 'acquireForAnyone' )->willReturn( $status );
|
|
|
|
|
$poolCounter->method( 'release' )->willReturn( Status::newGood( PoolCounter::RELEASED ) );
|
|
|
|
|
$poolCounter->method( 'isFastStaleEnabled' )->willReturn( $fastStale );
|
|
|
|
|
|
|
|
|
|
return $poolCounter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function providePoolWorkDirty() {
|
|
|
|
|
yield [ Status::newGood( PoolCounter::QUEUE_FULL ), false, 'view-pool-overload' ];
|
|
|
|
|
yield [ Status::newGood( PoolCounter::TIMEOUT ), false, 'view-pool-overload' ];
|
|
|
|
|
yield [ Status::newGood( PoolCounter::TIMEOUT ), true, 'view-pool-contention' ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that under some circumstances, stale cache entries will be returned, but get
|
|
|
|
|
* flagged as "dirty".
|
|
|
|
|
*
|
|
|
|
|
* @dataProvider providePoolWorkDirty
|
|
|
|
|
*/
|
|
|
|
|
public function testPoolWorkDirty( $status, $fastStale, $expectedMessage ) {
|
|
|
|
|
MWTimestamp::setFakeTime( '2020-04-04T01:02:03' );
|
|
|
|
|
|
|
|
|
|
$access = $this->getParserOutputAccessWithCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
|
|
|
|
|
// inject mock PoolCounter status
|
|
|
|
|
$this->setMwGlobals( [ 'wgParserCacheExpireTime' => 60, 'wgPoolCounterConf' => [
|
|
|
|
|
'ArticleView' => [ 'factory' => function () use ( $status, $fastStale ) {
|
|
|
|
|
return $this->makePoolCounter( $status, $fastStale );
|
|
|
|
|
} ],
|
|
|
|
|
] ] );
|
|
|
|
|
|
|
|
|
|
// expire parser cache
|
|
|
|
|
MWTimestamp::setFakeTime( '2020-05-05T01:02:03' );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$cachedResult = $access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertContainsHtml( 'World', $cachedResult );
|
|
|
|
|
|
|
|
|
|
$this->assertTrue( $cachedResult->hasMessage( $expectedMessage ) );
|
|
|
|
|
$this->assertTrue( $cachedResult->hasMessage( 'view-pool-dirty-output' ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that a failure to acquire a work lock will be reported as an error if no
|
|
|
|
|
* stale output can be returned.
|
|
|
|
|
*/
|
|
|
|
|
public function testPoolWorkTimeout() {
|
|
|
|
|
$this->setMwGlobals( [ 'wgParserCacheExpireTime' => 60, 'wgPoolCounterConf' => [
|
|
|
|
|
'ArticleView' => [ 'factory' => function () {
|
|
|
|
|
return $this->makePoolCounter( Status::newGood( PoolCounter::TIMEOUT ) );
|
|
|
|
|
} ],
|
|
|
|
|
] ] );
|
|
|
|
|
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$result = $access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertFalse( $result->isOK() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests that a PoolCounter error does not prevent output from being generated.
|
|
|
|
|
*/
|
|
|
|
|
public function testPoolWorkError() {
|
|
|
|
|
$this->setMwGlobals( [ 'wgParserCacheExpireTime' => 60, 'wgPoolCounterConf' => [
|
|
|
|
|
'ArticleView' => [ 'factory' => function () {
|
|
|
|
|
return $this->makePoolCounter( Status::newFatal( 'some-error' ) );
|
|
|
|
|
} ],
|
|
|
|
|
] ] );
|
|
|
|
|
|
|
|
|
|
$access = $this->getParserOutputAccessNoCache();
|
|
|
|
|
|
|
|
|
|
$page = $this->getNonexistingTestPage( __METHOD__ );
|
|
|
|
|
$this->editPage( $page, 'Hello \'\'World\'\'!' );
|
|
|
|
|
|
|
|
|
|
$parserOptions = $this->getParserOptions();
|
|
|
|
|
$result = $access->getParserOutput( $page, $parserOptions );
|
|
|
|
|
$this->assertContainsHtml( 'World', $result );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|