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