Merge "StringUtils: Add a utility for checking if a string is a valid regex"
[lhc/web/wiklou.git] / tests / phpunit / includes / deferred / LinksUpdateTest.php
1 <?php
2
3 /**
4 * @covers LinksUpdate
5 * @group LinksUpdate
6 * @group Database
7 * ^--- make sure temporary tables are used.
8 */
9 class LinksUpdateTest extends MediaWikiLangTestCase {
10 protected static $testingPageId;
11
12 function __construct( $name = null, array $data = [], $dataName = '' ) {
13 parent::__construct( $name, $data, $dataName );
14
15 $this->tablesUsed = array_merge( $this->tablesUsed,
16 [
17 'interwiki',
18 'page_props',
19 'pagelinks',
20 'categorylinks',
21 'langlinks',
22 'externallinks',
23 'imagelinks',
24 'templatelinks',
25 'iwlinks',
26 'recentchanges',
27 ]
28 );
29 }
30
31 protected function setUp() {
32 parent::setUp();
33 $dbw = wfGetDB( DB_MASTER );
34 $dbw->replace(
35 'interwiki',
36 [ 'iw_prefix' ],
37 [
38 'iw_prefix' => 'linksupdatetest',
39 'iw_url' => 'http://testing.com/wiki/$1',
40 'iw_api' => 'http://testing.com/w/api.php',
41 'iw_local' => 0,
42 'iw_trans' => 0,
43 'iw_wikiid' => 'linksupdatetest',
44 ]
45 );
46 $this->setMwGlobals( 'wgRCWatchCategoryMembership', true );
47 }
48
49 public function addDBDataOnce() {
50 $res = $this->insertPage( 'Testing' );
51 self::$testingPageId = $res['id'];
52 $this->insertPage( 'Some_other_page' );
53 $this->insertPage( 'Template:TestingTemplate' );
54 }
55
56 protected function makeTitleAndParserOutput( $name, $id ) {
57 $t = Title::newFromText( $name );
58 $t->mArticleID = $id; # XXX: this is fugly
59
60 $po = new ParserOutput();
61 $po->setTitleText( $t->getPrefixedText() );
62
63 return [ $t, $po ];
64 }
65
66 /**
67 * @covers ParserOutput::addLink
68 */
69 public function testUpdate_pagelinks() {
70 /** @var Title $t */
71 /** @var ParserOutput $po */
72 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
73
74 $po->addLink( Title::newFromText( "Foo" ) );
75 $po->addLink( Title::newFromText( "Special:Foo" ) ); // special namespace should be ignored
76 $po->addLink( Title::newFromText( "linksupdatetest:Foo" ) ); // interwiki link should be ignored
77 $po->addLink( Title::newFromText( "#Foo" ) ); // hash link should be ignored
78
79 $update = $this->assertLinksUpdate(
80 $t,
81 $po,
82 'pagelinks',
83 'pl_namespace,
84 pl_title',
85 'pl_from = ' . self::$testingPageId,
86 [ [ NS_MAIN, 'Foo' ] ]
87 );
88 $this->assertArrayEquals( [
89 Title::makeTitle( NS_MAIN, 'Foo' ), // newFromText doesn't yield the same internal state....
90 ], $update->getAddedLinks() );
91
92 $po = new ParserOutput();
93 $po->setTitleText( $t->getPrefixedText() );
94
95 $po->addLink( Title::newFromText( "Bar" ) );
96 $po->addLink( Title::newFromText( "Talk:Bar" ) );
97
98 $update = $this->assertLinksUpdate(
99 $t,
100 $po,
101 'pagelinks',
102 'pl_namespace,
103 pl_title',
104 'pl_from = ' . self::$testingPageId,
105 [
106 [ NS_MAIN, 'Bar' ],
107 [ NS_TALK, 'Bar' ],
108 ]
109 );
110 $this->assertArrayEquals( [
111 Title::makeTitle( NS_MAIN, 'Bar' ),
112 Title::makeTitle( NS_TALK, 'Bar' ),
113 ], $update->getAddedLinks() );
114 $this->assertArrayEquals( [
115 Title::makeTitle( NS_MAIN, 'Foo' ),
116 ], $update->getRemovedLinks() );
117 }
118
119 /**
120 * @covers ParserOutput::addExternalLink
121 * @covers LinksUpdate::getAddedExternalLinks
122 * @covers LinksUpdate::getRemovedExternalLinks
123 */
124 public function testUpdate_externallinks() {
125 /** @var ParserOutput $po */
126 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
127
128 $po->addExternalLink( "http://testing.com/wiki/Foo" );
129
130 $update = $this->assertLinksUpdate(
131 $t,
132 $po,
133 'externallinks',
134 'el_to, el_index',
135 'el_from = ' . self::$testingPageId,
136 [
137 [ 'http://testing.com/wiki/Foo', 'http://com.testing./wiki/Foo' ],
138 ]
139 );
140
141 $this->assertArrayEquals( [
142 "http://testing.com/wiki/Foo"
143 ], $update->getAddedExternalLinks() );
144
145 $po = new ParserOutput();
146 $po->setTitleText( $t->getPrefixedText() );
147 $po->addExternalLink( 'http://testing.com/wiki/Bar' );
148 $update = $this->assertLinksUpdate(
149 $t,
150 $po,
151 'externallinks',
152 'el_to, el_index',
153 'el_from = ' . self::$testingPageId,
154 [
155 [ 'http://testing.com/wiki/Bar', 'http://com.testing./wiki/Bar' ],
156 ]
157 );
158
159 $this->assertArrayEquals( [
160 "http://testing.com/wiki/Bar"
161 ], $update->getAddedExternalLinks() );
162 $this->assertArrayEquals( [
163 "http://testing.com/wiki/Foo"
164 ], $update->getRemovedExternalLinks() );
165 }
166
167 /**
168 * @covers ParserOutput::addCategory
169 */
170 public function testUpdate_categorylinks() {
171 /** @var ParserOutput $po */
172 $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
173
174 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
175
176 $po->addCategory( "Foo", "FOO" );
177
178 $this->assertLinksUpdate(
179 $t,
180 $po,
181 'categorylinks',
182 'cl_to, cl_sortkey',
183 'cl_from = ' . self::$testingPageId,
184 [ [ 'Foo', "FOO\nTESTING" ] ]
185 );
186 }
187
188 public function testOnAddingAndRemovingCategory_recentChangesRowIsAdded() {
189 $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
190
191 $title = Title::newFromText( 'Testing' );
192 $wikiPage = new WikiPage( $title );
193 $wikiPage->doEditContent( new WikitextContent( '[[Category:Foo]]' ), 'added category' );
194 $this->runAllRelatedJobs();
195
196 $this->assertRecentChangeByCategorization(
197 $title,
198 $wikiPage->getParserOutput( ParserOptions::newCanonical() ),
199 Title::newFromText( 'Category:Foo' ),
200 [ [ 'Foo', '[[:Testing]] added to category' ] ]
201 );
202
203 $wikiPage->doEditContent( new WikitextContent( '[[Category:Bar]]' ), 'replaced category' );
204 $this->runAllRelatedJobs();
205
206 $this->assertRecentChangeByCategorization(
207 $title,
208 $wikiPage->getParserOutput( ParserOptions::newCanonical() ),
209 Title::newFromText( 'Category:Foo' ),
210 [
211 [ 'Foo', '[[:Testing]] added to category' ],
212 [ 'Foo', '[[:Testing]] removed from category' ],
213 ]
214 );
215
216 $this->assertRecentChangeByCategorization(
217 $title,
218 $wikiPage->getParserOutput( ParserOptions::newCanonical() ),
219 Title::newFromText( 'Category:Bar' ),
220 [
221 [ 'Bar', '[[:Testing]] added to category' ],
222 ]
223 );
224 }
225
226 public function testOnAddingAndRemovingCategoryToTemplates_embeddingPagesAreIgnored() {
227 $this->setMwGlobals( 'wgCategoryCollation', 'uppercase' );
228
229 $templateTitle = Title::newFromText( 'Template:TestingTemplate' );
230 $templatePage = new WikiPage( $templateTitle );
231
232 $wikiPage = new WikiPage( Title::newFromText( 'Testing' ) );
233 $wikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' );
234 $this->runAllRelatedJobs();
235
236 $otherWikiPage = new WikiPage( Title::newFromText( 'Some_other_page' ) );
237 $otherWikiPage->doEditContent( new WikitextContent( '{{TestingTemplate}}' ), 'added template' );
238 $this->runAllRelatedJobs();
239
240 $this->assertRecentChangeByCategorization(
241 $templateTitle,
242 $templatePage->getParserOutput( ParserOptions::newCanonical() ),
243 Title::newFromText( 'Baz' ),
244 []
245 );
246
247 $templatePage->doEditContent( new WikitextContent( '[[Category:Baz]]' ), 'added category' );
248 $this->runAllRelatedJobs();
249
250 $this->assertRecentChangeByCategorization(
251 $templateTitle,
252 $templatePage->getParserOutput( ParserOptions::newCanonical() ),
253 Title::newFromText( 'Baz' ),
254 [ [
255 'Baz',
256 '[[:Template:TestingTemplate]] added to category, ' .
257 '[[Special:WhatLinksHere/Template:TestingTemplate|this page is included within other pages]]'
258 ] ]
259 );
260 }
261
262 /**
263 * @covers ParserOutput::addInterwikiLink
264 */
265 public function testUpdate_iwlinks() {
266 /** @var ParserOutput $po */
267 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
268
269 $target = Title::makeTitleSafe( NS_MAIN, "Foo", '', 'linksupdatetest' );
270 $po->addInterwikiLink( $target );
271
272 $this->assertLinksUpdate(
273 $t,
274 $po,
275 'iwlinks',
276 'iwl_prefix, iwl_title',
277 'iwl_from = ' . self::$testingPageId,
278 [ [ 'linksupdatetest', 'Foo' ] ]
279 );
280 }
281
282 /**
283 * @covers ParserOutput::addTemplate
284 */
285 public function testUpdate_templatelinks() {
286 /** @var ParserOutput $po */
287 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
288
289 $po->addTemplate( Title::newFromText( "Template:Foo" ), 23, 42 );
290
291 $this->assertLinksUpdate(
292 $t,
293 $po,
294 'templatelinks',
295 'tl_namespace,
296 tl_title',
297 'tl_from = ' . self::$testingPageId,
298 [ [ NS_TEMPLATE, 'Foo' ] ]
299 );
300 }
301
302 /**
303 * @covers ParserOutput::addImage
304 */
305 public function testUpdate_imagelinks() {
306 /** @var ParserOutput $po */
307 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
308
309 $po->addImage( "Foo.png" );
310
311 $this->assertLinksUpdate(
312 $t,
313 $po,
314 'imagelinks',
315 'il_to',
316 'il_from = ' . self::$testingPageId,
317 [ [ 'Foo.png' ] ]
318 );
319 }
320
321 /**
322 * @covers ParserOutput::addLanguageLink
323 */
324 public function testUpdate_langlinks() {
325 $this->setMwGlobals( [
326 'wgCapitalLinks' => true,
327 ] );
328
329 /** @var ParserOutput $po */
330 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
331
332 $po->addLanguageLink( Title::newFromText( "en:Foo" )->getFullText() );
333
334 $this->assertLinksUpdate(
335 $t,
336 $po,
337 'langlinks',
338 'll_lang, ll_title',
339 'll_from = ' . self::$testingPageId,
340 [ [ 'En', 'Foo' ] ]
341 );
342 }
343
344 /**
345 * @covers ParserOutput::setProperty
346 */
347 public function testUpdate_page_props() {
348 global $wgPagePropsHaveSortkey;
349
350 /** @var ParserOutput $po */
351 list( $t, $po ) = $this->makeTitleAndParserOutput( "Testing", self::$testingPageId );
352
353 $fields = [ 'pp_propname', 'pp_value' ];
354 $expected = [];
355
356 $po->setProperty( "bool", true );
357 $expected[] = [ "bool", true ];
358
359 $po->setProperty( "float", 4.0 + 1.0 / 4.0 );
360 $expected[] = [ "float", 4.0 + 1.0 / 4.0 ];
361
362 $po->setProperty( "int", -7 );
363 $expected[] = [ "int", -7 ];
364
365 $po->setProperty( "string", "33 bar" );
366 $expected[] = [ "string", "33 bar" ];
367
368 // compute expected sortkey values
369 if ( $wgPagePropsHaveSortkey ) {
370 $fields[] = 'pp_sortkey';
371
372 foreach ( $expected as &$row ) {
373 $value = $row[1];
374
375 if ( is_int( $value ) || is_float( $value ) || is_bool( $value ) ) {
376 $row[] = floatval( $value );
377 } else {
378 $row[] = null;
379 }
380 }
381 }
382
383 $this->assertLinksUpdate(
384 $t, $po, 'page_props', $fields, 'pp_page = ' . self::$testingPageId, $expected );
385 }
386
387 public function testUpdate_page_props_without_sortkey() {
388 $this->setMwGlobals( 'wgPagePropsHaveSortkey', false );
389
390 $this->testUpdate_page_props();
391 }
392
393 // @todo test recursive, too!
394
395 protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput,
396 $table, $fields, $condition, array $expectedRows
397 ) {
398 $update = new LinksUpdate( $title, $parserOutput );
399
400 $update->doUpdate();
401
402 $this->assertSelect( $table, $fields, $condition, $expectedRows );
403 return $update;
404 }
405
406 protected function assertRecentChangeByCategorization(
407 Title $pageTitle, ParserOutput $parserOutput, Title $categoryTitle, $expectedRows
408 ) {
409 $this->assertSelect(
410 [ 'recentchanges', 'comment' ],
411 'rc_title, comment_text',
412 [
413 'rc_type' => RC_CATEGORIZE,
414 'rc_namespace' => NS_CATEGORY,
415 'rc_title' => $categoryTitle->getDBkey(),
416 'comment_id = rc_comment_id',
417 ],
418 $expectedRows
419 );
420 }
421
422 private function runAllRelatedJobs() {
423 $queueGroup = JobQueueGroup::singleton();
424 while ( $job = $queueGroup->pop( 'refreshLinksPrioritized' ) ) {
425 $job->run();
426 $queueGroup->ack( $job );
427 }
428 while ( $job = $queueGroup->pop( 'categoryMembershipChange' ) ) {
429 $job->run();
430 $queueGroup->ack( $job );
431 }
432 }
433
434 public function testIsRecursive() {
435 list( $title, $po ) = $this->makeTitleAndParserOutput( 'Test', 1 );
436 $linksUpdate = new LinksUpdate( $title, $po );
437 $this->assertTrue( $linksUpdate->isRecursive(), 'LinksUpdate is recursive by default' );
438
439 $linksUpdate = new LinksUpdate( $title, $po, true );
440 $this->assertTrue( $linksUpdate->isRecursive(),
441 'LinksUpdate is recursive when asked to be recursive' );
442
443 $linksUpdate = new LinksUpdate( $title, $po, false );
444 $this->assertFalse( $linksUpdate->isRecursive(),
445 'LinksUpdate is not recursive when asked to be not recursive' );
446 }
447 }