~ubuntu-branches/ubuntu/trusty/mediawiki/trusty-proposed

« back to all changes in this revision

Viewing changes to tests/parser/parserTest.inc

  • Committer: Package Import Robot
  • Author(s): Thorsten Glaser
  • Date: 2014-03-28 09:56:29 UTC
  • mfrom: (1.3.14)
  • Revision ID: package-import@ubuntu.com-20140328095629-1526y9tchdd507id
Tags: 1:1.19.14+dfsg-1
* New upstream security fix release (Closes: #742857):
  - (bug 62497) SECURITY: Add CSRF token on Special:ChangePassword
  - (bug 62467) Set a title for the context during import on the cli
* Use upstream-provided signing key bundle

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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 Testing
 
25
 */
 
26
 
 
27
/**
 
28
 * @ingroup Testing
 
29
 */
 
30
class ParserTest {
 
31
        /**
 
32
         * boolean $color whereas output should be colorized
 
33
         */
 
34
        private $color;
 
35
 
 
36
        /**
 
37
         * boolean $showOutput Show test output
 
38
         */
 
39
        private $showOutput;
 
40
 
 
41
        /**
 
42
         * boolean $useTemporaryTables Use temporary tables for the temporary database
 
43
         */
 
44
        private $useTemporaryTables = true;
 
45
 
 
46
        /**
 
47
         * boolean $databaseSetupDone True if the database has been set up
 
48
         */
 
49
        private $databaseSetupDone = false;
 
50
 
 
51
        /**
 
52
         * Our connection to the database
 
53
         * @var DatabaseBase
 
54
         */
 
55
        private $db;
 
56
 
 
57
        /**
 
58
         * Database clone helper
 
59
         * @var CloneDatabase
 
60
         */
 
61
        private $dbClone;
 
62
 
 
63
        /**
 
64
         * string $oldTablePrefix Original table prefix
 
65
         */
 
66
        private $oldTablePrefix;
 
67
 
 
68
        private $maxFuzzTestLength = 300;
 
69
        private $fuzzSeed = 0;
 
70
        private $memoryLimit = 50;
 
71
        private $uploadDir = null;
 
72
 
 
73
        public $regex = "";
 
74
        private $savedGlobals = array();
 
75
        /**
 
76
         * Sets terminal colorization and diff/quick modes depending on OS and
 
77
         * command-line options (--color and --quick).
 
78
         */
 
79
        public function __construct( $options = array() ) {
 
80
                # Only colorize output if stdout is a terminal.
 
81
                $this->color = !wfIsWindows() && Maintenance::posix_isatty( 1 );
 
82
 
 
83
                if ( isset( $options['color'] ) ) {
 
84
                        switch( $options['color'] ) {
 
85
                        case 'no':
 
86
                                $this->color = false;
 
87
                                break;
 
88
                        case 'yes':
 
89
                        default:
 
90
                                $this->color = true;
 
91
                                break;
 
92
                        }
 
93
                }
 
94
 
 
95
                $this->term = $this->color
 
96
                        ? new AnsiTermColorer()
 
97
                        : new DummyTermColorer();
 
98
 
 
99
                $this->showDiffs = !isset( $options['quick'] );
 
100
                $this->showProgress = !isset( $options['quiet'] );
 
101
                $this->showFailure = !(
 
102
                        isset( $options['quiet'] )
 
103
                        && ( isset( $options['record'] )
 
104
                                || isset( $options['compare'] ) ) ); // redundant output
 
105
 
 
106
                $this->showOutput = isset( $options['show-output'] );
 
107
 
 
108
                if ( isset( $options['filter'] ) ) {
 
109
                        $options['regex'] = $options['filter'];
 
110
                }
 
111
 
 
112
                if ( isset( $options['regex'] ) ) {
 
113
                        if ( isset( $options['record'] ) ) {
 
114
                                echo "Warning: --record cannot be used with --regex, disabling --record\n";
 
115
                                unset( $options['record'] );
 
116
                        }
 
117
                        $this->regex = $options['regex'];
 
118
                } else {
 
119
                        # Matches anything
 
120
                        $this->regex = '';
 
121
                }
 
122
 
 
123
                $this->setupRecorder( $options );
 
124
                $this->keepUploads = isset( $options['keep-uploads'] );
 
125
 
 
126
                if ( isset( $options['seed'] ) ) {
 
127
                        $this->fuzzSeed = intval( $options['seed'] ) - 1;
 
128
                }
 
129
 
 
130
                $this->runDisabled = isset( $options['run-disabled'] );
 
131
 
 
132
                $this->hooks = array();
 
133
                $this->functionHooks = array();
 
134
                self::setUp();
 
135
        }
 
136
 
 
137
        static function setUp() {
 
138
                global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc,
 
139
                        $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, $wgEnableParserCache,
 
140
                        $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo,
 
141
                        $parserMemc, $wgThumbnailScriptPath, $wgScriptPath,
 
142
                        $wgArticlePath, $wgStyleSheetPath, $wgScript, $wgStylePath, $wgExtensionAssetsPath,
 
143
                        $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType;
 
144
 
 
145
                $wgScript = '/index.php';
 
146
                $wgScriptPath = '/';
 
147
                $wgArticlePath = '/wiki/$1';
 
148
                $wgStyleSheetPath = '/skins';
 
149
                $wgStylePath = '/skins';
 
150
                $wgExtensionAssetsPath = '/extensions';
 
151
                $wgThumbnailScriptPath = false;
 
152
                $wgLocalFileRepo = array(
 
153
                        'class'           => 'LocalRepo',
 
154
                        'name'            => 'local',
 
155
                        'url'             => 'http://example.com/images',
 
156
                        'hashLevels'      => 2,
 
157
                        'transformVia404' => false,
 
158
                        'backend'         => new FSFileBackend( array(
 
159
                                'name'        => 'local-backend',
 
160
                                'lockManager' => 'fsLockManager',
 
161
                                'containerPaths' => array(
 
162
                                        'local-public'  => wfTempDir() . '/test-repo/public',
 
163
                                        'local-thumb'   => wfTempDir() . '/test-repo/thumb',
 
164
                                        'local-temp'    => wfTempDir() . '/test-repo/temp',
 
165
                                        'local-deleted' => wfTempDir() . '/test-repo/deleted',
 
166
                                )
 
167
                        ) )
 
168
                );
 
169
                $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
 
170
                $wgNamespaceAliases['Image'] = NS_FILE;
 
171
                $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
 
172
 
 
173
                // XXX: tests won't run without this (for CACHE_DB)
 
174
                if ( $wgMainCacheType === CACHE_DB ) {
 
175
                        $wgMainCacheType = CACHE_NONE;
 
176
                }
 
177
                if ( $wgMessageCacheType === CACHE_DB ) {
 
178
                        $wgMessageCacheType = CACHE_NONE;
 
179
                }
 
180
                if ( $wgParserCacheType === CACHE_DB ) {
 
181
                        $wgParserCacheType = CACHE_NONE;
 
182
                }
 
183
 
 
184
                $wgEnableParserCache = false;
 
185
                DeferredUpdates::clearPendingUpdates();
 
186
                $wgMemc = wfGetMainCache(); // checks $wgMainCacheType
 
187
                $messageMemc = wfGetMessageCacheStorage();
 
188
                $parserMemc = wfGetParserCacheStorage();
 
189
 
 
190
                // $wgContLang = new StubContLang;
 
191
                $wgUser = new User;
 
192
                $context = new RequestContext;
 
193
                $wgLang = $context->getLanguage();
 
194
                $wgOut = $context->getOutput();
 
195
                $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
 
196
                $wgRequest = $context->getRequest();
 
197
 
 
198
                if ( $wgStyleDirectory === false ) {
 
199
                        $wgStyleDirectory   = "$IP/skins";
 
200
                }
 
201
 
 
202
        }
 
203
 
 
204
        public function setupRecorder ( $options ) {
 
205
                if ( isset( $options['record'] ) ) {
 
206
                        $this->recorder = new DbTestRecorder( $this );
 
207
                        $this->recorder->version = isset( $options['setversion'] ) ?
 
208
                                        $options['setversion'] : SpecialVersion::getVersion();
 
209
                } elseif ( isset( $options['compare'] ) ) {
 
210
                        $this->recorder = new DbTestPreviewer( $this );
 
211
                } else {
 
212
                        $this->recorder = new TestRecorder( $this );
 
213
                }
 
214
        }
 
215
 
 
216
        /**
 
217
         * Remove last character if it is a newline
 
218
         * @group utility
 
219
         */
 
220
        static public function chomp( $s ) {
 
221
                if ( substr( $s, -1 ) === "\n" ) {
 
222
                        return substr( $s, 0, -1 );
 
223
                }
 
224
                else {
 
225
                        return $s;
 
226
                }
 
227
        }
 
228
 
 
229
        /**
 
230
         * Run a fuzz test series
 
231
         * Draw input from a set of test files
 
232
         */
 
233
        function fuzzTest( $filenames ) {
 
234
                $GLOBALS['wgContLang'] = Language::factory( 'en' );
 
235
                $dict = $this->getFuzzInput( $filenames );
 
236
                $dictSize = strlen( $dict );
 
237
                $logMaxLength = log( $this->maxFuzzTestLength );
 
238
                $this->setupDatabase();
 
239
                ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
 
240
 
 
241
                $numTotal = 0;
 
242
                $numSuccess = 0;
 
243
                $user = new User;
 
244
                $opts = ParserOptions::newFromUser( $user );
 
245
                $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
 
246
 
 
247
                while ( true ) {
 
248
                        // Generate test input
 
249
                        mt_srand( ++$this->fuzzSeed );
 
250
                        $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
 
251
                        $input = '';
 
252
 
 
253
                        while ( strlen( $input ) < $totalLength ) {
 
254
                                $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
 
255
                                $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
 
256
                                $offset = mt_rand( 0, $dictSize - $hairLength );
 
257
                                $input .= substr( $dict, $offset, $hairLength );
 
258
                        }
 
259
 
 
260
                        $this->setupGlobals();
 
261
                        $parser = $this->getParser();
 
262
 
 
263
                        // Run the test
 
264
                        try {
 
265
                                $parser->parse( $input, $title, $opts );
 
266
                                $fail = false;
 
267
                        } catch ( Exception $exception ) {
 
268
                                $fail = true;
 
269
                        }
 
270
 
 
271
                        if ( $fail ) {
 
272
                                echo "Test failed with seed {$this->fuzzSeed}\n";
 
273
                                echo "Input:\n";
 
274
                                printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
 
275
                                echo "$exception\n";
 
276
                        } else {
 
277
                                $numSuccess++;
 
278
                        }
 
279
 
 
280
                        $numTotal++;
 
281
                        $this->teardownGlobals();
 
282
                        $parser->__destruct();
 
283
 
 
284
                        if ( $numTotal % 100 == 0 ) {
 
285
                                $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
 
286
                                echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
 
287
                                if ( $usage > 90 ) {
 
288
                                        echo "Out of memory:\n";
 
289
                                        $memStats = $this->getMemoryBreakdown();
 
290
 
 
291
                                        foreach ( $memStats as $name => $usage ) {
 
292
                                                echo "$name: $usage\n";
 
293
                                        }
 
294
                                        $this->abort();
 
295
                                }
 
296
                        }
 
297
                }
 
298
        }
 
299
 
 
300
        /**
 
301
         * Get an input dictionary from a set of parser test files
 
302
         */
 
303
        function getFuzzInput( $filenames ) {
 
304
                $dict = '';
 
305
 
 
306
                foreach ( $filenames as $filename ) {
 
307
                        $contents = file_get_contents( $filename );
 
308
                        preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
 
309
 
 
310
                        foreach ( $matches[1] as $match ) {
 
311
                                $dict .= $match . "\n";
 
312
                        }
 
313
                }
 
314
 
 
315
                return $dict;
 
316
        }
 
317
 
 
318
        /**
 
319
         * Get a memory usage breakdown
 
320
         */
 
321
        function getMemoryBreakdown() {
 
322
                $memStats = array();
 
323
 
 
324
                foreach ( $GLOBALS as $name => $value ) {
 
325
                        $memStats['$' . $name] = strlen( serialize( $value ) );
 
326
                }
 
327
 
 
328
                $classes = get_declared_classes();
 
329
 
 
330
                foreach ( $classes as $class ) {
 
331
                        $rc = new ReflectionClass( $class );
 
332
                        $props = $rc->getStaticProperties();
 
333
                        $memStats[$class] = strlen( serialize( $props ) );
 
334
                        $methods = $rc->getMethods();
 
335
 
 
336
                        foreach ( $methods as $method ) {
 
337
                                $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
 
338
                        }
 
339
                }
 
340
 
 
341
                $functions = get_defined_functions();
 
342
 
 
343
                foreach ( $functions['user'] as $function ) {
 
344
                        $rf = new ReflectionFunction( $function );
 
345
                        $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
 
346
                }
 
347
 
 
348
                asort( $memStats );
 
349
 
 
350
                return $memStats;
 
351
        }
 
352
 
 
353
        function abort() {
 
354
                $this->abort();
 
355
        }
 
356
 
 
357
        /**
 
358
         * Run a series of tests listed in the given text files.
 
359
         * Each test consists of a brief description, wikitext input,
 
360
         * and the expected HTML output.
 
361
         *
 
362
         * Prints status updates on stdout and counts up the total
 
363
         * number and percentage of passed tests.
 
364
         *
 
365
         * @param $filenames Array of strings
 
366
         * @return Boolean: true if passed all tests, false if any tests failed.
 
367
         */
 
368
        public function runTestsFromFiles( $filenames ) {
 
369
                $ok = false;
 
370
                $GLOBALS['wgContLang'] = Language::factory( 'en' );
 
371
                $this->recorder->start();
 
372
                try {
 
373
                        $this->setupDatabase();
 
374
                        $ok = true;
 
375
 
 
376
                        foreach ( $filenames as $filename ) {
 
377
                                $tests = new TestFileIterator( $filename, $this );
 
378
                                $ok = $this->runTests( $tests ) && $ok;
 
379
                        }
 
380
 
 
381
                        $this->teardownDatabase();
 
382
                        $this->recorder->report();
 
383
                } catch (DBError $e) {
 
384
                        echo $e->getMessage();
 
385
                }
 
386
                $this->recorder->end();
 
387
 
 
388
                return $ok;
 
389
        }
 
390
 
 
391
        function runTests( $tests ) {
 
392
                $ok = true;
 
393
 
 
394
                foreach ( $tests as $t ) {
 
395
                        $result =
 
396
                                $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] );
 
397
                        $ok = $ok && $result;
 
398
                        $this->recorder->record( $t['test'], $result );
 
399
                }
 
400
 
 
401
                if ( $this->showProgress ) {
 
402
                        print "\n";
 
403
                }
 
404
 
 
405
                return $ok;
 
406
        }
 
407
 
 
408
        /**
 
409
         * Get a Parser object
 
410
         */
 
411
        function getParser( $preprocessor = null ) {
 
412
                global $wgParserConf;
 
413
 
 
414
                $class = $wgParserConf['class'];
 
415
                $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
 
416
 
 
417
                foreach ( $this->hooks as $tag => $callback ) {
 
418
                        $parser->setHook( $tag, $callback );
 
419
                }
 
420
 
 
421
                foreach ( $this->functionHooks as $tag => $bits ) {
 
422
                        list( $callback, $flags ) = $bits;
 
423
                        $parser->setFunctionHook( $tag, $callback, $flags );
 
424
                }
 
425
 
 
426
                wfRunHooks( 'ParserTestParser', array( &$parser ) );
 
427
 
 
428
                return $parser;
 
429
        }
 
430
 
 
431
        /**
 
432
         * Run a given wikitext input through a freshly-constructed wiki parser,
 
433
         * and compare the output against the expected results.
 
434
         * Prints status and explanatory messages to stdout.
 
435
         *
 
436
         * @param $desc String: test's description
 
437
         * @param $input String: wikitext to try rendering
 
438
         * @param $result String: result to output
 
439
         * @param $opts Array: test's options
 
440
         * @param $config String: overrides for global variables, one per line
 
441
         * @return Boolean
 
442
         */
 
443
        public function runTest( $desc, $input, $result, $opts, $config ) {
 
444
                if ( $this->showProgress ) {
 
445
                        $this->showTesting( $desc );
 
446
                }
 
447
 
 
448
                $opts = $this->parseOptions( $opts );
 
449
                $context = $this->setupGlobals( $opts, $config );
 
450
 
 
451
                $user = $context->getUser();
 
452
                $options = ParserOptions::newFromContext( $context );
 
453
 
 
454
                if ( isset( $opts['title'] ) ) {
 
455
                        $titleText = $opts['title'];
 
456
                }
 
457
                else {
 
458
                        $titleText = 'Parser test';
 
459
                }
 
460
 
 
461
                $local = isset( $opts['local'] );
 
462
                $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
 
463
                $parser = $this->getParser( $preprocessor );
 
464
                $title = Title::newFromText( $titleText );
 
465
 
 
466
                if ( isset( $opts['pst'] ) ) {
 
467
                        $out = $parser->preSaveTransform( $input, $title, $user, $options );
 
468
                } elseif ( isset( $opts['msg'] ) ) {
 
469
                        $out = $parser->transformMsg( $input, $options, $title );
 
470
                } elseif ( isset( $opts['section'] ) ) {
 
471
                        $section = $opts['section'];
 
472
                        $out = $parser->getSection( $input, $section );
 
473
                } elseif ( isset( $opts['replace'] ) ) {
 
474
                        $section = $opts['replace'][0];
 
475
                        $replace = $opts['replace'][1];
 
476
                        $out = $parser->replaceSection( $input, $section, $replace );
 
477
                } elseif ( isset( $opts['comment'] ) ) {
 
478
                        $out = Linker::formatComment( $input, $title, $local );
 
479
                } elseif ( isset( $opts['preload'] ) ) {
 
480
                        $out = $parser->getpreloadText( $input, $title, $options );
 
481
                } else {
 
482
                        $output = $parser->parse( $input, $title, $options, true, true, 1337 );
 
483
                        $out = $output->getText();
 
484
 
 
485
                        if ( isset( $opts['showtitle'] ) ) {
 
486
                                if ( $output->getTitleText() ) {
 
487
                                        $title = $output->getTitleText();
 
488
                                }
 
489
 
 
490
                                $out = "$title\n$out";
 
491
                        }
 
492
 
 
493
                        if ( isset( $opts['ill'] ) ) {
 
494
                                $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
 
495
                        } elseif ( isset( $opts['cat'] ) ) {
 
496
                                $outputPage = $context->getOutput();
 
497
                                $outputPage->addCategoryLinks( $output->getCategories() );
 
498
                                $cats = $outputPage->getCategoryLinks();
 
499
 
 
500
                                if ( isset( $cats['normal'] ) ) {
 
501
                                        $out = $this->tidy( implode( ' ', $cats['normal'] ) );
 
502
                                } else {
 
503
                                        $out = '';
 
504
                                }
 
505
                        }
 
506
 
 
507
                        $result = $this->tidy( $result );
 
508
                }
 
509
 
 
510
                $this->teardownGlobals();
 
511
                return $this->showTestResult( $desc, $result, $out );
 
512
        }
 
513
 
 
514
        /**
 
515
         *
 
516
         */
 
517
        function showTestResult( $desc, $result, $out ) {
 
518
                if ( $result === $out ) {
 
519
                        $this->showSuccess( $desc );
 
520
                        return true;
 
521
                } else {
 
522
                        $this->showFailure( $desc, $result, $out );
 
523
                        return false;
 
524
                }
 
525
        }
 
526
 
 
527
        /**
 
528
         * Use a regex to find out the value of an option
 
529
         * @param $key String: name of option val to retrieve
 
530
         * @param $opts Options array to look in
 
531
         * @param $default Mixed: default value returned if not found
 
532
         */
 
533
        private static function getOptionValue( $key, $opts, $default ) {
 
534
                $key = strtolower( $key );
 
535
 
 
536
                if ( isset( $opts[$key] ) ) {
 
537
                        return $opts[$key];
 
538
                } else {
 
539
                        return $default;
 
540
                }
 
541
        }
 
542
 
 
543
        private function parseOptions( $instring ) {
 
544
                $opts = array();
 
545
                // foo
 
546
                // foo=bar
 
547
                // foo="bar baz"
 
548
                // foo=[[bar baz]]
 
549
                // foo=bar,"baz quux"
 
550
                $regex = '/\b
 
551
                        ([\w-]+)                                                # Key
 
552
                        \b
 
553
                        (?:\s*
 
554
                                =                                               # First sub-value
 
555
                                \s*
 
556
                                (
 
557
                                        "
 
558
                                                [^"]*                   # Quoted val
 
559
                                        "
 
560
                                |
 
561
                                        \[\[
 
562
                                                [^]]*                   # Link target
 
563
                                        \]\]
 
564
                                |
 
565
                                        [\w-]+                          # Plain word
 
566
                                )
 
567
                                (?:\s*
 
568
                                        ,                                       # Sub-vals 1..N
 
569
                                        \s*
 
570
                                        (
 
571
                                                "[^"]*"                 # Quoted val
 
572
                                        |
 
573
                                                \[\[[^]]*\]\]   # Link target
 
574
                                        |
 
575
                                                [\w-]+                  # Plain word
 
576
                                        )
 
577
                                )*
 
578
                        )?
 
579
                        /x';
 
580
 
 
581
                if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
 
582
                        foreach ( $matches as $bits ) {
 
583
                                array_shift( $bits );
 
584
                                $key = strtolower( array_shift( $bits ) );
 
585
                                if ( count( $bits ) == 0 ) {
 
586
                                        $opts[$key] = true;
 
587
                                } elseif ( count( $bits ) == 1 ) {
 
588
                                        $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
 
589
                                } else {
 
590
                                        // Array!
 
591
                                        $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
 
592
                                }
 
593
                        }
 
594
                }
 
595
                return $opts;
 
596
        }
 
597
 
 
598
        private function cleanupOption( $opt ) {
 
599
                if ( substr( $opt, 0, 1 ) == '"' ) {
 
600
                        return substr( $opt, 1, -1 );
 
601
                }
 
602
 
 
603
                if ( substr( $opt, 0, 2 ) == '[[' ) {
 
604
                        return substr( $opt, 2, -2 );
 
605
                }
 
606
                return $opt;
 
607
        }
 
608
 
 
609
        /**
 
610
         * Set up the global variables for a consistent environment for each test.
 
611
         * Ideally this should replace the global configuration entirely.
 
612
         */
 
613
        private function setupGlobals( $opts = '', $config = '' ) {
 
614
                # Find out values for some special options.
 
615
                $lang =
 
616
                        self::getOptionValue( 'language', $opts, 'en' );
 
617
                $variant =
 
618
                        self::getOptionValue( 'variant', $opts, false );
 
619
                $maxtoclevel =
 
620
                        self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
 
621
                $linkHolderBatchSize =
 
622
                        self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
 
623
 
 
624
                $settings = array(
 
625
                        'wgServer' => 'http://Britney-Spears',
 
626
                        'wgScript' => '/index.php',
 
627
                        'wgScriptPath' => '/',
 
628
                        'wgArticlePath' => '/wiki/$1',
 
629
                        'wgActionPaths' => array(),
 
630
                        'wgLocalFileRepo' => array(
 
631
                                'class' => 'LocalRepo',
 
632
                                'name' => 'local',
 
633
                                'url' => 'http://example.com/images',
 
634
                                'hashLevels' => 2,
 
635
                                'transformVia404' => false,
 
636
                                'backend'         => new FSFileBackend( array(
 
637
                                        'name'        => 'local-backend',
 
638
                                        'lockManager' => 'fsLockManager',
 
639
                                        'containerPaths' => array(
 
640
                                                'local-public'  => $this->uploadDir,
 
641
                                                'local-thumb'   => $this->uploadDir . '/thumb',
 
642
                                                'local-temp'    => $this->uploadDir . '/temp',
 
643
                                                'local-deleted' => $this->uploadDir . '/delete',
 
644
                                        )
 
645
                                ) )
 
646
                        ),
 
647
                        'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
 
648
                        'wgStylePath' => '/skins',
 
649
                        'wgStyleSheetPath' => '/skins',
 
650
                        'wgSitename' => 'MediaWiki',
 
651
                        'wgLanguageCode' => $lang,
 
652
                        'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_',
 
653
                        'wgRawHtml' => isset( $opts['rawhtml'] ),
 
654
                        'wgLang' => null,
 
655
                        'wgContLang' => null,
 
656
                        'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
 
657
                        'wgMaxTocLevel' => $maxtoclevel,
 
658
                        'wgCapitalLinks' => true,
 
659
                        'wgNoFollowLinks' => true,
 
660
                        'wgNoFollowDomainExceptions' => array(),
 
661
                        'wgThumbnailScriptPath' => false,
 
662
                        'wgUseImageResize' => false,
 
663
                        'wgLocaltimezone' => 'UTC',
 
664
                        'wgAllowExternalImages' => true,
 
665
                        'wgUseTidy' => false,
 
666
                        'wgDefaultLanguageVariant' => $variant,
 
667
                        'wgVariantArticlePath' => false,
 
668
                        'wgGroupPermissions' => array( '*' => array(
 
669
                                'createaccount' => true,
 
670
                                'read'          => true,
 
671
                                'edit'          => true,
 
672
                                'createpage'    => true,
 
673
                                'createtalk'    => true,
 
674
                        ) ),
 
675
                        'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
 
676
                        'wgDefaultExternalStore' => array(),
 
677
                        'wgForeignFileRepos' => array(),
 
678
                        'wgLinkHolderBatchSize' => $linkHolderBatchSize,
 
679
                        'wgExperimentalHtmlIds' => false,
 
680
                        'wgExternalLinkTarget' => false,
 
681
                        'wgAlwaysUseTidy' => false,
 
682
                        'wgHtml5' => true,
 
683
                        'wgCleanupPresentationalAttributes' => true,
 
684
                        'wgWellFormedXml' => true,
 
685
                        'wgAllowMicrodataAttributes' => true,
 
686
                        'wgAdaptiveMessageCache' => true,
 
687
                        'wgDisableLangConversion' => false,
 
688
                        'wgDisableTitleConversion' => false,
 
689
                );
 
690
 
 
691
                if ( $config ) {
 
692
                        $configLines = explode( "\n", $config );
 
693
 
 
694
                        foreach ( $configLines as $line ) {
 
695
                                list( $var, $value ) = explode( '=', $line, 2 );
 
696
 
 
697
                                $settings[$var] = eval( "return $value;" );
 
698
                        }
 
699
                }
 
700
 
 
701
                $this->savedGlobals = array();
 
702
 
 
703
                foreach ( $settings as $var => $val ) {
 
704
                        if ( array_key_exists( $var, $GLOBALS ) ) {
 
705
                                $this->savedGlobals[$var] = $GLOBALS[$var];
 
706
                        }
 
707
 
 
708
                        $GLOBALS[$var] = $val;
 
709
                }
 
710
 
 
711
                $GLOBALS['wgContLang'] = Language::factory( $lang );
 
712
                $GLOBALS['wgMemc'] = new EmptyBagOStuff;
 
713
 
 
714
                $context = new RequestContext();
 
715
                $GLOBALS['wgLang'] = $context->getLanguage();
 
716
                $GLOBALS['wgOut'] = $context->getOutput();
 
717
 
 
718
                $GLOBALS['wgUser'] = new User();
 
719
 
 
720
                global $wgHooks;
 
721
 
 
722
                $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
 
723
                $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
 
724
 
 
725
                MagicWord::clearCache();
 
726
 
 
727
                return $context;
 
728
        }
 
729
 
 
730
        /**
 
731
         * List of temporary tables to create, without prefix.
 
732
         * Some of these probably aren't necessary.
 
733
         */
 
734
        private function listTables() {
 
735
                $tables = array( 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
 
736
                        'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
 
737
                        'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
 
738
                        'site_stats', 'hitcounter',     'ipblocks', 'image', 'oldimage',
 
739
                        'recentchanges', 'watchlist', 'interwiki', 'logging',
 
740
                        'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
 
741
                        'archive', 'user_groups', 'page_props', 'category', 'msg_resource', 'msg_resource_links'
 
742
                );
 
743
 
 
744
                if ( in_array( $this->db->getType(), array( 'mysql', 'sqlite', 'oracle' ) ) ) {
 
745
                        array_push( $tables, 'searchindex' );
 
746
                }
 
747
 
 
748
                // Allow extensions to add to the list of tables to duplicate;
 
749
                // may be necessary if they hook into page save or other code
 
750
                // which will require them while running tests.
 
751
                wfRunHooks( 'ParserTestTables', array( &$tables ) );
 
752
 
 
753
                return $tables;
 
754
        }
 
755
 
 
756
        /**
 
757
         * Set up a temporary set of wiki tables to work with for the tests.
 
758
         * Currently this will only be done once per run, and any changes to
 
759
         * the db will be visible to later tests in the run.
 
760
         */
 
761
        public function setupDatabase() {
 
762
                global $wgDBprefix;
 
763
 
 
764
                if ( $this->databaseSetupDone ) {
 
765
                        return;
 
766
                }
 
767
 
 
768
                $this->db = wfGetDB( DB_MASTER );
 
769
                $dbType = $this->db->getType();
 
770
 
 
771
                if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) {
 
772
                        throw new MWException( 'setupDatabase should be called before setupGlobals' );
 
773
                }
 
774
 
 
775
                $this->databaseSetupDone = true;
 
776
                $this->oldTablePrefix = $wgDBprefix;
 
777
 
 
778
                # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892).
 
779
                # It seems to have been fixed since (r55079?), but regressed at some point before r85701.
 
780
                # This works around it for now...
 
781
                ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
 
782
 
 
783
                # CREATE TEMPORARY TABLE breaks if there is more than one server
 
784
                if ( wfGetLB()->getServerCount() != 1 ) {
 
785
                        $this->useTemporaryTables = false;
 
786
                }
 
787
 
 
788
                $temporary = $this->useTemporaryTables || $dbType == 'postgres';
 
789
                $prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_';
 
790
 
 
791
                $this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix );
 
792
                $this->dbClone->useTemporaryTables( $temporary );
 
793
                $this->dbClone->cloneTableStructure();
 
794
 
 
795
                if ( $dbType == 'oracle' ) {
 
796
                        $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
 
797
                        # Insert 0 user to prevent FK violations
 
798
 
 
799
                        # Anonymous user
 
800
                        $this->db->insert( 'user', array(
 
801
                                'user_id'         => 0,
 
802
                                'user_name'       => 'Anonymous' ) );
 
803
                }
 
804
 
 
805
                # Hack: insert a few Wikipedia in-project interwiki prefixes,
 
806
                # for testing inter-language links
 
807
                $this->db->insert( 'interwiki', array(
 
808
                        array( 'iw_prefix' => 'wikipedia',
 
809
                                   'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
 
810
                                   'iw_api'    => '',
 
811
                                   'iw_wikiid' => '',
 
812
                                   'iw_local'  => 0 ),
 
813
                        array( 'iw_prefix' => 'meatball',
 
814
                                   'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
 
815
                                   'iw_api'    => '',
 
816
                                   'iw_wikiid' => '',
 
817
                                   'iw_local'  => 0 ),
 
818
                        array( 'iw_prefix' => 'zh',
 
819
                                   'iw_url'    => 'http://zh.wikipedia.org/wiki/$1',
 
820
                                   'iw_api'    => '',
 
821
                                   'iw_wikiid' => '',
 
822
                                   'iw_local'  => 1 ),
 
823
                        array( 'iw_prefix' => 'es',
 
824
                                   'iw_url'    => 'http://es.wikipedia.org/wiki/$1',
 
825
                                   'iw_api'    => '',
 
826
                                   'iw_wikiid' => '',
 
827
                                   'iw_local'  => 1 ),
 
828
                        array( 'iw_prefix' => 'fr',
 
829
                                   'iw_url'    => 'http://fr.wikipedia.org/wiki/$1',
 
830
                                   'iw_api'    => '',
 
831
                                   'iw_wikiid' => '',
 
832
                                   'iw_local'  => 1 ),
 
833
                        array( 'iw_prefix' => 'ru',
 
834
                                   'iw_url'    => 'http://ru.wikipedia.org/wiki/$1',
 
835
                                   'iw_api'    => '',
 
836
                                   'iw_wikiid' => '',
 
837
                                   'iw_local'  => 1 ),
 
838
                        ) );
 
839
 
 
840
                # Update certain things in site_stats
 
841
                $this->db->insert( 'site_stats', array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ) );
 
842
 
 
843
                # Reinitialise the LocalisationCache to match the database state
 
844
                Language::getLocalisationCache()->unloadAll();
 
845
 
 
846
                # Clear the message cache
 
847
                MessageCache::singleton()->clear();
 
848
 
 
849
                $this->uploadDir = $this->setupUploadDir();
 
850
                $user = User::createNew( 'WikiSysop' );
 
851
                $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
 
852
                $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', array(
 
853
                        'size'        => 12345,
 
854
                        'width'       => 1941,
 
855
                        'height'      => 220,
 
856
                        'bits'        => 24,
 
857
                        'media_type'  => MEDIATYPE_BITMAP,
 
858
                        'mime'        => 'image/jpeg',
 
859
                        'metadata'    => serialize( array() ),
 
860
                        'sha1'        => wfBaseConvert( '', 16, 36, 31 ),
 
861
                        'fileExists'  => true
 
862
                        ), $this->db->timestamp( '20010115123500' ), $user );
 
863
 
 
864
                # This image will be blacklisted in [[MediaWiki:Bad image list]]
 
865
                $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
 
866
                $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', array(
 
867
                        'size'        => 12345,
 
868
                        'width'       => 320,
 
869
                        'height'      => 240,
 
870
                        'bits'        => 24,
 
871
                        'media_type'  => MEDIATYPE_BITMAP,
 
872
                        'mime'        => 'image/jpeg',
 
873
                        'metadata'    => serialize( array() ),
 
874
                        'sha1'        => wfBaseConvert( '', 16, 36, 31 ),
 
875
                        'fileExists'  => true
 
876
                        ), $this->db->timestamp( '20010115123500' ), $user );
 
877
        }
 
878
 
 
879
        public function teardownDatabase() {
 
880
                if ( !$this->databaseSetupDone ) {
 
881
                        $this->teardownGlobals();
 
882
                        return;
 
883
                }
 
884
                $this->teardownUploadDir( $this->uploadDir );
 
885
 
 
886
                $this->dbClone->destroy();
 
887
                $this->databaseSetupDone = false;
 
888
 
 
889
                if ( $this->useTemporaryTables ) {
 
890
                        if( $this->db->getType() == 'sqlite' ) {
 
891
                                # Under SQLite the searchindex table is virtual and need
 
892
                                # to be explicitly destroyed. See bug 29912
 
893
                                # See also MediaWikiTestCase::destroyDB()
 
894
                                wfDebug( __METHOD__ . " explicitly destroying sqlite virtual table parsertest_searchindex\n" );
 
895
                                $this->db->query( "DROP TABLE `parsertest_searchindex`" );
 
896
                        }
 
897
                        # Don't need to do anything
 
898
                        $this->teardownGlobals();
 
899
                        return;
 
900
                }
 
901
 
 
902
                $tables = $this->listTables();
 
903
 
 
904
                foreach ( $tables as $table ) {
 
905
                        $sql = $this->db->getType() == 'oracle' ? "DROP TABLE pt_$table DROP CONSTRAINTS" : "DROP TABLE `parsertest_$table`";
 
906
                        $this->db->query( $sql );
 
907
                }
 
908
 
 
909
                if ( $this->db->getType() == 'oracle' )
 
910
                        $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
 
911
 
 
912
                $this->teardownGlobals();
 
913
        }
 
914
 
 
915
        /**
 
916
         * Create a dummy uploads directory which will contain a couple
 
917
         * of files in order to pass existence tests.
 
918
         *
 
919
         * @return String: the directory
 
920
         */
 
921
        private function setupUploadDir() {
 
922
                global $IP;
 
923
 
 
924
                if ( $this->keepUploads ) {
 
925
                        $dir = wfTempDir() . '/mwParser-images';
 
926
 
 
927
                        if ( is_dir( $dir ) ) {
 
928
                                return $dir;
 
929
                        }
 
930
                } else {
 
931
                        $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
 
932
                }
 
933
 
 
934
                // wfDebug( "Creating upload directory $dir\n" );
 
935
                if ( file_exists( $dir ) ) {
 
936
                        wfDebug( "Already exists!\n" );
 
937
                        return $dir;
 
938
                }
 
939
 
 
940
                wfMkdirParents( $dir . '/3/3a', null, __METHOD__ );
 
941
                copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
 
942
                wfMkdirParents( $dir . '/0/09', null, __METHOD__ );
 
943
                copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" );
 
944
 
 
945
                return $dir;
 
946
        }
 
947
 
 
948
        /**
 
949
         * Restore default values and perform any necessary clean-up
 
950
         * after each test runs.
 
951
         */
 
952
        private function teardownGlobals() {
 
953
                RepoGroup::destroySingleton();
 
954
                LinkCache::singleton()->clear();
 
955
 
 
956
                foreach ( $this->savedGlobals as $var => $val ) {
 
957
                        $GLOBALS[$var] = $val;
 
958
                }
 
959
        }
 
960
 
 
961
        /**
 
962
         * Remove the dummy uploads directory
 
963
         */
 
964
        private function teardownUploadDir( $dir ) {
 
965
                if ( $this->keepUploads ) {
 
966
                        return;
 
967
                }
 
968
 
 
969
                // delete the files first, then the dirs.
 
970
                self::deleteFiles(
 
971
                        array (
 
972
                                "$dir/3/3a/Foobar.jpg",
 
973
                                "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
 
974
                                "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
 
975
                                "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
 
976
                                "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
 
977
 
 
978
                                "$dir/0/09/Bad.jpg",
 
979
 
 
980
                                "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
 
981
                        )
 
982
                );
 
983
 
 
984
                self::deleteDirs(
 
985
                        array (
 
986
                                "$dir/3/3a",
 
987
                                "$dir/3",
 
988
                                "$dir/thumb/6/65",
 
989
                                "$dir/thumb/6",
 
990
                                "$dir/thumb/3/3a/Foobar.jpg",
 
991
                                "$dir/thumb/3/3a",
 
992
                                "$dir/thumb/3",
 
993
 
 
994
                                "$dir/0/09/",
 
995
                                "$dir/0/",
 
996
                                "$dir/thumb",
 
997
                                "$dir/math/f/a/5",
 
998
                                "$dir/math/f/a",
 
999
                                "$dir/math/f",
 
1000
                                "$dir/math",
 
1001
                                "$dir",
 
1002
                        )
 
1003
                );
 
1004
        }
 
1005
 
 
1006
        /**
 
1007
         * Delete the specified files, if they exist.
 
1008
         * @param $files Array: full paths to files to delete.
 
1009
         */
 
1010
        private static function deleteFiles( $files ) {
 
1011
                foreach ( $files as $file ) {
 
1012
                        if ( file_exists( $file ) ) {
 
1013
                                unlink( $file );
 
1014
                        }
 
1015
                }
 
1016
        }
 
1017
 
 
1018
        /**
 
1019
         * Delete the specified directories, if they exist. Must be empty.
 
1020
         * @param $dirs Array: full paths to directories to delete.
 
1021
         */
 
1022
        private static function deleteDirs( $dirs ) {
 
1023
                foreach ( $dirs as $dir ) {
 
1024
                        if ( is_dir( $dir ) ) {
 
1025
                                rmdir( $dir );
 
1026
                        }
 
1027
                }
 
1028
        }
 
1029
 
 
1030
        /**
 
1031
         * "Running test $desc..."
 
1032
         */
 
1033
        protected function showTesting( $desc ) {
 
1034
                print "Running test $desc... ";
 
1035
        }
 
1036
 
 
1037
        /**
 
1038
         * Print a happy success message.
 
1039
         *
 
1040
         * @param $desc String: the test name
 
1041
         * @return Boolean
 
1042
         */
 
1043
        protected function showSuccess( $desc ) {
 
1044
                if ( $this->showProgress ) {
 
1045
                        print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
 
1046
                }
 
1047
 
 
1048
                return true;
 
1049
        }
 
1050
 
 
1051
        /**
 
1052
         * Print a failure message and provide some explanatory output
 
1053
         * about what went wrong if so configured.
 
1054
         *
 
1055
         * @param $desc String: the test name
 
1056
         * @param $result String: expected HTML output
 
1057
         * @param $html String: actual HTML output
 
1058
         * @return Boolean
 
1059
         */
 
1060
        protected function showFailure( $desc, $result, $html ) {
 
1061
                if ( $this->showFailure ) {
 
1062
                        if ( !$this->showProgress ) {
 
1063
                                # In quiet mode we didn't show the 'Testing' message before the
 
1064
                                # test, in case it succeeded. Show it now:
 
1065
                                $this->showTesting( $desc );
 
1066
                        }
 
1067
 
 
1068
                        print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
 
1069
 
 
1070
                        if ( $this->showOutput ) {
 
1071
                                print "--- Expected ---\n$result\n--- Actual ---\n$html\n";
 
1072
                        }
 
1073
 
 
1074
                        if ( $this->showDiffs ) {
 
1075
                                print $this->quickDiff( $result, $html );
 
1076
                                if ( !$this->wellFormed( $html ) ) {
 
1077
                                        print "XML error: $this->mXmlError\n";
 
1078
                                }
 
1079
                        }
 
1080
                }
 
1081
 
 
1082
                return false;
 
1083
        }
 
1084
 
 
1085
        /**
 
1086
         * Run given strings through a diff and return the (colorized) output.
 
1087
         * Requires writable /tmp directory and a 'diff' command in the PATH.
 
1088
         *
 
1089
         * @param $input String
 
1090
         * @param $output String
 
1091
         * @param $inFileTail String: tailing for the input file name
 
1092
         * @param $outFileTail String: tailing for the output file name
 
1093
         * @return String
 
1094
         */
 
1095
        protected function quickDiff( $input, $output, $inFileTail = 'expected', $outFileTail = 'actual' ) {
 
1096
                # Windows, or at least the fc utility, is retarded
 
1097
                $slash = wfIsWindows() ? '\\' : '/';
 
1098
                $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand();
 
1099
 
 
1100
                $infile = "$prefix-$inFileTail";
 
1101
                $this->dumpToFile( $input, $infile );
 
1102
 
 
1103
                $outfile = "$prefix-$outFileTail";
 
1104
                $this->dumpToFile( $output, $outfile );
 
1105
 
 
1106
                $shellInfile = wfEscapeShellArg($infile);
 
1107
                $shellOutfile = wfEscapeShellArg($outfile);
 
1108
 
 
1109
                global $wgDiff3;
 
1110
                // we assume that people with diff3 also have usual diff
 
1111
                $diff = ( wfIsWindows() && !$wgDiff3 )
 
1112
                        ? `fc $shellInfile $shellOutfile`
 
1113
                        : `diff -au $shellInfile $shellOutfile`;
 
1114
                unlink( $infile );
 
1115
                unlink( $outfile );
 
1116
 
 
1117
                return $this->colorDiff( $diff );
 
1118
        }
 
1119
 
 
1120
        /**
 
1121
         * Write the given string to a file, adding a final newline.
 
1122
         *
 
1123
         * @param $data String
 
1124
         * @param $filename String
 
1125
         */
 
1126
        private function dumpToFile( $data, $filename ) {
 
1127
                $file = fopen( $filename, "wt" );
 
1128
                fwrite( $file, $data . "\n" );
 
1129
                fclose( $file );
 
1130
        }
 
1131
 
 
1132
        /**
 
1133
         * Colorize unified diff output if set for ANSI color output.
 
1134
         * Subtractions are colored blue, additions red.
 
1135
         *
 
1136
         * @param $text String
 
1137
         * @return String
 
1138
         */
 
1139
        protected function colorDiff( $text ) {
 
1140
                return preg_replace(
 
1141
                        array( '/^(-.*)$/m', '/^(\+.*)$/m' ),
 
1142
                        array( $this->term->color( 34 ) . '$1' . $this->term->reset(),
 
1143
                                   $this->term->color( 31 ) . '$1' . $this->term->reset() ),
 
1144
                        $text );
 
1145
        }
 
1146
 
 
1147
        /**
 
1148
         * Show "Reading tests from ..."
 
1149
         *
 
1150
         * @param $path String
 
1151
         */
 
1152
        public function showRunFile( $path ) {
 
1153
                print $this->term->color( 1 ) .
 
1154
                        "Reading tests from \"$path\"..." .
 
1155
                        $this->term->reset() .
 
1156
                        "\n";
 
1157
        }
 
1158
 
 
1159
        /**
 
1160
         * Insert a temporary test article
 
1161
         * @param $name String: the title, including any prefix
 
1162
         * @param $text String: the article text
 
1163
         * @param $line Integer: the input line number, for reporting errors
 
1164
         * @param $ignoreDuplicate Boolean: whether to silently ignore duplicate pages
 
1165
         */
 
1166
        static public function addArticle( $name, $text, $line = 'unknown', $ignoreDuplicate = '' ) {
 
1167
                global $wgCapitalLinks;
 
1168
 
 
1169
                $oldCapitalLinks = $wgCapitalLinks;
 
1170
                $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637
 
1171
 
 
1172
                $text = self::chomp( $text );
 
1173
                $name = self::chomp( $name );
 
1174
 
 
1175
                $title = Title::newFromText( $name );
 
1176
 
 
1177
                if ( is_null( $title ) ) {
 
1178
                        throw new MWException( "invalid title '$name' at line $line\n" );
 
1179
                }
 
1180
 
 
1181
                $page = WikiPage::factory( $title );
 
1182
                $page->loadPageData( 'fromdbmaster' );
 
1183
 
 
1184
                if ( $page->exists() ) {
 
1185
                        if ( $ignoreDuplicate == 'ignoreduplicate' ) {
 
1186
                                return;
 
1187
                        } else {
 
1188
                                throw new MWException( "duplicate article '$name' at line $line\n" );
 
1189
                        }
 
1190
                }
 
1191
 
 
1192
                $page->doEdit( $text, '', EDIT_NEW );
 
1193
 
 
1194
                $wgCapitalLinks = $oldCapitalLinks;
 
1195
        }
 
1196
 
 
1197
        /**
 
1198
         * Steal a callback function from the primary parser, save it for
 
1199
         * application to our scary parser. If the hook is not installed,
 
1200
         * abort processing of this file.
 
1201
         *
 
1202
         * @param $name String
 
1203
         * @return Bool true if tag hook is present
 
1204
         */
 
1205
        public function requireHook( $name ) {
 
1206
                global $wgParser;
 
1207
 
 
1208
                $wgParser->firstCallInit( ); // make sure hooks are loaded.
 
1209
 
 
1210
                if ( isset( $wgParser->mTagHooks[$name] ) ) {
 
1211
                        $this->hooks[$name] = $wgParser->mTagHooks[$name];
 
1212
                } else {
 
1213
                        echo "   This test suite requires the '$name' hook extension, skipping.\n";
 
1214
                        return false;
 
1215
                }
 
1216
 
 
1217
                return true;
 
1218
        }
 
1219
 
 
1220
        /**
 
1221
         * Steal a callback function from the primary parser, save it for
 
1222
         * application to our scary parser. If the hook is not installed,
 
1223
         * abort processing of this file.
 
1224
         *
 
1225
         * @param $name String
 
1226
         * @return Bool true if function hook is present
 
1227
         */
 
1228
        public function requireFunctionHook( $name ) {
 
1229
                global $wgParser;
 
1230
 
 
1231
                $wgParser->firstCallInit( ); // make sure hooks are loaded.
 
1232
 
 
1233
                if ( isset( $wgParser->mFunctionHooks[$name] ) ) {
 
1234
                        $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name];
 
1235
                } else {
 
1236
                        echo "   This test suite requires the '$name' function hook extension, skipping.\n";
 
1237
                        return false;
 
1238
                }
 
1239
 
 
1240
                return true;
 
1241
        }
 
1242
 
 
1243
        /**
 
1244
         * Run the "tidy" command on text if the $wgUseTidy
 
1245
         * global is true
 
1246
         *
 
1247
         * @param $text String: the text to tidy
 
1248
         * @return String
 
1249
         */
 
1250
        private function tidy( $text ) {
 
1251
                global $wgUseTidy;
 
1252
 
 
1253
                if ( $wgUseTidy ) {
 
1254
                        $text = MWTidy::tidy( $text );
 
1255
                }
 
1256
 
 
1257
                return $text;
 
1258
        }
 
1259
 
 
1260
        private function wellFormed( $text ) {
 
1261
                $html =
 
1262
                        Sanitizer::hackDocType() .
 
1263
                        '<html>' .
 
1264
                        $text .
 
1265
                        '</html>';
 
1266
 
 
1267
                $parser = xml_parser_create( "UTF-8" );
 
1268
 
 
1269
                # case folding violates XML standard, turn it off
 
1270
                xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
 
1271
 
 
1272
                if ( !xml_parse( $parser, $html, true ) ) {
 
1273
                        $err = xml_error_string( xml_get_error_code( $parser ) );
 
1274
                        $position = xml_get_current_byte_index( $parser );
 
1275
                        $fragment = $this->extractFragment( $html, $position );
 
1276
                        $this->mXmlError = "$err at byte $position:\n$fragment";
 
1277
                        xml_parser_free( $parser );
 
1278
 
 
1279
                        return false;
 
1280
                }
 
1281
 
 
1282
                xml_parser_free( $parser );
 
1283
 
 
1284
                return true;
 
1285
        }
 
1286
 
 
1287
        private function extractFragment( $text, $position ) {
 
1288
                $start = max( 0, $position - 10 );
 
1289
                $before = $position - $start;
 
1290
                $fragment = '...' .
 
1291
                        $this->term->color( 34 ) .
 
1292
                        substr( $text, $start, $before ) .
 
1293
                        $this->term->color( 0 ) .
 
1294
                        $this->term->color( 31 ) .
 
1295
                        $this->term->color( 1 ) .
 
1296
                        substr( $text, $position, 1 ) .
 
1297
                        $this->term->color( 0 ) .
 
1298
                        $this->term->color( 34 ) .
 
1299
                        substr( $text, $position + 1, 9 ) .
 
1300
                        $this->term->color( 0 ) .
 
1301
                        '...';
 
1302
                $display = str_replace( "\n", ' ', $fragment );
 
1303
                $caret = '   ' .
 
1304
                        str_repeat( ' ', $before ) .
 
1305
                        $this->term->color( 31 ) .
 
1306
                        '^' .
 
1307
                        $this->term->color( 0 );
 
1308
 
 
1309
                return "$display\n$caret";
 
1310
        }
 
1311
 
 
1312
        static function getFakeTimestamp( &$parser, &$ts ) {
 
1313
                $ts = 123;
 
1314
                return true;
 
1315
        }
 
1316
}