]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - maintenance/parserTests.inc
MediaWiki 1.16.0
[autoinstalls/mediawiki.git] / maintenance / parserTests.inc
1 <?php
2 # Copyright (C) 2004, 2010 Brion Vibber <brion@pobox.com>
3 # http://www.mediawiki.org/
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # http://www.gnu.org/copyleft/gpl.html
19
20 /**
21  * @todo Make this more independent of the configuration (and if possible the database)
22  * @todo document
23  * @file
24  * @ingroup Maintenance
25  */
26
27 /** */
28 $options = array( 'quick', 'color', 'quiet', 'help', 'show-output', 'record', 'run-disabled' );
29 $optionsWithArgs = array( 'regex', 'seed', 'setversion' );
30
31 if ( !defined( "NO_COMMAND_LINE" ) ) {
32         require_once( dirname(__FILE__) . '/commandLine.inc' );
33 }
34 require_once( "$IP/maintenance/parserTestsParserHook.php" );
35 require_once( "$IP/maintenance/parserTestsStaticParserHook.php" );
36 require_once( "$IP/maintenance/parserTestsParserTime.php" );
37
38 /**
39  * @ingroup Maintenance
40  */
41 class ParserTest {
42         /**
43          * boolean $color whereas output should be colorized
44          */
45         private $color;
46
47         /**
48          * boolean $showOutput Show test output
49          */
50         private $showOutput;
51
52         /**
53          * boolean $useTemporaryTables Use temporary tables for the temporary database
54          */
55         private $useTemporaryTables = true;
56
57         /**
58          * boolean $databaseSetupDone True if the database has been set up
59          */
60         private $databaseSetupDone = false;
61
62         /**
63          * string $oldTablePrefix Original table prefix
64          */
65         private $oldTablePrefix;
66
67         private $maxFuzzTestLength = 300;
68         private $fuzzSeed = 0;
69         private $memoryLimit = 50;
70
71         /**
72          * Sets terminal colorization and diff/quick modes depending on OS and
73          * command-line options (--color and --quick).
74          */
75         public function ParserTest() {
76                 global $options;
77
78                 # Only colorize output if stdout is a terminal.
79                 $this->color = !wfIsWindows() && posix_isatty(1);
80
81                 if( isset( $options['color'] ) ) {
82                         switch( $options['color'] ) {
83                         case 'no':
84                                 $this->color = false;
85                                 break;
86                         case 'yes':
87                         default:
88                                 $this->color = true;
89                                 break;
90                         }
91                 }
92                 $this->term = $this->color
93                         ? new AnsiTermColorer()
94                         : new DummyTermColorer();
95
96                 $this->showDiffs = !isset( $options['quick'] );
97                 $this->showProgress = !isset( $options['quiet'] );
98                 $this->showFailure = !(
99                         isset( $options['quiet'] )
100                         && ( isset( $options['record'] )
101                                 || isset( $options['compare'] ) ) ); // redundant output
102
103                 $this->showOutput = isset( $options['show-output'] );
104
105
106                 if (isset($options['regex'])) {
107                         if ( isset( $options['record'] ) ) {
108                                 echo "Warning: --record cannot be used with --regex, disabling --record\n";
109                                 unset( $options['record'] );
110                         }
111                         $this->regex = $options['regex'];
112                 } else {
113                         # Matches anything
114                         $this->regex = '';
115                 }
116
117                 if( isset( $options['record'] ) ) {
118                         $this->recorder = new DbTestRecorder( $this );
119                 } elseif( isset( $options['compare'] ) ) {
120                         $this->recorder = new DbTestPreviewer( $this );
121                 } elseif( isset( $options['upload'] ) ) {
122                         $this->recorder = new RemoteTestRecorder( $this );
123                 } elseif( class_exists( 'PHPUnitTestRecorder' ) ) {
124                         $this->recorder = new PHPUnitTestRecorder( $this );
125                 } else {
126                         $this->recorder = new TestRecorder( $this );
127                 }
128                 $this->keepUploads = isset( $options['keep-uploads'] );
129
130                 if ( isset( $options['seed'] ) ) {
131                         $this->fuzzSeed = intval( $options['seed'] ) - 1;
132                 }
133
134                 $this->runDisabled = isset( $options['run-disabled'] );
135
136                 $this->hooks = array();
137                 $this->functionHooks = array();
138         }
139
140         /**
141          * Remove last character if it is a newline
142          */
143         public function chomp($s) {
144                 if (substr($s, -1) === "\n") {
145                         return substr($s, 0, -1);
146                 }
147                 else {
148                         return $s;
149                 }
150         }
151
152         /**
153          * Run a fuzz test series
154          * Draw input from a set of test files
155          */
156         function fuzzTest( $filenames ) {
157                 $dict = $this->getFuzzInput( $filenames );
158                 $dictSize = strlen( $dict );
159                 $logMaxLength = log( $this->maxFuzzTestLength );
160                 $this->setupDatabase();
161                 ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
162
163                 $numTotal = 0;
164                 $numSuccess = 0;
165                 $user = new User;
166                 $opts = ParserOptions::newFromUser( $user );
167                 $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
168
169                 while ( true ) {
170                         // Generate test input
171                         mt_srand( ++$this->fuzzSeed );
172                         $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
173                         $input = '';
174                         while ( strlen( $input ) < $totalLength ) {
175                                 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
176                                 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
177                                 $offset = mt_rand( 0, $dictSize - $hairLength );
178                                 $input .= substr( $dict, $offset, $hairLength );
179                         }
180
181                         $this->setupGlobals();
182                         $parser = $this->getParser();
183                         // Run the test
184                         try {
185                                 $parser->parse( $input, $title, $opts );
186                                 $fail = false;
187                         } catch ( Exception $exception ) {
188                                 $fail = true;
189                         }
190
191                         if ( $fail ) {
192                                 echo "Test failed with seed {$this->fuzzSeed}\n";
193                                 echo "Input:\n";
194                                 var_dump( $input );
195                                 echo "\n\n";
196                                 echo "$exception\n";
197                         } else {
198                                 $numSuccess++;
199                         }
200                         $numTotal++;
201                         $this->teardownGlobals();
202                         $parser->__destruct();
203
204                         if ( $numTotal % 100 == 0 ) {
205                                 $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
206                                 echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
207                                 if ( $usage > 90 ) {
208                                         echo "Out of memory:\n";
209                                         $memStats = $this->getMemoryBreakdown();
210                                         foreach ( $memStats as $name => $usage ) {
211                                                 echo "$name: $usage\n";
212                                         }
213                                         $this->abort();
214                                 }
215                         }
216                 }
217         }
218
219         /**
220          * Get an input dictionary from a set of parser test files
221          */
222         function getFuzzInput( $filenames ) {
223                 $dict = '';
224                 foreach( $filenames as $filename ) {
225                         $contents = file_get_contents( $filename );
226                         preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
227                         foreach ( $matches[1] as $match ) {
228                                 $dict .= $match . "\n";
229                         }
230                 }
231                 return $dict;
232         }
233
234         /**
235          * Get a memory usage breakdown
236          */
237         function getMemoryBreakdown() {
238                 $memStats = array();
239                 foreach ( $GLOBALS as $name => $value ) {
240                         $memStats['$'.$name] = strlen( serialize( $value ) );
241                 }
242                 $classes = get_declared_classes();
243                 foreach ( $classes as $class ) {
244                         $rc = new ReflectionClass( $class );
245                         $props = $rc->getStaticProperties();
246                         $memStats[$class] = strlen( serialize( $props ) );
247                         $methods = $rc->getMethods();
248                         foreach ( $methods as $method ) {
249                                 $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
250                         }
251                 }
252                 $functions = get_defined_functions();
253                 foreach ( $functions['user'] as $function ) {
254                         $rf = new ReflectionFunction( $function );
255                         $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
256                 }
257                 asort( $memStats );
258                 return $memStats;
259         }
260
261         function abort() {
262                 $this->abort();
263         }
264
265         /**
266          * Run a series of tests listed in the given text files.
267          * Each test consists of a brief description, wikitext input,
268          * and the expected HTML output.
269          *
270          * Prints status updates on stdout and counts up the total
271          * number and percentage of passed tests.
272          *
273          * @param array of strings $filenames
274          * @return bool True if passed all tests, false if any tests failed.
275          */
276         public function runTestsFromFiles( $filenames ) {
277                 $this->recorder->start();
278                 $this->setupDatabase();
279                 $ok = true;
280                 foreach( $filenames as $filename ) {
281                         $tests = new TestFileIterator( $filename, $this );
282                         $ok = $this->runTests( $tests ) && $ok;
283                 }
284                 $this->teardownDatabase();
285                 $this->recorder->report();
286                 $this->recorder->end();
287                 return $ok;
288         }
289
290         function runTests($tests) {
291                 $ok = true;
292         foreach($tests as $i => $t) {
293                         $result =
294                                 $this->runTest($t['test'], $t['input'], $t['result'], $t['options'], $t['config']);
295                         $ok = $ok && $result;
296                         $this->recorder->record( $t['test'], $result );
297                 }
298                 if ( $this->showProgress ) {
299                         print "\n";
300                 }
301         }
302
303         /**
304          * Get a Parser object
305          */
306         function getParser() {
307                 global $wgParserConf;
308                 $class = $wgParserConf['class'];
309                 $parser = new $class( $wgParserConf );
310                 foreach( $this->hooks as $tag => $callback ) {
311                         $parser->setHook( $tag, $callback );
312                 }
313                 foreach( $this->functionHooks as $tag => $bits ) {
314                         list( $callback, $flags ) = $bits;
315                         $parser->setFunctionHook( $tag, $callback, $flags );
316                 }
317                 wfRunHooks( 'ParserTestParser', array( &$parser ) );
318                 return $parser;
319         }
320
321         /**
322          * Run a given wikitext input through a freshly-constructed wiki parser,
323          * and compare the output against the expected results.
324          * Prints status and explanatory messages to stdout.
325          *
326          * @param string $input Wikitext to try rendering
327          * @param string $result Result to output
328          * @return bool
329          */
330         public function runTest( $desc, $input, $result, $opts, $config ) {
331                 if( $this->showProgress ) {
332                         $this->showTesting( $desc );
333                 }
334
335                 $opts = $this->parseOptions( $opts );
336                 $this->setupGlobals($opts, $config);
337
338                 $user = new User();
339                 $options = ParserOptions::newFromUser( $user );
340
341                 $m = array();
342                 if (isset( $opts['title'] ) ) {
343                         $titleText = $opts['title'];
344                 }
345                 else {
346                         $titleText = 'Parser test';
347                 }
348
349                 $noxml = isset( $opts['noxml'] );
350                 $local = isset( $opts['local'] );
351                 $parser = $this->getParser();
352                 $title = Title::newFromText( $titleText );
353
354                 $matches = array();
355                 if( isset( $opts['pst'] ) ) {
356                         $out = $parser->preSaveTransform( $input, $title, $user, $options );
357                 } elseif( isset( $opts['msg'] ) ) {
358                         $out = $parser->transformMsg( $input, $options );
359                 } elseif( isset( $opts['section'] ) ) {
360                         $section = $opts['section'];
361                         $out = $parser->getSection( $input, $section );
362                 } elseif( isset( $opts['replace'] ) ) {
363                         $section = $opts['replace'][0];
364                         $replace = $opts['replace'][1];
365                         $out = $parser->replaceSection( $input, $section, $replace );
366                 } elseif( isset( $opts['comment'] ) ) {
367                         $linker = $user->getSkin();
368                         $out = $linker->formatComment( $input, $title, $local );
369                 } else {
370                         $output = $parser->parse( $input, $title, $options, true, true, 1337 );
371                         $out = $output->getText();
372
373                         if ( isset( $opts['showtitle'] ) ) {
374                                 if($output->getTitleText()) $title = $output->getTitleText();
375                                 $out = "$title\n$out";
376                         }
377                         if (isset( $opts['ill'] ) ) {
378                                 $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
379                         } elseif( isset( $opts['cat'] ) ) {
380                                 global $wgOut;
381                                 $wgOut->addCategoryLinks($output->getCategories());
382                                 $cats = $wgOut->getCategoryLinks();
383                                 if ( isset( $cats['normal'] ) ) {
384                                         $out = $this->tidy( implode( ' ', $cats['normal'] ) );
385                                 } else {
386                                         $out = '';
387                                 }
388                         }
389
390                         $result = $this->tidy($result);
391                 }
392
393
394                 $this->teardownGlobals();
395
396                 if( $result === $out && ( $noxml === true || $this->wellFormed( $out ) ) ) {
397                         return $this->showSuccess( $desc );
398                 } else {
399                         return $this->showFailure( $desc, $result, $out );
400                 }
401         }
402
403
404         /**
405          * Use a regex to find out the value of an option
406          * @param $key name of option val to retrieve
407          * @param $opts Options array to look in
408          * @param $defaults Default value returned if not found
409          */
410         private static function getOptionValue( $key, $opts, $default ) {
411                 $key = strtolower( $key );
412                 if( isset( $opts[$key] ) ) {
413                         return $opts[$key];
414                 } else {
415                         return $default;
416                 }
417         }
418
419         private function parseOptions( $instring ) {
420                 $opts = array();
421                 $lines = explode( "\n", $instring );
422                 // foo
423                 // foo=bar
424                 // foo="bar baz"
425                 // foo=[[bar baz]]
426                 // foo=bar,"baz quux"
427                 $regex = '/\b
428                         ([\w-]+)                                                # Key
429                         \b
430                         (?:\s*
431                                 =                                               # First sub-value
432                                 \s*
433                                 (
434                                         "
435                                                 [^"]*                   # Quoted val
436                                         "
437                                 |
438                                         \[\[
439                                                 [^]]*                   # Link target
440                                         \]\]
441                                 |
442                                         [\w-]+                          # Plain word
443                                 )
444                                 (?:\s*
445                                         ,                                       # Sub-vals 1..N
446                                         \s*
447                                         (
448                                                 "[^"]*"                 # Quoted val
449                                         |
450                                                 \[\[[^]]*\]\]   # Link target
451                                         |
452                                                 [\w-]+                  # Plain word
453                                         )
454                                 )*
455                         )?
456                         /x';
457
458                 if( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
459                         foreach( $matches as $bits ) {
460                                 $match = array_shift( $bits );
461                                 $key = strtolower( array_shift( $bits ) );
462                                 if( count( $bits ) == 0 ) {
463                                         $opts[$key] = true;
464                                 } elseif( count( $bits ) == 1 ) {
465                                         $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
466                                 } else {
467                                         // Array!
468                                         $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
469                                 }
470                         }
471                 }
472                 return $opts;
473         }
474
475         private function cleanupOption( $opt ) {
476                 if( substr( $opt, 0, 1 ) == '"' ) {
477                         return substr( $opt, 1, -1 );
478                 }
479                 if( substr( $opt, 0, 2 ) == '[[' ) {
480                         return substr( $opt, 2, -2 );
481                 }
482                 return $opt;
483         }
484
485         /**
486          * Set up the global variables for a consistent environment for each test.
487          * Ideally this should replace the global configuration entirely.
488          */
489         private function setupGlobals($opts = '', $config = '') {
490                 global $wgDBtype;
491                 if( !isset( $this->uploadDir ) ) {
492                         $this->uploadDir = $this->setupUploadDir();
493                 }
494
495                 # Find out values for some special options.
496                 $lang =
497                         self::getOptionValue( 'language', $opts, 'en' );
498                 $variant =
499                         self::getOptionValue( 'variant', $opts, false );
500                 $maxtoclevel =
501                         self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
502                 $linkHolderBatchSize =
503                         self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
504
505                 $settings = array(
506                         'wgServer' => 'http://localhost',
507                         'wgScript' => '/index.php',
508                         'wgScriptPath' => '/',
509                         'wgArticlePath' => '/wiki/$1',
510                         'wgActionPaths' => array(),
511                         'wgLocalFileRepo' => array(
512                                 'class' => 'LocalRepo',
513                                 'name' => 'local',
514                                 'directory' => $this->uploadDir,
515                                 'url' => 'http://example.com/images',
516                                 'hashLevels' => 2,
517                                 'transformVia404' => false,
518                         ),
519                         'wgEnableUploads' => true,
520                         'wgStyleSheetPath' => '/skins',
521                         'wgSitename' => 'MediaWiki',
522                         'wgServerName' => 'Britney-Spears',
523                         'wgLanguageCode' => $lang,
524                         'wgContLanguageCode' => $lang,
525                         'wgDBprefix' => $wgDBtype != 'oracle' ? 'parsertest_' : 'pt_',
526                         'wgRawHtml' => isset( $opts['rawhtml'] ),
527                         'wgLang' => null,
528                         'wgContLang' => null,
529                         'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
530                         'wgMaxTocLevel' => $maxtoclevel,
531                         'wgCapitalLinks' => true,
532                         'wgNoFollowLinks' => true,
533                         'wgNoFollowDomainExceptions' => array(),
534                         'wgThumbnailScriptPath' => false,
535                         'wgUseImageResize' => false,
536                         'wgUseTeX' => isset( $opts['math'] ),
537                         'wgMathDirectory' => $this->uploadDir . '/math',
538                         'wgLocaltimezone' => 'UTC',
539                         'wgAllowExternalImages' => true,
540                         'wgUseTidy' => false,
541                         'wgDefaultLanguageVariant' => $variant,
542                         'wgVariantArticlePath' => false,
543                         'wgGroupPermissions' => array( '*' => array(
544                                 'createaccount' => true,
545                                 'read'          => true,
546                                 'edit'          => true,
547                                 'createpage'    => true,
548                                 'createtalk'    => true,
549                         ) ),
550                         'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
551                         'wgDefaultExternalStore' => array(),
552                         'wgForeignFileRepos' => array(),
553                         'wgLinkHolderBatchSize' => $linkHolderBatchSize,
554                         'wgExperimentalHtmlIds' => false,
555                         'wgExternalLinkTarget' => false,
556                         'wgAlwaysUseTidy' => false,
557                         'wgHtml5' => true,
558                         'wgWellFormedXml' => true,
559                         'wgAllowMicrodataAttributes' => true,
560                 );
561
562                 if ($config) {
563                         $configLines = explode( "\n", $config );
564
565                         foreach( $configLines as $line ) {
566                                 list( $var, $value ) = explode( '=', $line, 2 );
567
568                                 $settings[$var] = eval("return $value;" );
569                         }
570                 }
571
572                 $this->savedGlobals = array();
573                 foreach( $settings as $var => $val ) {
574                         if( array_key_exists( $var, $GLOBALS ) ) {
575                                 $this->savedGlobals[$var] = $GLOBALS[$var];
576                         }
577                         $GLOBALS[$var] = $val;
578                 }
579                 $langObj = Language::factory( $lang );
580                 $GLOBALS['wgLang'] = $langObj;
581                 $GLOBALS['wgContLang'] = $langObj;
582                 $GLOBALS['wgMemc'] = new FakeMemCachedClient;
583                 $GLOBALS['wgOut'] = new OutputPage;
584
585                 MagicWord::clearCache();
586
587                 global $wgUser;
588                 $wgUser = new User();
589         }
590
591         /**
592          * List of temporary tables to create, without prefix.
593          * Some of these probably aren't necessary.
594          */
595         private function listTables() {
596                 global $wgDBtype;
597                 $tables = array('user', 'page', 'page_restrictions',
598                         'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
599                         'categorylinks', 'templatelinks', 'externallinks', 'langlinks',
600                         'site_stats', 'hitcounter',     'ipblocks', 'image', 'oldimage',
601                         'recentchanges', 'watchlist', 'math', 'interwiki',
602                         'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
603                         'archive', 'user_groups', 'page_props', 'category'
604                 );
605
606                 if ($wgDBtype === 'mysql')
607                         array_push( $tables, 'searchindex' );
608
609                 // Allow extensions to add to the list of tables to duplicate;
610                 // may be necessary if they hook into page save or other code
611                 // which will require them while running tests.
612                 wfRunHooks( 'ParserTestTables', array( &$tables ) );
613
614                 return $tables;
615         }
616
617         /**
618          * Set up a temporary set of wiki tables to work with for the tests.
619          * Currently this will only be done once per run, and any changes to
620          * the db will be visible to later tests in the run.
621          */
622         function setupDatabase() {
623                 global $wgDBprefix, $wgDBtype;
624                 if ( $this->databaseSetupDone ) {
625                         return;
626                 }
627                 if ( $wgDBprefix === 'parsertest_' || ($wgDBtype == 'oracle' && $wgDBprefix === 'pt_')) {
628                         throw new MWException( 'setupDatabase should be called before setupGlobals' );
629                 }
630                 $this->databaseSetupDone = true;
631                 $this->oldTablePrefix = $wgDBprefix;
632
633                 # CREATE TEMPORARY TABLE breaks if there is more than one server
634                 # FIXME: r40209 makes temporary tables break even with just one server
635                 # FIXME: (bug 15892); disabling the feature entirely as a temporary fix
636                 if ( true || wfGetLB()->getServerCount() != 1 ) {
637                         $this->useTemporaryTables = false;
638                 }
639
640                 $temporary = $this->useTemporaryTables || $wgDBtype == 'postgres';
641
642                 $db = wfGetDB( DB_MASTER );
643                 $tables = $this->listTables();
644
645                 foreach ( $tables as $tbl ) {
646                         # Clean up from previous aborted run.  So that table escaping
647                         # works correctly across DB engines, we need to change the pre-
648                         # fix back and forth so tableName() works right.
649                         $this->changePrefix( $this->oldTablePrefix );
650                         $oldTableName = $db->tableName( $tbl );
651                         $this->changePrefix( $wgDBtype != 'oracle' ? 'parsertest_' : 'pt_' );
652                         $newTableName = $db->tableName( $tbl );
653
654                         if ( $db->tableExists( $tbl ) && $wgDBtype != 'postgres' && $wgDBtype != 'oracle' ) {
655                                 $db->query( "DROP TABLE $newTableName" );
656                         }
657                         # Create new table
658                         $db->duplicateTableStructure( $oldTableName, $newTableName, $temporary );
659                 }
660                 if ($wgDBtype == 'oracle')
661                         $db->query('BEGIN FILL_WIKI_INFO; END;');
662
663                 $this->changePrefix( $wgDBtype != 'oracle' ? 'parsertest_' : 'pt_' );
664
665                 # Hack: insert a few Wikipedia in-project interwiki prefixes,
666                 # for testing inter-language links
667                 $db->insert( 'interwiki', array(
668                         array( 'iw_prefix' => 'wikipedia',
669                                    'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
670                                    'iw_local'  => 0 ),
671                         array( 'iw_prefix' => 'meatball',
672                                    'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
673                                    'iw_local'  => 0 ),
674                         array( 'iw_prefix' => 'zh',
675                                    'iw_url'    => 'http://zh.wikipedia.org/wiki/$1',
676                                    'iw_local'  => 1 ),
677                         array( 'iw_prefix' => 'es',
678                                    'iw_url'    => 'http://es.wikipedia.org/wiki/$1',
679                                    'iw_local'  => 1 ),
680                         array( 'iw_prefix' => 'fr',
681                                    'iw_url'    => 'http://fr.wikipedia.org/wiki/$1',
682                                    'iw_local'  => 1 ),
683                         array( 'iw_prefix' => 'ru',
684                                    'iw_url'    => 'http://ru.wikipedia.org/wiki/$1',
685                                    'iw_local'  => 1 ),
686                         ) );
687
688
689                 if ($wgDBtype == 'oracle') {
690                         # Insert 0 and 1 user_ids to prevent FK violations
691
692                         #Anonymous user
693                         $db->insert( 'user', array(
694                                 'user_id'         => 0,
695                                 'user_name'       => 'Anonymous') );
696
697                         # Hack-on-Hack: Insert a test user to be able to insert an image
698                         $db->insert( 'user', array(
699                                 'user_id'         => 1,
700                                 'user_name'       => 'Tester') );
701                 }
702
703                 # Hack: Insert an image to work with
704                 $db->insert( 'image', array(
705                         'img_name'        => 'Foobar.jpg',
706                         'img_size'        => 12345,
707                         'img_description' => 'Some lame file',
708                         'img_user'        => 1,
709                         'img_user_text'   => 'WikiSysop',
710                         'img_timestamp'   => $db->timestamp( '20010115123500' ),
711                         'img_width'       => 1941,
712                         'img_height'      => 220,
713                         'img_bits'        => 24,
714                         'img_media_type'  => MEDIATYPE_BITMAP,
715                         'img_major_mime'  => "image",
716                         'img_minor_mime'  => "jpeg",
717                         'img_metadata'    => serialize( array() ),
718                         ) );
719
720                 # This image will be blacklisted in [[MediaWiki:Bad image list]]
721                 $db->insert( 'image', array(
722                         'img_name'        => 'Bad.jpg',
723                         'img_size'        => 12345,
724                         'img_description' => 'zomgnotcensored',
725                         'img_user'        => 1,
726                         'img_user_text'   => 'WikiSysop',
727                         'img_timestamp'   => $db->timestamp( '20010115123500' ),
728                         'img_width'       => 320,
729                         'img_height'      => 240,
730                         'img_bits'        => 24,
731                         'img_media_type'  => MEDIATYPE_BITMAP,
732                         'img_major_mime'  => "image",
733                         'img_minor_mime'  => "jpeg",
734                         'img_metadata'    => serialize( array() ),
735                         ) );
736
737                 # Update certain things in site_stats
738                 $db->insert( 'site_stats', array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ) );
739
740                 # Reinitialise the LocalisationCache to match the database state
741                 Language::getLocalisationCache()->unloadAll();
742
743                 # Make a new message cache
744                 global $wgMessageCache, $wgMemc;
745                 $wgMessageCache = new MessageCache( $wgMemc, true, 3600, '' );
746         }
747
748         /**
749          * Change the table prefix on all open DB connections/
750          */
751         protected function changePrefix( $prefix ) {
752                 global $wgDBprefix;
753                 wfGetLBFactory()->forEachLB( array( $this, 'changeLBPrefix' ), array( $prefix ) );
754                 $wgDBprefix = $prefix;
755         }
756
757         public function changeLBPrefix( $lb, $prefix ) {
758                 $lb->forEachOpenConnection( array( $this, 'changeDBPrefix' ), array( $prefix ) );
759         }
760
761         public function changeDBPrefix( $db, $prefix ) {
762                 $db->tablePrefix( $prefix );
763         }
764
765         private function teardownDatabase() {
766                 global $wgDBtype;
767                 if ( !$this->databaseSetupDone ) {
768                         return;
769                 }
770                 $this->changePrefix( $this->oldTablePrefix );
771                 $this->databaseSetupDone = false;
772                 if ( $this->useTemporaryTables ) {
773                         # Don't need to do anything
774                         return;
775                 }
776
777                 /*
778                 $tables = $this->listTables();
779                 $db = wfGetDB( DB_MASTER );
780                 foreach ( $tables as $table ) {
781                         $sql = $wgDBtype == 'oracle' ? "DROP TABLE pt_$table DROP CONSTRAINTS" : "DROP TABLE `parsertest_$table`";
782                         $db->query( $sql );
783                 }
784                 if ($wgDBtype == 'oracle')
785                         $db->query('BEGIN FILL_WIKI_INFO; END;');
786                 */
787         }
788
789         /**
790          * Create a dummy uploads directory which will contain a couple
791          * of files in order to pass existence tests.
792          * @return string The directory
793          */
794         private function setupUploadDir() {
795                 global $IP;
796                 if ( $this->keepUploads ) {
797                         $dir = wfTempDir() . '/mwParser-images';
798                         if ( is_dir( $dir ) ) {
799                                 return $dir;
800                         }
801                 } else {
802                         $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
803                 }
804
805                 wfDebug( "Creating upload directory $dir\n" );
806                 if ( file_exists( $dir ) ) {
807                         wfDebug( "Already exists!\n" );
808                         return $dir;
809                 }
810                 wfMkdirParents( $dir . '/3/3a' );
811                 copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
812
813                 wfMkdirParents( $dir . '/0/09' );
814                 copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" );
815                 return $dir;
816         }
817
818         /**
819          * Restore default values and perform any necessary clean-up
820          * after each test runs.
821          */
822         private function teardownGlobals() {
823                 RepoGroup::destroySingleton();
824                 LinkCache::singleton()->clear();
825                 foreach( $this->savedGlobals as $var => $val ) {
826                         $GLOBALS[$var] = $val;
827                 }
828                 if( isset( $this->uploadDir ) ) {
829                         $this->teardownUploadDir( $this->uploadDir );
830                         unset( $this->uploadDir );
831                 }
832         }
833
834         /**
835          * Remove the dummy uploads directory
836          */
837         private function teardownUploadDir( $dir ) {
838                 if ( $this->keepUploads ) {
839                         return;
840                 }
841
842                 // delete the files first, then the dirs.
843                 self::deleteFiles(
844                         array (
845                                 "$dir/3/3a/Foobar.jpg",
846                                 "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
847                                 "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
848                                 "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
849                                 "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
850
851                                 "$dir/0/09/Bad.jpg",
852
853                                 "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
854                         )
855                 );
856
857                 self::deleteDirs(
858                         array (
859                                 "$dir/3/3a",
860                                 "$dir/3",
861                                 "$dir/thumb/6/65",
862                                 "$dir/thumb/6",
863                                 "$dir/thumb/3/3a/Foobar.jpg",
864                                 "$dir/thumb/3/3a",
865                                 "$dir/thumb/3",
866
867                                 "$dir/0/09/",
868                                 "$dir/0/",
869                                 "$dir/thumb",
870                                 "$dir/math/f/a/5",
871                                 "$dir/math/f/a",
872                                 "$dir/math/f",
873                                 "$dir/math",
874                                 "$dir",
875                         )
876                 );
877         }
878
879         /**
880          * Delete the specified files, if they exist.
881          * @param array $files full paths to files to delete.
882          */
883         private static function deleteFiles( $files ) {
884                 foreach( $files as $file ) {
885                         if( file_exists( $file ) ) {
886                                 unlink( $file );
887                         }
888                 }
889         }
890
891         /**
892          * Delete the specified directories, if they exist. Must be empty.
893          * @param array $dirs full paths to directories to delete.
894          */
895         private static function deleteDirs( $dirs ) {
896                 foreach( $dirs as $dir ) {
897                         if( is_dir( $dir ) ) {
898                                 rmdir( $dir );
899                         }
900                 }
901         }
902
903         /**
904          * "Running test $desc..."
905          */
906         protected function showTesting( $desc ) {
907                 print "Running test $desc... ";
908         }
909
910         /**
911          * Print a happy success message.
912          *
913          * @param string $desc The test name
914          * @return bool
915          */
916         protected function showSuccess( $desc ) {
917                 if( $this->showProgress ) {
918                         print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
919                 }
920                 return true;
921         }
922
923         /**
924          * Print a failure message and provide some explanatory output
925          * about what went wrong if so configured.
926          *
927          * @param string $desc The test name
928          * @param string $result Expected HTML output
929          * @param string $html Actual HTML output
930          * @return bool
931          */
932         protected function showFailure( $desc, $result, $html ) {
933                 if( $this->showFailure ) {
934                         if( !$this->showProgress ) {
935                                 # In quiet mode we didn't show the 'Testing' message before the
936                                 # test, in case it succeeded. Show it now:
937                                 $this->showTesting( $desc );
938                         }
939                         print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
940                         if ( $this->showOutput ) {
941                                 print "--- Expected ---\n$result\n--- Actual ---\n$html\n";
942                         }
943                         if( $this->showDiffs ) {
944                                 print $this->quickDiff( $result, $html );
945                                 if( !$this->wellFormed( $html ) ) {
946                                         print "XML error: $this->mXmlError\n";
947                                 }
948                         }
949                 }
950                 return false;
951         }
952
953         /**
954          * Run given strings through a diff and return the (colorized) output.
955          * Requires writable /tmp directory and a 'diff' command in the PATH.
956          *
957          * @param string $input
958          * @param string $output
959          * @param string $inFileTail Tailing for the input file name
960          * @param string $outFileTail Tailing for the output file name
961          * @return string
962          */
963         protected function quickDiff( $input, $output, $inFileTail='expected', $outFileTail='actual' ) {
964                 $prefix = wfTempDir() . "/mwParser-" . mt_rand();
965
966                 $infile = "$prefix-$inFileTail";
967                 $this->dumpToFile( $input, $infile );
968
969                 $outfile = "$prefix-$outFileTail";
970                 $this->dumpToFile( $output, $outfile );
971
972                 $diff = `diff -au $infile $outfile`;
973                 unlink( $infile );
974                 unlink( $outfile );
975
976                 return $this->colorDiff( $diff );
977         }
978
979         /**
980          * Write the given string to a file, adding a final newline.
981          *
982          * @param string $data
983          * @param string $filename
984          */
985         private function dumpToFile( $data, $filename ) {
986                 $file = fopen( $filename, "wt" );
987                 fwrite( $file, $data . "\n" );
988                 fclose( $file );
989         }
990
991         /**
992          * Colorize unified diff output if set for ANSI color output.
993          * Subtractions are colored blue, additions red.
994          *
995          * @param string $text
996          * @return string
997          */
998         protected function colorDiff( $text ) {
999                 return preg_replace(
1000                         array( '/^(-.*)$/m', '/^(\+.*)$/m' ),
1001                         array( $this->term->color( 34 ) . '$1' . $this->term->reset(),
1002                                $this->term->color( 31 ) . '$1' . $this->term->reset() ),
1003                         $text );
1004         }
1005
1006         /**
1007          * Show "Reading tests from ..."
1008          *
1009          * @param String $path
1010          */
1011         public function showRunFile( $path ){
1012                 print $this->term->color( 1 ) .
1013                         "Reading tests from \"$path\"..." .
1014                         $this->term->reset() .
1015                         "\n";
1016         }
1017
1018         /**
1019          * Insert a temporary test article
1020          * @param string $name the title, including any prefix
1021          * @param string $text the article text
1022          * @param int $line the input line number, for reporting errors
1023          */
1024         public function addArticle($name, $text, $line) {
1025                 $this->setupGlobals();
1026                 $title = Title::newFromText( $name );
1027                 if ( is_null($title) ) {
1028                         wfDie( "invalid title at line $line\n" );
1029                 }
1030
1031                 $aid = $title->getArticleID( GAID_FOR_UPDATE );
1032                 if ($aid != 0) {
1033                         wfDie( "duplicate article '$name' at line $line\n" );
1034                 }
1035
1036                 $art = new Article($title);
1037                 $art->insertNewArticle($text, '', false, false );
1038
1039                 $this->teardownGlobals();
1040         }
1041
1042         /**
1043          * Steal a callback function from the primary parser, save it for
1044          * application to our scary parser. If the hook is not installed,
1045          * die a painful dead to warn the others.
1046          * @param string $name
1047          */
1048         public function requireHook( $name ) {
1049                 global $wgParser;
1050                 $wgParser->firstCallInit( ); //make sure hooks are loaded.
1051                 if( isset( $wgParser->mTagHooks[$name] ) ) {
1052                         $this->hooks[$name] = $wgParser->mTagHooks[$name];
1053                 } else {
1054                         wfDie( "This test suite requires the '$name' hook extension.\n" );
1055                 }
1056         }
1057
1058         /**
1059          * Steal a callback function from the primary parser, save it for
1060          * application to our scary parser. If the hook is not installed,
1061          * die a painful dead to warn the others.
1062          * @param string $name
1063          */
1064         private function requireFunctionHook( $name ) {
1065                 global $wgParser;
1066                 $wgParser->firstCallInit( ); //make sure hooks are loaded.
1067                 if( isset( $wgParser->mFunctionHooks[$name] ) ) {
1068                         $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name];
1069                 } else {
1070                         wfDie( "This test suite requires the '$name' function hook extension.\n" );
1071                 }
1072         }
1073
1074         /*
1075          * Run the "tidy" command on text if the $wgUseTidy
1076          * global is true
1077          *
1078          * @param string $text the text to tidy
1079          * @return string
1080          * @static
1081          */
1082         private function tidy( $text ) {
1083                 global $wgUseTidy;
1084                 if ($wgUseTidy) {
1085                         $text = Parser::tidy($text);
1086                 }
1087                 return $text;
1088         }
1089
1090         private function wellFormed( $text ) {
1091                 $html =
1092                         Sanitizer::hackDocType() .
1093                         '<html>' .
1094                         $text .
1095                         '</html>';
1096
1097                 $parser = xml_parser_create( "UTF-8" );
1098
1099                 # case folding violates XML standard, turn it off
1100                 xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
1101
1102                 if( !xml_parse( $parser, $html, true ) ) {
1103                         $err = xml_error_string( xml_get_error_code( $parser ) );
1104                         $position = xml_get_current_byte_index( $parser );
1105                         $fragment = $this->extractFragment( $html, $position );
1106                         $this->mXmlError = "$err at byte $position:\n$fragment";
1107                         xml_parser_free( $parser );
1108                         return false;
1109                 }
1110                 xml_parser_free( $parser );
1111                 return true;
1112         }
1113
1114         private function extractFragment( $text, $position ) {
1115                 $start = max( 0, $position - 10 );
1116                 $before = $position - $start;
1117                 $fragment = '...' .
1118                         $this->term->color( 34 ) .
1119                         substr( $text, $start, $before ) .
1120                         $this->term->color( 0 ) .
1121                         $this->term->color( 31 ) .
1122                         $this->term->color( 1 ) .
1123                         substr( $text, $position, 1 ) .
1124                         $this->term->color( 0 ) .
1125                         $this->term->color( 34 ) .
1126                         substr( $text, $position + 1, 9 ) .
1127                         $this->term->color( 0 ) .
1128                         '...';
1129                 $display = str_replace( "\n", ' ', $fragment );
1130                 $caret = '   ' .
1131                         str_repeat( ' ', $before ) .
1132                         $this->term->color( 31 ) .
1133                         '^' .
1134                         $this->term->color( 0 );
1135                 return "$display\n$caret";
1136         }
1137 }
1138
1139 class AnsiTermColorer {
1140         function __construct() {
1141         }
1142
1143         /**
1144          * Return ANSI terminal escape code for changing text attribs/color
1145          *
1146          * @param string $color Semicolon-separated list of attribute/color codes
1147          * @return string
1148          */
1149         public function color( $color ) {
1150                 global $wgCommandLineDarkBg;
1151                 $light = $wgCommandLineDarkBg ? "1;" : "0;";
1152                 return "\x1b[{$light}{$color}m";
1153         }
1154
1155         /**
1156          * Return ANSI terminal escape code for restoring default text attributes
1157          *
1158          * @return string
1159          */
1160         public function reset() {
1161                 return $this->color( 0 );
1162         }
1163 }
1164
1165 /* A colour-less terminal */
1166 class DummyTermColorer {
1167         public function color( $color ) {
1168                 return '';
1169         }
1170
1171         public function reset() {
1172                 return '';
1173         }
1174 }
1175
1176 class TestRecorder {
1177         var $parent;
1178         var $term;
1179
1180         function __construct( $parent ) {
1181                 $this->parent = $parent;
1182                 $this->term = $parent->term;
1183         }
1184
1185         function start() {
1186                 $this->total = 0;
1187                 $this->success = 0;
1188         }
1189
1190         function record( $test, $result ) {
1191                 $this->total++;
1192                 $this->success += ($result ? 1 : 0);
1193         }
1194
1195         function end() {
1196                 // dummy
1197         }
1198
1199         function report() {
1200                 if( $this->total > 0 ) {
1201                         $this->reportPercentage( $this->success, $this->total );
1202                 } else {
1203                         wfDie( "No tests found.\n" );
1204                 }
1205         }
1206
1207         function reportPercentage( $success, $total ) {
1208                 $ratio = wfPercent( 100 * $success / $total );
1209                 print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)... ";
1210                 if( $success == $total ) {
1211                         print $this->term->color( 32 ) . "ALL TESTS PASSED!";
1212                 } else {
1213                         $failed = $total - $success ;
1214                         print $this->term->color( 31 ) . "$failed tests failed!";
1215                 }
1216                 print $this->term->reset() . "\n";
1217                 return ($success == $total);
1218         }
1219 }
1220
1221 class DbTestPreviewer extends TestRecorder  {
1222         protected $lb;      ///< Database load balancer
1223         protected $db;      ///< Database connection to the main DB
1224         protected $curRun;  ///< run ID number for the current run
1225         protected $prevRun; ///< run ID number for the previous run, if any
1226         protected $results; ///< Result array
1227
1228         /**
1229          * This should be called before the table prefix is changed
1230          */
1231         function __construct( $parent ) {
1232                 parent::__construct( $parent );
1233                 $this->lb = wfGetLBFactory()->newMainLB();
1234                 // This connection will have the wiki's table prefix, not parsertest_
1235                 $this->db = $this->lb->getConnection( DB_MASTER );
1236         }
1237
1238         /**
1239          * Set up result recording; insert a record for the run with the date
1240          * and all that fun stuff
1241          */
1242         function start() {
1243                 global $wgDBtype;
1244                 parent::start();
1245
1246                 if( ! $this->db->tableExists( 'testrun' )
1247                         or ! $this->db->tableExists( 'testitem' ) )
1248                 {
1249                         print "WARNING> `testrun` table not found in database.\n";
1250                         $this->prevRun = false;
1251                 } else {
1252                         // We'll make comparisons against the previous run later...
1253                         $this->prevRun = $this->db->selectField( 'testrun', 'MAX(tr_id)' );
1254                 }
1255                 $this->results = array();
1256         }
1257
1258         function record( $test, $result ) {
1259                 parent::record( $test, $result );
1260                 $this->results[$test] = $result;
1261         }
1262
1263         function report() {
1264                 if( $this->prevRun ) {
1265                         // f = fail, p = pass, n = nonexistent
1266                         // codes show before then after
1267                         $table = array(
1268                                 'fp' => 'previously failing test(s) now PASSING! :)',
1269                                 'pn' => 'previously PASSING test(s) removed o_O',
1270                                 'np' => 'new PASSING test(s) :)',
1271
1272                                 'pf' => 'previously passing test(s) now FAILING! :(',
1273                                 'fn' => 'previously FAILING test(s) removed O_o',
1274                                 'nf' => 'new FAILING test(s) :(',
1275                                 'ff' => 'still FAILING test(s) :(',
1276                         );
1277
1278                         $prevResults = array();
1279
1280                         $res = $this->db->select( 'testitem', array( 'ti_name', 'ti_success' ),
1281                                 array( 'ti_run' => $this->prevRun ), __METHOD__ );
1282                         foreach ( $res as $row ) {
1283                                 if ( !$this->parent->regex
1284                                         || preg_match( "/{$this->parent->regex}/i", $row->ti_name ) )
1285                                 {
1286                                         $prevResults[$row->ti_name] = $row->ti_success;
1287                                 }
1288                         }
1289
1290                         $combined = array_keys( $this->results + $prevResults );
1291
1292                         # Determine breakdown by change type
1293                         $breakdown = array();
1294                         foreach ( $combined as $test ) {
1295                                 if ( !isset( $prevResults[$test] ) ) {
1296                                         $before = 'n';
1297                                 } elseif ( $prevResults[$test] == 1 ) {
1298                                         $before = 'p';
1299                                 } else /* if ( $prevResults[$test] == 0 )*/ {
1300                                         $before = 'f';
1301                                 }
1302                                 if ( !isset( $this->results[$test] ) ) {
1303                                         $after = 'n';
1304                                 } elseif ( $this->results[$test] == 1 ) {
1305                                         $after = 'p';
1306                                 } else /*if ( $this->results[$test] == 0 ) */ {
1307                                         $after = 'f';
1308                                 }
1309                                 $code = $before . $after;
1310                                 if ( isset( $table[$code] ) ) {
1311                                         $breakdown[$code][$test] = $this->getTestStatusInfo( $test, $after );
1312                                 }
1313                         }
1314
1315                         # Write out results
1316                         foreach ( $table as $code => $label ) {
1317                                 if( !empty( $breakdown[$code] ) ) {
1318                                         $count = count($breakdown[$code]);
1319                                         printf( "\n%4d %s\n", $count, $label );
1320                                         foreach ($breakdown[$code] as $differing_test_name => $statusInfo) {
1321                                                 print "      * $differing_test_name  [$statusInfo]\n";
1322                                         }
1323                                 }
1324                         }
1325                 } else {
1326                         print "No previous test runs to compare against.\n";
1327                 }
1328                 print "\n";
1329                 parent::report();
1330         }
1331
1332         /**
1333          ** Returns a string giving information about when a test last had a status change.
1334          ** Could help to track down when regressions were introduced, as distinct from tests
1335          ** which have never passed (which are more change requests than regressions).
1336          */
1337         private function getTestStatusInfo($testname, $after) {
1338
1339                 // If we're looking at a test that has just been removed, then say when it first appeared.
1340                 if ( $after == 'n' ) {
1341                         $changedRun = $this->db->selectField ( 'testitem',
1342                                                                                                    'MIN(ti_run)',
1343                                                                                                    array( 'ti_name' => $testname ),
1344                                                                                                    __METHOD__ );
1345                         $appear = $this->db->selectRow ( 'testrun',
1346                                                                                          array( 'tr_date', 'tr_mw_version' ),
1347                                                                                          array( 'tr_id' => $changedRun ),
1348                                                                                          __METHOD__ );
1349                         return "First recorded appearance: "
1350                                . date( "d-M-Y H:i:s",  strtotime ( $appear->tr_date ) )
1351                                .  ", " . $appear->tr_mw_version;
1352                 }
1353
1354                 // Otherwise, this test has previous recorded results.
1355                 // See when this test last had a different result to what we're seeing now.
1356                 $conds = array(
1357                         'ti_name'    => $testname,
1358                         'ti_success' => ($after == 'f' ? "1" : "0") );
1359                 if ( $this->curRun ) {
1360                         $conds[] = "ti_run != " . $this->db->addQuotes ( $this->curRun );
1361                 }
1362
1363                 $changedRun = $this->db->selectField ( 'testitem', 'MAX(ti_run)', $conds, __METHOD__ );
1364
1365                 // If no record of ever having had a different result.
1366                 if ( is_null ( $changedRun ) ) {
1367                         if ($after == "f") {
1368                                 return "Has never passed";
1369                         } else {
1370                                 return "Has never failed";
1371                         }
1372                 }
1373
1374                 // Otherwise, we're looking at a test whose status has changed.
1375                 // (i.e. it used to work, but now doesn't; or used to fail, but is now fixed.)
1376                 // In this situation, give as much info as we can as to when it changed status.
1377                 $pre  = $this->db->selectRow ( 'testrun',
1378                                                                                 array( 'tr_date', 'tr_mw_version' ),
1379                                                                                 array( 'tr_id' => $changedRun ),
1380                                                                                 __METHOD__ );
1381                 $post = $this->db->selectRow ( 'testrun',
1382                                                                                 array( 'tr_date', 'tr_mw_version' ),
1383                                                                                 array( "tr_id > " . $this->db->addQuotes ( $changedRun) ),
1384                                                                                 __METHOD__,
1385                                                                                 array( "LIMIT" => 1, "ORDER BY" => 'tr_id' )
1386                                                                          );
1387
1388                 if ( $post ) {
1389                         $postDate = date( "d-M-Y H:i:s",  strtotime ( $post->tr_date  ) ) . ", {$post->tr_mw_version}";
1390                 } else {
1391                         $postDate = 'now';
1392                 }
1393                 return ( $after == "f" ? "Introduced" : "Fixed" ) . " between "
1394                                 . date( "d-M-Y H:i:s",  strtotime ( $pre->tr_date ) ) .  ", " . $pre->tr_mw_version
1395                                 . " and $postDate";
1396
1397         }
1398
1399         /**
1400          * Commit transaction and clean up for result recording
1401          */
1402         function end() {
1403                 $this->lb->commitMasterChanges();
1404                 $this->lb->closeAll();
1405                 parent::end();
1406         }
1407
1408 }
1409
1410 class DbTestRecorder extends DbTestPreviewer  {
1411         /**
1412          * Set up result recording; insert a record for the run with the date
1413          * and all that fun stuff
1414          */
1415         function start() {
1416                 global $wgDBtype, $options;
1417                 $this->db->begin();
1418
1419                 if( ! $this->db->tableExists( 'testrun' )
1420                         or ! $this->db->tableExists( 'testitem' ) )
1421                 {
1422                         print "WARNING> `testrun` table not found in database. Trying to create table.\n";
1423                         if ($wgDBtype === 'postgres')
1424                                 $this->db->sourceFile( dirname(__FILE__) . '/testRunner.postgres.sql' );
1425                         elseif ($wgDBtype === 'oracle')
1426                                 $this->db->sourceFile( dirname(__FILE__) . '/testRunner.ora.sql' );
1427                         else
1428                                 $this->db->sourceFile( dirname(__FILE__) . '/testRunner.sql' );
1429                         echo "OK, resuming.\n";
1430                 }
1431
1432                 parent::start();
1433
1434                 $this->db->insert( 'testrun',
1435                         array(
1436                                 'tr_date'        => $this->db->timestamp(),
1437                                 'tr_mw_version'  => isset( $options['setversion'] ) ?
1438                                         $options['setversion'] : SpecialVersion::getVersion(),
1439                                 'tr_php_version' => phpversion(),
1440                                 'tr_db_version'  => $this->db->getServerVersion(),
1441                                 'tr_uname'       => php_uname()
1442                         ),
1443                         __METHOD__ );
1444                         if ($wgDBtype === 'postgres')
1445                                 $this->curRun = $this->db->currentSequenceValue('testrun_id_seq');
1446                         else
1447                                 $this->curRun = $this->db->insertId();
1448         }
1449
1450         /**
1451          * Record an individual test item's success or failure to the db
1452          * @param string $test
1453          * @param bool $result
1454          */
1455         function record( $test, $result ) {
1456                 parent::record( $test, $result );
1457                 $this->db->insert( 'testitem',
1458                         array(
1459                                 'ti_run'     => $this->curRun,
1460                                 'ti_name'    => $test,
1461                                 'ti_success' => $result ? 1 : 0,
1462                         ),
1463                         __METHOD__ );
1464         }
1465 }
1466
1467 class RemoteTestRecorder extends TestRecorder {
1468         function start() {
1469                 parent::start();
1470                 $this->results = array();
1471                 $this->ping( 'running' );
1472         }
1473
1474         function record( $test, $result ) {
1475                 parent::record( $test, $result );
1476                 $this->results[$test] = (bool)$result;
1477         }
1478
1479         function end() {
1480                 $this->ping( 'complete', $this->results );
1481                 parent::end();
1482         }
1483
1484         /**
1485          * Inform a CodeReview instance that we've started or completed a test run...
1486          * @param $remote array: info on remote target
1487          * @param $status string: "running" - tell it we've started
1488          *                        "complete" - provide test results array
1489          *                        "abort" - something went horribly awry
1490          * @param $data array of test name => true/false
1491          */
1492         function ping( $status, $results=false ) {
1493                 global $wgParserTestRemote, $IP;
1494
1495                 $remote = $wgParserTestRemote;
1496                 $revId = SpecialVersion::getSvnRevision( $IP );
1497                 $jsonResults = json_encode( $results );
1498
1499                 if( !$remote ) {
1500                         print "Can't do remote upload without configuring \$wgParserTestRemote!\n";
1501                         exit( 1 );
1502                 }
1503
1504                 // Generate a hash MAC to validate our credentials
1505                 $message = array(
1506                         $remote['repo'],
1507                         $remote['suite'],
1508                         $revId,
1509                         $status,
1510                 );
1511                 if( $status == "complete" ) {
1512                         $message[] = $jsonResults;
1513                 }
1514                 $hmac = hash_hmac( "sha1", implode( "|", $message ), $remote['secret'] );
1515
1516                 $postData = array(
1517                         'action' => 'codetestupload',
1518                         'format' => 'json',
1519                         'repo'   => $remote['repo'],
1520                         'suite'  => $remote['suite'],
1521                         'rev'    => $revId,
1522                         'status' => $status,
1523                         'hmac'   => $hmac,
1524                 );
1525                 if( $status == "complete" ) {
1526                         $postData['results'] = $jsonResults;
1527                 }
1528                 $response = $this->post( $remote['api-url'], $postData );
1529
1530                 if( $response === false ) {
1531                         print "CodeReview info upload failed to reach server.\n";
1532                         exit( 1 );
1533                 }
1534                 $responseData = json_decode( $response, true );
1535                 if( !is_array( $responseData ) ) {
1536                         print "CodeReview API response not recognized...\n";
1537                         wfDebug( "Unrecognized CodeReview API response: $response\n" );
1538                         exit( 1 );
1539                 }
1540                 if( isset( $responseData['error'] ) ) {
1541                         $code = $responseData['error']['code'];
1542                         $info = $responseData['error']['info'];
1543                         print "CodeReview info upload failed: $code $info\n";
1544                         exit( 1 );
1545                 }
1546         }
1547
1548         function post( $url, $data ) {
1549                 return Http::post( $url, array( 'postData' => $data) );
1550         }
1551 }
1552
1553 class TestFileIterator implements Iterator {
1554     private $file;
1555     private $fh;
1556     private $parser;
1557     private $index = 0;
1558     private $test;
1559         private $lineNum;
1560         private $eof;
1561
1562         function __construct( $file, $parser = null ) {
1563                 global $IP;
1564
1565                 $this->file = $file;
1566         $this->fh = fopen($this->file, "rt");
1567         if( !$this->fh ) {
1568                         wfDie( "Couldn't open file '$file'\n" );
1569                 }
1570
1571                 $this->parser = $parser;
1572
1573                 if( $this->parser ) $this->parser->showRunFile( wfRelativePath( $this->file, $IP ) );
1574                 $this->lineNum = $this->index = 0;
1575         }
1576
1577         function setParser( ParserTest $parser ) {
1578                 $this->parser = $parser;
1579         }
1580
1581         function rewind() {
1582                 if(fseek($this->fh, 0)) {
1583                         wfDie( "Couldn't fseek to the start of '$filename'\n" );
1584                 }
1585                 $this->index = 0;
1586                 $this->lineNum = 0;
1587                 $this->eof = false;
1588                 $this->readNextTest();
1589
1590                 return true;
1591     }
1592
1593     function current() {
1594                 return $this->test;
1595     }
1596
1597     function key() {
1598                 return $this->index;
1599     }
1600
1601     function next() {
1602         if($this->readNextTest()) {
1603                         $this->index++;
1604                         return true;
1605                 } else {
1606                         $this->eof = true;
1607                 }
1608     }
1609
1610     function valid() {
1611                 return $this->eof != true;
1612     }
1613
1614         function readNextTest() {
1615                 $data = array();
1616                 $section = null;
1617
1618                 while( false !== ($line = fgets( $this->fh ) ) ) {
1619                         $this->lineNum++;
1620                         $matches = array();
1621                         if( preg_match( '/^!!\s*(\w+)/', $line, $matches ) ) {
1622                                 $section = strtolower( $matches[1] );
1623                                 if( $section == 'endarticle') {
1624                                         if( !isset( $data['text'] ) ) {
1625                                                 wfDie( "'endarticle' without 'text' at line {$this->lineNum} of $filename\n" );
1626                                         }
1627                                         if( !isset( $data['article'] ) ) {
1628                                                 wfDie( "'endarticle' without 'article' at line {$this->lineNum} of $filename\n" );
1629                                         }
1630                                         if( $this->parser ) $this->parser->addArticle($this->parser->chomp($data['article']), $this->parser->chomp($data['text']),
1631                                                                                           $this->lineNum);
1632                                         $data = array();
1633                                         $section = null;
1634                                         continue;
1635                                 }
1636                                 if( $section == 'endhooks' ) {
1637                                         if( !isset( $data['hooks'] ) ) {
1638                                                 wfDie( "'endhooks' without 'hooks' at line {$this->lineNum} of $filename\n" );
1639                                         }
1640                                         foreach( explode( "\n", $data['hooks'] ) as $line ) {
1641                                                 $line = trim( $line );
1642                                                 if( $line ) {
1643                                                         if( $this->parser ) $this->parser->requireHook( $line );
1644                                                 }
1645                                         }
1646                                         $data = array();
1647                                         $section = null;
1648                                         continue;
1649                                 }
1650                                 if( $section == 'endfunctionhooks' ) {
1651                                         if( !isset( $data['functionhooks'] ) ) {
1652                                                 wfDie( "'endfunctionhooks' without 'functionhooks' at line {$this->lineNum} of $filename\n" );
1653                                         }
1654                                         foreach( explode( "\n", $data['functionhooks'] ) as $line ) {
1655                                                 $line = trim( $line );
1656                                                 if( $line ) {
1657                                                         if( $this->parser ) $this->parser->requireFunctionHook( $line );
1658                                                 }
1659                                         }
1660                                         $data = array();
1661                                         $section = null;
1662                                         continue;
1663                                 }
1664                                 if( $section == 'end' ) {
1665                                         if( !isset( $data['test'] ) ) {
1666                                                 wfDie( "'end' without 'test' at line {$this->lineNum} of $filename\n" );
1667                                         }
1668                                         if( !isset( $data['input'] ) ) {
1669                                                 wfDie( "'end' without 'input' at line {$this->lineNum} of $filename\n" );
1670                                         }
1671                                         if( !isset( $data['result'] ) ) {
1672                                                 wfDie( "'end' without 'result' at line {$this->lineNum} of $filename\n" );
1673                                         }
1674                                         if( !isset( $data['options'] ) ) {
1675                                                 $data['options'] = '';
1676                                         }
1677                                         if (!isset( $data['config'] ) )
1678                                                 $data['config'] = '';
1679
1680                                         if ( $this->parser && (preg_match('/\\bdisabled\\b/i', $data['options'])
1681                                                 || !preg_match("/{$this->parser->regex}/i", $data['test'])) && !$this->parser->runDisabled ) {
1682                                                 # disabled test
1683                                                 $data = array();
1684                                                 $section = null;
1685                                                 continue;
1686                                         }
1687                                         if ( $this->parser &&
1688                                                  preg_match('/\\bmath\\b/i', $data['options']) && !$this->parser->savedGlobals['wgUseTeX'] ) {
1689                                                 # don't run math tests if $wgUseTeX is set to false in LocalSettings
1690                                                 $data = array();
1691                                                 $section = null;
1692                                                 continue;
1693                                         }
1694
1695                                         if( $this->parser ) {
1696                                                 $this->test = array(
1697                                                         'test' => $this->parser->chomp( $data['test'] ),
1698                                                         'input' => $this->parser->chomp( $data['input'] ),
1699                                                         'result' => $this->parser->chomp( $data['result'] ),
1700                                                         'options' => $this->parser->chomp( $data['options'] ),
1701                                                         'config' => $this->parser->chomp( $data['config'] ) );
1702                                         } else {
1703                                                 $this->test['test'] = $data['test'];
1704                                         }
1705                                         return true;
1706                                 }
1707                                 if ( isset ($data[$section] ) ) {
1708                                         wfDie( "duplicate section '$section' at line {$this->lineNum} of $filename\n" );
1709                                 }
1710                                 $data[$section] = '';
1711                                 continue;
1712                         }
1713                         if( $section ) {
1714                                 $data[$section] .= $line;
1715                         }
1716                 }
1717                 return false;
1718         }
1719 }