Workaround for duplicate key errors
[lhc/web/wiklou.git] / tests / phpunit / includes / parser / NewParserTest.php
1 <?php
2
3 /**
4 * @group Database
5 * @group Parser
6 * @group Stub (can also work independently)
7 */
8 class NewParserTest extends MediaWikiTestCase {
9
10 static protected $articles = array(); // Array of test articles defined by the tests
11 /* The dataProvider is run on a different instance than the test, so it must be static
12 * When running tests from several files, all tests will see all articles.
13 */
14
15 public $uploadDir;
16 public $keepUploads = false;
17 public $runDisabled = false;
18 public $regex = '';
19 public $showProgress = true;
20 public $savedInitialGlobals = array();
21 public $savedWeirdGlobals = array();
22 public $savedGlobals = array();
23 public $hooks = array();
24 public $functionHooks = array();
25
26 //Fuzz test
27 public $maxFuzzTestLength = 300;
28 public $fuzzSeed = 0;
29 public $memoryLimit = 50;
30
31 protected $file = false;
32
33 /*function __construct($a = null,$b = array(),$c = null ) {
34 parent::__construct($a,$b,$c);
35 }*/
36
37 function setUp() {
38 global $wgContLang, $wgNamespaceProtection, $wgNamespaceAliases;
39 global $wgHooks, $IP;
40 $wgContLang = Language::factory( 'en' );
41
42 //Setup CLI arguments
43 if ( $this->getCliArg( 'regex=' ) ) {
44 $this->regex = $this->getCliArg( 'regex=' );
45 } else {
46 # Matches anything
47 $this->regex = '';
48 }
49
50 $this->keepUploads = $this->getCliArg( 'keep-uploads' );
51
52 $tmpGlobals = array();
53
54 $tmpGlobals['wgScript'] = '/index.php';
55 $tmpGlobals['wgScriptPath'] = '/';
56 $tmpGlobals['wgArticlePath'] = '/wiki/$1';
57 $tmpGlobals['wgStyleSheetPath'] = '/skins';
58 $tmpGlobals['wgStylePath'] = '/skins';
59 $tmpGlobals['wgThumbnailScriptPath'] = false;
60 $tmpGlobals['wgLocalFileRepo'] = array(
61 'class' => 'LocalRepo',
62 'name' => 'local',
63 'directory' => wfTempDir() . '/test-repo',
64 'url' => 'http://example.com/images',
65 'deletedDir' => wfTempDir() . '/test-repo/delete',
66 'hashLevels' => 2,
67 'transformVia404' => false,
68 );
69
70 $tmpGlobals['wgEnableParserCache'] = false;
71 $tmpGlobals['wgHooks'] = $wgHooks;
72 $tmpGlobals['wgDeferredUpdateList'] = array();
73 $tmpGlobals['wgMemc'] = wfGetMainCache();
74 $tmpGlobals['messageMemc'] = wfGetMessageCacheStorage();
75 $tmpGlobals['parserMemc'] = wfGetParserCacheStorage();
76
77 // $tmpGlobals['wgContLang'] = new StubContLang;
78 $tmpGlobals['wgUser'] = new User;
79 $context = new RequestContext();
80 $tmpGlobals['wgLang'] = $context->lang;
81 $tmpGlobals['wgOut'] = $context->output;
82 $tmpGlobals['wgParser'] = new StubObject( 'wgParser', $GLOBALS['wgParserConf']['class'], array( $GLOBALS['wgParserConf'] ) );
83 $tmpGlobals['wgRequest'] = new WebRequest;
84
85 if ( $GLOBALS['wgStyleDirectory'] === false ) {
86 $tmpGlobals['wgStyleDirectory'] = "$IP/skins";
87 }
88
89
90 foreach ( $tmpGlobals as $var => $val ) {
91 if ( array_key_exists( $var, $GLOBALS ) ) {
92 $this->savedInitialGlobals[$var] = $GLOBALS[$var];
93 }
94
95 $GLOBALS[$var] = $val;
96 }
97
98 $this->savedWeirdGlobals['mw_namespace_protection'] = $wgNamespaceProtection[NS_MEDIAWIKI];
99 $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image'];
100 $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk'];
101
102 $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
103 $wgNamespaceAliases['Image'] = NS_FILE;
104 $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
105
106 }
107
108 public function tearDown() {
109
110 foreach ( $this->savedInitialGlobals as $var => $val ) {
111 $GLOBALS[$var] = $val;
112 }
113
114 global $wgNamespaceProtection, $wgNamespaceAliases;
115
116 $wgNamespaceProtection[NS_MEDIAWIKI] = $this->savedWeirdGlobals['mw_namespace_protection'];
117 $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias'];
118 $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias'];
119 }
120
121 function addDBData() {
122 # Hack: insert a few Wikipedia in-project interwiki prefixes,
123 # for testing inter-language links
124 $this->db->insert( 'interwiki', array(
125 array( 'iw_prefix' => 'wikipedia',
126 'iw_url' => 'http://en.wikipedia.org/wiki/$1',
127 'iw_api' => '',
128 'iw_wikiid' => '',
129 'iw_local' => 0 ),
130 array( 'iw_prefix' => 'meatball',
131 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
132 'iw_api' => '',
133 'iw_wikiid' => '',
134 'iw_local' => 0 ),
135 array( 'iw_prefix' => 'zh',
136 'iw_url' => 'http://zh.wikipedia.org/wiki/$1',
137 'iw_api' => '',
138 'iw_wikiid' => '',
139 'iw_local' => 1 ),
140 array( 'iw_prefix' => 'es',
141 'iw_url' => 'http://es.wikipedia.org/wiki/$1',
142 'iw_api' => '',
143 'iw_wikiid' => '',
144 'iw_local' => 1 ),
145 array( 'iw_prefix' => 'fr',
146 'iw_url' => 'http://fr.wikipedia.org/wiki/$1',
147 'iw_api' => '',
148 'iw_wikiid' => '',
149 'iw_local' => 1 ),
150 array( 'iw_prefix' => 'ru',
151 'iw_url' => 'http://ru.wikipedia.org/wiki/$1',
152 'iw_api' => '',
153 'iw_wikiid' => '',
154 'iw_local' => 1 ),
155 /**
156 * @todo Fixme! Why are we inserting duplicate data here? Shouldn't
157 * need this IGNORE or shouldn't need the insert at all.
158 */
159 ), __METHOD__, array( 'IGNORE' ) );
160
161
162 # Update certain things in site_stats
163 $this->db->insert( 'site_stats',
164 array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ),
165 __METHOD__,
166 /**
167 * @todo Fixme! Same as above!
168 */
169 array( 'IGNORE' )
170 );
171
172 # Reinitialise the LocalisationCache to match the database state
173 Language::getLocalisationCache()->unloadAll();
174
175 # Clear the message cache
176 MessageCache::singleton()->clear();
177
178 $this->uploadDir = $this->setupUploadDir();
179
180 $user = User::newFromId( 0 );
181 LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision
182
183 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
184 $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', array(
185 'size' => 12345,
186 'width' => 1941,
187 'height' => 220,
188 'bits' => 24,
189 'media_type' => MEDIATYPE_BITMAP,
190 'mime' => 'image/jpeg',
191 'metadata' => serialize( array() ),
192 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
193 'fileExists' => true
194 ), $this->db->timestamp( '20010115123500' ), $user );
195
196 # This image will be blacklisted in [[MediaWiki:Bad image list]]
197 $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
198 $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', array(
199 'size' => 12345,
200 'width' => 320,
201 'height' => 240,
202 'bits' => 24,
203 'media_type' => MEDIATYPE_BITMAP,
204 'mime' => 'image/jpeg',
205 'metadata' => serialize( array() ),
206 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
207 'fileExists' => true
208 ), $this->db->timestamp( '20010115123500' ), $user );
209
210 }
211
212
213
214
215 //ParserTest setup/teardown functions
216
217 /**
218 * Set up the global variables for a consistent environment for each test.
219 * Ideally this should replace the global configuration entirely.
220 */
221 protected function setupGlobals( $opts = '', $config = '' ) {
222 # Find out values for some special options.
223 $lang =
224 self::getOptionValue( 'language', $opts, 'en' );
225 $variant =
226 self::getOptionValue( 'variant', $opts, false );
227 $maxtoclevel =
228 self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
229 $linkHolderBatchSize =
230 self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
231
232 $settings = array(
233 'wgServer' => 'http://Britney-Spears',
234 'wgScript' => '/index.php',
235 'wgScriptPath' => '/',
236 'wgArticlePath' => '/wiki/$1',
237 'wgActionPaths' => array(),
238 'wgLocalFileRepo' => array(
239 'class' => 'LocalRepo',
240 'name' => 'local',
241 'directory' => $this->uploadDir,
242 'url' => 'http://example.com/images',
243 'hashLevels' => 2,
244 'transformVia404' => false,
245 ),
246 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
247 'wgStylePath' => '/skins',
248 'wgStyleSheetPath' => '/skins',
249 'wgSitename' => 'MediaWiki',
250 'wgLanguageCode' => $lang,
251 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'unittest_' : 'ut_',
252 'wgRawHtml' => isset( $opts['rawhtml'] ),
253 'wgLang' => null,
254 'wgContLang' => null,
255 'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
256 'wgMaxTocLevel' => $maxtoclevel,
257 'wgCapitalLinks' => true,
258 'wgNoFollowLinks' => true,
259 'wgNoFollowDomainExceptions' => array(),
260 'wgThumbnailScriptPath' => false,
261 'wgUseImageResize' => false,
262 'wgUseTeX' => isset( $opts['math'] ),
263 'wgMathDirectory' => $this->uploadDir . '/math',
264 'wgLocaltimezone' => 'UTC',
265 'wgAllowExternalImages' => true,
266 'wgUseTidy' => false,
267 'wgDefaultLanguageVariant' => $variant,
268 'wgVariantArticlePath' => false,
269 'wgGroupPermissions' => array( '*' => array(
270 'createaccount' => true,
271 'read' => true,
272 'edit' => true,
273 'createpage' => true,
274 'createtalk' => true,
275 ) ),
276 'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
277 'wgDefaultExternalStore' => array(),
278 'wgForeignFileRepos' => array(),
279 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
280 'wgExperimentalHtmlIds' => false,
281 'wgExternalLinkTarget' => false,
282 'wgAlwaysUseTidy' => false,
283 'wgHtml5' => true,
284 'wgWellFormedXml' => true,
285 'wgAllowMicrodataAttributes' => true,
286 'wgAdaptiveMessageCache' => true,
287 'wgUseDatabaseMessages' => true,
288 );
289
290 if ( $config ) {
291 $configLines = explode( "\n", $config );
292
293 foreach ( $configLines as $line ) {
294 list( $var, $value ) = explode( '=', $line, 2 );
295
296 $settings[$var] = eval( "return $value;" ); //???
297 }
298 }
299
300 $this->savedGlobals = array();
301
302 foreach ( $settings as $var => $val ) {
303 if ( array_key_exists( $var, $GLOBALS ) ) {
304 $this->savedGlobals[$var] = $GLOBALS[$var];
305 }
306
307 $GLOBALS[$var] = $val;
308 }
309
310 $langObj = Language::factory( $lang );
311 $GLOBALS['wgContLang'] = $langObj;
312 $context = new RequestContext();
313 $GLOBALS['wgLang'] = $context->lang;
314
315 $GLOBALS['wgMemc'] = new EmptyBagOStuff;
316 $GLOBALS['wgOut'] = $context->output;
317
318 global $wgHooks;
319
320 $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
321 $wgHooks['ParserTestParser'][] = 'ParserTestStaticParserHook::setup';
322 $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
323
324 MagicWord::clearCache();
325
326 # Publish the articles after we have the final language set
327 $this->publishTestArticles();
328
329 # The entries saved into RepoGroup cache with previous globals will be wrong.
330 RepoGroup::destroySingleton();
331 MessageCache::singleton()->destroyInstance();
332
333 global $wgUser;
334 $wgUser = new User();
335 }
336
337 /**
338 * Restore default values and perform any necessary clean-up
339 * after each test runs.
340 */
341 protected function teardownGlobals() {
342 RepoGroup::destroySingleton();
343 LinkCache::singleton()->clear();
344
345 foreach ( $this->savedGlobals as $var => $val ) {
346 $GLOBALS[$var] = $val;
347 }
348 }
349
350 /**
351 * Create a dummy uploads directory which will contain a couple
352 * of files in order to pass existence tests.
353 *
354 * @return String: the directory
355 */
356 protected function setupUploadDir() {
357 global $IP;
358
359 if ( $this->keepUploads ) {
360 $dir = wfTempDir() . '/mwParser-images';
361
362 if ( is_dir( $dir ) ) {
363 return $dir;
364 }
365 } else {
366 $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
367 }
368
369 // wfDebug( "Creating upload directory $dir\n" );
370 if ( file_exists( $dir ) ) {
371 wfDebug( "Already exists!\n" );
372 return $dir;
373 }
374
375 wfMkdirParents( $dir . '/3/3a' );
376 copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
377 wfMkdirParents( $dir . '/0/09' );
378 copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" );
379
380 return $dir;
381 }
382
383 public function parserTestProvider() {
384 if ( $this->file === false ) {
385 global $wgParserTestFiles;
386 $this->file = $wgParserTestFiles[0];
387 }
388 return new TestFileIterator( $this->file, $this );
389 }
390
391 /**
392 * Set the file from whose tests will be run by this instance
393 */
394 public function setParserTestFile( $filename ) {
395 $this->file = $filename;
396 }
397
398 /** @dataProvider parserTestProvider */
399 public function testParserTest( $desc, $input, $result, $opts, $config ) {
400 if ( !preg_match( '/' . $this->regex . '/', $desc ) ) return; //$this->markTestSkipped( 'Filtered out by the user' );
401
402 wfDebug( "Running parser test: $desc\n" );
403
404 $opts = $this->parseOptions( $opts );
405 $this->setupGlobals( $opts, $config );
406
407 $user = new User();
408 $options = ParserOptions::newFromUser( $user );
409
410 if ( isset( $opts['title'] ) ) {
411 $titleText = $opts['title'];
412 }
413 else {
414 $titleText = 'Parser test';
415 }
416
417 $local = isset( $opts['local'] );
418 $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
419 $parser = $this->getParser( $preprocessor );
420
421 $title = Title::newFromText( $titleText );
422
423 if ( isset( $opts['pst'] ) ) {
424 $out = $parser->preSaveTransform( $input, $title, $user, $options );
425 } elseif ( isset( $opts['msg'] ) ) {
426 $out = $parser->transformMsg( $input, $options );
427 } elseif ( isset( $opts['section'] ) ) {
428 $section = $opts['section'];
429 $out = $parser->getSection( $input, $section );
430 } elseif ( isset( $opts['replace'] ) ) {
431 $section = $opts['replace'][0];
432 $replace = $opts['replace'][1];
433 $out = $parser->replaceSection( $input, $section, $replace );
434 } elseif ( isset( $opts['comment'] ) ) {
435 $linker = $user->getSkin();
436 $out = $linker->formatComment( $input, $title, $local );
437 } elseif ( isset( $opts['preload'] ) ) {
438 $out = $parser->getpreloadText( $input, $title, $options );
439 } else {
440 $output = $parser->parse( $input, $title, $options, true, true, 1337 );
441 $out = $output->getText();
442
443 if ( isset( $opts['showtitle'] ) ) {
444 if ( $output->getTitleText() ) {
445 $title = $output->getTitleText();
446 }
447
448 $out = "$title\n$out";
449 }
450
451 if ( isset( $opts['ill'] ) ) {
452 $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
453 } elseif ( isset( $opts['cat'] ) ) {
454 global $wgOut;
455
456 $wgOut->addCategoryLinks( $output->getCategories() );
457 $cats = $wgOut->getCategoryLinks();
458
459 if ( isset( $cats['normal'] ) ) {
460 $out = $this->tidy( implode( ' ', $cats['normal'] ) );
461 } else {
462 $out = '';
463 }
464 }
465 $parser->mPreprocessor = null;
466
467 $result = $this->tidy( $result );
468 }
469
470 $this->teardownGlobals();
471
472 $this->assertEquals( $result, $out, $desc );
473 }
474
475 /**
476 * Run a fuzz test series
477 * Draw input from a set of test files
478 */
479 function testFuzzTests() {
480
481 $this->markTestIncomplete( 'Breaks tesla due to memory restrictions' );
482
483 global $wgParserTestFiles;
484
485 $files = $wgParserTestFiles;
486
487 if( $this->getCliArg( 'file=' ) ) {
488 $files = array( $this->getCliArg( 'file=' ) );
489 }
490
491 $dict = $this->getFuzzInput( $files );
492 $dictSize = strlen( $dict );
493 $logMaxLength = log( $this->maxFuzzTestLength );
494
495 ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
496
497 $user = new User;
498 $opts = ParserOptions::newFromUser( $user );
499 $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
500
501 $id = 1;
502
503 while ( true ) {
504
505 // Generate test input
506 mt_srand( ++$this->fuzzSeed );
507 $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
508 $input = '';
509
510 while ( strlen( $input ) < $totalLength ) {
511 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
512 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
513 $offset = mt_rand( 0, $dictSize - $hairLength );
514 $input .= substr( $dict, $offset, $hairLength );
515 }
516
517 $this->setupGlobals();
518 $parser = $this->getParser();
519
520 // Run the test
521 try {
522 $parser->parse( $input, $title, $opts );
523 $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" );
524 } catch ( Exception $exception ) {
525 $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input );
526
527 $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\nInput: $input_dump\n\nError: {$exception->getMessage()}\n\nBacktrace: {$exception->getTraceAsString()}" );
528 }
529
530 $this->teardownGlobals();
531 $parser->__destruct();
532
533 if ( $id % 100 == 0 ) {
534 $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
535 //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
536 if ( $usage > 90 ) {
537 $ret = "Out of memory:\n";
538 $memStats = $this->getMemoryBreakdown();
539
540 foreach ( $memStats as $name => $usage ) {
541 $ret .= "$name: $usage\n";
542 }
543
544 throw new MWException( $ret );
545 }
546 }
547
548 $id++;
549
550 }
551 }
552
553 //Various getter functions
554
555 /**
556 * Get an input dictionary from a set of parser test files
557 */
558 function getFuzzInput( $filenames ) {
559 $dict = '';
560
561 foreach ( $filenames as $filename ) {
562 $contents = file_get_contents( $filename );
563 preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
564
565 foreach ( $matches[1] as $match ) {
566 $dict .= $match . "\n";
567 }
568 }
569
570 return $dict;
571 }
572
573 /**
574 * Get a memory usage breakdown
575 */
576 function getMemoryBreakdown() {
577 $memStats = array();
578
579 foreach ( $GLOBALS as $name => $value ) {
580 $memStats['$' . $name] = strlen( serialize( $value ) );
581 }
582
583 $classes = get_declared_classes();
584
585 foreach ( $classes as $class ) {
586 $rc = new ReflectionClass( $class );
587 $props = $rc->getStaticProperties();
588 $memStats[$class] = strlen( serialize( $props ) );
589 $methods = $rc->getMethods();
590
591 foreach ( $methods as $method ) {
592 $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
593 }
594 }
595
596 $functions = get_defined_functions();
597
598 foreach ( $functions['user'] as $function ) {
599 $rf = new ReflectionFunction( $function );
600 $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
601 }
602
603 asort( $memStats );
604
605 return $memStats;
606 }
607
608 /**
609 * Get a Parser object
610 */
611 function getParser( $preprocessor = null ) {
612 global $wgParserConf;
613
614 $class = $wgParserConf['class'];
615 $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
616
617 wfRunHooks( 'ParserTestParser', array( &$parser ) );
618
619 return $parser;
620 }
621
622 //Various action functions
623
624 public function addArticle( $name, $text, $line ) {
625 self::$articles[$name] = $text;
626 }
627
628 public function publishTestArticles() {
629 if ( empty( self::$articles ) ) {
630 return;
631 }
632
633 foreach ( self::$articles as $name => $text ) {
634 $title = Title::newFromText( $name );
635
636 if ( $title->getArticleID( Title::GAID_FOR_UPDATE ) == 0 ) {
637 ParserTest::addArticle( $name, $text );
638 }
639 }
640 }
641
642 /**
643 * Steal a callback function from the primary parser, save it for
644 * application to our scary parser. If the hook is not installed,
645 * abort processing of this file.
646 *
647 * @param $name String
648 * @return Bool true if tag hook is present
649 */
650 public function requireHook( $name ) {
651 global $wgParser;
652 $wgParser->firstCallInit( ); // make sure hooks are loaded.
653 return isset( $wgParser->mTagHooks[$name] );
654 }
655
656 public function requireFunctionHook( $name ) {
657 global $wgParser;
658 $wgParser->firstCallInit( ); // make sure hooks are loaded.
659 return isset( $wgParser->mFunctionHooks[$name] );
660 }
661 //Various "cleanup" functions
662
663 /*
664 * Run the "tidy" command on text if the $wgUseTidy
665 * global is true
666 *
667 * @param $text String: the text to tidy
668 * @return String
669 * @static
670 */
671 protected function tidy( $text ) {
672 global $wgUseTidy;
673
674 if ( $wgUseTidy ) {
675 $text = MWTidy::tidy( $text );
676 }
677
678 return $text;
679 }
680
681 /**
682 * Remove last character if it is a newline
683 */
684 public function removeEndingNewline( $s ) {
685 if ( substr( $s, -1 ) === "\n" ) {
686 return substr( $s, 0, -1 );
687 }
688 else {
689 return $s;
690 }
691 }
692
693 public function showRunFile( $file ) {
694 /* NOP */
695 }
696
697 //Test options parser functions
698
699 protected function parseOptions( $instring ) {
700 $opts = array();
701 // foo
702 // foo=bar
703 // foo="bar baz"
704 // foo=[[bar baz]]
705 // foo=bar,"baz quux"
706 $regex = '/\b
707 ([\w-]+) # Key
708 \b
709 (?:\s*
710 = # First sub-value
711 \s*
712 (
713 "
714 [^"]* # Quoted val
715 "
716 |
717 \[\[
718 [^]]* # Link target
719 \]\]
720 |
721 [\w-]+ # Plain word
722 )
723 (?:\s*
724 , # Sub-vals 1..N
725 \s*
726 (
727 "[^"]*" # Quoted val
728 |
729 \[\[[^]]*\]\] # Link target
730 |
731 [\w-]+ # Plain word
732 )
733 )*
734 )?
735 /x';
736
737 if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
738 foreach ( $matches as $bits ) {
739 array_shift( $bits );
740 $key = strtolower( array_shift( $bits ) );
741 if ( count( $bits ) == 0 ) {
742 $opts[$key] = true;
743 } elseif ( count( $bits ) == 1 ) {
744 $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
745 } else {
746 // Array!
747 $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
748 }
749 }
750 }
751 return $opts;
752 }
753
754 protected function cleanupOption( $opt ) {
755 if ( substr( $opt, 0, 1 ) == '"' ) {
756 return substr( $opt, 1, -1 );
757 }
758
759 if ( substr( $opt, 0, 2 ) == '[[' ) {
760 return substr( $opt, 2, -2 );
761 }
762 return $opt;
763 }
764
765 /**
766 * Use a regex to find out the value of an option
767 * @param $key String: name of option val to retrieve
768 * @param $opts Options array to look in
769 * @param $default Mixed: default value returned if not found
770 */
771 protected static function getOptionValue( $key, $opts, $default ) {
772 $key = strtolower( $key );
773
774 if ( isset( $opts[$key] ) ) {
775 return $opts[$key];
776 } else {
777 return $default;
778 }
779 }
780 }