ParserOutput::getCacheTime should stay the same after the first call.
[lhc/web/wiklou.git] / tests / phpunit / includes / parser / ParserOutputTest.php
1 <?php
2 use Wikimedia\TestingAccessWrapper;
3
4 /**
5 * @group Database
6 * ^--- trigger DB shadowing because we are using Title magic
7 */
8 class ParserOutputTest extends MediaWikiLangTestCase {
9
10 public static function provideIsLinkInternal() {
11 return [
12 // Different domains
13 [ false, 'http://example.org', 'http://mediawiki.org' ],
14 // Same domains
15 [ true, 'http://example.org', 'http://example.org' ],
16 [ true, 'https://example.org', 'https://example.org' ],
17 [ true, '//example.org', '//example.org' ],
18 // Same domain different cases
19 [ true, 'http://example.org', 'http://EXAMPLE.ORG' ],
20 // Paths, queries, and fragments are not relevant
21 [ true, 'http://example.org', 'http://example.org/wiki/Main_Page' ],
22 [ true, 'http://example.org', 'http://example.org?my=query' ],
23 [ true, 'http://example.org', 'http://example.org#its-a-fragment' ],
24 // Different protocols
25 [ false, 'http://example.org', 'https://example.org' ],
26 [ false, 'https://example.org', 'http://example.org' ],
27 // Protocol relative servers always match http and https links
28 [ true, '//example.org', 'http://example.org' ],
29 [ true, '//example.org', 'https://example.org' ],
30 // But they don't match strange things like this
31 [ false, '//example.org', 'irc://example.org' ],
32 ];
33 }
34
35 public function tearDown() {
36 MWTimestamp::setFakeTime( false );
37
38 parent::tearDown();
39 }
40
41 /**
42 * Test to make sure ParserOutput::isLinkInternal behaves properly
43 * @dataProvider provideIsLinkInternal
44 * @covers ParserOutput::isLinkInternal
45 */
46 public function testIsLinkInternal( $shouldMatch, $server, $url ) {
47 $this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
48 }
49
50 /**
51 * @covers ParserOutput::setExtensionData
52 * @covers ParserOutput::getExtensionData
53 */
54 public function testExtensionData() {
55 $po = new ParserOutput();
56
57 $po->setExtensionData( "one", "Foo" );
58
59 $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
60 $this->assertNull( $po->getExtensionData( "spam" ) );
61
62 $po->setExtensionData( "two", "Bar" );
63 $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
64 $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
65
66 $po->setExtensionData( "one", null );
67 $this->assertNull( $po->getExtensionData( "one" ) );
68 $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
69 }
70
71 /**
72 * @covers ParserOutput::setProperty
73 * @covers ParserOutput::getProperty
74 * @covers ParserOutput::unsetProperty
75 * @covers ParserOutput::getProperties
76 */
77 public function testProperties() {
78 $po = new ParserOutput();
79
80 $po->setProperty( 'foo', 'val' );
81
82 $properties = $po->getProperties();
83 $this->assertEquals( $po->getProperty( 'foo' ), 'val' );
84 $this->assertEquals( $properties['foo'], 'val' );
85
86 $po->setProperty( 'foo', 'second val' );
87
88 $properties = $po->getProperties();
89 $this->assertEquals( $po->getProperty( 'foo' ), 'second val' );
90 $this->assertEquals( $properties['foo'], 'second val' );
91
92 $po->unsetProperty( 'foo' );
93
94 $properties = $po->getProperties();
95 $this->assertEquals( $po->getProperty( 'foo' ), false );
96 $this->assertArrayNotHasKey( 'foo', $properties );
97 }
98
99 /**
100 * @covers ParserOutput::getWrapperDivClass
101 * @covers ParserOutput::addWrapperDivClass
102 * @covers ParserOutput::clearWrapperDivClass
103 * @covers ParserOutput::getText
104 */
105 public function testWrapperDivClass() {
106 $po = new ParserOutput();
107
108 $po->setText( 'Kittens' );
109 $this->assertContains( 'Kittens', $po->getText() );
110 $this->assertNotContains( '<div', $po->getText() );
111 $this->assertSame( 'Kittens', $po->getRawText() );
112
113 $po->addWrapperDivClass( 'foo' );
114 $text = $po->getText();
115 $this->assertContains( 'Kittens', $text );
116 $this->assertContains( '<div', $text );
117 $this->assertContains( 'class="foo"', $text );
118
119 $po->addWrapperDivClass( 'bar' );
120 $text = $po->getText();
121 $this->assertContains( 'Kittens', $text );
122 $this->assertContains( '<div', $text );
123 $this->assertContains( 'class="foo bar"', $text );
124
125 $po->addWrapperDivClass( 'bar' ); // second time does nothing, no "foo bar bar".
126 $text = $po->getText( [ 'unwrap' => true ] );
127 $this->assertContains( 'Kittens', $text );
128 $this->assertNotContains( '<div', $text );
129 $this->assertNotContains( 'class="foo bar"', $text );
130
131 $text = $po->getText( [ 'wrapperDivClass' => '' ] );
132 $this->assertContains( 'Kittens', $text );
133 $this->assertNotContains( '<div', $text );
134 $this->assertNotContains( 'class="foo bar"', $text );
135
136 $text = $po->getText( [ 'wrapperDivClass' => 'xyzzy' ] );
137 $this->assertContains( 'Kittens', $text );
138 $this->assertContains( '<div', $text );
139 $this->assertContains( 'class="xyzzy"', $text );
140 $this->assertNotContains( 'class="foo bar"', $text );
141
142 $text = $po->getRawText();
143 $this->assertSame( 'Kittens', $text );
144
145 $po->clearWrapperDivClass();
146 $text = $po->getText();
147 $this->assertContains( 'Kittens', $text );
148 $this->assertNotContains( '<div', $text );
149 $this->assertNotContains( 'class="foo bar"', $text );
150 }
151
152 public function testT203716() {
153 // simulate extra wrapping from old parser cache
154 $out = new ParserOutput( '<div class="mw-parser-output">Foo</div>' );
155 $out = unserialize( serialize( $out ) );
156
157 $plainText = $out->getText( [ 'unwrap' => true ] );
158 $wrappedText = $out->getText( [ 'unwrap' => false ] );
159 $wrappedText2 = $out->getText( [ 'wrapperDivClass' => 'mw-parser-output' ] );
160
161 $this->assertNotContains( '<div', $plainText );
162 $this->assertContains( '<div', $wrappedText );
163 $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText );
164 $this->assertContains( '<div', $wrappedText2 );
165 $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText2 );
166
167 // simulate ParserOuput creation by new parser code
168 $out = new ParserOutput( 'Foo' );
169 $out->addWrapperDivClass( 'mw-parser-outout' );
170 $out = unserialize( serialize( $out ) );
171
172 $plainText = $out->getText( [ 'unwrap' => true ] );
173 $wrappedText = $out->getText( [ 'unwrap' => false ] );
174 $wrappedText2 = $out->getText( [ 'wrapperDivClass' => 'mw-parser-output' ] );
175
176 $this->assertNotContains( '<div', $plainText );
177 $this->assertContains( '<div', $wrappedText );
178 $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText );
179 $this->assertContains( '<div', $wrappedText2 );
180 $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText2 );
181 }
182
183 /**
184 * @covers ParserOutput::getText
185 * @dataProvider provideGetText
186 * @param array $options Options to getText()
187 * @param string $text Parser text
188 * @param string $expect Expected output
189 */
190 public function testGetText( $options, $text, $expect ) {
191 $this->setMwGlobals( [
192 'wgArticlePath' => '/wiki/$1',
193 'wgScriptPath' => '/w',
194 'wgScript' => '/w/index.php',
195 ] );
196
197 $po = new ParserOutput( $text );
198 $actual = $po->getText( $options );
199 $this->assertSame( $expect, $actual );
200 }
201
202 public static function provideGetText() {
203 // phpcs:disable Generic.Files.LineLength
204 $text = <<<EOF
205 <p>Test document.
206 </p>
207 <mw:toc><div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
208 <ul>
209 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
210 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
211 <ul>
212 <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>
213 </ul>
214 </li>
215 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
216 </ul>
217 </div>
218 </mw:toc>
219 <h2><span class="mw-headline" id="Section_1">Section 1</span><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></h2>
220 <p>One
221 </p>
222 <h2><span class="mw-headline" id="Section_2">Section 2</span><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></h2>
223 <p>Two
224 </p>
225 <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><mw:editsection page="Test Page" section="3">Section 2.1</mw:editsection></h3>
226 <p>Two point one
227 </p>
228 <h2><span class="mw-headline" id="Section_3">Section 3</span><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></h2>
229 <p>Three
230 </p>
231 EOF;
232
233 $dedupText = <<<EOF
234 <p>This is a test document.</p>
235 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
236 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
237 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
238 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
239 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
240 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
241 <style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
242 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
243 <style>.Duplicate1 {}</style>
244 EOF;
245
246 return [
247 'No options' => [
248 [], $text, <<<EOF
249 <p>Test document.
250 </p>
251 <div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
252 <ul>
253 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
254 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
255 <ul>
256 <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>
257 </ul>
258 </li>
259 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
260 </ul>
261 </div>
262
263 <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&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
264 <p>One
265 </p>
266 <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&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
267 <p>Two
268 </p>
269 <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=3" title="Edit section: Section 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
270 <p>Two point one
271 </p>
272 <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&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
273 <p>Three
274 </p>
275 EOF
276 ],
277 'Disable section edit links' => [
278 [ 'enableSectionEditLinks' => false ], $text, <<<EOF
279 <p>Test document.
280 </p>
281 <div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
282 <ul>
283 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
284 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
285 <ul>
286 <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>
287 </ul>
288 </li>
289 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
290 </ul>
291 </div>
292
293 <h2><span class="mw-headline" id="Section_1">Section 1</span></h2>
294 <p>One
295 </p>
296 <h2><span class="mw-headline" id="Section_2">Section 2</span></h2>
297 <p>Two
298 </p>
299 <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
300 <p>Two point one
301 </p>
302 <h2><span class="mw-headline" id="Section_3">Section 3</span></h2>
303 <p>Three
304 </p>
305 EOF
306 ],
307 'Disable TOC, but wrap' => [
308 [ 'allowTOC' => false, 'wrapperDivClass' => 'mw-parser-output' ], $text, <<<EOF
309 <div class="mw-parser-output"><p>Test document.
310 </p>
311
312 <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&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
313 <p>One
314 </p>
315 <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&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
316 <p>Two
317 </p>
318 <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=3" title="Edit section: Section 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
319 <p>Two point one
320 </p>
321 <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&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
322 <p>Three
323 </p></div>
324 EOF
325 ],
326 'Style deduplication' => [
327 [], $dedupText, <<<EOF
328 <p>This is a test document.</p>
329 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
330 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
331 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
332 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
333 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2"/>
334 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
335 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
336 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
337 <style>.Duplicate1 {}</style>
338 EOF
339 ],
340 'Style deduplication disabled' => [
341 [ 'deduplicateStyles' => false ], $dedupText, $dedupText
342 ],
343 ];
344 // phpcs:enable
345 }
346
347 /**
348 * @covers ParserOutput::hasText
349 */
350 public function testHasText() {
351 $po = new ParserOutput();
352 $this->assertTrue( $po->hasText() );
353
354 $po = new ParserOutput( null );
355 $this->assertFalse( $po->hasText() );
356
357 $po = new ParserOutput( '' );
358 $this->assertTrue( $po->hasText() );
359
360 $po = new ParserOutput( null );
361 $po->setText( '' );
362 $this->assertTrue( $po->hasText() );
363 }
364
365 /**
366 * @covers ParserOutput::getText
367 */
368 public function testGetText_failsIfNoText() {
369 $po = new ParserOutput( null );
370
371 $this->setExpectedException( LogicException::class );
372 $po->getText();
373 }
374
375 /**
376 * @covers ParserOutput::getRawText
377 */
378 public function testGetRawText_failsIfNoText() {
379 $po = new ParserOutput( null );
380
381 $this->setExpectedException( LogicException::class );
382 $po->getRawText();
383 }
384
385 public function provideMergeHtmlMetaDataFrom() {
386 // title text ------------
387 $a = new ParserOutput();
388 $a->setTitleText( 'X' );
389 $b = new ParserOutput();
390 yield 'only left title text' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
391
392 $a = new ParserOutput();
393 $b = new ParserOutput();
394 $b->setTitleText( 'Y' );
395 yield 'only right title text' => [ $a, $b, [ 'getTitleText' => 'Y' ] ];
396
397 $a = new ParserOutput();
398 $a->setTitleText( 'X' );
399 $b = new ParserOutput();
400 $b->setTitleText( 'Y' );
401 yield 'left title text wins' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
402
403 // index policy ------------
404 $a = new ParserOutput();
405 $a->setIndexPolicy( 'index' );
406 $b = new ParserOutput();
407 yield 'only left index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
408
409 $a = new ParserOutput();
410 $b = new ParserOutput();
411 $b->setIndexPolicy( 'index' );
412 yield 'only right index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
413
414 $a = new ParserOutput();
415 $a->setIndexPolicy( 'noindex' );
416 $b = new ParserOutput();
417 $b->setIndexPolicy( 'index' );
418 yield 'left noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
419
420 $a = new ParserOutput();
421 $a->setIndexPolicy( 'index' );
422 $b = new ParserOutput();
423 $b->setIndexPolicy( 'noindex' );
424 yield 'right noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
425
426 // head items and friends ------------
427 $a = new ParserOutput();
428 $a->addHeadItem( '<foo1>' );
429 $a->addHeadItem( '<bar1>', 'bar' );
430 $a->addModules( 'test-module-a' );
431 $a->addModuleScripts( 'test-module-script-a' );
432 $a->addModuleStyles( 'test-module-styles-a' );
433 $b->addJsConfigVars( 'test-config-var-a', 'a' );
434
435 $b = new ParserOutput();
436 $b->setIndexPolicy( 'noindex' );
437 $b->addHeadItem( '<foo2>' );
438 $b->addHeadItem( '<bar2>', 'bar' );
439 $b->addModules( 'test-module-b' );
440 $b->addModuleScripts( 'test-module-script-b' );
441 $b->addModuleStyles( 'test-module-styles-b' );
442 $b->addJsConfigVars( 'test-config-var-b', 'b' );
443 $b->addJsConfigVars( 'test-config-var-a', 'X' );
444
445 yield 'head items and friends' => [ $a, $b, [
446 'getHeadItems' => [
447 '<foo1>',
448 '<foo2>',
449 'bar' => '<bar2>', // overwritten
450 ],
451 'getModules' => [
452 'test-module-a',
453 'test-module-b',
454 ],
455 'getModuleScripts' => [
456 'test-module-script-a',
457 'test-module-script-b',
458 ],
459 'getModuleStyles' => [
460 'test-module-styles-a',
461 'test-module-styles-b',
462 ],
463 'getJsConfigVars' => [
464 'test-config-var-a' => 'X', // overwritten
465 'test-config-var-b' => 'b',
466 ],
467 ] ];
468
469 // TOC ------------
470 $a = new ParserOutput();
471 $a->setTOCHTML( '<p>TOC A</p>' );
472 $a->setSections( [ [ 'fromtitle' => 'A1' ], [ 'fromtitle' => 'A2' ] ] );
473
474 $b = new ParserOutput();
475 $b->setTOCHTML( '<p>TOC B</p>' );
476 $b->setSections( [ [ 'fromtitle' => 'B1' ], [ 'fromtitle' => 'B2' ] ] );
477
478 yield 'concat TOC' => [ $a, $b, [
479 'getTOCHTML' => '<p>TOC A</p><p>TOC B</p>',
480 'getSections' => [
481 [ 'fromtitle' => 'A1' ],
482 [ 'fromtitle' => 'A2' ],
483 [ 'fromtitle' => 'B1' ],
484 [ 'fromtitle' => 'B2' ]
485 ],
486 ] ];
487
488 // Skin Control ------------
489 $a = new ParserOutput();
490 $a->setNewSection( true );
491 $a->hideNewSection( true );
492 $a->setNoGallery( true );
493 $a->addWrapperDivClass( 'foo' );
494
495 $a->setIndicator( 'foo', 'Foo!' );
496 $a->setIndicator( 'bar', 'Bar!' );
497
498 $a->setExtensionData( 'foo', 'Foo!' );
499 $a->setExtensionData( 'bar', 'Bar!' );
500
501 $b = new ParserOutput();
502 $b->setNoGallery( true );
503 $b->setEnableOOUI( true );
504 $b->preventClickjacking( true );
505 $a->addWrapperDivClass( 'bar' );
506
507 $b->setIndicator( 'zoo', 'Zoo!' );
508 $b->setIndicator( 'bar', 'Barrr!' );
509
510 $b->setExtensionData( 'zoo', 'Zoo!' );
511 $b->setExtensionData( 'bar', 'Barrr!' );
512
513 yield 'skin control flags' => [ $a, $b, [
514 'getNewSection' => true,
515 'getHideNewSection' => true,
516 'getNoGallery' => true,
517 'getEnableOOUI' => true,
518 'preventClickjacking' => true,
519 'getIndicators' => [
520 'foo' => 'Foo!',
521 'bar' => 'Barrr!',
522 'zoo' => 'Zoo!',
523 ],
524 'getWrapperDivClass' => 'foo bar',
525 '$mExtensionData' => [
526 'foo' => 'Foo!',
527 'bar' => 'Barrr!',
528 'zoo' => 'Zoo!',
529 ],
530 ] ];
531 }
532
533 /**
534 * @dataProvider provideMergeHtmlMetaDataFrom
535 * @covers ParserOutput::mergeHtmlMetaDataFrom
536 *
537 * @param ParserOutput $a
538 * @param ParserOutput $b
539 * @param array $expected
540 */
541 public function testMergeHtmlMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
542 $a->mergeHtmlMetaDataFrom( $b );
543
544 $this->assertFieldValues( $a, $expected );
545
546 // test twice, to make sure the operation is idempotent (except for the TOC, see below)
547 $a->mergeHtmlMetaDataFrom( $b );
548
549 // XXX: TOC joining should get smarter. Can we make it idempotent as well?
550 unset( $expected['getTOCHTML'] );
551 unset( $expected['getSections'] );
552
553 $this->assertFieldValues( $a, $expected );
554 }
555
556 private function assertFieldValues( ParserOutput $po, $expected ) {
557 $po = TestingAccessWrapper::newFromObject( $po );
558
559 foreach ( $expected as $method => $value ) {
560 if ( $method[0] === '$' ) {
561 $field = substr( $method, 1 );
562 $actual = $po->__get( $field );
563 } else {
564 $actual = $po->__call( $method, [] );
565 }
566
567 $this->assertEquals( $value, $actual, $method );
568 }
569 }
570
571 public function provideMergeTrackingMetaDataFrom() {
572 // links ------------
573 $a = new ParserOutput();
574 $a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
575 $a->addLink( Title::makeTitle( NS_TALK, 'Kittens' ), 16 );
576 $a->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
577
578 $a->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Goats' ), 107, 1107 );
579
580 $a->addLanguageLink( 'de' );
581 $a->addLanguageLink( 'ru' );
582 $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens DE', '', 'de' ) );
583 $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens RU', '', 'ru' ) );
584 $a->addExternalLink( 'https://kittens.wikimedia.test' );
585 $a->addExternalLink( 'https://goats.wikimedia.test' );
586
587 $a->addCategory( 'Foo', 'X' );
588 $a->addImage( 'Billy.jpg', '20180101000013', 'DEAD' );
589
590 $b = new ParserOutput();
591 $b->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
592 $b->addLink( Title::makeTitle( NS_TALK, 'Goats' ), 17 );
593 $b->addLink( Title::makeTitle( NS_MAIN, 'Dragons' ), 8 );
594 $b->addLink( Title::makeTitle( NS_FILE, 'Dragons.jpg' ), 28 );
595
596 $b->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Dragons' ), 108, 1108 );
597 $a->addTemplate( Title::makeTitle( NS_MAIN, 'Dragons' ), 118, 1118 );
598
599 $b->addLanguageLink( 'fr' );
600 $b->addLanguageLink( 'ru' );
601 $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens FR', '', 'fr' ) );
602 $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Dragons RU', '', 'ru' ) );
603 $b->addExternalLink( 'https://dragons.wikimedia.test' );
604 $b->addExternalLink( 'https://goats.wikimedia.test' );
605
606 $b->addCategory( 'Bar', 'Y' );
607 $b->addImage( 'Puff.jpg', '20180101000017', 'BEEF' );
608
609 yield 'all kinds of links' => [ $a, $b, [
610 'getLinks' => [
611 NS_MAIN => [
612 'Kittens' => 6,
613 'Goats' => 7,
614 'Dragons' => 8,
615 ],
616 NS_TALK => [
617 'Kittens' => 16,
618 'Goats' => 17,
619 ],
620 NS_FILE => [
621 'Dragons.jpg' => 28,
622 ],
623 ],
624 'getTemplates' => [
625 NS_MAIN => [
626 'Dragons' => 118,
627 ],
628 NS_TEMPLATE => [
629 'Dragons' => 108,
630 'Goats' => 107,
631 ],
632 ],
633 'getTemplateIds' => [
634 NS_MAIN => [
635 'Dragons' => 1118,
636 ],
637 NS_TEMPLATE => [
638 'Dragons' => 1108,
639 'Goats' => 1107,
640 ],
641 ],
642 'getLanguageLinks' => [ 'de', 'ru', 'fr' ],
643 'getInterwikiLinks' => [
644 'de' => [ 'Kittens_DE' => 1 ],
645 'ru' => [ 'Kittens_RU' => 1, 'Dragons_RU' => 1, ],
646 'fr' => [ 'Kittens_FR' => 1 ],
647 ],
648 'getCategories' => [ 'Foo' => 'X', 'Bar' => 'Y' ],
649 'getImages' => [ 'Billy.jpg' => 1, 'Puff.jpg' => 1 ],
650 'getFileSearchOptions' => [
651 'Billy.jpg' => [ 'time' => '20180101000013', 'sha1' => 'DEAD' ],
652 'Puff.jpg' => [ 'time' => '20180101000017', 'sha1' => 'BEEF' ],
653 ],
654 'getExternalLinks' => [
655 'https://dragons.wikimedia.test' => 1,
656 'https://kittens.wikimedia.test' => 1,
657 'https://goats.wikimedia.test' => 1,
658 ]
659 ] ];
660
661 // properties ------------
662 $a = new ParserOutput();
663
664 $a->setProperty( 'foo', 'Foo!' );
665 $a->setProperty( 'bar', 'Bar!' );
666
667 $a->setExtensionData( 'foo', 'Foo!' );
668 $a->setExtensionData( 'bar', 'Bar!' );
669
670 $b = new ParserOutput();
671
672 $b->setProperty( 'zoo', 'Zoo!' );
673 $b->setProperty( 'bar', 'Barrr!' );
674
675 $b->setExtensionData( 'zoo', 'Zoo!' );
676 $b->setExtensionData( 'bar', 'Barrr!' );
677
678 yield 'properties' => [ $a, $b, [
679 'getProperties' => [
680 'foo' => 'Foo!',
681 'bar' => 'Barrr!',
682 'zoo' => 'Zoo!',
683 ],
684 '$mExtensionData' => [
685 'foo' => 'Foo!',
686 'bar' => 'Barrr!',
687 'zoo' => 'Zoo!',
688 ],
689 ] ];
690 }
691
692 /**
693 * @dataProvider provideMergeTrackingMetaDataFrom
694 * @covers ParserOutput::mergeTrackingMetaDataFrom
695 *
696 * @param ParserOutput $a
697 * @param ParserOutput $b
698 * @param array $expected
699 */
700 public function testMergeTrackingMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
701 $a->mergeTrackingMetaDataFrom( $b );
702
703 $this->assertFieldValues( $a, $expected );
704
705 // test twice, to make sure the operation is idempotent
706 $a->mergeTrackingMetaDataFrom( $b );
707
708 $this->assertFieldValues( $a, $expected );
709 }
710
711 public function provideMergeInternalMetaDataFrom() {
712 // hooks
713 $a = new ParserOutput();
714
715 $a->addOutputHook( 'foo', 'X' );
716 $a->addOutputHook( 'bar' );
717
718 $b = new ParserOutput();
719
720 $b->addOutputHook( 'foo', 'Y' );
721 $b->addOutputHook( 'bar' );
722 $b->addOutputHook( 'zoo' );
723
724 yield 'hooks' => [ $a, $b, [
725 'getOutputHooks' => [
726 [ 'foo', 'X' ],
727 [ 'bar', false ],
728 [ 'foo', 'Y' ],
729 [ 'zoo', false ],
730 ],
731 ] ];
732
733 // flags & co
734 $a = new ParserOutput();
735
736 $a->addWarning( 'Oops' );
737 $a->addWarning( 'Whoops' );
738
739 $a->setFlag( 'foo' );
740 $a->setFlag( 'bar' );
741
742 $a->recordOption( 'Foo' );
743 $a->recordOption( 'Bar' );
744
745 $b = new ParserOutput();
746
747 $b->addWarning( 'Yikes' );
748 $b->addWarning( 'Whoops' );
749
750 $b->setFlag( 'zoo' );
751 $b->setFlag( 'bar' );
752
753 $b->recordOption( 'Zoo' );
754 $b->recordOption( 'Bar' );
755
756 yield 'flags' => [ $a, $b, [
757 'getWarnings' => [ 'Oops', 'Whoops', 'Yikes' ],
758 '$mFlags' => [ 'foo' => true, 'bar' => true, 'zoo' => true ],
759 'getUsedOptions' => [ 'Foo', 'Bar', 'Zoo' ],
760 ] ];
761
762 // timestamp ------------
763 $a = new ParserOutput();
764 $a->setTimestamp( '20180101000011' );
765 $b = new ParserOutput();
766 yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
767
768 $a = new ParserOutput();
769 $b = new ParserOutput();
770 $b->setTimestamp( '20180101000011' );
771 yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
772
773 $a = new ParserOutput();
774 $a->setTimestamp( '20180101000011' );
775 $b = new ParserOutput();
776 $b->setTimestamp( '20180101000001' );
777 yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
778
779 $a = new ParserOutput();
780 $a->setTimestamp( '20180101000001' );
781 $b = new ParserOutput();
782 $b->setTimestamp( '20180101000011' );
783 yield 'right timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
784
785 // speculative rev id ------------
786 $a = new ParserOutput();
787 $a->setSpeculativeRevIdUsed( 9 );
788 $b = new ParserOutput();
789 yield 'only left speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
790
791 $a = new ParserOutput();
792 $b = new ParserOutput();
793 $b->setSpeculativeRevIdUsed( 9 );
794 yield 'only right speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
795
796 $a = new ParserOutput();
797 $a->setSpeculativeRevIdUsed( 9 );
798 $b = new ParserOutput();
799 $b->setSpeculativeRevIdUsed( 9 );
800 yield 'same speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
801
802 // limit report (recursive max) ------------
803 $a = new ParserOutput();
804
805 $a->setLimitReportData( 'naive1', 7 );
806 $a->setLimitReportData( 'naive2', 27 );
807
808 $a->setLimitReportData( 'limitreport-simple1', 7 );
809 $a->setLimitReportData( 'limitreport-simple2', 27 );
810
811 $a->setLimitReportData( 'limitreport-pair1', [ 7, 9 ] );
812 $a->setLimitReportData( 'limitreport-pair2', [ 27, 29 ] );
813
814 $a->setLimitReportData( 'limitreport-more1', [ 7, 9, 1 ] );
815 $a->setLimitReportData( 'limitreport-more2', [ 27, 29, 21 ] );
816
817 $a->setLimitReportData( 'limitreport-only-a', 13 );
818
819 $b = new ParserOutput();
820
821 $b->setLimitReportData( 'naive1', 17 );
822 $b->setLimitReportData( 'naive2', 17 );
823
824 $b->setLimitReportData( 'limitreport-simple1', 17 );
825 $b->setLimitReportData( 'limitreport-simple2', 17 );
826
827 $b->setLimitReportData( 'limitreport-pair1', [ 17, 19 ] );
828 $b->setLimitReportData( 'limitreport-pair2', [ 17, 19 ] );
829
830 $b->setLimitReportData( 'limitreport-more1', [ 17, 19, 11 ] );
831 $b->setLimitReportData( 'limitreport-more2', [ 17, 19, 11 ] );
832
833 $b->setLimitReportData( 'limitreport-only-b', 23 );
834
835 // first write wins
836 yield 'limit report' => [ $a, $b, [
837 'getLimitReportData' => [
838 'naive1' => 7,
839 'naive2' => 27,
840 'limitreport-simple1' => 7,
841 'limitreport-simple2' => 27,
842 'limitreport-pair1' => [ 7, 9 ],
843 'limitreport-pair2' => [ 27, 29 ],
844 'limitreport-more1' => [ 7, 9, 1 ],
845 'limitreport-more2' => [ 27, 29, 21 ],
846 'limitreport-only-a' => 13,
847 ],
848 'getLimitReportJSData' => [
849 'naive1' => 7,
850 'naive2' => 27,
851 'limitreport' => [
852 'simple1' => 7,
853 'simple2' => 27,
854 'pair1' => [ 'value' => 7, 'limit' => 9 ],
855 'pair2' => [ 'value' => 27, 'limit' => 29 ],
856 'more1' => [ 7, 9, 1 ],
857 'more2' => [ 27, 29, 21 ],
858 'only-a' => 13,
859 ],
860 ],
861 ] ];
862 }
863
864 /**
865 * @dataProvider provideMergeInternalMetaDataFrom
866 * @covers ParserOutput::mergeInternalMetaDataFrom
867 *
868 * @param ParserOutput $a
869 * @param ParserOutput $b
870 * @param array $expected
871 */
872 public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
873 $a->mergeInternalMetaDataFrom( $b );
874
875 $this->assertFieldValues( $a, $expected );
876
877 // test twice, to make sure the operation is idempotent
878 $a->mergeInternalMetaDataFrom( $b );
879
880 $this->assertFieldValues( $a, $expected );
881 }
882
883 public function testMergeInternalMetaDataFrom_parseStartTime() {
884 /** @var object $a */
885 $a = new ParserOutput();
886 $a = TestingAccessWrapper::newFromObject( $a );
887
888 $a->resetParseStartTime();
889 $aClocks = $a->mParseStartTime;
890
891 $b = new ParserOutput();
892
893 $a->mergeInternalMetaDataFrom( $b );
894 $mergedClocks = $a->mParseStartTime;
895
896 foreach ( $mergedClocks as $clock => $timestamp ) {
897 $this->assertSame( $aClocks[$clock], $timestamp, $clock );
898 }
899
900 // try again, with times in $b also set, and later than $a's
901 usleep( 1234 );
902
903 /** @var object $b */
904 $b = new ParserOutput();
905 $b = TestingAccessWrapper::newFromObject( $b );
906
907 $b->resetParseStartTime();
908
909 $bClocks = $b->mParseStartTime;
910
911 $a->mergeInternalMetaDataFrom( $b->object, 'b' );
912 $mergedClocks = $a->mParseStartTime;
913
914 foreach ( $mergedClocks as $clock => $timestamp ) {
915 $this->assertSame( $aClocks[$clock], $timestamp, $clock );
916 $this->assertLessThanOrEqual( $bClocks[$clock], $timestamp, $clock );
917 }
918
919 // try again, with $a's times being later
920 usleep( 1234 );
921 $a->resetParseStartTime();
922 $aClocks = $a->mParseStartTime;
923
924 $a->mergeInternalMetaDataFrom( $b->object, 'b' );
925 $mergedClocks = $a->mParseStartTime;
926
927 foreach ( $mergedClocks as $clock => $timestamp ) {
928 $this->assertSame( $bClocks[$clock], $timestamp, $clock );
929 $this->assertLessThanOrEqual( $aClocks[$clock], $timestamp, $clock );
930 }
931
932 // try again, with no times in $a set
933 $a = new ParserOutput();
934 $a = TestingAccessWrapper::newFromObject( $a );
935
936 $a->mergeInternalMetaDataFrom( $b->object, 'b' );
937 $mergedClocks = $a->mParseStartTime;
938
939 foreach ( $mergedClocks as $clock => $timestamp ) {
940 $this->assertSame( $bClocks[$clock], $timestamp, $clock );
941 }
942 }
943
944 /**
945 * @covers ParserOutput::getCacheTime
946 * @covers ParserOutput::setCacheTime
947 */
948 public function testGetCacheTime() {
949 $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
950 MWTimestamp::setFakeTime( function () use ( &$clock ) {
951 return $clock++;
952 } );
953
954 $po = new ParserOutput();
955 $time = $po->getCacheTime();
956
957 // Use current (fake) time per default. Ignore the last digit.
958 // Subsequent calls must yield the exact same timestamp as the first.
959 $this->assertStringStartsWith( '2010010100000', $time );
960 $this->assertSame( $time, $po->getCacheTime() );
961
962 // After setting, the getter must return the time that was set.
963 $time = '20110606112233';
964 $po->setCacheTime( $time );
965 $this->assertSame( $time, $po->getCacheTime() );
966
967 // support -1 as a marker for "not cacheable"
968 $time = -1;
969 $po->setCacheTime( $time );
970 $this->assertSame( $time, $po->getCacheTime() );
971 }
972
973 }