Pages that are fast to render can be omitted from the parser cache to preserve disk space and cache write operations. The threshold is configurable per namespace, so the tradeoff can be evaluated based on different access patterns. For example, pages that are accessed rarely, like file description pages on commons, may have a high threshold configured, while pages that are read frequently, like wikipedia articles, may be configured to be always cached, using a 0 threshold. Filtering is based on a time profile recorded in the ParserOutput. A generic mechanism for capturing the timing profile is implemented in the ContentHandler base class. Subclasses may implement a more rigorous capture mechanism. Bug: T346765 Change-Id: I38a6f3ef064f98f3ad6a7c60856b0248a94fe9ac
1322 lines
44 KiB
PHP
1322 lines
44 KiB
PHP
<?php
|
|
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\Parser\ParserOutputStringSets;
|
|
use MediaWiki\Tests\Parser\ParserCacheSerializationTestCases;
|
|
use MediaWiki\Title\Title;
|
|
use MediaWiki\Title\TitleValue;
|
|
use MediaWiki\Utils\MWTimestamp;
|
|
use Wikimedia\Bcp47Code\Bcp47CodeValue;
|
|
use Wikimedia\Parsoid\Core\SectionMetadata;
|
|
use Wikimedia\Parsoid\Core\TOCData;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
use Wikimedia\Tests\SerializationTestTrait;
|
|
|
|
/**
|
|
* @covers ParserOutput
|
|
* @covers CacheTime
|
|
* @group Database
|
|
* ^--- trigger DB shadowing because we are using Title magic
|
|
*/
|
|
class ParserOutputTest extends MediaWikiLangTestCase {
|
|
use SerializationTestTrait;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
MWTimestamp::setFakeTime( ParserCacheSerializationTestCases::FAKE_TIME );
|
|
$this->overrideConfigValue(
|
|
MainConfigNames::ParserCacheExpireTime,
|
|
ParserCacheSerializationTestCases::FAKE_CACHE_EXPIRY
|
|
);
|
|
// Serialization tests still use these methods.
|
|
$this->hideDeprecated( 'ParserOutput::setTOCHTML' );
|
|
$this->hideDeprecated( 'ParserOutput::getTOCHTML' );
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getClassToTest
|
|
* @return string
|
|
*/
|
|
protected function getClassToTest(): string {
|
|
return ParserOutput::class;
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getSerializedDataPath
|
|
* @return string
|
|
*/
|
|
protected function getSerializedDataPath(): string {
|
|
return __DIR__ . '/../../data/ParserCache';
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getTestInstancesAndAssertions
|
|
* @return array
|
|
*/
|
|
protected function getTestInstancesAndAssertions(): array {
|
|
return ParserCacheSerializationTestCases::getParserOutputTestCases();
|
|
}
|
|
|
|
/**
|
|
* Overrides SerializationTestTrait::getSupportedSerializationFormats
|
|
* @return array
|
|
*/
|
|
protected function getSupportedSerializationFormats(): array {
|
|
return ParserCacheSerializationTestCases::getSupportedSerializationFormats(
|
|
$this->getClassToTest() );
|
|
}
|
|
|
|
public static function provideIsLinkInternal() {
|
|
return [
|
|
// Different domains
|
|
[ false, 'http://example.org', 'http://mediawiki.org' ],
|
|
// Same domains
|
|
[ true, 'http://example.org', 'http://example.org' ],
|
|
[ true, 'https://example.org', 'https://example.org' ],
|
|
[ true, '//example.org', '//example.org' ],
|
|
// Same domain different cases
|
|
[ true, 'http://example.org', 'http://EXAMPLE.ORG' ],
|
|
// Paths, queries, and fragments are not relevant
|
|
[ true, 'http://example.org', 'http://example.org/wiki/Main_Page' ],
|
|
[ true, 'http://example.org', 'http://example.org?my=query' ],
|
|
[ true, 'http://example.org', 'http://example.org#its-a-fragment' ],
|
|
// Different protocols
|
|
[ false, 'http://example.org', 'https://example.org' ],
|
|
[ false, 'https://example.org', 'http://example.org' ],
|
|
// Protocol relative servers always match http and https links
|
|
[ true, '//example.org', 'http://example.org' ],
|
|
[ true, '//example.org', 'https://example.org' ],
|
|
// But they don't match strange things like this
|
|
[ false, '//example.org', 'irc://example.org' ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Test to make sure ParserOutput::isLinkInternal behaves properly
|
|
* @dataProvider provideIsLinkInternal
|
|
* @covers ParserOutput::isLinkInternal
|
|
*/
|
|
public function testIsLinkInternal( $shouldMatch, $server, $url ) {
|
|
$this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::appendJsConfigVar
|
|
* @covers ParserOutput::setJsConfigVar
|
|
* @covers ParserOutput::getJsConfigVars
|
|
*/
|
|
public function testJsConfigVars() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setJsConfigVar( 'a', '1' );
|
|
$po->appendJsConfigVar( 'b', 'a' );
|
|
$po->appendJsConfigVar( 'b', '0' );
|
|
|
|
$this->assertEqualsCanonicalizing( [
|
|
'a' => 1,
|
|
'b' => [ 'a' => true, '0' => true ],
|
|
], $po->getJsConfigVars() );
|
|
|
|
$po->setJsConfigVar( 'c', '2' );
|
|
$po->appendJsConfigVar( 'b', 'b' );
|
|
$po->appendJsConfigVar( 'b', '1' );
|
|
|
|
$this->assertEqualsCanonicalizing( [
|
|
'a' => 1,
|
|
'b' => [ 'a' => true, 'b' => true, '0' => true, '1' => true ],
|
|
'c' => 2,
|
|
], $po->getJsConfigVars() );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::appendExtensionData
|
|
* @covers ParserOutput::setExtensionData
|
|
* @covers ParserOutput::getExtensionData
|
|
*/
|
|
public function testExtensionData() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setExtensionData( "one", "Foo" );
|
|
$po->appendExtensionData( "three", "abc" );
|
|
|
|
$this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
|
|
$this->assertNull( $po->getExtensionData( "spam" ) );
|
|
|
|
$po->setExtensionData( "two", "Bar" );
|
|
$this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
|
|
$this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
|
|
|
|
// Note that overwriting extension data (as this test case
|
|
// does) is deprecated and will eventually throw an
|
|
// exception. However, at the moment it is still worth testing
|
|
// this case to ensure backward compatibility. (T300981)
|
|
$po->setExtensionData( "one", null );
|
|
$this->assertNull( $po->getExtensionData( "one" ) );
|
|
$this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
|
|
|
|
$this->assertEqualsCanonicalizing( [
|
|
'abc' => true,
|
|
], $po->getExtensionData( "three" ) );
|
|
|
|
$po->appendExtensionData( "three", "xyz" );
|
|
$this->assertEqualsCanonicalizing( [
|
|
'abc' => true,
|
|
'xyz' => true,
|
|
], $po->getExtensionData( "three" ) );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::setPageProperty
|
|
* @covers ParserOutput::getPageProperty
|
|
* @covers ParserOutput::unsetPageProperty
|
|
* @covers ParserOutput::getPageProperties
|
|
*/
|
|
public function testProperties() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setPageProperty( 'foo', 'val' );
|
|
|
|
$properties = $po->getPageProperties();
|
|
$this->assertSame( 'val', $po->getPageProperty( 'foo' ) );
|
|
$this->assertSame( 'val', $properties['foo'] );
|
|
|
|
$po->setPageProperty( 'foo', 'second val' );
|
|
|
|
$properties = $po->getPageProperties();
|
|
$this->assertSame( 'second val', $po->getPageProperty( 'foo' ) );
|
|
$this->assertSame( 'second val', $properties['foo'] );
|
|
|
|
$po->unsetPageProperty( 'foo' );
|
|
|
|
$properties = $po->getPageProperties();
|
|
$this->assertSame( null, $po->getPageProperty( 'foo' ) );
|
|
$this->assertArrayNotHasKey( 'foo', $properties );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::setLanguage
|
|
* @covers ParserOutput::getLanguage
|
|
*/
|
|
public function testLanguage() {
|
|
$po = new ParserOutput();
|
|
|
|
$langFr = new Bcp47CodeValue( 'fr' );
|
|
$langCrhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
|
|
|
|
// Fallback to null
|
|
$this->assertSame( null, $po->getLanguage() );
|
|
|
|
// Simple case
|
|
$po->setLanguage( $langFr );
|
|
$this->assertSame( $langFr->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
|
|
|
|
// Language with a variant
|
|
$po->setLanguage( $langCrhCyrl );
|
|
$this->assertSame( $langCrhCyrl->toBcp47Code(), $po->getLanguage()->toBcp47Code() );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::getWrapperDivClass
|
|
* @covers ParserOutput::addWrapperDivClass
|
|
* @covers ParserOutput::clearWrapperDivClass
|
|
* @covers ParserOutput::getText
|
|
*/
|
|
public function testWrapperDivClass() {
|
|
$po = new ParserOutput();
|
|
|
|
$po->setText( 'Kittens' );
|
|
$this->assertStringContainsString( 'Kittens', $po->getText() );
|
|
$this->assertStringNotContainsString( '<div', $po->getText() );
|
|
$this->assertSame( 'Kittens', $po->getRawText() );
|
|
|
|
$po->addWrapperDivClass( 'foo' );
|
|
$text = $po->getText();
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringContainsString( '<div', $text );
|
|
$this->assertStringContainsString( 'class="mw-content-ltr foo"', $text );
|
|
|
|
$po->addWrapperDivClass( 'bar' );
|
|
$text = $po->getText();
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringContainsString( '<div', $text );
|
|
$this->assertStringContainsString( 'class="mw-content-ltr foo bar"', $text );
|
|
|
|
$po->addWrapperDivClass( 'bar' ); // second time does nothing, no "foo bar bar".
|
|
$text = $po->getText( [ 'unwrap' => true ] );
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringNotContainsString( '<div', $text );
|
|
$this->assertStringNotContainsString( 'class="', $text );
|
|
|
|
$text = $po->getText( [ 'wrapperDivClass' => '' ] );
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringNotContainsString( '<div', $text );
|
|
$this->assertStringNotContainsString( 'class="', $text );
|
|
|
|
$text = $po->getText( [ 'wrapperDivClass' => 'xyzzy' ] );
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringContainsString( '<div', $text );
|
|
$this->assertStringContainsString( 'class="mw-content-ltr xyzzy"', $text );
|
|
$this->assertStringNotContainsString( 'foo bar', $text );
|
|
|
|
$text = $po->getRawText();
|
|
$this->assertSame( 'Kittens', $text );
|
|
|
|
$po->clearWrapperDivClass();
|
|
$text = $po->getText();
|
|
$this->assertStringContainsString( 'Kittens', $text );
|
|
$this->assertStringNotContainsString( '<div', $text );
|
|
$this->assertStringNotContainsString( 'class="', $text );
|
|
}
|
|
|
|
/**
|
|
* This test aims at being replaced by its version in DefaultOutputTransformTest when ParserOutput::getText
|
|
* gets deprecated.
|
|
* @covers ParserOutput::getText
|
|
* @dataProvider provideGetText
|
|
* @param array $options Options to getText()
|
|
* @param string $text Parser text
|
|
* @param string $expect Expected output
|
|
*/
|
|
public function testGetText( $options, $text, $expect ) {
|
|
// Avoid other skins affecting the section edit links
|
|
$this->overrideConfigValue( MainConfigNames::DefaultSkin, 'fallback' );
|
|
RequestContext::resetMain();
|
|
|
|
$this->overrideConfigValues( [
|
|
MainConfigNames::ArticlePath => '/wiki/$1',
|
|
MainConfigNames::ScriptPath => '/w',
|
|
MainConfigNames::Script => '/w/index.php',
|
|
] );
|
|
|
|
$po = new ParserOutput( $text );
|
|
self::initSections( $po );
|
|
$actual = $po->getText( $options );
|
|
$this->assertSame( $expect, $actual );
|
|
}
|
|
|
|
private static function initSections( ParserOutput $po ): void {
|
|
$po->setTOCData( new TOCData(
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "1",
|
|
'level' => 1,
|
|
'toclevel' => 1,
|
|
'number' => "1",
|
|
'line' => "Section 1",
|
|
'anchor' => "Section_1"
|
|
] ),
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "2",
|
|
'level' => 1,
|
|
'toclevel' => 1,
|
|
'number' => "2",
|
|
'line' => "Section 2",
|
|
'anchor' => "Section_2"
|
|
] ),
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "3",
|
|
'level' => 2,
|
|
'toclevel' => 2,
|
|
'number' => "2.1",
|
|
'line' => "Section 2.1",
|
|
'anchor' => "Section_2.1"
|
|
] ),
|
|
SectionMetadata::fromLegacy( [
|
|
'index' => "4",
|
|
'level' => 1,
|
|
'toclevel' => 1,
|
|
'number' => "3",
|
|
'line' => "Section 3",
|
|
'anchor' => "Section_3"
|
|
] ),
|
|
) );
|
|
}
|
|
|
|
public static function provideGetText() {
|
|
$text = <<<EOF
|
|
<p>Test document.
|
|
</p>
|
|
<meta property="mw:PageProp/toc" />
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><mw:editsection page="Talk:User:Bug_T261347" section="3">Section 2.1</mw:editsection></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></h2>
|
|
<p>Three
|
|
</p>
|
|
EOF;
|
|
|
|
$dedupText = <<<EOF
|
|
<p>This is a test document.</p>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
|
|
<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
|
|
<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
|
|
<style>.Duplicate1 {}</style>
|
|
EOF;
|
|
|
|
return [
|
|
'No options' => [
|
|
[], $text, <<<EOF
|
|
<p>Test document.
|
|
</p>
|
|
<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading"><input type="checkbox" role="button" id="toctogglecheckbox" class="toctogglecheckbox" style="display:none" /><div class="toctitle" lang="en" dir="ltr"><h2 id="mw-toc-heading">Contents</h2><span class="toctogglespan"><label class="toctogglelabel" for="toctogglecheckbox"></label></span></div>
|
|
<ul>
|
|
<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
|
|
<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
|
|
<ul>
|
|
<li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Three
|
|
</p>
|
|
EOF
|
|
],
|
|
'Disable section edit links' => [
|
|
[ 'enableSectionEditLinks' => false ], $text, <<<EOF
|
|
<p>Test document.
|
|
</p>
|
|
<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading"><input type="checkbox" role="button" id="toctogglecheckbox" class="toctogglecheckbox" style="display:none" /><div class="toctitle" lang="en" dir="ltr"><h2 id="mw-toc-heading">Contents</h2><span class="toctogglespan"><label class="toctogglelabel" for="toctogglecheckbox"></label></span></div>
|
|
<ul>
|
|
<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
|
|
<li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
|
|
<ul>
|
|
<li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li>
|
|
</ul>
|
|
</li>
|
|
<li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span></h2>
|
|
<p>Three
|
|
</p>
|
|
EOF
|
|
],
|
|
'Disable TOC, but wrap' => [
|
|
[ 'allowTOC' => false, 'wrapperDivClass' => 'mw-parser-output' ], $text, <<<EOF
|
|
<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"><p>Test document.
|
|
</p>
|
|
|
|
<h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>One
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Two
|
|
</p>
|
|
<h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
|
|
<p>Two point one
|
|
</p>
|
|
<h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&action=edit&section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
|
|
<p>Three
|
|
</p></div>
|
|
EOF
|
|
],
|
|
'Style deduplication' => [
|
|
[], $dedupText, <<<EOF
|
|
<p>This is a test document.</p>
|
|
<style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
|
|
<style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2">
|
|
<style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
|
|
<link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1">
|
|
<style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
|
|
<style>.Duplicate1 {}</style>
|
|
EOF
|
|
],
|
|
'Style deduplication disabled' => [
|
|
[ 'deduplicateStyles' => false ], $dedupText, $dedupText
|
|
],
|
|
];
|
|
// phpcs:enable
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::hasText
|
|
*/
|
|
public function testHasText() {
|
|
$po = new ParserOutput( '' );
|
|
$this->assertTrue( $po->hasText() );
|
|
|
|
$po = new ParserOutput( null );
|
|
$this->assertFalse( $po->hasText() );
|
|
|
|
$po = new ParserOutput();
|
|
$this->assertFalse( $po->hasText() );
|
|
|
|
$po = new ParserOutput( '' );
|
|
$this->assertTrue( $po->hasText() );
|
|
|
|
$po = new ParserOutput( null );
|
|
$po->setText( '' );
|
|
$this->assertTrue( $po->hasText() );
|
|
|
|
$po = new ParserOutput( 'foo' );
|
|
$po->setText( null );
|
|
$this->assertFalse( $po->hasText() );
|
|
}
|
|
|
|
/**
|
|
* This test aims at being replaced by its version in DefaultOutputTransformTest when ParserOutput::getText
|
|
* gets deprecated.
|
|
* @covers ParserOutput::getText
|
|
*/
|
|
public function testGetText_failsIfNoText() {
|
|
$po = new ParserOutput( null );
|
|
|
|
$this->expectException( LogicException::class );
|
|
$po->getText();
|
|
}
|
|
|
|
public static function provideGetText_absoluteURLs() {
|
|
yield 'empty' => [
|
|
'text' => '',
|
|
'expectedText' => '',
|
|
];
|
|
yield 'no-links' => [
|
|
'text' => '<p>test</p>',
|
|
'expectedText' => '<p>test</p>',
|
|
];
|
|
yield 'simple link' => [
|
|
'text' => '<a href="/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="//TEST_SERVER/wiki/Test">test</a>',
|
|
];
|
|
yield 'already absolute, relative' => [
|
|
'text' => '<a href="//TEST_SERVER/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="//TEST_SERVER/wiki/Test">test</a>',
|
|
];
|
|
yield 'already absolute, https' => [
|
|
'text' => '<a href="https://TEST_SERVER/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="https://TEST_SERVER/wiki/Test">test</a>',
|
|
];
|
|
yield 'external' => [
|
|
'text' => '<a href="https://en.wikipedia.org/wiki/Test">test</a>',
|
|
'expectedText' => '<a href="https://en.wikipedia.org/wiki/Test">test</a>',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* This test aims at being replaced by its version in DefaultOutputTransformTest when ParserOutput::getText
|
|
* gets deprecated.
|
|
* @dataProvider provideGetText_absoluteURLs
|
|
*/
|
|
public function testGetText_absoluteURLs( string $text, string $expectedText ) {
|
|
$this->overrideConfigValue( MainConfigNames::Server, '//TEST_SERVER' );
|
|
$parserOutput = new ParserOutput( $text );
|
|
$this->assertSame( $expectedText, $parserOutput->getText( [ 'absoluteURLs' => true ] ) );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::getRawText
|
|
*/
|
|
public function testGetRawText_failsIfNoText() {
|
|
$po = new ParserOutput( null );
|
|
|
|
$this->expectException( LogicException::class );
|
|
$po->getRawText();
|
|
}
|
|
|
|
public static function provideMergeHtmlMetaDataFrom() {
|
|
// title text ------------
|
|
$a = new ParserOutput();
|
|
$a->setTitleText( 'X' );
|
|
$b = new ParserOutput();
|
|
yield 'only left title text' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setTitleText( 'Y' );
|
|
yield 'only right title text' => [ $a, $b, [ 'getTitleText' => 'Y' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setTitleText( 'X' );
|
|
$b = new ParserOutput();
|
|
$b->setTitleText( 'Y' );
|
|
yield 'left title text wins' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
|
|
|
|
// index policy ------------
|
|
$a = new ParserOutput();
|
|
$a->setIndexPolicy( 'index' );
|
|
$b = new ParserOutput();
|
|
yield 'only left index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'index' );
|
|
yield 'only right index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setIndexPolicy( 'noindex' );
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'index' );
|
|
yield 'left noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setIndexPolicy( 'index' );
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'noindex' );
|
|
yield 'right noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
|
|
|
|
$crhCyrl = new Bcp47CodeValue( 'crh-cyrl' );
|
|
|
|
$a = new ParserOutput();
|
|
$a->setLanguage( $crhCyrl );
|
|
$b = new ParserOutput();
|
|
yield 'only left language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setLanguage( $crhCyrl );
|
|
yield 'only right language' => [ $a, $b, [ 'getLanguage' => $crhCyrl ] ];
|
|
|
|
// head items and friends ------------
|
|
$a = new ParserOutput();
|
|
$a->addHeadItem( '<foo1>' );
|
|
$a->addHeadItem( '<bar1>', 'bar' );
|
|
$a->addModules( [ 'test-module-a' ] );
|
|
$a->addModuleStyles( [ 'test-module-styles-a' ] );
|
|
$a->setJsConfigVar( 'test-config-var-a', 'a' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'abc' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'def' );
|
|
$a->addExtraCSPStyleSrc( 'css.com' );
|
|
$a->addExtraCSPStyleSrc( 'css2.com' );
|
|
$a->addExtraCSPScriptSrc( 'js.com' );
|
|
$a->addExtraCSPDefaultSrc( 'img.com' );
|
|
|
|
$b = new ParserOutput();
|
|
$b->setIndexPolicy( 'noindex' );
|
|
$b->addHeadItem( '<foo2>' );
|
|
$b->addHeadItem( '<bar2>', 'bar' );
|
|
$b->addModules( [ 'test-module-b' ] );
|
|
$b->addModuleStyles( [ 'test-module-styles-b' ] );
|
|
$b->setJsConfigVar( 'test-config-var-b', 'b' );
|
|
$b->setJsConfigVar( 'test-config-var-a', 'X' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'xyz' );
|
|
$a->appendJsConfigVar( 'test-config-var-c', 'def' );
|
|
$b->addExtraCSPStyleSrc( 'https://css.ca' );
|
|
$b->addExtraCSPScriptSrc( 'jscript.com' );
|
|
$b->addExtraCSPScriptSrc( 'vbscript.com' );
|
|
$b->addExtraCSPDefaultSrc( 'img.com/foo.jpg' );
|
|
|
|
// Note that overwriting test-config-var-a during the merge
|
|
// (as this test case does) is deprecated and will eventually
|
|
// throw an exception. However, at the moment it is still worth
|
|
// testing this case to ensure backward compatibility. (T300307)
|
|
yield 'head items and friends' => [ $a, $b, [
|
|
'getHeadItems' => [
|
|
'<foo1>',
|
|
'<foo2>',
|
|
'bar' => '<bar2>', // overwritten
|
|
],
|
|
'getModules' => [
|
|
'test-module-a',
|
|
'test-module-b',
|
|
],
|
|
'getModuleStyles' => [
|
|
'test-module-styles-a',
|
|
'test-module-styles-b',
|
|
],
|
|
'getJsConfigVars' => [
|
|
'test-config-var-a' => 'X', // overwritten
|
|
'test-config-var-b' => 'b',
|
|
'test-config-var-c' => [ // merged safely
|
|
'abc' => true, 'def' => true, 'xyz' => true,
|
|
],
|
|
],
|
|
'getExtraCSPStyleSrcs' => [
|
|
'css.com',
|
|
'css2.com',
|
|
'https://css.ca'
|
|
],
|
|
'getExtraCSPScriptSrcs' => [
|
|
'js.com',
|
|
'jscript.com',
|
|
'vbscript.com'
|
|
],
|
|
'getExtraCSPDefaultSrcs' => [
|
|
'img.com',
|
|
'img.com/foo.jpg'
|
|
]
|
|
] ];
|
|
|
|
// TOC ------------
|
|
$a = new ParserOutput( '' );
|
|
$a->setSections( [ [ 'fromtitle' => 'A1' ], [ 'fromtitle' => 'A2' ] ] );
|
|
|
|
$b = new ParserOutput( '' );
|
|
$b->setSections( [ [ 'fromtitle' => 'B1' ], [ 'fromtitle' => 'B2' ] ] );
|
|
|
|
yield 'concat TOC' => [ $a, $b, [
|
|
'getTOCHTML' => '',
|
|
'getSections' => [
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'A1' ] )->toLegacy(),
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'A2' ] )->toLegacy(),
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'B1' ] )->toLegacy(),
|
|
SectionMetadata::fromLegacy( [ 'fromtitle' => 'B2' ] )->toLegacy()
|
|
],
|
|
] ];
|
|
|
|
// Skin Control ------------
|
|
$a = new ParserOutput();
|
|
$a->setNewSection( true );
|
|
$a->setHideNewSection( true );
|
|
$a->setNoGallery( true );
|
|
$a->addWrapperDivClass( 'foo' );
|
|
|
|
$a->setIndicator( 'foo', 'Foo!' );
|
|
$a->setIndicator( 'bar', 'Bar!' );
|
|
|
|
$a->setExtensionData( 'foo', 'Foo!' );
|
|
$a->setExtensionData( 'bar', 'Bar!' );
|
|
$a->appendExtensionData( 'bat', 'abc' );
|
|
|
|
$b = new ParserOutput();
|
|
$b->setNoGallery( true );
|
|
$b->setEnableOOUI( true );
|
|
$b->setPreventClickjacking( true );
|
|
$a->addWrapperDivClass( 'bar' );
|
|
|
|
$b->setIndicator( 'zoo', 'Zoo!' );
|
|
$b->setIndicator( 'bar', 'Barrr!' );
|
|
|
|
$b->setExtensionData( 'zoo', 'Zoo!' );
|
|
$b->setExtensionData( 'bar', 'Barrr!' );
|
|
$b->appendExtensionData( 'bat', 'xyz' );
|
|
|
|
// Note that overwriting extension data during the merge
|
|
// (as this test case does for 'bar') is deprecated and will eventually
|
|
// throw an exception. However, at the moment it is still worth
|
|
// testing this case to ensure backward compatibility. (T300981)
|
|
yield 'skin control flags' => [ $a, $b, [
|
|
'getNewSection' => true,
|
|
'getHideNewSection' => true,
|
|
'getNoGallery' => true,
|
|
'getEnableOOUI' => true,
|
|
'getPreventClickjacking' => true,
|
|
'getIndicators' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
],
|
|
'getWrapperDivClass' => 'foo bar',
|
|
'$mExtensionData' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
// internal strategy key is exposed here because we're looking
|
|
// at the raw property value, not using getExtensionData()
|
|
'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
|
|
],
|
|
] ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMergeHtmlMetaDataFrom
|
|
* @covers ParserOutput::mergeHtmlMetaDataFrom
|
|
*
|
|
* @param ParserOutput $a
|
|
* @param ParserOutput $b
|
|
* @param array $expected
|
|
*/
|
|
public function testMergeHtmlMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
|
|
$a->mergeHtmlMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
|
|
// test twice, to make sure the operation is idempotent (except for the TOC, see below)
|
|
$a->mergeHtmlMetaDataFrom( $b );
|
|
|
|
// XXX: TOC joining should get smarter. Can we make it idempotent as well?
|
|
unset( $expected['getTOCHTML'] );
|
|
unset( $expected['getSections'] );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
}
|
|
|
|
private function assertFieldValues( ParserOutput $po, $expected ) {
|
|
$po = TestingAccessWrapper::newFromObject( $po );
|
|
|
|
foreach ( $expected as $method => $value ) {
|
|
$canonicalize = false;
|
|
if ( $method[0] === '$' ) {
|
|
$field = substr( $method, 1 );
|
|
$actual = $po->__get( $field );
|
|
} else {
|
|
$actual = $po->__call( $method, [] );
|
|
}
|
|
if ( $method === 'getJsConfigVars' ) {
|
|
$canonicalize = true;
|
|
}
|
|
|
|
if ( $canonicalize ) {
|
|
// order of entries isn't significant
|
|
$this->assertEqualsCanonicalizing( $value, $actual, $method );
|
|
} else {
|
|
$this->assertEquals( $value, $actual, $method );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::addLink
|
|
* @covers ParserOutput::getLinks
|
|
*/
|
|
public function testAddLink() {
|
|
$a = new ParserOutput();
|
|
$a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
|
|
$a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
|
|
$a->addLink( new TitleValue( NS_MAIN, 'Goats_786827346' ) );
|
|
|
|
$expected = [
|
|
NS_MAIN => [ 'Kittens' => 6, 'Goats_786827346' => 0 ],
|
|
NS_TALK => [ 'Kittens' => 16 ]
|
|
];
|
|
$this->assertSame( $expected, $a->getLinks() );
|
|
}
|
|
|
|
public static function provideMergeTrackingMetaDataFrom() {
|
|
// links ------------
|
|
$a = new ParserOutput();
|
|
$a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
|
|
$a->addLink( new TitleValue( NS_TALK, 'Kittens' ), 16 );
|
|
$a->addLink( new TitleValue( NS_MAIN, 'Goats' ), 7 );
|
|
|
|
$a->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Goats' ), 107, 1107 );
|
|
|
|
$a->addLanguageLink( 'de' );
|
|
$a->addLanguageLink( 'ru' );
|
|
$a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens DE', '', 'de' ) );
|
|
$a->addInterwikiLink( new TitleValue( NS_MAIN, 'Kittens RU', '', 'ru' ) );
|
|
$a->addExternalLink( 'https://kittens.wikimedia.test' );
|
|
$a->addExternalLink( 'https://goats.wikimedia.test' );
|
|
|
|
$a->addCategory( 'Foo', 'X' );
|
|
$a->addImage( 'Billy.jpg', '20180101000013', 'DEAD' );
|
|
|
|
$b = new ParserOutput();
|
|
$b->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
|
|
$b->addLink( Title::makeTitle( NS_TALK, 'Goats' ), 17 );
|
|
$b->addLink( new TitleValue( NS_MAIN, 'Dragons' ), 8 );
|
|
$b->addLink( new TitleValue( NS_FILE, 'Dragons.jpg' ), 28 );
|
|
|
|
$b->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Dragons' ), 108, 1108 );
|
|
$a->addTemplate( new TitleValue( NS_MAIN, 'Dragons' ), 118, 1118 );
|
|
|
|
$b->addLanguageLink( 'fr' );
|
|
$b->addLanguageLink( 'ru' );
|
|
$b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens FR', '', 'fr' ) );
|
|
$b->addInterwikiLink( new TitleValue( NS_MAIN, 'Dragons RU', '', 'ru' ) );
|
|
$b->addExternalLink( 'https://dragons.wikimedia.test' );
|
|
$b->addExternalLink( 'https://goats.wikimedia.test' );
|
|
|
|
$b->addCategory( 'Bar', 'Y' );
|
|
$b->addImage( 'Puff.jpg', '20180101000017', 'BEEF' );
|
|
|
|
yield 'all kinds of links' => [ $a, $b, [
|
|
'getLinks' => [
|
|
NS_MAIN => [
|
|
'Kittens' => 6,
|
|
'Goats' => 7,
|
|
'Dragons' => 8,
|
|
],
|
|
NS_TALK => [
|
|
'Kittens' => 16,
|
|
'Goats' => 17,
|
|
],
|
|
NS_FILE => [
|
|
'Dragons.jpg' => 28,
|
|
],
|
|
],
|
|
'getTemplates' => [
|
|
NS_MAIN => [
|
|
'Dragons' => 118,
|
|
],
|
|
NS_TEMPLATE => [
|
|
'Dragons' => 108,
|
|
'Goats' => 107,
|
|
],
|
|
],
|
|
'getTemplateIds' => [
|
|
NS_MAIN => [
|
|
'Dragons' => 1118,
|
|
],
|
|
NS_TEMPLATE => [
|
|
'Dragons' => 1108,
|
|
'Goats' => 1107,
|
|
],
|
|
],
|
|
'getLanguageLinks' => [ 'de', 'ru', 'fr' ],
|
|
'getInterwikiLinks' => [
|
|
'de' => [ 'Kittens_DE' => 1 ],
|
|
'ru' => [ 'Kittens_RU' => 1, 'Dragons_RU' => 1, ],
|
|
'fr' => [ 'Kittens_FR' => 1 ],
|
|
],
|
|
'getCategoryMap' => [ 'Foo' => 'X', 'Bar' => 'Y' ],
|
|
'getImages' => [ 'Billy.jpg' => 1, 'Puff.jpg' => 1 ],
|
|
'getFileSearchOptions' => [
|
|
'Billy.jpg' => [ 'time' => '20180101000013', 'sha1' => 'DEAD' ],
|
|
'Puff.jpg' => [ 'time' => '20180101000017', 'sha1' => 'BEEF' ],
|
|
],
|
|
'getExternalLinks' => [
|
|
'https://dragons.wikimedia.test' => 1,
|
|
'https://kittens.wikimedia.test' => 1,
|
|
'https://goats.wikimedia.test' => 1,
|
|
]
|
|
] ];
|
|
|
|
// properties ------------
|
|
$a = new ParserOutput();
|
|
|
|
$a->setPageProperty( 'foo', 'Foo!' );
|
|
$a->setPageProperty( 'bar', 'Bar!' );
|
|
|
|
$a->setExtensionData( 'foo', 'Foo!' );
|
|
$a->setExtensionData( 'bar', 'Bar!' );
|
|
$a->appendExtensionData( 'bat', 'abc' );
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$b->setPageProperty( 'zoo', 'Zoo!' );
|
|
$b->setPageProperty( 'bar', 'Barrr!' );
|
|
|
|
$b->setExtensionData( 'zoo', 'Zoo!' );
|
|
$b->setExtensionData( 'bar', 'Barrr!' );
|
|
$b->appendExtensionData( 'bat', 'xyz' );
|
|
|
|
// Note that overwriting extension data during the merge
|
|
// (as this test case does for 'bar') is deprecated and will eventually
|
|
// throw an exception. However, at the moment it is still worth
|
|
// testing this case to ensure backward compatibility. (T300981)
|
|
yield 'properties' => [ $a, $b, [
|
|
'getPageProperties' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
],
|
|
'$mExtensionData' => [
|
|
'foo' => 'Foo!',
|
|
'bar' => 'Barrr!', // overwritten
|
|
'zoo' => 'Zoo!',
|
|
// internal strategy key is exposed here because we're looking
|
|
// at the raw property value, not using getExtensionData()
|
|
'bat' => [ 'abc' => true, 'xyz' => true, '_mw-strategy' => 'union' ],
|
|
],
|
|
] ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMergeTrackingMetaDataFrom
|
|
* @covers ParserOutput::mergeTrackingMetaDataFrom
|
|
*
|
|
* @param ParserOutput $a
|
|
* @param ParserOutput $b
|
|
* @param array $expected
|
|
*/
|
|
public function testMergeTrackingMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
|
|
$a->mergeTrackingMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
|
|
// test twice, to make sure the operation is idempotent
|
|
$a->mergeTrackingMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
}
|
|
|
|
public function provideMergeInternalMetaDataFrom() {
|
|
// flags & co
|
|
$a = new ParserOutput();
|
|
|
|
$a->addWarningMsg( 'duplicate-args-warning', 'A', 'B', 'C' );
|
|
$a->addWarningMsg( 'template-loop-warning', 'D' );
|
|
|
|
$a->setOutputFlag( 'foo' );
|
|
$a->setOutputFlag( 'bar' );
|
|
|
|
$a->recordOption( 'Foo' );
|
|
$a->recordOption( 'Bar' );
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$b->addWarningMsg( 'template-equals-warning' );
|
|
$b->addWarningMsg( 'template-loop-warning', 'D' );
|
|
|
|
$b->setOutputFlag( 'zoo' );
|
|
$b->setOutputFlag( 'bar' );
|
|
|
|
$b->recordOption( 'Zoo' );
|
|
$b->recordOption( 'Bar' );
|
|
|
|
yield 'flags' => [ $a, $b, [
|
|
'getWarnings' => [
|
|
wfMessage( 'duplicate-args-warning', 'A', 'B', 'C' )->text(),
|
|
wfMessage( 'template-loop-warning', 'D' )->text(),
|
|
wfMessage( 'template-equals-warning' )->text(),
|
|
],
|
|
'$mFlags' => [ 'foo' => true, 'bar' => true, 'zoo' => true ],
|
|
'getUsedOptions' => [ 'Foo', 'Bar', 'Zoo' ],
|
|
] ];
|
|
|
|
// timestamp ------------
|
|
$a = new ParserOutput();
|
|
$a->setTimestamp( '20180101000011' );
|
|
$b = new ParserOutput();
|
|
yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setTimestamp( '20180101000011' );
|
|
yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setTimestamp( '20180101000011' );
|
|
$b = new ParserOutput();
|
|
$b->setTimestamp( '20180101000001' );
|
|
yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setTimestamp( '20180101000001' );
|
|
$b = new ParserOutput();
|
|
$b->setTimestamp( '20180101000011' );
|
|
yield 'right timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
|
|
|
|
// speculative rev id ------------
|
|
$a = new ParserOutput();
|
|
$a->setSpeculativeRevIdUsed( 9 );
|
|
$b = new ParserOutput();
|
|
yield 'only left speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$b = new ParserOutput();
|
|
$b->setSpeculativeRevIdUsed( 9 );
|
|
yield 'only right speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
|
|
|
|
$a = new ParserOutput();
|
|
$a->setSpeculativeRevIdUsed( 9 );
|
|
$b = new ParserOutput();
|
|
$b->setSpeculativeRevIdUsed( 9 );
|
|
yield 'same speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
|
|
|
|
// limit report (recursive max) ------------
|
|
$a = new ParserOutput();
|
|
|
|
$a->setLimitReportData( 'naive1', 7 );
|
|
$a->setLimitReportData( 'naive2', 27 );
|
|
|
|
$a->setLimitReportData( 'limitreport-simple1', 7 );
|
|
$a->setLimitReportData( 'limitreport-simple2', 27 );
|
|
|
|
$a->setLimitReportData( 'limitreport-pair1', [ 7, 9 ] );
|
|
$a->setLimitReportData( 'limitreport-pair2', [ 27, 29 ] );
|
|
|
|
$a->setLimitReportData( 'limitreport-more1', [ 7, 9, 1 ] );
|
|
$a->setLimitReportData( 'limitreport-more2', [ 27, 29, 21 ] );
|
|
|
|
$a->setLimitReportData( 'limitreport-only-a', 13 );
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$b->setLimitReportData( 'naive1', 17 );
|
|
$b->setLimitReportData( 'naive2', 17 );
|
|
|
|
$b->setLimitReportData( 'limitreport-simple1', 17 );
|
|
$b->setLimitReportData( 'limitreport-simple2', 17 );
|
|
|
|
$b->setLimitReportData( 'limitreport-pair1', [ 17, 19 ] );
|
|
$b->setLimitReportData( 'limitreport-pair2', [ 17, 19 ] );
|
|
|
|
$b->setLimitReportData( 'limitreport-more1', [ 17, 19, 11 ] );
|
|
$b->setLimitReportData( 'limitreport-more2', [ 17, 19, 11 ] );
|
|
|
|
$b->setLimitReportData( 'limitreport-only-b', 23 );
|
|
|
|
// first write wins
|
|
yield 'limit report' => [ $a, $b, [
|
|
'getLimitReportData' => [
|
|
'naive1' => 7,
|
|
'naive2' => 27,
|
|
'limitreport-simple1' => 7,
|
|
'limitreport-simple2' => 27,
|
|
'limitreport-pair1' => [ 7, 9 ],
|
|
'limitreport-pair2' => [ 27, 29 ],
|
|
'limitreport-more1' => [ 7, 9, 1 ],
|
|
'limitreport-more2' => [ 27, 29, 21 ],
|
|
'limitreport-only-a' => 13,
|
|
],
|
|
'getLimitReportJSData' => [
|
|
'naive1' => 7,
|
|
'naive2' => 27,
|
|
'limitreport' => [
|
|
'simple1' => 7,
|
|
'simple2' => 27,
|
|
'pair1' => [ 'value' => 7, 'limit' => 9 ],
|
|
'pair2' => [ 'value' => 27, 'limit' => 29 ],
|
|
'more1' => [ 7, 9, 1 ],
|
|
'more2' => [ 27, 29, 21 ],
|
|
'only-a' => 13,
|
|
],
|
|
],
|
|
] ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMergeInternalMetaDataFrom
|
|
* @covers ParserOutput::mergeInternalMetaDataFrom
|
|
*
|
|
* @param ParserOutput $a
|
|
* @param ParserOutput $b
|
|
* @param array $expected
|
|
*/
|
|
public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
|
|
// test twice, to make sure the operation is idempotent
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
|
|
$this->assertFieldValues( $a, $expected );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::mergeInternalMetaDataFrom
|
|
* @covers ParserOutput::getTimes
|
|
* @covers ParserOutput::resetParseStartTime
|
|
*/
|
|
public function testMergeInternalMetaDataFrom_parseStartTime() {
|
|
/** @var object $a */
|
|
$a = new ParserOutput();
|
|
$a = TestingAccessWrapper::newFromObject( $a );
|
|
|
|
$a->resetParseStartTime();
|
|
$aClocks = $a->mParseStartTime;
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $aClocks[$clock], $timestamp, $clock );
|
|
}
|
|
|
|
// try again, with times in $b also set, and later than $a's
|
|
usleep( 1234 );
|
|
|
|
/** @var object $b */
|
|
$b = new ParserOutput();
|
|
$b = TestingAccessWrapper::newFromObject( $b );
|
|
|
|
$b->resetParseStartTime();
|
|
|
|
$bClocks = $b->mParseStartTime;
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $aClocks[$clock], $timestamp, $clock );
|
|
$this->assertLessThanOrEqual( $bClocks[$clock], $timestamp, $clock );
|
|
}
|
|
|
|
// try again, with $a's times being later
|
|
usleep( 1234 );
|
|
$a->resetParseStartTime();
|
|
$aClocks = $a->mParseStartTime;
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $bClocks[$clock], $timestamp, $clock );
|
|
$this->assertLessThanOrEqual( $aClocks[$clock], $timestamp, $clock );
|
|
}
|
|
|
|
// try again, with no times in $a set
|
|
$a = new ParserOutput();
|
|
$a = TestingAccessWrapper::newFromObject( $a );
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mParseStartTime;
|
|
|
|
foreach ( $mergedClocks as $clock => $timestamp ) {
|
|
$this->assertSame( $bClocks[$clock], $timestamp, $clock );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::mergeInternalMetaDataFrom
|
|
* @covers ParserOutput::getTimes
|
|
* @covers ParserOutput::resetParseStartTime
|
|
* @covers ParserOutput::recordTimeProfile
|
|
* @covers ParserOutput::getTimeProfile
|
|
*/
|
|
public function testMergeInternalMetaDataFrom_timeProfile() {
|
|
/** @var object $a */
|
|
$a = new ParserOutput();
|
|
$a = TestingAccessWrapper::newFromObject( $a );
|
|
|
|
$a->resetParseStartTime();
|
|
usleep( 1234 );
|
|
$a->recordTimeProfile();
|
|
|
|
$aClocks = $a->mTimeProfile;
|
|
|
|
// make sure a second call to recordTimeProfile has no effect
|
|
usleep( 1234 );
|
|
$a->recordTimeProfile();
|
|
|
|
foreach ( $aClocks as $clock => $duration ) {
|
|
$this->assertNotNull( $duration );
|
|
$this->assertGreaterThan( 0, $duration );
|
|
$this->assertSame( $aClocks[$clock], $a->getTimeProfile( $clock ) );
|
|
}
|
|
|
|
$b = new ParserOutput();
|
|
|
|
$a->mergeInternalMetaDataFrom( $b );
|
|
$mergedClocks = $a->mTimeProfile;
|
|
|
|
foreach ( $mergedClocks as $clock => $duration ) {
|
|
$this->assertSame( $aClocks[$clock], $duration, $clock );
|
|
}
|
|
|
|
// try again, with times in $b also set, and later than $a's
|
|
$b->resetParseStartTime();
|
|
usleep( 1234 );
|
|
$b->recordTimeProfile();
|
|
|
|
$b = TestingAccessWrapper::newFromObject( $b );
|
|
$bClocks = $b->mTimeProfile;
|
|
|
|
$a->mergeInternalMetaDataFrom( $b->object );
|
|
$mergedClocks = $a->mTimeProfile;
|
|
|
|
foreach ( $mergedClocks as $clock => $duration ) {
|
|
$this->assertGreaterThanOrEqual( $aClocks[$clock], $duration, $clock );
|
|
$this->assertGreaterThanOrEqual( $bClocks[$clock], $duration, $clock );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::getCacheTime
|
|
* @covers ParserOutput::setCacheTime
|
|
*/
|
|
public function testGetCacheTime() {
|
|
$clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
|
|
MWTimestamp::setFakeTime( static function () use ( &$clock ) {
|
|
return $clock++;
|
|
} );
|
|
|
|
$po = new ParserOutput();
|
|
$time = $po->getCacheTime();
|
|
|
|
// Use current (fake) time per default. Ignore the last digit.
|
|
// Subsequent calls must yield the exact same timestamp as the first.
|
|
$this->assertStringStartsWith( '2010010100000', $time );
|
|
$this->assertSame( $time, $po->getCacheTime() );
|
|
|
|
// After setting, the getter must return the time that was set.
|
|
$time = '20110606112233';
|
|
$po->setCacheTime( $time );
|
|
$this->assertSame( $time, $po->getCacheTime() );
|
|
}
|
|
|
|
/**
|
|
* @covers ParserOutput::addExtraCSPScriptSrc
|
|
* @covers ParserOutput::addExtraCSPDefaultSrc
|
|
* @covers ParserOutput::addExtraCSPStyleSrc
|
|
* @covers ParserOutput::getExtraCSPScriptSrcs
|
|
* @covers ParserOutput::getExtraCSPDefaultSrcs
|
|
* @covers ParserOutput::getExtraCSPStyleSrcs
|
|
*/
|
|
public function testCSPSources() {
|
|
$po = new ParserOutput;
|
|
|
|
$this->assertEquals( [], $po->getExtraCSPScriptSrcs(), 'empty Script' );
|
|
$this->assertEquals( [], $po->getExtraCSPStyleSrcs(), 'empty Style' );
|
|
$this->assertEquals( [], $po->getExtraCSPDefaultSrcs(), 'empty Default' );
|
|
|
|
$po->addExtraCSPScriptSrc( 'foo.com' );
|
|
$po->addExtraCSPScriptSrc( 'bar.com' );
|
|
$po->addExtraCSPDefaultSrc( 'baz.com' );
|
|
$po->addExtraCSPStyleSrc( 'fred.com' );
|
|
$po->addExtraCSPStyleSrc( 'xyzzy.com' );
|
|
|
|
$this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs(), 'Script' );
|
|
$this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs(), 'Default' );
|
|
$this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs(), 'Style' );
|
|
}
|
|
|
|
public function testOutputStrings() {
|
|
$po = new ParserOutput;
|
|
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
|
|
$this->assertEquals( [], $po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
|
|
|
|
$this->assertEquals( [], $po->getModules() );
|
|
$this->assertEquals( [], $po->getModuleStyles() );
|
|
$this->assertEquals( [], $po->getExtraCSPScriptSrcs() );
|
|
$this->assertEquals( [], $po->getExtraCSPStyleSrcs() );
|
|
$this->assertEquals( [], $po->getExtraCSPDefaultSrcs() );
|
|
|
|
$po->appendOutputStrings( ParserOutputStringSets::MODULE, [ 'a' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::MODULE_STYLE, [ 'b' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC, [ 'foo.com', 'bar.com' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC, [ 'baz.com' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'fred.com' ] );
|
|
$po->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC, [ 'xyzzy.com' ] );
|
|
|
|
$this->assertEquals( [ 'a' ], $po->getOutputStrings( ParserOutputStringSets::MODULE ) );
|
|
$this->assertEquals( [ 'b' ], $po->getOutputStrings( ParserOutputStringSets::MODULE_STYLE ) );
|
|
$this->assertEquals( [ 'foo.com', 'bar.com' ],
|
|
$po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_SCRIPT_SRC ) );
|
|
$this->assertEquals( [ 'baz.com' ],
|
|
$po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC ) );
|
|
$this->assertEquals( [ 'fred.com', 'xyzzy.com' ],
|
|
$po->getOutputStrings( ParserOutputStringSets::EXTRA_CSP_STYLE_SRC ) );
|
|
|
|
$this->assertEquals( [ 'a' ], $po->getModules() );
|
|
$this->assertEquals( [ 'b' ], $po->getModuleStyles() );
|
|
$this->assertEquals( [ 'foo.com', 'bar.com' ], $po->getExtraCSPScriptSrcs() );
|
|
$this->assertEquals( [ 'baz.com' ], $po->getExtraCSPDefaultSrcs() );
|
|
$this->assertEquals( [ 'fred.com', 'xyzzy.com' ], $po->getExtraCSPStyleSrcs() );
|
|
}
|
|
}
|