wiki.techinc.nl/tests/phpunit/includes/parser/ParserOutputTest.php
Isabelle Hurbain-Palatin a3cf629d2f Remove ParserOutput::getText() calls from core (direct pipeline)
This is the second patch of a series of patches to remove
ParserOutput::getText() calls from core. This series of patches should
be functionally equivalent to I2b4bcddb234f10fd8592570cb0496adf3271328e.

This patch replaces the calls to getText where the legacy parser is
called directly by creating a pipeline and invoking it on the generated.
These should probably eventually use the Content framework to generate
output instead of using Parser directly (T371008), which will also allow
them to transparently support Parsoid.

Bug: T293512
Change-Id: I45951a49e57a8031887ee6e4546335141d231c18
2024-08-23 18:15:00 +02:00

1473 lines
50 KiB
PHP

<?php
namespace MediaWiki\Tests\Parser;
use LogicException;
use MediaWiki\Context\RequestContext;
use MediaWiki\Debug\MWDebug;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Parser\ParserOutputFlags;
use MediaWiki\Parser\ParserOutputStringSets;
use MediaWiki\Title\Title;
use MediaWiki\Title\TitleValue;
use MediaWiki\Utils\MWTimestamp;
use MediaWikiLangTestCase;
use ParserOptions;
use Wikimedia\Bcp47Code\Bcp47CodeValue;
use Wikimedia\Parsoid\Core\SectionMetadata;
use Wikimedia\Parsoid\Core\TOCData;
use Wikimedia\TestingAccessWrapper;
use Wikimedia\Tests\SerializationTestTrait;
/**
* @covers \MediaWiki\Parser\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
);
}
/**
* Overrides SerializationTestTrait::getClassToTest
* @return string
*/
public static function getClassToTest(): string {
return ParserOutput::class;
}
/**
* Overrides SerializationTestTrait::getSerializedDataPath
* @return string
*/
public static function getSerializedDataPath(): string {
return __DIR__ . '/../../data/ParserCache';
}
/**
* Overrides SerializationTestTrait::getTestInstancesAndAssertions
* @return array
*/
public static function getTestInstancesAndAssertions(): array {
return ParserCacheSerializationTestCases::getParserOutputTestCases();
}
/**
* Overrides SerializationTestTrait::getSupportedSerializationFormats
* @return array
*/
public static function getSupportedSerializationFormats(): array {
return ParserCacheSerializationTestCases::getSupportedSerializationFormats(
self::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 \MediaWiki\Parser\ParserOutput::isLinkInternal
*/
public function testIsLinkInternal( $shouldMatch, $server, $url ) {
$this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
}
/**
* @covers \MediaWiki\Parser\ParserOutput::appendJsConfigVar
* @covers \MediaWiki\Parser\ParserOutput::setJsConfigVar
* @covers \MediaWiki\Parser\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 \MediaWiki\Parser\ParserOutput::appendExtensionData
* @covers \MediaWiki\Parser\ParserOutput::setExtensionData
* @covers \MediaWiki\Parser\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 \MediaWiki\Parser\ParserOutput::setPageProperty
* @covers \MediaWiki\Parser\ParserOutput::setNumericPageProperty
* @covers \MediaWiki\Parser\ParserOutput::setUnsortedPageProperty
* @covers \MediaWiki\Parser\ParserOutput::getPageProperty
* @covers \MediaWiki\Parser\ParserOutput::unsetPageProperty
* @covers \MediaWiki\Parser\ParserOutput::getPageProperties
* @dataProvider providePageProperties
*/
public function testPageProperties( string $setPageProperty, $value1, $value2 ) {
$po = new ParserOutput();
$po->$setPageProperty( 'foo', $value1 );
$properties = $po->getPageProperties();
$this->assertSame( $value1, $po->getPageProperty( 'foo' ) );
$this->assertSame( $value1, $properties['foo'] );
$po->$setPageProperty( 'foo', $value2 );
$properties = $po->getPageProperties();
$this->assertSame( $value2, $po->getPageProperty( 'foo' ) );
$this->assertSame( $value2, $properties['foo'] );
$po->unsetPageProperty( 'foo' );
$properties = $po->getPageProperties();
$this->assertSame( null, $po->getPageProperty( 'foo' ) );
$this->assertArrayNotHasKey( 'foo', $properties );
}
public static function providePageProperties() {
yield 'Unsorted' => [ 'setUnsortedPageProperty', 'val', 'second val' ];
yield 'Numeric' => [ 'setNumericPageProperty', 42, 3.14 ];
yield 'Unsorted (old style)' => [ 'setPageProperty', 'val', 'second val' ];
yield 'Numeric (old style)' => [ 'setPageProperty', 123, 456 ];
}
/**
* @covers \MediaWiki\Parser\ParserOutput::setNumericPageProperty
*/
public function testNumericPageProperties() {
$po = new ParserOutput();
$po->setNumericPageProperty( 'foo', '123' );
$properties = $po->getPageProperties();
$this->assertSame( 123, $po->getPageProperty( 'foo' ) );
$this->assertSame( 123, $properties['foo'] );
}
/**
* @covers \MediaWiki\Parser\ParserOutput::setUnsortedPageProperty
*/
public function testUnsortedPageProperties() {
$po = new ParserOutput();
$po->setUnsortedPageProperty( 'foo', 123 );
$properties = $po->getPageProperties();
$this->assertSame( '123', $po->getPageProperty( 'foo' ) );
$this->assertSame( '123', $properties['foo'] );
}
/**
* @covers \MediaWiki\Parser\ParserOutput::setLanguage
* @covers \MediaWiki\Parser\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 \MediaWiki\Parser\ParserOutput::getWrapperDivClass
* @covers \MediaWiki\Parser\ParserOutput::addWrapperDivClass
* @covers \MediaWiki\Parser\ParserOutput::clearWrapperDivClass
* @covers \MediaWiki\Parser\ParserOutput::getText
*/
public function testWrapperDivClass() {
$po = new ParserOutput();
$opts = ParserOptions::newFromAnon();
$pipeline = MediaWikiServices::getInstance()->getDefaultOutputPipeline();
$po->setRawText( 'Kittens' );
$this->assertStringContainsString( 'Kittens', $po->getText() );
$this->assertStringNotContainsString( '<div', $po->getText() );
$this->assertSame( 'Kittens', $po->getRawText() );
$po->addWrapperDivClass( 'foo' );
$text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
$this->assertStringContainsString( 'Kittens', $text );
$this->assertStringContainsString( '<div', $text );
$this->assertStringContainsString( 'class="mw-content-ltr foo"', $text );
$po->addWrapperDivClass( 'bar' );
$text = $pipeline->run( $po, $opts, [] )->getContentHolderText();
$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 = $pipeline->run( $po, $opts, [ 'unwrap' => true ] )->getContentHolderText();
$this->assertStringContainsString( 'Kittens', $text );
$this->assertStringNotContainsString( '<div', $text );
$this->assertStringNotContainsString( 'class="', $text );
$text = $pipeline->run( $po, $opts, [ 'wrapperDivClass' => '' ] )->getContentHolderText();
$this->assertStringContainsString( 'Kittens', $text );
$this->assertStringNotContainsString( '<div', $text );
$this->assertStringNotContainsString( 'class="', $text );
$text = $pipeline->run( $po, $opts, [ 'wrapperDivClass' => 'xyzzy' ] )->getContentHolderText();
$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 = $pipeline->run( $po, $opts, [] )->getContentHolderText();
$this->assertStringContainsString( 'Kittens', $text );
$this->assertStringNotContainsString( '<div', $text );
$this->assertStringNotContainsString( 'class="', $text );
}
/**
* This test aims at being replaced by its version in DefaultOutputPipelineFactoryTest when
* ParserOutput::getText gets deprecated.
* @covers \MediaWiki\Parser\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::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" />
<div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></div>
<p>One
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></div>
<p>Two
</p>
<div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
<p>Two point one
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></div>
<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>
<div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></div>
<p>One
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></div>
<p>Two
</p>
<div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
<p>Two point one
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></div>
<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>
<div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2></div>
<p>One
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2></div>
<p>Two
</p>
<div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
<p>Two point one
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2></div>
<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>
<div class="mw-heading mw-heading2"><h2 id="Section_1">Section 1</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></div>
<p>One
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_2">Section 2</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></div>
<p>Two
</p>
<div class="mw-heading mw-heading3"><h3 id="Section_2.1">Section 2.1</h3></div>
<p>Two point one
</p>
<div class="mw-heading mw-heading2"><h2 id="Section_3">Section 3</h2><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></div>
<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 \MediaWiki\Parser\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->setRawText( '' );
$this->assertTrue( $po->hasText() );
$po = new ParserOutput( 'foo' );
$po->setRawText( null );
$this->assertFalse( $po->hasText() );
}
/**
* This test aims at being replaced by its version in DefaultOutputPipelineFactoryTest when
* ParserOutput::getText gets deprecated.
* @covers \MediaWiki\Parser\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 \MediaWiki\Parser\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, [
'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 \MediaWiki\Parser\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['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 \MediaWiki\Parser\ParserOutput::addLink
* @covers \MediaWiki\Parser\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( new TitleValue( NS_MAIN, 'de' ) );
$a->addLanguageLink( new TitleValue( NS_MAIN, '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( new TitleValue( NS_FILE, '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( new TitleValue( NS_MAIN, 'fr' ) );
$b->addLanguageLink( new TitleValue( NS_MAIN, '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( new TitleValue( NS_FILE, '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 \MediaWiki\Parser\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() {
MWDebug::filterDeprecationForTest( '/^CacheTime::setCacheTime called with -1 as an argument/' );
// 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' ],
] ];
// cache time
$someTime = "20240207202040";
$someLaterTime = "20240207202112";
$a = new ParserOutput();
$a->setCacheTime( $someTime );
$b = new ParserOutput();
yield 'only left cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
$a = new ParserOutput();
$b = new ParserOutput();
$b->setCacheTime( $someTime );
yield 'only right cache time' => [ $a, $b, [ 'getCacheTime' => $someTime ] ];
$a = new ParserOutput();
$b = new ParserOutput();
$a->setCacheTime( $someLaterTime );
$b->setCacheTime( $someTime );
yield 'left has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
$a = new ParserOutput();
$b = new ParserOutput();
$a->setCacheTime( $someTime );
$b->setCacheTime( $someLaterTime );
yield 'right has later cache time' => [ $a, $b, [ 'getCacheTime' => $someLaterTime ] ];
$a = new ParserOutput();
$b = new ParserOutput();
$a->setCacheTime( -1 );
$b->setCacheTime( $someTime );
yield 'left is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
$a = new ParserOutput();
$b = new ParserOutput();
$a->setCacheTime( $someTime );
$b->setCacheTime( -1 );
yield 'right is uncacheable' => [ $a, $b, [ 'getCacheTime' => "-1" ] ];
// timestamp ------------
$a = new ParserOutput();
$a->setRevisionTimestamp( '20180101000011' );
$b = new ParserOutput();
yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
$a = new ParserOutput();
$b = new ParserOutput();
$b->setRevisionTimestamp( '20180101000011' );
yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
$a = new ParserOutput();
$a->setRevisionTimestamp( '20180101000011' );
$b = new ParserOutput();
$b->setRevisionTimestamp( '20180101000001' );
yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
$a = new ParserOutput();
$a->setRevisionTimestamp( '20180101000001' );
$b = new ParserOutput();
$b->setRevisionTimestamp( '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,
],
],
] ];
MWDebug::clearDeprecationFilters();
}
/**
* @dataProvider provideMergeInternalMetaDataFrom
* @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
*
* @param ParserOutput $a
* @param ParserOutput $b
* @param array $expected
*/
public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
$this->filterDeprecated( '/^CacheTime::setCacheTime called with -1 as an argument/' );
$a->mergeInternalMetaDataFrom( $b );
$this->assertFieldValues( $a, $expected );
// test twice, to make sure the operation is idempotent
$a->mergeInternalMetaDataFrom( $b );
$this->assertFieldValues( $a, $expected );
}
/**
* @covers \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
* @covers \MediaWiki\Parser\ParserOutput::getTimes
* @covers \MediaWiki\Parser\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 \MediaWiki\Parser\ParserOutput::mergeInternalMetaDataFrom
* @covers \MediaWiki\Parser\ParserOutput::getTimes
* @covers \MediaWiki\Parser\ParserOutput::resetParseStartTime
* @covers \MediaWiki\Parser\ParserOutput::recordTimeProfile
* @covers \MediaWiki\Parser\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 \MediaWiki\Parser\ParserOutput::getCacheTime
* @covers \MediaWiki\Parser\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 by 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 \MediaWiki\Parser\ParserOutput::addExtraCSPScriptSrc
* @covers \MediaWiki\Parser\ParserOutput::addExtraCSPDefaultSrc
* @covers \MediaWiki\Parser\ParserOutput::addExtraCSPStyleSrc
* @covers \MediaWiki\Parser\ParserOutput::getExtraCSPScriptSrcs
* @covers \MediaWiki\Parser\ParserOutput::getExtraCSPDefaultSrcs
* @covers \MediaWiki\Parser\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() );
}
/**
* @covers \MediaWiki\Parser\ParserOutput::getCacheTime()
* @covers \MediaWiki\Parser\ParserOutput::setCacheTime()
*/
public function testCacheTime() {
$po = new ParserOutput();
// Should not have a cache time yet
$this->assertFalse( $po->hasCacheTime() );
// But calling ::get assigns a cache time
$po->getCacheTime();
$this->assertTrue( $po->hasCacheTime() );
// Reset cache time
$po->setCacheTime( "20240207202040" );
$this->assertSame( "20240207202040", $po->getCacheTime() );
}
/**
* @covers \MediaWiki\Parser\ParserOutput::getRenderId()
* @covers \MediaWiki\Parser\ParserOutput::setRenderId()
*/
public function testRenderId() {
$po = new ParserOutput();
// Should be null when unset
$this->assertNull( $po->getRenderId() );
// Sanity check for setter and getter
$po->setRenderId( "TestRenderId" );
$this->assertEquals( "TestRenderId", $po->getRenderId() );
}
/**
* @covers \MediaWiki\Parser\ParserOutput::getRenderId()
*/
public function testRenderIdBackCompat() {
$po = new ParserOutput();
// Parser cache used to contain extension data under a different name
$po->setExtensionData( 'parsoid-render-id', "1234/LegacyRenderId" );
$this->assertEquals( "LegacyRenderId", $po->getRenderId() );
}
public function testSetFromParserOptions() {
// parser output set from canonical parser options
$pOptions = ParserOptions::newFromAnon();
$pOutput = new ParserOutput;
$pOutput->setFromParserOptions( $pOptions );
$this->assertSame( 'mw-parser-output', $pOutput->getWrapperDivClass() );
$this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
$this->assertTrue( $pOutput->isCacheable() );
$this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
$this->assertFalse( $pOutput->getOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS ) );
// set the various parser options and verify in parser output
$pOptions->setWrapOutputClass( 'test-wrapper' );
$pOptions->setIsPreview( true );
$pOptions->setSuppressSectionEditLinks();
$pOptions->setCollapsibleSections();
$pOutput = new ParserOutput;
$pOutput->setFromParserOptions( $pOptions );
$this->assertEquals( 'test-wrapper', $pOutput->getWrapperDivClass() );
$this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::IS_PREVIEW ) );
$this->assertFalse( $pOutput->isCacheable() );
$this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::NO_SECTION_EDIT_LINKS ) );
$this->assertTrue( $pOutput->getOutputFlag( ParserOutputFlags::COLLAPSIBLE_SECTIONS ) );
}
}