2
# Copyright (C) 2004, 2010 Brion Vibber <brion@pobox.com>
3
# http://www.mediawiki.org/
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.
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.
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
21
* @todo Make this more independent of the configuration (and if possible the database)
32
* boolean $color whereas output should be colorized
37
* boolean $showOutput Show test output
42
* boolean $useTemporaryTables Use temporary tables for the temporary database
44
private $useTemporaryTables = true;
47
* boolean $databaseSetupDone True if the database has been set up
49
private $databaseSetupDone = false;
52
* Our connection to the database
58
* Database clone helper
64
* string $oldTablePrefix Original table prefix
66
private $oldTablePrefix;
68
private $maxFuzzTestLength = 300;
69
private $fuzzSeed = 0;
70
private $memoryLimit = 50;
71
private $uploadDir = null;
74
private $savedGlobals = array();
76
* Sets terminal colorization and diff/quick modes depending on OS and
77
* command-line options (--color and --quick).
79
public function __construct( $options = array() ) {
80
# Only colorize output if stdout is a terminal.
81
$this->color = !wfIsWindows() && Maintenance::posix_isatty( 1 );
83
if ( isset( $options['color'] ) ) {
84
switch( $options['color'] ) {
95
$this->term = $this->color
96
? new AnsiTermColorer()
97
: new DummyTermColorer();
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
106
$this->showOutput = isset( $options['show-output'] );
108
if ( isset( $options['filter'] ) ) {
109
$options['regex'] = $options['filter'];
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'] );
117
$this->regex = $options['regex'];
123
$this->setupRecorder( $options );
124
$this->keepUploads = isset( $options['keep-uploads'] );
126
if ( isset( $options['seed'] ) ) {
127
$this->fuzzSeed = intval( $options['seed'] ) - 1;
130
$this->runDisabled = isset( $options['run-disabled'] );
132
$this->hooks = array();
133
$this->functionHooks = array();
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;
145
$wgScript = '/index.php';
147
$wgArticlePath = '/wiki/$1';
148
$wgStyleSheetPath = '/skins';
149
$wgStylePath = '/skins';
150
$wgExtensionAssetsPath = '/extensions';
151
$wgThumbnailScriptPath = false;
152
$wgLocalFileRepo = array(
153
'class' => 'LocalRepo',
155
'url' => 'http://example.com/images',
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',
169
$wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
170
$wgNamespaceAliases['Image'] = NS_FILE;
171
$wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
173
// XXX: tests won't run without this (for CACHE_DB)
174
if ( $wgMainCacheType === CACHE_DB ) {
175
$wgMainCacheType = CACHE_NONE;
177
if ( $wgMessageCacheType === CACHE_DB ) {
178
$wgMessageCacheType = CACHE_NONE;
180
if ( $wgParserCacheType === CACHE_DB ) {
181
$wgParserCacheType = CACHE_NONE;
184
$wgEnableParserCache = false;
185
DeferredUpdates::clearPendingUpdates();
186
$wgMemc = wfGetMainCache(); // checks $wgMainCacheType
187
$messageMemc = wfGetMessageCacheStorage();
188
$parserMemc = wfGetParserCacheStorage();
190
// $wgContLang = new StubContLang;
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();
198
if ( $wgStyleDirectory === false ) {
199
$wgStyleDirectory = "$IP/skins";
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 );
212
$this->recorder = new TestRecorder( $this );
217
* Remove last character if it is a newline
220
static public function chomp( $s ) {
221
if ( substr( $s, -1 ) === "\n" ) {
222
return substr( $s, 0, -1 );
230
* Run a fuzz test series
231
* Draw input from a set of test files
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 );
244
$opts = ParserOptions::newFromUser( $user );
245
$title = Title::makeTitle( NS_MAIN, 'Parser_test' );
248
// Generate test input
249
mt_srand( ++$this->fuzzSeed );
250
$totalLength = mt_rand( 1, $this->maxFuzzTestLength );
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 );
260
$this->setupGlobals();
261
$parser = $this->getParser();
265
$parser->parse( $input, $title, $opts );
267
} catch ( Exception $exception ) {
272
echo "Test failed with seed {$this->fuzzSeed}\n";
274
printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
281
$this->teardownGlobals();
282
$parser->__destruct();
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";
288
echo "Out of memory:\n";
289
$memStats = $this->getMemoryBreakdown();
291
foreach ( $memStats as $name => $usage ) {
292
echo "$name: $usage\n";
301
* Get an input dictionary from a set of parser test files
303
function getFuzzInput( $filenames ) {
306
foreach ( $filenames as $filename ) {
307
$contents = file_get_contents( $filename );
308
preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
310
foreach ( $matches[1] as $match ) {
311
$dict .= $match . "\n";
319
* Get a memory usage breakdown
321
function getMemoryBreakdown() {
324
foreach ( $GLOBALS as $name => $value ) {
325
$memStats['$' . $name] = strlen( serialize( $value ) );
328
$classes = get_declared_classes();
330
foreach ( $classes as $class ) {
331
$rc = new ReflectionClass( $class );
332
$props = $rc->getStaticProperties();
333
$memStats[$class] = strlen( serialize( $props ) );
334
$methods = $rc->getMethods();
336
foreach ( $methods as $method ) {
337
$memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
341
$functions = get_defined_functions();
343
foreach ( $functions['user'] as $function ) {
344
$rf = new ReflectionFunction( $function );
345
$memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
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.
362
* Prints status updates on stdout and counts up the total
363
* number and percentage of passed tests.
365
* @param $filenames Array of strings
366
* @return Boolean: true if passed all tests, false if any tests failed.
368
public function runTestsFromFiles( $filenames ) {
370
$GLOBALS['wgContLang'] = Language::factory( 'en' );
371
$this->recorder->start();
373
$this->setupDatabase();
376
foreach ( $filenames as $filename ) {
377
$tests = new TestFileIterator( $filename, $this );
378
$ok = $this->runTests( $tests ) && $ok;
381
$this->teardownDatabase();
382
$this->recorder->report();
383
} catch (DBError $e) {
384
echo $e->getMessage();
386
$this->recorder->end();
391
function runTests( $tests ) {
394
foreach ( $tests as $t ) {
396
$this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] );
397
$ok = $ok && $result;
398
$this->recorder->record( $t['test'], $result );
401
if ( $this->showProgress ) {
409
* Get a Parser object
411
function getParser( $preprocessor = null ) {
412
global $wgParserConf;
414
$class = $wgParserConf['class'];
415
$parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
417
foreach ( $this->hooks as $tag => $callback ) {
418
$parser->setHook( $tag, $callback );
421
foreach ( $this->functionHooks as $tag => $bits ) {
422
list( $callback, $flags ) = $bits;
423
$parser->setFunctionHook( $tag, $callback, $flags );
426
wfRunHooks( 'ParserTestParser', array( &$parser ) );
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.
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
443
public function runTest( $desc, $input, $result, $opts, $config ) {
444
if ( $this->showProgress ) {
445
$this->showTesting( $desc );
448
$opts = $this->parseOptions( $opts );
449
$context = $this->setupGlobals( $opts, $config );
451
$user = $context->getUser();
452
$options = ParserOptions::newFromContext( $context );
454
if ( isset( $opts['title'] ) ) {
455
$titleText = $opts['title'];
458
$titleText = 'Parser test';
461
$local = isset( $opts['local'] );
462
$preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
463
$parser = $this->getParser( $preprocessor );
464
$title = Title::newFromText( $titleText );
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 );
482
$output = $parser->parse( $input, $title, $options, true, true, 1337 );
483
$out = $output->getText();
485
if ( isset( $opts['showtitle'] ) ) {
486
if ( $output->getTitleText() ) {
487
$title = $output->getTitleText();
490
$out = "$title\n$out";
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();
500
if ( isset( $cats['normal'] ) ) {
501
$out = $this->tidy( implode( ' ', $cats['normal'] ) );
507
$result = $this->tidy( $result );
510
$this->teardownGlobals();
511
return $this->showTestResult( $desc, $result, $out );
517
function showTestResult( $desc, $result, $out ) {
518
if ( $result === $out ) {
519
$this->showSuccess( $desc );
522
$this->showFailure( $desc, $result, $out );
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
533
private static function getOptionValue( $key, $opts, $default ) {
534
$key = strtolower( $key );
536
if ( isset( $opts[$key] ) ) {
543
private function parseOptions( $instring ) {
549
// foo=bar,"baz quux"
573
\[\[[^]]*\]\] # Link target
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 ) {
587
} elseif ( count( $bits ) == 1 ) {
588
$opts[$key] = $this->cleanupOption( array_shift( $bits ) );
591
$opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
598
private function cleanupOption( $opt ) {
599
if ( substr( $opt, 0, 1 ) == '"' ) {
600
return substr( $opt, 1, -1 );
603
if ( substr( $opt, 0, 2 ) == '[[' ) {
604
return substr( $opt, 2, -2 );
610
* Set up the global variables for a consistent environment for each test.
611
* Ideally this should replace the global configuration entirely.
613
private function setupGlobals( $opts = '', $config = '' ) {
614
# Find out values for some special options.
616
self::getOptionValue( 'language', $opts, 'en' );
618
self::getOptionValue( 'variant', $opts, false );
620
self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
621
$linkHolderBatchSize =
622
self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
625
'wgServer' => 'http://Britney-Spears',
626
'wgScript' => '/index.php',
627
'wgScriptPath' => '/',
628
'wgArticlePath' => '/wiki/$1',
629
'wgActionPaths' => array(),
630
'wgLocalFileRepo' => array(
631
'class' => 'LocalRepo',
633
'url' => 'http://example.com/images',
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',
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'] ),
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,
672
'createpage' => true,
673
'createtalk' => true,
675
'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
676
'wgDefaultExternalStore' => array(),
677
'wgForeignFileRepos' => array(),
678
'wgLinkHolderBatchSize' => $linkHolderBatchSize,
679
'wgExperimentalHtmlIds' => false,
680
'wgExternalLinkTarget' => false,
681
'wgAlwaysUseTidy' => false,
683
'wgCleanupPresentationalAttributes' => true,
684
'wgWellFormedXml' => true,
685
'wgAllowMicrodataAttributes' => true,
686
'wgAdaptiveMessageCache' => true,
687
'wgDisableLangConversion' => false,
688
'wgDisableTitleConversion' => false,
692
$configLines = explode( "\n", $config );
694
foreach ( $configLines as $line ) {
695
list( $var, $value ) = explode( '=', $line, 2 );
697
$settings[$var] = eval( "return $value;" );
701
$this->savedGlobals = array();
703
foreach ( $settings as $var => $val ) {
704
if ( array_key_exists( $var, $GLOBALS ) ) {
705
$this->savedGlobals[$var] = $GLOBALS[$var];
708
$GLOBALS[$var] = $val;
711
$GLOBALS['wgContLang'] = Language::factory( $lang );
712
$GLOBALS['wgMemc'] = new EmptyBagOStuff;
714
$context = new RequestContext();
715
$GLOBALS['wgLang'] = $context->getLanguage();
716
$GLOBALS['wgOut'] = $context->getOutput();
718
$GLOBALS['wgUser'] = new User();
722
$wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
723
$wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
725
MagicWord::clearCache();
731
* List of temporary tables to create, without prefix.
732
* Some of these probably aren't necessary.
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'
744
if ( in_array( $this->db->getType(), array( 'mysql', 'sqlite', 'oracle' ) ) ) {
745
array_push( $tables, 'searchindex' );
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 ) );
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.
761
public function setupDatabase() {
764
if ( $this->databaseSetupDone ) {
768
$this->db = wfGetDB( DB_MASTER );
769
$dbType = $this->db->getType();
771
if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) {
772
throw new MWException( 'setupDatabase should be called before setupGlobals' );
775
$this->databaseSetupDone = true;
776
$this->oldTablePrefix = $wgDBprefix;
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;
783
# CREATE TEMPORARY TABLE breaks if there is more than one server
784
if ( wfGetLB()->getServerCount() != 1 ) {
785
$this->useTemporaryTables = false;
788
$temporary = $this->useTemporaryTables || $dbType == 'postgres';
789
$prefix = $dbType != 'oracle' ? 'parsertest_' : 'pt_';
791
$this->dbClone = new CloneDatabase( $this->db, $this->listTables(), $prefix );
792
$this->dbClone->useTemporaryTables( $temporary );
793
$this->dbClone->cloneTableStructure();
795
if ( $dbType == 'oracle' ) {
796
$this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
797
# Insert 0 user to prevent FK violations
800
$this->db->insert( 'user', array(
802
'user_name' => 'Anonymous' ) );
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',
813
array( 'iw_prefix' => 'meatball',
814
'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
818
array( 'iw_prefix' => 'zh',
819
'iw_url' => 'http://zh.wikipedia.org/wiki/$1',
823
array( 'iw_prefix' => 'es',
824
'iw_url' => 'http://es.wikipedia.org/wiki/$1',
828
array( 'iw_prefix' => 'fr',
829
'iw_url' => 'http://fr.wikipedia.org/wiki/$1',
833
array( 'iw_prefix' => 'ru',
834
'iw_url' => 'http://ru.wikipedia.org/wiki/$1',
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 ) );
843
# Reinitialise the LocalisationCache to match the database state
844
Language::getLocalisationCache()->unloadAll();
846
# Clear the message cache
847
MessageCache::singleton()->clear();
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(
857
'media_type' => MEDIATYPE_BITMAP,
858
'mime' => 'image/jpeg',
859
'metadata' => serialize( array() ),
860
'sha1' => wfBaseConvert( '', 16, 36, 31 ),
862
), $this->db->timestamp( '20010115123500' ), $user );
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(
871
'media_type' => MEDIATYPE_BITMAP,
872
'mime' => 'image/jpeg',
873
'metadata' => serialize( array() ),
874
'sha1' => wfBaseConvert( '', 16, 36, 31 ),
876
), $this->db->timestamp( '20010115123500' ), $user );
879
public function teardownDatabase() {
880
if ( !$this->databaseSetupDone ) {
881
$this->teardownGlobals();
884
$this->teardownUploadDir( $this->uploadDir );
886
$this->dbClone->destroy();
887
$this->databaseSetupDone = false;
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`" );
897
# Don't need to do anything
898
$this->teardownGlobals();
902
$tables = $this->listTables();
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 );
909
if ( $this->db->getType() == 'oracle' )
910
$this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
912
$this->teardownGlobals();
916
* Create a dummy uploads directory which will contain a couple
917
* of files in order to pass existence tests.
919
* @return String: the directory
921
private function setupUploadDir() {
924
if ( $this->keepUploads ) {
925
$dir = wfTempDir() . '/mwParser-images';
927
if ( is_dir( $dir ) ) {
931
$dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
934
// wfDebug( "Creating upload directory $dir\n" );
935
if ( file_exists( $dir ) ) {
936
wfDebug( "Already exists!\n" );
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" );
949
* Restore default values and perform any necessary clean-up
950
* after each test runs.
952
private function teardownGlobals() {
953
RepoGroup::destroySingleton();
954
LinkCache::singleton()->clear();
956
foreach ( $this->savedGlobals as $var => $val ) {
957
$GLOBALS[$var] = $val;
962
* Remove the dummy uploads directory
964
private function teardownUploadDir( $dir ) {
965
if ( $this->keepUploads ) {
969
// delete the files first, then the dirs.
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",
980
"$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
990
"$dir/thumb/3/3a/Foobar.jpg",
1007
* Delete the specified files, if they exist.
1008
* @param $files Array: full paths to files to delete.
1010
private static function deleteFiles( $files ) {
1011
foreach ( $files as $file ) {
1012
if ( file_exists( $file ) ) {
1019
* Delete the specified directories, if they exist. Must be empty.
1020
* @param $dirs Array: full paths to directories to delete.
1022
private static function deleteDirs( $dirs ) {
1023
foreach ( $dirs as $dir ) {
1024
if ( is_dir( $dir ) ) {
1031
* "Running test $desc..."
1033
protected function showTesting( $desc ) {
1034
print "Running test $desc... ";
1038
* Print a happy success message.
1040
* @param $desc String: the test name
1043
protected function showSuccess( $desc ) {
1044
if ( $this->showProgress ) {
1045
print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
1052
* Print a failure message and provide some explanatory output
1053
* about what went wrong if so configured.
1055
* @param $desc String: the test name
1056
* @param $result String: expected HTML output
1057
* @param $html String: actual HTML output
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 );
1068
print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
1070
if ( $this->showOutput ) {
1071
print "--- Expected ---\n$result\n--- Actual ---\n$html\n";
1074
if ( $this->showDiffs ) {
1075
print $this->quickDiff( $result, $html );
1076
if ( !$this->wellFormed( $html ) ) {
1077
print "XML error: $this->mXmlError\n";
1086
* Run given strings through a diff and return the (colorized) output.
1087
* Requires writable /tmp directory and a 'diff' command in the PATH.
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
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();
1100
$infile = "$prefix-$inFileTail";
1101
$this->dumpToFile( $input, $infile );
1103
$outfile = "$prefix-$outFileTail";
1104
$this->dumpToFile( $output, $outfile );
1106
$shellInfile = wfEscapeShellArg($infile);
1107
$shellOutfile = wfEscapeShellArg($outfile);
1110
// we assume that people with diff3 also have usual diff
1111
$diff = ( wfIsWindows() && !$wgDiff3 )
1112
? `fc $shellInfile $shellOutfile`
1113
: `diff -au $shellInfile $shellOutfile`;
1117
return $this->colorDiff( $diff );
1121
* Write the given string to a file, adding a final newline.
1123
* @param $data String
1124
* @param $filename String
1126
private function dumpToFile( $data, $filename ) {
1127
$file = fopen( $filename, "wt" );
1128
fwrite( $file, $data . "\n" );
1133
* Colorize unified diff output if set for ANSI color output.
1134
* Subtractions are colored blue, additions red.
1136
* @param $text String
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() ),
1148
* Show "Reading tests from ..."
1150
* @param $path String
1152
public function showRunFile( $path ) {
1153
print $this->term->color( 1 ) .
1154
"Reading tests from \"$path\"..." .
1155
$this->term->reset() .
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
1166
static public function addArticle( $name, $text, $line = 'unknown', $ignoreDuplicate = '' ) {
1167
global $wgCapitalLinks;
1169
$oldCapitalLinks = $wgCapitalLinks;
1170
$wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637
1172
$text = self::chomp( $text );
1173
$name = self::chomp( $name );
1175
$title = Title::newFromText( $name );
1177
if ( is_null( $title ) ) {
1178
throw new MWException( "invalid title '$name' at line $line\n" );
1181
$page = WikiPage::factory( $title );
1182
$page->loadPageData( 'fromdbmaster' );
1184
if ( $page->exists() ) {
1185
if ( $ignoreDuplicate == 'ignoreduplicate' ) {
1188
throw new MWException( "duplicate article '$name' at line $line\n" );
1192
$page->doEdit( $text, '', EDIT_NEW );
1194
$wgCapitalLinks = $oldCapitalLinks;
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.
1202
* @param $name String
1203
* @return Bool true if tag hook is present
1205
public function requireHook( $name ) {
1208
$wgParser->firstCallInit( ); // make sure hooks are loaded.
1210
if ( isset( $wgParser->mTagHooks[$name] ) ) {
1211
$this->hooks[$name] = $wgParser->mTagHooks[$name];
1213
echo " This test suite requires the '$name' hook extension, skipping.\n";
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.
1225
* @param $name String
1226
* @return Bool true if function hook is present
1228
public function requireFunctionHook( $name ) {
1231
$wgParser->firstCallInit( ); // make sure hooks are loaded.
1233
if ( isset( $wgParser->mFunctionHooks[$name] ) ) {
1234
$this->functionHooks[$name] = $wgParser->mFunctionHooks[$name];
1236
echo " This test suite requires the '$name' function hook extension, skipping.\n";
1244
* Run the "tidy" command on text if the $wgUseTidy
1247
* @param $text String: the text to tidy
1250
private function tidy( $text ) {
1254
$text = MWTidy::tidy( $text );
1260
private function wellFormed( $text ) {
1262
Sanitizer::hackDocType() .
1267
$parser = xml_parser_create( "UTF-8" );
1269
# case folding violates XML standard, turn it off
1270
xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
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 );
1282
xml_parser_free( $parser );
1287
private function extractFragment( $text, $position ) {
1288
$start = max( 0, $position - 10 );
1289
$before = $position - $start;
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 ) .
1302
$display = str_replace( "\n", ' ', $fragment );
1304
str_repeat( ' ', $before ) .
1305
$this->term->color( 31 ) .
1307
$this->term->color( 0 );
1309
return "$display\n$caret";
1312
static function getFakeTimestamp( &$parser, &$ts ) {