Merge "Revision: Assert that $mRecord is never null in Revision"
[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 /**
153 * @covers ParserOutput::getText
154 * @dataProvider provideGetText
155 * @param array $options Options to getText()
156 * @param string $text Parser text
157 * @param string $expect Expected output
158 */
159 public function testGetText( $options, $text, $expect ) {
160 $this->setMwGlobals( [
161 'wgArticlePath' => '/wiki/$1',
162 'wgScriptPath' => '/w',
163 'wgScript' => '/w/index.php',
164 ] );
165
166 $po = new ParserOutput( $text );
167 $actual = $po->getText( $options );
168 $this->assertSame( $expect, $actual );
169 }
170
171 public static function provideGetText() {
172 // phpcs:disable Generic.Files.LineLength
173 $text = <<<EOF
174 <p>Test document.
175 </p>
176 <mw:toc><div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
177 <ul>
178 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
179 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
180 <ul>
181 <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>
182 </ul>
183 </li>
184 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
185 </ul>
186 </div>
187 </mw:toc>
188 <h2><span class="mw-headline" id="Section_1">Section 1</span><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></h2>
189 <p>One
190 </p>
191 <h2><span class="mw-headline" id="Section_2">Section 2</span><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></h2>
192 <p>Two
193 </p>
194 <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>
195 <p>Two point one
196 </p>
197 <h2><span class="mw-headline" id="Section_3">Section 3</span><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></h2>
198 <p>Three
199 </p>
200 EOF;
201
202 $dedupText = <<<EOF
203 <p>This is a test document.</p>
204 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
205 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
206 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
207 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
208 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
209 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
210 <style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
211 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
212 <style>.Duplicate1 {}</style>
213 EOF;
214
215 return [
216 'No options' => [
217 [], $text, <<<EOF
218 <p>Test document.
219 </p>
220 <div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
221 <ul>
222 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
223 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
224 <ul>
225 <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>
226 </ul>
227 </li>
228 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
229 </ul>
230 </div>
231
232 <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>
233 <p>One
234 </p>
235 <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>
236 <p>Two
237 </p>
238 <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>
239 <p>Two point one
240 </p>
241 <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>
242 <p>Three
243 </p>
244 EOF
245 ],
246 'Disable section edit links' => [
247 [ 'enableSectionEditLinks' => false ], $text, <<<EOF
248 <p>Test document.
249 </p>
250 <div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
251 <ul>
252 <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
253 <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
254 <ul>
255 <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>
256 </ul>
257 </li>
258 <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
259 </ul>
260 </div>
261
262 <h2><span class="mw-headline" id="Section_1">Section 1</span></h2>
263 <p>One
264 </p>
265 <h2><span class="mw-headline" id="Section_2">Section 2</span></h2>
266 <p>Two
267 </p>
268 <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
269 <p>Two point one
270 </p>
271 <h2><span class="mw-headline" id="Section_3">Section 3</span></h2>
272 <p>Three
273 </p>
274 EOF
275 ],
276 'Disable TOC, but wrap' => [
277 [ 'allowTOC' => false, 'wrapperDivClass' => 'mw-parser-output' ], $text, <<<EOF
278 <div class="mw-parser-output"><p>Test document.
279 </p>
280
281 <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>
282 <p>One
283 </p>
284 <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>
285 <p>Two
286 </p>
287 <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>
288 <p>Two point one
289 </p>
290 <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>
291 <p>Three
292 </p></div>
293 EOF
294 ],
295 'Style deduplication' => [
296 [], $dedupText, <<<EOF
297 <p>This is a test document.</p>
298 <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
299 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
300 <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
301 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
302 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2"/>
303 <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
304 <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
305 <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
306 <style>.Duplicate1 {}</style>
307 EOF
308 ],
309 'Style deduplication disabled' => [
310 [ 'deduplicateStyles' => false ], $dedupText, $dedupText
311 ],
312 ];
313 // phpcs:enable
314 }
315
316 /**
317 * @covers ParserOutput::hasText
318 */
319 public function testHasText() {
320 $po = new ParserOutput();
321 $this->assertTrue( $po->hasText() );
322
323 $po = new ParserOutput( null );
324 $this->assertFalse( $po->hasText() );
325
326 $po = new ParserOutput( '' );
327 $this->assertTrue( $po->hasText() );
328
329 $po = new ParserOutput( null );
330 $po->setText( '' );
331 $this->assertTrue( $po->hasText() );
332 }
333
334 /**
335 * @covers ParserOutput::getText
336 */
337 public function testGetText_failsIfNoText() {
338 $po = new ParserOutput( null );
339
340 $this->setExpectedException( LogicException::class );
341 $po->getText();
342 }
343
344 /**
345 * @covers ParserOutput::getRawText
346 */
347 public function testGetRawText_failsIfNoText() {
348 $po = new ParserOutput( null );
349
350 $this->setExpectedException( LogicException::class );
351 $po->getRawText();
352 }
353
354 public function provideMergeHtmlMetaDataFrom() {
355 // title text ------------
356 $a = new ParserOutput();
357 $a->setTitleText( 'X' );
358 $b = new ParserOutput();
359 yield 'only left title text' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
360
361 $a = new ParserOutput();
362 $b = new ParserOutput();
363 $b->setTitleText( 'Y' );
364 yield 'only right title text' => [ $a, $b, [ 'getTitleText' => 'Y' ] ];
365
366 $a = new ParserOutput();
367 $a->setTitleText( 'X' );
368 $b = new ParserOutput();
369 $b->setTitleText( 'Y' );
370 yield 'left title text wins' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
371
372 // index policy ------------
373 $a = new ParserOutput();
374 $a->setIndexPolicy( 'index' );
375 $b = new ParserOutput();
376 yield 'only left index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
377
378 $a = new ParserOutput();
379 $b = new ParserOutput();
380 $b->setIndexPolicy( 'index' );
381 yield 'only right index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
382
383 $a = new ParserOutput();
384 $a->setIndexPolicy( 'noindex' );
385 $b = new ParserOutput();
386 $b->setIndexPolicy( 'index' );
387 yield 'left noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
388
389 $a = new ParserOutput();
390 $a->setIndexPolicy( 'index' );
391 $b = new ParserOutput();
392 $b->setIndexPolicy( 'noindex' );
393 yield 'right noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
394
395 // head items and friends ------------
396 $a = new ParserOutput();
397 $a->addHeadItem( '<foo1>' );
398 $a->addHeadItem( '<bar1>', 'bar' );
399 $a->addModules( 'test-module-a' );
400 $a->addModuleScripts( 'test-module-script-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->addModuleScripts( 'test-module-script-b' );
410 $b->addModuleStyles( 'test-module-styles-b' );
411 $b->addJsConfigVars( 'test-config-var-b', 'b' );
412 $b->addJsConfigVars( 'test-config-var-a', 'X' );
413
414 yield 'head items and friends' => [ $a, $b, [
415 'getHeadItems' => [
416 '<foo1>',
417 '<foo2>',
418 'bar' => '<bar2>', // overwritten
419 ],
420 'getModules' => [
421 'test-module-a',
422 'test-module-b',
423 ],
424 'getModuleScripts' => [
425 'test-module-script-a',
426 'test-module-script-b',
427 ],
428 'getModuleStyles' => [
429 'test-module-styles-a',
430 'test-module-styles-b',
431 ],
432 'getJsConfigVars' => [
433 'test-config-var-a' => 'X', // overwritten
434 'test-config-var-b' => 'b',
435 ],
436 ] ];
437
438 // TOC ------------
439 $a = new ParserOutput();
440 $a->setTOCHTML( '<p>TOC A</p>' );
441 $a->setSections( [ [ 'fromtitle' => 'A1' ], [ 'fromtitle' => 'A2' ] ] );
442
443 $b = new ParserOutput();
444 $b->setTOCHTML( '<p>TOC B</p>' );
445 $b->setSections( [ [ 'fromtitle' => 'B1' ], [ 'fromtitle' => 'B2' ] ] );
446
447 yield 'concat TOC' => [ $a, $b, [
448 'getTOCHTML' => '<p>TOC A</p><p>TOC B</p>',
449 'getSections' => [
450 [ 'fromtitle' => 'A1' ],
451 [ 'fromtitle' => 'A2' ],
452 [ 'fromtitle' => 'B1' ],
453 [ 'fromtitle' => 'B2' ]
454 ],
455 ] ];
456
457 // Skin Control ------------
458 $a = new ParserOutput();
459 $a->setNewSection( true );
460 $a->hideNewSection( true );
461 $a->setNoGallery( true );
462 $a->addWrapperDivClass( 'foo' );
463
464 $a->setIndicator( 'foo', 'Foo!' );
465 $a->setIndicator( 'bar', 'Bar!' );
466
467 $a->setExtensionData( 'foo', 'Foo!' );
468 $a->setExtensionData( 'bar', 'Bar!' );
469
470 $b = new ParserOutput();
471 $b->setNoGallery( true );
472 $b->setEnableOOUI( true );
473 $b->preventClickjacking( true );
474 $a->addWrapperDivClass( 'bar' );
475
476 $b->setIndicator( 'zoo', 'Zoo!' );
477 $b->setIndicator( 'bar', 'Barrr!' );
478
479 $b->setExtensionData( 'zoo', 'Zoo!' );
480 $b->setExtensionData( 'bar', 'Barrr!' );
481
482 yield 'skin control flags' => [ $a, $b, [
483 'getNewSection' => true,
484 'getHideNewSection' => true,
485 'getNoGallery' => true,
486 'getEnableOOUI' => true,
487 'preventClickjacking' => true,
488 'getIndicators' => [
489 'foo' => 'Foo!',
490 'bar' => 'Barrr!',
491 'zoo' => 'Zoo!',
492 ],
493 'getWrapperDivClass' => 'foo bar',
494 '$mExtensionData' => [
495 'foo' => 'Foo!',
496 'bar' => 'Barrr!',
497 'zoo' => 'Zoo!',
498 ],
499 ] ];
500 }
501
502 /**
503 * @dataProvider provideMergeHtmlMetaDataFrom
504 * @covers ParserOutput::mergeHtmlMetaDataFrom
505 *
506 * @param ParserOutput $a
507 * @param ParserOutput $b
508 * @param array $expected
509 */
510 public function testMergeHtmlMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
511 $a->mergeHtmlMetaDataFrom( $b );
512
513 $this->assertFieldValues( $a, $expected );
514
515 // test twice, to make sure the operation is idempotent (except for the TOC, see below)
516 $a->mergeHtmlMetaDataFrom( $b );
517
518 // XXX: TOC joining should get smarter. Can we make it idempotent as well?
519 unset( $expected['getTOCHTML'] );
520 unset( $expected['getSections'] );
521
522 $this->assertFieldValues( $a, $expected );
523 }
524
525 private function assertFieldValues( ParserOutput $po, $expected ) {
526 $po = TestingAccessWrapper::newFromObject( $po );
527
528 foreach ( $expected as $method => $value ) {
529 if ( $method[0] === '$' ) {
530 $field = substr( $method, 1 );
531 $actual = $po->__get( $field );
532 } else {
533 $actual = $po->__call( $method, [] );
534 }
535
536 $this->assertEquals( $value, $actual, $method );
537 }
538 }
539
540 public function provideMergeTrackingMetaDataFrom() {
541 // links ------------
542 $a = new ParserOutput();
543 $a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
544 $a->addLink( Title::makeTitle( NS_TALK, 'Kittens' ), 16 );
545 $a->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
546
547 $a->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Goats' ), 107, 1107 );
548
549 $a->addLanguageLink( 'de' );
550 $a->addLanguageLink( 'ru' );
551 $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens DE', '', 'de' ) );
552 $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens RU', '', 'ru' ) );
553 $a->addExternalLink( 'https://kittens.wikimedia.test' );
554 $a->addExternalLink( 'https://goats.wikimedia.test' );
555
556 $a->addCategory( 'Foo', 'X' );
557 $a->addImage( 'Billy.jpg', '20180101000013', 'DEAD' );
558
559 $b = new ParserOutput();
560 $b->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
561 $b->addLink( Title::makeTitle( NS_TALK, 'Goats' ), 17 );
562 $b->addLink( Title::makeTitle( NS_MAIN, 'Dragons' ), 8 );
563 $b->addLink( Title::makeTitle( NS_FILE, 'Dragons.jpg' ), 28 );
564
565 $b->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Dragons' ), 108, 1108 );
566 $a->addTemplate( Title::makeTitle( NS_MAIN, 'Dragons' ), 118, 1118 );
567
568 $b->addLanguageLink( 'fr' );
569 $b->addLanguageLink( 'ru' );
570 $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens FR', '', 'fr' ) );
571 $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Dragons RU', '', 'ru' ) );
572 $b->addExternalLink( 'https://dragons.wikimedia.test' );
573 $b->addExternalLink( 'https://goats.wikimedia.test' );
574
575 $b->addCategory( 'Bar', 'Y' );
576 $b->addImage( 'Puff.jpg', '20180101000017', 'BEEF' );
577
578 yield 'all kinds of links' => [ $a, $b, [
579 'getLinks' => [
580 NS_MAIN => [
581 'Kittens' => 6,
582 'Goats' => 7,
583 'Dragons' => 8,
584 ],
585 NS_TALK => [
586 'Kittens' => 16,
587 'Goats' => 17,
588 ],
589 NS_FILE => [
590 'Dragons.jpg' => 28,
591 ],
592 ],
593 'getTemplates' => [
594 NS_MAIN => [
595 'Dragons' => 118,
596 ],
597 NS_TEMPLATE => [
598 'Dragons' => 108,
599 'Goats' => 107,
600 ],
601 ],
602 'getTemplateIds' => [
603 NS_MAIN => [
604 'Dragons' => 1118,
605 ],
606 NS_TEMPLATE => [
607 'Dragons' => 1108,
608 'Goats' => 1107,
609 ],
610 ],
611 'getLanguageLinks' => [ 'de', 'ru', 'fr' ],
612 'getInterwikiLinks' => [
613 'de' => [ 'Kittens_DE' => 1 ],
614 'ru' => [ 'Kittens_RU' => 1, 'Dragons_RU' => 1, ],
615 'fr' => [ 'Kittens_FR' => 1 ],
616 ],
617 'getCategories' => [ 'Foo' => 'X', 'Bar' => 'Y' ],
618 'getImages' => [ 'Billy.jpg' => 1, 'Puff.jpg' => 1 ],
619 'getFileSearchOptions' => [
620 'Billy.jpg' => [ 'time' => '20180101000013', 'sha1' => 'DEAD' ],
621 'Puff.jpg' => [ 'time' => '20180101000017', 'sha1' => 'BEEF' ],
622 ],
623 'getExternalLinks' => [
624 'https://dragons.wikimedia.test' => 1,
625 'https://kittens.wikimedia.test' => 1,
626 'https://goats.wikimedia.test' => 1,
627 ]
628 ] ];
629
630 // properties ------------
631 $a = new ParserOutput();
632
633 $a->setProperty( 'foo', 'Foo!' );
634 $a->setProperty( 'bar', 'Bar!' );
635
636 $a->setExtensionData( 'foo', 'Foo!' );
637 $a->setExtensionData( 'bar', 'Bar!' );
638
639 $b = new ParserOutput();
640
641 $b->setProperty( 'zoo', 'Zoo!' );
642 $b->setProperty( 'bar', 'Barrr!' );
643
644 $b->setExtensionData( 'zoo', 'Zoo!' );
645 $b->setExtensionData( 'bar', 'Barrr!' );
646
647 yield 'properties' => [ $a, $b, [
648 'getProperties' => [
649 'foo' => 'Foo!',
650 'bar' => 'Barrr!',
651 'zoo' => 'Zoo!',
652 ],
653 '$mExtensionData' => [
654 'foo' => 'Foo!',
655 'bar' => 'Barrr!',
656 'zoo' => 'Zoo!',
657 ],
658 ] ];
659 }
660
661 /**
662 * @dataProvider provideMergeTrackingMetaDataFrom
663 * @covers ParserOutput::mergeTrackingMetaDataFrom
664 *
665 * @param ParserOutput $a
666 * @param ParserOutput $b
667 * @param array $expected
668 */
669 public function testMergeTrackingMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
670 $a->mergeTrackingMetaDataFrom( $b );
671
672 $this->assertFieldValues( $a, $expected );
673
674 // test twice, to make sure the operation is idempotent
675 $a->mergeTrackingMetaDataFrom( $b );
676
677 $this->assertFieldValues( $a, $expected );
678 }
679
680 public function provideMergeInternalMetaDataFrom() {
681 // hooks
682 $a = new ParserOutput();
683
684 $a->addOutputHook( 'foo', 'X' );
685 $a->addOutputHook( 'bar' );
686
687 $b = new ParserOutput();
688
689 $b->addOutputHook( 'foo', 'Y' );
690 $b->addOutputHook( 'bar' );
691 $b->addOutputHook( 'zoo' );
692
693 yield 'hooks' => [ $a, $b, [
694 'getOutputHooks' => [
695 [ 'foo', 'X' ],
696 [ 'bar', false ],
697 [ 'foo', 'Y' ],
698 [ 'zoo', false ],
699 ],
700 ] ];
701
702 // flags & co
703 $a = new ParserOutput();
704
705 $a->addWarning( 'Oops' );
706 $a->addWarning( 'Whoops' );
707
708 $a->setFlag( 'foo' );
709 $a->setFlag( 'bar' );
710
711 $a->recordOption( 'Foo' );
712 $a->recordOption( 'Bar' );
713
714 $b = new ParserOutput();
715
716 $b->addWarning( 'Yikes' );
717 $b->addWarning( 'Whoops' );
718
719 $b->setFlag( 'zoo' );
720 $b->setFlag( 'bar' );
721
722 $b->recordOption( 'Zoo' );
723 $b->recordOption( 'Bar' );
724
725 yield 'flags' => [ $a, $b, [
726 'getWarnings' => [ 'Oops', 'Whoops', 'Yikes' ],
727 '$mFlags' => [ 'foo' => true, 'bar' => true, 'zoo' => true ],
728 'getUsedOptions' => [ 'Foo', 'Bar', 'Zoo' ],
729 ] ];
730
731 // timestamp ------------
732 $a = new ParserOutput();
733 $a->setTimestamp( '20180101000011' );
734 $b = new ParserOutput();
735 yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
736
737 $a = new ParserOutput();
738 $b = new ParserOutput();
739 $b->setTimestamp( '20180101000011' );
740 yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
741
742 $a = new ParserOutput();
743 $a->setTimestamp( '20180101000011' );
744 $b = new ParserOutput();
745 $b->setTimestamp( '20180101000001' );
746 yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
747
748 $a = new ParserOutput();
749 $a->setTimestamp( '20180101000001' );
750 $b = new ParserOutput();
751 $b->setTimestamp( '20180101000011' );
752 yield 'right timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
753
754 // speculative rev id ------------
755 $a = new ParserOutput();
756 $a->setSpeculativeRevIdUsed( 9 );
757 $b = new ParserOutput();
758 yield 'only left speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
759
760 $a = new ParserOutput();
761 $b = new ParserOutput();
762 $b->setSpeculativeRevIdUsed( 9 );
763 yield 'only right speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
764
765 $a = new ParserOutput();
766 $a->setSpeculativeRevIdUsed( 9 );
767 $b = new ParserOutput();
768 $b->setSpeculativeRevIdUsed( 9 );
769 yield 'same speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
770
771 // limit report (recursive max) ------------
772 $a = new ParserOutput();
773
774 $a->setLimitReportData( 'naive1', 7 );
775 $a->setLimitReportData( 'naive2', 27 );
776
777 $a->setLimitReportData( 'limitreport-simple1', 7 );
778 $a->setLimitReportData( 'limitreport-simple2', 27 );
779
780 $a->setLimitReportData( 'limitreport-pair1', [ 7, 9 ] );
781 $a->setLimitReportData( 'limitreport-pair2', [ 27, 29 ] );
782
783 $a->setLimitReportData( 'limitreport-more1', [ 7, 9, 1 ] );
784 $a->setLimitReportData( 'limitreport-more2', [ 27, 29, 21 ] );
785
786 $a->setLimitReportData( 'limitreport-only-a', 13 );
787
788 $b = new ParserOutput();
789
790 $b->setLimitReportData( 'naive1', 17 );
791 $b->setLimitReportData( 'naive2', 17 );
792
793 $b->setLimitReportData( 'limitreport-simple1', 17 );
794 $b->setLimitReportData( 'limitreport-simple2', 17 );
795
796 $b->setLimitReportData( 'limitreport-pair1', [ 17, 19 ] );
797 $b->setLimitReportData( 'limitreport-pair2', [ 17, 19 ] );
798
799 $b->setLimitReportData( 'limitreport-more1', [ 17, 19, 11 ] );
800 $b->setLimitReportData( 'limitreport-more2', [ 17, 19, 11 ] );
801
802 $b->setLimitReportData( 'limitreport-only-b', 23 );
803
804 // first write wins
805 yield 'limit report' => [ $a, $b, [
806 'getLimitReportData' => [
807 'naive1' => 7,
808 'naive2' => 27,
809 'limitreport-simple1' => 7,
810 'limitreport-simple2' => 27,
811 'limitreport-pair1' => [ 7, 9 ],
812 'limitreport-pair2' => [ 27, 29 ],
813 'limitreport-more1' => [ 7, 9, 1 ],
814 'limitreport-more2' => [ 27, 29, 21 ],
815 'limitreport-only-a' => 13,
816 ],
817 'getLimitReportJSData' => [
818 'naive1' => 7,
819 'naive2' => 27,
820 'limitreport' => [
821 'simple1' => 7,
822 'simple2' => 27,
823 'pair1' => [ 'value' => 7, 'limit' => 9 ],
824 'pair2' => [ 'value' => 27, 'limit' => 29 ],
825 'more1' => [ 7, 9, 1 ],
826 'more2' => [ 27, 29, 21 ],
827 'only-a' => 13,
828 ],
829 ],
830 ] ];
831 }
832
833 /**
834 * @dataProvider provideMergeInternalMetaDataFrom
835 * @covers ParserOutput::mergeInternalMetaDataFrom
836 *
837 * @param ParserOutput $a
838 * @param ParserOutput $b
839 * @param array $expected
840 */
841 public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
842 $a->mergeInternalMetaDataFrom( $b );
843
844 $this->assertFieldValues( $a, $expected );
845
846 // test twice, to make sure the operation is idempotent
847 $a->mergeInternalMetaDataFrom( $b );
848
849 $this->assertFieldValues( $a, $expected );
850 }
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, 'b' );
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, 'b' );
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, 'b' );
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 }