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