--- /dev/null
+<?php
+# Copyright (C) 2004, 2010 Brion Vibber <brion@pobox.com>
+# http://www.mediawiki.org/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+# http://www.gnu.org/copyleft/gpl.html
+
+/**
+ * @todo Make this more independent of the configuration (and if possible the database)
+ * @todo document
+ * @file
+ * @ingroup Testing
+ */
+
+/**
+ * @ingroup Testing
+ */
+class ParserTest {
+ /**
+ * boolean $color whereas output should be colorized
+ */
+ private $color;
+
+ /**
+ * boolean $showOutput Show test output
+ */
+ private $showOutput;
+
+ /**
+ * boolean $useTemporaryTables Use temporary tables for the temporary database
+ */
+ private $useTemporaryTables = true;
+
+ /**
+ * boolean $databaseSetupDone True if the database has been set up
+ */
+ private $databaseSetupDone = false;
+
+ /**
+ * Our connection to the database
+ */
+ private $db;
+
+ /**
+ * string $oldTablePrefix Original table prefix
+ */
+ private $oldTablePrefix;
+
+ private $maxFuzzTestLength = 300;
+ private $fuzzSeed = 0;
+ private $memoryLimit = 50;
+ private $uploadDir = null;
+
+ public $regex = "";
+ private $savedGlobals = array();
+ /**
+ * Sets terminal colorization and diff/quick modes depending on OS and
+ * command-line options (--color and --quick).
+ */
+ public function __construct( $options = array() ) {
+ # Only colorize output if stdout is a terminal.
+ $this->color = !wfIsWindows() && posix_isatty( 1 );
+
+ if ( isset( $options['color'] ) ) {
+ switch( $options['color'] ) {
+ case 'no':
+ $this->color = false;
+ break;
+ case 'yes':
+ default:
+ $this->color = true;
+ break;
+ }
+ }
+
+ $this->term = $this->color
+ ? new AnsiTermColorer()
+ : new DummyTermColorer();
+
+ $this->showDiffs = !isset( $options['quick'] );
+ $this->showProgress = !isset( $options['quiet'] );
+ $this->showFailure = !(
+ isset( $options['quiet'] )
+ && ( isset( $options['record'] )
+ || isset( $options['compare'] ) ) ); // redundant output
+
+ $this->showOutput = isset( $options['show-output'] );
+
+
+ if ( isset( $options['regex'] ) ) {
+ if ( isset( $options['record'] ) ) {
+ echo "Warning: --record cannot be used with --regex, disabling --record\n";
+ unset( $options['record'] );
+ }
+ $this->regex = $options['regex'];
+ } else {
+ # Matches anything
+ $this->regex = '';
+ }
+
+ $this->setupRecorder( $options );
+ $this->keepUploads = isset( $options['keep-uploads'] );
+
+ if ( isset( $options['seed'] ) ) {
+ $this->fuzzSeed = intval( $options['seed'] ) - 1;
+ }
+
+ $this->runDisabled = isset( $options['run-disabled'] );
+
+ $this->hooks = array();
+ $this->functionHooks = array();
+ self::setUp();
+ }
+
+ static function setUp() {
+ global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, $wgDeferredUpdateList,
+ $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, $wgEnableParserCache,
+ $wgMessageCache, $wgUseDatabaseMessages, $wgMsgCacheExpiry, $parserMemc,
+ $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo,
+ $wgThumbnailScriptPath, $wgScriptPath,
+ $wgArticlePath, $wgStyleSheetPath, $wgScript, $wgStylePath;
+
+ $wgScript = '/index.php';
+ $wgScriptPath = '/';
+ $wgArticlePath = '/wiki/$1';
+ $wgStyleSheetPath = '/skins';
+ $wgStylePath = '/skins';
+ $wgThumbnailScriptPath = false;
+ $wgLocalFileRepo = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'directory' => wfTempDir() . '/test-repo',
+ 'url' => 'http://example.com/images',
+ 'deletedDir' => wfTempDir() . '/test-repo/delete',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ );
+ $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+
+ $wgEnableParserCache = false;
+ $wgDeferredUpdateList = array();
+ $wgMemc = &wfGetMainCache();
+ $messageMemc = &wfGetMessageCacheStorage();
+ $parserMemc = &wfGetParserCacheStorage();
+
+ // $wgContLang = new StubContLang;
+ $wgUser = new User;
+ $wgLang = new StubUserLang;
+ $wgOut = new StubObject( 'wgOut', 'OutputPage' );
+ $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
+ $wgRequest = new WebRequest;
+
+ $wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache',
+ array( $messageMemc, $wgUseDatabaseMessages,
+ $wgMsgCacheExpiry ) );
+ if ( $wgStyleDirectory === false ) {
+ $wgStyleDirectory = "$IP/skins";
+ }
+
+ }
+
+ public function setupRecorder ( $options ) {
+ if ( isset( $options['record'] ) ) {
+ $this->recorder = new DbTestRecorder( $this );
+ $this->recorder->version = isset( $options['setversion'] ) ?
+ $options['setversion'] : SpecialVersion::getVersion();
+ } elseif ( isset( $options['compare'] ) ) {
+ $this->recorder = new DbTestPreviewer( $this );
+ } elseif ( isset( $options['upload'] ) ) {
+ $this->recorder = new RemoteTestRecorder( $this );
+ } else {
+ $this->recorder = new TestRecorder( $this );
+ }
+ }
+
+ /**
+ * Remove last character if it is a newline
+ * @group utility
+ */
+ static public function chomp( $s ) {
+ if ( substr( $s, -1 ) === "\n" ) {
+ return substr( $s, 0, -1 );
+ }
+ else {
+ return $s;
+ }
+ }
+
+ /**
+ * Run a fuzz test series
+ * Draw input from a set of test files
+ */
+ function fuzzTest( $filenames ) {
+ $GLOBALS['wgContLang'] = Language::factory( 'en' );
+ $dict = $this->getFuzzInput( $filenames );
+ $dictSize = strlen( $dict );
+ $logMaxLength = log( $this->maxFuzzTestLength );
+ $this->setupDatabase();
+ ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
+
+ $numTotal = 0;
+ $numSuccess = 0;
+ $user = new User;
+ $opts = ParserOptions::newFromUser( $user );
+ $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+ while ( true ) {
+ // Generate test input
+ mt_srand( ++$this->fuzzSeed );
+ $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
+ $input = '';
+
+ while ( strlen( $input ) < $totalLength ) {
+ $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
+ $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
+ $offset = mt_rand( 0, $dictSize - $hairLength );
+ $input .= substr( $dict, $offset, $hairLength );
+ }
+
+ $this->setupGlobals();
+ $parser = $this->getParser();
+
+ // Run the test
+ try {
+ $parser->parse( $input, $title, $opts );
+ $fail = false;
+ } catch ( Exception $exception ) {
+ $fail = true;
+ }
+
+ if ( $fail ) {
+ echo "Test failed with seed {$this->fuzzSeed}\n";
+ echo "Input:\n";
+ var_dump( $input );
+ echo "\n\n";
+ echo "$exception\n";
+ } else {
+ $numSuccess++;
+ }
+
+ $numTotal++;
+ $this->teardownGlobals();
+ $parser->__destruct();
+
+ if ( $numTotal % 100 == 0 ) {
+ $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+ echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
+ if ( $usage > 90 ) {
+ echo "Out of memory:\n";
+ $memStats = $this->getMemoryBreakdown();
+
+ foreach ( $memStats as $name => $usage ) {
+ echo "$name: $usage\n";
+ }
+ $this->abort();
+ }
+ }
+ }
+ }
+
+ /**
+ * Get an input dictionary from a set of parser test files
+ */
+ function getFuzzInput( $filenames ) {
+ $dict = '';
+
+ foreach ( $filenames as $filename ) {
+ $contents = file_get_contents( $filename );
+ preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
+
+ foreach ( $matches[1] as $match ) {
+ $dict .= $match . "\n";
+ }
+ }
+
+ return $dict;
+ }
+
+ /**
+ * Get a memory usage breakdown
+ */
+ function getMemoryBreakdown() {
+ $memStats = array();
+
+ foreach ( $GLOBALS as $name => $value ) {
+ $memStats['$' . $name] = strlen( serialize( $value ) );
+ }
+
+ $classes = get_declared_classes();
+
+ foreach ( $classes as $class ) {
+ $rc = new ReflectionClass( $class );
+ $props = $rc->getStaticProperties();
+ $memStats[$class] = strlen( serialize( $props ) );
+ $methods = $rc->getMethods();
+
+ foreach ( $methods as $method ) {
+ $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
+ }
+ }
+
+ $functions = get_defined_functions();
+
+ foreach ( $functions['user'] as $function ) {
+ $rf = new ReflectionFunction( $function );
+ $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
+ }
+
+ asort( $memStats );
+
+ return $memStats;
+ }
+
+ function abort() {
+ $this->abort();
+ }
+
+ /**
+ * Run a series of tests listed in the given text files.
+ * Each test consists of a brief description, wikitext input,
+ * and the expected HTML output.
+ *
+ * Prints status updates on stdout and counts up the total
+ * number and percentage of passed tests.
+ *
+ * @param $filenames Array of strings
+ * @return Boolean: true if passed all tests, false if any tests failed.
+ */
+ public function runTestsFromFiles( $filenames ) {
+ $ok = false;
+ $GLOBALS['wgContLang'] = Language::factory( 'en' );
+ $this->recorder->start();
+ try {
+ $this->setupDatabase();
+ $ok = true;
+
+ foreach ( $filenames as $filename ) {
+ $tests = new TestFileIterator( $filename, $this );
+ $ok = $this->runTests( $tests ) && $ok;
+ }
+
+ $this->teardownDatabase();
+ $this->recorder->report();
+ } catch (DBError $e) {
+ echo $e->getMessage();
+ }
+ $this->recorder->end();
+
+ return $ok;
+ }
+
+ function runTests( $tests ) {
+ $ok = true;
+
+ foreach ( $tests as $t ) {
+ $result =
+ $this->runTest( $t['test'], $t['input'], $t['result'], $t['options'], $t['config'] );
+ $ok = $ok && $result;
+ $this->recorder->record( $t['test'], $result );
+ }
+
+ if ( $this->showProgress ) {
+ print "\n";
+ }
+
+ return $ok;
+ }
+
+ /**
+ * Get a Parser object
+ */
+ function getParser( $preprocessor = null ) {
+ global $wgParserConf;
+
+ $class = $wgParserConf['class'];
+ $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
+
+ foreach ( $this->hooks as $tag => $callback ) {
+ $parser->setHook( $tag, $callback );
+ }
+
+ foreach ( $this->functionHooks as $tag => $bits ) {
+ list( $callback, $flags ) = $bits;
+ $parser->setFunctionHook( $tag, $callback, $flags );
+ }
+
+ wfRunHooks( 'ParserTestParser', array( &$parser ) );
+
+ return $parser;
+ }
+
+ /**
+ * Run a given wikitext input through a freshly-constructed wiki parser,
+ * and compare the output against the expected results.
+ * Prints status and explanatory messages to stdout.
+ *
+ * @param $desc String: test's description
+ * @param $input String: wikitext to try rendering
+ * @param $result String: result to output
+ * @param $opts Array: test's options
+ * @param $config String: overrides for global variables, one per line
+ * @return Boolean
+ */
+ public function runTest( $desc, $input, $result, $opts, $config ) {
+ if ( $this->showProgress ) {
+ $this->showTesting( $desc );
+ }
+
+ $opts = $this->parseOptions( $opts );
+ $this->setupGlobals( $opts, $config );
+
+ $user = new User();
+ $options = ParserOptions::newFromUser( $user );
+
+ if ( isset( $opts['title'] ) ) {
+ $titleText = $opts['title'];
+ }
+ else {
+ $titleText = 'Parser test';
+ }
+
+ $local = isset( $opts['local'] );
+ $preprocessor = isset( $opts['preprocessor'] ) ? $opts['preprocessor'] : null;
+ $parser = $this->getParser( $preprocessor );
+ $title = Title::newFromText( $titleText );
+
+ if ( isset( $opts['pst'] ) ) {
+ $out = $parser->preSaveTransform( $input, $title, $user, $options );
+ } elseif ( isset( $opts['msg'] ) ) {
+ $out = $parser->transformMsg( $input, $options );
+ } elseif ( isset( $opts['section'] ) ) {
+ $section = $opts['section'];
+ $out = $parser->getSection( $input, $section );
+ } elseif ( isset( $opts['replace'] ) ) {
+ $section = $opts['replace'][0];
+ $replace = $opts['replace'][1];
+ $out = $parser->replaceSection( $input, $section, $replace );
+ } elseif ( isset( $opts['comment'] ) ) {
+ $linker = $user->getSkin();
+ $out = $linker->formatComment( $input, $title, $local );
+ } elseif ( isset( $opts['preload'] ) ) {
+ $out = $parser->getpreloadText( $input, $title, $options );
+ } else {
+ $output = $parser->parse( $input, $title, $options, true, true, 1337 );
+ $out = $output->getText();
+
+ if ( isset( $opts['showtitle'] ) ) {
+ if ( $output->getTitleText() ) {
+ $title = $output->getTitleText();
+ }
+
+ $out = "$title\n$out";
+ }
+
+ if ( isset( $opts['ill'] ) ) {
+ $out = $this->tidy( implode( ' ', $output->getLanguageLinks() ) );
+ } elseif ( isset( $opts['cat'] ) ) {
+ global $wgOut;
+
+ $wgOut->addCategoryLinks( $output->getCategories() );
+ $cats = $wgOut->getCategoryLinks();
+
+ if ( isset( $cats['normal'] ) ) {
+ $out = $this->tidy( implode( ' ', $cats['normal'] ) );
+ } else {
+ $out = '';
+ }
+ }
+
+ $result = $this->tidy( $result );
+ }
+
+ $this->teardownGlobals();
+ return $this->showTestResult( $desc, $result, $out );
+ }
+
+ /**
+ *
+ */
+ function showTestResult( $desc, $result, $out ) {
+ if ( $result === $out ) {
+ $this->showSuccess( $desc );
+ return true;
+ } else {
+ $this->showFailure( $desc, $result, $out );
+ return false;
+ }
+ }
+
+ /**
+ * Use a regex to find out the value of an option
+ * @param $key String: name of option val to retrieve
+ * @param $opts Options array to look in
+ * @param $default Mixed: default value returned if not found
+ */
+ private static function getOptionValue( $key, $opts, $default ) {
+ $key = strtolower( $key );
+
+ if ( isset( $opts[$key] ) ) {
+ return $opts[$key];
+ } else {
+ return $default;
+ }
+ }
+
+ private function parseOptions( $instring ) {
+ $opts = array();
+ // foo
+ // foo=bar
+ // foo="bar baz"
+ // foo=[[bar baz]]
+ // foo=bar,"baz quux"
+ $regex = '/\b
+ ([\w-]+) # Key
+ \b
+ (?:\s*
+ = # First sub-value
+ \s*
+ (
+ "
+ [^"]* # Quoted val
+ "
+ |
+ \[\[
+ [^]]* # Link target
+ \]\]
+ |
+ [\w-]+ # Plain word
+ )
+ (?:\s*
+ , # Sub-vals 1..N
+ \s*
+ (
+ "[^"]*" # Quoted val
+ |
+ \[\[[^]]*\]\] # Link target
+ |
+ [\w-]+ # Plain word
+ )
+ )*
+ )?
+ /x';
+
+ if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
+ foreach ( $matches as $bits ) {
+ array_shift( $bits );
+ $key = strtolower( array_shift( $bits ) );
+ if ( count( $bits ) == 0 ) {
+ $opts[$key] = true;
+ } elseif ( count( $bits ) == 1 ) {
+ $opts[$key] = $this->cleanupOption( array_shift( $bits ) );
+ } else {
+ // Array!
+ $opts[$key] = array_map( array( $this, 'cleanupOption' ), $bits );
+ }
+ }
+ }
+ return $opts;
+ }
+
+ private function cleanupOption( $opt ) {
+ if ( substr( $opt, 0, 1 ) == '"' ) {
+ return substr( $opt, 1, -1 );
+ }
+
+ if ( substr( $opt, 0, 2 ) == '[[' ) {
+ return substr( $opt, 2, -2 );
+ }
+ return $opt;
+ }
+
+ /**
+ * Set up the global variables for a consistent environment for each test.
+ * Ideally this should replace the global configuration entirely.
+ */
+ private function setupGlobals( $opts = '', $config = '' ) {
+ # Find out values for some special options.
+ $lang =
+ self::getOptionValue( 'language', $opts, 'en' );
+ $variant =
+ self::getOptionValue( 'variant', $opts, false );
+ $maxtoclevel =
+ self::getOptionValue( 'wgMaxTocLevel', $opts, 999 );
+ $linkHolderBatchSize =
+ self::getOptionValue( 'wgLinkHolderBatchSize', $opts, 1000 );
+
+ $settings = array(
+ 'wgServer' => 'http://Britney-Spears',
+ 'wgScript' => '/index.php',
+ 'wgScriptPath' => '/',
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgActionPaths' => array(),
+ 'wgLocalFileRepo' => array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'directory' => $this->uploadDir,
+ 'url' => 'http://example.com/images',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ ),
+ 'wgEnableUploads' => self::getOptionValue( 'wgEnableUploads', $opts, true ),
+ 'wgStylePath' => '/skins',
+ 'wgStyleSheetPath' => '/skins',
+ 'wgSitename' => 'MediaWiki',
+ 'wgLanguageCode' => $lang,
+ 'wgDBprefix' => $this->db->getType() != 'oracle' ? 'parsertest_' : 'pt_',
+ 'wgRawHtml' => isset( $opts['rawhtml'] ),
+ 'wgLang' => null,
+ 'wgContLang' => null,
+ 'wgNamespacesWithSubpages' => array( 0 => isset( $opts['subpage'] ) ),
+ 'wgMaxTocLevel' => $maxtoclevel,
+ 'wgCapitalLinks' => true,
+ 'wgNoFollowLinks' => true,
+ 'wgNoFollowDomainExceptions' => array(),
+ 'wgThumbnailScriptPath' => false,
+ 'wgUseImageResize' => false,
+ 'wgUseTeX' => isset( $opts['math'] ),
+ 'wgMathDirectory' => $this->uploadDir . '/math',
+ 'wgLocaltimezone' => 'UTC',
+ 'wgAllowExternalImages' => true,
+ 'wgUseTidy' => false,
+ 'wgDefaultLanguageVariant' => $variant,
+ 'wgVariantArticlePath' => false,
+ 'wgGroupPermissions' => array( '*' => array(
+ 'createaccount' => true,
+ 'read' => true,
+ 'edit' => true,
+ 'createpage' => true,
+ 'createtalk' => true,
+ ) ),
+ 'wgNamespaceProtection' => array( NS_MEDIAWIKI => 'editinterface' ),
+ 'wgDefaultExternalStore' => array(),
+ 'wgForeignFileRepos' => array(),
+ 'wgLinkHolderBatchSize' => $linkHolderBatchSize,
+ 'wgExperimentalHtmlIds' => false,
+ 'wgExternalLinkTarget' => false,
+ 'wgAlwaysUseTidy' => false,
+ 'wgHtml5' => true,
+ 'wgWellFormedXml' => true,
+ 'wgAllowMicrodataAttributes' => true,
+ );
+
+ if ( $config ) {
+ $configLines = explode( "\n", $config );
+
+ foreach ( $configLines as $line ) {
+ list( $var, $value ) = explode( '=', $line, 2 );
+
+ $settings[$var] = eval( "return $value;" );
+ }
+ }
+
+ $this->savedGlobals = array();
+
+ foreach ( $settings as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedGlobals[$var] = $GLOBALS[$var];
+ }
+
+ $GLOBALS[$var] = $val;
+ }
+
+ $langObj = Language::factory( $lang );
+ $GLOBALS['wgLang'] = $langObj;
+ $GLOBALS['wgContLang'] = $langObj;
+ $GLOBALS['wgMemc'] = new FakeMemCachedClient;
+ $GLOBALS['wgOut'] = new OutputPage;
+
+ global $wgHooks;
+
+ $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
+ $wgHooks['ParserTestParser'][] = 'ParserTestStaticParserHook::setup';
+ $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
+
+ MagicWord::clearCache();
+
+ global $wgUser;
+ $wgUser = new User();
+ }
+
+ /**
+ * List of temporary tables to create, without prefix.
+ * Some of these probably aren't necessary.
+ */
+ private function listTables() {
+ $tables = array( 'user', 'user_properties', 'page', 'page_restrictions',
+ 'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
+ 'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
+ 'site_stats', 'hitcounter', 'ipblocks', 'image', 'oldimage',
+ 'recentchanges', 'watchlist', 'math', 'interwiki', 'logging',
+ 'querycache', 'objectcache', 'job', 'l10n_cache', 'redirect', 'querycachetwo',
+ 'archive', 'user_groups', 'page_props', 'category', 'msg_resource', 'msg_resource_links'
+ );
+
+ if ( in_array( $this->db->getType(), array( 'mysql', 'sqlite', 'oracle' ) ) )
+ array_push( $tables, 'searchindex' );
+
+ // Allow extensions to add to the list of tables to duplicate;
+ // may be necessary if they hook into page save or other code
+ // which will require them while running tests.
+ wfRunHooks( 'ParserTestTables', array( &$tables ) );
+
+ return $tables;
+ }
+
+ /**
+ * Set up a temporary set of wiki tables to work with for the tests.
+ * Currently this will only be done once per run, and any changes to
+ * the db will be visible to later tests in the run.
+ */
+ public function setupDatabase() {
+ global $wgDBprefix;
+
+ if ( $this->databaseSetupDone ) {
+ return;
+ }
+
+ $this->db = wfGetDB( DB_MASTER );
+ $dbType = $this->db->getType();
+
+ if ( $wgDBprefix === 'parsertest_' || ( $dbType == 'oracle' && $wgDBprefix === 'pt_' ) ) {
+ throw new MWException( 'setupDatabase should be called before setupGlobals' );
+ }
+
+ $this->databaseSetupDone = true;
+ $this->oldTablePrefix = $wgDBprefix;
+
+ # SqlBagOStuff broke when using temporary tables on r40209 (bug 15892).
+ # It seems to have been fixed since (r55079?).
+ # If it fails, $wgCaches[CACHE_DB] = new HashBagOStuff(); should work around it.
+
+ # CREATE TEMPORARY TABLE breaks if there is more than one server
+ if ( wfGetLB()->getServerCount() != 1 ) {
+ $this->useTemporaryTables = false;
+ }
+
+ $temporary = $this->useTemporaryTables || $dbType == 'postgres';
+ $tables = $this->listTables();
+
+ foreach ( $tables as $tbl ) {
+ # Clean up from previous aborted run. So that table escaping
+ # works correctly across DB engines, we need to change the pre-
+ # fix back and forth so tableName() works right.
+ $this->changePrefix( $this->oldTablePrefix );
+ $oldTableName = $this->db->tableName( $tbl );
+ $this->changePrefix( $dbType != 'oracle' ? 'parsertest_' : 'pt_' );
+ $newTableName = $this->db->tableName( $tbl );
+
+ if ( $dbType == 'mysql' ) {
+ $this->db->query( "DROP TABLE IF EXISTS $newTableName" );
+ } elseif ( in_array( $dbType, array( 'postgres', 'oracle' ) ) ) {
+ /* DROPs wouldn't work due to Foreign Key Constraints (bug 14990, r58669)
+ * Use "DROP TABLE IF EXISTS $newTableName CASCADE" for postgres? That
+ * syntax would also work for mysql.
+ */
+ } elseif ( $this->db->tableExists( $tbl ) ) {
+ $this->db->query( "DROP TABLE $newTableName" );
+ }
+
+ # Create new table
+ $this->db->duplicateTableStructure( $oldTableName, $newTableName, $temporary );
+ }
+
+ if ( $dbType == 'oracle' )
+ $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+
+ $this->changePrefix( $dbType != 'oracle' ? 'parsertest_' : 'pt_' );
+
+ if ( $dbType == 'oracle' ) {
+ # Insert 0 user to prevent FK violations
+
+ # Anonymous user
+ $this->db->insert( 'user', array(
+ 'user_id' => 0,
+ 'user_name' => 'Anonymous' ) );
+ }
+
+ # Hack: insert a few Wikipedia in-project interwiki prefixes,
+ # for testing inter-language links
+ $this->db->insert( 'interwiki', array(
+ array( 'iw_prefix' => 'wikipedia',
+ 'iw_url' => 'http://en.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ array( 'iw_prefix' => 'meatball',
+ 'iw_url' => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 0 ),
+ array( 'iw_prefix' => 'zh',
+ 'iw_url' => 'http://zh.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ array( 'iw_prefix' => 'es',
+ 'iw_url' => 'http://es.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ array( 'iw_prefix' => 'fr',
+ 'iw_url' => 'http://fr.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ array( 'iw_prefix' => 'ru',
+ 'iw_url' => 'http://ru.wikipedia.org/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => 1 ),
+ ) );
+
+
+ # Update certain things in site_stats
+ $this->db->insert( 'site_stats', array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ) );
+
+ # Reinitialise the LocalisationCache to match the database state
+ Language::getLocalisationCache()->unloadAll();
+
+ # Make a new message cache
+ global $wgMessageCache, $wgMemc;
+ $wgMessageCache = new MessageCache( $wgMemc, true, 3600 );
+
+ $this->uploadDir = $this->setupUploadDir();
+ $user = User::createNew( 'WikiSysop' );
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Foobar.jpg' ) );
+ $image->recordUpload2( '', 'Upload of some lame file', 'Some lame file', array(
+ 'size' => 12345,
+ 'width' => 1941,
+ 'height' => 220,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+
+ # This image will be blacklisted in [[MediaWiki:Bad image list]]
+ $image = wfLocalFile( Title::makeTitle( NS_FILE, 'Bad.jpg' ) );
+ $image->recordUpload2( '', 'zomgnotcensored', 'Borderline image', array(
+ 'size' => 12345,
+ 'width' => 320,
+ 'height' => 240,
+ 'bits' => 24,
+ 'media_type' => MEDIATYPE_BITMAP,
+ 'mime' => 'image/jpeg',
+ 'metadata' => serialize( array() ),
+ 'sha1' => wfBaseConvert( '', 16, 36, 31 ),
+ 'fileExists' => true
+ ), $this->db->timestamp( '20010115123500' ), $user );
+ }
+
+ /**
+ * Change the table prefix on all open DB connections/
+ */
+ protected function changePrefix( $prefix ) {
+ global $wgDBprefix;
+ wfGetLBFactory()->forEachLB( array( $this, 'changeLBPrefix' ), array( $prefix ) );
+ $wgDBprefix = $prefix;
+ }
+
+ public function changeLBPrefix( $lb, $prefix ) {
+ $lb->forEachOpenConnection( array( $this, 'changeDBPrefix' ), array( $prefix ) );
+ }
+
+ public function changeDBPrefix( $db, $prefix ) {
+ $db->tablePrefix( $prefix );
+ }
+
+ public function teardownDatabase() {
+ if ( !$this->databaseSetupDone ) {
+ $this->teardownGlobals();
+ return;
+ }
+ $this->teardownUploadDir( $this->uploadDir );
+
+ $this->changePrefix( $this->oldTablePrefix );
+ $this->databaseSetupDone = false;
+
+ if ( $this->useTemporaryTables ) {
+ # Don't need to do anything
+ $this->teardownGlobals();
+ return;
+ }
+
+ $tables = $this->listTables();
+
+ foreach ( $tables as $table ) {
+ $sql = $this->db->getType() == 'oracle' ? "DROP TABLE pt_$table DROP CONSTRAINTS" : "DROP TABLE `parsertest_$table`";
+ $this->db->query( $sql );
+ }
+
+ if ( $this->db->getType() == 'oracle' )
+ $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' );
+
+ $this->teardownGlobals();
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return String: the directory
+ */
+ private function setupUploadDir() {
+ global $IP;
+
+ if ( $this->keepUploads ) {
+ $dir = wfTempDir() . '/mwParser-images';
+
+ if ( is_dir( $dir ) ) {
+ return $dir;
+ }
+ } else {
+ $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
+ }
+
+ // wfDebug( "Creating upload directory $dir\n" );
+ if ( file_exists( $dir ) ) {
+ wfDebug( "Already exists!\n" );
+ return $dir;
+ }
+
+ wfMkdirParents( $dir . '/3/3a' );
+ copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
+ wfMkdirParents( $dir . '/0/09' );
+ copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" );
+
+ return $dir;
+ }
+
+ /**
+ * Restore default values and perform any necessary clean-up
+ * after each test runs.
+ */
+ private function teardownGlobals() {
+ RepoGroup::destroySingleton();
+ LinkCache::singleton()->clear();
+
+ foreach ( $this->savedGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+ }
+
+ /**
+ * Remove the dummy uploads directory
+ */
+ private function teardownUploadDir( $dir ) {
+ if ( $this->keepUploads ) {
+ return;
+ }
+
+ // delete the files first, then the dirs.
+ self::deleteFiles(
+ array (
+ "$dir/3/3a/Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
+
+ "$dir/0/09/Bad.jpg",
+
+ "$dir/math/f/a/5/fa50b8b616463173474302ca3e63586b.png",
+ )
+ );
+
+ self::deleteDirs(
+ array (
+ "$dir/3/3a",
+ "$dir/3",
+ "$dir/thumb/6/65",
+ "$dir/thumb/6",
+ "$dir/thumb/3/3a/Foobar.jpg",
+ "$dir/thumb/3/3a",
+ "$dir/thumb/3",
+
+ "$dir/0/09/",
+ "$dir/0/",
+ "$dir/thumb",
+ "$dir/math/f/a/5",
+ "$dir/math/f/a",
+ "$dir/math/f",
+ "$dir/math",
+ "$dir",
+ )
+ );
+ }
+
+ /**
+ * Delete the specified files, if they exist.
+ * @param $files Array: full paths to files to delete.
+ */
+ private static function deleteFiles( $files ) {
+ foreach ( $files as $file ) {
+ if ( file_exists( $file ) ) {
+ unlink( $file );
+ }
+ }
+ }
+
+ /**
+ * Delete the specified directories, if they exist. Must be empty.
+ * @param $dirs Array: full paths to directories to delete.
+ */
+ private static function deleteDirs( $dirs ) {
+ foreach ( $dirs as $dir ) {
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir );
+ }
+ }
+ }
+
+ /**
+ * "Running test $desc..."
+ */
+ protected function showTesting( $desc ) {
+ print "Running test $desc... ";
+ }
+
+ /**
+ * Print a happy success message.
+ *
+ * @param $desc String: the test name
+ * @return Boolean
+ */
+ protected function showSuccess( $desc ) {
+ if ( $this->showProgress ) {
+ print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
+ }
+
+ return true;
+ }
+
+ /**
+ * Print a failure message and provide some explanatory output
+ * about what went wrong if so configured.
+ *
+ * @param $desc String: the test name
+ * @param $result String: expected HTML output
+ * @param $html String: actual HTML output
+ * @return Boolean
+ */
+ protected function showFailure( $desc, $result, $html ) {
+ if ( $this->showFailure ) {
+ if ( !$this->showProgress ) {
+ # In quiet mode we didn't show the 'Testing' message before the
+ # test, in case it succeeded. Show it now:
+ $this->showTesting( $desc );
+ }
+
+ print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
+
+ if ( $this->showOutput ) {
+ print "--- Expected ---\n$result\n--- Actual ---\n$html\n";
+ }
+
+ if ( $this->showDiffs ) {
+ print $this->quickDiff( $result, $html );
+ if ( !$this->wellFormed( $html ) ) {
+ print "XML error: $this->mXmlError\n";
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Run given strings through a diff and return the (colorized) output.
+ * Requires writable /tmp directory and a 'diff' command in the PATH.
+ *
+ * @param $input String
+ * @param $output String
+ * @param $inFileTail String: tailing for the input file name
+ * @param $outFileTail String: tailing for the output file name
+ * @return String
+ */
+ protected function quickDiff( $input, $output, $inFileTail = 'expected', $outFileTail = 'actual' ) {
+ $prefix = wfTempDir() . "/mwParser-" . mt_rand();
+
+ $infile = "$prefix-$inFileTail";
+ $this->dumpToFile( $input, $infile );
+
+ $outfile = "$prefix-$outFileTail";
+ $this->dumpToFile( $output, $outfile );
+
+ $diff = `diff -au $infile $outfile`;
+ unlink( $infile );
+ unlink( $outfile );
+
+ return $this->colorDiff( $diff );
+ }
+
+ /**
+ * Write the given string to a file, adding a final newline.
+ *
+ * @param $data String
+ * @param $filename String
+ */
+ private function dumpToFile( $data, $filename ) {
+ $file = fopen( $filename, "wt" );
+ fwrite( $file, $data . "\n" );
+ fclose( $file );
+ }
+
+ /**
+ * Colorize unified diff output if set for ANSI color output.
+ * Subtractions are colored blue, additions red.
+ *
+ * @param $text String
+ * @return String
+ */
+ protected function colorDiff( $text ) {
+ return preg_replace(
+ array( '/^(-.*)$/m', '/^(\+.*)$/m' ),
+ array( $this->term->color( 34 ) . '$1' . $this->term->reset(),
+ $this->term->color( 31 ) . '$1' . $this->term->reset() ),
+ $text );
+ }
+
+ /**
+ * Show "Reading tests from ..."
+ *
+ * @param $path String
+ */
+ public function showRunFile( $path ) {
+ print $this->term->color( 1 ) .
+ "Reading tests from \"$path\"..." .
+ $this->term->reset() .
+ "\n";
+ }
+
+ /**
+ * Insert a temporary test article
+ * @param $name String: the title, including any prefix
+ * @param $text String: the article text
+ * @param $line Integer: the input line number, for reporting errors
+ */
+ static public function addArticle( $name, $text, $line = 'unknown' ) {
+ global $wgCapitalLinks;
+
+ $text = self::chomp($text);
+
+ $oldCapitalLinks = $wgCapitalLinks;
+ $wgCapitalLinks = true; // We only need this from SetupGlobals() See r70917#c8637
+
+ $name = self::chomp( $name );
+ $title = Title::newFromText( $name );
+
+ if ( is_null( $title ) ) {
+ wfDie( "invalid title ('$name' => '$title') at line $line\n" );
+ }
+
+ $aid = $title->getArticleID( Title::GAID_FOR_UPDATE );
+
+ if ( $aid != 0 ) {
+ debug_print_backtrace();
+ wfDie( "duplicate article '$name' at line $line\n" );
+ }
+
+ $art = new Article( $title );
+ $art->doEdit( $text, '', EDIT_NEW );
+
+ $wgCapitalLinks = $oldCapitalLinks;
+ }
+
+ /**
+ * Steal a callback function from the primary parser, save it for
+ * application to our scary parser. If the hook is not installed,
+ * abort processing of this file.
+ *
+ * @param $name String
+ * @return Bool true if tag hook is present
+ */
+ public function requireHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit( ); // make sure hooks are loaded.
+
+ if ( isset( $wgParser->mTagHooks[$name] ) ) {
+ $this->hooks[$name] = $wgParser->mTagHooks[$name];
+ } else {
+ echo " This test suite requires the '$name' hook extension, skipping.\n";
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Steal a callback function from the primary parser, save it for
+ * application to our scary parser. If the hook is not installed,
+ * abort processing of this file.
+ *
+ * @param $name String
+ * @return Bool true if function hook is present
+ */
+ public function requireFunctionHook( $name ) {
+ global $wgParser;
+
+ $wgParser->firstCallInit( ); // make sure hooks are loaded.
+
+ if ( isset( $wgParser->mFunctionHooks[$name] ) ) {
+ $this->functionHooks[$name] = $wgParser->mFunctionHooks[$name];
+ } else {
+ echo " This test suite requires the '$name' function hook extension, skipping.\n";
+ return false;
+ }
+
+ return true;
+ }
+
+ /*
+ * Run the "tidy" command on text if the $wgUseTidy
+ * global is true
+ *
+ * @param $text String: the text to tidy
+ * @return String
+ * @static
+ */
+ private function tidy( $text ) {
+ global $wgUseTidy;
+
+ if ( $wgUseTidy ) {
+ $text = MWTidy::tidy( $text );
+ }
+
+ return $text;
+ }
+
+ private function wellFormed( $text ) {
+ $html =
+ Sanitizer::hackDocType() .
+ '<html>' .
+ $text .
+ '</html>';
+
+ $parser = xml_parser_create( "UTF-8" );
+
+ # case folding violates XML standard, turn it off
+ xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+ if ( !xml_parse( $parser, $html, true ) ) {
+ $err = xml_error_string( xml_get_error_code( $parser ) );
+ $position = xml_get_current_byte_index( $parser );
+ $fragment = $this->extractFragment( $html, $position );
+ $this->mXmlError = "$err at byte $position:\n$fragment";
+ xml_parser_free( $parser );
+
+ return false;
+ }
+
+ xml_parser_free( $parser );
+
+ return true;
+ }
+
+ private function extractFragment( $text, $position ) {
+ $start = max( 0, $position - 10 );
+ $before = $position - $start;
+ $fragment = '...' .
+ $this->term->color( 34 ) .
+ substr( $text, $start, $before ) .
+ $this->term->color( 0 ) .
+ $this->term->color( 31 ) .
+ $this->term->color( 1 ) .
+ substr( $text, $position, 1 ) .
+ $this->term->color( 0 ) .
+ $this->term->color( 34 ) .
+ substr( $text, $position + 1, 9 ) .
+ $this->term->color( 0 ) .
+ '...';
+ $display = str_replace( "\n", ' ', $fragment );
+ $caret = ' ' .
+ str_repeat( ' ', $before ) .
+ $this->term->color( 31 ) .
+ '^' .
+ $this->term->color( 0 );
+
+ return "$display\n$caret";
+ }
+
+ static function getFakeTimestamp( &$parser, &$ts ) {
+ $ts = 123;
+ return true;
+ }
+}
--- /dev/null
+# MediaWiki Parser test cases
+# Some taken from http://meta.wikimedia.org/wiki/Parser_testing
+# All (C) their respective authors and released under the GPL
+#
+# The syntax should be fairly self-explanatory.
+#
+# Currently supported test options:
+# One of the following three:
+#
+# (default) generate HTML output
+# pst apply pre-save transform
+# msg apply message transform
+#
+# Plus any combination of these:
+#
+# cat add category links
+# ill add inter-language links
+# subpage enable subpages (disabled by default)
+# noxml don't check for XML well formdness
+# title=[[XXX]] run test using article title XXX
+# language=XXX set content language to XXX for this test
+# variant=XXX set the variant of language for this test (eg zh-tw)
+# disabled do not run test
+# showtitle make the first line the title
+# comment run through Linker::formatComment() instead of main parser
+# local format section links in edit comment text as local links
+#
+# For testing purposes, temporary articles can created:
+# !!article / NAMESPACE:TITLE / !!text / ARTICLE TEXT / !!endarticle
+# where '/' denotes a newline.
+
+# This is the standard article assumed to exist.
+!! article
+Main Page
+!! text
+blah blah
+!! endarticle
+
+!!article
+Template:Foo
+!!text
+FOO
+!!endarticle
+
+!! article
+Template:Blank
+!! text
+!! endarticle
+
+!! article
+Template:!
+!! text
+|
+!! endarticle
+
+!!article
+MediaWiki:bad image list
+!!text
+* [[File:Bad.jpg]] except [[Nasty page]]
+!!endarticle
+
+###
+### Basic tests
+###
+!! test
+Blank input
+!! input
+!! result
+!! end
+
+
+!! test
+Simple paragraph
+!! input
+This is a simple paragraph.
+!! result
+<p>This is a simple paragraph.
+</p>
+!! end
+
+!! test
+Simple list
+!! input
+* Item 1
+* Item 2
+!! result
+<ul><li> Item 1
+</li><li> Item 2
+</li></ul>
+
+!! end
+
+!! test
+Italics and bold
+!! input
+* plain
+* plain''italic''plain
+* plain''italic''plain''italic''plain
+* plain'''bold'''plain
+* plain'''bold'''plain'''bold'''plain
+* plain''italic''plain'''bold'''plain
+* plain'''bold'''plain''italic''plain
+* plain''italic'''bold-italic'''italic''plain
+* plain'''bold''bold-italic''bold'''plain
+* plain'''''bold-italic'''italic''plain
+* plain'''''bold-italic''bold'''plain
+* plain''italic'''bold-italic'''''plain
+* plain'''bold''bold-italic'''''plain
+* plain l'''italic''plain
+* plain l''''bold''' plain
+!! result
+<ul><li> plain
+</li><li> plain<i>italic</i>plain
+</li><li> plain<i>italic</i>plain<i>italic</i>plain
+</li><li> plain<b>bold</b>plain
+</li><li> plain<b>bold</b>plain<b>bold</b>plain
+</li><li> plain<i>italic</i>plain<b>bold</b>plain
+</li><li> plain<b>bold</b>plain<i>italic</i>plain
+</li><li> plain<i>italic<b>bold-italic</b>italic</i>plain
+</li><li> plain<b>bold<i>bold-italic</i>bold</b>plain
+</li><li> plain<i><b>bold-italic</b>italic</i>plain
+</li><li> plain<b><i>bold-italic</i>bold</b>plain
+</li><li> plain<i>italic<b>bold-italic</b></i>plain
+</li><li> plain<b>bold<i>bold-italic</i></b>plain
+</li><li> plain l'<i>italic</i>plain
+</li><li> plain l'<b>bold</b> plain
+</li></ul>
+
+!! end
+
+###
+### <nowiki> test cases
+###
+
+!! test
+<nowiki> unordered list
+!! input
+<nowiki>* This is not an unordered list item.</nowiki>
+!! result
+<p>* This is not an unordered list item.
+</p>
+!! end
+
+!! test
+<nowiki> spacing
+!! input
+<nowiki>Lorem ipsum dolor
+
+sed abit.
+ sed nullum.
+
+:and a colon
+</nowiki>
+!! result
+<p>Lorem ipsum dolor
+
+sed abit.
+ sed nullum.
+
+:and a colon
+
+</p>
+!! end
+
+!! test
+nowiki 3
+!! input
+:There is not nowiki.
+:There is <nowiki>nowiki</nowiki>.
+
+#There is not nowiki.
+#There is <nowiki>nowiki</nowiki>.
+
+*There is not nowiki.
+*There is <nowiki>nowiki</nowiki>.
+!! result
+<dl><dd>There is not nowiki.
+</dd><dd>There is nowiki.
+</dd></dl>
+<ol><li>There is not nowiki.
+</li><li>There is nowiki.
+</li></ol>
+<ul><li>There is not nowiki.
+</li><li>There is nowiki.
+</li></ul>
+
+!! end
+
+
+###
+### Comments
+###
+!! test
+Comment test 1
+!! input
+<!-- comment 1 --> asdf
+<!-- comment 2 -->
+!! result
+<pre>asdf
+</pre>
+
+!! end
+
+!! test
+Comment test 2
+!! input
+asdf
+<!-- comment 1 -->
+jkl
+!! result
+<p>asdf
+jkl
+</p>
+!! end
+
+!! test
+Comment test 3
+!! input
+asdf
+<!-- comment 1 -->
+<!-- comment 2 -->
+jkl
+!! result
+<p>asdf
+jkl
+</p>
+!! end
+
+!! test
+Comment test 4
+!! input
+asdf<!-- comment 1 -->jkl
+!! result
+<p>asdfjkl
+</p>
+!! end
+
+!! test
+Comment spacing
+!! input
+a
+ <!-- foo --> b <!-- bar -->
+c
+!! result
+<p>a
+</p>
+<pre> b
+</pre>
+<p>c
+</p>
+!! end
+
+!! test
+Comment whitespace
+!! input
+<!-- returns a single newline, not nothing, since the newline after > is not stripped -->
+!! result
+
+!! end
+
+!! test
+Comment semantics and delimiters
+!! input
+<!-- --><!----><!-----><!------>
+!! result
+
+!! end
+
+!! test
+Comment semantics and delimiters, redux
+!! input
+<!-- In SGML every "foo" here would actually show up in the text -- foo -- bar
+-- foo -- funky huh? ... -->
+!! result
+
+!! end
+
+!! test
+Comment semantics and delimiters: directors cut
+!! input
+<!-- ... However we like to keep things simple and somewhat XML-ish so we eat
+everything starting with < followed by !-- until the first -- and > we see,
+that wouldn't be valid XML however, since in XML -- has to terminate a comment
+-->-->
+!! result
+<p>-->
+</p>
+!! end
+
+!! test
+Comment semantics: nesting
+!! input
+<!--<!-- no, we're not going to do anything fancy here -->-->
+!! result
+<p>-->
+</p>
+!! end
+
+!! test
+Comment semantics: unclosed comment at end
+!! input
+<!--This comment will run out to the end of the document
+!! result
+
+!! end
+
+!! test
+Comment in template title
+!! input
+{{f<!---->oo}}
+!! result
+<p>FOO
+</p>
+!! end
+
+!! test
+Comment on its own line post-expand
+!! input
+a
+{{blank}}<!---->
+b
+!! result
+<p>a
+</p><p>b
+</p>
+!! end
+
+###
+### Preformatted text
+###
+!! test
+Preformatted text
+!! input
+ This is some
+ Preformatted text
+ With ''italic''
+ And '''bold'''
+ And a [[Main Page|link]]
+!! result
+<pre>This is some
+Preformatted text
+With <i>italic</i>
+And <b>bold</b>
+And a <a href="/wiki/Main_Page" title="Main Page">link</a>
+</pre>
+!! end
+
+!! test
+<pre> with <nowiki> inside (compatibility with 1.6 and earlier)
+!! input
+<pre><nowiki>
+<b>
+<cite>
+<em>
+</nowiki></pre>
+!! result
+<pre>
+<b>
+<cite>
+<em>
+</pre>
+
+!! end
+
+!! test
+Regression with preformatted in <center>
+!! input
+<center>
+ Blah
+</center>
+!! result
+<center>
+<pre>Blah
+</pre>
+</center>
+
+!! end
+
+# Expected output in the following test is not really expected (there should be
+# <pre> in the output) -- it's only testing for well-formedness.
+!! test
+Bug 6200: Preformatted in <blockquote>
+!! input
+<blockquote>
+ Blah
+</blockquote>
+!! result
+<blockquote>
+ Blah
+</blockquote>
+
+!! end
+
+!! test
+<pre> with attributes (bug 3202)
+!! input
+<pre style="background: blue; color:white">Bluescreen of WikiDeath</pre>
+!! result
+<pre style="background: blue; color:white">Bluescreen of WikiDeath</pre>
+
+!! end
+
+!! test
+<pre> with width attribute (bug 3202)
+!! input
+<pre width="8">Narrow screen goodies</pre>
+!! result
+<pre width="8">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+<pre> with forbidden attribute (bug 3202)
+!! input
+<pre width="8" onmouseover="alert(document.cookie)">Narrow screen goodies</pre>
+!! result
+<pre width="8">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+<pre> with forbidden attribute values (bug 3202)
+!! input
+<pre width="8" style="border-width: expression(alert(document.cookie))">Narrow screen goodies</pre>
+!! result
+<pre width="8" style="/* insecure input */">Narrow screen goodies</pre>
+
+!! end
+
+!! test
+<nowiki> inside <pre> (bug 13238)
+!! input
+<pre>
+<nowiki>
+</pre>
+<pre>
+<nowiki></nowiki>
+</pre>
+<pre><nowiki><nowiki></nowiki>Foo<nowiki></nowiki></nowiki></pre>
+!! result
+<pre>
+<nowiki>
+</pre>
+<pre>
+
+</pre>
+<pre><nowiki>Foo</nowiki></pre>
+
+!! end
+
+!! test
+<nowiki> and <pre> preference (first one wins)
+!! input
+<pre>
+<nowiki>
+</pre>
+</nowiki>
+</pre>
+
+<nowiki>
+<pre>
+<nowiki>
+</pre>
+</nowiki>
+</pre>
+
+!! result
+<pre>
+<nowiki>
+</pre>
+<p></nowiki>
+</pre>
+</p><p>
+<pre>
+<nowiki>
+</pre>
+
+</pre>
+</p>
+!! end
+
+
+###
+### Definition lists
+###
+!! test
+Simple definition
+!! input
+; name : Definition
+!! result
+<dl><dt> name </dt><dd> Definition
+</dd></dl>
+
+!! end
+
+!! test
+Definition list for indentation only
+!! input
+: Indented text
+!! result
+<dl><dd> Indented text
+</dd></dl>
+
+!! end
+
+!! test
+Definition list with no space
+!! input
+;name:Definition
+!! result
+<dl><dt>name</dt><dd>Definition
+</dd></dl>
+
+!!end
+
+!! test
+Definition list with URL link
+!! input
+; http://example.com/ : definition
+!! result
+<dl><dt> <a href="http://example.com/" class="external free" rel="nofollow">http://example.com/</a> </dt><dd> definition
+</dd></dl>
+
+!! end
+
+!! test
+Definition list with bracketed URL link
+!! input
+;[http://www.example.com/ Example]:Something about it
+!! result
+<dl><dt><a href="http://www.example.com/" class="external text" rel="nofollow">Example</a></dt><dd>Something about it
+</dd></dl>
+
+!! end
+
+!! test
+Definition list with wikilink containing colon
+!! input
+; [[Help:FAQ]]: The least-read page on Wikipedia
+!! result
+<dl><dt> <a href="/index.php?title=Help:FAQ&action=edit&redlink=1" class="new" title="Help:FAQ (page does not exist)">Help:FAQ</a></dt><dd> The least-read page on Wikipedia
+</dd></dl>
+
+!! end
+
+# At Brion's and JeLuF's insistence... :)
+!! test
+Definition list with news link containing colon
+!! input
+; news:alt.wikipedia.rox: This isn't even a real newsgroup!
+!! result
+<dl><dt> <a href="news:alt.wikipedia.rox" class="external free" rel="nofollow">news:alt.wikipedia.rox</a></dt><dd> This isn't even a real newsgroup!
+</dd></dl>
+
+!! end
+
+!! test
+Malformed definition list with colon
+!! input
+; news:alt.wikipedia.rox -- don't crash or enter an infinite loop
+!! result
+<dl><dt> <a href="news:alt.wikipedia.rox" class="external free" rel="nofollow">news:alt.wikipedia.rox</a> -- don't crash or enter an infinite loop
+</dt></dl>
+
+!! end
+
+!! test
+Definition lists: colon in external link text
+!! input
+; [http://www.wikipedia2.org/ Wikipedia : The Next Generation]: OK, I made that up
+!! result
+<dl><dt> <a href="http://www.wikipedia2.org/" class="external text" rel="nofollow">Wikipedia : The Next Generation</a></dt><dd> OK, I made that up
+</dd></dl>
+
+!! end
+
+!! test
+Definition lists: colon in HTML attribute
+!! input
+;<b style="display: inline">bold</b>
+!! result
+<dl><dt><b style="display: inline">bold</b>
+</dt></dl>
+
+!! end
+
+
+!! test
+Definition lists: self-closed tag
+!! input
+;one<br/>two : two-line fun
+!! result
+<dl><dt>one<br />two </dt><dd> two-line fun
+</dd></dl>
+
+!! end
+
+
+###
+### External links
+###
+!! test
+External links: non-bracketed
+!! input
+Non-bracketed: http://example.com
+!! result
+<p>Non-bracketed: <a href="http://example.com" class="external free" rel="nofollow">http://example.com</a>
+</p>
+!! end
+
+!! test
+External links: numbered
+!! input
+Numbered: [http://example.com]
+Numbered: [http://example.net]
+Numbered: [http://example.com]
+!! result
+<p>Numbered: <a href="http://example.com" class="external autonumber" rel="nofollow">[1]</a>
+Numbered: <a href="http://example.net" class="external autonumber" rel="nofollow">[2]</a>
+Numbered: <a href="http://example.com" class="external autonumber" rel="nofollow">[3]</a>
+</p>
+!!end
+
+!! test
+External links: specified text
+!! input
+Specified text: [http://example.com link]
+!! result
+<p>Specified text: <a href="http://example.com" class="external text" rel="nofollow">link</a>
+</p>
+!!end
+
+!! test
+External links: trail
+!! input
+Linktrails should not work for external links: [http://example.com link]s
+!! result
+<p>Linktrails should not work for external links: <a href="http://example.com" class="external text" rel="nofollow">link</a>s
+</p>
+!! end
+
+!! test
+External links: dollar sign in URL
+!! input
+http://example.com/1$2345
+!! result
+<p><a href="http://example.com/1$2345" class="external free" rel="nofollow">http://example.com/1$2345</a>
+</p>
+!! end
+
+!! test
+External links: dollar sign in URL (named)
+!! input
+[http://example.com/1$2345]
+!! result
+<p><a href="http://example.com/1$2345" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!!end
+
+!! test
+External links: open square bracket forbidden in URL (bug 4377)
+!! input
+http://example.com/1[2345
+!! result
+<p><a href="http://example.com/1" class="external free" rel="nofollow">http://example.com/1</a>[2345
+</p>
+!! end
+
+!! test
+External links: open square bracket forbidden in URL (named) (bug 4377)
+!! input
+[http://example.com/1[2345]
+!! result
+<p><a href="http://example.com/1" class="external text" rel="nofollow">[2345</a>
+</p>
+!!end
+
+!! test
+External links: nowiki in URL link text (bug 6230)
+!!input
+[http://example.com/ <nowiki>''example site''</nowiki>]
+!! result
+<p><a href="http://example.com/" class="external text" rel="nofollow">''example site''</a>
+</p>
+!! end
+
+!! test
+External links: newline forbidden in text (bug 6230 regression check)
+!! input
+[http://example.com/ first
+second]
+!! result
+<p>[<a href="http://example.com/" class="external free" rel="nofollow">http://example.com/</a> first
+second]
+</p>
+!!end
+
+!! test
+External image
+!! input
+External image: http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! result
+<p>External image: <img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" />
+</p>
+!! end
+
+!! test
+External image from https
+!! input
+External image from https: https://meta.wikimedia.org/upload/f/f1/Ncwikicol.png
+!! result
+<p>External image from https: <img src="https://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" />
+</p>
+!! end
+
+!! test
+Link to non-http image, no img tag
+!! input
+Link to non-http image, no img tag: ftp://example.com/test.jpg
+!! result
+<p>Link to non-http image, no img tag: <a href="ftp://example.com/test.jpg" class="external free" rel="nofollow">ftp://example.com/test.jpg</a>
+</p>
+!! end
+
+!! test
+External links: terminating separator
+!! input
+Terminating separator: http://example.com/thing,
+!! result
+<p>Terminating separator: <a href="http://example.com/thing" class="external free" rel="nofollow">http://example.com/thing</a>,
+</p>
+!! end
+
+!! test
+External links: intervening separator
+!! input
+Intervening separator: http://example.com/1,2,3
+!! result
+<p>Intervening separator: <a href="http://example.com/1,2,3" class="external free" rel="nofollow">http://example.com/1,2,3</a>
+</p>
+!! end
+
+!! test
+External links: old bug with URL in query
+!! input
+Old bug with URL in query: [http://example.com/thing?url=http://example.com link]
+!! result
+<p>Old bug with URL in query: <a href="http://example.com/thing?url=http://example.com" class="external text" rel="nofollow">link</a>
+</p>
+!! end
+
+!! test
+External links: old URL-in-URL bug, mixed protocols
+!! input
+And again with mixed protocols: [ftp://example.com?url=http://example.com link]
+!! result
+<p>And again with mixed protocols: <a href="ftp://example.com?url=http://example.com" class="external text" rel="nofollow">link</a>
+</p>
+!!end
+
+!! test
+External links: URL in text
+!! input
+URL in text: [http://example.com http://example.com]
+!! result
+<p>URL in text: <a href="http://example.com" class="external free" rel="nofollow">http://example.com</a>
+</p>
+!! end
+
+!! test
+External links: Clickable images
+!! input
+ja-style clickable images: [http://example.com http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png]
+!! result
+<p>ja-style clickable images: <a href="http://example.com" class="external text" rel="nofollow"><img src="http://meta.wikimedia.org/upload/f/f1/Ncwikicol.png" alt="Ncwikicol.png" /></a>
+</p>
+!!end
+
+!! test
+External links: raw ampersand
+!! input
+Old & use: http://x&y
+!! result
+<p>Old & use: <a href="http://x&y" class="external free" rel="nofollow">http://x&y</a>
+</p>
+!! end
+
+!! test
+External links: encoded ampersand
+!! input
+Old & use: http://x&y
+!! result
+<p>Old & use: <a href="http://x&y" class="external free" rel="nofollow">http://x&y</a>
+</p>
+!! end
+
+!! test
+External links: encoded equals (bug 6102)
+!! input
+http://example.com/?foo=bar
+!! result
+<p><a href="http://example.com/?foo=bar" class="external free" rel="nofollow">http://example.com/?foo=bar</a>
+</p>
+!! end
+
+!! test
+External links: [raw ampersand]
+!! input
+Old & use: [http://x&y]
+!! result
+<p>Old & use: <a href="http://x&y" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+!! test
+External links: [encoded ampersand]
+!! input
+Old & use: [http://x&y]
+!! result
+<p>Old & use: <a href="http://x&y" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+!! test
+External links: [encoded equals] (bug 6102)
+!! input
+[http://example.com/?foo=bar]
+!! result
+<p><a href="http://example.com/?foo=bar" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+!! test
+External links: [IDN ignored character reference in hostname; strip it right off]
+!! input
+[http://e‌xample.com/]
+!! result
+<p><a href="http://example.com/" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+!! test
+External links: IDN ignored character reference in hostname; strip it right off
+!! input
+http://e‌xample.com/
+!! result
+<p><a href="http://example.com/" class="external free" rel="nofollow">http://example.com/</a>
+</p>
+!! end
+
+!! test
+External links: www.jpeg.org (bug 554)
+!! input
+http://www.jpeg.org
+!!result
+<p><a href="http://www.jpeg.org" class="external free" rel="nofollow">http://www.jpeg.org</a>
+</p>
+!! end
+
+!! test
+External links: URL within URL (original bug 2)
+!! input
+[http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp]
+!! result
+<p><a href="http://www.unausa.org/newindex.asp?place=http://www.unausa.org/programs/mun.asp" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+!! test
+BUG 361: URL inside bracketed URL
+!! input
+[http://www.example.com/foo http://www.example.com/bar]
+!! result
+<p><a href="http://www.example.com/foo" class="external text" rel="nofollow">http://www.example.com/bar</a>
+</p>
+!! end
+
+!! test
+BUG 361: URL within URL, not bracketed
+!! input
+http://www.example.com/foo?=http://www.example.com/bar
+!! result
+<p><a href="http://www.example.com/foo?=http://www.example.com/bar" class="external free" rel="nofollow">http://www.example.com/foo?=http://www.example.com/bar</a>
+</p>
+!! end
+
+!! test
+BUG 289: ">"-token in URL-tail
+!! input
+http://www.example.com/<hello>
+!! result
+<p><a href="http://www.example.com/" class="external free" rel="nofollow">http://www.example.com/</a><hello>
+</p>
+!!end
+
+!! test
+BUG 289: literal ">"-token in URL-tail
+!! input
+http://www.example.com/<b>html</b>
+!! result
+<p><a href="http://www.example.com/" class="external free" rel="nofollow">http://www.example.com/</a><b>html</b>
+</p>
+!!end
+
+!! test
+BUG 289: ">"-token in bracketed URL
+!! input
+[http://www.example.com/<hello> stuff]
+!! result
+<p><a href="http://www.example.com/" class="external text" rel="nofollow"><hello> stuff</a>
+</p>
+!!end
+
+!! test
+BUG 289: literal ">"-token in bracketed URL
+!! input
+[http://www.example.com/<b>html</b> stuff]
+!! result
+<p><a href="http://www.example.com/" class="external text" rel="nofollow"><b>html</b> stuff</a>
+</p>
+!!end
+
+!! test
+BUG 289: literal double quote at end of URL
+!! input
+http://www.example.com/"hello"
+!! result
+<p><a href="http://www.example.com/" class="external free" rel="nofollow">http://www.example.com/</a>"hello"
+</p>
+!!end
+
+!! test
+BUG 289: literal double quote in bracketed URL
+!! input
+[http://www.example.com/"hello" stuff]
+!! result
+<p><a href="http://www.example.com/" class="external text" rel="nofollow">"hello" stuff</a>
+</p>
+!!end
+
+!! test
+External links: multiple legal whitespace is fine, Magnus. Don't break it please. (bug 5081)
+!! input
+[http://www.example.com test]
+!! result
+<p><a href="http://www.example.com" class="external text" rel="nofollow">test</a>
+</p>
+!! end
+
+!! test
+External links: wiki links within external link (Bug 3695)
+!! input
+[http://example.com [[wikilink]] embedded in ext link]
+!! result
+<p><a href="http://example.com" class="external text" rel="nofollow"></a><a href="/index.php?title=Wikilink&action=edit&redlink=1" class="new" title="Wikilink (page does not exist)">wikilink</a><a href="http://example.com" class="external text" rel="nofollow"> embedded in ext link</a>
+</p>
+!! end
+
+!! test
+BUG 787: Links with one slash after the url protocol are invalid
+!! input
+http:/example.com
+
+[http:/example.com title]
+!! result
+<p>http:/example.com
+</p><p>[http:/example.com title]
+</p>
+!! end
+
+!! test
+Bug 2702: Mismatched <i>, <b> and <a> tags are invalid
+!! input
+''[http://example.com text'']
+[http://example.com '''text]'''
+''Something [http://example.com in italic'']
+''Something [http://example.com mixed''''', even bold]'''
+'''''Now [http://example.com both''''']
+!! result
+<p><a href="http://example.com" class="external text" rel="nofollow"><i>text</i></a>
+<a href="http://example.com" class="external text" rel="nofollow"><b>text</b></a>
+<i>Something </i><a href="http://example.com" class="external text" rel="nofollow"><i>in italic</i></a>
+<i>Something </i><a href="http://example.com" class="external text" rel="nofollow"><i>mixed</i><b>, even bold</b></a>
+<i><b>Now </b></i><a href="http://example.com" class="external text" rel="nofollow"><i><b>both</b></i></a>
+</p>
+!! end
+
+
+!! test
+Bug 4781: %26 in URL
+!! input
+http://www.example.com/?title=AT%26T
+!! result
+<p><a href="http://www.example.com/?title=AT%26T" class="external free" rel="nofollow">http://www.example.com/?title=AT%26T</a>
+</p>
+!! end
+
+!! test
+Bug 4781, 5267: %26 in URL
+!! input
+http://www.example.com/?title=100%25_Bran
+!! result
+<p><a href="http://www.example.com/?title=100%25_Bran" class="external free" rel="nofollow">http://www.example.com/?title=100%25_Bran</a>
+</p>
+!! end
+
+!! test
+Bug 4781, 5267: %28, %29 in URL
+!! input
+http://www.example.com/?title=Ben-Hur_%281959_film%29
+!! result
+<p><a href="http://www.example.com/?title=Ben-Hur_%281959_film%29" class="external free" rel="nofollow">http://www.example.com/?title=Ben-Hur_%281959_film%29</a>
+</p>
+!! end
+
+
+!! test
+Bug 4781: %26 in autonumber URL
+!! input
+[http://www.example.com/?title=AT%26T]
+!! result
+<p><a href="http://www.example.com/?title=AT%26T" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+!! test
+Bug 4781, 5267: %26 in autonumber URL
+!! input
+[http://www.example.com/?title=100%25_Bran]
+!! result
+<p><a href="http://www.example.com/?title=100%25_Bran" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+!! test
+Bug 4781, 5267: %28, %29 in autonumber URL
+!! input
+[http://www.example.com/?title=Ben-Hur_%281959_film%29]
+!! result
+<p><a href="http://www.example.com/?title=Ben-Hur_%281959_film%29" class="external autonumber" rel="nofollow">[1]</a>
+</p>
+!! end
+
+
+!! test
+Bug 4781: %26 in bracketed URL
+!! input
+[http://www.example.com/?title=AT%26T link]
+!! result
+<p><a href="http://www.example.com/?title=AT%26T" class="external text" rel="nofollow">link</a>
+</p>
+!! end
+
+!! test
+Bug 4781, 5267: %26 in bracketed URL
+!! input
+[http://www.example.com/?title=100%25_Bran link]
+!! result
+<p><a href="http://www.example.com/?title=100%25_Bran" class="external text" rel="nofollow">link</a>
+</p>
+!! end
+
+!! test
+Bug 4781, 5267: %28, %29 in bracketed URL
+!! input
+[http://www.example.com/?title=Ben-Hur_%281959_film%29 link]
+!! result
+<p><a href="http://www.example.com/?title=Ben-Hur_%281959_film%29" class="external text" rel="nofollow">link</a>
+</p>
+!! end
+
+!! test
+External link containing double-single-quotes in text '' (bug 4598 sanity check)
+!! input
+Some [http://example.com/ pretty ''italics'' and stuff]!
+!! result
+<p>Some <a href="http://example.com/" class="external text" rel="nofollow">pretty <i>italics</i> and stuff</a>!
+</p>
+!! end
+
+!! test
+External link containing double-single-quotes in text embedded in italics (bug 4598 sanity check)
+!! input
+''Some [http://example.com/ pretty ''italics'' and stuff]!''
+!! result
+<p><i>Some </i><a href="http://example.com/" class="external text" rel="nofollow"><i>pretty </i>italics<i> and stuff</i></a><i>!</i>
+</p>
+!! end
+
+!! test
+External link containing double-single-quotes with no space separating the url from text in italics
+!! input
+[http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm''La muerte de Casagemas'' (1901) en el sitio de [[Museo Picasso (París)|Museo Picasso]].]
+!! result
+<p><a href="http://www.musee-picasso.fr/pages/page_id18528_u1l2.htm" class="external text" rel="nofollow"><i>La muerte de Casagemas</i> (1901) en el sitio de <a href="/index.php?title=Museo_Picasso_(Par%C3%ADs)&action=edit&redlink=1" class="new" title="Museo Picasso (París) (page does not exist)">Museo Picasso</a>.</a>
+</p>
+!! end
+
+!! test
+URL-encoding in URL functions (single parameter)
+!! input
+{{localurl:Some page|amp=&}}
+!! result
+<p>/index.php?title=Some_page&amp=&
+</p>
+!! end
+
+!! test
+URL-encoding in URL functions (multiple parameters)
+!! input
+{{localurl:Some page|q=?&=&}}
+!! result
+<p>/index.php?title=Some_page&q=?&amp=&
+</p>
+!! end
+
+###
+### Quotes
+###
+
+!! test
+Quotes
+!! input
+Normal text. '''Bold text.''' Normal text. ''Italic text.''
+
+Normal text. '''''Bold italic text.''''' Normal text.
+!!result
+<p>Normal text. <b>Bold text.</b> Normal text. <i>Italic text.</i>
+</p><p>Normal text. <i><b>Bold italic text.</b></i> Normal text.
+</p>
+!! end
+
+
+!! test
+Unclosed and unmatched quotes
+!! input
+'''''Bold italic text '''with bold deactivated''' in between.'''''
+
+'''''Bold italic text ''with italic deactivated'' in between.'''''
+
+'''Bold text..
+
+..spanning two paragraphs (should not work).'''
+
+'''Bold tag left open
+
+''Italic tag left open
+
+Normal text.
+
+<!-- Unmatching number of opening, closing tags: -->
+'''This year''''s election ''should'' beat '''last year''''s.
+
+''Tom'''s car is bigger than ''Susan'''s.
+!! result
+<p><i><b>Bold italic text </b>with bold deactivated<b> in between.</b></i>
+</p><p><b><i>Bold italic text </i>with italic deactivated<i> in between.</i></b>
+</p><p><b>Bold text..</b>
+</p><p>..spanning two paragraphs (should not work).
+</p><p><b>Bold tag left open</b>
+</p><p><i>Italic tag left open</i>
+</p><p>Normal text.
+</p><p><b>This year'</b>s election <i>should</i> beat <b>last year'</b>s.
+</p><p><i>Tom<b>s car is bigger than </b></i><b>Susan</b>s.
+</p>
+!! end
+
+###
+### Tables
+###
+### some content taken from http://meta.wikimedia.org/wiki/MediaWiki_User%27s_Guide:_Using_tables
+###
+
+# This should not produce <table></table> as <table><tr><td></td></tr></table>
+# is the bare minimun required by the spec, see:
+# http://www.w3.org/TR/xhtml-modularization/dtd_module_defs.html#a_module_Basic_Tables
+!! test
+A table with no data.
+!! input
+{||}
+!! result
+!! end
+
+# A table with nothing but a caption is invalid XHTML, we might want to render
+# this as <p>caption</p>
+!! test
+A table with nothing but a caption
+!! input
+{|
+|+ caption
+|}
+!! result
+<table>
+<caption> caption
+</caption><tr><td></td></tr></table>
+
+!! end
+
+!! test
+Simple table
+!! input
+{|
+| 1 || 2
+|-
+| 3 || 4
+|}
+!! result
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+
+!! end
+
+!! test
+Multiplication table
+!! input
+{| border="1" cellpadding="2"
+|+Multiplication table
+|-
+! × !! 1 !! 2 !! 3
+|-
+! 1
+| 1 || 2 || 3
+|-
+! 2
+| 2 || 4 || 6
+|-
+! 3
+| 3 || 6 || 9
+|-
+! 4
+| 4 || 8 || 12
+|-
+! 5
+| 5 || 10 || 15
+|}
+!! result
+<table border="1" cellpadding="2">
+<caption>Multiplication table
+</caption>
+<tr>
+<th> × </th>
+<th> 1 </th>
+<th> 2 </th>
+<th> 3
+</th></tr>
+<tr>
+<th> 1
+</th>
+<td> 1 </td>
+<td> 2 </td>
+<td> 3
+</td></tr>
+<tr>
+<th> 2
+</th>
+<td> 2 </td>
+<td> 4 </td>
+<td> 6
+</td></tr>
+<tr>
+<th> 3
+</th>
+<td> 3 </td>
+<td> 6 </td>
+<td> 9
+</td></tr>
+<tr>
+<th> 4
+</th>
+<td> 4 </td>
+<td> 8 </td>
+<td> 12
+</td></tr>
+<tr>
+<th> 5
+</th>
+<td> 5 </td>
+<td> 10 </td>
+<td> 15
+</td></tr></table>
+
+!! end
+
+!! test
+Table rowspan
+!! input
+{| align=right border=1
+| Cell 1, row 1
+|rowspan=2| Cell 2, row 1 (and 2)
+| Cell 3, row 1
+|-
+| Cell 1, row 2
+| Cell 3, row 2
+|}
+!! result
+<table align="right" border="1">
+<tr>
+<td> Cell 1, row 1
+</td>
+<td rowspan="2"> Cell 2, row 1 (and 2)
+</td>
+<td> Cell 3, row 1
+</td></tr>
+<tr>
+<td> Cell 1, row 2
+</td>
+<td> Cell 3, row 2
+</td></tr></table>
+
+!! end
+
+!! test
+Nested table
+!! input
+{| border=1
+| α
+|
+{| bgcolor=#ABCDEF border=2
+|nested
+|-
+|table
+|}
+|the original table again
+|}
+!! result
+<table border="1">
+<tr>
+<td> α
+</td>
+<td>
+<table bgcolor="#ABCDEF" border="2">
+<tr>
+<td>nested
+</td></tr>
+<tr>
+<td>table
+</td></tr></table>
+</td>
+<td>the original table again
+</td></tr></table>
+
+!! end
+
+!! test
+Invalid attributes in table cell (bug 1830)
+!! input
+{|
+|Cell:|broken
+|}
+!! result
+<table>
+<tr>
+<td>broken
+</td></tr></table>
+
+!! end
+
+
+!! test
+Table security: embedded pipes (http://lists.wikimedia.org/mailman/htdig/wikitech-l/2006-April/022293.html)
+!! input
+{|
+| |[ftp://|x||]" onmouseover="alert(document.cookie)">test
+!! result
+<table>
+<tr>
+<td>[<a href="ftp://%7Cx" class="external free" rel="nofollow">ftp://%7Cx</a></td>
+<td>]" onmouseover="alert(document.cookie)">test
+</td>
+</tr>
+</table>
+
+!! end
+
+
+###
+### Internal links
+###
+!! test
+Plain link, capitalized
+!! input
+[[Main Page]]
+!! result
+<p><a href="/wiki/Main_Page">Main Page</a>
+</p>
+!! end
+
+!! test
+Plain link, uncapitalized
+!! input
+[[main Page]]
+!! result
+<p><a href="/wiki/Main_Page">main Page</a>
+</p>
+!! end
+
+!! test
+Piped link
+!! input
+[[Main Page|The Main Page]]
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">The Main Page</a>
+</p>
+!! end
+
+!! test
+Broken link
+!! input
+[[Zigzagzogzagzig]]
+!! result
+<p><a href="/index.php?title=Zigzagzogzagzig&action=edit&redlink=1" class="new" title="Zigzagzogzagzig (page does not exist)">Zigzagzogzagzig</a>
+</p>
+!! end
+
+!! test
+Broken link with fragment
+!! input
+[[Zigzagzogzagzig#zug]]
+!! result
+<p><a href="/index.php?title=Zigzagzogzagzig&action=edit&redlink=1" class="new" title="Zigzagzogzagzig (page does not exist)">Zigzagzogzagzig#zug</a>
+</p>
+!! end
+
+!! test
+Special page link with fragment
+!! input
+[[Special:Version#anchor]]
+!! result
+<p><a href="/wiki/Special:Version#anchor" title="Special:Version">Special:Version#anchor</a>
+</p>
+!! end
+
+!! test
+Nonexistent special page link with fragment
+!! input
+[[Special:ThisNameWillHopefullyNeverBeUsed#anchor]]
+!! result
+<p><a href="/wiki/Special:ThisNameWillHopefullyNeverBeUsed" class="new" title="Special:ThisNameWillHopefullyNeverBeUsed (page does not exist)">Special:ThisNameWillHopefullyNeverBeUsed#anchor</a>
+</p>
+!! end
+
+!! test
+Link with prefix
+!! input
+xxx[[main Page]], xxx[[Main Page]], Xxx[[main Page]] XXX[[main Page]], XXX[[Main Page]]
+!! result
+<p>xxx<a href="/wiki/Main_Page">main Page</a>, xxx<a href="/wiki/Main_Page">Main Page</a>, Xxx<a href="/wiki/Main_Page">main Page</a> XXX<a href="/wiki/Main_Page">main Page</a>, XXX<a href="/wiki/Main_Page">Main Page</a>
+</p>
+!! end
+
+!! test
+Link with suffix
+!! input
+[[Main Page]]xxx, [[Main Page]]XXX, [[Main Page]]!!!
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">Main Pagexxx</a>, <a href="/wiki/Main_Page">Main Page</a>XXX, <a href="/wiki/Main_Page">Main Page</a>!!!
+</p>
+!! end
+
+!! test
+Link with 3 brackets
+!! input
+[[[main page]]]
+!! result
+<p>[[[main page]]]
+</p>
+!! end
+
+!! test
+Piped link with 3 brackets
+!! input
+[[[main page|the main page]]]
+!! result
+<p>[[[main page|the main page]]]
+</p>
+!! end
+
+!! test
+Link with multiple pipes
+!! input
+[[Main Page|The|Main|Page]]
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">The|Main|Page</a>
+</p>
+!! end
+
+!! test
+Link to namespaces
+!! input
+[[Talk:Parser testing]], [[Meta:Disclaimers]]
+!! result
+<p><a href="/index.php?title=Talk:Parser_testing&action=edit&redlink=1" class="new" title="Talk:Parser testing (page does not exist)">Talk:Parser testing</a>, <a href="/index.php?title=Meta:Disclaimers&action=edit&redlink=1" class="new" title="Meta:Disclaimers (page does not exist)">Meta:Disclaimers</a>
+</p>
+!! end
+
+!! test
+Piped link to namespace
+!! input
+[[Meta:Disclaimers|The disclaimers]]
+!! result
+<p><a href="/index.php?title=Meta:Disclaimers&action=edit&redlink=1" class="new" title="Meta:Disclaimers (page does not exist)">The disclaimers</a>
+</p>
+!! end
+
+!! test
+Link containing }
+!! input
+[[Usually caused by a typo (oops}]]
+!! result
+<p>[[Usually caused by a typo (oops}]]
+</p>
+!! end
+
+!! test
+Link containing % (not as a hex sequence)
+!! input
+[[7% Solution]]
+!! result
+<p><a href="/index.php?title=7%25_Solution&action=edit&redlink=1" class="new" title="7% Solution (page does not exist)">7% Solution</a>
+</p>
+!! end
+
+!! test
+Link containing % as a single hex sequence interpreted to char
+!! input
+[[7%25 Solution]]
+!! result
+<p><a href="/index.php?title=7%25_Solution&action=edit&redlink=1" class="new" title="7% Solution (page does not exist)">7% Solution</a>
+</p>
+!!end
+
+!! test
+Link containing % as a double hex sequence interpreted to hex sequence
+!! input
+[[7%2525 Solution]]
+!! result
+<p>[[7%2525 Solution]]
+</p>
+!!end
+
+!! test
+Link containing "#<" and "#>" % as a hex sequences- these are valid section anchors
+Example for such a section: == < ==
+!! input
+[[%23%3c]][[%23%3e]]
+!! result
+<p><a href="#.3C">#<</a><a href="#.3E">#></a>
+</p>
+!! end
+
+!! test
+Link containing "<#" and ">#" as a hex sequences
+!! input
+[[%3c%23]][[%3e%23]]
+!! result
+<p>[[%3c%23]][[%3e%23]]
+</p>
+!! end
+
+!! test
+Link containing double-single-quotes '' (bug 4598)
+!! input
+[[Lista d''e paise d''o munno]]
+!! result
+<p><a href="/index.php?title=Lista_d%27%27e_paise_d%27%27o_munno&action=edit&redlink=1" class="new" title="Lista d''e paise d''o munno (page does not exist)">Lista d''e paise d''o munno</a>
+</p>
+!! end
+
+!! test
+Link containing double-single-quotes '' in text (bug 4598 sanity check)
+!! input
+Some [[Link|pretty ''italics'' and stuff]]!
+!! result
+<p>Some <a href="/index.php?title=Link&action=edit&redlink=1" class="new" title="Link (page does not exist)">pretty <i>italics</i> and stuff</a>!
+</p>
+!! end
+
+!! test
+Link containing double-single-quotes '' in text embedded in italics (bug 4598 sanity check)
+!! input
+''Some [[Link|pretty ''italics'' and stuff]]!
+!! result
+<p><i>Some <a href="/index.php?title=Link&action=edit&redlink=1" class="new" title="Link (page does not exist)">pretty <i>italics</i> and stuff</a>!</i>
+</p>
+!! end
+
+!! test
+Link with double quotes in title part (literal) and alternate part (interpreted)
+!! input
+[[File:Denys Savchenko ''Pentecoste''.jpg]]
+
+[[''Pentecoste'']]
+
+[[''Pentecoste''|Pentecoste]]
+
+[[''Pentecoste''|''Pentecoste'']]
+!! result
+<p><a href="/index.php?title=Special:Upload&wpDestFile=Denys_Savchenko_%27%27Pentecoste%27%27.jpg" class="new" title="File:Denys Savchenko ''Pentecoste''.jpg">File:Denys Savchenko <i>Pentecoste</i>.jpg</a>
+</p><p><a href="/index.php?title=%27%27Pentecoste%27%27&action=edit&redlink=1" class="new" title="''Pentecoste'' (page does not exist)">''Pentecoste''</a>
+</p><p><a href="/index.php?title=%27%27Pentecoste%27%27&action=edit&redlink=1" class="new" title="''Pentecoste'' (page does not exist)">Pentecoste</a>
+</p><p><a href="/index.php?title=%27%27Pentecoste%27%27&action=edit&redlink=1" class="new" title="''Pentecoste'' (page does not exist)"><i>Pentecoste</i></a>
+</p>
+!! end
+
+!! test
+Plain link to URL
+!! input
+[[http://www.example.com]]
+!! result
+<p>[<a href="http://www.example.com" class="external autonumber" rel="nofollow">[1]</a>]
+</p>
+!! end
+
+# I'm fairly sure the expected result here is wrong.
+# We want these to be URL links, not pseudo-pages with URLs for titles....
+# However the current output is also pretty screwy.
+#
+# ----
+# I'm changing it to match the current output--it arguably makes more
+# sense in the light of the test above. Old expected result was:
+#<p>Piped link to URL: <a href="/index.php?title=Http://www.example.com&action=edit" class="new">an example URL</a>
+#</p>
+# But I think this test is bordering on "garbage in, garbage out" anyway.
+# -- wtm
+!! test
+Piped link to URL
+!! input
+Piped link to URL: [[http://www.example.com|an example URL]]
+!! result
+<p>Piped link to URL: [<a href="http://www.example.com%7Can" class="external text" rel="nofollow">example URL</a>]
+</p>
+!! end
+
+!! test
+BUG 2: [[page|http://url/]] should link to page, not http://url/
+!! input
+[[Main Page|http://url/]]
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">http://url/</a>
+</p>
+!! end
+
+!! test
+BUG 337: Escaped self-links should be bold
+!! options
+title=[[Bug462]]
+!! input
+[[Bug462]] [[Bug462]]
+!! result
+<p><strong class="selflink">Bug462</strong> <strong class="selflink">Bug462</strong>
+</p>
+!! end
+
+!! test
+Self-link to section should not be bold
+!! options
+title=[[Main Page]]
+!! input
+[[Main Page#section]]
+!! result
+<p><a href="/wiki/Main_Page#section" title="Main Page">Main Page#section</a>
+</p>
+!! end
+
+!! article
+00
+!! text
+This is 00.
+!! endarticle
+
+!!test
+Self-link to numeric title
+!!options
+title=[[0]]
+!!input
+[[0]]
+!!result
+<p><strong class="selflink">0</strong>
+</p>
+!!end
+
+!!test
+Link to numeric-equivalent title
+!!options
+title=[[0]]
+!!input
+[[00]]
+!!result
+<p><a href="/wiki/00">00</a>
+</p>
+!!end
+
+!! test
+<nowiki> inside a link
+!! input
+[[Main<nowiki> Page</nowiki>]] [[Main Page|the main page <nowiki>[it's not very good]</nowiki>]]
+!! result
+<p>[[Main Page]] <a href="/wiki/Main_Page" title="Main Page">the main page [it's not very good]</a>
+</p>
+!! end
+
+!! test
+Non-breaking spaces in title
+!! input
+[[ Main Page ]]
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page"> Main Page </a>
+</p>
+!!end
+
+
+###
+### Interwiki links (see maintenance/interwiki.sql)
+###
+
+!! test
+Inline interwiki link
+!! input
+[[MeatBall:SoftSecurity]]
+!! result
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?SoftSecurity" class="extiw">MeatBall:SoftSecurity</a>
+</p>
+!! end
+
+!! test
+Inline interwiki link with empty title (bug 2372)
+!! input
+[[MeatBall:]]
+!! result
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?" class="extiw">MeatBall:</a>
+</p>
+!! end
+
+!! test
+Interwiki link encoding conversion (bug 1636)
+!! input
+*[[Wikipedia:ro:Olteniţa]]
+*[[Wikipedia:ro:Olteniţa]]
+!! result
+<ul><li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a>
+</li><li><a href="http://en.wikipedia.org/wiki/ro:Olteni%C5%A3a" class="extiw" title="wikipedia:ro:Olteniţa">Wikipedia:ro:Olteniţa</a>
+</li></ul>
+
+!! end
+
+!! test
+Interwiki link with fragment (bug 2130)
+!! input
+[[MeatBall:SoftSecurity#foo]]
+!! result
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?SoftSecurity#foo" class="extiw" title="meatball:SoftSecurity">MeatBall:SoftSecurity#foo</a>
+</p>
+!! end
+
+!! test
+Interlanguage link
+!! input
+Blah blah blah
+[[zh:Chinese]]
+!!result
+<p>Blah blah blah
+</p>
+!! end
+
+!! test
+Double interlanguage link
+!! input
+Blah blah blah
+[[es:Spanish]]
+[[zh:Chinese]]
+!!result
+<p>Blah blah blah
+</p>
+!! end
+
+!! test
+Interlanguage link, with prefix links
+!! options
+language=ln
+!! input
+Blah blah blah
+[[zh:Chinese]]
+!!result
+<p>Blah blah blah
+</p>
+!! end
+
+!! test
+Double interlanguage link, with prefix links (bug 8897)
+!! options
+language=ln
+!! input
+Blah blah blah
+[[es:Spanish]]
+[[zh:Chinese]]
+!!result
+<p>Blah blah blah
+</p>
+!! end
+
+
+##
+## XHTML tidiness
+###
+
+!! test
+<br> to <br />
+!! input
+1<br>2<br />3
+!! result
+<p>1<br />2<br />3
+</p>
+!! end
+
+!! test
+Incorrecly removing closing slashes from correctly formed XHTML
+!! input
+<br style="clear:both;" />
+!! result
+<p><br style="clear:both;" />
+</p>
+!! end
+
+!! test
+Failing to transform badly formed HTML into correct XHTML
+!! input
+<br clear=left>
+<br clear=right>
+<br clear=all>
+!! result
+<p><br clear="left" />
+<br clear="right" />
+<br clear="all" />
+</p>
+!!end
+
+!! test
+Horizontal ruler (should it add that extra space?)
+!! input
+<hr>
+<hr >
+foo <hr
+> bar
+!! result
+<hr />
+<hr />
+foo <hr /> bar
+
+!! end
+
+###
+### Block-level elements
+###
+!! test
+Common list
+!! input
+*Common list
+* item 2
+*item 3
+!! result
+<ul><li>Common list
+</li><li> item 2
+</li><li>item 3
+</li></ul>
+
+!! end
+
+!! test
+Numbered list
+!! input
+#Numbered list
+#item 2
+# item 3
+!! result
+<ol><li>Numbered list
+</li><li>item 2
+</li><li> item 3
+</li></ol>
+
+!! end
+
+!! test
+Mixed list
+!! input
+*Mixed list
+*# with numbers
+** and bullets
+*# and numbers
+*bullets again
+**bullet level 2
+***bullet level 3
+***#Number on level 4
+**bullet level 2
+**#Number on level 3
+**#Number on level 3
+*#number level 2
+*Level 1
+!! result
+<ul><li>Mixed list
+<ol><li> with numbers
+</li></ol>
+<ul><li> and bullets
+</li></ul>
+<ol><li> and numbers
+</li></ol>
+</li><li>bullets again
+<ul><li>bullet level 2
+<ul><li>bullet level 3
+<ol><li>Number on level 4
+</li></ol>
+</li></ul>
+</li><li>bullet level 2
+<ol><li>Number on level 3
+</li><li>Number on level 3
+</li></ol>
+</li></ul>
+<ol><li>number level 2
+</li></ol>
+</li><li>Level 1
+</li></ul>
+
+!! end
+
+!! test
+List items are not parsed correctly following a <pre> block (bug 785)
+!! input
+* <pre>foo</pre>
+* <pre>bar</pre>
+* zar
+!! result
+<ul><li> <pre>foo</pre>
+</li><li> <pre>bar</pre>
+</li><li> zar
+</li></ul>
+
+!! end
+
+###
+### Magic Words
+###
+
+!! test
+Magic Word: {{CURRENTDAY}}
+!! input
+{{CURRENTDAY}}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTDAY2}}
+!! input
+{{CURRENTDAY2}}
+!! result
+<p>01
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTDAYNAME}}
+!! input
+{{CURRENTDAYNAME}}
+!! result
+<p>Thursday
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTDOW}}
+!! input
+{{CURRENTDOW}}
+!! result
+<p>4
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTH}}
+!! input
+{{CURRENTMONTH}}
+!! result
+<p>01
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTHABBREV}}
+!! input
+{{CURRENTMONTHABBREV}}
+!! result
+<p>Jan
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTHNAME}}
+!! input
+{{CURRENTMONTHNAME}}
+!! result
+<p>January
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTMONTHNAMEGEN}}
+!! input
+{{CURRENTMONTHNAMEGEN}}
+!! result
+<p>January
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTTIME}}
+!! input
+{{CURRENTTIME}}
+!! result
+<p>00:02
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTWEEK}} (@bug 4594)
+!! input
+{{CURRENTWEEK}}
+!! result
+<p>1
+</p>
+!! end
+
+!! test
+Magic Word: {{CURRENTYEAR}}
+!! input
+{{CURRENTYEAR}}
+!! result
+<p>1970
+</p>
+!! end
+
+!! test
+Magic Word: {{FULLPAGENAME}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! input
+{{FULLPAGENAME}}
+!! result
+<p>User:Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{FULLPAGENAMEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! input
+{{FULLPAGENAMEE}}
+!! result
+<p>User:%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{NAMESPACE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! input
+{{NAMESPACE}}
+!! result
+<p>User
+</p>
+!! end
+
+!! test
+Magic Word: {{NAMESPACEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! input
+{{NAMESPACEE}}
+!! result
+<p>User
+</p>
+!! end
+
+!! test
+Magic Word: {{NUMBEROFFILES}}
+!! input
+{{NUMBEROFFILES}}
+!! result
+<p>2
+</p>
+!! end
+
+!! test
+Magic Word: {{PAGENAME}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! input
+{{PAGENAME}}
+!! result
+<p>Ævar Arnfjörð Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{PAGENAMEE}}
+!! options
+title=[[User:Ævar Arnfjörð Bjarmason]]
+!! input
+{{PAGENAMEE}}
+!! result
+<p>%C3%86var_Arnfj%C3%B6r%C3%B0_Bjarmason
+</p>
+!! end
+
+!! test
+Magic Word: {{REVISIONID}}
+!! input
+{{REVISIONID}}
+!! result
+<p>1337
+</p>
+!! end
+
+!! test
+Magic Word: {{SCRIPTPATH}}
+!! input
+{{SCRIPTPATH}}
+!! result
+<p>/
+</p>
+!! end
+
+!! test
+Magic Word: {{SERVER}}
+!! input
+{{SERVER}}
+!! result
+<p><a href="http://Britney-Spears" class="external free" rel="nofollow">http://Britney-Spears</a>
+</p>
+!! end
+
+!! test
+Magic Word: {{SERVERNAME}}
+!! input
+{{SERVERNAME}}
+!! result
+<p>Britney-Spears
+</p>
+!! end
+
+!! test
+Magic Word: {{SITENAME}}
+!! input
+{{SITENAME}}
+!! result
+<p>MediaWiki
+</p>
+!! end
+
+!! test
+Namespace 1 {{ns:1}}
+!! input
+{{ns:1}}
+!! result
+<p>Talk
+</p>
+!! end
+
+!! test
+Namespace 1 {{ns:01}}
+!! input
+{{ns:01}}
+!! result
+<p>Talk
+</p>
+!! end
+
+!! test
+Namespace 0 {{ns:0}} (bug 4783)
+!! input
+{{ns:0}}
+!! result
+
+!! end
+
+!! test
+Namespace 0 {{ns:00}} (bug 4783)
+!! input
+{{ns:00}}
+!! result
+
+!! end
+
+!! test
+Namespace -1 {{ns:-1}}
+!! input
+{{ns:-1}}
+!! result
+<p>Special
+</p>
+!! end
+
+!! test
+Namespace User {{ns:User}}
+!! input
+{{ns:User}}
+!! result
+<p>User
+</p>
+!! end
+
+!! test
+Namespace User talk {{ns:User_talk}}
+!! input
+{{ns:User_talk}}
+!! result
+<p>User talk
+</p>
+!! end
+
+!! test
+Namespace User talk {{ns:uSeR tAlK}}
+!! input
+{{ns:uSeR tAlK}}
+!! result
+<p>User talk
+</p>
+!! end
+
+!! test
+Namespace File {{ns:File}}
+!! input
+{{ns:File}}
+!! result
+<p>File
+</p>
+!! end
+
+!! test
+Namespace File {{ns:Image}}
+!! input
+{{ns:Image}}
+!! result
+<p>File
+</p>
+!! end
+
+!! test
+Namespace (lang=de) Benutzer {{ns:User}}
+!! options
+language=de
+!! input
+{{ns:User}}
+!! result
+<p>Benutzer
+</p>
+!! end
+
+!! test
+Namespace (lang=de) Benutzer Diskussion {{ns:3}}
+!! options
+language=de
+!! input
+{{ns:3}}
+!! result
+<p>Benutzer Diskussion
+</p>
+!! end
+
+
+!! test
+Urlencode
+!! input
+{{urlencode:hi world?!}}
+{{urlencode:hi world?!|WIKI}}
+{{urlencode:hi world?!|PATH}}
+{{urlencode:hi world?!|QUERY}}
+!! result
+<p>hi+world%3F%21
+hi_world%3F!
+hi%20world%3F%21
+hi+world%3F%21
+</p>
+!! end
+
+###
+### Magic links
+###
+!! test
+Magic links: internal link to RFC (bug 479)
+!! input
+[[RFC 123]]
+!! result
+<p><a href="/index.php?title=RFC_123&action=edit&redlink=1" class="new" title="RFC 123 (page does not exist)">RFC 123</a>
+</p>
+!! end
+
+!! test
+Magic links: RFC (bug 479)
+!! input
+RFC 822
+!! result
+<p><a href="http://tools.ietf.org/html/rfc822" class="external mw-magiclink-rfc">RFC 822</a>
+</p>
+!! end
+
+!! test
+Magic links: ISBN (bug 1937)
+!! input
+ISBN 0-306-40615-2
+!! result
+<p><a href="/wiki/Special:BookSources/0306406152" class="internal mw-magiclink-isbn">ISBN 0-306-40615-2</a>
+</p>
+!! end
+
+!! test
+Magic links: PMID incorrectly converts space to underscore
+!! input
+PMID 1234
+!! result
+<p><a href="http://www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" class="external mw-magiclink-pmid">PMID 1234</a>
+</p>
+!! end
+
+###
+### Templates
+####
+
+!! test
+Nonexistent template
+!! input
+{{thistemplatedoesnotexist}}
+!! result
+<p><a href="/index.php?title=Template:Thistemplatedoesnotexist&action=edit&redlink=1" class="new" title="Template:Thistemplatedoesnotexist (page does not exist)">Template:Thistemplatedoesnotexist</a>
+</p>
+!! end
+
+!! article
+Template:test
+!! text
+This is a test template
+!! endarticle
+
+!! test
+Simple template
+!! input
+{{test}}
+!! result
+<p>This is a test template
+</p>
+!! end
+
+!! test
+Template with explicit namespace
+!! input
+{{Template:test}}
+!! result
+<p>This is a test template
+</p>
+!! end
+
+
+!! article
+Template:paramtest
+!! text
+This is a test template with parameter {{{param}}}
+!! endarticle
+
+!! test
+Template parameter
+!! input
+{{paramtest|param=foo}}
+!! result
+<p>This is a test template with parameter foo
+</p>
+!! end
+
+!! article
+Template:paramtestnum
+!! text
+[[{{{1}}}|{{{2}}}]]
+!! endarticle
+
+!! test
+Template unnamed parameter
+!! input
+{{paramtestnum|Main Page|the main page}}
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">the main page</a>
+</p>
+!! end
+
+!! article
+Template:templatesimple
+!! text
+(test)
+!! endarticle
+
+!! article
+Template:templateredirect
+!! text
+#redirect [[Template:templatesimple]]
+!! endarticle
+
+!! article
+Template:templateasargtestnum
+!! text
+{{{{{1}}}}}
+!! endarticle
+
+!! article
+Template:templateasargtest
+!! text
+{{template{{{templ}}}}}
+!! endarticle
+
+!! article
+Template:templateasargtest2
+!! text
+{{{{{templ}}}}}
+!! endarticle
+
+!! test
+Template with template name as unnamed argument
+!! input
+{{templateasargtestnum|templatesimple}}
+!! result
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with template name as argument
+!! input
+{{templateasargtest|templ=simple}}
+!! result
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with template name as argument (2)
+!! input
+{{templateasargtest2|templ=templatesimple}}
+!! result
+<p>(test)
+</p>
+!! end
+
+!! article
+Template:templateasargtestdefault
+!! text
+{{{{{templ|templatesimple}}}}}
+!! endarticle
+
+!! article
+Template:templa
+!! text
+'''templ'''
+!! endarticle
+
+!! test
+Template with default value
+!! input
+{{templateasargtestdefault}}
+!! result
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with default value (value set)
+!! input
+{{templateasargtestdefault|templ=templa}}
+!! result
+<p><b>templ</b>
+</p>
+!! end
+
+!! test
+Template redirect
+!! input
+{{templateredirect}}
+!! result
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with argument in separate line
+!! input
+{{ templateasargtest |
+ templ = simple }}
+!! result
+<p>(test)
+</p>
+!! end
+
+!! test
+Template with complex template as argument
+!! input
+{{paramtest|
+ param ={{ templateasargtest |
+ templ = simple }}}}
+!! result
+<p>This is a test template with parameter (test)
+</p>
+!! end
+
+!! test
+Template with thumb image (with link in description)
+!! input
+{{paramtest|
+ param =[[Image:noimage.png|thumb|[[no link|link]] [[no link|caption]]]]}}
+!! result
+This is a test template with parameter <div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/index.php?title=Special:Upload&wpDestFile=Noimage.png" class="new" title="File:Noimage.png">File:Noimage.png</a> <div class="thumbcaption"><a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">link</a> <a href="/index.php?title=No_link&action=edit&redlink=1" class="new" title="No link (page does not exist)">caption</a></div></div></div>
+
+!! end
+
+!! article
+Template:complextemplate
+!! text
+{{{1}}} {{paramtest|
+ param ={{{param}}}}}
+!! endarticle
+
+!! test
+Template with complex arguments
+!! input
+{{complextemplate|
+ param ={{ templateasargtest |
+ templ = simple }}|[[Template:complextemplate|link]]}}
+!! result
+<p><a href="/wiki/Template:Complextemplate" title="Template:Complextemplate">link</a> This is a test template with parameter (test)
+</p>
+!! end
+
+!! test
+BUG 553: link with two variables in a piped link
+!! input
+{|
+|[[{{{1}}}|{{{2}}}]]
+|}
+!! result
+<table>
+<tr>
+<td>[[{{{1}}}|{{{2}}}]]
+</td></tr></table>
+
+!! end
+
+!! test
+Magic variable as template parameter
+!! input
+{{paramtest|param={{SITENAME}}}}
+!! result
+<p>This is a test template with parameter MediaWiki
+</p>
+!! end
+
+!! article
+Template:linktest
+!! text
+[[{{{param}}}|link]]
+!! endarticle
+
+!! test
+Template parameter as link source
+!! input
+{{linktest|param=Main Page}}
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">link</a>
+</p>
+!! end
+
+
+!!article
+Template:paramtest2
+!! text
+including another template, {{paramtest|param={{{arg}}}}}
+!! endarticle
+
+!! test
+Template passing argument to another template
+!! input
+{{paramtest2|arg='hmm'}}
+!! result
+<p>including another template, This is a test template with parameter 'hmm'
+</p>
+!! end
+
+!! article
+Template:Linktest2
+!! text
+Main Page
+!! endarticle
+
+!! test
+Template as link source
+!! input
+[[{{linktest2}}]]
+!! result
+<p><a href="/wiki/Main_Page">Main Page</a>
+</p>
+!! end
+
+
+!! article
+Template:loop1
+!! text
+{{loop2}}
+!! endarticle
+
+!! article
+Template:loop2
+!! text
+{{loop1}}
+!! endarticle
+
+!! test
+Template infinite loop
+!! input
+{{loop1}}
+!! result
+<p><span class="error">Template loop detected: <a href="/wiki/Template:Loop1">Template:Loop1</a></span>
+</p>
+!! end
+
+!! test
+Template from main namespace
+!! input
+{{:Main Page}}
+!! result
+<p>blah blah
+</p>
+!! end
+
+!! article
+Template:table
+!! text
+{|
+| 1 || 2
+|-
+| 3 || 4
+|}
+!! endarticle
+
+!! test
+BUG 529: Template with table, not included at beginning of line
+!! input
+foo {{table}}
+!! result
+<p>foo
+</p>
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+
+!! end
+
+!! test
+BUG 523: Template shouldn't eat newline (or add an extra one before table)
+!! input
+foo
+{{table}}
+!! result
+<p>foo
+</p>
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+
+!! end
+
+!! test
+BUG 41: Template parameters shown as broken links
+!! input
+{{{parameter}}}
+!! result
+<p>{{{parameter}}}
+</p>
+!! end
+
+
+!! article
+Template:MSGNW test
+!! text
+''None'' of '''this''' should be
+* interpreted
+ but rather passed unmodified
+{{test}}
+!! endarticle
+
+# hmm, fix this or just deprecate msgnw and document its behavior?
+!! test
+msgnw keyword
+!! options
+disabled
+!! input
+{{msgnw:MSGNW test}}
+!! result
+<p>''None'' of '''this''' should be
+* interpreted
+ but rather passed unmodified
+{{test}}
+</p>
+!! end
+
+!! test
+int keyword
+!! input
+{{int:youhavenewmessages|lots of money|not!}}
+!! result
+<p>You have lots of money (not!).
+</p>
+!! end
+
+!! article
+Template:Includes
+!! text
+Foo<noinclude>zar</noinclude><includeonly>bar</includeonly>
+!! endarticle
+
+!! test
+<includeonly> and <noinclude> being included
+!! input
+{{Includes}}
+!! result
+<p>Foobar
+</p>
+!! end
+
+!! article
+Template:Includes2
+!! text
+<onlyinclude>Foo</onlyinclude>bar
+!! endarticle
+
+!! test
+<onlyinclude> being included
+!! input
+{{Includes2}}
+!! result
+<p>Foo
+</p>
+!! end
+
+
+!! article
+Template:Includes3
+!! text
+<onlyinclude>Foo</onlyinclude>bar<includeonly>zar</includeonly>
+!! endarticle
+
+!! test
+<onlyinclude> and <includeonly> being included
+!! input
+{{Includes3}}
+!! result
+<p>Foo
+</p>
+!! end
+
+!! test
+<includeonly> and <noinclude> on a page
+!! input
+Foo<noinclude>zar</noinclude><includeonly>bar</includeonly>
+!! result
+<p>Foozar
+</p>
+!! end
+
+!! test
+<onlyinclude> on a page
+!! input
+<onlyinclude>Foo</onlyinclude>bar
+!! result
+<p>Foobar
+</p>
+!! end
+
+!! article
+Template:Includeonly section
+!! text
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section T-1==
+!!endarticle
+
+!! test
+Bug 6563: Edit link generation for section shown by <includeonly>
+!! input
+{{includeonly section}}
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Template:Includeonly_section&action=edit&section=T-1" title="Template:Includeonly section">edit</a>]</span> <span class="mw-headline" id="Includeonly_section">Includeonly section</span></h2>
+<h2><span class="editsection">[<a href="/index.php?title=Template:Includeonly_section&action=edit&section=T-2" title="Template:Includeonly section">edit</a>]</span> <span class="mw-headline" id="Section_T-1">Section T-1</span></h2>
+
+!! end
+
+# Uses same input as the contents of [[Template:Includeonly section]]
+!! test
+Bug 6563: Section extraction for section shown by <includeonly>
+!! options
+section=T-2
+!! input
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section T-2==
+!! result
+==Section T-2==
+!! end
+
+!! test
+Bug 6563: Edit link generation for section suppressed by <includeonly>
+!! input
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section 1==
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section 1">edit</a>]</span> <span class="mw-headline" id="Section_1">Section 1</span></h2>
+
+!! end
+
+!! test
+Bug 6563: Section extraction for section suppressed by <includeonly>
+!! options
+section=1
+!! input
+<includeonly>
+==Includeonly section==
+</includeonly>
+==Section 1==
+!! result
+==Section 1==
+!! end
+
+###
+### Pre-save transform tests
+###
+!! test
+pre-save transform: subst:
+!! options
+PST
+!! input
+{{subst:test}}
+!! result
+This is a test template
+!! end
+
+!! test
+pre-save transform: normal template
+!! options
+PST
+!! input
+{{test}}
+!! result
+{{test}}
+!! end
+
+!! test
+pre-save transform: nonexistent template
+!! options
+PST
+!! input
+{{thistemplatedoesnotexist}}
+!! result
+{{thistemplatedoesnotexist}}
+!! end
+
+
+!! test
+pre-save transform: subst magic variables
+!! options
+PST
+!! input
+{{subst:SITENAME}}
+!! result
+MediaWiki
+!! end
+
+# This is bug 89, which I fixed. -- wtm
+!! test
+pre-save transform: subst: templates with parameters
+!! options
+pst
+!! input
+{{subst:paramtest|param="something else"}}
+!! result
+This is a test template with parameter "something else"
+!! end
+
+!! article
+Template:nowikitest
+!! text
+<nowiki>'''not wiki'''</nowiki>
+!! endarticle
+
+!! test
+pre-save transform: nowiki in subst (bug 1188)
+!! options
+pst
+!! input
+{{subst:nowikitest}}
+!! result
+<nowiki>'''not wiki'''</nowiki>
+!! end
+
+
+!! article
+Template:commenttest
+!! text
+This template has <!-- a comment --> in it.
+!! endarticle
+
+!! test
+pre-save transform: comment in subst (bug 1936)
+!! options
+pst
+!! input
+{{subst:commenttest}}
+!! result
+This template has <!-- a comment --> in it.
+!! end
+
+!! test
+pre-save transform: unclosed tag
+!! options
+pst noxml
+!! input
+<nowiki>'''not wiki'''
+!! result
+<nowiki>'''not wiki'''
+!! end
+
+!! test
+pre-save transform: mixed tag case
+!! options
+pst noxml
+!! input
+<NOwiki>'''not wiki'''</noWIKI>
+!! result
+<NOwiki>'''not wiki'''</noWIKI>
+!! end
+
+!! test
+pre-save transform: unclosed comment in <nowiki>
+!! options
+pst noxml
+!! input
+wiki<nowiki>nowiki<!--nowiki</nowiki>wiki
+!! result
+wiki<nowiki>nowiki<!--nowiki</nowiki>wiki
+!!end
+
+!! article
+Template:dangerous
+!!text
+<span onmouseover="alert('crap')">Oh no</span>
+!!endarticle
+
+!!test
+(confirming safety of fix for subst bug 1936)
+!! input
+{{Template:dangerous}}
+!! result
+<p><span>Oh no</span>
+</p>
+!! end
+
+!! test
+pre-save transform: comment containing gallery (bug 5024)
+!! options
+pst
+!! input
+<!-- <gallery>data</gallery> -->
+!!result
+<!-- <gallery>data</gallery> -->
+!!end
+
+!! test
+pre-save transform: comment containing extension
+!! options
+pst
+!! input
+<!-- <tag>data</tag> -->
+!!result
+<!-- <tag>data</tag> -->
+!!end
+
+!! test
+pre-save transform: comment containing nowiki
+!! options
+pst
+!! input
+<!-- <nowiki>data</nowiki> -->
+!!result
+<!-- <nowiki>data</nowiki> -->
+!!end
+
+!! test
+pre-save transform: comment containing math
+!! options
+pst
+!! input
+<!-- <math>data</math> -->
+!!result
+<!-- <math>data</math> -->
+!!end
+
+!! test
+pre-save transform: <noinclude> in subst (bug 3298)
+!! options
+pst
+!! input
+{{subst:Includes}}
+!! result
+Foobar
+!! end
+
+!! test
+pre-save transform: <onlyinclude> in subst (bug 3298)
+!! options
+pst
+!! input
+{{subst:Includes2}}
+!! result
+Foo
+!! end
+
+!! article
+Template:SubstTest
+!!text
+{{<includeonly>subst:</includeonly>Includes}}
+!! endarticle
+
+!! article
+Template:SafeSubstTest
+!! text
+{{<includeonly>safesubst:</includeonly>Includes}}
+!! endarticle
+
+!! test
+bug 22297: safesubst: works during PST
+!! options
+pst
+!! input
+{{subst:SafeSubstTest}}{{safesubst:SubstTest}}
+!! result
+FoobarFoobar
+!! end
+
+!! test
+bug 22297: safesubst: works during normal parse
+!! input
+{{SafeSubstTest}}
+!! result
+<p>Foobar
+</p>
+!! end
+
+!! test:
+subst: does not work during normal parse
+!! input
+{{SubstTest}}
+!! result
+<p>{{subst:Includes}}
+</p>
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick")
+!! options
+pst
+!! input
+[[Article (context)|]]
+[[Bar:Article|]]
+[[:Bar:Article|]]
+[[Bar:Article (context)|]]
+[[:Bar:Article (context)|]]
+[[|Article]]
+[[|Article (context)]]
+[[Bar:X (Y) Z|]]
+[[:Bar:X (Y) Z|]]
+!! result
+[[Article (context)|Article]]
+[[Bar:Article|Article]]
+[[:Bar:Article|Article]]
+[[Bar:Article (context)|Article]]
+[[:Bar:Article (context)|Article]]
+[[Article]]
+[[Article (context)]]
+[[Bar:X (Y) Z|X (Y) Z]]
+[[:Bar:X (Y) Z|X (Y) Z]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with interwiki prefix
+!! options
+pst
+!! input
+[[interwiki:Article|]]
+[[:interwiki:Article|]]
+[[interwiki:Bar:Article|]]
+[[:interwiki:Bar:Article|]]
+!! result
+[[interwiki:Article|Article]]
+[[:interwiki:Article|Article]]
+[[interwiki:Bar:Article|Bar:Article]]
+[[:interwiki:Bar:Article|Bar:Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with parens in title
+!! options
+pst title=[[Somearticle (context)]]
+!! input
+[[|Article]]
+!! result
+[[Article (context)|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with comma in title
+!! options
+pst title=[[Someplace, Somewhere]]
+!! input
+[[|Otherplace]]
+[[Otherplace, Elsewhere|]]
+[[Otherplace, Elsewhere, Anywhere|]]
+!! result
+[[Otherplace, Somewhere|Otherplace]]
+[[Otherplace, Elsewhere|Otherplace]]
+[[Otherplace, Elsewhere, Anywhere|Otherplace]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with parens and comma
+!! options
+pst title=[[Someplace (IGNORED), Somewhere]]
+!! input
+[[|Otherplace]]
+[[Otherplace (place), Elsewhere|]]
+!! result
+[[Otherplace, Somewhere|Otherplace]]
+[[Otherplace (place), Elsewhere|Otherplace]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with comma and parens
+!! options
+pst title=[[Who, me? (context)]]
+!! input
+[[|Yes, you.]]
+[[Me, Myself, and I (1937 song)|]]
+!! result
+[[Yes, you. (context)|Yes, you.]]
+[[Me, Myself, and I (1937 song)|Me, Myself, and I]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace
+!! options
+pst title=[[Ns:Somearticle]]
+!! input
+[[|Article]]
+!! result
+[[Ns:Article|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace and parens
+!! options
+pst title=[[Ns:Somearticle (context)]]
+!! input
+[[|Article]]
+!! result
+[[Ns:Article (context)|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace and comma
+!! options
+pst title=[[Ns:Somearticle, Context, Whatever]]
+!! input
+[[|Article]]
+!! result
+[[Ns:Article, Context, Whatever|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace, comma and parens
+!! options
+pst title=[[Ns:Somearticle, Context (context)]]
+!! input
+[[|Article]]
+!! result
+[[Ns:Article (context)|Article]]
+!! end
+
+!! test
+pre-save transform: context links ("pipe trick") with namespace, parens and comma
+!! options
+pst title=[[Ns:Somearticle (IGNORED), Context]]
+!! input
+[[|Article]]
+!! result
+[[Ns:Article, Context|Article]]
+!! end
+
+
+###
+### Message transform tests
+###
+!! test
+message transform: magic variables
+!! options
+msg
+!! input
+{{SITENAME}}
+!! result
+MediaWiki
+!! end
+
+!! test
+message transform: should not transform wiki markup
+!! options
+msg
+!! input
+''test''
+!! result
+''test''
+!! end
+
+!! test
+message transform: <noinclude> in transcluded template (bug 4926)
+!! options
+msg
+!! input
+{{Includes}}
+!! result
+Foobar
+!! end
+
+!! test
+message transform: <onlyinclude> in transcluded template (bug 4926)
+!! options
+msg
+!! input
+{{Includes2}}
+!! result
+Foo
+!! end
+
+!! test
+{{#special:}} page name, known
+!! options
+msg
+!! input
+{{#special:Recentchanges}}
+!! result
+Special:RecentChanges
+!! end
+
+!! test
+{{#special:}} page name with subpage, known
+!! options
+msg
+!! input
+{{#special:Recentchanges/param}}
+!! result
+Special:RecentChanges/param
+!! end
+
+!! test
+{{#special:}} page name, unknown
+!! options
+msg
+!! input
+{{#special:foobarnonexistent}}
+!! result
+No such special page
+!! end
+
+###
+### Images
+###
+!! test
+Simple image
+!! input
+[[Image:foobar.jpg]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Right-aligned image
+!! input
+[[Image:foobar.jpg|right]]
+!! result
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! end
+
+!! test
+Simple image (using File: namespace, now canonical)
+!! input
+[[File:foobar.jpg]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with caption
+!! input
+[[Image:foobar.jpg|right|Caption text]]
+!! result
+<div class="floatright"><a href="/wiki/File:Foobar.jpg" class="image" title="Caption text"><img alt="Caption text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!! end
+
+!! test
+Image with link parameter, wiki target
+!! input
+[[Image:foobar.jpg|link=Target page]]
+!! result
+<p><a href="/wiki/Target_page" title="Target page"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with link parameter, URL target
+!! input
+[[Image:foobar.jpg|link=http://example.com/]]
+!! result
+<p><a href="http://example.com/"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with empty link parameter
+!! input
+[[Image:foobar.jpg|link=]]
+!! result
+<p><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" />
+</p>
+!! end
+
+!! test
+Image with link parameter (wiki target) and unnamed parameter
+!! input
+[[Image:foobar.jpg|link=Target page|Title]]
+!! result
+<p><a href="/wiki/Target_page" title="Title"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with link parameter (URL target) and unnamed parameter
+!! input
+[[Image:foobar.jpg|link=http://example.com/|Title]]
+!! result
+<p><a href="http://example.com/" title="Title"><img alt="Title" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Thumbnail image with link parameter
+!! input
+[[Image:foobar.jpg|thumb|link=http://example.com/|Title]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="http://example.com/"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>Title</div></div></div>
+
+!! end
+
+!! test
+Image with frame and link
+!! input
+[[Image:Foobar.jpg|frame|left|This is a test image [[Main Page]]]]
+!! result
+<div class="thumb tleft"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption">This is a test image <a href="/wiki/Main_Page">Main Page</a></div></div></div>
+
+!! end
+
+!! test
+Image with frame and link and explicit alt
+!! input
+[[Image:Foobar.jpg|frame|left|This is a test image [[Main Page]]|alt=Altitude]]
+!! result
+<div class="thumb tleft"><div class="thumbinner" style="width:1943px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Altitude" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" class="thumbimage" /></a> <div class="thumbcaption">This is a test image <a href="/wiki/Main_Page">Main Page</a></div></div></div>
+
+!! end
+
+!! test
+Image with wiki markup in implicit alt
+!! input
+[[Image:Foobar.jpg|testing '''bold''' in alt]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="testing bold in alt"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Image with wiki markup in explicit alt
+!! input
+[[Image:Foobar.jpg|alt=testing '''bold''' in alt]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="testing bold in alt" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Link to image page- image page normally doesn't exists, hence edit link
+Add test with existing image page
+#<p><a href="/wiki/File:Test" title="Image:Test">Image:test</a>
+!! input
+[[:Image:test]]
+!! result
+<p><a href="/index.php?title=File:Test&action=edit&redlink=1" class="new" title="File:Test (page does not exist)">Image:test</a>
+</p>
+!! end
+
+!! test
+bug 18784 Link to non-existent image page with caption should use caption as link text
+!! input
+[[:Image:test|caption]]
+!! result
+<p><a href="/index.php?title=File:Test&action=edit&redlink=1" class="new" title="File:Test (page does not exist)">caption</a>
+</p>
+!! end
+
+!! test
+Frameless image caption with a free URL
+!! input
+[[Image:foobar.jpg|http://example.com]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="http://example.com"><img alt="http://example.com" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Thumbnail image caption with a free URL
+!! input
+[[Image:foobar.jpg|thumb|http://example.com]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="http://example.com" class="external free" rel="nofollow">http://example.com</a></div></div></div>
+
+!! end
+
+!! test
+Thumbnail image caption with a free URL and explicit alt
+!! input
+[[Image:foobar.jpg|thumb|http://example.com|alt=Alteration]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Alteration" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="http://example.com" class="external free" rel="nofollow">http://example.com</a></div></div></div>
+
+!! end
+
+!! test
+BUG 1887: A ISBN with a thumbnail
+!! input
+[[Image:foobar.jpg|thumb|ISBN 1235467890]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><a href="/wiki/Special:BookSources/1235467890" class="internal mw-magiclink-isbn">ISBN 1235467890</a></div></div></div>
+
+!! end
+
+!! test
+BUG 1887: A RFC with a thumbnail
+!! input
+[[Image:foobar.jpg|thumb|This is RFC 12354]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This is <a href="http://tools.ietf.org/html/rfc12354" class="external mw-magiclink-rfc">RFC 12354</a></div></div></div>
+
+!! end
+
+!! test
+BUG 1887: A mailto link with a thumbnail
+!! input
+[[Image:foobar.jpg|thumb|Please mailto:nobody@example.com]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>Please <a href="mailto:nobody@example.com" class="external free" rel="nofollow">mailto:nobody@example.com</a></div></div></div>
+
+!! end
+
+!! test
+BUG 1887: A <math> with a thumbnail- we don't render math in the parsertests by default,
+so math is not stripped and turns up as escaped <math> tags.
+!! input
+[[Image:foobar.jpg|thumb|<math>2+2</math>]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><math>2+2</math></div></div></div>
+
+!! end
+
+!! test
+BUG 1887, part 2: A <math> with a thumbnail- math enabled
+!! options
+math
+!! input
+[[Image:foobar.jpg|thumb|<math>2+2</math>]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div><span class="texhtml">2 + 2</span></div></div></div>
+
+!! end
+
+# Pending resolution to bug 368
+!! test
+BUG 648: Frameless image caption with a link
+!! input
+[[Image:foobar.jpg|text with a [[link]] in it]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+BUG 648: Frameless image caption with a link (suffix)
+!! input
+[[Image:foobar.jpg|text with a [[link]]foo in it]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a linkfoo in it"><img alt="text with a linkfoo in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+BUG 648: Frameless image caption with an interwiki link
+!! input
+[[Image:foobar.jpg|text with a [[MeatBall:Link]] in it]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a MeatBall:Link in it"><img alt="text with a MeatBall:Link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+BUG 648: Frameless image caption with a piped interwiki link
+!! input
+[[Image:foobar.jpg|text with a [[MeatBall:Link|link]] in it]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="text with a link in it"><img alt="text with a link in it" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Escape HTML special chars in image alt text
+!! input
+[[Image:foobar.jpg|& < > "]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="& < > ""><img alt="& < > "" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+BUG 499: Alt text should have Ӓ, not &1234;
+!! input
+[[Image:foobar.jpg|♀]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="♀"><img alt="♀" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!! end
+
+!! test
+Broken image caption with link
+!! input
+[[Image:Foobar.jpg|thumb|This is a broken caption. But [[Main Page|this]] is just an ordinary link.
+!! result
+<p>[[Image:Foobar.jpg|thumb|This is a broken caption. But <a href="/wiki/Main_Page" title="Main Page">this</a> is just an ordinary link.
+</p>
+!! end
+
+!! test
+Image caption containing another image
+!! input
+[[Image:Foobar.jpg|thumb|This is a caption with another [[Image:icon.png|image]] inside it!]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This is a caption with another <a href="/index.php?title=Special:Upload&wpDestFile=Icon.png" class="new" title="File:Icon.png">image</a> inside it!</div></div></div>
+
+!! end
+
+!! test
+Image caption containing a newline
+!! input
+[[Image:Foobar.jpg|This
+*is some text]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image" title="This *is some text"><img alt="This *is some text" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!!end
+
+
+!! test
+Bug 3090: External links other than http: in image captions
+!! input
+[[Image:Foobar.jpg|thumb|200px|This caption has [irc://example.net irc] and [https://example.com Secure] ext links in it.]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:202px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="200" height="23" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>This caption has <a href="irc://example.net" class="external text" rel="nofollow">irc</a> and <a href="https://example.com" class="external text" rel="nofollow">Secure</a> ext links in it.</div></div></div>
+
+!! end
+
+!! article
+File:Barfoo.jpg
+!! text
+#REDIRECT [[File:Barfoo.jpg]]
+!! endarticle
+
+!! test
+Redirected image
+!! input
+[[Image:Barfoo.jpg]]
+!! result
+<p><a href="/wiki/File:Barfoo.jpg">File:Barfoo.jpg</a>
+</p>
+!! end
+
+!! test
+Missing image with uploads disabled
+!! options
+wgEnableUploads=0
+!! input
+[[Image:Foobaz.jpg]]
+!! result
+<p><a href="/wiki/File:Foobaz.jpg">File:Foobaz.jpg</a>
+</p>
+!! end
+
+
+###
+### Subpages
+###
+!! article
+Subpage test/subpage
+!! text
+foo
+!! endarticle
+
+!! test
+Subpage link
+!! options
+subpage title=[[Subpage test]]
+!! input
+[[/subpage]]
+!! result
+<p><a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">/subpage</a>
+</p>
+!! end
+
+!! test
+Subpage noslash link
+!! options
+subpage title=[[Subpage test]]
+!!input
+[[/subpage/]]
+!! result
+<p><a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">subpage</a>
+</p>
+!! end
+
+!! test
+Disabled subpages
+!! input
+[[/subpage]]
+!! result
+<p><a href="/index.php?title=/subpage&action=edit&redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a>
+</p>
+!! end
+
+!! test
+BUG 561: {{/Subpage}}
+!! options
+subpage title=[[Page]]
+!! input
+{{/Subpage}}
+!! result
+<p><a href="/index.php?title=Page/Subpage&action=edit&redlink=1" class="new" title="Page/Subpage (page does not exist)">Page/Subpage</a>
+</p>
+!! end
+
+###
+### Categories
+###
+!! article
+Category:MediaWiki User's Guide
+!! text
+blah
+!! endarticle
+
+!! test
+Link to category
+!! input
+[[:Category:MediaWiki User's Guide]]
+!! result
+<p><a href="/wiki/Category:MediaWiki_User%27s_Guide">Category:MediaWiki User's Guide</a>
+</p>
+!! end
+
+!! test
+Simple category
+!! options
+cat
+!! input
+[[Category:MediaWiki User's Guide]]
+!! result
+<a href="/wiki/Category:MediaWiki_User%27s_Guide" title="Category:MediaWiki User's Guide">MediaWiki User's Guide</a>
+!! end
+
+!! test
+PAGESINCATEGORY invalid title fatal (r33546 fix)
+!! input
+{{PAGESINCATEGORY:<bogus>}}
+!! result
+<p>0
+</p>
+!! end
+
+###
+### Inter-language links
+###
+!! test
+Inter-language links
+!! options
+ill
+!! input
+[[es:Alimento]]
+[[fr:Nourriture]]
+[[zh:食品]]
+!! result
+es:Alimento fr:Nourriture zh:食品
+!! end
+
+###
+### Sections
+###
+!! test
+Basic section headings
+!! input
+== Headline 1 ==
+Some text
+
+==Headline 2==
+More
+===Smaller headline===
+Blah blah
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Headline 1">edit</a>]</span> <span class="mw-headline" id="Headline_1"> Headline 1 </span></h2>
+<p>Some text
+</p>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Headline 2">edit</a>]</span> <span class="mw-headline" id="Headline_2">Headline 2</span></h2>
+<p>More
+</p>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Smaller headline">edit</a>]</span> <span class="mw-headline" id="Smaller_headline">Smaller headline</span></h3>
+<p>Blah blah
+</p>
+!! end
+
+!! test
+Section headings with TOC
+!! input
+== Headline 1 ==
+=== Subheadline 1 ===
+===== Skipping a level =====
+====== Skipping a level ======
+
+== Headline 2 ==
+Some text
+===Another headline===
+!! result
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Headline_1"><span class="tocnumber">1</span> <span class="toctext">Headline 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#Subheadline_1"><span class="tocnumber">1.1</span> <span class="toctext">Subheadline 1</span></a>
+<ul>
+<li class="toclevel-3 tocsection-3"><a href="#Skipping_a_level"><span class="tocnumber">1.1.1</span> <span class="toctext">Skipping a level</span></a>
+<ul>
+<li class="toclevel-4 tocsection-4"><a href="#Skipping_a_level_2"><span class="tocnumber">1.1.1.1</span> <span class="toctext">Skipping a level</span></a></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#Headline_2"><span class="tocnumber">2</span> <span class="toctext">Headline 2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-6"><a href="#Another_headline"><span class="tocnumber">2.1</span> <span class="toctext">Another headline</span></a></li>
+</ul>
+</li>
+</ul>
+</td></tr></table>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Headline 1">edit</a>]</span> <span class="mw-headline" id="Headline_1"> Headline 1 </span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Subheadline 1">edit</a>]</span> <span class="mw-headline" id="Subheadline_1"> Subheadline 1 </span></h3>
+<h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Skipping a level">edit</a>]</span> <span class="mw-headline" id="Skipping_a_level"> Skipping a level </span></h5>
+<h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Skipping a level">edit</a>]</span> <span class="mw-headline" id="Skipping_a_level_2"> Skipping a level </span></h6>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Headline 2">edit</a>]</span> <span class="mw-headline" id="Headline_2"> Headline 2 </span></h2>
+<p>Some text
+</p>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: Another headline">edit</a>]</span> <span class="mw-headline" id="Another_headline">Another headline</span></h3>
+
+!! end
+
+# perl -e 'print "="x$_," Level $_ heading","="x$_,"\n" for 1..10'
+!! test
+Handling of sections up to level 6 and beyond
+!! input
+= Level 1 Heading=
+== Level 2 Heading==
+=== Level 3 Heading===
+==== Level 4 Heading====
+===== Level 5 Heading=====
+====== Level 6 Heading======
+======= Level 7 Heading=======
+======== Level 8 Heading========
+========= Level 9 Heading=========
+========== Level 10 Heading==========
+!! result
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Level_1_Heading"><span class="tocnumber">1</span> <span class="toctext">Level 1 Heading</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#Level_2_Heading"><span class="tocnumber">1.1</span> <span class="toctext">Level 2 Heading</span></a>
+<ul>
+<li class="toclevel-3 tocsection-3"><a href="#Level_3_Heading"><span class="tocnumber">1.1.1</span> <span class="toctext">Level 3 Heading</span></a>
+<ul>
+<li class="toclevel-4 tocsection-4"><a href="#Level_4_Heading"><span class="tocnumber">1.1.1.1</span> <span class="toctext">Level 4 Heading</span></a>
+<ul>
+<li class="toclevel-5 tocsection-5"><a href="#Level_5_Heading"><span class="tocnumber">1.1.1.1.1</span> <span class="toctext">Level 5 Heading</span></a>
+<ul>
+<li class="toclevel-6 tocsection-6"><a href="#Level_6_Heading"><span class="tocnumber">1.1.1.1.1.1</span> <span class="toctext">Level 6 Heading</span></a></li>
+<li class="toclevel-6 tocsection-7"><a href="#.3D_Level_7_Heading.3D"><span class="tocnumber">1.1.1.1.1.2</span> <span class="toctext">= Level 7 Heading=</span></a></li>
+<li class="toclevel-6 tocsection-8"><a href="#.3D.3D_Level_8_Heading.3D.3D"><span class="tocnumber">1.1.1.1.1.3</span> <span class="toctext">== Level 8 Heading==</span></a></li>
+<li class="toclevel-6 tocsection-9"><a href="#.3D.3D.3D_Level_9_Heading.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.4</span> <span class="toctext">=== Level 9 Heading===</span></a></li>
+<li class="toclevel-6 tocsection-10"><a href="#.3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D"><span class="tocnumber">1.1.1.1.1.5</span> <span class="toctext">==== Level 10 Heading====</span></a></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</td></tr></table>
+<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Level 1 Heading">edit</a>]</span> <span class="mw-headline" id="Level_1_Heading"> Level 1 Heading</span></h1>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Level 2 Heading">edit</a>]</span> <span class="mw-headline" id="Level_2_Heading"> Level 2 Heading</span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Level 3 Heading">edit</a>]</span> <span class="mw-headline" id="Level_3_Heading"> Level 3 Heading</span></h3>
+<h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Level 4 Heading">edit</a>]</span> <span class="mw-headline" id="Level_4_Heading"> Level 4 Heading</span></h4>
+<h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Level 5 Heading">edit</a>]</span> <span class="mw-headline" id="Level_5_Heading"> Level 5 Heading</span></h5>
+<h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: Level 6 Heading">edit</a>]</span> <span class="mw-headline" id="Level_6_Heading"> Level 6 Heading</span></h6>
+<h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=7" title="Edit section: = Level 7 Heading=">edit</a>]</span> <span class="mw-headline" id=".3D_Level_7_Heading.3D">= Level 7 Heading=</span></h6>
+<h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=8" title="Edit section: == Level 8 Heading==">edit</a>]</span> <span class="mw-headline" id=".3D.3D_Level_8_Heading.3D.3D">== Level 8 Heading==</span></h6>
+<h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=9" title="Edit section: === Level 9 Heading===">edit</a>]</span> <span class="mw-headline" id=".3D.3D.3D_Level_9_Heading.3D.3D.3D">=== Level 9 Heading===</span></h6>
+<h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=10" title="Edit section: ==== Level 10 Heading====">edit</a>]</span> <span class="mw-headline" id=".3D.3D.3D.3D_Level_10_Heading.3D.3D.3D.3D">==== Level 10 Heading====</span></h6>
+
+!! end
+
+!! test
+TOC regression (bug 9764)
+!! input
+== title 1 ==
+=== title 1.1 ===
+==== title 1.1.1 ====
+=== title 1.2 ===
+== title 2 ==
+=== title 2.1 ===
+!! result
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#title_1.1"><span class="tocnumber">1.1</span> <span class="toctext">title 1.1</span></a>
+<ul>
+<li class="toclevel-3 tocsection-3"><a href="#title_1.1.1"><span class="tocnumber">1.1.1</span> <span class="toctext">title 1.1.1</span></a></li>
+</ul>
+</li>
+<li class="toclevel-2 tocsection-4"><a href="#title_1.2"><span class="tocnumber">1.2</span> <span class="toctext">title 1.2</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#title_2"><span class="tocnumber">2</span> <span class="toctext">title 2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-6"><a href="#title_2.1"><span class="tocnumber">2.1</span> <span class="toctext">title 2.1</span></a></li>
+</ul>
+</li>
+</ul>
+</td></tr></table>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline" id="title_1"> title 1 </span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline" id="title_1.1"> title 1.1 </span></h3>
+<h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 1.1.1">edit</a>]</span> <span class="mw-headline" id="title_1.1.1"> title 1.1.1 </span></h4>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: title 1.2">edit</a>]</span> <span class="mw-headline" id="title_1.2"> title 1.2 </span></h3>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline" id="title_2"> title 2 </span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: title 2.1">edit</a>]</span> <span class="mw-headline" id="title_2.1"> title 2.1 </span></h3>
+
+!! end
+
+!! test
+TOC with wgMaxTocLevel=3 (bug 6204)
+!! options
+wgMaxTocLevel=3
+!! input
+== title 1 ==
+=== title 1.1 ===
+==== title 1.1.1 ====
+=== title 1.2 ===
+== title 2 ==
+=== title 2.1 ===
+!! result
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#title_1.1"><span class="tocnumber">1.1</span> <span class="toctext">title 1.1</span></a></li>
+<li class="toclevel-2 tocsection-4"><a href="#title_1.2"><span class="tocnumber">1.2</span> <span class="toctext">title 1.2</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#title_2"><span class="tocnumber">2</span> <span class="toctext">title 2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-6"><a href="#title_2.1"><span class="tocnumber">2.1</span> <span class="toctext">title 2.1</span></a></li>
+</ul>
+</li>
+</ul>
+</td></tr></table>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline" id="title_1"> title 1 </span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline" id="title_1.1"> title 1.1 </span></h3>
+<h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 1.1.1">edit</a>]</span> <span class="mw-headline" id="title_1.1.1"> title 1.1.1 </span></h4>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: title 1.2">edit</a>]</span> <span class="mw-headline" id="title_1.2"> title 1.2 </span></h3>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline" id="title_2"> title 2 </span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: title 2.1">edit</a>]</span> <span class="mw-headline" id="title_2.1"> title 2.1 </span></h3>
+
+!! end
+
+!! test
+TOC with wgMaxTocLevel=3 and two level four headings (bug 6204)
+!! options
+wgMaxTocLevel=3
+!! input
+==Section 1==
+===Section 1.1===
+====Section 1.1.1====
+====Section 1.1.1.1====
+==Section 2==
+!! result
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#Section_1.1"><span class="tocnumber">1.1</span> <span class="toctext">Section 1.1</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-5"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a></li>
+</ul>
+</td></tr></table>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section 1">edit</a>]</span> <span class="mw-headline" id="Section_1">Section 1</span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Section 1.1">edit</a>]</span> <span class="mw-headline" id="Section_1.1">Section 1.1</span></h3>
+<h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: Section 1.1.1">edit</a>]</span> <span class="mw-headline" id="Section_1.1.1">Section 1.1.1</span></h4>
+<h4><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: Section 1.1.1.1">edit</a>]</span> <span class="mw-headline" id="Section_1.1.1.1">Section 1.1.1.1</span></h4>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: Section 2">edit</a>]</span> <span class="mw-headline" id="Section_2">Section 2</span></h2>
+
+!! end
+
+
+!! test
+Resolving duplicate section names
+!! input
+== Foo bar ==
+== Foo bar ==
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline" id="Foo_bar"> Foo bar </span></h2>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline" id="Foo_bar_2"> Foo bar </span></h2>
+
+!! end
+
+!! test
+Resolving duplicate section names with differing case (bug 10721)
+!! input
+== Foo bar ==
+== Foo Bar ==
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Foo bar">edit</a>]</span> <span class="mw-headline" id="Foo_bar"> Foo bar </span></h2>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Foo Bar">edit</a>]</span> <span class="mw-headline" id="Foo_Bar_2"> Foo Bar </span></h2>
+
+!! end
+
+!! article
+Template:sections
+!! text
+===Section 1===
+==Section 2==
+!! endarticle
+
+!! test
+Template with sections, __NOTOC__
+!! input
+__NOTOC__
+==Section 0==
+{{sections}}
+==Section 4==
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section 0">edit</a>]</span> <span class="mw-headline" id="Section_0">Section 0</span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Template:Sections&action=edit&section=T-1" title="Template:Sections">edit</a>]</span> <span class="mw-headline" id="Section_1">Section 1</span></h3>
+<h2><span class="editsection">[<a href="/index.php?title=Template:Sections&action=edit&section=T-2" title="Template:Sections">edit</a>]</span> <span class="mw-headline" id="Section_2">Section 2</span></h2>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: Section 4">edit</a>]</span> <span class="mw-headline" id="Section_4">Section 4</span></h2>
+
+!! end
+
+!! test
+__NOEDITSECTION__ keyword
+!! input
+__NOEDITSECTION__
+==Section 1==
+==Section 2==
+!! result
+<h2> <span class="mw-headline" id="Section_1">Section 1</span></h2>
+<h2> <span class="mw-headline" id="Section_2">Section 2</span></h2>
+
+!! end
+
+!! test
+Link inside a section heading
+!! input
+==Section with a [[Main Page|link]] in it==
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: Section with a link in it">edit</a>]</span> <span class="mw-headline" id="Section_with_a_link_in_it">Section with a <a href="/wiki/Main_Page" title="Main Page">link</a> in it</span></h2>
+
+!! end
+
+!! test
+TOC regression (bug 12077)
+!! input
+__TOC__
+== title 1 ==
+=== title 1.1 ===
+== title 2 ==
+!! result
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#title_1"><span class="tocnumber">1</span> <span class="toctext">title 1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#title_1.1"><span class="tocnumber">1.1</span> <span class="toctext">title 1.1</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-3"><a href="#title_2"><span class="tocnumber">2</span> <span class="toctext">title 2</span></a></li>
+</ul>
+</td></tr></table>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: title 1">edit</a>]</span> <span class="mw-headline" id="title_1"> title 1 </span></h2>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: title 1.1">edit</a>]</span> <span class="mw-headline" id="title_1.1"> title 1.1 </span></h3>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: title 2">edit</a>]</span> <span class="mw-headline" id="title_2"> title 2 </span></h2>
+
+!! end
+
+!! test
+BUG 1219 URL next to image (good)
+!! input
+http://example.com [[Image:foobar.jpg]]
+!! result
+<p><a href="http://example.com" class="external free" rel="nofollow">http://example.com</a> <a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!!end
+
+!! test
+Short headings with trailing space should match behaviour of Parser::doHeadings (bug 19910)
+!! input
+===
+The line above must have a trailing space!
+=== <!--
+--> <!-- -->
+But just in case it doesn't...
+!! result
+<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: =">edit</a>]</span> <span class="mw-headline" id=".3D">=</span></h1>
+<p>The line above must have a trailing space!
+</p>
+<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: =">edit</a>]</span> <span class="mw-headline" id=".3D_2">=</span></h1>
+<p>But just in case it doesn't...
+</p>
+!! end
+
+!! test
+Header with special characters (bug 25462)
+!! input
+The tooltips shall not show entities to the user (ie. be double escaped)
+
+== text > text ==
+section 1
+
+== text < text ==
+section 2
+
+== text & text ==
+section 3
+
+== text ' text ==
+section 4
+
+== text " text ==
+section 5
+!! result
+<p>The tooltips shall not show entities to the user (ie. be double escaped)
+</p>
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#text_.3E_text"><span class="tocnumber">1</span> <span class="toctext">text > text</span></a></li>
+<li class="toclevel-1 tocsection-2"><a href="#text_.3C_text"><span class="tocnumber">2</span> <span class="toctext">text < text</span></a></li>
+<li class="toclevel-1 tocsection-3"><a href="#text_.26_text"><span class="tocnumber">3</span> <span class="toctext">text & text</span></a></li>
+<li class="toclevel-1 tocsection-4"><a href="#text_.27_text"><span class="tocnumber">4</span> <span class="toctext">text ' text</span></a></li>
+<li class="toclevel-1 tocsection-5"><a href="#text_.22_text"><span class="tocnumber">5</span> <span class="toctext">text " text</span></a></li>
+</ul>
+</td></tr></table>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: text > text">edit</a>]</span> <span class="mw-headline" id="text_.3E_text"> text > text </span></h2>
+<p>section 1
+</p>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: text < text">edit</a>]</span> <span class="mw-headline" id="text_.3C_text"> text < text </span></h2>
+<p>section 2
+</p>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: text & text">edit</a>]</span> <span class="mw-headline" id="text_.26_text"> text & text </span></h2>
+<p>section 3
+</p>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: text ' text">edit</a>]</span> <span class="mw-headline" id="text_.27_text"> text ' text </span></h2>
+<p>section 4
+</p>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: text " text">edit</a>]</span> <span class="mw-headline" id="text_.22_text"> text " text </span></h2>
+<p>section 5
+</p>
+!! end
+
+!! test
+BUG 1219 URL next to image (broken)
+!! input
+http://example.com[[Image:foobar.jpg]]
+!! result
+<p><a href="http://example.com" class="external free" rel="nofollow">http://example.com</a><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a>
+</p>
+!!end
+
+!! test
+Bug 1186 news: in the middle of text
+!! input
+http://en.wikinews.org/wiki/Wikinews:Workplace
+!! result
+<p><a href="http://en.wikinews.org/wiki/Wikinews:Workplace" class="external free" rel="nofollow">http://en.wikinews.org/wiki/Wikinews:Workplace</a>
+</p>
+!!end
+
+
+!! test
+Namespaced link must have a title
+!! input
+[[Project:]]
+!! result
+<p>[[Project:]]
+</p>
+!!end
+
+!! test
+Namespaced link must have a title (bad fragment version)
+!! input
+[[Project:#fragment]]
+!! result
+<p>[[Project:#fragment]]
+</p>
+!!end
+
+
+!! test
+div with no attributes
+!! input
+<div>HTML rocks</div>
+!! result
+<div>HTML rocks</div>
+
+!! end
+
+!! test
+div with double-quoted attribute
+!! input
+<div id="rock">HTML rocks</div>
+!! result
+<div id="rock">HTML rocks</div>
+
+!! end
+
+!! test
+div with single-quoted attribute
+!! input
+<div id='rock'>HTML rocks</div>
+!! result
+<div id="rock">HTML rocks</div>
+
+!! end
+
+!! test
+div with unquoted attribute
+!! input
+<div id=rock>HTML rocks</div>
+!! result
+<div id="rock">HTML rocks</div>
+
+!! end
+
+!! test
+div with illegal double attributes
+!! input
+<div align="center" align="right">HTML rocks</div>
+!! result
+<div align="right">HTML rocks</div>
+
+!!end
+
+!! test
+HTML multiple attributes correction
+!! input
+<p class="error" class="awesome">Awesome!</p>
+!! result
+<p class="awesome">Awesome!</p>
+
+!!end
+
+!! test
+Table multiple attributes correction
+!! input
+{|
+!+ class="error" class="awesome"| status
+|}
+!! result
+<table>
+<tr>
+<th class="awesome"> status
+</th></tr></table>
+
+!!end
+
+!! test
+DIV IN UPPERCASE
+!! input
+<DIV ALIGN="center">HTML ROCKS</DIV>
+!! result
+<div align="center">HTML ROCKS</div>
+
+!!end
+
+
+!! test
+text with amp in the middle of nowhere
+!! input
+Remember AT&T?
+!!result
+<p>Remember AT&T?
+</p>
+!! end
+
+!! test
+text with character entity: eacute
+!! input
+I always thought é was a cute letter.
+!! result
+<p>I always thought é was a cute letter.
+</p>
+!! end
+
+!! test
+text with undefined character entity: xacute
+!! input
+I always thought &xacute; was a cute letter.
+!! result
+<p>I always thought &xacute; was a cute letter.
+</p>
+!! end
+
+
+###
+### Media links
+###
+
+!! test
+Media link
+!! input
+[[Media:Foobar.jpg]]
+!! result
+<p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Media:Foobar.jpg</a>
+</p>
+!! end
+
+!! test
+Media link with text
+!! input
+[[Media:Foobar.jpg|A neat file to look at]]
+!! result
+<p><a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">A neat file to look at</a>
+</p>
+!! end
+
+# FIXME: this is still bad HTML tag nesting
+!! test
+Media link with nasty text
+fixme: doBlockLevels won't wrap this in a paragraph because it contains a div
+!! input
+[[Media:Foobar.jpg|Safe Link<div style=display:none>" onmouseover="alert(document.cookie)" onfoo="</div>]]
+!! result
+<a href="http://example.com/images/3/3a/Foobar.jpg" class="internal" title="Foobar.jpg">Safe Link<div style="display:none">" onmouseover="alert(document.cookie)" onfoo="</div></a>
+
+!! end
+
+!! test
+Media link to nonexistent file (bug 1702)
+!! input
+[[Media:No such.jpg]]
+!! result
+<p><a href="/index.php?title=Special:Upload&wpDestFile=No_such.jpg" class="new" title="No such.jpg">Media:No such.jpg</a>
+</p>
+!! end
+
+!! test
+Image link to nonexistent file (bug 1850 - good)
+!! input
+[[Image:No such.jpg]]
+!! result
+<p><a href="/index.php?title=Special:Upload&wpDestFile=No_such.jpg" class="new" title="File:No such.jpg">File:No such.jpg</a>
+</p>
+!! end
+
+!! test
+:Image link to nonexistent file (bug 1850 - bad)
+!! input
+[[:Image:No such.jpg]]
+!! result
+<p><a href="/index.php?title=File:No_such.jpg&action=edit&redlink=1" class="new" title="File:No such.jpg (page does not exist)">Image:No such.jpg</a>
+</p>
+!! end
+
+
+
+!! test
+Character reference normalization in link text (bug 1938)
+!! input
+[[Main Page|this&that]]
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">this&that</a>
+</p>
+!!end
+
+!! article
+אַ
+!! text
+Test for unicode normalization
+
+The page's name is U+05d0 U+05b7, with non-canonical form U+FB2E
+!! endarticle
+
+!! test
+(bug 19451) Links should refer to the normalized form.
+!! input
+[[אַ]]
+[[אַ]]
+[[אַ]]
+[[אַ]]
+[[אַ]]
+!! result
+<p><a href="/wiki/%D7%90%D6%B7" title="אַ">אַ</a>
+<a href="/wiki/%D7%90%D6%B7" title="אַ">אַ</a>
+<a href="/wiki/%D7%90%D6%B7" title="אַ">אַ</a>
+<a href="/wiki/%D7%90%D6%B7" title="אַ">אַ</a>
+<a href="/wiki/%D7%90%D6%B7">אַ</a>
+</p>
+!! end
+
+!! test
+Empty attribute crash test (bug 2067)
+!! input
+<font color="">foo</font>
+!! result
+<p><font color="">foo</font>
+</p>
+!! end
+
+!! test
+Empty attribute crash test single-quotes (bug 2067)
+!! input
+<font color=''>foo</font>
+!! result
+<p><font color="">foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: equals, then nothing
+!! input
+<font color=>foo</font>
+!! result
+<p><font>foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: unquoted value
+!! input
+<font color=x>foo</font>
+!! result
+<p><font color="x">foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: unquoted but illegal value (hash)
+!! input
+<font color=#x>foo</font>
+!! result
+<p><font color="#x">foo</font>
+</p>
+!! end
+
+!! test
+Attribute test: no value
+!! input
+<font color>foo</font>
+!! result
+<p><font color="color">foo</font>
+</p>
+!! end
+
+!! test
+Bug 2095: link with three closing brackets
+!! input
+[[Main Page]]]
+!! result
+<p><a href="/wiki/Main_Page">Main Page</a>]
+</p>
+!! end
+
+!! test
+Bug 2095: link with pipe and three closing brackets
+!! input
+[[Main Page|link]]]
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">link</a>]
+</p>
+!! end
+
+!! test
+Bug 2095: link with pipe and three closing brackets, version 2
+!! input
+[[Main Page|[http://example.com/]]]
+!! result
+<p><a href="/wiki/Main_Page" title="Main Page">[http://example.com/]</a>
+</p>
+!! end
+
+
+###
+### Safety
+###
+
+!! article
+Template:Dangerous attribute
+!! text
+" onmouseover="alert(document.cookie)
+!! endarticle
+
+!! article
+Template:Dangerous style attribute
+!! text
+border-size: expression(alert(document.cookie))
+!! endarticle
+
+!! article
+Template:Div style
+!! text
+<div style="float: right; {{{1}}}">Magic div</div>
+!! endarticle
+
+!! test
+Bug 2304: HTML attribute safety (safe template; regression bug 2309)
+!! input
+<div title="{{test}}"></div>
+!! result
+<div title="This is a test template"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (dangerous template; 2309)
+!! input
+<div title="{{dangerous attribute}}"></div>
+!! result
+<div title=""></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (dangerous style template; 2309)
+!! input
+<div style="{{dangerous style attribute}}"></div>
+!! result
+<div style="/* insecure input */"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (safe parameter; 2309)
+!! input
+{{div style|width: 200px}}
+!! result
+<div style="float: right; width: 200px">Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (unsafe parameter; 2309)
+!! input
+{{div style|width: expression(alert(document.cookie))}}
+!! result
+<div style="/* insecure input */">Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (unsafe breakout parameter; 2309)
+!! input
+{{div style|"><script>alert(document.cookie)</script>}}
+!! result
+<div style="float: right;"><script>alert(document.cookie)</script>">Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (unsafe breakout parameter 2; 2309)
+!! input
+{{div style|" ><script>alert(document.cookie)</script>}}
+!! result
+<div style="float: right;"><script>alert(document.cookie)</script>">Magic div</div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (link)
+!! input
+<div title="[[Main Page]]"></div>
+!! result
+<div title="[[Main Page]]"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (italics)
+!! input
+<div title="''foobar''"></div>
+!! result
+<div title="''foobar''"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (bold)
+!! input
+<div title="'''foobar'''"></div>
+!! result
+<div title="'''foobar'''"></div>
+
+!! end
+
+
+!! test
+Bug 2304: HTML attribute safety (ISBN)
+!! input
+<div title="ISBN 1234567890"></div>
+!! result
+<div title="ISBN 1234567890"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (RFC)
+!! input
+<div title="RFC 1234"></div>
+!! result
+<div title="RFC 1234"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (PMID)
+!! input
+<div title="PMID 1234567890"></div>
+!! result
+<div title="PMID 1234567890"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (web link)
+!! input
+<div title="http://example.com/"></div>
+!! result
+<div title="http://example.com/"></div>
+
+!! end
+
+!! test
+Bug 2304: HTML attribute safety (named web link)
+!! input
+<div title="[http://example.com/ link]"></div>
+!! result
+<div title="[http://example.com/ link]"></div>
+
+!! end
+
+!! test
+Bug 3244: HTML attribute safety (extension; safe)
+!! input
+<div style="<nowiki>background:blue</nowiki>"></div>
+!! result
+<div style="background:blue"></div>
+
+!! end
+
+!! test
+Bug 3244: HTML attribute safety (extension; unsafe)
+!! input
+<div style="<nowiki>border-left:expression(alert(document.cookie))</nowiki>"></div>
+!! result
+<div style="/* insecure input */"></div>
+
+!! end
+
+!! test
+Math section safety when disabled
+!! input
+<math><script>alert(document.cookies);</script></math>
+!! result
+<p><math><script>alert(document.cookies);</script></math>
+</p>
+!! end
+
+# More MSIE fun discovered by Tom Gilder
+
+!! test
+MSIE CSS safety test: spurious slash
+!! input
+<div style="background-image:u\rl(javascript:alert('boo'))">evil</div>
+!! result
+<div style="/* insecure input */">evil</div>
+
+!! end
+
+!! test
+MSIE CSS safety test: hex code
+!! input
+<div style="background-image:u\72l(javascript:alert('boo'))">evil</div>
+!! result
+<div style="/* insecure input */">evil</div>
+
+!! end
+
+!! test
+MSIE CSS safety test: comment in url
+!! input
+<div style="background-image:u/**/rl(javascript:alert('boo'))">evil</div>
+!! result
+<div style="background-image:u rl(javascript:alert('boo'))">evil</div>
+
+!! end
+
+!! test
+MSIE CSS safety test: comment in expression
+!! input
+<div style="background-image:expres/**/sion(alert('boo4'))">evil4</div>
+!! result
+<div style="background-image:expres sion(alert('boo4'))">evil4</div>
+
+!! end
+
+
+!! test
+Table attribute legitimate extension
+!! input
+{|
+!+ style="<nowiki>color:blue</nowiki>"| status
+|}
+!! result
+<table>
+<tr>
+<th style="color:blue"> status
+</th></tr></table>
+
+!!end
+
+!! test
+Table attribute safety
+!! input
+{|
+!+ style="<nowiki>border-width:expression(0+alert(document.cookie))</nowiki>"| status
+|}
+!! result
+<table>
+<tr>
+<th style="/* insecure input */"> status
+</th></tr></table>
+
+!! end
+
+!! test
+CSS line continuation 1
+!! input
+<div style="background-image: u\ rl(test.jpg);"></div>
+!! result
+<div style="/* insecure input */"></div>
+
+!! end
+
+!! test
+CSS line continuation 2
+!! input
+<div style="background-image: u\ rl(test.jpg); "></div>
+!! result
+<div style="/* insecure input */"></div>
+
+!! end
+
+!! article
+Template:Identity
+!! text
+{{{1}}}
+!! endarticle
+
+!! test
+Expansion of multi-line templates in attribute values (bug 6255)
+!! input
+<div style="background: {{identity|#00FF00}}">-</div>
+!! result
+<div style="background: #00FF00">-</div>
+
+!! end
+
+
+!! test
+Expansion of multi-line templates in attribute values (bug 6255 sanity check)
+!! input
+<div style="background:
+#00FF00">-</div>
+!! result
+<div style="background: #00FF00">-</div>
+
+!! end
+
+!! test
+Expansion of multi-line templates in attribute values (bug 6255 sanity check 2)
+!! input
+<div style="background: #00FF00">-</div>
+!! result
+<div style="background: #00FF00">-</div>
+
+!! end
+
+###
+### Parser hooks (see maintenance/parserTestsParserHook.php for the <tag> extension)
+###
+!! test
+Parser hook: empty input
+!! input
+<tag></tag>
+!! result
+<pre>
+string(0) ""
+array(0) {
+}
+</pre>
+
+!! end
+
+!! test
+Parser hook: empty input using terminated empty elements
+!! input
+<tag/>
+!! result
+<pre>
+NULL
+array(0) {
+}
+</pre>
+
+!! end
+
+!! test
+Parser hook: empty input using terminated empty elements (space before)
+!! input
+<tag />
+!! result
+<pre>
+NULL
+array(0) {
+}
+</pre>
+
+!! end
+
+!! test
+Parser hook: basic input
+!! input
+<tag>input</tag>
+!! result
+<pre>
+string(5) "input"
+array(0) {
+}
+</pre>
+
+!! end
+
+
+!! test
+Parser hook: case insensitive
+!! input
+<TAG>input</TAG>
+!! result
+<pre>
+string(5) "input"
+array(0) {
+}
+</pre>
+
+!! end
+
+
+!! test
+Parser hook: case insensitive, redux
+!! input
+<TaG>input</TAg>
+!! result
+<pre>
+string(5) "input"
+array(0) {
+}
+</pre>
+
+!! end
+
+!! test
+Parser hook: nested tags
+!! options
+noxml
+!! input
+<tag><tag></tag></tag>
+!! result
+<pre>
+string(5) "<tag>"
+array(0) {
+}
+</pre></tag>
+
+!! end
+
+!! test
+Parser hook: basic arguments
+!! input
+<tag width=200 height = "100" depth = '50' square></tag>
+!! result
+<pre>
+string(0) ""
+array(4) {
+ ["width"]=>
+ string(3) "200"
+ ["height"]=>
+ string(3) "100"
+ ["depth"]=>
+ string(2) "50"
+ ["square"]=>
+ string(6) "square"
+}
+</pre>
+
+!! end
+
+!! test
+Parser hook: argument containing a forward slash (bug 5344)
+!! input
+<tag filename='/tmp/bla'></tag>
+!! result
+<pre>
+string(0) ""
+array(1) {
+ ["filename"]=>
+ string(8) "/tmp/bla"
+}
+</pre>
+
+!! end
+
+!! test
+Parser hook: empty input using terminated empty elements (bug 2374)
+!! input
+<tag foo=bar/>text
+!! result
+<pre>
+NULL
+array(1) {
+ ["foo"]=>
+ string(3) "bar"
+}
+</pre>text
+
+!! end
+
+# </tag> should be output literally since there is no matching tag that begins it
+!! test
+Parser hook: basic arguments using terminated empty elements (bug 2374)
+!! input
+<tag width=200 height = "100" depth = '50' square/>
+other stuff
+</tag>
+!! result
+<pre>
+NULL
+array(4) {
+ ["width"]=>
+ string(3) "200"
+ ["height"]=>
+ string(3) "100"
+ ["depth"]=>
+ string(2) "50"
+ ["square"]=>
+ string(6) "square"
+}
+</pre>
+<p>other stuff
+</tag>
+</p>
+!! end
+
+###
+### (see maintenance/parserTestsStaticParserHook.php for the <statictag> extension)
+###
+
+!! test
+Parser hook: static parser hook not inside a comment
+!! input
+<statictag>hello, world</statictag>
+<statictag action=flush/>
+!! result
+<p>hello, world
+</p>
+!! end
+
+
+!! test
+Parser hook: static parser hook inside a comment
+!! input
+<!-- <statictag>hello, world</statictag> -->
+<statictag action=flush/>
+!! result
+<p><br />
+</p>
+!! end
+
+# Nested template calls; this case was broken by Parser.php rev 1.506,
+# since reverted.
+
+!! article
+Template:One-parameter
+!! text
+(My parameter is: {{{1}}})
+!! endarticle
+
+!! article
+Template:Map-one-parameter
+!! text
+{{{{{1}}}|{{{2}}}}}
+!! endarticle
+
+!! test
+Nested template calls
+!! input
+{{Map-one-parameter|One-parameter|param}}
+!! result
+<p>(My parameter is: param)
+</p>
+!! end
+
+
+###
+### Sanitizer
+###
+!! test
+Sanitizer: Closing of open tags
+!! input
+<s></s><table></table>
+!! result
+<s></s><table></table>
+
+!! end
+
+!! test
+Sanitizer: Closing of open but not closed tags
+!! input
+<s>foo
+!! result
+<p><s>foo</s>
+</p>
+!! end
+
+!! test
+Sanitizer: Closing of closed but not open tags
+!! input
+</s>
+!! result
+<p></s>
+</p>
+!! end
+
+!! test
+Sanitizer: Closing of closed but not open table tags
+!! input
+Table not started</td></tr></table>
+!! result
+<p>Table not started</td></tr></table>
+</p>
+!! end
+
+!! test
+Sanitizer: Escaping of spaces, multibyte characters, colons & other stuff in id=""
+!! input
+<span id="æ: v">byte</span>[[#æ: v|backlink]]
+!! result
+<p><span id=".C3.A6:_v">byte</span><a href="#.C3.A6:_v">backlink</a>
+</p>
+!! end
+
+!! test
+Sanitizer: Validating the contents of the id attribute (bug 4515)
+!! options
+disabled
+!! input
+<br id=9 />
+!! result
+Something, but definitely not <br id="9" />...
+!! end
+
+!! test
+Sanitizer: Validating id attribute uniqueness (bug 4515, bug 6301)
+!! options
+disabled
+!! input
+<br id="foo" /><br id="foo" />
+!! result
+Something need to be done. foo-2 ?
+!! end
+
+!! test
+Language converter: output gets cut off unexpectedly (bug 5757)
+!! options
+language=zh
+!! input
+this bit is safe: }-
+
+but if we add a conversion instance: -{zh-cn:xxx;zh-tw:yyy}-
+
+then we get cut off here: }-
+
+all additional text is vanished
+!! result
+<p>this bit is safe: }-
+</p><p>but if we add a conversion instance: xxx
+</p><p>then we get cut off here: }-
+</p><p>all additional text is vanished
+</p>
+!! end
+
+!! test
+Self closed html pairs (bug 5487)
+!! options
+!! input
+<center><font id="bug" />Centered text</center>
+<div><font id="bug2" />In div text</div>
+!! result
+<center><font id="bug" />Centered text</center>
+<div><font id="bug2" />In div text</div>
+
+!! end
+
+#
+#
+#
+
+!! test
+Punctuation: nbsp before exclamation
+!! input
+C'est grave !
+!! result
+<p>C'est grave !
+</p>
+!! end
+
+!! test
+Punctuation: CSS !important (bug 11874)
+!! input
+<div style="width:50% !important">important</div>
+!! result
+<div style="width:50% !important">important</div>
+
+!!end
+
+!! test
+Punctuation: CSS ! important (bug 11874; with space after)
+!! input
+<div style="width:50% ! important">important</div>
+!! result
+<div style="width:50% ! important">important</div>
+
+!!end
+
+
+!! test
+HTML bullet list, closed tags (bug 5497)
+!! input
+<ul>
+<li>One</li>
+<li>Two</li>
+</ul>
+!! result
+<ul>
+<li>One</li>
+<li>Two</li>
+</ul>
+
+!! end
+
+!! test
+HTML bullet list, unclosed tags (bug 5497)
+!! options
+disabled
+!! input
+<ul>
+<li>One
+<li>Two
+</ul>
+!! result
+<ul>
+<li>One
+</li><li>Two
+</li></ul>
+
+!! end
+
+!! test
+HTML ordered list, closed tags (bug 5497)
+!! input
+<ol>
+<li>One</li>
+<li>Two</li>
+</ol>
+!! result
+<ol>
+<li>One</li>
+<li>Two</li>
+</ol>
+
+!! end
+
+!! test
+HTML ordered list, unclosed tags (bug 5497)
+!! options
+disabled
+!! input
+<ol>
+<li>One
+<li>Two
+</ol>
+!! result
+<ol>
+<li>One
+</li><li>Two
+</li></ol>
+
+!! end
+
+!! test
+HTML nested bullet list, closed tags (bug 5497)
+!! input
+<ul>
+<li>One</li>
+<li>Two:
+<ul>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ul>
+</li>
+</ul>
+!! result
+<ul>
+<li>One</li>
+<li>Two:
+<ul>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ul>
+</li>
+</ul>
+
+!! end
+
+!! test
+HTML nested bullet list, open tags (bug 5497)
+!! options
+disabled
+!! input
+<ul>
+<li>One
+<li>Two:
+<ul>
+<li>Sub-one
+<li>Sub-two
+</ul>
+</ul>
+!! result
+<ul>
+<li>One
+</li><li>Two:
+<ul>
+<li>Sub-one
+</li><li>Sub-two
+</li></ul>
+</li></ul>
+
+!! end
+
+!! test
+HTML nested ordered list, closed tags (bug 5497)
+!! input
+<ol>
+<li>One</li>
+<li>Two:
+<ol>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ol>
+</li>
+</ol>
+!! result
+<ol>
+<li>One</li>
+<li>Two:
+<ol>
+<li>Sub-one</li>
+<li>Sub-two</li>
+</ol>
+</li>
+</ol>
+
+!! end
+
+!! test
+HTML nested ordered list, open tags (bug 5497)
+!! options
+disabled
+!! input
+<ol>
+<li>One
+<li>Two:
+<ol>
+<li>Sub-one
+<li>Sub-two
+</ol>
+</ol>
+!! result
+<ol>
+<li>One
+</li><li>Two:
+<ol>
+<li>Sub-one
+</li><li>Sub-two
+</li></ol>
+</li></ol>
+
+!! end
+
+!! test
+HTML ordered list item with parameters oddity
+!! input
+<ol><li id="fragment">One</li></ol>
+!! result
+<ol><li id="fragment">One</li></ol>
+
+!! end
+
+!!test
+bug 5918: autonumbering
+!! input
+[http://first/] [http://second] [ftp://ftp]
+
+ftp://inlineftp
+
+[mailto:enclosed@mail.tld With target]
+
+[mailto:enclosed@mail.tld]
+
+mailto:inline@mail.tld
+!! result
+<p><a href="http://first/" class="external autonumber" rel="nofollow">[1]</a> <a href="http://second" class="external autonumber" rel="nofollow">[2]</a> <a href="ftp://ftp" class="external autonumber" rel="nofollow">[3]</a>
+</p><p><a href="ftp://inlineftp" class="external free" rel="nofollow">ftp://inlineftp</a>
+</p><p><a href="mailto:enclosed@mail.tld" class="external text" rel="nofollow">With target</a>
+</p><p><a href="mailto:enclosed@mail.tld" class="external autonumber" rel="nofollow">[4]</a>
+</p><p><a href="mailto:inline@mail.tld" class="external free" rel="nofollow">mailto:inline@mail.tld</a>
+</p>
+!! end
+
+
+#
+# Security and HTML correctness
+# From Nick Jenkins' fuzz testing
+#
+
+!! test
+Fuzz testing: Parser13
+!! input
+{|
+| http://a|
+!! result
+<table>
+<tr>
+<td>
+</td>
+</tr>
+</table>
+
+!! end
+
+!! test
+Fuzz testing: Parser14
+!! input
+== onmouseover= ==
+http://__TOC__
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: onmouseover=">edit</a>]</span> <span class="mw-headline" id="onmouseover.3D"> onmouseover= </span></h2>
+http://<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#onmouseover.3D"><span class="tocnumber">1</span> <span class="toctext">onmouseover=</span></a></li>
+</ul>
+</td></tr></table>
+
+!! end
+
+!! test
+Fuzz testing: Parser14-table
+!! input
+==a==
+{| STYLE=__TOC__
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: a">edit</a>]</span> <span class="mw-headline" id="a">a</span></h2>
+<table style="__TOC__">
+<tr><td></td></tr>
+</table>
+
+!! end
+
+# Known to produce bogus xml (extra </td>)
+!! test
+Fuzz testing: Parser16
+!! options
+noxml
+!! input
+{|
+!https://||||||
+!! result
+<table>
+<tr>
+<th>https://</th>
+<th></th>
+<th></th>
+<th>
+</td>
+</tr>
+</table>
+
+!! end
+
+!! test
+Fuzz testing: Parser21
+!! input
+{|
+! irc://{{ftp://a" onmouseover="alert('hello world');"
+|
+!! result
+<table>
+<tr>
+<th> <a href="irc://{{ftp://a" class="external free" rel="nofollow">irc://{{ftp://a</a>" onmouseover="alert('hello world');"
+</th>
+<td>
+</td>
+</tr>
+</table>
+
+!! end
+
+!! test
+Fuzz testing: Parser22
+!! input
+http://===r:::https://b
+
+{|
+!!result
+<p><a href="http://===r:::https://b" class="external free" rel="nofollow">http://===r:::https://b</a>
+</p>
+<table>
+<tr><td></td></tr>
+</table>
+
+!! end
+
+# Known to produce bad XML for now
+!! test
+Fuzz testing: Parser24
+!! options
+noxml
+!! input
+{|
+{{{|
+<u CLASS=
+| {{{{SSSll!!!!!!!VVVV)]]][[Special:*xxxxxxx--><noinclude>}}}} >
+<br style="onmouseover='alert(document.cookie);' " />
+
+MOVE YOUR MOUSE CURSOR OVER THIS TEXT
+|
+!! result
+<table>
+{{{|
+<u class="|">}}}} >
+<br style="onmouseover='alert(document.cookie);'" />
+
+MOVE YOUR MOUSE CURSOR OVER THIS TEXT
+<tr>
+<td></u>
+</td>
+</tr>
+</table>
+
+!! end
+
+# Note: the current result listed for this is not what the original one was,
+# but the original bug was JavaScript injection, which is fixed in any case.
+# It's not clear that the original result listed was any more correct than the
+# current one. Original result:
+# <p>{{{|
+# </p>
+# <li class="||">
+# }}}blah" onmouseover="alert('hello world');" align="left"<b>MOVE MOUSE CURSOR OVER HERE</b>
+!!test
+Fuzz testing: Parser25 (bug 6055)
+!! input
+{{{
+|
+<LI CLASS=||
+ >
+}}}blah" onmouseover="alert('hello world');" align="left"'''MOVE MOUSE CURSOR OVER HERE
+!! result
+<p><LI CLASS=blah" onmouseover="alert('hello world');" align="left"<b>MOVE MOUSE CURSOR OVER HERE</b>
+</p>
+!! end
+
+!!test
+Fuzz testing: URL adjacent extension (with space, clean)
+!! options
+!! input
+http://example.com <nowiki>junk</nowiki>
+!! result
+<p><a href="http://example.com" class="external free" rel="nofollow">http://example.com</a> junk
+</p>
+!!end
+
+!!test
+Fuzz testing: URL adjacent extension (no space, dirty; nowiki)
+!! options
+!! input
+http://example.com<nowiki>junk</nowiki>
+!! result
+<p><a href="http://example.com" class="external free" rel="nofollow">http://example.com</a>junk
+</p>
+!!end
+
+!!test
+Fuzz testing: URL adjacent extension (no space, dirty; pre)
+!! options
+!! input
+http://example.com<pre>junk</pre>
+!! result
+<a href="http://example.com" class="external free" rel="nofollow">http://example.com</a><pre>junk</pre>
+
+!!end
+
+!!test
+Fuzz testing: image with bogus manual thumbnail
+!!input
+[[Image:foobar.jpg|thumbnail= ]]
+!!result
+<div class="thumb tright"><div class="thumbinner" style="width:1943px;">Error creating thumbnail: <div class="thumbcaption"></div></div></div>
+
+!!end
+
+!! test
+Fuzz testing: encoded newline in generated HTML replacements (bug 6577)
+!! input
+<pre dir=" "></pre>
+!! result
+<pre dir=" "></pre>
+
+!! end
+
+!! test
+Parsing optional HTML elements (Bug 6171)
+!! options
+!! input
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...
+ <td> And yet som tabular data</td>
+ </tr>
+</table>
+!! result
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...
+ </td><td> And yet som tabular data</td>
+ </tr>
+</table>
+
+!! end
+
+!! test
+Correct handling of <td>, <tr> (Bug 6171)
+!! options
+!! input
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...</td>
+ <td> And yet som tabular data</td>
+ </tr>
+</table>
+!! result
+<table>
+ <tr>
+ <td> Some tabular data</td>
+ <td> More tabular data ...</td>
+ <td> And yet som tabular data</td>
+ </tr>
+</table>
+
+!! end
+
+
+!! test
+Parsing crashing regression (fr:JavaScript)
+!! input
+</body></x>
+!! result
+<p></body></x>
+</p>
+!! end
+
+!! test
+Inline wiki vs wiki block nesting
+!! input
+'''Bold paragraph
+
+New wiki paragraph
+!! result
+<p><b>Bold paragraph</b>
+</p><p>New wiki paragraph
+</p>
+!! end
+
+!! test
+Inline HTML vs wiki block nesting
+!! options
+disabled
+!! input
+<b>Bold paragraph
+
+New wiki paragraph
+!! result
+<p><b>Bold paragraph</b>
+</p><p>New wiki paragraph
+</p>
+!! end
+
+# Original result was this:
+# <p><b>bold</b><b>bold<i>bolditalics</i></b>
+# </p>
+# While that might be marginally more intuitive, maybe, the six-apostrophe
+# construct is clearly pathological and the result stated here (which is what
+# the parser actually does) is about as reasonable as anything.
+!!test
+Mixing markup for italics and bold
+!! options
+!! input
+'''bold''''''bold''bolditalics'''''
+!! result
+<p>'<i>bold'</i><b>bold<i>bolditalics</i></b>
+</p>
+!! end
+
+
+!! article
+Xyzzyx
+!! text
+Article for special page transclusion test
+!! endarticle
+
+!! test
+Special page transclusion
+!! options
+!! input
+{{Special:Prefixindex/Xyzzyx}}
+!! result
+<p><br />
+</p>
+<table border="0" id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx">Xyzzyx</a></td></tr></table>
+
+!! end
+
+!! test
+Special page transclusion twice (bug 5021)
+!! options
+!! input
+{{Special:Prefixindex/Xyzzyx}}
+{{Special:Prefixindex/Xyzzyx}}
+!! result
+<p><br />
+</p>
+<table border="0" id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx">Xyzzyx</a></td></tr></table>
+<p><br />
+</p>
+<table border="0" id="mw-prefixindex-list-table"><tr><td><a href="/wiki/Xyzzyx">Xyzzyx</a></td></tr></table>
+
+!! end
+
+!! test
+Transclusion of default MediaWiki message
+!! input
+{{MediaWiki:Mainpage}}
+!!result
+<p>Main Page
+</p>
+!! end
+
+!! test
+Transclusion of nonexistent MediaWiki message
+!! input
+{{MediaWiki:Mainpagexxx}}
+!!result
+<p><a href="/index.php?title=MediaWiki:Mainpagexxx&action=edit&redlink=1" class="new" title="MediaWiki:Mainpagexxx (page does not exist)">MediaWiki:Mainpagexxx</a>
+</p>
+!! end
+
+!! test
+Transclusion of MediaWiki message with underscore
+!! input
+{{MediaWiki:history_short}}
+!! result
+<p>History
+</p>
+!! end
+
+!! test
+Transclusion of MediaWiki message with space
+!! input
+{{MediaWiki:history short}}
+!! result
+<p>History
+</p>
+!! end
+
+!! test
+Invalid header with following text
+!! input
+= x = y
+!! result
+<p>= x = y
+</p>
+!! end
+
+
+!! test
+Section extraction test (section 0)
+!! options
+section=0
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+!! end
+
+!! test
+Section extraction test (section 1)
+!! options
+section=1
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+==a==
+===aa===
+====aaa====
+!! end
+
+!! test
+Section extraction test (section 2)
+!! options
+section=2
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+===aa===
+====aaa====
+!! end
+
+!! test
+Section extraction test (section 3)
+!! options
+section=3
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+====aaa====
+!! end
+
+!! test
+Section extraction test (section 4)
+!! options
+section=4
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+!! end
+
+!! test
+Section extraction test (section 5)
+!! options
+section=5
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+===ba===
+!! end
+
+!! test
+Section extraction test (section 6)
+!! options
+section=6
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+===bb===
+====bba====
+!! end
+
+!! test
+Section extraction test (section 7)
+!! options
+section=7
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+====bba====
+!! end
+
+!! test
+Section extraction test (section 8)
+!! options
+section=8
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+===bc===
+!! end
+
+!! test
+Section extraction test (section 9)
+!! options
+section=9
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+==c==
+===ca===
+!! end
+
+!! test
+Section extraction test (section 10)
+!! options
+section=10
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+===ca===
+!! end
+
+!! test
+Section extraction test (nonexistent section 11)
+!! options
+section=11
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+!! end
+
+!! test
+Section extraction test with bogus heading (section 1)
+!! options
+section=1
+!! input
+==a==
+==bogus== not a legal section
+==b==
+!! result
+==a==
+==bogus== not a legal section
+!! end
+
+!! test
+Section extraction test with bogus heading (section 2)
+!! options
+section=2
+!! input
+==a==
+==bogus== not a legal section
+==b==
+!! result
+==b==
+!! end
+
+!! test
+Section extraction test with comment after heading (section 1)
+!! options
+section=1
+!! input
+==a==
+==b== <!-- -->
+==c==
+!! result
+==a==
+!! end
+
+!! test
+Section extraction test with comment after heading (section 2)
+!! options
+section=2
+!! input
+==a==
+==b== <!-- -->
+==c==
+!! result
+==b== <!-- -->
+!! end
+
+!! test
+Section extraction test with bogus <nowiki> heading (section 1)
+!! options
+section=1
+!! input
+==a==
+==bogus== <nowiki>not a legal section</nowiki>
+==b==
+!! result
+==a==
+==bogus== <nowiki>not a legal section</nowiki>
+!! end
+
+!! test
+Section extraction test with bogus <nowiki> heading (section 2)
+!! options
+section=2
+!! input
+==a==
+==bogus== <nowiki>not a legal section</nowiki>
+==b==
+!! result
+==b==
+!! end
+
+
+# Formerly testing for bug 2587, now resolved by the use of unmarked sections
+# instead of respecting commented sections
+!! test
+Section extraction prefixed by comment (section 1)
+!! options
+section=1
+!! input
+<!-- -->==sec1==
+==sec2==
+!!result
+==sec2==
+!!end
+
+!! test
+Section extraction prefixed by comment (section 2)
+!! options
+section=2
+!! input
+<!-- -->==sec1==
+==sec2==
+!!result
+
+!!end
+
+
+# Formerly testing for bug 2607, now resolved by the use of unmarked sections
+# instead of respecting HTML-style headings
+!! test
+Section extraction, mixed wiki and html (section 1)
+!! options
+section=1
+!! input
+<h2>unmarked</h2>
+unmarked
+==1==
+one
+==2==
+two
+!! result
+==1==
+one
+!! end
+
+!! test
+Section extraction, mixed wiki and html (section 2)
+!! options
+section=2
+!! input
+<h2>unmarked</h2>
+unmarked
+==1==
+one
+==2==
+two
+!! result
+==2==
+two
+!! end
+
+
+# Formerly testing for bug 3342
+!! test
+Section extraction, heading surrounded by <noinclude>
+!! options
+section=1
+!! input
+<noinclude>==unmarked==</noinclude>
+==marked==
+!! result
+==marked==
+!!end
+
+# Test behaviour of bug 19910
+!! test
+Sectiion with all-equals
+!! options
+section=2
+!! input
+===
+The line above must have a trailing space
+=== <!--
+--> <!-- -->
+But just in case it doesn't...
+!! result
+=== <!--
+--> <!-- -->
+But just in case it doesn't...
+!! end
+
+!! test
+Section replacement test (section 0)
+!! options
+replace=0,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+xxx
+
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 1)
+!! options
+replace=1,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+xxx
+
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 2)
+!! options
+replace=2,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+xxx
+
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 3)
+!! options
+replace=3,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+xxx
+
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 4)
+!! options
+replace=4,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+====aaa====
+xxx
+
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 5)
+!! options
+replace=5,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+====aaa====
+==b==
+xxx
+
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 6)
+!! options
+replace=6,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+xxx
+
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 7)
+!! options
+replace=7,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+xxx
+
+===bc===
+==c==
+===ca===
+!! end
+
+!! test
+Section replacement test (section 8)
+!! options
+replace=8,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+xxx
+
+==c==
+===ca===
+!!end
+
+!! test
+Section replacement test (section 9)
+!! options
+replace=9,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+xxx
+!! end
+
+!! test
+Section replacement test (section 10)
+!! options
+replace=10,"xxx"
+!! input
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+===ca===
+!! result
+start
+==a==
+===aa===
+====aaa====
+==b==
+===ba===
+===bb===
+====bba====
+===bc===
+==c==
+xxx
+!! end
+
+!! test
+Section replacement test with initial whitespace (bug 13728)
+!! options
+replace=2,"xxx"
+!! input
+ Preformatted initial line
+==a==
+===a===
+!! result
+ Preformatted initial line
+==a==
+xxx
+!! end
+
+
+!! test
+Section extraction, heading followed by pre with 20 spaces (bug 6398)
+!! options
+section=1
+!! input
+==a==
+ a
+!! result
+==a==
+ a
+!! end
+
+!! test
+Section extraction, heading followed by pre with 19 spaces (bug 6398 sanity check)
+!! options
+section=1
+!! input
+==a==
+ a
+!! result
+==a==
+ a
+!! end
+
+
+!! test
+Section extraction, <pre> around bogus header (bug 10309)
+!! options
+noxml section=2
+!! input
+== Section One ==
+<pre>
+=======
+</pre>
+
+== Section Two ==
+stuff
+!! result
+== Section Two ==
+stuff
+!! end
+
+!! test
+Section replacement, <pre> around bogus header (bug 10309)
+!! options
+noxml replace=2,"xxx"
+!! input
+== Section One ==
+<pre>
+=======
+</pre>
+
+== Section Two ==
+stuff
+!! result
+== Section One ==
+<pre>
+=======
+</pre>
+
+xxx
+!! end
+
+
+
+!! test
+Handling of 
 in URLs
+!! input
+**irc://
a
+!! result
+<ul><li><ul><li><a href="irc://%0Aa" class="external free" rel="nofollow">irc://%0Aa</a>
+</li></ul>
+</li></ul>
+
+!!end
+
+!! test
+5 quotes, code coverage +1 line
+!! input
+'''''
+!! result
+!! end
+
+!! test
+Special:Search page linking.
+!! input
+{{Special:search}}
+!! result
+<p><a href="/wiki/Special:Search">Special:Search</a>
+</p>
+!! end
+
+!! test
+Say the magic word
+!! input
+* {{PAGENAME}}
+* {{BASEPAGENAME}}
+* {{SUBPAGENAME}}
+* {{SUBPAGENAMEE}}
+* {{BASEPAGENAME}}
+* {{BASEPAGENAMEE}}
+* {{TALKPAGENAME}}
+* {{TALKPAGENAMEE}}
+* {{SUBJECTPAGENAME}}
+* {{SUBJECTPAGENAMEE}}
+* {{NAMESPACEE}}
+* {{NAMESPACE}}
+* {{TALKSPACE}}
+* {{TALKSPACEE}}
+* {{SUBJECTSPACE}}
+* {{SUBJECTSPACEE}}
+* {{Dynamic|{{NUMBEROFUSERS}}|{{NUMBEROFPAGES}}|{{CURRENTVERSION}}|{{CONTENTLANGUAGE}}|{{DIRECTIONMARK}}|{{CURRENTTIMESTAMP}}|{{NUMBEROFARTICLES}}}}
+!! result
+<ul><li> Parser test
+</li><li> Parser test
+</li><li> Parser test
+</li><li> Parser_test
+</li><li> Parser test
+</li><li> Parser_test
+</li><li> Talk:Parser test
+</li><li> Talk:Parser_test
+</li><li> Parser test
+</li><li> Parser_test
+</li><li>
+</li><li>
+</li><li> Talk
+</li><li> Talk
+</li><li>
+</li><li>
+</li><li> <a href="/index.php?title=Template:Dynamic&action=edit&redlink=1" class="new" title="Template:Dynamic (page does not exist)">Template:Dynamic</a>
+</li></ul>
+
+!! end
+### Note: Above tests excludes the "{{NUMBEROFADMINS}}" magic word because it generates a MySQL error when included.
+
+!! test
+Gallery
+!! input
+<gallery>
+image1.png |
+image2.gif|||||
+
+image3|
+image4 |300px| centre
+ image5.svg| http://///////
+[[x|xx]]]]
+* image6
+</gallery>
+!! result
+<ul class="gallery">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">Image1.png</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">Image2.gif</div>
+ <div class="gallerytext">
+<p>||||
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">Image3</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">Image4</div>
+ <div class="gallerytext">
+<p>300px| centre
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">Image5.svg</div>
+ <div class="gallerytext">
+<p><a href="http://///////" class="external free" rel="nofollow">http://///////</a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">* image6</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+Gallery (with options)
+!! input
+<gallery widths='60px' heights='40px' perrow='2' caption='Foo [[Main Page]]' >
+File:Nonexistant.jpg|caption
+File:Nonexistant.jpg
+image:foobar.jpg|some '''caption''' [[Main Page]]
+image:foobar.jpg
+</gallery>
+!! result
+<ul class="gallery" style="max-width: 220px;_width: 220px;">
+ <li class='gallerycaption'>Foo <a href="/wiki/Main_Page">Main Page</a></li>
+ <li class="gallerybox" style="width: 95px"><div style="width: 95px">
+ <div style="height: 70px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+<p>caption
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 95px"><div style="width: 95px">
+ <div style="height: 70px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 95px"><div style="width: 95px">
+ <div class="thumb" style="width: 90px; height: 70px;"><div style="margin:31px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="60" height="7" /></a></div></div>
+ <div class="gallerytext">
+<p>some <b>caption</b> <a href="/wiki/Main_Page">Main Page</a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 95px"><div style="width: 95px">
+ <div class="thumb" style="width: 90px; height: 70px;"><div style="margin:31px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="60" height="7" /></a></div></div>
+ <div class="gallerytext">
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+gallery (with showfilename option)
+!! input
+<gallery showfilename>
+File:Nonexistant.jpg|caption
+File:Nonexistant.jpg
+image:foobar.jpg|some '''caption''' [[Main Page]]
+File:Foobar.jpg
+</gallery>
+!! result
+<ul class="gallery">
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Nonexistant.jpg" title="File:Nonexistant.jpg">Nonexistant.jpg</a><br />
+caption
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div style="height: 150px;">Nonexistant.jpg</div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Nonexistant.jpg" title="File:Nonexistant.jpg">Nonexistant.jpg</a><br />
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px; height: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Foobar.jpg" title="File:Foobar.jpg">Foobar.jpg</a><br />
+some <b>caption</b> <a href="/wiki/Main_Page">Main Page</a>
+</p>
+ </div>
+ </div></li>
+ <li class="gallerybox" style="width: 155px"><div style="width: 155px">
+ <div class="thumb" style="width: 150px; height: 150px;"><div style="margin:68px auto;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="120" height="14" /></a></div></div>
+ <div class="gallerytext">
+<p><a href="/wiki/File:Foobar.jpg" title="File:Foobar.jpg">Foobar.jpg</a><br />
+</p>
+ </div>
+ </div></li>
+</ul>
+
+!! end
+
+!! test
+HTML Hex character encoding (spells the word "JavaScript")
+!! input
+JavaScript
+!! result
+<p>JavaScript
+</p>
+!! end
+
+!! test
+__FORCETOC__ override
+!! input
+__NEWSECTIONLINK__
+__FORCETOC__
+!! result
+<p><br />
+</p>
+!! end
+
+!! test
+ISBN code coverage
+!! input
+ISBN 978-0-1234-56 789
+!! result
+<p><a href="/wiki/Special:BookSources/9780123456" class="internal mw-magiclink-isbn">ISBN 978-0-1234-56</a> 789
+</p>
+!! end
+
+!! test
+ISBN followed by 5 spaces
+!! input
+ISBN
+!! result
+<p>ISBN
+</p>
+!! end
+
+!! test
+Double ISBN
+!! input
+ISBN ISBN 1234567890
+!! result
+<p>ISBN <a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1234567890</a>
+</p>
+!! end
+
+!! test
+Bug 22905: <abbr> followed by ISBN followed by </a>
+!! input
+<abbr>(fr)</abbr> ISBN 2753300917 [http://www.example.com example.com]
+!! result
+<p><abbr>(fr)</abbr> <a href="/wiki/Special:BookSources/2753300917" class="internal mw-magiclink-isbn">ISBN 2753300917</a> <a href="http://www.example.com" class="external text" rel="nofollow">example.com</a>
+</p>
+!! end
+
+!! test
+Double RFC
+!! input
+RFC RFC 1234
+!! result
+<p>RFC <a href="http://tools.ietf.org/html/rfc1234" class="external mw-magiclink-rfc">RFC 1234</a>
+</p>
+!! end
+
+!! test
+Double RFC with a wiki link
+!! input
+RFC [[RFC 1234]]
+!! result
+<p>RFC <a href="/index.php?title=RFC_1234&action=edit&redlink=1" class="new" title="RFC 1234 (page does not exist)">RFC 1234</a>
+</p>
+!! end
+
+!! test
+RFC code coverage
+!! input
+RFC 983 987
+!! result
+<p><a href="http://tools.ietf.org/html/rfc983" class="external mw-magiclink-rfc">RFC 983</a> 987
+</p>
+!! end
+
+!! test
+Centre-aligned image
+!! input
+[[Image:foobar.jpg|centre]]
+!! result
+<div class="center"><div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div></div>
+
+!!end
+
+!! test
+None-aligned image
+!! input
+[[Image:foobar.jpg|none]]
+!! result
+<div class="floatnone"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="1941" height="220" /></a></div>
+
+!!end
+
+!! test
+Width + Height sized image (using px) (height is ignored)
+!! input
+[[Image:foobar.jpg|640x480px]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="640" height="73" /></a>
+</p>
+!!end
+
+!! test
+Width-sized image (using px, no following whitespace)
+!! input
+[[Image:foobar.jpg|640px]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="640" height="73" /></a>
+</p>
+!!end
+
+!! test
+Width-sized image (using px, with following whitespace - test regression from r39467)
+!! input
+[[Image:foobar.jpg|640px ]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="640" height="73" /></a>
+</p>
+!!end
+
+!! test
+Width-sized image (using px, with preceding whitespace - test regression from r39467)
+!! input
+[[Image:foobar.jpg| 640px]]
+!! result
+<p><a href="/wiki/File:Foobar.jpg" class="image"><img alt="Foobar.jpg" src="http://example.com/images/3/3a/Foobar.jpg" width="640" height="73" /></a>
+</p>
+!!end
+
+!! test
+Another italics / bold test
+!! input
+ ''' ''x'
+!! result
+<pre>'<i> </i>x'
+</pre>
+!!end
+
+# Note the results may be incorrect, as parserTest output included this:
+# XML error: Mismatched tag at byte 6120:
+# ...<dd> </dt></dl> </dd...
+!! test
+dt/dd/dl test
+!! options
+disabled
+!! input
+:;;;::
+!! result
+<dl><dd><dl><dt><dl><dt><dl><dt><dl><dd><dl><dd>
+</dd></dl>
+</dd></dl>
+</dt></dl>
+</dt></dl>
+</dt></dl>
+</dd></dl>
+
+!!end
+
+
+# Images with the "|" character in external URLs in comment tags; Eats half the comment, leaves unmatched "</a>" tag.
+!! test
+Images with the "|" character in the comment
+!! input
+[[image:Foobar.jpg|thumb|An [http://test/?param1=|left|¶m2=|x external] URL]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>An <a href="http://test/?param1=%7Cleft%7C&param2=%7Cx" class="external text" rel="nofollow">external</a> URL</div></div></div>
+
+!!end
+
+!! test
+[Before] HTML without raw HTML enabled ($wgRawHtml==false)
+!! input
+<html><script>alert(1);</script></html>
+!! result
+<p><html><script>alert(1);</script></html>
+</p>
+!! end
+
+!! test
+HTML with raw HTML ($wgRawHtml==true)
+!! options
+rawhtml
+!! input
+<html><script>alert(1);</script></html>
+!! result
+<p><script>alert(1);</script>
+</p>
+!! end
+
+!! test
+Parents of subpages, one level up
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! input
+[[../|L2]]
+!! result
+<p><a href="/index.php?title=Subpage_test/L1/L2&action=edit&redlink=1" class="new" title="Subpage test/L1/L2 (page does not exist)">L2</a>
+</p>
+!! end
+
+
+!! test
+Parents of subpages, one level up, not named
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! input
+[[../]]
+!! result
+<p><a href="/index.php?title=Subpage_test/L1/L2&action=edit&redlink=1" class="new" title="Subpage test/L1/L2 (page does not exist)">Subpage test/L1/L2</a>
+</p>
+!! end
+
+
+
+!! test
+Parents of subpages, two levels up
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! input
+[[../../|L1]]2
+
+[[../../|L1]]l
+!! result
+<p><a href="/index.php?title=Subpage_test/L1&action=edit&redlink=1" class="new" title="Subpage test/L1 (page does not exist)">L1</a>2
+</p><p><a href="/index.php?title=Subpage_test/L1&action=edit&redlink=1" class="new" title="Subpage test/L1 (page does not exist)">L1l</a>
+</p>
+!! end
+
+!! test
+Parents of subpages, two levels up, without trailing slash or name.
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! input
+[[../..]]
+!! result
+<p>[[../..]]
+</p>
+!! end
+
+!! test
+Parents of subpages, two levels up, with lots of extra trailing slashes.
+!! options
+subpage title=[[Subpage test/L1/L2/L3]]
+!! input
+[[../../////]]
+!! result
+<p><a href="/index.php?title=Subpage_test/L1////&action=edit&redlink=1" class="new" title="Subpage test/L1//// (page does not exist)">///</a>
+</p>
+!! end
+
+!! test
+Definition list code coverage
+!! input
+; title : def
+; title : def
+;title: def
+!! result
+<dl><dt> title  </dt><dd> def
+</dd><dt> title </dt><dd> def
+</dd><dt>title</dt><dd> def
+</dd></dl>
+
+!! end
+
+!! test
+Don't fall for the self-closing div
+!! input
+<div>hello world</div/>
+!! result
+<div>hello world</div>
+
+!! end
+
+!! test
+MSGNW magic word
+!! input
+{{MSGNW:msg}}
+!! result
+<p>[[:Template:Msg]]
+</p>
+!! end
+
+!! test
+RAW magic word
+!! input
+{{RAW:QUERTY}}
+!! result
+<p><a href="/index.php?title=Template:QUERTY&action=edit&redlink=1" class="new" title="Template:QUERTY (page does not exist)">Template:QUERTY</a>
+</p>
+!! end
+
+# This isn't needed for XHTML conformance, but would be handy as a fallback security measure
+!! test
+Always escape literal '>' in output, not just after '<'
+!! input
+><>
+!! result
+<p>><>
+</p>
+!! end
+
+!! test
+Template caching
+!! input
+{{Test}}
+{{Test}}
+!! result
+<p>This is a test template
+This is a test template
+</p>
+!! end
+
+
+!! article
+MediaWiki:Fake
+!! text
+==header==
+!! endarticle
+
+!! test
+Inclusion of !userCanEdit() content
+!! input
+{{MediaWiki:Fake}}
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=MediaWiki:Fake&action=edit&section=T-1" title="MediaWiki:Fake">edit</a>]</span> <span class="mw-headline" id="header">header</span></h2>
+
+!! end
+
+
+!! test
+Out-of-order TOC heading levels
+!! input
+==2==
+======6======
+===3===
+=1=
+=====5=====
+==2==
+!! result
+<table id="toc" class="toc"><tr><td><div id="toctitle"><h2>Contents</h2></div>
+<ul>
+<li class="toclevel-1 tocsection-1"><a href="#2"><span class="tocnumber">1</span> <span class="toctext">2</span></a>
+<ul>
+<li class="toclevel-2 tocsection-2"><a href="#6"><span class="tocnumber">1.1</span> <span class="toctext">6</span></a></li>
+<li class="toclevel-2 tocsection-3"><a href="#3"><span class="tocnumber">1.2</span> <span class="toctext">3</span></a></li>
+</ul>
+</li>
+<li class="toclevel-1 tocsection-4"><a href="#1"><span class="tocnumber">2</span> <span class="toctext">1</span></a>
+<ul>
+<li class="toclevel-2 tocsection-5"><a href="#5"><span class="tocnumber">2.1</span> <span class="toctext">5</span></a></li>
+<li class="toclevel-2 tocsection-6"><a href="#2_2"><span class="tocnumber">2.2</span> <span class="toctext">2</span></a></li>
+</ul>
+</li>
+</ul>
+</td></tr></table>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: 2">edit</a>]</span> <span class="mw-headline" id="2">2</span></h2>
+<h6><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=2" title="Edit section: 6">edit</a>]</span> <span class="mw-headline" id="6">6</span></h6>
+<h3><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=3" title="Edit section: 3">edit</a>]</span> <span class="mw-headline" id="3">3</span></h3>
+<h1><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=4" title="Edit section: 1">edit</a>]</span> <span class="mw-headline" id="1">1</span></h1>
+<h5><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=5" title="Edit section: 5">edit</a>]</span> <span class="mw-headline" id="5">5</span></h5>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=6" title="Edit section: 2">edit</a>]</span> <span class="mw-headline" id="2_2">2</span></h2>
+
+!! end
+
+
+!! test
+ISBN with a dummy number
+!! input
+ISBN ---
+!! result
+<p>ISBN ---
+</p>
+!! end
+
+
+!! test
+ISBN with space-delimited number
+!! input
+ISBN 92 9017 032 8
+!! result
+<p><a href="/wiki/Special:BookSources/9290170328" class="internal mw-magiclink-isbn">ISBN 92 9017 032 8</a>
+</p>
+!! end
+
+
+!! test
+ISBN with multiple spaces, no number
+!! input
+ISBN foo
+!! result
+<p>ISBN foo
+</p>
+!! end
+
+
+!! test
+ISBN length
+!! input
+ISBN 123456789
+
+ISBN 1234567890
+
+ISBN 12345678901
+!! result
+<p>ISBN 123456789
+</p><p><a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1234567890</a>
+</p><p>ISBN 12345678901
+</p>
+!! end
+
+
+!! test
+ISBN with trailing year (bug 8110)
+!! input
+ISBN 1-234-56789-0 - 2006
+
+ISBN 1 234 56789 0 - 2006
+!! result
+<p><a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1-234-56789-0</a> - 2006
+</p><p><a href="/wiki/Special:BookSources/1234567890" class="internal mw-magiclink-isbn">ISBN 1 234 56789 0</a> - 2006
+</p>
+!! end
+
+
+!! test
+anchorencode
+!! input
+{{anchorencode:foo bar©#%n}}
+!! result
+<p>foo_bar.C2.A9.23.25n
+</p>
+!! end
+
+!! test
+anchorencode trims spaces
+!! input
+{{anchorencode: __pretty__please__}}
+!! result
+<p>pretty_please
+</p>
+!! end
+
+!! test
+anchorencode deals with links
+!! input
+{{anchorencode: [[hello|world]] [[hi]]}}
+!! result
+<p>world_hi
+</p>
+!! end
+
+!! test
+anchorencode deals with templates
+!! input
+{{anchorencode: {{Foo}} }}
+!! result
+<p>FOO
+</p>
+!! end
+
+!! test
+anchorencode encodes like the TOC generator: (bug 18431)
+!! input
+=== _ +:.3A%3A&&]] ===
+{{anchorencode: _ +:.3A%3A&&]] }}
+__NOEDITSECTION__
+!! result
+<h3> <span class="mw-headline" id=".2B:.3A.253A.26.26.5D.5D"> _ +:.3A%3A&&]] </span></h3>
+<p>.2B:.3A.253A.26.26.5D.5D
+</p>
+!! end
+
+# Expected output in the following test is not necessarily expected (there
+# should probably be <p> tags inside the <blockquote> in the output) -- it's
+# only testing for well-formedness.
+!! test
+Bug 6200: blockquotes and paragraph formatting
+!! input
+<blockquote>
+foo
+</blockquote>
+
+bar
+
+ baz
+!! result
+<blockquote>
+foo
+</blockquote>
+<p>bar
+</p>
+<pre>baz
+</pre>
+!! end
+
+!! test
+Bug 8293: Use of center tag ruins paragraph formatting
+!! input
+<center>
+foo
+</center>
+
+bar
+
+ baz
+!! result
+<center>
+<p>foo
+</p>
+</center>
+<p>bar
+</p>
+<pre>baz
+</pre>
+!! end
+
+
+###
+### Language variants related tests
+###
+!! test
+Self-link in language variants
+!! options
+title=[[Dunav]] language=sr
+!! input
+Both [[Dunav]] and [[Дунав]] are names for this river.
+!! result
+<p>Both <strong class="selflink">Dunav</strong> and <strong class="selflink">Дунав</strong> are names for this river.
+</p>
+!!end
+
+
+!! test
+Link to pages in language variants
+!! options
+language=sr
+!! input
+Main Page can be written as [[Маин Паге]]
+!! result
+<p>Main Page can be written as <a href="/wiki/Main_Page" title="Main Page">Маин Паге</a>
+</p>
+!!end
+
+
+!! test
+Multiple links to pages in language variants
+!! options
+language=sr
+!! input
+[[Main Page]] can be written as [[Маин Паге]] same as [[Маин Паге]].
+!! result
+<p><a href="/wiki/Main_Page">Main Page</a> can be written as <a href="/wiki/Main_Page" title="Main Page">Маин Паге</a> same as <a href="/wiki/Main_Page" title="Main Page">Маин Паге</a>.
+</p>
+!!end
+
+
+!! test
+Simple template in language variants
+!! options
+language=sr
+!! input
+{{тест}}
+!! result
+<p>This is a test template
+</p>
+!! end
+
+
+!! test
+Template with explicit namespace in language variants
+!! options
+language=sr
+!! input
+{{Template:тест}}
+!! result
+<p>This is a test template
+</p>
+!! end
+
+
+!! test
+Basic test for template parameter in language variants
+!! options
+language=sr
+!! input
+{{парамтест|param=foo}}
+!! result
+<p>This is a test template with parameter foo
+</p>
+!! end
+
+
+!! test
+Simple category in language variants
+!! options
+language=sr cat
+!! input
+[[Category:МедиаWики Усер'с Гуиде]]
+!! result
+<a href="/wiki/%D0%9A%D0%B0%D1%82%D0%B5%D0%B3%D0%BE%D1%80%D0%B8%D1%98%D0%B0:MediaWiki_User%27s_Guide" title="Категорија:MediaWiki User's Guide">MediaWiki User's Guide</a>
+!! end
+
+
+!! test
+Stripping -{}- tags (language variants)
+!! options
+language=sr
+!! input
+Latin proverb: -{Ne nuntium necare}-
+!! result
+<p>Latin proverb: Ne nuntium necare
+</p>
+!! end
+
+
+!! test
+Prevent conversion with -{}- tags (language variants)
+!! options
+language=sr variant=sr-ec
+!! input
+Latinski: -{Ne nuntium necare}-
+!! result
+<p>Латински: Ne nuntium necare
+</p>
+!! end
+
+
+!! test
+Prevent conversion of text with -{}- tags (language variants)
+!! options
+language=sr variant=sr-ec
+!! input
+Latinski: -{Ne nuntium necare}-
+!! result
+<p>Латински: Ne nuntium necare
+</p>
+!! end
+
+
+!! test
+Prevent conversion of links with -{}- tags (language variants)
+!! options
+language=sr variant=sr-ec
+!! input
+-{[[Main Page]]}-
+!! result
+<p><a href="/wiki/Main_Page">Main Page</a>
+</p>
+!! end
+
+
+!! test
+-{}- tags within headlines (within html for parserConvert())
+!! options
+language=sr variant=sr-ec
+!! input
+== -{Naslov}- ==
+!! result
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Уреди део: Naslov">уреди</a>]</span> <span class="mw-headline" id="-.7BNaslov.7D-"> Naslov </span></h2>
+
+!! end
+
+
+!! test
+Explicit definition of language variant alternatives
+!! options
+language=zh variant=zh-tw
+!! input
+-{zh:China;zh-tw:Taiwan}-, not China
+!! result
+<p>Taiwan, not China
+</p>
+!! end
+
+
+!! test
+Explicit session-wise language variant mapping (A flag and - flag)
+!! options
+language=zh variant=zh-tw
+!! input
+Taiwan is not China.
+But -{A|zh:China;zh-tw:Taiwan}- is China,
+(This-{-|zh:China;zh-tw:Taiwan}- should be stripped!)
+and -{China}- is China.
+!! result
+<p>Taiwan is not China.
+But Taiwan is Taiwan,
+(This should be stripped!)
+and China is China.
+</p>
+!! end
+
+!! test
+Explicit session-wise language variant mapping (H flag for hide)
+!! options
+language=zh variant=zh-tw
+!! input
+(This-{H|zh:China;zh-tw:Taiwan}- should be stripped!)
+Taiwan is China.
+!! result
+<p>(This should be stripped!)
+Taiwan is Taiwan.
+</p>
+!! end
+
+!! test
+Adding explicit conversion rule for title (T flag)
+!! options
+language=zh variant=zh-tw showtitle
+!! input
+Should be stripped-{T|zh:China;zh-tw:Taiwan}-!
+!! result
+Taiwan
+<p>Should be stripped!
+</p>
+!! end
+
+!! test
+Testing that changing the language variant here in the tests actually works
+!! options
+language=zh variant=zh showtitle
+!! input
+Should be stripped-{T|zh:China;zh-tw:Taiwan}-!
+!! result
+China
+<p>Should be stripped!
+</p>
+!! end
+
+!! test
+Bug 24072: more test on conversion rule for title
+!! options
+language=zh variant=zh-tw showtitle
+!! input
+This should be stripped-{T|zh:China;zh-tw:Taiwan}-!
+This won't take interferes with the title rule-{H|zh:Beijing;zh-tw:Taipei}-.
+!! result
+Taiwan
+<p>This should be stripped!
+This won't take interferes with the title rule.
+</p>
+!! end
+
+!! test
+Raw output of variant escape tags (R flag)
+!! options
+language=zh variant=zh-tw
+!! input
+Raw: -{R|zh:China;zh-tw:Taiwan}-
+!! result
+<p>Raw: zh:China;zh-tw:Taiwan
+</p>
+!! end
+
+!! test
+Nested using of manual convert syntax
+!! options
+language=zh variant=zh-hk
+!! input
+Nested: -{zh-hans:Hi -{zh-cn:China;zh-sg:Singapore;}-;zh-hant:Hello -{zh-tw:Taiwan;zh-hk:H-{ong}- K-{}-ong;}-;}-!
+!! result
+<p>Nested: Hello Hong Kong!
+</p>
+!! end
+
+!! test
+Do not convert roman numbers to language variants
+!! options
+language=sr variant=sr-ec
+!! input
+Fridrih IV je car.
+!! result
+<p>Фридрих IV је цар.
+</p>
+!! end
+
+!! test
+Unclosed language converter markup "-{"
+!! options
+language=sr
+!! input
+-{T|hello
+!! result
+<p>-{T|hello
+</p>
+!! end
+
+!! test
+Don't convert raw rule "-{R|=>}-" to "=>"
+!! options
+language=sr
+!! input
+-{R|=>}-
+!! result
+<p>=>
+</p>
+!!end
+
+!!article
+Template:Bullet
+!!text
+* Bar
+!!endarticle
+
+!! test
+Bug 529: Uncovered bullet
+!! input
+* Foo {{bullet}}
+!! result
+<ul><li> Foo
+</li><li> Bar
+</li></ul>
+
+!! end
+
+!! test
+Bug 529: Uncovered table already at line-start
+!! input
+x
+
+{{table}}
+y
+!! result
+<p>x
+</p>
+<table>
+<tr>
+<td> 1 </td>
+<td> 2
+</td></tr>
+<tr>
+<td> 3 </td>
+<td> 4
+</td></tr></table>
+<p>y
+</p>
+!! end
+
+!! test
+Bug 529: Uncovered bullet in parser function result
+!! input
+* Foo {{lc:{{bullet}} }}
+!! result
+<ul><li> Foo
+</li><li> bar
+</li></ul>
+
+!! end
+
+!! test
+Bug 5678: Double-parsed template argument
+!! input
+{{lc:{{{1}}}|hello}}
+!! result
+<p>{{{1}}}
+</p>
+!! end
+
+!! test
+Bug 5678: Double-parsed template invocation
+!! input
+{{lc:{{paramtest {{!}} param = hello }} }}
+!! result
+<p>{{paramtest | param = hello }}
+</p>
+!! end
+
+!! test
+Case insensitivity of parser functions for non-ASCII characters (bug 8143)
+!! options
+language=cs
+title=[[Main Page]]
+!! input
+{{PRVNÍVELKÉ:ěščř}}
+{{prvnívelké:ěščř}}
+{{PRVNÍMALÉ:ěščř}}
+{{prvnímalé:ěščř}}
+{{MALÁ:ěščř}}
+{{malá:ěščř}}
+{{VELKÁ:ěščř}}
+{{velká:ěščř}}
+!! result
+<p>Ěščř
+Ěščř
+ěščř
+ěščř
+ěščř
+ěščř
+ĚŠČŘ
+ĚŠČŘ
+</p>
+!! end
+
+!! test
+Morwen/13: Unclosed link followed by heading
+!! input
+[[link
+==heading==
+!! result
+<p>[[link
+</p>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: heading">edit</a>]</span> <span class="mw-headline" id="heading">heading</span></h2>
+
+!! end
+
+!! test
+HHP2.1: Heuristics for headings in preprocessor parenthetical structures
+!! input
+{{foo|
+=heading=
+!! result
+<p>{{foo|
+</p>
+<h1> <span class="mw-headline" id="heading">heading</span></h1>
+
+!! end
+
+!! test
+HHP2.2: Heuristics for headings in preprocessor parenthetical structures
+!! input
+{{foo|
+==heading==
+!! result
+<p>{{foo|
+</p>
+<h2><span class="editsection">[<a href="/index.php?title=Parser_test&action=edit&section=1" title="Edit section: heading">edit</a>]</span> <span class="mw-headline" id="heading">heading</span></h2>
+
+!! end
+
+!! test
+Tildes in comments
+!! options
+pst
+!! input
+<!-- ~~~~ -->
+!! result
+<!-- ~~~~ -->
+!! end
+
+!! test
+Paragraphs inside divs (no extra line breaks)
+!! input
+<div>Line one
+
+Line two</div>
+!! result
+<div>Line one
+Line two</div>
+
+!! end
+
+!! test
+Paragraphs inside divs (extra line break on open)
+!! input
+<div>
+Line one
+
+Line two</div>
+!! result
+<div>
+<p>Line one
+</p>
+Line two</div>
+
+!! end
+
+!! test
+Paragraphs inside divs (extra line break on close)
+!! input
+<div>Line one
+
+Line two
+</div>
+!! result
+<div>Line one
+<p>Line two
+</p>
+</div>
+
+!! end
+
+!! test
+Paragraphs inside divs (extra line break on open and close)
+!! input
+<div>
+Line one
+
+Line two
+</div>
+!! result
+<div>
+<p>Line one
+</p><p>Line two
+</p>
+</div>
+
+!! end
+
+!! test
+Nesting tags, paragraphs on lines which begin with <div>
+!! options
+disabled
+!! input
+<div></div><strong>A
+B</strong>
+!! result
+<div></div>
+<p><strong>A
+B</strong>
+</p>
+!! end
+
+# Bug 6200: <blockquote> should behave like <div> with respect to line breaks
+!! test
+Bug 6200: paragraphs inside blockquotes (no extra line breaks)
+!! options
+disabled
+!! input
+<blockquote>Line one
+
+Line two</blockquote>
+!! result
+<blockquote>Line one
+Line two</blockquote>
+
+!! end
+
+!! test
+Bug 6200: paragraphs inside blockquotes (extra line break on open)
+!! options
+disabled
+!! input
+<blockquote>
+Line one
+
+Line two</blockquote>
+!! result
+<blockquote>
+<p>Line one
+</p>
+Line two</blockquote>
+
+!! end
+
+!! test
+Bug 6200: paragraphs inside blockquotes (extra line break on close)
+!! options
+disabled
+!! input
+<blockquote>Line one
+
+Line two
+</blockquote>
+!! result
+<blockquote>Line one
+<p>Line two
+</p>
+</blockquote>
+
+!! end
+
+!! test
+Bug 6200: paragraphs inside blockquotes (extra line break on open and close)
+!! options
+disabled
+!! input
+<blockquote>
+Line one
+
+Line two
+</blockquote>
+!! result
+<blockquote>
+<p>Line one
+</p><p>Line two
+</p>
+</blockquote>
+
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (no extra line breaks)
+!! input
+<blockquote><div>Line one
+
+Line two</div></blockquote>
+!! result
+<blockquote><div>Line one
+Line two</div></blockquote>
+
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (extra line break on open)
+!! input
+<blockquote><div>
+Line one
+
+Line two</div></blockquote>
+!! result
+<blockquote><div>
+<p>Line one
+</p>
+Line two</div></blockquote>
+
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (extra line break on close)
+!! input
+<blockquote><div>Line one
+
+Line two
+</div></blockquote>
+!! result
+<blockquote><div>Line one
+<p>Line two
+</p>
+</div></blockquote>
+
+!! end
+
+!! test
+Paragraphs inside blockquotes/divs (extra line break on open and close)
+!! input
+<blockquote><div>
+Line one
+
+Line two
+</div></blockquote>
+!! result
+<blockquote><div>
+<p>Line one
+</p><p>Line two
+</p>
+</div></blockquote>
+
+!! end
+
+!! test
+Interwiki links trounced by replaceExternalLinks after early LinkHolderArray expansion
+!! options
+wgLinkHolderBatchSize=0
+!! input
+[[meatball:1]]
+[[meatball:2]]
+[[meatball:3]]
+!! result
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?1" class="extiw">meatball:1</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?2" class="extiw">meatball:2</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?3" class="extiw">meatball:3</a>
+</p>
+!! end
+
+!! test
+Free external link invading image caption
+!! input
+[[Image:Foobar.jpg|thumb|http://x|hello]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/File:Foobar.jpg" class="image"><img alt="" src="http://example.com/images/3/3a/Foobar.jpg" width="180" height="20" class="thumbimage" /></a> <div class="thumbcaption"><div class="magnify"><a href="/wiki/File:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>hello</div></div></div>
+
+!! end
+
+!! test
+Bug 15196: localised external link numbers
+!! options
+language=fa
+!! input
+[http://en.wikipedia.org/]
+!! result
+<p><a href="http://en.wikipedia.org/" class="external autonumber" rel="nofollow">[۱]</a>
+</p>
+!! end
+
+!! test
+Multibyte character in padleft
+!! input
+{{padleft:-Hello|7|Æ}}
+!! result
+<p>Æ-Hello
+</p>
+!! end
+
+!! test
+Multibyte character in padright
+!! input
+{{padright:Hello-|7|Æ}}
+!! result
+<p>Hello-Æ
+</p>
+!! end
+
+!! test
+Formatted date
+!! config
+wgUseDynamicDates=1
+!! input
+[[2009-03-24]]
+!! result
+<p><span class="mw-formatted-date" title="2009-03-24"><a href="/index.php?title=2009&action=edit&redlink=1" class="new" title="2009 (page does not exist)">2009</a>-<a href="/index.php?title=March_24&action=edit&redlink=1" class="new" title="March 24 (page does not exist)">03-24</a></span>
+</p>
+!!end
+
+!!test
+formatdate parser function
+!!input
+{{#formatdate:2009-03-24}}
+!! result
+<p><span class="mw-formatted-date" title="2009-03-24">2009-03-24</span>
+</p>
+!! end
+
+!!test
+formatdate parser function, with default format
+!!input
+{{#formatdate:2009-03-24|mdy}}
+!! result
+<p><span class="mw-formatted-date" title="2009-03-24">March 24, 2009</span>
+</p>
+!! end
+
+!! test
+Linked date with autoformatting disabled
+!! config
+wgUseDynamicDates=false
+!! input
+[[2009-03-24]]
+!! result
+<p><a href="/index.php?title=2009-03-24&action=edit&redlink=1" class="new" title="2009-03-24 (page does not exist)">2009-03-24</a>
+</p>
+!! end
+
+!! test
+Spacing of numbers in formatted dates
+!! input
+{{#formatdate:January 15}}
+!! result
+<p><span class="mw-formatted-date" title="01-15">January 15</span>
+</p>
+!! end
+
+!! test
+Spacing of numbers in formatted dates (linked)
+!! config
+wgUseDynamicDates=true
+!! input
+[[January 15]]
+!! result
+<p><span class="mw-formatted-date" title="01-15"><a href="/index.php?title=January_15&action=edit&redlink=1" class="new" title="January 15 (page does not exist)">January 15</a></span>
+</p>
+!! end
+
+#
+#
+#
+
+#
+# Edit comments
+#
+
+!! test
+Edit comment with link
+!! options
+comment
+!! input
+I like the [[Main Page]] a lot
+!! result
+I like the <a href="/wiki/Main_Page">Main Page</a> a lot
+!!end
+
+!! test
+Edit comment with link and link text
+!! options
+comment
+!! input
+I like the [[Main Page|best pages]] a lot
+!! result
+I like the <a href="/wiki/Main_Page" title="Main Page">best pages</a> a lot
+!!end
+
+!! test
+Edit comment with link and link text with suffix
+!! options
+comment
+!! input
+I like the [[Main Page|best page]]s a lot
+!! result
+I like the <a href="/wiki/Main_Page" title="Main Page">best pages</a> a lot
+!!end
+
+!! test
+Edit comment with section link (non-local, eg in history list)
+!! options
+comment title=[[Main Page]]
+!! input
+/* External links */ removed bogus entries
+!! result
+<span class="autocomment"><a href="/wiki/Main_Page#External_links" title="Main Page">→</a>External links: </span> removed bogus entries
+!!end
+
+!! test
+Edit comment with section link (local, eg in diff view)
+!! options
+comment local title=[[Main Page]]
+!! input
+/* External links */ removed bogus entries
+!! result
+<span class="autocomment"><a href="#External_links">→</a>External links: </span> removed bogus entries
+!!end
+
+!! test
+Edit comment with subpage link (bug 14080)
+!! options
+comment
+subpage
+title=[[Subpage test]]
+!! input
+Poked at a [[/subpage]] here...
+!! result
+Poked at a <a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">/subpage</a> here...
+!!end
+
+!! test
+Edit comment with subpage link and link text (bug 14080)
+!! options
+comment
+subpage
+title=[[Subpage test]]
+!! input
+Poked at a [[/subpage|neat little page]] here...
+!! result
+Poked at a <a href="/wiki/Subpage_test/subpage" title="Subpage test/subpage">neat little page</a> here...
+!!end
+
+!! test
+Edit comment with bogus subpage link in non-subpage NS (bug 14080)
+!! options
+comment
+title=[[Subpage test]]
+!! input
+Poked at a [[/subpage]] here...
+!! result
+Poked at a <a href="/index.php?title=/subpage&action=edit&redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a> here...
+!!end
+
+!! test
+Edit comment with bare anchor link (local, as on diff)
+!! options
+comment
+local
+title=[[Main Page]]
+!!input
+[[#section]]
+!! result
+<a href="#section">#section</a>
+!! end
+
+!! test
+Edit comment with bare anchor link (non-local, as on history)
+!! options
+comment
+title=[[Main Page]]
+!!input
+[[#section]]
+!! result
+<a href="/wiki/Main_Page#section" title="Main Page">#section</a>
+!! end
+
+!! test
+Space normalisation on autocomment (bug 22784)
+!! options
+comment
+title=[[Main Page]]
+!!input
+/* __hello__world__ */
+!! result
+<span class="autocomment"><a href="/wiki/Main_Page#hello_world" title="Main Page">→</a>__hello__world__</span>
+!! end
+
+!! test
+Bad images - basic functionality
+!! input
+[[File:Bad.jpg]]
+!! result
+!! end
+
+!! test
+Bad images - bug 16039: text after bad image disappears
+!! input
+Foo bar
+[[File:Bad.jpg]]
+Bar foo
+!! result
+<p>Foo bar
+</p><p>Bar foo
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) no displaytitle
+!! options
+showtitle
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=false
+!! input
+this is not the the title
+!! result
+Parser test
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) RestrictDisplayTitle=false
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=false
+!! input
+this is not the the title
+{{DISPLAYTITLE:whatever}}
+!! result
+whatever
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) RestrictDisplayTitle=true mismatch
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=true
+!! input
+this is not the the title
+{{DISPLAYTITLE:whatever}}
+!! result
+Screen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) RestrictDisplayTitle=true matching
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=true
+wgRestrictDisplayTitle=true
+!! input
+this is not the the title
+{{DISPLAYTITLE:screen}}
+!! result
+screen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) AllowDisplayTitle=false
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=false
+!! input
+this is not the the title
+{{DISPLAYTITLE:screen}}
+!! result
+Screen
+<p>this is not the the title
+<a href="/index.php?title=Template:DISPLAYTITLE:screen&action=edit&redlink=1" class="new" title="Template:DISPLAYTITLE:screen (page does not exist)">Template:DISPLAYTITLE:screen</a>
+</p>
+!! end
+
+!! test
+Verify that displaytitle works (bug #22501) AllowDisplayTitle=false no DISPLAYTITLE
+!! options
+showtitle
+title=[[Screen]]
+!! config
+wgAllowDisplayTitle=false
+!! input
+this is not the the title
+!! result
+Screen
+<p>this is not the the title
+</p>
+!! end
+
+!! test
+preload: check <noinclude> and <includeonly>
+!! options
+preload
+!! input
+Hello <noinclude>cruel</noinclude><includeonly>kind</includeonly> world.
+!! result
+Hello kind world.
+!! end
+
+!! test
+preload: check <onlyinclude>
+!! options
+preload
+!! input
+Goodbye <onlyinclude>Hello world</onlyinclude>
+!! result
+Hello world
+!! end
+
+!! test
+preload: can pass tags through if we want to
+!! options
+preload
+!! input
+<includeonly><</includeonly>includeonly>Hello world<includeonly><</includeonly>/includeonly>
+!! result
+<includeonly>Hello world</includeonly>
+!! end
+
+!! test
+preload: check that it doesn't try to do tricks
+!! options
+preload
+!! input
+* <!-- Hello --> ''{{world}}'' {{<includeonly>subst:</includeonly>How are you}}{{ {{{|safesubst:}}} #if:1|2|3}}
+!! result
+* <!-- Hello --> ''{{world}}'' {{subst:How are you}}{{ {{{|safesubst:}}} #if:1|2|3}}
+!! end
+
+!! test
+Play a bit with r67090 and bug 3158
+!! options
+disabled
+!! input
+<div style="width:50% !important"> </div>
+<div style="width:50% !important"> </div>
+<div style="width:50% !important"> </div>
+<div style="border : solid;"> </div>
+!! result
+<div style="width:50% !important"> </div>
+<div style="width:50% !important"> </div>
+<div style="width:50% !important"> </div>
+<div style="border : solid;"> </div>
+
+!! end
+
+!! test
+HTML5 data attributes
+!! input
+<span data-foo="bar">Baz</span>
+<p data-abc-def_hij="">Quuz</p>
+!! result
+<p><span data-foo="bar">Baz</span>
+</p>
+<p data-abc-def_hij="">Quuz</p>
+
+!! end
+
+
+TODO:
+more images
+more tables
+math
+character entities
+and much more
+Try for 100% code coverage
--- /dev/null
+<?php
+/**
+ * A basic extension that's used by the parser tests to test whether input and
+ * arguments are passed to extensions properly.
+ *
+ * Copyright © 2005, 2006 Ævar Arnfjörð Bjarmason
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ */
+
+class ParserTestParserHook {
+
+ static function setup( &$parser ) {
+ $parser->setHook( 'tag', array( __CLASS__, 'hook' ) );
+
+ return true;
+ }
+
+ static function hook( $in, $argv ) {
+ ob_start();
+ var_dump(
+ $in,
+ $argv
+ );
+ $ret = ob_get_clean();
+
+ return "<pre>\n$ret</pre>";
+ }
+}
--- /dev/null
+<?php
+/**
+ * A basic extension that's used by the parser tests to test whether the parser
+ * calls extensions when they're called inside comments, it shouldn't do that
+ *
+ * Copyright © 2005, 2006 Ævar Arnfjörð Bjarmason
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Testing
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ */
+
+class ParserTestStaticParserHook {
+ static function setup( &$parser ) {
+ $parser->setHook( 'statictag', array( __CLASS__, 'hook' ) );
+
+ return true;
+ }
+
+ static function hook( $in, $argv, $parser ) {
+ if ( ! count( $argv ) ) {
+ $parser->static_tag_buf = $in;
+ return '';
+ } else if ( count( $argv ) === 1 && isset( $argv['action'] )
+ && $argv['action'] === 'flush' && $in === null )
+ {
+ // Clear the buffer, we probably don't need to
+ if ( isset( $parser->static_tag_buf ) ) {
+ $tmp = $parser->static_tag_buf;
+ } else {
+ $tmp = '';
+ }
+ $parser->static_tag_buf = null;
+ return $tmp;
+ } else
+ // wtf?
+ return
+ "\nCall this extension as <statictag>string</statictag> or as" .
+ " <statictag action=flush/>, not in any other way.\n" .
+ "text: " . var_export( $in, true ) . "\n" .
+ "argv: " . var_export( $argv, true ) . "\n";
+ }
+}
--- /dev/null
+.PHONY: help test phpunit install coverage warning destructive parser noparser safe databaseless list-groups
+.DEFAULT: warning
+
+SHELL = /bin/sh
+CONFIG_FILE = $(shell pwd)/suite.xml
+PHP = php
+PU = ${PHP} phpunit.php --configuration ${CONFIG_FILE} ${FLAGS}
+
+all test: warning
+
+warning:
+ # Use 'make help' to get usage
+ @echo "WARNING -- some tests are DESTRUCTIVE and will alter your wiki."
+ @echo "DO NOT RUN THESE TESTS on a production wiki."
+ @echo ""
+ @echo "Until the default suites are made non-destructive, you can run"
+ @echo "the destructive tests like so:"
+ @echo " make destructive"
+ @echo ""
+ @echo "Some tests are expected to be safe, you can run them with"
+ @echo " make safe"
+ @echo ""
+ @echo "You are recommended to run them with read-only credentials, though."
+ @echo ""
+ @echo "If you don't have a database running, you can still run"
+ @echo " make databaseless"
+ @echo ""
+
+destructive: phpunit
+
+phpunit:
+ ${PU}
+
+install:
+ php install-phpunit.sh
+
+tap:
+ ${PU} --tap
+
+coverage:
+ ${PU} --coverage-html ../../../docs/code-coverage
+
+parser:
+ ${PU} --group Parser
+
+noparser:
+ ${PU} --exclude-group Parser,Broken
+
+safe:
+ ${PU} --exclude-group Broken,Destructive
+
+databaseless:
+ ${PU} --exclude-group Broken,Destructive,Database
+
+list-groups:
+ ${PU} --list-groups
+
+help:
+ # Usage:
+ # make <target> [OPTION=value]
+ #
+ # Targets:
+ # phpunit (default) Run all the tests with phpunit
+ # install Install PHPUnit from phpunit.de
+ # tap Run the tests individually through Test::Harness's prove(1)
+ # help You're looking at it!
+ # coverage Run the tests and generates an HTML code coverage report
+ # You will need the Xdebug PHP extension for the later.
+ # [no]parser Skip or only run Parser tests
+ #
+ # list-groups List availabe Tests groups.
+ #
+ # Options:
+ # CONFIG_FILE Path to a PHPUnit configuration file (default: suite.xml)
+ # FLAGS Additional flags to pass to PHPUnit
+ # PHP Path to php
--- /dev/null
+== MediaWiki PHPUnit Tests ==
+
+Some quickie unit tests done with the PHPUnit testing framework. To run the
+test suite, run 'make test' in this (maintenance/tests/phpunit) directory.
+
+=== WARNING ===
+
+The current versions of some of these tests are DESTRUCTIVE AND WILL ALTER
+YOUR WIKI'S CONTENTS. DO NOT RUN ON A PRODUCTION SYSTEM OR ONE WHERE YOU
+NEED TO RETAIN YOUR DATA.
+
+=== Installation ===
+
+PHPUnit is no longer maintained by PEAR. To get the current version of
+PHPUnit, first uninstall any old version of PHPUnit or PHPUnit2 from PEAR,
+then install the current version from phpunit.de like this:
+
+ pear channel-discover pear.phpunit.de
+ pear install phpunit/PHPUnit
+
+You also may wish to install this via your normal package mechanism:
+
+ aptitude install phpunit
+- or -
+ yum install phpunit
+
+=== Notes ===
+
+* Label currently broken tests in the group Broken and they will not be run
+by phpunit. You can add them to the group by putting the following comment at
+the top of the file:
+ /**
+ * @group Broken
+ */
+* Need to fix some broken tests
--- /dev/null
+== Things To Do ==
+
+* DEFAULT TESTS NEED TO MADE NON-DESTRUCTIVE. Any destructive tests which alter the contents of the live wiki need to
+be protected with an explicit confirmation so people exploring their system don't accidentally destroy their main page
+or register user accounts with default passwords.
+
+* Most of the tests are named poorly; naming should describe a use case in story-like language, not simply identify the
+unit under test. An example would be the difference between testCalculate and testAddingIntegersTogetherWorks.
+* Many of the tests make multiple assertions, and are thus not unitary tests. By using data-providers and more use-case
+oriented test selection nearly all of these cases can be easily resolved.
+* Some of the test files are either incorrectly named or in the wrong folder. Tests should be organized in a mirrored
+structure to the source they are testing, and named the same, with the exception of the word "Test" at the end.
+* Shared set-up code or base classes are present, but usually named improperly or appear to be poorly factored. Support
+code should share as much of the same naming as the code it's supporting, and test and test-case depenencies should be
+considered to resolve other shared needs.
--- /dev/null
+<?php
+/**
+ * Bootstrapping for MediaWiki PHPUnit tests
+ * This file is included by phpunit and is NOT in the global scope.
+ *
+ * @file
+ */
+
+if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ echo <<<EOF
+You are running these tests directly from phpunit. You may not have all globals correctly set.
+Running phpunit.php instead is recommended.
+EOF;
+ require_once ( dirname( __FILE__ ) . "/phpunit.php" );
+}
+
+// Output a notice when running with older versions of PHPUnit
+if ( !version_compare( PHPUnit_Runner_Version::id(), "3.4.1", ">" ) ) {
+ echo <<<EOF
+********************************************************************************
+
+These tests run best with version PHPUnit 3.4.2 or better. Earlier versions may
+show failures because earlier versions of PHPUnit do not properly implement
+dependencies.
+
+********************************************************************************
+
+EOF;
+}
+
+global $wgLocalisationCacheConf, $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType;
+global $wgMessageCache, $messageMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry;
+$wgLocalisationCacheConf['storeClass'] = 'LCStore_Null';
+$wgMainCacheType = CACHE_NONE;
+$wgMessageCacheType = CACHE_NONE;
+$wgParserCacheType = CACHE_NONE;
+$wgUseDatabaseMessages = false; # Set for future resets
+
+# The message cache was already created in Setup.php
+$wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache',
+ array( $messageMemc, $wgUseDatabaseMessages, $wgMsgCacheExpiry ) );
+
+/* Classes */
+
+abstract class MediaWikiTestSetup extends PHPUnit_Framework_TestCase {
+ protected $suite;
+ public $regex = '';
+ public $runDisabled = false;
+
+ function __construct( PHPUnit_Framework_TestSuite $suite = null ) {
+ if ( null !== $suite ) {
+ $this->suite = $suite;
+ }
+ }
+
+ function __call( $func, $args ) {
+ if ( method_exists( $this->suite, $func ) ) {
+ return call_user_func_array( array( $this->suite, $func ), $args);
+ } else {
+ throw new MWException( "Called non-existant $func method on "
+ . get_class( $this ) );
+ }
+ }
+}
+
--- /dev/null
+<?php
+
+/**
+ * Test the CDB reader/writer
+ */
+
+class CdbTest extends PHPUnit_Framework_TestCase {
+
+ public function setUp() {
+ if ( !CdbReader::haveExtension() ) {
+ $this->markTestIncomplete( 'This test requires native CDB support to be present.' );
+ }
+ }
+
+ public function testCdb() {
+ $dir = wfTempDir();
+ if ( !is_writable( $dir ) ) {
+ $this->markTestSkipped( "Temp dir isn't writable" );
+ }
+
+ $w1 = new CdbWriter_PHP( "$dir/php.cdb" );
+ $w2 = new CdbWriter_DBA( "$dir/dba.cdb" );
+
+ $data = array();
+ for ( $i = 0; $i < 1000; $i++ ) {
+ $key = $this->randomString();
+ $value = $this->randomString();
+ $w1->set( $key, $value );
+ $w2->set( $key, $value );
+
+ if ( !isset( $data[$key] ) ) {
+ $data[$key] = $value;
+ }
+ }
+
+ $w1->close();
+ $w2->close();
+
+ $this->assertEquals(
+ md5_file( "$dir/dba.cdb" ),
+ md5_file( "$dir/php.cdb" ),
+ 'same hash'
+ );
+
+ $r1 = new CdbReader_PHP( "$dir/php.cdb" );
+ $r2 = new CdbReader_DBA( "$dir/dba.cdb" );
+
+ foreach ( $data as $key => $value ) {
+ if ( $key === '' ) {
+ // Known bug
+ continue;
+ }
+ $v1 = $r1->get( $key );
+ $v2 = $r2->get( $key );
+
+ $v1 = $v1 === false ? '(not found)' : $v1;
+ $v2 = $v2 === false ? '(not found)' : $v2;
+
+ # cdbAssert( 'Mismatch', $key, $v1, $v2 );
+ $this->cdbAssert( "PHP error", $key, $v1, $value );
+ $this->cdbAssert( "DBA error", $key, $v2, $value );
+ }
+
+ unlink( "$dir/dba.cdb" );
+ unlink( "$dir/php.cdb" );
+ }
+
+ private function randomString() {
+ $len = mt_rand( 0, 10 );
+ $s = '';
+ for ( $j = 0; $j < $len; $j++ ) {
+ $s .= chr( mt_rand( 0, 255 ) );
+ }
+ return $s;
+ }
+
+ private function cdbAssert( $msg, $key, $v1, $v2 ) {
+ $this->assertEquals(
+ $v2,
+ $v1,
+ $msg . ', k=' . bin2hex( $key )
+ );
+ }
+}
--- /dev/null
+<?php
+/**
+ * External Store tests
+ */
+
+class ExternalStoreTest extends PHPUnit_Framework_TestCase {
+ private $saved_wgExternalStores;
+
+ function setUp() {
+ global $wgExternalStores;
+ $this->saved_wgExternalStores = $wgExternalStores ;
+ }
+
+ function tearDown() {
+ global $wgExternalStores;
+ $wgExternalStores = $this->saved_wgExternalStores ;
+ }
+
+ function testExternalStoreDoesNotFetchIncorrectURL() {
+ global $wgExternalStores;
+ $wgExternalStores = true;
+
+ # Assertions for r68900
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'http://' ) );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( 'ftp.wikimedia.org' ) );
+ $this->assertFalse(
+ ExternalStore::fetchFromURL( '/super.txt' ) );
+ }
+}
+
--- /dev/null
+<?php
+/**
+ * Parser-related tests that don't suit for parserTests.txt
+ */
+
+class ExtraParserTest extends PHPUnit_Framework_TestCase {
+
+ function setUp() {
+ global $wgMemc;
+ global $wgContLang;
+ global $wgShowDBErrorBacktrace;
+
+ $wgShowDBErrorBacktrace = true;
+ if ( $wgContLang === null ) $wgContLang = new Language;
+ $wgMemc = new FakeMemCachedClient;
+ }
+
+ // Bug 8689 - Long numeric lines kill the parser
+ function testBug8689() {
+ global $wgLang;
+ global $wgUser;
+ $longLine = '1.' . str_repeat( '1234567890', 100000 ) . "\n";
+
+ if ( $wgLang === null ) $wgLang = new Language;
+ $parser = new Parser();
+ $t = Title::newFromText( 'Unit test' );
+ $options = ParserOptions::newFromUser( $wgUser );
+ $this->assertEquals( "<p>$longLine</p>",
+ $parser->parse( $longLine, $t, $options )->getText() );
+ }
+ }
--- /dev/null
+<?php
+
+class GlobalTest extends PHPUnit_Framework_TestCase {
+ function setUp() {
+ global $wgReadOnlyFile, $wgContLang, $wgLang;
+ $this->originals['wgReadOnlyFile'] = $wgReadOnlyFile;
+ $wgReadOnlyFile = tempnam( wfTempDir(), "mwtest_readonly" );
+ unlink( $wgReadOnlyFile );
+ $wgContLang = $wgLang = Language::factory( 'en' );
+ }
+
+ function tearDown() {
+ global $wgReadOnlyFile;
+ if ( file_exists( $wgReadOnlyFile ) ) {
+ unlink( $wgReadOnlyFile );
+ }
+ $wgReadOnlyFile = $this->originals['wgReadOnlyFile'];
+ }
+
+ function testRandom() {
+ # This could hypothetically fail, but it shouldn't ;)
+ $this->assertFalse(
+ wfRandom() == wfRandom() );
+ }
+
+ function testUrlencode() {
+ $this->assertEquals(
+ "%E7%89%B9%E5%88%A5:Contributions/Foobar",
+ wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) );
+ }
+
+ function testReadOnlyEmpty() {
+ global $wgReadOnly;
+ $wgReadOnly = null;
+
+ $this->assertFalse( wfReadOnly() );
+ $this->assertFalse( wfReadOnly() );
+ }
+
+ function testReadOnlySet() {
+ global $wgReadOnly, $wgReadOnlyFile;
+
+ $f = fopen( $wgReadOnlyFile, "wt" );
+ fwrite( $f, 'Message' );
+ fclose( $f );
+ $wgReadOnly = null;
+
+ $this->assertTrue( wfReadOnly() );
+ $this->assertTrue( wfReadOnly() );
+
+ unlink( $wgReadOnlyFile );
+ $wgReadOnly = null;
+
+ $this->assertFalse( wfReadOnly() );
+ $this->assertFalse( wfReadOnly() );
+ }
+
+ function testQuotedPrintable() {
+ $this->assertEquals(
+ "=?UTF-8?Q?=C4=88u=20legebla=3F?=",
+ UserMailer::quotedPrintable( "\xc4\x88u legebla?", "UTF-8" ) );
+ }
+
+ function testTime() {
+ $start = wfTime();
+ $this->assertType( 'float', $start );
+ $end = wfTime();
+ $this->assertTrue( $end > $start, "Time is running backwards!" );
+ }
+
+ function testArrayToCGI() {
+ $this->assertEquals(
+ "baz=AT%26T&foo=bar",
+ wfArrayToCGI(
+ array( 'baz' => 'AT&T', 'ignore' => '' ),
+ array( 'foo' => 'bar', 'baz' => 'overridden value' ) ) );
+ }
+
+ function testMimeTypeMatch() {
+ $this->assertEquals(
+ 'text/html',
+ mimeTypeMatch( 'text/html',
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.7,
+ 'text/plain' => 0.3 ) ) );
+ $this->assertEquals(
+ 'text/*',
+ mimeTypeMatch( 'text/html',
+ array( 'image/*' => 1.0,
+ 'text/*' => 0.5 ) ) );
+ $this->assertEquals(
+ '*/*',
+ mimeTypeMatch( 'text/html',
+ array( '*/*' => 1.0 ) ) );
+ $this->assertNull(
+ mimeTypeMatch( 'text/html',
+ array( 'image/png' => 1.0,
+ 'image/svg+xml' => 0.5 ) ) );
+ }
+
+ function testNegotiateType() {
+ $this->assertEquals(
+ 'text/html',
+ wfNegotiateType(
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.7,
+ 'text/plain' => 0.5,
+ 'text/*' => 0.2 ),
+ array( 'text/html' => 1.0 ) ) );
+ $this->assertEquals(
+ 'application/xhtml+xml',
+ wfNegotiateType(
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.7,
+ 'text/plain' => 0.5,
+ 'text/*' => 0.2 ),
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.5 ) ) );
+ $this->assertEquals(
+ 'text/html',
+ wfNegotiateType(
+ array( 'text/html' => 1.0,
+ 'text/plain' => 0.5,
+ 'text/*' => 0.5,
+ 'application/xhtml+xml' => 0.2 ),
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.5 ) ) );
+ $this->assertEquals(
+ 'text/html',
+ wfNegotiateType(
+ array( 'text/*' => 1.0,
+ 'image/*' => 0.7,
+ '*/*' => 0.3 ),
+ array( 'application/xhtml+xml' => 1.0,
+ 'text/html' => 0.5 ) ) );
+ $this->assertNull(
+ wfNegotiateType(
+ array( 'text/*' => 1.0 ),
+ array( 'application/xhtml+xml' => 1.0 ) ) );
+ }
+
+ function testTimestamp() {
+ $t = gmmktime( 12, 34, 56, 1, 15, 2001 );
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, $t ),
+ 'TS_UNIX to TS_MW' );
+ $this->assertEquals(
+ '19690115123456',
+ wfTimestamp( TS_MW, -30281104 ),
+ 'Negative TS_UNIX to TS_MW' );
+ $this->assertEquals(
+ 979562096,
+ wfTimestamp( TS_UNIX, $t ),
+ 'TS_UNIX to TS_UNIX' );
+ $this->assertEquals(
+ '2001-01-15 12:34:56',
+ wfTimestamp( TS_DB, $t ),
+ 'TS_UNIX to TS_DB' );
+ $this->assertEquals(
+ '20010115T123456Z',
+ wfTimestamp( TS_ISO_8601_BASIC, $t ),
+ 'TS_ISO_8601_BASIC to TS_DB' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, '20010115123456' ),
+ 'TS_MW to TS_MW' );
+ $this->assertEquals(
+ 979562096,
+ wfTimestamp( TS_UNIX, '20010115123456' ),
+ 'TS_MW to TS_UNIX' );
+ $this->assertEquals(
+ '2001-01-15 12:34:56',
+ wfTimestamp( TS_DB, '20010115123456' ),
+ 'TS_MW to TS_DB' );
+ $this->assertEquals(
+ '20010115T123456Z',
+ wfTimestamp( TS_ISO_8601_BASIC, '20010115123456' ),
+ 'TS_MW to TS_ISO_8601_BASIC' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_MW' );
+ $this->assertEquals(
+ 979562096,
+ wfTimestamp( TS_UNIX, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_UNIX' );
+ $this->assertEquals(
+ '2001-01-15 12:34:56',
+ wfTimestamp( TS_DB, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_DB' );
+ $this->assertEquals(
+ '20010115T123456Z',
+ wfTimestamp( TS_ISO_8601_BASIC, '2001-01-15 12:34:56' ),
+ 'TS_DB to TS_ISO_8601_BASIC' );
+
+ # rfc2822 section 3.3
+
+ $this->assertEquals(
+ 'Mon, 15 Jan 2001 12:34:56 GMT',
+ wfTimestamp( TS_RFC2822, '20010115123456' ),
+ 'TS_MW to TS_RFC2822' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, 'Mon, 15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 to TS_MW' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, ' Mon, 15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 with leading space to TS_MW' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, '15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 without optional day-of-week to TS_MW' );
+
+ # FWS = ([*WSP CRLF] 1*WSP) / obs-FWS ; Folding white space
+ # obs-FWS = 1*WSP *(CRLF 1*WSP) ; Section 4.2
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, 'Mon, 15 Jan 2001 12:34:56 GMT' ),
+ 'TS_RFC2822 to TS_MW' );
+
+ # WSP = SP / HTAB ; rfc2234
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, "Mon, 15 Jan\x092001 12:34:56 GMT" ),
+ 'TS_RFC2822 with HTAB to TS_MW' );
+
+ $this->assertEquals(
+ '20010115123456',
+ wfTimestamp( TS_MW, "Mon, 15 Jan\x09 \x09 2001 12:34:56 GMT" ),
+ 'TS_RFC2822 with HTAB and SP to TS_MW' );
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, "Sun, 6 Nov 94 08:49:37 GMT" ),
+ 'TS_RFC2822 with obsolete year to TS_MW' );
+ }
+
+ /**
+ * This test checks wfTimestamp() with values outside.
+ * It needs PHP 64 bits or PHP > 5.1.
+ * See r74778 and bug 25451
+ */
+ function testOldTimestamps() {
+ $this->assertEquals( 'Fri, 13 Dec 1901 20:45:54 GMT',
+ wfTimestamp( TS_RFC2822, '19011213204554' ),
+ 'Earliest time according to php documentation' );
+
+ $this->assertEquals( 'Tue, 19 Jan 2038 03:14:07 GMT',
+ wfTimestamp( TS_RFC2822, '20380119031407' ),
+ 'Latest 32 bit time' );
+
+ $this->assertEquals( '-2147483648',
+ wfTimestamp( TS_UNIX, '19011213204552' ),
+ 'Earliest 32 bit unix time' );
+
+ $this->assertEquals( '2147483647',
+ wfTimestamp( TS_UNIX, '20380119031407' ),
+ 'Latest 32 bit unix time' );
+
+ $this->assertEquals( 'Fri, 13 Dec 1901 20:45:52 GMT',
+ wfTimestamp( TS_RFC2822, '19011213204552' ),
+ 'Earliest 32 bit time' );
+
+ $this->assertEquals( 'Fri, 13 Dec 1901 20:45:51 GMT',
+ wfTimestamp( TS_RFC2822, '19011213204551' ),
+ 'Earliest 32 bit time - 1' );
+
+ $this->assertEquals( 'Tue, 19 Jan 2038 03:14:08 GMT',
+ wfTimestamp( TS_RFC2822, '20380119031408' ),
+ 'Latest 32 bit time + 1' );
+
+ $this->assertEquals( '19011212000000',
+ wfTimestamp(TS_MW, '19011212000000'),
+ 'Convert to itself r74778#c10645' );
+
+ $this->assertEquals( '-2147483649',
+ wfTimestamp( TS_UNIX, '19011213204551' ),
+ 'Earliest 32 bit unix time - 1' );
+
+ $this->assertEquals( '2147483648',
+ wfTimestamp( TS_UNIX, '20380119031408' ),
+ 'Latest 32 bit unix time + 1' );
+
+ $this->assertEquals( '19011213204551',
+ wfTimestamp( TS_MW, '-2147483649' ),
+ '1901 negative unix time to MediaWiki' );
+
+ $this->assertEquals( '18010115123456',
+ wfTimestamp( TS_MW, '-5331871504' ),
+ '1801 negative unix time to MediaWiki' );
+
+ $this->assertEquals( 'Tue, 09 Aug 0117 12:34:56 GMT',
+ wfTimestamp( TS_RFC2822, '0117-08-09 12:34:56'),
+ 'Death of Roman Emperor [[Trajan]]');
+
+ /* FIXME: 00 to 101 years are taken as being in [1970-2069] */
+
+ $this->assertEquals( 'Sun, 01 Jan 0101 00:00:00 GMT',
+ wfTimestamp( TS_RFC2822, '-58979923200'),
+ '1/1/101');
+
+ $this->assertEquals( 'Mon, 01 Jan 0001 00:00:00 GMT',
+ wfTimestamp( TS_RFC2822, '-62135596800'),
+ 'Year 1');
+
+ /* It is not clear if we should generate a year 0 or not
+ * We are completely off RFC2822 requirement of year being
+ * 1900 or later.
+ */
+ $this->assertEquals( 'Wed, 18 Oct 0000 00:00:00 GMT',
+ wfTimestamp( TS_RFC2822, '-62142076800'),
+ 'ISO 8601:2004 [[year 0]], also called [[1 BC]]');
+ }
+
+ function testHttpDate() {
+ # The Resource Loader uses wfTimestamp() to convert timestamps
+ # from If-Modified-Since header.
+ # Thus it must be able to parse all rfc2616 date formats
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, 'Sun, 06 Nov 1994 08:49:37 GMT' ),
+ 'RFC 822 date' );
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, 'Sunday, 06-Nov-94 08:49:37 GMT' ),
+ 'RFC 850 date' );
+
+ $this->assertEquals(
+ '19941106084937',
+ wfTimestamp( TS_MW, 'Sun Nov 6 08:49:37 1994' ),
+ "ANSI C's asctime() format" );
+
+ // See http://www.squid-cache.org/mail-archive/squid-users/200307/0122.html and r77171
+ $this->assertEquals(
+ '20101122141242',
+ wfTimestamp( TS_MW, 'Mon, 22 Nov 2010 14:12:42 GMT; length=52626' ),
+ "Netscape extension to HTTP/1.0" );
+
+ }
+
+ function testBasename() {
+ $sets = array(
+ '' => '',
+ '/' => '',
+ '\\' => '',
+ '//' => '',
+ '\\\\' => '',
+ 'a' => 'a',
+ 'aaaa' => 'aaaa',
+ '/a' => 'a',
+ '\\a' => 'a',
+ '/aaaa' => 'aaaa',
+ '\\aaaa' => 'aaaa',
+ '/aaaa/' => 'aaaa',
+ '\\aaaa\\' => 'aaaa',
+ '\\aaaa\\' => 'aaaa',
+ '/mnt/upload3/wikipedia/en/thumb/8/8b/Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg' => '93px-Zork_Grand_Inquisitor_box_cover.jpg',
+ 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE' => 'VIEWER.EXE',
+ 'Östergötland_coat_of_arms.png' => 'Östergötland_coat_of_arms.png',
+ );
+ foreach ( $sets as $from => $to ) {
+ $this->assertEquals( $to, wfBaseName( $from ),
+ "wfBaseName('$from') => '$to'" );
+ }
+ }
+
+ /* TODO: many more! */
+}
+
+
--- /dev/null
+<?php
+
+class MockCookie extends Cookie {
+ public function canServeDomain( $arg ) { return parent::canServeDomain( $arg ); }
+ public function canServePath( $arg ) { return parent::canServePath( $arg ); }
+ public function isUnExpired() { return parent::isUnExpired(); }
+}
+
+/**
+ * @group Broken
+ */
+class HttpTest extends PHPUnit_Framework_TestCase {
+ static $content;
+ static $headers;
+ static $has_curl;
+ static $has_fopen;
+ static $has_proxy = false;
+ static $proxy = "http://hulk:8080/";
+ var $test_geturl = array(
+ "http://www.example.com/",
+ "http://pecl.php.net/feeds/pkg_apc.rss",
+ "http://toolserver.org/~jan/poll/dev/main.php?page=wiki_output&id=3",
+ "http://meta.wikimedia.org/w/index.php?title=Interwiki_map&action=raw",
+ "http://www.mediawiki.org/w/api.php?action=query&list=categorymembers&cmtitle=Category:MediaWiki_hooks&format=php",
+ );
+ var $test_requesturl = array( "http://en.wikipedia.org/wiki/Special:Export/User:MarkAHershberger" );
+
+ var $test_posturl = array( "http://www.comp.leeds.ac.uk/cgi-bin/Perl/environment-example" => "review=test" );
+
+ function setUp() {
+ putenv( "http_proxy" ); /* Remove any proxy env var, so curl doesn't get confused */
+ if ( is_array( self::$content ) ) {
+ return;
+ }
+ self::$has_curl = function_exists( 'curl_init' );
+ self::$has_fopen = wfIniGetBool( 'allow_url_fopen' );
+
+ if ( !file_exists( "/usr/bin/curl" ) ) {
+ $this->markTestIncomplete( "This test requires the curl binary at /usr/bin/curl. If you have curl, please file a bug on this test, or, better yet, provide a patch." );
+ }
+
+ $content = tempnam( wfTempDir(), "" );
+ $headers = tempnam( wfTempDir(), "" );
+ if ( !$content && !$headers ) {
+ die( "Couldn't create temp file!" );
+ }
+
+ // This probably isn't the best test for a proxy, but it works on my system!
+ system( "curl -0 -o $content -s " . self::$proxy );
+ $out = file_get_contents( $content );
+ if ( $out ) {
+ self::$has_proxy = true;
+ }
+
+ /* Maybe use wget instead of curl here ... just to use a different codebase? */
+ foreach ( $this->test_geturl as $u ) {
+ system( "curl -0 -s -D $headers '$u' -o $content" );
+ self::$content["GET $u"] = file_get_contents( $content );
+ self::$headers["GET $u"] = file_get_contents( $headers );
+ }
+ foreach ( $this->test_requesturl as $u ) {
+ system( "curl -0 -s -X POST -H 'Content-Length: 0' -D $headers '$u' -o $content" );
+ self::$content["POST $u"] = file_get_contents( $content );
+ self::$headers["POST $u"] = file_get_contents( $headers );
+ }
+ foreach ( $this->test_posturl as $u => $postData ) {
+ system( "curl -0 -s -X POST -d '$postData' -D $headers '$u' -o $content" );
+ self::$content["POST $u => $postData"] = file_get_contents( $content );
+ self::$headers["POST $u => $postData"] = file_get_contents( $headers );
+ }
+ unlink( $content );
+ unlink( $headers );
+ }
+
+
+ function testInstantiation() {
+ Http::$httpEngine = false;
+
+ $r = MWHttpRequest::factory( "http://www.example.com/" );
+ if ( self::$has_curl ) {
+ $this->assertThat( $r, $this->isInstanceOf( 'CurlHttpRequest' ) );
+ } else {
+ $this->assertThat( $r, $this->isInstanceOf( 'PhpHttpRequest' ) );
+ }
+ unset( $r );
+
+ if ( !self::$has_fopen ) {
+ $this->setExpectedException( 'MWException' );
+ }
+ Http::$httpEngine = 'php';
+ $r = MWHttpRequest::factory( "http://www.example.com/" );
+ $this->assertThat( $r, $this->isInstanceOf( 'PhpHttpRequest' ) );
+ unset( $r );
+
+ if ( !self::$has_curl ) {
+ $this->setExpectedException( 'MWException' );
+ }
+ Http::$httpEngine = 'curl';
+ $r = MWHttpRequest::factory( "http://www.example.com/" );
+ if ( self::$has_curl ) {
+ $this->assertThat( $r, $this->isInstanceOf( 'CurlHttpRequest' ) );
+ }
+ }
+
+ function runHTTPFailureChecks() {
+ // Each of the following requests should result in a failure.
+
+ $timeout = 1;
+ $start_time = time();
+ $r = Http::get( "http://www.example.com:1/", $timeout );
+ $end_time = time();
+ $this->assertLessThan( $timeout + 2, $end_time - $start_time,
+ "Request took less than {$timeout}s via " . Http::$httpEngine );
+ $this->assertEquals( $r, false, "false -- what we get on error from Http::get()" );
+
+ $r = Http::get( "http://www.example.com/this-file-does-not-exist", $timeout );
+ $this->assertFalse( $r, "False on 404s" );
+
+
+ $r = MWHttpRequest::factory( "http://www.example.com/this-file-does-not-exist" );
+ $er = $r->execute();
+ if ( $r instanceof PhpHttpRequest && version_compare( '5.2.10', phpversion(), '>' ) ) {
+ $this->assertRegexp( "/HTTP request failed/", $er->getWikiText() );
+ } else {
+ $this->assertRegexp( "/404 Not Found/", $er->getWikiText() );
+ }
+ }
+
+ function testFailureDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPFailureChecks();
+ }
+
+ function testFailurePhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPFailureChecks();
+ }
+
+ function testFailureCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPFailureChecks();
+ }
+
+ /* ./phase3/includes/Import.php:1108: $data = Http::request( $method, $url ); */
+ /* ./includes/Import.php:1124: $link = Title::newFromText( "$interwiki:Special:Export/$page" ); */
+ /* ./includes/Import.php:1134: return ImportStreamSource::newFromURL( $url, "POST" ); */
+ function runHTTPRequests( $proxy = null ) {
+ $opt = array();
+
+ if ( $proxy ) {
+ $opt['proxy'] = $proxy;
+ } elseif ( $proxy === false ) {
+ $opt['noProxy'] = true;
+ }
+
+ /* no postData here because the only request I could find in code so far didn't have any */
+ foreach ( $this->test_requesturl as $u ) {
+ $r = Http::request( "POST", $u, $opt );
+ $this->assertEquals( self::$content["POST $u"], "$r", "POST $u with " . Http::$httpEngine );
+ }
+ }
+
+ function testRequestDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPRequests();
+ }
+
+ function testRequestPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPRequests();
+ }
+
+ function testRequestCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPRequests();
+ }
+
+ function runHTTPGets( $proxy = null ) {
+ $opt = array();
+
+ if ( $proxy ) {
+ $opt['proxy'] = $proxy;
+ } elseif ( $proxy === false ) {
+ $opt['noProxy'] = true;
+ }
+
+ foreach ( $this->test_geturl as $u ) {
+ $r = Http::get( $u, 30, $opt ); /* timeout of 30s */
+ $this->assertEquals( self::$content["GET $u"], "$r", "Get $u with " . Http::$httpEngine );
+ }
+ }
+
+ function testGetDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPGets();
+ }
+
+ function testGetPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPGets();
+ }
+
+ function testGetCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPGets();
+ }
+
+ /* ./phase3/maintenance/parserTests.inc:1618: return Http::post( $url, array( 'postData' => wfArrayToCGI( $data ) ) ); */
+ function runHTTPPosts( $proxy = null ) {
+ $opt = array();
+
+ if ( $proxy ) {
+ $opt['proxy'] = $proxy;
+ } elseif ( $proxy === false ) {
+ $opt['noProxy'] = true;
+ }
+
+ foreach ( $this->test_posturl as $u => $postData ) {
+ $opt['postData'] = $postData;
+ $r = Http::post( $u, $opt );
+ $this->assertEquals( self::$content["POST $u => $postData"], "$r",
+ "POST $u (postData=$postData) with " . Http::$httpEngine );
+ }
+ }
+
+ function testPostDefault() {
+ Http::$httpEngine = false;
+ $this->runHTTPPosts();
+ }
+
+ function testPostPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = "php";
+ $this->runHTTPPosts();
+ }
+
+ function testPostCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = "curl";
+ $this->runHTTPPosts();
+ }
+
+ function runProxyRequests() {
+ if ( !self::$has_proxy ) {
+ $this->markTestIncomplete( "This test requires a proxy." );
+ }
+ $this->runHTTPGets( self::$proxy );
+ $this->runHTTPPosts( self::$proxy );
+ $this->runHTTPRequests( self::$proxy );
+
+ // Set false here to do noProxy
+ $this->runHTTPGets( false );
+ $this->runHTTPPosts( false );
+ $this->runHTTPRequests( false );
+ }
+
+ function testProxyDefault() {
+ Http::$httpEngine = false;
+ $this->runProxyRequests();
+ }
+
+ function testProxyPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = 'php';
+ $this->runProxyRequests();
+ }
+
+ function testProxyCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = 'curl';
+ $this->runProxyRequests();
+ }
+
+ function testIsLocalUrl() {
+ }
+
+ /* ./extensions/DonationInterface/payflowpro_gateway/payflowpro_gateway.body.php:559: $user_agent = Http::userAgent(); */
+ function testUserAgent() {
+ }
+
+ function testIsValidUrl() {
+ }
+
+ function testValidateCookieDomain() {
+ $this->assertFalse( Cookie::validateCookieDomain( "co.uk" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( ".co.uk" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "gov.uk" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( ".gov.uk" ) );
+ $this->assertTrue( Cookie::validateCookieDomain( "supermarket.uk" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "uk" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( ".uk" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "127.0.0." ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "127." ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "127.0.0.1." ) );
+ $this->assertTrue( Cookie::validateCookieDomain( "127.0.0.1" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "333.0.0.1" ) );
+ $this->assertTrue( Cookie::validateCookieDomain( "example.com" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "example.com." ) );
+ $this->assertTrue( Cookie::validateCookieDomain( ".example.com" ) );
+
+ $this->assertTrue( Cookie::validateCookieDomain( ".example.com", "www.example.com" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "example.com", "www.example.com" ) );
+ $this->assertTrue( Cookie::validateCookieDomain( "127.0.0.1", "127.0.0.1" ) );
+ $this->assertFalse( Cookie::validateCookieDomain( "127.0.0.1", "localhost" ) );
+
+
+ }
+
+ function testSetCooke() {
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => "ac.th",
+ "path" => "/path/",
+ ) );
+ $this->assertFalse( $c->canServeDomain( "ac.th" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => "example.com",
+ "path" => "/path/",
+ ) );
+
+ $this->assertTrue( $c->canServeDomain( "example.com" ) );
+ $this->assertFalse( $c->canServeDomain( "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ ) );
+
+ $this->assertFalse( $c->canServeDomain( "www.example.net" ) );
+ $this->assertFalse( $c->canServeDomain( "example.com" ) );
+ $this->assertTrue( $c->canServeDomain( "www.example.com" ) );
+
+ $this->assertFalse( $c->canServePath( "/" ) );
+ $this->assertFalse( $c->canServePath( "/bogus/path/" ) );
+ $this->assertFalse( $c->canServePath( "/path" ) );
+ $this->assertTrue( $c->canServePath( "/path/" ) );
+
+ $this->assertTrue( $c->isUnExpired() );
+
+ $this->assertEquals( "", $c->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ $this->assertEquals( "", $c->serializeToHttpRequest( "/", "www.example.com" ) );
+ $this->assertEquals( "name=value", $c->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => "www.example.com",
+ "path" => "/path/",
+ ) );
+ $this->assertFalse( $c->canServeDomain( "example.com" ) );
+ $this->assertFalse( $c->canServeDomain( "www.example.net" ) );
+ $this->assertTrue( $c->canServeDomain( "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ "expires" => "-1 day",
+ ) );
+ $this->assertFalse( $c->isUnExpired() );
+ $this->assertEquals( "", $c->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $c = new MockCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ "expires" => "+1 day",
+ ) );
+ $this->assertTrue( $c->isUnExpired() );
+ $this->assertEquals( "name=value", $c->serializeToHttpRequest( "/path/", "www.example.com" ) );
+ }
+
+ function testCookieJarSetCookie() {
+ $cj = new CookieJar;
+ $cj->setCookie( "name", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/",
+ ) );
+ $cj->setCookie( "name2", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/path/sub",
+ ) );
+ $cj->setCookie( "name3", "value",
+ array(
+ "domain" => ".example.com",
+ "path" => "/",
+ ) );
+ $cj->setCookie( "name4", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ ) );
+ $cj->setCookie( "name5", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "-1 day",
+ ) );
+
+ $this->assertEquals( "name4=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ $this->assertEquals( "name3=value", $cj->serializeToHttpRequest( "/", "www.example.com" ) );
+ $this->assertEquals( "name=value; name3=value", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $cj->setCookie( "name5", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "+1 day",
+ ) );
+ $this->assertEquals( "name4=value; name5=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+
+ $cj->setCookie( "name4", "value",
+ array(
+ "domain" => ".example.net",
+ "path" => "/path/",
+ "expires" => "-1 day",
+ ) );
+ $this->assertEquals( "name5=value", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ }
+
+ function testParseResponseHeader() {
+ $cj = new CookieJar;
+
+ $h[] = "Set-Cookie: name4=value; domain=.example.com; path=/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[0], "www.example.com" );
+ $this->assertEquals( "name4=value", $cj->serializeToHttpRequest( "/", "www.example.com" ) );
+
+ $h[] = "name4=value2; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[1], "www.example.com" );
+ $this->assertEquals( "", $cj->serializeToHttpRequest( "/", "www.example.com" ) );
+ $this->assertEquals( "name4=value2", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $h[] = "name5=value3; domain=.example.com; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[2], "www.example.com" );
+ $this->assertEquals( "name4=value2; name5=value3", $cj->serializeToHttpRequest( "/path/", "www.example.com" ) );
+
+ $h[] = "name6=value3; domain=.example.net; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[3], "www.example.com" );
+ $this->assertEquals( "", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+
+ $h[] = "name6=value0; domain=.example.net; path=/path/; expires=Mon, 09-Dec-1999 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[4], "www.example.net" );
+ $this->assertEquals( "", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+
+ $h[] = "name6=value4; domain=.example.net; path=/path/; expires=Mon, 09-Dec-2029 13:46:00 GMT";
+ $cj->parseCookieResponseHeader( $h[5], "www.example.net" );
+ $this->assertEquals( "name6=value4", $cj->serializeToHttpRequest( "/path/", "www.example.net" ) );
+ }
+
+ function runCookieRequests() {
+ $r = MWHttpRequest::factory( "http://www.php.net/manual", array( 'followRedirects' => true ) );
+ $r->execute();
+
+ $jar = $r->getCookieJar();
+ $this->assertThat( $jar, $this->isInstanceOf( 'CookieJar' ) );
+
+ if ( $r instanceof PhpHttpRequest && version_compare( '5.1.7', phpversion(), '>' ) ) {
+ $this->markTestSkipped( 'Redirection fails or crashes PHP on 5.1.6 and prior' );
+ }
+ $serialized = $jar->serializeToHttpRequest( "/search?q=test", "www.php.net" );
+ $this->assertRegExp( '/\bCOUNTRY=[^=;]+/', $serialized );
+ $this->assertRegExp( '/\bLAST_LANG=[^=;]+/', $serialized );
+ $this->assertEquals( '', $jar->serializeToHttpRequest( "/search?q=test", "www.php.com" ) );
+ }
+
+ function testCookieRequestDefault() {
+ Http::$httpEngine = false;
+ $this->runCookieRequests();
+ }
+ function testCookieRequestPhp() {
+ if ( !self::$has_fopen ) {
+ $this->markTestIncomplete( "This test requires allow_url_fopen=true." );
+ }
+
+ Http::$httpEngine = 'php';
+ $this->runCookieRequests();
+ }
+ function testCookieRequestCurl() {
+ if ( !self::$has_curl ) {
+ $this->markTestIncomplete( "This test requires curl." );
+ }
+
+ Http::$httpEngine = 'curl';
+ $this->runCookieRequests();
+ }
+}
--- /dev/null
+<?php
+/*
+ * Tests for IP validity functions. Ported from /t/inc/IP.t by avar.
+ */
+
+class IPTest extends PHPUnit_Framework_TestCase {
+ // not sure it should be tested with boolean false. hashar 20100924
+ public function testisIPAddress() {
+ $this->assertFalse( IP::isIPAddress( false ), 'Boolean false is not an IP' );
+ $this->assertFalse( IP::isIPAddress( true ), 'Boolean true is not an IP' );
+ $this->assertFalse( IP::isIPAddress( "" ), 'Empty string is not an IP' );
+ $this->assertFalse( IP::isIPAddress( 'abc' ), 'Garbage IP string' );
+ $this->assertFalse( IP::isIPAddress( ':' ), 'Single ":" is not an IP' );
+ $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::1'), 'IPv6 with a double :: occurence' );
+ $this->assertFalse( IP::isIPAddress( '2001:0DB8::A:1::'), 'IPv6 with a double :: occurence, last at end' );
+ $this->assertFalse( IP::isIPAddress( '::2001:0DB8::5:1'), 'IPv6 with a double :: occurence, firt at beginning' );
+ $this->assertFalse( IP::isIPAddress( '124.24.52' ), 'IPv4 not enough quads' );
+ $this->assertFalse( IP::isIPAddress( '24.324.52.13' ), 'IPv4 out of range' );
+ $this->assertFalse( IP::isIPAddress( '.24.52.13' ), 'IPv4 starts with period' );
+ $this->assertFalse( IP::isIPAddress( 'fc:100:300' ), 'IPv6 with only 3 words' );
+
+ $this->assertTrue( IP::isIPAddress( '::' ), 'RFC 4291 IPv6 Unspecified Address' );
+ $this->assertTrue( IP::isIPAddress( '::1' ), 'RFC 4291 IPv6 Loopback Address' );
+ $this->assertTrue( IP::isIPAddress( '74.24.52.13/20', 'IPv4 range' ) );
+ $this->assertTrue( IP::isIPAddress( 'fc:100:a:d:1:e:ac:0/24' ), 'IPv6 range' );
+ $this->assertTrue( IP::isIPAddress( 'fc::100:a:d:1:e:ac/96' ), 'IPv6 range with "::"' );
+
+ $validIPs = array( 'fc:100::', 'fc:100:a:d:1:e:ac::', 'fc::100', '::fc:100:a:d:1:e:ac',
+ '::fc', 'fc::100:a:d:1:e:ac', 'fc:100:a:d:1:e:ac:0', '124.24.52.13', '1.24.52.13' );
+ foreach ( $validIPs as $ip ) {
+ $this->assertTrue( IP::isIPAddress( $ip ), "$ip is a valid IP address" );
+ }
+ }
+
+ public function testisIPv6() {
+ $this->assertFalse( IP::isIPv6( ':fc:100::' ), 'IPv6 starting with lone ":"' );
+ $this->assertFalse( IP::isIPv6( 'fc:100:::' ), 'IPv6 ending with a ":::"' );
+ $this->assertFalse( IP::isIPv6( 'fc:300' ), 'IPv6 with only 2 words' );
+ $this->assertFalse( IP::isIPv6( 'fc:100:300' ), 'IPv6 with only 3 words' );
+ $this->assertTrue( IP::isIPv6( 'fc:100::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e::' ) );
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac::' ) );
+ $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' );
+ $this->assertFalse( IP::isIPv6( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' );
+
+ $this->assertFalse( IP::isIPv6( ':::' ) );
+ $this->assertFalse( IP::isIPv6( '::0:' ), 'IPv6 ending in a lone ":"' );
+ $this->assertTrue( IP::isIPv6( '::' ), 'IPv6 zero address' );
+ $this->assertTrue( IP::isIPv6( '::0' ) );
+ $this->assertTrue( IP::isIPv6( '::fc' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e' ) );
+ $this->assertTrue( IP::isIPv6( '::fc:100:a:d:1:e:ac' ) );
+ $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+ $this->assertFalse( IP::isIPv6( '::fc:100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+ $this->assertFalse( IP::isIPv6( ':fc::100' ), 'IPv6 starting with lone ":"' );
+ $this->assertFalse( IP::isIPv6( 'fc::100:' ), 'IPv6 ending with lone ":"' );
+ $this->assertFalse( IP::isIPv6( 'fc:::100' ), 'IPv6 with ":::" in the middle' );
+ $this->assertTrue( IP::isIPv6( 'fc::100' ), 'IPv6 with "::" and 2 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a' ), 'IPv6 with "::" and 3 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d', 'IPv6 with "::" and 4 words' ) );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e' ), 'IPv6 with "::" and 6 words' );
+ $this->assertTrue( IP::isIPv6( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' );
+ $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0' ), 'IPv6 with "::" and 8 words' );
+ $this->assertFalse( IP::isIPv6( 'fc::100:a:d:1:e:ac:0:1' ), 'IPv6 with 9 words' );
+
+ $this->assertTrue( IP::isIPv6( 'fc:100:a:d:1:e:ac:0' ) );
+ }
+
+ public function testisIPv4() {
+ $this->assertFalse( IP::isIPv4( false ), 'Boolean false is not an IP' );
+ $this->assertFalse( IP::isIPv4( true ), 'Boolean true is not an IP' );
+ $this->assertFalse( IP::isIPv4( "" ), 'Empty string is not an IP' );
+ $this->assertFalse( IP::isIPv4( 'abc' ) );
+ $this->assertFalse( IP::isIPv4( ':' ) );
+ $this->assertFalse( IP::isIPv4( '124.24.52' ), 'IPv4 not enough quads' );
+ $this->assertFalse( IP::isIPv4( '24.324.52.13' ), 'IPv4 out of range' );
+ $this->assertFalse( IP::isIPv4( '.24.52.13' ), 'IPv4 starts with period' );
+
+ $this->assertTrue( IP::isIPv4( '124.24.52.13' ) );
+ $this->assertTrue( IP::isIPv4( '1.24.52.13' ) );
+ $this->assertTrue( IP::isIPv4( '74.24.52.13/20', 'IPv4 range' ) );
+ }
+
+ // tests isValid()
+ public function testValidIPs() {
+ foreach ( range( 0, 255 ) as $i ) {
+ $a = sprintf( "%03d", $i );
+ $b = sprintf( "%02d", $i );
+ $c = sprintf( "%01d", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f.$f.$f.$f";
+ $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv4 address" );
+ }
+ }
+ foreach ( range( 0x0, 0xFFFF ) as $i ) {
+ $a = sprintf( "%04x", $i );
+ $b = sprintf( "%03x", $i );
+ $c = sprintf( "%02x", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+ $this->assertTrue( IP::isValid( $ip ) , "$ip is a valid IPv6 address" );
+ }
+ }
+ }
+
+ // tests isValid()
+ public function testInvalidIPs() {
+ // Out of range...
+ foreach ( range( 256, 999 ) as $i ) {
+ $a = sprintf( "%03d", $i );
+ $b = sprintf( "%02d", $i );
+ $c = sprintf( "%01d", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f.$f.$f.$f";
+ $this->assertFalse( IP::isValid( $ip ), "$ip is not a valid IPv4 address" );
+ }
+ }
+ foreach ( range( 'g', 'z' ) as $i ) {
+ $a = sprintf( "%04s", $i );
+ $b = sprintf( "%03s", $i );
+ $c = sprintf( "%02s", $i );
+ foreach ( array_unique( array( $a, $b, $c ) ) as $f ) {
+ $ip = "$f:$f:$f:$f:$f:$f:$f:$f";
+ $this->assertFalse( IP::isValid( $ip ) , "$ip is not a valid IPv6 address" );
+ }
+ }
+ // Have CIDR
+ $ipCIDRs = array(
+ '212.35.31.121/32',
+ '212.35.31.121/18',
+ '212.35.31.121/24',
+ '::ff:d:321:5/96',
+ 'ff::d3:321:5/116',
+ 'c:ff:12:1:ea:d:321:5/120',
+ );
+ foreach ( $ipCIDRs as $i ) {
+ $this->assertFalse( IP::isValid( $i ),
+ "$i is an invalid IP address because it is a block" );
+ }
+ // Incomplete/garbage
+ $invalid = array(
+ 'www.xn--var-xla.net',
+ '216.17.184.G',
+ '216.17.184.1.',
+ '216.17.184',
+ '216.17.184.',
+ '256.17.184.1'
+ );
+ foreach ( $invalid as $i ) {
+ $this->assertFalse( IP::isValid( $i ), "$i is an invalid IP address" );
+ }
+ }
+
+ // tests isValidBlock()
+ public function testValidBlocks() {
+ $valid = array(
+ '116.17.184.5/32',
+ '0.17.184.5/30',
+ '16.17.184.1/24',
+ '30.242.52.14/1',
+ '10.232.52.13/8',
+ '30.242.52.14/0',
+ '::e:f:2001/96',
+ '::c:f:2001/128',
+ '::10:f:2001/70',
+ '::fe:f:2001/1',
+ '::6d:f:2001/8',
+ '::fe:f:2001/0',
+ );
+ foreach ( $valid as $i ) {
+ $this->assertTrue( IP::isValidBlock( $i ), "$i is a valid IP block" );
+ }
+ }
+
+ // tests isValidBlock()
+ public function testInvalidBlocks() {
+ $invalid = array(
+ '116.17.184.5/33',
+ '0.17.184.5/130',
+ '16.17.184.1/-1',
+ '10.232.52.13/*',
+ '7.232.52.13/ab',
+ '11.232.52.13/',
+ '::e:f:2001/129',
+ '::c:f:2001/228',
+ '::10:f:2001/-1',
+ '::6d:f:2001/*',
+ '::86:f:2001/ab',
+ '::23:f:2001/',
+ );
+ foreach ( $invalid as $i ) {
+ $this->assertFalse( IP::isValidBlock( $i ), "$i is not a valid IP block" );
+ }
+ }
+
+ // test wrapper around ip2long which might return -1 or false depending on PHP version
+ public function testip2longWrapper() {
+ // fixme : add more tests ?
+ $this->assertEquals( pow(2,32) - 1, IP::toUnsigned( '255.255.255.255' ));
+ $i = 'IN.VA.LI.D';
+ $this->assertFalse( IP::toUnSigned( $i ) );
+ }
+
+ // tests isPublic()
+ public function testPrivateIPs() {
+ $private = array( 'fc::3', 'fc::ff', '::1', '10.0.0.1', '172.16.0.1', '192.168.0.1' );
+ foreach ( $private as $p ) {
+ $this->assertFalse( IP::isPublic( $p ), "$p is not a public IP address" );
+ }
+ }
+
+ // Private wrapper used to test CIDR Parsing.
+ private function assertFalseCIDR( $CIDR, $msg='' ) {
+ $ff = array( false, false );
+ $this->assertEquals( $ff, IP::parseCIDR( $CIDR ), $msg );
+ }
+
+ // Private wrapper to test network shifting using only dot notation
+ private function assertNet( $expected, $CIDR ) {
+ $parse = IP::parseCIDR( $CIDR );
+ $this->assertEquals( $expected, long2ip( $parse[0] ), "network shifting $CIDR" );
+ }
+
+ public function testHexToQuad() {
+ $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '00000001' ) );
+ $this->assertEquals( '255.0.0.0' , IP::hexToQuad( 'FF000000' ) );
+ $this->assertEquals( '255.255.255.255', IP::hexToQuad( 'FFFFFFFF' ) );
+ $this->assertEquals( '10.188.222.255' , IP::hexToQuad( '0ABCDEFF' ) );
+ // hex not left-padded...
+ $this->assertEquals( '0.0.0.0' , IP::hexToQuad( '0' ) );
+ $this->assertEquals( '0.0.0.1' , IP::hexToQuad( '1' ) );
+ $this->assertEquals( '0.0.0.255' , IP::hexToQuad( 'FF' ) );
+ $this->assertEquals( '0.0.255.0' , IP::hexToQuad( 'FF00' ) );
+ }
+
+ public function testHexToOctet() {
+ $this->assertEquals( '0:0:0:0:0:0:0:1',
+ IP::hexToOctet( '00000000000000000000000000000001' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FF:3',
+ IP::hexToOctet( '00000000000000000000000000FF0003' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FF00:6',
+ IP::hexToOctet( '000000000000000000000000FF000006' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF',
+ IP::hexToOctet( '000000000000000000000000FCCFFAFF' ) );
+ $this->assertEquals( 'FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF',
+ IP::hexToOctet( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF' ) );
+ // hex not left-padded...
+ $this->assertEquals( '0:0:0:0:0:0:0:0' , IP::hexToOctet( '0' ) );
+ $this->assertEquals( '0:0:0:0:0:0:0:1' , IP::hexToOctet( '1' ) );
+ $this->assertEquals( '0:0:0:0:0:0:0:FF' , IP::hexToOctet( 'FF' ) );
+ $this->assertEquals( '0:0:0:0:0:0:0:FFD0' , IP::hexToOctet( 'FFD0' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FA00:0' , IP::hexToOctet( 'FA000000' ) );
+ $this->assertEquals( '0:0:0:0:0:0:FCCF:FAFF', IP::hexToOctet( 'FCCFFAFF' ) );
+ }
+
+ /*
+ * IP::parseCIDR() returns an array containing a signed IP address
+ * representing the network mask and the bit mask.
+ */
+ function testCIDRParsing() {
+ $this->assertFalseCIDR( '192.0.2.0' , "missing mask" );
+ $this->assertFalseCIDR( '192.0.2.0/', "missing bitmask" );
+
+ // Verify if statement
+ $this->assertFalseCIDR( '256.0.0.0/32', "invalid net" );
+ $this->assertFalseCIDR( '192.0.2.0/AA', "mask not numeric" );
+ $this->assertFalseCIDR( '192.0.2.0/-1', "mask < 0" );
+ $this->assertFalseCIDR( '192.0.2.0/33', "mask > 32" );
+
+ // Check internal logic
+ # 0 mask always result in array(0,0)
+ $this->assertEquals( array( 0, 0 ), IP::parseCIDR('192.0.0.2/0') );
+ $this->assertEquals( array( 0, 0 ), IP::parseCIDR('0.0.0.0/0') );
+ $this->assertEquals( array( 0, 0 ), IP::parseCIDR('255.255.255.255/0') );
+
+ // FIXME : add more tests.
+
+ # This part test network shifting
+ $this->assertNet( '192.0.0.0' , '192.0.0.2/24' );
+ $this->assertNet( '192.168.5.0', '192.168.5.13/24');
+ $this->assertNet( '10.0.0.160' , '10.0.0.161/28' );
+ $this->assertNet( '10.0.0.0' , '10.0.0.3/28' );
+ $this->assertNet( '10.0.0.0' , '10.0.0.3/30' );
+ $this->assertNet( '10.0.0.4' , '10.0.0.4/30' );
+ $this->assertNet( '172.17.32.0', '172.17.35.48/21' );
+ $this->assertNet( '10.128.0.0' , '10.135.0.0/9' );
+ $this->assertNet( '134.0.0.0' , '134.0.5.1/8' );
+ }
+}
--- /dev/null
+<?php
+
+class ImageFunctionsTest extends PHPUnit_Framework_TestCase {
+ function testFitBoxWidth() {
+ $vals = array(
+ array(
+ 'width' => 50,
+ 'height' => 50,
+ 'tests' => array(
+ 50 => 50,
+ 17 => 17,
+ 18 => 18 ) ),
+ array(
+ 'width' => 366,
+ 'height' => 300,
+ 'tests' => array(
+ 50 => 61,
+ 17 => 21,
+ 18 => 22 ) ),
+ array(
+ 'width' => 300,
+ 'height' => 366,
+ 'tests' => array(
+ 50 => 41,
+ 17 => 14,
+ 18 => 15 ) ),
+ array(
+ 'width' => 100,
+ 'height' => 400,
+ 'tests' => array(
+ 50 => 12,
+ 17 => 4,
+ 18 => 4 ) ) );
+ foreach ( $vals as $row ) {
+ extract( $row );
+ foreach ( $tests as $max => $expected ) {
+ $y = round( $expected * $height / $width );
+ $result = wfFitBoxWidth( $width, $height, $max );
+ $y2 = round( $result * $height / $width );
+ $this->assertEquals( $expected,
+ $result,
+ "($width, $height, $max) wanted: {$expected}x$y, got: {$result}x$y2" );
+ }
+ }
+ }
+}
+
+
--- /dev/null
+<?php
+
+class LanguageConverterTest extends PHPUnit_Framework_TestCase {
+ protected $lang = null;
+ protected $lc = null;
+
+ function setUp() {
+ global $wgMemc, $wgRequest, $wgUser, $wgContLang;
+
+ $wgUser = new User;
+ $wgRequest = new FauxRequest( array() );
+ $wgMemc = new FakeMemCachedClient;
+ $wgContLang = Language::factory( 'tg' );
+ $this->lang = new LanguageToTest();
+ $this->lc = new TestConverter( $this->lang, 'tg',
+ array( 'tg', 'tg-latn' ) );
+ }
+
+ function tearDown() {
+ global $wgMemc, $wgContLang;
+ unset( $wgMemc );
+ unset( $this->lc );
+ unset( $this->lang );
+ $wgContLang = Language::factory( 'en' );
+ }
+
+ function testGetPreferredVariantDefaults() {
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaders() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderWeight() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg;q=1' );
+
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderWeight2() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderMulti() {
+ global $wgRequest;
+ $wgRequest->setHeader( 'Accept-Language', 'en, tg-latn;q=1' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantUserOption() {
+ global $wgUser;
+
+ $wgUser = new User;
+ $wgUser->setId( 1 );
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant', 'tg-latn' );
+
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantHeaderUserVsUrl() {
+ global $wgRequest, $wgUser, $wgContLang;
+
+ $wgContLang = Language::factory( 'tg-latn' );
+ $wgRequest->setVal( 'variant', 'tg' );
+ $wgUser = User::newFromId( "admin" );
+ $wgUser->setId( 1 );
+ $wgUser->mDataLoaded = true;
+ $wgUser->mOptionsLoaded = true;
+ $wgUser->setOption( 'variant', 'tg-latn' ); // The user's data is ignored
+ // because the variant is set in the URL.
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+
+
+ function testGetPreferredVariantDefaultLanguageVariant() {
+ global $wgDefaultLanguageVariant;
+
+ $wgDefaultLanguageVariant = 'tg-latn';
+ $this->assertEquals( 'tg-latn', $this->lc->getPreferredVariant() );
+ }
+
+ function testGetPreferredVariantDefaultLanguageVsUrlVariant() {
+ global $wgDefaultLanguageVariant, $wgRequest, $wgContLang;
+
+ $wgContLang = Language::factory( 'tg-latn' );
+ $wgDefaultLanguageVariant = 'tg';
+ $wgRequest->setVal( 'variant', null );
+ $this->assertEquals( 'tg', $this->lc->getPreferredVariant() );
+ }
+}
+
+/**
+ * Test converter (from Tajiki to latin orthography)
+ */
+class TestConverter extends LanguageConverter {
+ private $table = array(
+ 'б' => 'b',
+ 'в' => 'v',
+ 'г' => 'g',
+ );
+
+ function loadDefaultTables() {
+ $this->mTables = array(
+ 'tg-latn' => new ReplacementArray( $this->table ),
+ 'tg' => new ReplacementArray()
+ );
+ }
+
+}
+
+class LanguageToTest extends Language {
+ function __construct() {
+ parent::__construct();
+ $variants = array( 'tg', 'tg-latn' );
+ $this->mConverter = new TestConverter( $this, 'tg', $variants );
+ }
+}
--- /dev/null
+<?php
+
+class LicensesTest extends PHPUnit_Framework_TestCase {
+
+ function testLicenses() {
+ $str = "
+* Free licenses:
+** GFDL|Debian disagrees
+";
+
+ $lc = new Licenses( array( 'licenses' => $str ) );
+ $this->assertThat( $lc, $this->isInstanceOf( 'Licenses' ) );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * These tests should work regardless of $wgCapitalLinks
+ */
+
+class LocalFileTest extends PHPUnit_Framework_TestCase {
+ function setUp() {
+ global $wgContLang, $wgCapitalLinks;
+
+ $wgContLang = new Language;
+ $wgCapitalLinks = true;
+ $info = array(
+ 'name' => 'test',
+ 'directory' => '/testdir',
+ 'url' => '/testurl',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ );
+ $this->repo_hl0 = new LocalRepo( array( 'hashLevels' => 0 ) + $info );
+ $this->repo_hl2 = new LocalRepo( array( 'hashLevels' => 2 ) + $info );
+ $this->repo_lc = new LocalRepo( array( 'initialCapital' => false ) + $info );
+ $this->file_hl0 = $this->repo_hl0->newFile( 'test!' );
+ $this->file_hl2 = $this->repo_hl2->newFile( 'test!' );
+ $this->file_lc = $this->repo_lc->newFile( 'test!' );
+ }
+
+ function testGetHashPath() {
+ $this->assertEquals( '', $this->file_hl0->getHashPath() );
+ $this->assertEquals( 'a/a2/', $this->file_hl2->getHashPath() );
+ $this->assertEquals( 'c/c4/', $this->file_lc->getHashPath() );
+ }
+
+ function testGetRel() {
+ $this->assertEquals( 'Test!', $this->file_hl0->getRel() );
+ $this->assertEquals( 'a/a2/Test!', $this->file_hl2->getRel() );
+ $this->assertEquals( 'c/c4/test!', $this->file_lc->getRel() );
+ }
+
+ function testGetUrlRel() {
+ $this->assertEquals( 'Test%21', $this->file_hl0->getUrlRel() );
+ $this->assertEquals( 'a/a2/Test%21', $this->file_hl2->getUrlRel() );
+ $this->assertEquals( 'c/c4/test%21', $this->file_lc->getUrlRel() );
+ }
+
+ function testGetArchivePath() {
+ $this->assertEquals( '/testdir/archive', $this->file_hl0->getArchivePath() );
+ $this->assertEquals( '/testdir/archive/a/a2', $this->file_hl2->getArchivePath() );
+ $this->assertEquals( '/testdir/archive/!', $this->file_hl0->getArchivePath( '!' ) );
+ $this->assertEquals( '/testdir/archive/a/a2/!', $this->file_hl2->getArchivePath( '!' ) );
+ }
+
+ function testGetThumbPath() {
+ $this->assertEquals( '/testdir/thumb/Test!', $this->file_hl0->getThumbPath() );
+ $this->assertEquals( '/testdir/thumb/a/a2/Test!', $this->file_hl2->getThumbPath() );
+ $this->assertEquals( '/testdir/thumb/Test!/x', $this->file_hl0->getThumbPath( 'x' ) );
+ $this->assertEquals( '/testdir/thumb/a/a2/Test!/x', $this->file_hl2->getThumbPath( 'x' ) );
+ }
+
+ function testGetArchiveUrl() {
+ $this->assertEquals( '/testurl/archive', $this->file_hl0->getArchiveUrl() );
+ $this->assertEquals( '/testurl/archive/a/a2', $this->file_hl2->getArchiveUrl() );
+ $this->assertEquals( '/testurl/archive/%21', $this->file_hl0->getArchiveUrl( '!' ) );
+ $this->assertEquals( '/testurl/archive/a/a2/%21', $this->file_hl2->getArchiveUrl( '!' ) );
+ }
+
+ function testGetThumbUrl() {
+ $this->assertEquals( '/testurl/thumb/Test%21', $this->file_hl0->getThumbUrl() );
+ $this->assertEquals( '/testurl/thumb/a/a2/Test%21', $this->file_hl2->getThumbUrl() );
+ $this->assertEquals( '/testurl/thumb/Test%21/x', $this->file_hl0->getThumbUrl( 'x' ) );
+ $this->assertEquals( '/testurl/thumb/a/a2/Test%21/x', $this->file_hl2->getThumbUrl( 'x' ) );
+ }
+
+ function testGetArchiveVirtualUrl() {
+ $this->assertEquals( 'mwrepo://test/public/archive', $this->file_hl0->getArchiveVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/public/archive/a/a2', $this->file_hl2->getArchiveVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/public/archive/%21', $this->file_hl0->getArchiveVirtualUrl( '!' ) );
+ $this->assertEquals( 'mwrepo://test/public/archive/a/a2/%21', $this->file_hl2->getArchiveVirtualUrl( '!' ) );
+ }
+
+ function testGetThumbVirtualUrl() {
+ $this->assertEquals( 'mwrepo://test/thumb/Test%21', $this->file_hl0->getThumbVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21', $this->file_hl2->getThumbVirtualUrl() );
+ $this->assertEquals( 'mwrepo://test/thumb/Test%21/%21', $this->file_hl0->getThumbVirtualUrl( '!' ) );
+ $this->assertEquals( 'mwrepo://test/thumb/a/a2/Test%21/%21', $this->file_hl2->getThumbVirtualUrl( '!' ) );
+ }
+
+ function testGetUrl() {
+ $this->assertEquals( '/testurl/Test%21', $this->file_hl0->getUrl() );
+ $this->assertEquals( '/testurl/a/a2/Test%21', $this->file_hl2->getUrl() );
+ }
+
+ function testWfLocalFile() {
+ $file = wfLocalFile( "File:Some_file_that_probably_doesn't exist.png" );
+ $this->assertThat( $file, $this->isInstanceOf( 'LocalFile' ), 'wfLocalFile() returns LocalFile for valid Titles' );
+ }
+}
+
+
--- /dev/null
+<?php
+
+class MessageTest extends PHPUnit_Framework_TestCase {
+
+ function setUp() {
+ global $wgLanguageCode, $wgLang, $wgContLang, $wgMessageCache;
+
+ $wgLanguageCode = 'en'; # For mainpage to be 'Main Page'
+ //Note that a Stub Object is not enough for this test
+ $wgContLang = $wgLang = Language::factory( $wgLanguageCode );
+ $wgMessageCache = new MessageCache( false, false, 3600 );
+ }
+
+ function testExists() {
+ $this->assertTrue( wfMessage( 'mainpage' )->exists() );
+ $this->assertTrue( wfMessage( 'mainpage' )->params( array() )->exists() );
+ $this->assertTrue( wfMessage( 'mainpage' )->rawParams( 'foo', 123 )->exists() );
+ $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->exists() );
+ $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->params( array() )->exists() );
+ $this->assertFalse( wfMessage( 'i-dont-exist-evar' )->rawParams( 'foo', 123 )->exists() );
+ }
+
+ function testKey() {
+ $this->assertType( 'Message', wfMessage( 'mainpage' ) );
+ $this->assertType( 'Message', wfMessage( 'i-dont-exist-evar' ) );
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->text() );
+ $this->assertEquals( '<i-dont-exist-evar>', wfMessage( 'i-dont-exist-evar' )->text() );
+ }
+
+ function testInLanguage() {
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( 'en' )->text() );
+ $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( 'ru' )->text() );
+ $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'en' ) )->text() );
+ $this->assertEquals( 'Заглавная страница', wfMessage( 'mainpage' )->inLanguage( Language::factory( 'ru' ) )->text() );
+ }
+
+ function testMessagePararms() {
+ $this->assertEquals( 'Return to $1.', wfMessage( 'returnto' )->text() );
+ $this->assertEquals( 'Return to $1.', wfMessage( 'returnto', array() )->text() );
+ $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', 'foo', 'bar' )->text() );
+ $this->assertEquals( 'You have foo (bar).', wfMessage( 'youhavenewmessages', array( 'foo', 'bar' ) )->text() );
+ }
+
+ /**
+ * @expectedException MWException
+ */
+ function testInLanguageThrows() {
+ wfMessage( 'foo' )->inLanguage( 123 );
+ }
+}
--- /dev/null
+<?php
+
+class ParserOptionsTest extends PHPUnit_Framework_TestCase {
+
+ private $popts;
+ private $pcache;
+
+ function setUp() {
+ ParserTest::setUp(); //reuse setup from parser tests
+ global $wgContLang, $wgUser;
+ $wgContLang = new StubContLang;
+ $this->popts = new ParserOptions( $wgUser );
+ $this->pcache = ParserCache::singleton();
+ }
+
+ function tearDown() {
+ parent::tearDown();
+ }
+
+ /**
+ * ParserOptions::optionsHash was not giving consistent results when $wgUseDynamicDates was set
+ * @group Database
+ */
+ function testGetParserCacheKeyWithDynamicDates() {
+ global $wgUseDynamicDates;
+ $wgUseDynamicDates = true;
+
+ $title = Title::newFromText( "Some test article" );
+ $article = new Article( $title );
+
+ $pcacheKeyBefore = $this->pcache->getKey( $article, $this->popts );
+ $this->assertNotNull( $this->popts->getDateFormat() );
+ $pcacheKeyAfter = $this->pcache->getKey( $article, $this->popts );
+ $this->assertEquals( $pcacheKeyBefore, $pcacheKeyAfter );
+ }
+}
--- /dev/null
+<?php
+
+class ResourceLoaderFileModuleTest extends PHPUnit_Framework_TestCase {
+ /* Provider Methods */
+
+ public function provide() {
+
+ }
+
+ /* Test Methods */
+
+ public function test() {
+
+ }
+}
--- /dev/null
+<?php
+
+class ResourceLoaderTest extends PHPUnit_Framework_TestCase {
+
+ protected static $resourceLoaderRegisterModulesHook;
+
+ /* Hook Methods */
+
+ /**
+ * ResourceLoaderRegisterModules hook
+ */
+ public static function resourceLoaderRegisterModules( &$resourceLoader ) {
+ self::$resourceLoaderRegisterModulesHook = true;
+ return true;
+ }
+
+ /* Provider Methods */
+ public function provideValidModules() {
+ return array(
+ array( 'TEST.validModule1', new ResourceLoaderTestModule() ),
+ );
+ }
+
+ /* Test Methods */
+
+ /**
+ * Ensures that the ResourceLoaderRegisterModules hook is called when a new ResourceLoader object is constructed
+ * @covers ResourceLoader::__construct
+ */
+ public function testCreatingNewResourceLoaderCallsRegistrationHook() {
+ self::$resourceLoaderRegisterModulesHook = false;
+ $resourceLoader = new ResourceLoader();
+ $this->assertTrue( self::$resourceLoaderRegisterModulesHook );
+ return $resourceLoader;
+ }
+
+ /**
+ * @dataProvider provideValidModules
+ * @depends testCreatingNewResourceLoaderCallsRegistrationHook
+ * @covers ResourceLoader::register
+ * @covers ResourceLoader::getModule
+ */
+ public function testRegisteredValidModulesAreAccessible(
+ $name, ResourceLoaderModule $module, ResourceLoader $resourceLoader
+ ) {
+ $resourceLoader->register( $name, $module );
+ $this->assertEquals( $module, $resourceLoader->getModule( $name ) );
+ }
+
+ /**
+ * Allthough ResourceLoader::register uses type hinting to prevent arbitrary information being passed through as a
+ * ResourceLoaderModule object, null can still get through.
+ *
+ * @depends testCreatingNewResourceLoaderCallsRegistrationHook
+ * @covers ResourceLoader::register
+ * @covers ResourceLoader::getModule
+ * @expectedException MWException
+ */
+ public function testRegisteringNullModuleThrowsAnException( ResourceLoader $resourceLoader ) {
+ $this->markTestIncomplete( "Broken by r77011" );
+ $resourceLoader->register( 'TEST.nullModule', null );
+ }
+}
+
+/* Stubs */
+
+class ResourceLoaderTestModule extends ResourceLoaderModule { }
+
+/* Hooks */
+global $wgHooks;
+$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules';
\ No newline at end of file
--- /dev/null
+<?php
+
+class RevisionTest extends PHPUnit_Framework_TestCase {
+ var $saveGlobals = array();
+
+ function setUp() {
+ global $wgContLang;
+ $wgContLang = Language::factory( 'en' );
+ $globalSet = array(
+ 'wgLegacyEncoding' => false,
+ 'wgCompressRevisions' => false,
+ 'wgInputEncoding' => 'utf-8',
+ 'wgOutputEncoding' => 'utf-8' );
+ foreach ( $globalSet as $var => $data ) {
+ $this->saveGlobals[$var] = $GLOBALS[$var];
+ $GLOBALS[$var] = $data;
+ }
+ }
+
+ function tearDown() {
+ foreach ( $this->saveGlobals as $var => $data ) {
+ $GLOBALS[$var] = $data;
+ }
+ }
+
+ function testGetRevisionText() {
+ $row = new stdClass;
+ $row->old_flags = '';
+ $row->old_text = 'This is a bunch of revision text.';
+ $this->assertEquals(
+ 'This is a bunch of revision text.',
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextGzip() {
+ $row = new stdClass;
+ $row->old_flags = 'gzip';
+ $row->old_text = gzdeflate( 'This is a bunch of revision text.' );
+ $this->assertEquals(
+ 'This is a bunch of revision text.',
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextUtf8Native() {
+ $row = new stdClass;
+ $row->old_flags = 'utf-8';
+ $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextUtf8Legacy() {
+ $row = new stdClass;
+ $row->old_flags = '';
+ $row->old_text = "Wiki est l'\xe9cole superieur !";
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextUtf8NativeGzip() {
+ $row = new stdClass;
+ $row->old_flags = 'gzip,utf-8';
+ $row->old_text = gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" );
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testGetRevisionTextUtf8LegacyGzip() {
+ $row = new stdClass;
+ $row->old_flags = 'gzip';
+ $row->old_text = gzdeflate( "Wiki est l'\xe9cole superieur !" );
+ $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1';
+ $this->assertEquals(
+ "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ) );
+ }
+
+ function testCompressRevisionTextUtf8() {
+ $row = new stdClass;
+ $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+ $row->old_flags = Revision::compressRevisionText( $row->old_text );
+ $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
+ "Flags should contain 'utf-8'" );
+ $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ),
+ "Flags should not contain 'gzip'" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ $row->old_text, "Direct check" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ), "getRevisionText" );
+ }
+
+ function testCompressRevisionTextUtf8Gzip() {
+ $GLOBALS['wgCompressRevisions'] = true;
+ $row = new stdClass;
+ $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
+ $row->old_flags = Revision::compressRevisionText( $row->old_text );
+ $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ),
+ "Flags should contain 'utf-8'" );
+ $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ),
+ "Flags should contain 'gzip'" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ gzinflate( $row->old_text ), "Direct check" );
+ $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
+ Revision::getRevisionText( $row ), "getRevisionText" );
+ }
+}
+
+
--- /dev/null
+<?php
+
+class TestSample extends PHPUnit_Framework_TestCase {
+
+ /**
+ * Anything that needs to happen before your tests should go here.
+ */
+ function setUp() {
+ global $wgContLang;
+ /* For example, we need to set $wgContLang for creating a new Title */
+ $wgContLang = Language::factory( 'en' );
+ }
+
+ /**
+ * Anything cleanup you need to do should go here.
+ */
+ function tearDown() {
+ }
+
+ /**
+ * Name tests so that PHPUnit can turn them into sentances when
+ * they run. While MediaWiki isn't strictly an Agile Programming
+ * project, you are encouraged to use the naming described under
+ * "Agile Documentation" at
+ * http://www.phpunit.de/manual/3.4/en/other-uses-for-tests.html
+ */
+ function testTitleObjectStringConversion() {
+ $title = Title::newFromText("text");
+ $this->assertEquals("Text", $title->__toString(), "Title creation");
+ $this->assertEquals("Text", "Text", "Automatic string conversion");
+
+ $title = Title::newFromText("text", NS_MEDIA);
+ $this->assertEquals("Media:Text", $title->__toString(), "Title creation with namespace");
+
+ }
+
+ /**
+ * If you want to run a the same test with a variety of data. use a data provider.
+ * see: http://www.phpunit.de/manual/3.4/en/writing-tests-for-phpunit.html
+ */
+ public function provideTitles() {
+ return array(
+ array( 'Text', NS_MEDIA, 'Media:Text' ),
+ array( 'Text', null, 'Text' ),
+ array( 'text', null, 'Text' ),
+ array( 'Text', NS_USER, 'User:Text' ),
+ array( 'Photo.jpg', NS_IMAGE, 'File:Photo.jpg' )
+ );
+ }
+
+ /**
+ * @dataProvider provideTitles
+ * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.dataProvider
+ */
+ public function testCreateBasicListOfTitles($titleName, $ns, $text) {
+ $title = Title::newFromText($titleName, $ns);
+ $this->assertEquals($text, "$title", "see if '$titleName' matches '$text'");
+ }
+
+ public function testSetUpMainPageTitleForNextTest() {
+ $title = Title::newMainPage();
+ $this->assertEquals("Main Page", "$title", "Test initial creation of a title");
+
+ return $title;
+ }
+
+ /**
+ * Instead of putting a bunch of tests in a single test method,
+ * you should put only one or two tests in each test method. This
+ * way, the test method names can remain descriptive.
+ *
+ * If you want to make tests depend on data created in another
+ * method, you can create dependencies feed whatever you return
+ * from the dependant method (e.g. testInitialCreation in this
+ * example) as arguments to the next method (e.g. $title in
+ * testTitleDepends is whatever testInitialCreatiion returned.)
+ */
+ /**
+ * @depends testSetUpMainPageTitleForNextTest
+ * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.depends
+ */
+ public function testCheckMainPageTitleIsConsideredLocal( $title ) {
+ $this->assertTrue( $title->isLocal() );
+ }
+
+ /**
+ * @expectedException MWException object
+ * See http://www.phpunit.de/manual/3.4/en/appendixes.annotations.html#appendixes.annotations.expectedException
+ */
+ function testTitleObjectFromObject() {
+ $title = Title::newFromText( new Title( "test" ) );
+ $this->assertEquals( "Test", $title->isLocal() );
+ }
+}
+
--- /dev/null
+<?php
+
+class SanitizerTest extends PHPUnit_Framework_TestCase {
+
+ function setUp() {
+ AutoLoader::loadClass( 'Sanitizer' );
+ }
+
+ function testDecodeNamedEntities() {
+ $this->assertEquals(
+ "\xc3\xa9cole",
+ Sanitizer::decodeCharReferences( 'école' ),
+ 'decode named entities'
+ );
+ }
+
+ function testDecodeNumericEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+ Sanitizer::decodeCharReferences( "Ĉio bonas dans l'école!" ),
+ 'decode numeric entities'
+ );
+ }
+
+ function testDecodeMixedEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole!",
+ Sanitizer::decodeCharReferences( "Ĉio bonas dans l'école!" ),
+ 'decode mixed numeric/named entities'
+ );
+ }
+
+ function testDecodeMixedComplexEntities() {
+ $this->assertEquals(
+ "\xc4\x88io bonas dans l'\xc3\xa9cole! (mais pas Ĉio dans l'école)",
+ Sanitizer::decodeCharReferences(
+ "Ĉio bonas dans l'école! (mais pas &#x108;io dans l'&eacute;cole)"
+ ),
+ 'decode mixed complex entities'
+ );
+ }
+
+ function testInvalidAmpersand() {
+ $this->assertEquals(
+ 'a & b',
+ Sanitizer::decodeCharReferences( 'a & b' ),
+ 'Invalid ampersand'
+ );
+ }
+
+ function testInvalidEntities() {
+ $this->assertEquals(
+ '&foo;',
+ Sanitizer::decodeCharReferences( '&foo;' ),
+ 'Invalid named entity'
+ );
+ }
+
+ function testInvalidNumberedEntities() {
+ $this->assertEquals( UTF8_REPLACEMENT, Sanitizer::decodeCharReferences( "�" ), 'Invalid numbered entity' );
+ }
+
+ function testSelfClosingTag() {
+ $GLOBALS['wgUseTidy'] = false;
+ $this->assertEquals(
+ '<div>Hello world</div>',
+ Sanitizer::removeHTMLtags( '<div>Hello world</div />' ),
+ 'Self-closing closing div'
+ );
+ }
+}
+
--- /dev/null
+<?php
+
+class SeleniumConfigurationTest extends PHPUnit_Framework_TestCase {
+
+ /*
+ * The file where the test temporarity stores the selenium config.
+ * This should be cleaned up as part of teardown.
+ */
+ private $tempFileName;
+
+ /*
+ * String containing the a sample selenium settings
+ */
+ private $testConfig0 =
+'
+[SeleniumSettings]
+browsers[firefox] = "*firefox"
+browsers[iexplorer] = "*iexploreproxy"
+browsers[chrome] = "*chrome"
+host = "localhost"
+port = "foobarr"
+wikiUrl = "http://localhost/deployment"
+username = "xxxxxxx"
+userPassword = ""
+testBrowser = "chrome"
+startserver =
+stopserver =
+jUnitLogFile =
+runAgainstGrid = false
+
+[SeleniumTests]
+testSuite[SimpleSeleniumTestSuite] = "maintenance/tests/selenium/SimpleSeleniumTestSuite.php"
+testSuite[TestSuiteName] = "testSuitePath"
+';
+ /*
+ * Array of expected browsers from $testConfig0
+ */
+ private $testBrowsers0 = array( 'firefox' => '*firefox',
+ 'iexplorer' => '*iexploreproxy',
+ 'chrome' => '*chrome'
+ );
+ /*
+ * Array of expected selenium settings from $testConfig0
+ */
+ private $testSettings0 = array(
+ 'host' => 'localhost',
+ 'port' => 'foobarr',
+ 'wikiUrl' => 'http://localhost/deployment',
+ 'username' => 'xxxxxxx',
+ 'userPassword' => '',
+ 'testBrowser' => 'chrome',
+ 'startserver' => null,
+ 'stopserver' => null,
+ 'seleniumserverexecpath' => null,
+ 'jUnitLogFile' => null,
+ 'runAgainstGrid' => null
+ );
+ /*
+ * Array of expected testSuites from $testConfig0
+ */
+ private $testSuites0 = array(
+ 'SimpleSeleniumTestSuite' => 'maintenance/tests/selenium/SimpleSeleniumTestSuite.php',
+ 'TestSuiteName' => 'testSuitePath'
+ );
+
+
+ /*
+ * Another sample selenium settings file contents
+ */
+ private $testConfig1 =
+'
+[SeleniumSettings]
+host = "localhost"
+testBrowser = "firefox"
+';
+ /*
+ * Expected browsers from $testConfig1
+ */
+ private $testBrowsers1 = null;
+ /*
+ * Expected selenium settings from $testConfig1
+ */
+ private $testSettings1 = array(
+ 'host' => 'localhost',
+ 'port' => null,
+ 'wikiUrl' => null,
+ 'username' => null,
+ 'userPassword' => null,
+ 'testBrowser' => 'firefox',
+ 'startserver' => null,
+ 'stopserver' => null,
+ 'seleniumserverexecpath' => null,
+ 'jUnitLogFile' => null,
+ 'runAgainstGrid' => null
+ );
+ /*
+ * Expected test suites from $testConfig1
+ */
+ private $testSuites1 = null;
+
+
+ public function setUp() {
+ if ( !defined( 'SELENIUMTEST' ) ) {
+ define( 'SELENIUMTEST', true );
+ }
+ }
+
+ /*
+ * Clean up the temporary file used to store the selenium settings.
+ */
+ public function tearDown() {
+ if ( strlen( $this->tempFileName ) > 0 ) {
+ unlink( $this->tempFileName );
+ unset( $this->tempFileName );
+ }
+ parent::tearDown();
+ }
+
+ /**
+ * @expectedException MWException
+ * @group SeleniumFramework
+ */
+ public function testErrorOnIncorrectConfigFile() {
+ $seleniumSettings;
+ $seleniumBrowsers;
+ $seleniumTestSuites;
+
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites,
+ "Some_fake_settings_file.ini" );
+
+ }
+
+ /**
+ * @expectedException MWException
+ * @group SeleniumFramework
+ */
+ public function testErrorOnMissingConfigFile() {
+ $seleniumSettings;
+ $seleniumBrowsers;
+ $seleniumTestSuites;
+ global $wgSeleniumConfigFile;
+ $wgSeleniumConfigFile = '';
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites);
+ }
+
+ /**
+ * @group SeleniumFramework
+ */
+ public function testUsesGlobalVarForConfigFile() {
+ $seleniumSettings;
+ $seleniumBrowsers;
+ $seleniumTestSuites;
+ global $wgSeleniumConfigFile;
+ $this->writeToTempFile( $this->testConfig0 );
+ $wgSeleniumConfigFile = $this->tempFileName;
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites);
+ $this->assertEquals($seleniumSettings, $this->testSettings0 ,
+ 'The selenium settings should have been read from the file defined in $wgSeleniumConfigFile'
+ );
+ $this->assertEquals($seleniumBrowsers, $this->testBrowsers0,
+ 'The available browsers should have been read from the file defined in $wgSeleniumConfigFile'
+ );
+ $this->assertEquals($seleniumTestSuites, $this->testSuites0,
+ 'The test suites should have been read from the file defined in $wgSeleniumConfigFile'
+ );
+ }
+
+ /**
+ * @group SeleniumFramework
+ * @dataProvider sampleConfigs
+ */
+ public function testgetSeleniumSettings($sampleConfig, $expectedSettings, $expectedBrowsers, $expectedSuites ) {
+ $this->writeToTempFile( $sampleConfig );
+ $seleniumSettings;
+ $seleniumBrowsers;
+ $seleniumTestSuites;
+
+ SeleniumConfig::getSeleniumSettings($seleniumSettings,
+ $seleniumBrowsers,
+ $seleniumTestSuites,
+ $this->tempFileName );
+
+ $this->assertEquals($seleniumSettings, $expectedSettings,
+ "The selenium settings for the following test configuration was not retrieved correctly" . $sampleConfig
+ );
+ $this->assertEquals($seleniumBrowsers, $expectedBrowsers,
+ "The available browsers for the following test configuration was not retrieved correctly" . $sampleConfig
+ );
+ $this->assertEquals($seleniumTestSuites, $expectedSuites,
+ "The test suites for the following test configuration was not retrieved correctly" . $sampleConfig
+ );
+
+
+ }
+
+ /*
+ * create a temp file and write text to it.
+ * @param $testToWrite the text to write to the temp file
+ */
+ private function writeToTempFile($textToWrite) {
+ $this->tempFileName = tempnam(sys_get_temp_dir(), 'test_settings.');
+ $tempFile = fopen( $this->tempFileName, "w" );
+ fwrite($tempFile , $textToWrite);
+ fclose($tempFile);
+ }
+
+ /*
+ * Returns an array containing:
+ * The contents of the selenium cingiguration ini file
+ * The expected selenium configuration array that getSeleniumSettings should return
+ * The expected available browsers array that getSeleniumSettings should return
+ * The expected test suites arrya that getSeleniumSettings should return
+ */
+ public function sampleConfigs() {
+ return array(
+ array($this->testConfig0, $this->testSettings0, $this->testBrowsers0, $this->testSuites0 ),
+ array($this->testConfig1, $this->testSettings1, $this->testBrowsers1, $this->testSuites1 )
+ );
+ }
+
+
+}
--- /dev/null
+<?php
+
+function getSiteParams( $conf, $wiki ) {
+ $site = null;
+ $lang = null;
+ foreach ( $conf->suffixes as $suffix ) {
+ if ( substr( $wiki, -strlen( $suffix ) ) == $suffix ) {
+ $site = $suffix;
+ $lang = substr( $wiki, 0, -strlen( $suffix ) );
+ break;
+ }
+ }
+ return array(
+ 'suffix' => $site,
+ 'lang' => $lang,
+ 'params' => array(
+ 'lang' => $lang,
+ 'site' => $site,
+ 'wiki' => $wiki,
+ ),
+ 'tags' => array( 'tag' ),
+ );
+}
+
+class SiteConfigurationTest extends PHPUnit_Framework_TestCase {
+ var $mConf;
+
+ function setUp() {
+ $this->mConf = new SiteConfiguration;
+
+ $this->mConf->suffixes = array( 'wiki' );
+ $this->mConf->wikis = array( 'enwiki', 'dewiki', 'frwiki' );
+ $this->mConf->settings = array(
+ 'simple' => array(
+ 'wiki' => 'wiki',
+ 'tag' => 'tag',
+ 'enwiki' => 'enwiki',
+ 'dewiki' => 'dewiki',
+ 'frwiki' => 'frwiki',
+ ),
+
+ 'fallback' => array(
+ 'default' => 'default',
+ 'wiki' => 'wiki',
+ 'tag' => 'tag',
+ ),
+
+ 'params' => array(
+ 'default' => '$lang $site $wiki',
+ ),
+
+ '+global' => array(
+ 'wiki' => array(
+ 'wiki' => 'wiki',
+ ),
+ 'tag' => array(
+ 'tag' => 'tag',
+ ),
+ 'enwiki' => array(
+ 'enwiki' => 'enwiki',
+ ),
+ 'dewiki' => array(
+ 'dewiki' => 'dewiki',
+ ),
+ 'frwiki' => array(
+ 'frwiki' => 'frwiki',
+ ),
+ ),
+
+ 'merge' => array(
+ '+wiki' => array(
+ 'wiki' => 'wiki',
+ ),
+ '+tag' => array(
+ 'tag' => 'tag',
+ ),
+ 'default' => array(
+ 'default' => 'default',
+ ),
+ '+enwiki' => array(
+ 'enwiki' => 'enwiki',
+ ),
+ '+dewiki' => array(
+ 'dewiki' => 'dewiki',
+ ),
+ '+frwiki' => array(
+ 'frwiki' => 'frwiki',
+ ),
+ ),
+ );
+
+ $GLOBALS['global'] = array( 'global' => 'global' );
+ }
+
+
+ function testSiteFromDB() {
+ $this->assertEquals(
+ array( 'wikipedia', 'en' ),
+ $this->mConf->siteFromDB( 'enwiki' ),
+ 'siteFromDB()'
+ );
+ $this->assertEquals(
+ array( 'wikipedia', '' ),
+ $this->mConf->siteFromDB( 'wiki' ),
+ 'siteFromDB() on a suffix'
+ );
+ $this->assertEquals(
+ array( null, null ),
+ $this->mConf->siteFromDB( 'wikien' ),
+ 'siteFromDB() on a non-existing wiki'
+ );
+
+ $this->mConf->suffixes = array( 'wiki', '' );
+ $this->assertEquals(
+ array( '', 'wikien' ),
+ $this->mConf->siteFromDB( 'wikien' ),
+ 'siteFromDB() on a non-existing wiki (2)'
+ );
+ }
+
+ function testGetLocalDatabases() {
+ $this->assertEquals(
+ array( 'enwiki', 'dewiki', 'frwiki' ),
+ $this->mConf->getLocalDatabases(),
+ 'getLocalDatabases()'
+ );
+ }
+
+ function testGet() {
+ $this->assertEquals(
+ 'enwiki',
+ $this->mConf->get( 'simple', 'enwiki', 'wiki' ),
+ 'get(): simple setting on an existing wiki'
+ );
+ $this->assertEquals(
+ 'dewiki',
+ $this->mConf->get( 'simple', 'dewiki', 'wiki' ),
+ 'get(): simple setting on an existing wiki (2)'
+ );
+ $this->assertEquals(
+ 'frwiki',
+ $this->mConf->get( 'simple', 'frwiki', 'wiki' ),
+ 'get(): simple setting on an existing wiki (3)'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'simple', 'wiki', 'wiki' ),
+ 'get(): simple setting on an suffix'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'simple', 'eswiki', 'wiki' ),
+ 'get(): simple setting on an non-existing wiki'
+ );
+
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'enwiki', 'wiki' ),
+ 'get(): fallback setting on an existing wiki'
+ );
+ $this->assertEquals(
+ 'tag',
+ $this->mConf->get( 'fallback', 'dewiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): fallback setting on an existing wiki (with wiki tag)'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'wiki', 'wiki' ),
+ 'get(): fallback setting on an suffix'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'wiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): fallback setting on an suffix (with wiki tag)'
+ );
+ $this->assertEquals(
+ 'wiki',
+ $this->mConf->get( 'fallback', 'eswiki', 'wiki' ),
+ 'get(): fallback setting on an non-existing wiki'
+ );
+ $this->assertEquals(
+ 'tag',
+ $this->mConf->get( 'fallback', 'eswiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): fallback setting on an non-existing wiki (with wiki tag)'
+ );
+
+ $common = array( 'wiki' => 'wiki', 'default' => 'default' );
+ $commonTag = array( 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' );
+ $this->assertEquals(
+ array( 'enwiki' => 'enwiki' ) + $common,
+ $this->mConf->get( 'merge', 'enwiki', 'wiki' ),
+ 'get(): merging setting on an existing wiki'
+ );
+ $this->assertEquals(
+ array( 'enwiki' => 'enwiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'enwiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an existing wiki (with tag)'
+ );
+ $this->assertEquals(
+ array( 'dewiki' => 'dewiki' ) + $common,
+ $this->mConf->get( 'merge', 'dewiki', 'wiki' ),
+ 'get(): merging setting on an existing wiki (2)'
+ );
+ $this->assertEquals(
+ array( 'dewiki' => 'dewiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'dewiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an existing wiki (2) (with tag)'
+ );
+ $this->assertEquals(
+ array( 'frwiki' => 'frwiki' ) + $common,
+ $this->mConf->get( 'merge', 'frwiki', 'wiki' ),
+ 'get(): merging setting on an existing wiki (3)'
+ );
+ $this->assertEquals(
+ array( 'frwiki' => 'frwiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'frwiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an existing wiki (3) (with tag)'
+ );
+ $this->assertEquals(
+ array( 'wiki' => 'wiki' ) + $common,
+ $this->mConf->get( 'merge', 'wiki', 'wiki' ),
+ 'get(): merging setting on an suffix'
+ );
+ $this->assertEquals(
+ array( 'wiki' => 'wiki' ) + $commonTag,
+ $this->mConf->get( 'merge', 'wiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an suffix (with tag)'
+ );
+ $this->assertEquals(
+ $common,
+ $this->mConf->get( 'merge', 'eswiki', 'wiki' ),
+ 'get(): merging setting on an non-existing wiki'
+ );
+ $this->assertEquals(
+ $commonTag,
+ $this->mConf->get( 'merge', 'eswiki', 'wiki', array(), array( 'tag' ) ),
+ 'get(): merging setting on an non-existing wiki (with tag)'
+ );
+ }
+
+ function testSiteFromDBWithCallback() {
+ $this->mConf->siteParamsCallback = 'getSiteParams';
+
+ $this->assertEquals(
+ array( 'wiki', 'en' ),
+ $this->mConf->siteFromDB( 'enwiki' ),
+ 'siteFromDB() with callback'
+ );
+ $this->assertEquals(
+ array( 'wiki', '' ),
+ $this->mConf->siteFromDB( 'wiki' ),
+ 'siteFromDB() with callback on a suffix'
+ );
+ $this->assertEquals(
+ array( null, null ),
+ $this->mConf->siteFromDB( 'wikien' ),
+ 'siteFromDB() with callback on a non-existing wiki'
+ );
+ }
+
+ function testParamReplacement() {
+ $this->mConf->siteParamsCallback = 'getSiteParams';
+
+ $this->assertEquals(
+ 'en wiki enwiki',
+ $this->mConf->get( 'params', 'enwiki', 'wiki' ),
+ 'get(): parameter replacement on an existing wiki'
+ );
+ $this->assertEquals(
+ 'de wiki dewiki',
+ $this->mConf->get( 'params', 'dewiki', 'wiki' ),
+ 'get(): parameter replacement on an existing wiki (2)'
+ );
+ $this->assertEquals(
+ 'fr wiki frwiki',
+ $this->mConf->get( 'params', 'frwiki', 'wiki' ),
+ 'get(): parameter replacement on an existing wiki (3)'
+ );
+ $this->assertEquals(
+ ' wiki wiki',
+ $this->mConf->get( 'params', 'wiki', 'wiki' ),
+ 'get(): parameter replacement on an suffix'
+ );
+ $this->assertEquals(
+ 'es wiki eswiki',
+ $this->mConf->get( 'params', 'eswiki', 'wiki' ),
+ 'get(): parameter replacement on an non-existing wiki'
+ );
+ }
+
+ function testGetAll() {
+ $this->mConf->siteParamsCallback = 'getSiteParams';
+
+ $getall = array(
+ 'simple' => 'enwiki',
+ 'fallback' => 'tag',
+ 'params' => 'en wiki enwiki',
+ 'global' => array( 'enwiki' => 'enwiki' ) + $GLOBALS['global'],
+ 'merge' => array( 'enwiki' => 'enwiki', 'tag' => 'tag', 'wiki' => 'wiki', 'default' => 'default' ),
+ );
+ $this->assertEquals( $getall, $this->mConf->getAll( 'enwiki' ), 'getAll()' );
+
+ $this->mConf->extractAllGlobals( 'enwiki', 'wiki' );
+
+ $this->assertEquals( $getall['simple'], $GLOBALS['simple'], 'extractAllGlobals(): simple setting' );
+ $this->assertEquals( $getall['fallback'], $GLOBALS['fallback'], 'extractAllGlobals(): fallback setting' );
+ $this->assertEquals( $getall['params'], $GLOBALS['params'], 'extractAllGlobals(): parameter replacement' );
+ $this->assertEquals( $getall['global'], $GLOBALS['global'], 'extractAllGlobals(): merging with global' );
+ $this->assertEquals( $getall['merge'], $GLOBALS['merge'], 'extractAllGlobals(): merging setting' );
+ }
+}
--- /dev/null
+<?php
+
+class TimeAdjustTest extends PHPUnit_Framework_TestCase {
+ static $offset;
+
+ public function setUp() {
+ global $wgLocalTZoffset;
+ self::$offset = $wgLocalTZoffset;
+
+ $this->iniSet( 'precision', 15 );
+ }
+
+ public function tearDown() {
+ global $wgLocalTZoffset;
+ $wgLocalTZoffset = self::$offset;
+ }
+
+ # Test offset usage for a given language::userAdjust
+ function testUserAdjust() {
+ global $wgLocalTZoffset, $wgContLang;
+
+ $wgContLang = $en = Language::factory( 'en' );
+
+ # Collection of parameters for Language_t_Offset.
+ # Format: date to be formatted, localTZoffset value, expected date
+ $userAdjust_tests = array(
+ array( 20061231235959, 0, 20061231235959 ),
+ array( 20061231235959, 5, 20070101000459 ),
+ array( 20061231235959, 15, 20070101001459 ),
+ array( 20061231235959, 60, 20070101005959 ),
+ array( 20061231235959, 90, 20070101012959 ),
+ array( 20061231235959, 120, 20070101015959 ),
+ array( 20061231235959, 540, 20070101085959 ),
+ array( 20061231235959, -5, 20061231235459 ),
+ array( 20061231235959, -30, 20061231232959 ),
+ array( 20061231235959, -60, 20061231225959 ),
+ );
+
+ foreach ( $userAdjust_tests as $data ) {
+ $wgLocalTZoffset = $data[1];
+
+ $this->assertEquals(
+ strval( $data[2] ),
+ strval( $en->userAdjust( $data[0], '' ) ),
+ "User adjust {$data[0]} by {$data[1]} minutes should give {$data[2]}"
+ );
+ }
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group Database
+ * @group Destructive
+ */
+class TitlePermissionTest extends PHPUnit_Framework_TestCase {
+ static $title;
+ static $user;
+ static $anonUser;
+ static $userUser;
+ static $altUser;
+ static $userName;
+ static $altUserName;
+
+ function setUp() {
+ global $wgLocaltimezone, $wgLocalTZoffset, $wgMemc, $wgContLang, $wgLang, $wgMessageCache;
+
+ if(!$wgMemc) {
+ $wgMemc = new FakeMemCachedClient;
+ }
+ $wgMessageCache = new MessageCache( $wgMemc, true, 3600 );
+ $wgContLang = $wgLang = Language::factory( 'en' );
+
+ self::$userName = "Useruser";
+ self::$altUserName = "Altuseruser";
+ date_default_timezone_set( $wgLocaltimezone );
+ $wgLocalTZoffset = date( "Z" ) / 60;
+
+ self::$title = Title::makeTitle( NS_MAIN, "Main Page" );
+ if ( !isset( self::$userUser ) || !( self::$userUser instanceOf User ) ) {
+ self::$userUser = User::newFromName( self::$userName );
+
+ if ( !self::$userUser->getID() ) {
+ self::$userUser = User::createNew( self::$userName, array(
+ "email" => "test@example.com",
+ "real_name" => "Test User" ) );
+ }
+
+ self::$altUser = User::newFromName( self::$altUserName );
+ if ( !self::$altUser->getID() ) {
+ self::$altUser = User::createNew( self::$altUserName, array(
+ "email" => "alttest@example.com",
+ "real_name" => "Test User Alt" ) );
+ }
+
+ self::$anonUser = User::newFromId( 0 );
+
+ self::$user = self::$userUser;
+ }
+ }
+
+ function setUserPerm( $perm ) {
+ if ( is_array( $perm ) ) {
+ self::$user->mRights = $perm;
+ } else {
+ self::$user->mRights = array( $perm );
+ }
+ }
+
+ function setTitle( $ns, $title = "Main_Page" ) {
+ self::$title = Title::makeTitle( $ns, $title );
+ }
+
+ function setUser( $userName = null ) {
+ if ( $userName === 'anon' ) {
+ self::$user = self::$anonUser;
+ } else if ( $userName === null || $userName === self::$userName ) {
+ self::$user = self::$userUser;
+ } else {
+ self::$user = self::$altUser;
+ }
+
+ global $wgUser;
+ $wgUser = self::$user;
+ }
+
+ function testQuickPermissions() {
+ global $wgContLang;
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array(), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( array( "nocreatetext" ) ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( array( 'nocreatetext' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( array( 'nocreatetext' ) ), $res );
+
+ $this->setUser( self::$userName );
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createtalk" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "createpage" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_TALK );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createpage" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "createtalk" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'create', self::$user );
+ $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_USER, self::$userName . '' );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, self::$userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, self::$userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, self::$userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, self::$userName . '' );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, self::$userName . '/subpage' );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, self::$userName . '' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_USER, self::$userName . '/subpage' );
+ $this->setUserPerm( "move-rootuserpages" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setUser( self::$userName );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenotallowed' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ), $res );
+
+ $this->setTitle( NS_FILE, "img.png" );
+ $this->setUserPerm( "movefile" );
+ $res = self::$title->getUserPermissionsErrors( 'move', self::$user );
+ $this->assertEquals( array( array( 'movenologintext' ) ), $res );
+
+ $this->setUser( self::$userName );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ) );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ) ) );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ),
+ array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUser( 'anon' );
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( ) );
+
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ),
+ array( array( 'movenologintext' ) ) );
+
+ $this->setUser( self::$userName );
+ $this->setUserPerm( "" );
+ $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ) );
+
+ $this->setUserPerm( "move" );
+ $this->runGroupPermissions( 'move', array( ) );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( 'move' );
+ $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUserPerm( '' );
+ $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user );
+ $this->assertEquals( array( array( 'movenotallowed' ) ), $res );
+
+ $this->setTitle( NS_USER );
+ $this->setUser( self::$userName );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUserPerm( "move" );
+ $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user );
+ $this->assertEquals( array( array( 'cant-move-to-user-page' ) ), $res );
+
+ $this->setUser( 'anon' );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setTitle( NS_USER, "User/subpage" );
+ $this->setUserPerm( array( "move", "move-rootuserpages" ) );
+ $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUserPerm( "move" );
+ $res = self::$title->getUserPermissionsErrors( 'move-target', self::$user );
+ $this->assertEquals( array( ), $res );
+
+ $this->setUser( 'anon' );
+ $check = array( 'edit' => array( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( ), true ),
+ 'protect' => array( array( array( 'badaccess-groups', "[[$prefix:Administrators|Administrators]]", 1 ), array( 'protect-cantedit' ) ),
+ array( array( 'badaccess-group0' ), array( 'protect-cantedit' ) ),
+ array( array( 'protect-cantedit' ) ), false ),
+ '' => array( array( ), array( ), array( ), true ) );
+ global $wgUser;
+ $wgUser = self::$user;
+ foreach ( array( "edit", "protect", "" ) as $action ) {
+ $this->setUserPerm( null );
+ $this->assertEquals( $check[$action][0],
+ self::$title->getUserPermissionsErrors( $action, self::$user, true ) );
+
+ global $wgGroupPermissions;
+ $old = $wgGroupPermissions;
+ $wgGroupPermissions = array();
+
+ $this->assertEquals( $check[$action][1],
+ self::$title->getUserPermissionsErrors( $action, self::$user, true ) );
+ $wgGroupPermissions = $old;
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][2],
+ self::$title->getUserPermissionsErrors( $action, self::$user, true ) );
+
+ $this->setUserPerm( $action );
+ $this->assertEquals( $check[$action][3],
+ self::$title->userCan( $action, true ) );
+ $this->assertEquals( $check[$action][3],
+ self::$title->quickUserCan( $action, false ) );
+
+ # count( User::getGroupsWithPermissions( $action ) ) < 1
+ }
+ }
+
+ function runGroupPermissions( $action, $result, $result2 = null ) {
+ global $wgGroupPermissions;
+
+ if ( $result2 === null ) $result2 = $result;
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = self::$title->getUserPermissionsErrors( $action, self::$user );
+ $this->assertEquals( $result, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = false;
+ $res = self::$title->getUserPermissionsErrors( $action, self::$user );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = true;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = self::$title->getUserPermissionsErrors( $action, self::$user );
+ $this->assertEquals( $result2, $res );
+
+ $wgGroupPermissions['autoconfirmed']['move'] = false;
+ $wgGroupPermissions['user']['move'] = true;
+ $res = self::$title->getUserPermissionsErrors( $action, self::$user );
+ $this->assertEquals( $result2, $res );
+ }
+
+ function testPermissionHooks() { }
+ function testSpecialsAndNSPermissions() {
+ $this->setUser( self::$userName );
+ global $wgUser, $wgContLang;
+ $wgUser = self::$user;
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $this->setTitle( NS_SPECIAL );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'ns-specialprotected' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+ $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Administrators|Administrators]]", 2 ) ),
+ self::$title->getUserPermissionsErrors( 'createaccount', self::$user ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ self::$title->getUserPermissionsErrors( 'execute', self::$user ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+
+ $this->setTitle( NS_MAIN );
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+
+ global $wgNamespaceProtection;
+ $wgNamespaceProtection[NS_USER] = array ( 'bogus' );
+ $this->setTitle( NS_USER );
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ), array( 'namespaceprotected', 'User' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+
+ $this->setTitle( NS_MEDIAWIKI );
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( array( 'protectedinterface' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+
+ $wgNamespaceProtection = null;
+ $this->setUserPerm( 'bogus' );
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+ $this->assertEquals( true,
+ self::$title->userCan( 'bogus' ) );
+
+ $this->setUserPerm( '' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+ $this->assertEquals( false,
+ self::$title->userCan( 'bogus' ) );
+ }
+
+ function testCSSandJSPermissions() {
+ $this->setUser( self::$userName );
+ global $wgUser;
+ $wgUser = self::$user;
+
+ $this->setTitle( NS_USER, self::$altUserName . '/test.js' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ),
+ array( array( 'badaccess-group0' ) ) );
+
+ $this->setTitle( NS_USER, self::$altUserName . '/test.css' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ), array( 'customcssjsprotected' ) ) );
+
+ $this->setTitle( NS_USER, self::$altUserName . '/tempo' );
+ $this->runCSSandJSPermissions(
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ),
+ array( array( 'badaccess-group0' ) ) );
+ }
+
+ function runCSSandJSPermissions( $result0, $result1, $result2 ) {
+ $this->setUserPerm( '' );
+ $this->assertEquals( $result0,
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+
+ $this->setUserPerm( 'editusercss' );
+ $this->assertEquals( $result1,
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+
+ $this->setUserPerm( 'edituserjs' );
+ $this->assertEquals( $result2,
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+
+ $this->setUserPerm( 'editusercssjs' );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+
+ $this->setUserPerm( array( 'edituserjs', 'editusercss' ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+ }
+
+ function testPageRestrictions() {
+ global $wgUser, $wgContLang;
+
+ $prefix = $wgContLang->getFormattedNsText( NS_PROJECT );
+
+ $wgUser = self::$user;
+ $this->setTitle( NS_MAIN );
+ self::$title->mRestrictionsLoaded = true;
+ $this->setUserPerm( "edit" );
+ self::$title->mRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'edit',
+ self::$user ) );
+
+ $this->assertEquals( true,
+ self::$title->quickUserCan( 'edit', false ) );
+ self::$title->mRestrictions = array( "edit" => array( 'bogus', "sysop", "protect", "" ),
+ "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ self::$title->getUserPermissionsErrors( 'edit',
+ self::$user ) );
+ $this->setUserPerm( "" );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+ $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ self::$title->getUserPermissionsErrors( 'edit',
+ self::$user ) );
+ $this->setUserPerm( array( "edit", "editprotected" ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'edit',
+ self::$user ) );
+ self::$title->mCascadeRestriction = true;
+ $this->assertEquals( false,
+ self::$title->quickUserCan( 'bogus', false ) );
+ $this->assertEquals( false,
+ self::$title->quickUserCan( 'edit', false ) );
+ $this->assertEquals( array( array( 'badaccess-group0' ),
+ array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ self::$title->getUserPermissionsErrors( 'bogus',
+ self::$user ) );
+ $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ),
+ array( 'protectedpagetext', 'protect' ),
+ array( 'protectedpagetext', 'protect' ) ),
+ self::$title->getUserPermissionsErrors( 'edit',
+ self::$user ) );
+ }
+
+ function testCascadingSourcesRestrictions() {
+ global $wgUser;
+ $wgUser = self::$user;
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->setUserPerm( array( "edit", "bogus" ) );
+
+ self::$title->mCascadeSources = array( Title::makeTitle( NS_MAIN, "Bogus" ), Title::makeTitle( NS_MAIN, "UnBogus" ) );
+ self::$title->mCascadingRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) );
+
+ $this->assertEquals( false,
+ self::$title->userCan( 'bogus' ) );
+ $this->assertEquals( array( array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ),
+ array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ) ),
+ self::$title->getUserPermissionsErrors( 'bogus', self::$user ) );
+
+ $this->assertEquals( true,
+ self::$title->userCan( 'edit' ) );
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'edit', self::$user ) );
+
+ }
+
+ function testActionPermissions() {
+ global $wgUser;
+ $wgUser = self::$user;
+
+ $this->setUserPerm( array( "createpage" ) );
+ $this->setTitle( NS_MAIN, "test page" );
+ self::$title->mTitleProtection['pt_create_perm'] = '';
+ self::$title->mTitleProtection['pt_user'] = self::$user->getID();
+ self::$title->mTitleProtection['pt_expiry'] = Block::infinity();
+ self::$title->mTitleProtection['pt_reason'] = 'test';
+ self::$title->mCascadeRestriction = false;
+
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ self::$title->getUserPermissionsErrors( 'create', self::$user ) );
+ $this->assertEquals( false,
+ self::$title->userCan( 'create' ) );
+
+ self::$title->mTitleProtection['pt_create_perm'] = 'sysop';
+ $this->setUserPerm( array( 'createpage', 'protect' ) );
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'create', self::$user ) );
+ $this->assertEquals( true,
+ self::$title->userCan( 'create' ) );
+
+
+ $this->setUserPerm( array( 'createpage' ) );
+ $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ),
+ self::$title->getUserPermissionsErrors( 'create', self::$user ) );
+ $this->assertEquals( false,
+ self::$title->userCan( 'create' ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->setUserPerm( array( "move" ) );
+ $this->assertEquals( false,
+ self::$title->userCan( 'move' ) );
+ $this->assertEquals( array( array( 'immobile-source-namespace', 'Media' ) ),
+ self::$title->getUserPermissionsErrors( 'move', self::$user ) );
+
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'move', self::$user ) );
+ $this->assertEquals( true,
+ self::$title->userCan( 'move' ) );
+
+ self::$title->mInterwiki = "no";
+ $this->assertEquals( array( array( 'immobile-page' ) ),
+ self::$title->getUserPermissionsErrors( 'move', self::$user ) );
+ $this->assertEquals( false,
+ self::$title->userCan( 'move' ) );
+
+ $this->setTitle( NS_MEDIA, "test page" );
+ $this->assertEquals( false,
+ self::$title->userCan( 'move-target' ) );
+ $this->assertEquals( array( array( 'immobile-target-namespace', 'Media' ) ),
+ self::$title->getUserPermissionsErrors( 'move-target', self::$user ) );
+
+ $this->setTitle( NS_MAIN, "test page" );
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'move-target', self::$user ) );
+ $this->assertEquals( true,
+ self::$title->userCan( 'move-target' ) );
+
+ self::$title->mInterwiki = "no";
+ $this->assertEquals( array( array( 'immobile-target-page' ) ),
+ self::$title->getUserPermissionsErrors( 'move-target', self::$user ) );
+ $this->assertEquals( false,
+ self::$title->userCan( 'move-target' ) );
+
+ }
+
+ function testUserBlock() {
+ global $wgUser, $wgEmailConfirmToEdit, $wgEmailAuthentication;
+ $wgEmailConfirmToEdit = true;
+ $wgEmailAuthentication = true;
+ $wgUser = self::$user;
+
+ $this->setUserPerm( array( "createpage", "move" ) );
+ $this->setTitle( NS_MAIN, "test page" );
+
+ # $short
+ $this->assertEquals( array( array( 'confirmedittext' ) ),
+ self::$title->getUserPermissionsErrors( 'move-target', self::$user ) );
+ $wgEmailConfirmToEdit = false;
+ $this->assertEquals( true, self::$title->userCan( 'move-target' ) );
+
+ # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount'
+ $this->assertEquals( array( ),
+ self::$title->getUserPermissionsErrors( 'move-target',
+ self::$user ) );
+
+ global $wgLang;
+ $prev = time();
+ $now = time() + 120;
+ self::$user->mBlockedby = self::$user->getId();
+ self::$user->mBlock = new Block( '127.0.8.1', self::$user->getId(), self::$user->getId(),
+ 'no reason given', $prev + 3600, 1, 0 );
+ self::$user->mBlock->mTimestamp = 0;
+ $this->assertEquals( array( array( 'autoblockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', 0, 'infinite', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ) ),
+ self::$title->getUserPermissionsErrors( 'move-target',
+ self::$user ) );
+
+ $this->assertEquals( false,
+ self::$title->userCan( 'move-target', self::$user ) );
+
+ global $wgLocalTZoffset;
+ $wgLocalTZoffset = -60;
+ self::$user->mBlockedby = self::$user->getName();
+ self::$user->mBlock = new Block( '127.0.8.1', 2, 1, 'no reason given', $now, 0, 10 );
+ $this->assertEquals( array( array( 'blockedtext',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', 0, '23:00, 31 December 1969', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ) ),
+ self::$title->getUserPermissionsErrors( 'move-target', self::$user ) );
+
+ # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this )
+ # $user->blockedFor() == ''
+ # $user->mBlock->mExpiry == 'infinity'
+ }
+}
--- /dev/null
+<?php
+
+class TitleTest extends PHPUnit_Framework_TestCase {
+
+ function testLegalChars() {
+ $titlechars = Title::legalChars();
+
+ foreach ( range( 1, 255 ) as $num ) {
+ $chr = chr( $num );
+ if ( strpos( "#[]{}<>|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) {
+ $this->assertFalse( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is not a valid titlechar" );
+ } else {
+ $this->assertTrue( (bool)preg_match( "/[$titlechars]/", $chr ), "chr($num) = $chr is a valid titlechar" );
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+
+require_once dirname( __FILE__ ) . '/api/ApiSetup.php';
+
+/**
+ * @group Broken
+ */
+class UploadFromUrlTest extends ApiTestSetup {
+
+ public function setUp() {
+ global $wgEnableUploads, $wgAllowCopyUploads, $wgAllowAsyncCopyUploads;
+ parent::setUp();
+
+ $wgEnableUploads = true;
+ $wgAllowCopyUploads = true;
+ $wgAllowAsyncCopyUploads = true;
+ wfSetupSession();
+
+ ini_set( 'log_errors', 1 );
+ ini_set( 'error_reporting', 1 );
+ ini_set( 'display_errors', 1 );
+
+ if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) {
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+ }
+ }
+
+ protected function doApiRequest( $params, $unused = null ) {
+ $sessionId = session_id();
+ session_write_close();
+
+ $req = new FauxRequest( $params, true, $_SESSION );
+ $module = new ApiMain( $req, true );
+ $module->execute();
+
+ wfSetupSession( $sessionId );
+ return array( $module->getResultData(), $req );
+ }
+
+ /**
+ * Ensure that the job queue is empty before continuing
+ */
+ public function testClearQueue() {
+ while ( $job = Job::pop() ) { }
+ $this->assertFalse( $job );
+ }
+
+ /**
+ * @todo Document why we test login, since the $wgUser hack used doesn't
+ * require login
+ */
+ public function testLogin() {
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => self::$userName,
+ 'lgpassword' => self::$passWord ) );
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "NeedToken", $data[0]['login']['result'] );
+ $token = $data[0]['login']['token'];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ "lgtoken" => $token,
+ "lgname" => self::$userName,
+ "lgpassword" => self::$passWord ) );
+
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "Success", $data[0]['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testSetupUrlDownload( $data ) {
+ $token = self::$user->editToken();
+ $exception = false;
+
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ ) );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "The token parameter must be set", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'token' => $token,
+ ), $data );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required",
+ $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'url' => 'http://www.example.com/test.png',
+ 'token' => $token,
+ ), $data );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "The filename parameter must be set", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ self::$user->removeGroup( 'sysop' );
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'url' => 'http://www.example.com/test.png',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'token' => $token,
+ ), $data );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "Permission denied", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+
+ self::$user->addGroup( 'sysop' );
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'filename' => 'UploadFromUrlTest.png',
+ 'token' => $token,
+ ), $data );
+
+ $this->assertEquals( $data[0]['upload']['result'], 'Queued', 'Queued upload' );
+
+ $job = Job::pop();
+ $this->assertThat( $job, $this->isInstanceOf( 'UploadFromUrlJob' ), 'Queued upload inserted' );
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testAsyncUpload( $data ) {
+ $token = self::$user->editToken();
+
+ self::$user->addGroup( 'users' );
+
+ $data = $this->doAsyncUpload( $token, true );
+ $this->assertEquals( $data[0]['upload']['result'], 'Success' );
+ $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' );
+ $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() );
+
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testAsyncUploadWarning( $data ) {
+ $token = self::$user->editToken();
+
+ self::$user->addGroup( 'users' );
+
+
+ $data = $this->doAsyncUpload( $token );
+
+ $this->assertEquals( $data[0]['upload']['result'], 'Warning' );
+ $this->assertTrue( isset( $data[0]['upload']['sessionkey'] ) );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'sessionkey' => $data[0]['upload']['sessionkey'],
+ 'filename' => 'UploadFromUrlTest.png',
+ 'ignorewarnings' => 1,
+ 'token' => $token,
+ ) );
+ $this->assertEquals( $data[0]['upload']['result'], 'Success' );
+ $this->assertEquals( $data[0]['upload']['filename'], 'UploadFromUrlTest.png' );
+ $this->assertTrue( wfLocalFile( $data[0]['upload']['filename'] )->exists() );
+
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ /**
+ * @depends testLogin
+ * @depends testClearQueue
+ */
+ public function testSyncDownload( $data ) {
+ $token = self::$user->editToken();
+
+ $job = Job::pop();
+ $this->assertFalse( $job, 'Starting with an empty jobqueue' );
+
+ self::$user->addGroup( 'users' );
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'ignorewarnings' => true,
+ 'token' => $token,
+ ), $data );
+
+ $job = Job::pop();
+ $this->assertFalse( $job );
+
+ $this->assertEquals( 'Success', $data[0]['upload']['result'] );
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ return $data;
+ }
+
+ public function testLeaveMessage() {
+ $token = self::$user->editToken();
+
+ $talk = self::$user->getTalkPage();
+ if ( $talk->exists() ) {
+ $a = new Article( $talk );
+ $a->doDeleteArticle( '' );
+ }
+
+ $this->assertFalse( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk does not exist' );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'token' => $token,
+ 'leavemessage' => 1,
+ 'ignorewarnings' => 1,
+ ) );
+
+ $job = Job::pop();
+ $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) );
+ $job->run();
+
+ $this->assertTrue( wfLocalFile( 'UploadFromUrlTest.png' )->exists() );
+ $this->assertTrue( (bool)$talk->getArticleId( Title::GAID_FOR_UPDATE ), 'User talk exists' );
+
+ $this->deleteFile( 'UploadFromUrlTest.png' );
+
+ $talkRev = Revision::newFromTitle( $talk );
+ $talkSize = $talkRev->getSize();
+
+ $exception = false;
+ try {
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'token' => $token,
+ 'leavemessage' => 1,
+ ) );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( 'Using leavemessage without ignorewarnings is not supported', $e->getMessage() );
+ }
+ $this->assertTrue( $exception );
+
+ $job = Job::pop();
+ $this->assertFalse( $job );
+
+ return;
+
+ /**
+ // Broken until using leavemessage with ignorewarnings is supported
+ $job->run();
+
+ $this->assertFalse( wfLocalFile( 'UploadFromUrlTest.png' )->exists() );
+
+ $talkRev = Revision::newFromTitle( $talk );
+ $this->assertTrue( $talkRev->getSize() > $talkSize, 'New message left' );
+ */
+ }
+
+ /**
+ * Helper function to perform an async upload, execute the job and fetch
+ * the status
+ *
+ * @return array The result of action=upload&statuskey=key
+ */
+ private function doAsyncUpload( $token, $ignoreWarnings = false, $leaveMessage = false ) {
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => 'UploadFromUrlTest.png',
+ 'url' => 'http://bits.wikimedia.org/skins-1.5/common/images/poweredby_mediawiki_88x31.png',
+ 'asyncdownload' => 1,
+ 'token' => $token,
+ );
+ if ( $ignoreWarnings ) {
+ $params['ignorewarnings'] = 1;
+ }
+ if ( $leaveMessage ) {
+ $params['leavemessage'] = 1;
+ }
+
+ $data = $this->doApiRequest( $params );
+ $this->assertEquals( $data[0]['upload']['result'], 'Queued' );
+ $this->assertTrue( isset( $data[0]['upload']['statuskey'] ) );
+ $statusKey = $data[0]['upload']['statuskey'];
+
+ $job = Job::pop();
+ $this->assertEquals( 'UploadFromUrlJob', get_class( $job ) );
+
+ $status = $job->run();
+ $this->assertTrue( $status );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'upload',
+ 'statuskey' => $statusKey,
+ 'token' => $token,
+ ) );
+
+ return $data;
+ }
+
+
+ /**
+ *
+ */
+ protected function deleteFile( $name ) {
+ $t = Title::newFromText( $name, NS_FILE );
+ $this->assertTrue($t->exists(), "File '$name' exists");
+
+ if ( $t->exists() ) {
+ $file = wfFindFile( $name, array( 'ignoreRedirect' => true ) );
+ $empty = "";
+ FileDeleteForm::doDelete( $t, $file, $empty, "none", true );
+ $a = new Article ( $t );
+ $a->doDeleteArticle( "testing" );
+ }
+ $t = Title::newFromText( $name, NS_FILE );
+
+ $this->assertFalse($t->exists(), "File '$name' was deleted");
+ }
+ }
--- /dev/null
+<?php
+/**
+ * @group Upload
+ */
+class UploadTest extends PHPUnit_Framework_TestCase {
+ protected $upload;
+
+
+ function setUp() {
+ global $wgContLang;
+ parent::setUp();
+ $wgContLang = Language::factory( 'en' );
+ $this->upload = new UploadTestHandler;
+ }
+
+ /**
+ * Test various forms of valid and invalid titles that can be supplied.
+ */
+ public function testTitleValidation() {
+
+
+ /* Test a valid title */
+ $this->assertUploadTitleAndCode( 'ValidTitle.jpg',
+ 'ValidTitle.jpg', UploadTestHandler::OK,
+ 'upload valid title' );
+
+ /* A title with a slash */
+ $this->assertUploadTitleAndCode( 'A/B.jpg',
+ 'B.jpg', UploadTestHandler::OK,
+ 'upload title with slash' );
+
+ /* A title with illegal char */
+ $this->assertUploadTitleAndCode( 'A:B.jpg',
+ 'A-B.jpg', UploadTestHandler::OK,
+ 'upload title with colon' );
+
+ /* A title without extension */
+ $this->assertUploadTitleAndCode( 'A',
+ null, UploadTestHandler::FILETYPE_MISSING,
+ 'upload title without extension' );
+
+ /* A title with no basename */
+ $this->assertUploadTitleAndCode( '.jpg',
+ null, UploadTestHandler::MIN_LENGTH_PARTNAME,
+ 'upload title without basename' );
+
+ }
+ /**
+ * Helper function for testTitleValidation. First checks the return code
+ * of UploadBase::getTitle() and then the actual returned titl
+ */
+ private function assertUploadTitleAndCode( $srcFilename, $dstFilename, $code, $msg ) {
+ /* Check the result code */
+ $this->assertEquals( $code,
+ $this->upload->testTitleValidation( $srcFilename ),
+ "$msg code" );
+
+ /* If we expect a valid title, check the title itself. */
+ if ( $code == UploadTestHandler::OK ) {
+ $this->assertEquals( $dstFilename,
+ $this->upload->getTitle()->getText(),
+ "$msg text" );
+ }
+ }
+
+ /**
+ * Test the upload verification functions
+ */
+ public function testVerifyUpload() {
+ /* Setup with zero file size */
+ $this->upload->initializePathInfo( '', '', 0 );
+ $result = $this->upload->verifyUpload();
+ $this->assertEquals( UploadTestHandler::EMPTY_FILE,
+ $result['status'],
+ 'upload empty file' );
+ }
+
+}
+
+class UploadTestHandler extends UploadBase {
+ public function initializeFromRequest( &$request ) { }
+ public function testTitleValidation( $name ) {
+ $this->mTitle = false;
+ $this->mDesiredDestName = $name;
+ $this->getTitle();
+ return $this->mTitleError;
+ }
+
+
+}
--- /dev/null
+<?php
+
+class UserIsValidEmailAddrTest extends PHPUnit_Framework_TestCase {
+
+ private function checkEmail( $addr, $expected = true, $msg = '') {
+ $this->assertEquals(
+ $expected,
+ User::isValidEmailAddr( $addr ),
+ $msg
+ );
+ }
+ private function valid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, true, $msg );
+ }
+ private function invalid( $addr, $msg = '' ) {
+ $this->checkEmail( $addr, false, $msg );
+ }
+
+ function testEmailWellKnownUserAtHostDotTldAreValid() {
+ $this->valid( 'user@example.com' );
+ $this->valid( 'user@example.museum' );
+ }
+ function testEmailWithUpperCaseCharactersAreValid() {
+ $this->valid( 'USER@example.com' );
+ $this->valid( 'user@EXAMPLE.COM' );
+ $this->valid( 'user@Example.com' );
+ $this->valid( 'USER@eXAMPLE.com' );
+ }
+ function testEmailWithAPlusInUserName() {
+ $this->valid( 'user+sub@example.com' );
+ $this->valid( 'user+@example.com' );
+ }
+ function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() {
+ $this->invalid( " user@host" );
+ $this->invalid( "user@host " );
+ $this->invalid( "\tuser@host" );
+ $this->invalid( "user@host\t" );
+ }
+ function testEmailWithWhiteSpacesAreInvalids() {
+ $this->invalid( "User user@host" );
+ $this->invalid( "first last@mycompany" );
+ $this->invalid( "firstlast@my company" );
+ }
+ function testEmailDomainCanNotBeginWithDot() {
+ $this->invalid( "user@." );
+ $this->invalid( "user@.localdomain" );
+ $this->invalid( "user@localdomain." );
+ $this->invalid( "user.@localdomain" );
+ $this->invalid( ".@localdomain" );
+ $this->invalid( ".@a............" );
+ }
+ function testEmailWithFunnyCharacters() {
+ $this->valid( "\$user!ex{this}@123.com" );
+ }
+ function testEmailTopLevelDomainCanBeNumerical() {
+ $this->valid( "user@example.1234" );
+ }
+ function testEmailWithoutAtSignIsInvalid() {
+ $this->invalid( 'useràexample.com' );
+ }
+ function testEmailWithOneCharacterDomainIsInvalid() {
+ $this->invalid( 'user@a' );
+ }
+}
--- /dev/null
+<?php
+
+class XmlTest extends PHPUnit_Framework_TestCase {
+
+ public function testExpandAttributes() {
+ $this->assertNull( Xml::expandAttributes(null),
+ 'Converting a null list of attributes'
+ );
+ $this->assertEquals( '', Xml::expandAttributes( array() ),
+ 'Converting an empty list of attributes'
+ );
+ }
+
+ public function testExpandAttributesException() {
+ $this->setExpectedException('MWException');
+ Xml::expandAttributes('string');
+ }
+
+ function testElementOpen() {
+ $this->assertEquals(
+ '<element>',
+ Xml::element( 'element', null, null ),
+ 'Opening element with no attributes'
+ );
+ }
+
+ function testElementEmpty() {
+ $this->assertEquals(
+ '<element />',
+ Xml::element( 'element', null, '' ),
+ 'Terminated empty element'
+ );
+ }
+
+ function testElementInputCanHaveAValueOfZero() {
+ $this->assertEquals(
+ '<input name="name" value="0" />',
+ Xml::input( 'name', false, 0 ),
+ 'Input with a value of 0 (bug 23797)'
+ );
+ }
+ function testElementEscaping() {
+ $this->assertEquals(
+ '<element>hello <there> you & you</element>',
+ Xml::element( 'element', null, 'hello <there> you & you' ),
+ 'Element with no attributes and content that needs escaping'
+ );
+ }
+
+ public function testEscapeTagsOnly() {
+ $this->assertEquals( '"><', Xml::escapeTagsOnly( '"><' ),
+ 'replace " > and < with their HTML entitites'
+ );
+ }
+
+ function testElementAttributes() {
+ $this->assertEquals(
+ '<element key="value" <>="<>">',
+ Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ),
+ 'Element attributes, keys are not escaped'
+ );
+ }
+
+ function testOpenElement() {
+ $this->assertEquals(
+ '<element k="v">',
+ Xml::openElement( 'element', array( 'k' => 'v' ) ),
+ 'openElement() shortcut'
+ );
+ }
+
+ function testCloseElement() {
+ $this->assertEquals( '</element>', Xml::closeElement( 'element' ), 'closeElement() shortcut' );
+ }
+
+ #
+ # textarea
+ #
+ function testTextareaNoContent() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="40" rows="5"></textarea>',
+ Xml::textarea( 'name', '' ),
+ 'textarea() with not content'
+ );
+ }
+
+ function testTextareaAttribs() {
+ $this->assertEquals(
+ '<textarea name="name" id="name" cols="20" rows="10"><txt></textarea>',
+ Xml::textarea( 'name', '<txt>', 20, 10 ),
+ 'textarea() with custom attribs'
+ );
+ }
+
+ #
+ # input and label
+ #
+ function testLabelCreation() {
+ $this->assertEquals(
+ '<label for="id">name</label>',
+ Xml::label( 'name', 'id' ),
+ 'label() with no attribs'
+ );
+ }
+ function testLabelAttributeCanOnlyBeClass() {
+ $this->assertEquals(
+ '<label for="id">name</label>',
+ Xml::label( 'name', 'id', array( 'generated' => true ) ),
+ 'label() can not be given a generated attribute'
+ );
+ $this->assertEquals(
+ '<label for="id" class="nice">name</label>',
+ Xml::label( 'name', 'id', array( 'class' => 'nice' ) ),
+ 'label() can get a class attribute'
+ );
+ $this->assertEquals(
+ '<label for="id" class="nice">name</label>',
+ Xml::label( 'name', 'id', array(
+ 'generated' => true,
+ 'class' => 'nice',
+ 'anotherattr' => 'value',
+ )
+ ),
+ 'label() skip all attributes but "class"'
+ );
+ }
+
+ #
+ # JS
+ #
+ function testEscapeJsStringSpecialChars() {
+ $this->assertEquals(
+ '\\\\\r\n',
+ Xml::escapeJsString( "\\\r\n" ),
+ 'escapeJsString() with special characters'
+ );
+ }
+
+ function testEncodeJsVarBoolean() {
+ $this->assertEquals(
+ 'true',
+ Xml::encodeJsVar( true ),
+ 'encodeJsVar() with boolean'
+ );
+ }
+
+ function testEncodeJsVarNull() {
+ $this->assertEquals(
+ 'null',
+ Xml::encodeJsVar( null ),
+ 'encodeJsVar() with null'
+ );
+ }
+
+ function testEncodeJsVarArray() {
+ $this->assertEquals(
+ '["a", 1]',
+ Xml::encodeJsVar( array( 'a', 1 ) ),
+ 'encodeJsVar() with array'
+ );
+ $this->assertEquals(
+ '{"a": "a", "b": 1}',
+ Xml::encodeJsVar( array( 'a' => 'a', 'b' => 1 ) ),
+ 'encodeJsVar() with associative array'
+ );
+ }
+
+ function testEncodeJsVarObject() {
+ $this->assertEquals(
+ '{"a": "a", "b": 1}',
+ Xml::encodeJsVar( (object)array( 'a' => 'a', 'b' => 1 ) ),
+ 'encodeJsVar() with object'
+ );
+ }
+}
+
+// TODO
+class XmlSelectTest extends PHPUnit_Framework_TestCase {
+}
--- /dev/null
+<?php
+
+abstract class ApiTestSetup extends PHPUnit_Framework_TestCase {
+ protected static $user;
+ protected static $sysopUser;
+ protected static $apiUrl;
+
+ function setUp() {
+ global $wgServer, $wgContLang, $wgAuth, $wgMemc, $wgRequest;
+
+ self::$apiUrl = $wgServer . wfScript( 'api' );
+
+ $wgMemc = new FakeMemCachedClient;
+ $wgContLang = Language::factory( 'en' );
+ $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' );
+ $wgRequest = new FauxRequest( array() );
+ self::setupUser();
+ }
+
+ protected function doApiRequest( $params, $data = null ) {
+ $_SESSION = isset( $data[2] ) ? $data[2] : array();
+
+ $req = new FauxRequest( $params, true, $_SESSION );
+ $module = new ApiMain( $req, true );
+ $module->execute();
+
+ $data[0] = $module->getResultData();
+ $data[1] = $req;
+ $data[2] = $_SESSION;
+
+ return $data;
+ }
+
+ static function setupUser() {
+ if ( self::$user == null || self::$sysopUser == null ) {
+ self::$user = new UserWrapper( 'User for MediaWiki automated tests', User::randomPassword() );
+ self::$sysopUser = new UserWrapper( 'Sysop for MediaWiki automated tests', User::randomPassword(), 'sysop' );
+ }
+
+ $GLOBALS['wgUser'] = self::$sysopUser->user;
+ }
+}
+
+class UserWrapper {
+ public $userName, $password, $user;
+
+ public function __construct( $userName, $password, $group = '' ) {
+ $this->userName = $userName;
+ $this->password = $password;
+
+ $this->user = User::newFromName( $this->userName );
+ if ( !$this->user->getID() ) {
+ $this->user = User::createNew( $this->userName, array(
+ "email" => "test@example.com",
+ "real_name" => "Test User" ) );
+ }
+ $this->user->setPassword( $this->password );
+
+ if ( $group !== '' ) {
+ $this->user->addGroup( $group );
+ }
+ $this->user->saveSettings();
+ }
+}
+
--- /dev/null
+<?php
+
+require_once( dirname( __FILE__ ) . '/ApiSetup.php' );
+
+class MockApi extends ApiBase {
+ public function execute() { }
+ public function getVersion() { }
+
+ public function __construct() { }
+
+ public function getAllowedParams() {
+ return array(
+ 'filename' => null,
+ 'enablechunks' => false,
+ 'sessionkey' => null,
+ );
+ }
+}
+
+/**
+ * @group Database
+ * @group Destructive
+ */
+class ApiTest extends ApiTestSetup {
+
+ function testRequireOnlyOneParameterDefault() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => false ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ function testRequireOnlyOneParameterZero() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => 0 ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * @expectedException UsageException
+ */
+ function testRequireOnlyOneParameterTrue() {
+ $mock = new MockApi();
+
+ $this->assertEquals(
+ null, $mock->requireOnlyOneParameter( array( "filename" => "foo.txt",
+ "enablechunks" => true ), "filename", "enablechunks" ) );
+ }
+
+ /**
+ * Test that the API will accept a FauxRequest and execute. The help action
+ * (default) throws a UsageException. Just validate we're getting proper XML
+ *
+ * @expectedException UsageException
+ */
+ function testApi() {
+ $api = new ApiMain(
+ new FauxRequest( array( 'action' => 'help', 'format' => 'xml' ) )
+ );
+ $api->execute();
+ $api->getPrinter()->setBufferResult( true );
+ $api->printResult( false );
+ $resp = $api->getPrinter()->getBuffer();
+
+ libxml_use_internal_errors( true );
+ $sxe = simplexml_load_string( $resp );
+ $this->assertNotType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ }
+
+ /**
+ * Test result of attempted login with an empty username
+ */
+ function testApiLoginNoName() {
+ $data = $this->doApiRequest( array( 'action' => 'login',
+ 'lgname' => '', 'lgpassword' => self::$user->password,
+ ) );
+ $this->assertEquals( 'NoName', $data[0]['login']['result'] );
+ }
+
+ function testApiLoginBadPass() {
+ global $wgServer;
+
+ $user = self::$user;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgname" => $user->userName,
+ "lgpassword" => "bad",
+ )
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotType( "bool", $result );
+ $a = $result["login"]["result"];
+ $this->assertEquals( "NeedToken", $a );
+
+ $token = $result["login"]["token"];
+
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgtoken" => $token,
+ "lgname" => $user->userName,
+ "lgpassword" => "bad",
+ )
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "WrongPass", $a );
+ }
+
+ function testApiLoginGoodPass() {
+ global $wgServer;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+
+ $user = self::$user;
+
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgname" => $user->userName,
+ "lgpassword" => $user->password,
+ )
+ );
+
+ $result = $ret[0];
+ $this->assertNotType( "bool", $result );
+ $this->assertNotType( "null", $result["login"] );
+
+ $a = $result["login"]["result"];
+ $this->assertEquals( "NeedToken", $a );
+ $token = $result["login"]["token"];
+
+ $ret = $this->doApiRequest( array(
+ "action" => "login",
+ "lgtoken" => $token,
+ "lgname" => $user->userName,
+ "lgpassword" => $user->password,
+ )
+ );
+
+ $result = $ret[0];
+
+ $this->assertNotType( "bool", $result );
+ $a = $result["login"]["result"];
+
+ $this->assertEquals( "Success", $a );
+ }
+
+ function testApiGotCookie() {
+ $this->markTestIncomplete( "The server can't do external HTTP requests, and the internal one won't give cookies" );
+
+ global $wgServer, $wgScriptPath;
+
+ if ( !isset( $wgServer ) ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+ $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml",
+ array( "method" => "POST",
+ "postData" => array(
+ "lgname" => self::$user->userName,
+ "lgpassword" => self::$user->password ) ) );
+ $req->execute();
+
+ libxml_use_internal_errors( true );
+ $sxe = simplexml_load_string( $req->getContent() );
+ $this->assertNotType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ $this->assertNotType( "null", $sxe->login[0] );
+
+ $a = $sxe->login[0]->attributes()->result[0];
+ $this->assertEquals( ' result="NeedToken"', $a->asXML() );
+ $token = (string)$sxe->login[0]->attributes()->token;
+
+ $req->setData( array(
+ "lgtoken" => $token,
+ "lgname" => self::$user->userName,
+ "lgpassword" => self::$user->password ) );
+ $req->execute();
+
+ $cj = $req->getCookieJar();
+ $serverName = parse_url( $wgServer, PHP_URL_HOST );
+ $this->assertNotEquals( false, $serverName );
+ $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
+ $this->assertNotEquals( '', $serializedCookie );
+ $this->assertRegexp( '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . self::$user->userName . '; .*Token=/', $serializedCookie );
+
+ return $cj;
+ }
+
+ /**
+ * @depends testApiGotCookie
+ */
+ function testApiListPages( CookieJar $cj ) {
+ $this->markTestIncomplete( "Not done with this yet" );
+ global $wgServer;
+
+ if ( $wgServer == "http://localhost" ) {
+ $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+ }
+ $req = MWHttpRequest::factory( self::$apiUrl . "?action=query&format=xml&prop=revisions&" .
+ "titles=Main%20Page&rvprop=timestamp|user|comment|content" );
+ $req->setCookieJar( $cj );
+ $req->execute();
+ libxml_use_internal_errors( true );
+ $sxe = simplexml_load_string( $req->getContent() );
+ $this->assertNotType( "bool", $sxe );
+ $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) );
+ $a = $sxe->query[0]->pages[0]->page[0]->attributes();
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @group Database
+ * @group Destructive
+ */
+
+/**
+ * n.b. Ensure that you can write to the images/ directory as the
+ * user that will run tests.
+ */
+
+// Note for reviewers: this intentionally duplicates functionality already in "ApiSetup" and so on.
+// This framework works better IMO and has less strangeness (such as test cases inheriting from "ApiSetup"...)
+// (and in the case of the other Upload tests, this flat out just actually works... )
+
+// TODO: refactor into several files
+// TODO: port the other Upload tests, and other API tests to this framework
+
+require_once( dirname( __FILE__ ) . '/RandomImageGenerator.php' );
+require_once( dirname( __FILE__ ) . '/../../../../../includes/User.php' );
+
+/* Wraps the user object, so we can also retain full access to properties like password if we log in via the API */
+class ApiTestUser {
+ public $username;
+ public $password;
+ public $email;
+ public $groups;
+ public $user;
+
+ function __construct( $username, $realname = 'Real Name', $email = 'sample@sample.com', $groups = array() ) {
+ $this->username = $username;
+ $this->realname = $realname;
+ $this->email = $email;
+ $this->groups = $groups;
+
+ // don't allow user to hardcode or select passwords -- people sometimes run tests
+ // on live wikis. Sometimes we create sysop users in these tests. A sysop user with
+ // a known password would be a Bad Thing.
+ $this->password = User::randomPassword();
+
+ $this->user = User::newFromName( $this->username );
+ $this->user->load();
+
+ // In an ideal world we'd have a new wiki (or mock data store) for every single test.
+ // But for now, we just need to create or update the user with the desired properties.
+ // we particularly need the new password, since we just generated it randomly.
+ // In core MediaWiki, there is no functionality to delete users, so this is the best we can do.
+ if ( !$this->user->getID() ) {
+ // create the user
+ $this->user = User::createNew(
+ $this->username, array(
+ "email" => $this->email,
+ "real_name" => $this->realname
+ )
+ );
+ if ( !$this->user ) {
+ throw new Exception( "error creating user" );
+ }
+ }
+
+ // update the user to use the new random password and other details
+ $this->user->setPassword( $this->password );
+ $this->user->setEmail( $this->email );
+ $this->user->setRealName( $this->realname );
+ // remove all groups, replace with any groups specified
+ foreach ( $this->user->getGroups() as $group ) {
+ $this->user->removeGroup( $group );
+ }
+ if ( count( $this->groups ) ) {
+ foreach ( $this->groups as $group ) {
+ $this->user->addGroup( $group );
+ }
+ }
+ $this->user->saveSettings();
+
+ }
+
+}
+
+abstract class ApiTestCase extends PHPUnit_Framework_TestCase {
+ public static $users;
+
+ function setUp() {
+ global $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser;
+
+ $wgMemc = new FakeMemCachedClient();
+ $wgContLang = Language::factory( 'en' );
+ $wgAuth = new StubObject( 'wgAuth', 'AuthPlugin' );
+ $wgRequest = new FauxRequest( array() );
+
+ self::$users = array(
+ 'sysop' => new ApiTestUser(
+ 'Apitestsysop',
+ 'Api Test Sysop',
+ 'api_test_sysop@sample.com',
+ array( 'sysop' )
+ ),
+ 'uploader' => new ApiTestUser(
+ 'Apitestuser',
+ 'Api Test User',
+ 'api_test_user@sample.com',
+ array()
+ )
+ );
+
+ $wgUser = self::$users['sysop']->user;
+
+ }
+
+ protected function doApiRequest( $params, $session = null ) {
+ $_SESSION = isset( $session ) ? $session : array();
+
+ $request = new FauxRequest( $params, true, $_SESSION );
+ $module = new ApiMain( $request, true );
+ $module->execute();
+
+ return array( $module->getResultData(), $request, $_SESSION );
+ }
+
+ /**
+ * Add an edit token to the API request
+ * This is cheating a bit -- we grab a token in the correct format and then add it to the pseudo-session and to the
+ * request, without actually requesting a "real" edit token
+ * @param $params: key-value API params
+ * @param $session: session array
+ */
+ protected function doApiRequestWithToken( $params, $session ) {
+ if ( $session['wsToken'] ) {
+ // add edit token to fake session
+ $session['wsEditToken'] = $session['wsToken'];
+ // add token to request parameters
+ $params['token'] = md5( $session['wsToken'] ) . EDIT_TOKEN_SUFFIX;
+ return $this->doApiRequest( $params, $session );
+ } else {
+ throw new Exception( "request data not in right format" );
+ }
+ }
+
+}
+
+/**
+ * @group Database
+ * @group Destructive
+ */
+class ApiUploadTest extends ApiTestCase {
+ /**
+ * Fixture -- run before every test
+ */
+ public function setUp() {
+ global $wgEnableUploads, $wgEnableAPI;
+ parent::setUp();
+
+ $wgEnableUploads = true;
+ $wgEnableAPI = true;
+ wfSetupSession();
+
+ ini_set( 'log_errors', 1 );
+ ini_set( 'error_reporting', 1 );
+ ini_set( 'display_errors', 1 );
+
+ $this->clearFakeUploads();
+ }
+
+ /**
+ * Fixture -- run after every test
+ * Clean up temporary files etc.
+ */
+ function tearDown() {
+ }
+
+
+ /**
+ * Testing login
+ * XXX this is a funny way of getting session context
+ */
+ function testLogin() {
+ $user = self::$users['uploader'];
+
+ $params = array(
+ 'action' => 'login',
+ 'lgname' => $user->username,
+ 'lgpassword' => $user->password
+ );
+ list( $result, , ) = $this->doApiRequest( $params );
+ $this->assertArrayHasKey( "login", $result );
+ $this->assertArrayHasKey( "result", $result['login'] );
+ $this->assertEquals( "NeedToken", $result['login']['result'] );
+ $token = $result['login']['token'];
+
+ $params = array(
+ 'action' => 'login',
+ 'lgtoken' => $token,
+ 'lgname' => $user->username,
+ 'lgpassword' => $user->password
+ );
+ list( $result, , $session ) = $this->doApiRequest( $params );
+ $this->assertArrayHasKey( "login", $result );
+ $this->assertArrayHasKey( "result", $result['login'] );
+ $this->assertEquals( "Success", $result['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $result['login'] );
+
+ return $session;
+
+ }
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadRequiresToken( $session ) {
+ $exception = false;
+ try {
+ $this->doApiRequest( array(
+ 'action' => 'upload'
+ ) );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "The token parameter must be set", $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+ }
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadMissingParams( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $exception = false;
+ try {
+ $this->doApiRequestWithToken( array(
+ 'action' => 'upload',
+ ), $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ $this->assertEquals( "One of the parameters sessionkey, file, url, statuskey is required",
+ $e->getMessage() );
+ }
+ $this->assertTrue( $exception, "Got exception" );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUpload( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ $filePath = $filePaths[0];
+ $fileSize = filesize( $filePath );
+ $fileName = basename( $filePath );
+
+ $this->deleteFileByFileName( $fileName );
+ $this->deleteFileByContent( $filePath );
+
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ $exception = false;
+ try {
+ list( $result, , ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] );
+ $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadZeroLength( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $mimeType = 'image/png';
+
+ $filePath = tempnam( wfTempDir(), "" );
+ $fileName = "apiTestUploadZeroLength.png";
+
+ $this->deleteFileByFileName( $fileName );
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ $exception = false;
+ try {
+ $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $this->assertContains( 'The file you submitted was empty', $e->getMessage() );
+ $exception = true;
+ }
+ $this->assertTrue( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadSameFileName( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $filePaths = $randomImageGenerator->writeImages( 2, $extension, wfTempDir() );
+ // we'll reuse this filename
+ $fileName = basename( $filePaths[0] );
+
+ // clear any other files with the same name
+ $this->deleteFileByFileName( $fileName );
+
+ // we reuse these params
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ // first upload .... should succeed
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[0] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $exception = false;
+ try {
+ list( $result, , $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+ // second upload with the same name (but different content)
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePaths[1] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $exception = false;
+ try {
+ list( $result, , ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Warning', $result['upload']['result'] );
+ $this->assertTrue( isset( $result['upload']['warnings'] ) );
+ $this->assertTrue( isset( $result['upload']['warnings']['exists'] ) );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePaths[0] );
+ unlink( $filePaths[1] );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadSameContent( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ $fileNames[0] = basename( $filePaths[0] );
+ $fileNames[1] = "SameContentAs" . $fileNames[0];
+
+ // clear any other files with the same name or content
+ $this->deleteFileByContent( $filePaths[0] );
+ $this->deleteFileByFileName( $fileNames[0] );
+ $this->deleteFileByFileName( $fileNames[1] );
+
+ // first upload .... should succeed
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileNames[0],
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for " . $fileNames[0],
+ );
+
+ if (! $this->fakeUploadFile( 'file', $fileNames[0], $mimeType, $filePaths[0] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+
+ // second upload with the same content (but different name)
+
+ if (! $this->fakeUploadFile( 'file', $fileNames[1], $mimeType, $filePaths[0] ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'filename' => $fileNames[1],
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for " . $fileNames[1],
+ );
+
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Warning', $result['upload']['result'] );
+ $this->assertTrue( isset( $result['upload']['warnings'] ) );
+ $this->assertTrue( isset( $result['upload']['warnings']['duplicate'] ) );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileNames[0] );
+ $this->deleteFileByFilename( $fileNames[1] );
+ unlink( $filePaths[0] );
+ }
+
+
+ /**
+ * @depends testLogin
+ */
+ public function testUploadStash( $session ) {
+ global $wgUser;
+ $wgUser = self::$users['uploader']->user;
+
+ $extension = 'png';
+ $mimeType = 'image/png';
+
+ try {
+ $randomImageGenerator = new RandomImageGenerator();
+ }
+ catch ( Exception $e ) {
+ $this->markTestIncomplete( $e->getMessage() );
+ }
+
+ $filePaths = $randomImageGenerator->writeImages( 1, $extension, wfTempDir() );
+ $filePath = $filePaths[0];
+ $fileSize = filesize( $filePath );
+ $fileName = basename( $filePath );
+
+ $this->deleteFileByFileName( $fileName );
+ $this->deleteFileByContent( $filePath );
+
+ if (! $this->fakeUploadFile( 'file', $fileName, $mimeType, $filePath ) ) {
+ $this->markTestIncomplete( "Couldn't upload file!\n" );
+ }
+
+ $params = array(
+ 'action' => 'upload',
+ 'stash' => 1,
+ 'filename' => $fileName,
+ 'file' => 'dummy content',
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName",
+ );
+
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertFalse( $exception );
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertEquals( $fileSize, ( int )$result['upload']['imageinfo']['size'] );
+ $this->assertEquals( $mimeType, $result['upload']['imageinfo']['mime'] );
+ $this->assertTrue( isset( $result['upload']['sessionkey'] ) );
+ $sessionkey = $result['upload']['sessionkey'];
+
+ // it should be visible from Special:UploadStash
+ // XXX ...but how to test this, with a fake WebRequest with the session?
+
+ // now we should try to release the file from stash
+ $params = array(
+ 'action' => 'upload',
+ 'sessionkey' => $sessionkey,
+ 'filename' => $fileName,
+ 'comment' => 'dummy comment',
+ 'text' => "This is the page text for $fileName, altered",
+ );
+
+ $this->clearFakeUploads();
+ $exception = false;
+ try {
+ list( $result, $request, $session ) = $this->doApiRequestWithToken( $params, $session );
+ } catch ( UsageException $e ) {
+ $exception = true;
+ }
+ $this->assertTrue( isset( $result['upload'] ) );
+ $this->assertEquals( 'Success', $result['upload']['result'] );
+ $this->assertFalse( $exception );
+
+ // clean up
+ $this->deleteFileByFilename( $fileName );
+ unlink( $filePath );
+ }
+
+
+
+ /**
+ * Helper function -- remove files and associated articles by Title
+ * @param $title Title: title to be removed
+ */
+ public function deleteFileByTitle( $title ) {
+ if ( $title->exists() ) {
+ $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) );
+ $noOldArchive = ""; // yes this really needs to be set this way
+ $comment = "removing for test";
+ $restrictDeletedVersions = false;
+ $status = FileDeleteForm::doDelete( $title, $file, $noOldArchive, $comment, $restrictDeletedVersions );
+ if ( !$status->isGood() ) {
+ return false;
+ }
+ $article = new Article( $title );
+ $article->doDeleteArticle( "removing for test" );
+
+ // see if it now doesn't exist; reload
+ $title = Title::newFromText( $fileName, NS_FILE );
+ }
+ return ! ( $title && $title instanceof Title && $title->exists() );
+ }
+
+ /**
+ * Helper function -- remove files and associated articles with a particular filename
+ * @param $fileName String: filename to be removed
+ */
+ public function deleteFileByFileName( $fileName ) {
+ return $this->deleteFileByTitle( Title::newFromText( $fileName, NS_FILE ) );
+ }
+
+
+ /**
+ * Helper function -- given a file on the filesystem, find matching content in the db (and associated articles) and remove them.
+ * @param $filePath String: path to file on the filesystem
+ */
+ public function deleteFileByContent( $filePath ) {
+ $hash = File::sha1Base36( $filePath );
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ $success = true;
+ foreach ( $dupes as $dupe ) {
+ $success &= $this->deleteFileByTitle( $dupe->getTitle() );
+ }
+ return $success;
+ }
+
+ /**
+ * Fake an upload by dumping the file into temp space, and adding info to $_FILES.
+ * (This is what PHP would normally do).
+ * @param $fieldName String: name this would have in the upload form
+ * @param $fileName String: name to title this
+ * @param $type String: mime type
+ * @param $filePath String: path where to find file contents
+ */
+ function fakeUploadFile( $fieldName, $fileName, $type, $filePath ) {
+ $tmpName = tempnam( wfTempDir(), "" );
+ if ( !file_exists( $filePath ) ) {
+ throw new Exception( "$filePath doesn't exist!" );
+ };
+
+ if ( !copy( $filePath, $tmpName ) ) {
+ throw new Exception( "couldn't copy $filePath to $tmpName" );
+ }
+
+ clearstatcache();
+ $size = filesize( $tmpName );
+ if ( $size === false ) {
+ throw new Exception( "couldn't stat $tmpName" );
+ }
+
+ $_FILES[ $fieldName ] = array(
+ 'name' => $fileName,
+ 'type' => $type,
+ 'tmp_name' => $tmpName,
+ 'size' => $size,
+ 'error' => null
+ );
+
+ return true;
+
+ }
+
+ /**
+ * Remove traces of previous fake uploads
+ */
+ function clearFakeUploads() {
+ $_FILES = array();
+ }
+
+
+}
+
--- /dev/null
+<?php
+
+require_once dirname( __FILE__ ) . '/ApiSetup.php';
+
+/**
+ * @group Database
+ * @group Destructive
+ */
+class ApiWatchTest extends ApiTestSetup {
+
+ function setUp() {
+ parent::setUp();
+ }
+
+ function testLogin() {
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ 'lgname' => self::$sysopUser->userName,
+ 'lgpassword' => self::$sysopUser->password ) );
+
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "NeedToken", $data[0]['login']['result'] );
+ $token = $data[0]['login']['token'];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'login',
+ "lgtoken" => $token,
+ "lgname" => self::$sysopUser->userName,
+ "lgpassword" => self::$sysopUser->password ), $data );
+
+ $this->assertArrayHasKey( "login", $data[0] );
+ $this->assertArrayHasKey( "result", $data[0]['login'] );
+ $this->assertEquals( "Success", $data[0]['login']['result'] );
+ $this->assertArrayHasKey( 'lgtoken', $data[0]['login'] );
+
+ return $data;
+ }
+
+ function testGettingToken() {
+ foreach ( array( self::$user, self::$sysopUser ) as $user ) {
+ $this->getUserTokens( $user );
+ }
+ }
+
+ function getUserTokens( $user ) {
+ $GLOBALS['wgUser'] = $user->user;
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'titles' => 'Main Page',
+ 'intoken' => 'edit|delete|protect|move|block|unblock',
+ 'prop' => 'info' ) );
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'pages', $data[0]['query'] );
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+
+ $rights = $user->user->getRights();
+
+ $this->assertArrayHasKey( $key, $data[0]['query']['pages'] );
+ $this->assertArrayHasKey( 'edittoken', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 'movetoken', $data[0]['query']['pages'][$key] );
+
+ if ( isset( $rights['delete'] ) ) {
+ $this->assertArrayHasKey( 'deletetoken', $data[0]['query']['pages'][$key] );
+ }
+
+ if ( isset( $rights['block'] ) ) {
+ $this->assertArrayHasKey( 'blocktoken', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 'unblocktoken', $data[0]['query']['pages'][$key] );
+ }
+
+ if ( isset( $rights['protect'] ) ) {
+ $this->assertArrayHasKey( 'protecttoken', $data[0]['query']['pages'][$key] );
+ }
+
+ return $data;
+ }
+
+ function testGetToken() {
+ return $this->getUserTokens( self::$sysopUser );
+ }
+
+ /**
+ * @depends testGetToken
+ */
+ function testWatchEdit( $data ) {
+ $this->markTestIncomplete( "Broken" );
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'edit',
+ 'title' => 'Main Page',
+ 'text' => 'new text',
+ 'token' => $pageinfo['edittoken'],
+ 'watchlist' => 'watch' ), $data );
+ $this->assertArrayHasKey( 'edit', $data[0] );
+ $this->assertArrayHasKey( 'result', $data[0]['edit'] );
+ $this->assertEquals( 'Success', $data[0]['edit']['result'] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testWatchEdit
+ */
+ function testWatchClear( $data ) {
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ), $data );
+
+ if ( isset( $data[0]['query']['watchlist'] ) ) {
+ $wl = $data[0]['query']['watchlist'];
+
+ foreach ( $wl as $page ) {
+ $data = $this->doApiRequest( array(
+ 'action' => 'watch',
+ 'title' => $page['title'],
+ 'unwatch' => true ), $data );
+ }
+ }
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ), $data );
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'watchlist', $data[0]['query'] );
+ $this->assertEquals( 0, count( $data[0]['query']['watchlist'] ) );
+
+ return $data;
+ }
+
+ /**
+ * @depends testGetToken
+ */
+ function testWatchProtect( $data ) {
+ $this->markTestIncomplete( "Broken" );
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'protect',
+ 'token' => $pageinfo['protecttoken'],
+ 'title' => 'Main Page',
+ 'protections' => 'edit=sysop',
+ 'watchlist' => 'unwatch' ), $data );
+
+ $this->assertArrayHasKey( 'protect', $data[0] );
+ $this->assertArrayHasKey( 'protections', $data[0]['protect'] );
+ $this->assertEquals( 1, count( $data[0]['protect']['protections'] ) );
+ $this->assertArrayHasKey( 'edit', $data[0]['protect']['protections'][0] );
+ }
+
+ /**
+ * @depends testGetToken
+ */
+ function testGetRollbackToken( $data ) {
+ if ( !Title::newFromText( 'Main Page' )->exists() ) {
+ $this->markTestIncomplete( "The article [[Main Page]] does not exist" );
+ }
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'prop' => 'revisions',
+ 'titles' => 'Main Page',
+ 'rvtoken' => 'rollback' ), $data );
+
+ $this->assertArrayHasKey( 'query', $data[0] );
+ $this->assertArrayHasKey( 'pages', $data[0]['query'] );
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+
+ if ( isset( $data[0]['query']['pages'][$key]['missing'] ) ) {
+ $this->markTestIncomplete( "Target page (Main Page) doesn't exist" );
+ }
+
+ $this->assertArrayHasKey( 'pageid', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 'revisions', $data[0]['query']['pages'][$key] );
+ $this->assertArrayHasKey( 0, $data[0]['query']['pages'][$key]['revisions'] );
+ $this->assertArrayHasKey( 'rollbacktoken', $data[0]['query']['pages'][$key]['revisions'][0] );
+
+ return $data;
+ }
+
+ /**
+ * @depends testGetRollbackToken
+ */
+ function testWatchRollback( $data ) {
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key]['revisions'][0];
+
+ try {
+ $data = $this->doApiRequest( array(
+ 'action' => 'rollback',
+ 'title' => 'Main Page',
+ 'user' => $pageinfo['user'],
+ 'token' => $pageinfo['rollbacktoken'],
+ 'watchlist' => 'watch' ), $data );
+ } catch( UsageException $ue ) {
+ if( $ue->getCodeString() == 'onlyauthor' ) {
+ $this->markTestIncomplete( "Only one author to 'Main Page', cannot test rollback" );
+ } else {
+ $this->fail( "Received error " . $ue->getCodeString() );
+ }
+ }
+
+ $this->assertArrayHasKey( 'rollback', $data[0] );
+ $this->assertArrayHasKey( 'title', $data[0]['rollback'] );
+ }
+
+ /**
+ * @depends testGetToken
+ */
+ function testWatchDelete( $data ) {
+ $this->markTestIncomplete( "Broken" );
+ $keys = array_keys( $data[0]['query']['pages'] );
+ $key = array_pop( $keys );
+ $pageinfo = $data[0]['query']['pages'][$key];
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'delete',
+ 'token' => $pageinfo['deletetoken'],
+ 'title' => 'Main Page' ), $data );
+ $this->assertArrayHasKey( 'delete', $data[0] );
+ $this->assertArrayHasKey( 'title', $data[0]['delete'] );
+
+ $data = $this->doApiRequest( array(
+ 'action' => 'query',
+ 'list' => 'watchlist' ), $data );
+
+ $this->markTestIncomplete( 'This test needs to verify the deleted article was added to the users watchlist' );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * RandomImageGenerator -- does what it says on the tin.
+ * Requires Imagick, the ImageMagick library for PHP, or the command line equivalent (usually 'convert').
+ *
+ * Because MediaWiki tests the uniqueness of media upload content, and filenames, it is sometimes useful to generate
+ * files that are guaranteed (or at least very likely) to be unique in both those ways.
+ * This generates a number of filenames with random names and random content (colored circles)
+ *
+ * It is also useful to have fresh content because our tests currently run in a "destructive" mode, and don't create a fresh new wiki for each
+ * test run.
+ * Consequently, if we just had a few static files we kept re-uploading, we'd get lots of warnings about matching content or filenames,
+ * and even if we deleted those files, we'd get warnings about archived files.
+ *
+ * This can also be used with a cronjob to generate random files all the time -- I use it to have a constant, never ending supply when I'm
+ * testing interactively.
+ *
+ * @file
+ * @author Neil Kandalgaonkar <neilk@wikimedia.org>
+ */
+
+/**
+ * RandomImageGenerator: does what it says on the tin.
+ * Can fetch a random image, or also write a number of them to disk with random filenames.
+ */
+class RandomImageGenerator {
+
+ private $dictionaryFile;
+ private $minWidth = 400;
+ private $maxWidth = 800;
+ private $minHeight = 400;
+ private $maxHeight = 800;
+ private $circlesToDraw = 5;
+ private $imageWriteMethod;
+
+ public function __construct( $options ) {
+ global $wgUseImageMagick, $wgImageMagickConvertCommand;
+ foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxHeight', 'circlesToDraw' ) as $property ) {
+ if ( isset( $options[$property] ) ) {
+ $this->$property = $options[$property];
+ }
+ }
+
+ // find the dictionary file, to generate random names
+ if ( !isset( $this->dictionaryFile ) ) {
+ foreach ( array( '/usr/share/dict/words', '/usr/dict/words' ) as $dictionaryFile ) {
+ if ( is_file( $dictionaryFile ) and is_readable( $dictionaryFile ) ) {
+ $this->dictionaryFile = $dictionaryFile;
+ break;
+ }
+ }
+ }
+ if ( !isset( $this->dictionaryFile ) ) {
+ throw new Exception( "RandomImageGenerator: dictionary file not found or not specified properly" );
+ }
+
+ // figure out how to write images
+ if ( class_exists( 'Imagick' ) ) {
+ $this->imageWriteMethod = 'writeImageWithApi';
+ } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) {
+ $this->imageWriteMethod = 'writeImageWithCommandLine';
+ } else {
+ throw new Exception( "RandomImageGenerator: could not find a suitable method to write images" );
+ }
+ }
+
+ /**
+ * Writes random images with random filenames to disk in the directory you specify, or current working directory
+ *
+ * @param $number Integer: number of filenames to write
+ * @param $format String: optional, must be understood by ImageMagick, such as 'jpg' or 'gif'
+ * @param $dir String: directory, optional (will default to current working directory)
+ * @return Array: filenames we just wrote
+ */
+ function writeImages( $number, $format = 'jpg', $dir = null ) {
+ $filenames = $this->getRandomFilenames( $number, $format, $dir );
+ foreach( $filenames as $filename ) {
+ $this->{$this->imageWriteMethod}( $this->getImageSpec(), $format, $filename );
+ }
+ return $filenames;
+ }
+
+ /**
+ * Return a number of randomly-generated filenames
+ * Each filename uses two words randomly drawn from the dictionary, like elephantine_spatula.jpg
+ *
+ * @param $number Integer: of filenames to generate
+ * @param $extension String: optional, defaults to 'jpg'
+ * @param $dir String: optional, defaults to current working directory
+ * @return Array: of filenames
+ */
+ private function getRandomFilenames( $number, $extension = 'jpg', $dir = null ) {
+ if ( is_null( $dir ) ) {
+ $dir = getcwd();
+ }
+ $filenames = array();
+ foreach( $this->getRandomWordPairs( $number ) as $pair ) {
+ $basename = $pair[0] . '_' . $pair[1];
+ if ( !is_null( $extension ) ) {
+ $basename .= '.' . $extension;
+ }
+ $basename = preg_replace( '/\s+/', '', $basename );
+ $filenames[] = "$dir/$basename";
+ }
+
+ return $filenames;
+
+ }
+
+
+ /**
+ * Generate data representing an image of random size (within limits),
+ * consisting of randomly colored and sized circles against a random background color
+ * (This data is used in the writeImage* methods).
+ * @return {Mixed}
+ */
+ public function getImageSpec() {
+ $spec = array();
+
+ $spec['width'] = mt_rand( $this->minWidth, $this->maxWidth );
+ $spec['height'] = mt_rand( $this->minHeight, $this->maxHeight );
+ $spec['fill'] = $this->getRandomColor();
+
+ $diagonalLength = sqrt( pow( $spec['width'], 2 ) + pow( $spec['height'], 2 ) );
+
+ $draws = array();
+ for ( $i = 0; $i <= $this->circlesToDraw; $i++ ) {
+ $radius = mt_rand( 0, $diagonalLength / 4 );
+ $originX = mt_rand( -1 * $radius, $spec['width'] + $radius );
+ $originY = mt_rand( -1 * $radius, $spec['height'] + $radius );
+ $perimeterX = $originX + $radius;
+ $perimeterY = $originY + $radius;
+
+ $draw = array();
+ $draw['fill'] = $this->getRandomColor();
+ $draw['circle'] = array(
+ 'originX' => $originX,
+ 'originY' => $originY,
+ 'perimeterX' => $perimeterX,
+ 'perimeterY' => $perimeterY
+ );
+ $draws[] = $draw;
+
+ }
+
+ $spec['draws'] = $draws;
+
+ return $spec;
+ }
+
+
+ /**
+ * Based on an image specification, write such an image to disk, using Imagick PHP extension
+ * @param $spec: spec describing background and circles to draw
+ * @param $format: file format to write
+ * @param $filename: filename to write to
+ */
+ public function writeImageWithApi( $spec, $format, $filename ) {
+ $image = new Imagick();
+ $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) );
+
+ foreach ( $spec['draws'] as $drawSpec ) {
+ $draw = new ImagickDraw();
+ $draw->setFillColor( $drawSpec['fill'] );
+ $circle = $drawSpec['circle'];
+ $draw->circle( $circle['originX'], $circle['originY'], $circle['perimeterX'], $circle['perimeterY'] );
+ $image->drawImage( $draw );
+ }
+
+ $image->setImageFormat( $format );
+ $image->writeImage( $filename );
+ }
+
+
+ /**
+ * Based on an image specification, write such an image to disk, using the command line ImageMagick program ('convert').
+ *
+ * Sample command line:
+ * $ convert -size 100x60 xc:rgb(90,87,45) \
+ * -draw 'fill rgb(12,34,56) circle 41,39 44,57' \
+ * -draw 'fill rgb(99,123,231) circle 59,39 56,57' \
+ * -draw 'fill rgb(240,12,32) circle 50,21 50,3' filename.png
+ *
+ * @param $spec: spec describing background and circles to draw
+ * @param $format: file format to write (unused by this method but kept so it has the same signature as writeImageWithApi)
+ * @param $filename: filename to write to
+ */
+ public function writeImageWithCommandLine( $spec, $format, $filename ) {
+ global $wgImageMagickConvertCommand;
+ $args = array();
+ $args[] = "-size " . wfEscapeShellArg( $spec['width'] . 'x' . $spec['height'] );
+ $args[] = wfEscapeShellArg( "xc:" . $spec['fill'] );
+ foreach( $spec['draws'] as $draw ) {
+ $fill = $draw['fill'];
+ $originX = $draw['circle']['originX'];
+ $originY = $draw['circle']['originY'];
+ $perimeterX = $draw['circle']['perimeterX'];
+ $perimeterY = $draw['circle']['perimeterY'];
+ $drawCommand = "fill $fill circle $originX,$originY $perimeterX,$perimeterY";
+ $args[] = '-draw ' . wfEscapeShellArg( $drawCommand );
+ }
+ $args[] = wfEscapeShellArg( $filename );
+
+ $command = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " . implode( " ", $args );
+ $retval = null;
+ wfShellExec( $command, $retval );
+ return ( $retval === 0 );
+ }
+
+ /**
+ * Generate a string of random colors for ImageMagick, like "rgb(12, 37, 98)"
+ *
+ * @return {String}
+ */
+ public function getRandomColor() {
+ $components = array();
+ for ($i = 0; $i <= 2; $i++ ) {
+ $components[] = mt_rand( 0, 255 );
+ }
+ return 'rgb(' . join(', ', $components) . ')';
+ }
+
+ /**
+ * Get an array of random pairs of random words, like array( array( 'foo', 'bar' ), array( 'quux', 'baz' ) );
+ *
+ * @param $number Integer: number of pairs
+ * @return Array: of two-element arrays
+ */
+ private function getRandomWordPairs( $number ) {
+ $lines = $this->getRandomLines( $number * 2 );
+ // construct pairs of words
+ $pairs = array();
+ $count = count( $lines );
+ for( $i = 0; $i < $count; $i += 2 ) {
+ $pairs[] = array( $lines[$i], $lines[$i+1] );
+ }
+ return $pairs;
+ }
+
+
+ /**
+ * Return N random lines from a file
+ *
+ * Will throw exception if the file could not be read or if it had fewer lines than requested.
+ *
+ * @param $number_desired Integer: number of lines desired
+ * @return Array: of exactly n elements, drawn randomly from lines the file
+ */
+ private function getRandomLines( $number_desired ) {
+ $filepath = $this->dictionaryFile;
+
+ // initialize array of lines
+ $lines = array();
+ for ( $i = 0; $i < $number_desired; $i++ ) {
+ $lines[] = null;
+ }
+
+ /*
+ * This algorithm obtains N random lines from a file in one single pass. It does this by replacing elements of
+ * a fixed-size array of lines, less and less frequently as it reads the file.
+ */
+ $fh = fopen( $filepath, "r" );
+ if ( !$fh ) {
+ throw new Exception( "couldn't open $filepath" );
+ }
+ $line_number = 0;
+ $max_index = $number_desired - 1;
+ while( !feof( $fh ) ) {
+ $line = fgets( $fh );
+ if ( $line !== false ) {
+ $line_number++;
+ $line = trim( $line );
+ if ( mt_rand( 0, $line_number ) <= $max_index ) {
+ $lines[ mt_rand( 0, $max_index ) ] = $line;
+ }
+ }
+ }
+ fclose( $fh );
+ if ( $line_number < $number_desired ) {
+ throw new Exception( "not enough lines in $filepath" );
+ }
+
+ return $lines;
+ }
+
+}
--- /dev/null
+<?php
+
+require("RandomImageGenerator.php");
+
+$getOptSpec = array(
+ 'dictionaryFile::',
+ 'minWidth::',
+ 'maxWidth::',
+ 'minHeight::',
+ 'maxHeight::',
+ 'circlesToDraw::',
+
+ 'number::',
+ 'format::'
+);
+$options = getopt( null, $getOptSpec );
+
+$format = isset( $options['format'] ) ? $options['format'] : 'jpg';
+unset( $options['format'] );
+
+$number = isset( $options['number'] ) ? intval( $options['number'] ) : 10;
+unset( $options['number'] );
+
+$randomImageGenerator = new RandomImageGenerator( $options );
+$randomImageGenerator->writeImages( $number, $format );
--- /dev/null
+<?php
+
+class MockDatabaseSqlite extends DatabaseSqliteStandalone {
+ var $lastQuery;
+
+ function __construct( ) {
+ parent::__construct( ':memory:' );
+ }
+
+ function query( $sql, $fname = '', $tempIgnore = false ) {
+ $this->lastQuery = $sql;
+ return true;
+ }
+
+ function replaceVars( $s ) {
+ return parent::replaceVars( $s );
+ }
+}
+
+/**
+ * @group sqlite
+ */
+class DatabaseSqliteTest extends PHPUnit_Framework_TestCase {
+ var $db;
+
+ public function setUp() {
+ if ( !Sqlite::isPresent() ) {
+ $this->markTestSkipped( 'No SQLite support detected' );
+ }
+ $this->db = new MockDatabaseSqlite();
+ }
+
+ private function replaceVars( $sql ) {
+ // normalize spacing to hide implementation details
+ return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) );
+ }
+
+ public function testReplaceVars() {
+ $this->assertEquals( 'foo', $this->replaceVars( 'foo' ), "Don't break anything accidentally" );
+
+ $this->assertEquals( "CREATE TABLE /**/foo (foo_key INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+ . "foo_bar TEXT, foo_name TEXT NOT NULL DEFAULT '', foo_int INTEGER, foo_int2 INTEGER );",
+ $this->replaceVars( "CREATE TABLE /**/foo (foo_key int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ foo_bar char(13), foo_name varchar(255) binary NOT NULL DEFAULT '', foo_int tinyint ( 8 ), foo_int2 int(16) ) ENGINE=MyISAM;" )
+ );
+
+ $this->assertEquals( "CREATE TABLE foo ( foo1 REAL, foo2 REAL, foo3 REAL );",
+ $this->replaceVars( "CREATE TABLE foo ( foo1 FLOAT, foo2 DOUBLE( 1,10), foo3 DOUBLE PRECISION );" )
+ );
+
+ $this->assertEquals( "CREATE TABLE foo ( foo_binary1 BLOB, foo_binary2 BLOB );",
+ $this->replaceVars( "CREATE TABLE foo ( foo_binary1 binary(16), foo_binary2 varbinary(32) );" )
+ );
+
+ $this->assertEquals( "CREATE TABLE text ( text_foo TEXT );",
+ $this->replaceVars( "CREATE TABLE text ( text_foo tinytext );" ),
+ 'Table name changed'
+ );
+
+ $this->assertEquals( "CREATE TABLE enums( enum1 TEXT, myenum TEXT)",
+ $this->replaceVars( "CREATE TABLE enums( enum1 ENUM('A', 'B'), myenum ENUM ('X', 'Y'))" )
+ );
+
+ $this->assertEquals( "ALTER TABLE foo ADD COLUMN foo_bar INTEGER DEFAULT 42",
+ $this->replaceVars( "ALTER TABLE foo\nADD COLUMN foo_bar int(10) unsigned DEFAULT 42" )
+ );
+ }
+
+ public function testTableName() {
+ // @todo Moar!
+ $db = new DatabaseSqliteStandalone( ':memory:' );
+ $this->assertEquals( 'foo', $db->tableName( 'foo' ) );
+ $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
+ $db->tablePrefix( 'foo' );
+ $this->assertEquals( 'sqlite_master', $db->tableName( 'sqlite_master' ) );
+ $this->assertEquals( 'foobar', $db->tableName( 'bar' ) );
+ }
+
+ function testEntireSchema() {
+ global $IP;
+
+ $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" );
+ if ( $result !== true ) {
+ $this->fail( $result );
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+
+/**
+ * @group Database
+ */
+class DatabaseTest extends PHPUnit_Framework_TestCase {
+ var $db;
+
+ function setUp() {
+ $this->db = wfGetDB( DB_SLAVE );
+ }
+
+ function testAddQuotesNull() {
+ $check = "NULL";
+ if ( $this->db->getType() === 'sqlite' ) {
+ $check = "''";
+ }
+ $this->assertEquals( $check, $this->db->addQuotes( null ) );
+ }
+
+ function testAddQuotesInt() {
+ # returning just "1234" should be ok too, though...
+ # maybe
+ $this->assertEquals(
+ "'1234'",
+ $this->db->addQuotes( 1234 ) );
+ }
+
+ function testAddQuotesFloat() {
+ # returning just "1234.5678" would be ok too, though
+ $this->assertEquals(
+ "'1234.5678'",
+ $this->db->addQuotes( 1234.5678 ) );
+ }
+
+ function testAddQuotesString() {
+ $this->assertEquals(
+ "'string'",
+ $this->db->addQuotes( 'string' ) );
+ }
+
+ function testAddQuotesStringQuote() {
+ $check = "'string''s cause trouble'";
+ if ( $this->db->getType() === 'mysql' ) {
+ $check = "'string\'s cause trouble'";
+ }
+ $this->assertEquals(
+ $check,
+ $this->db->addQuotes( "string's cause trouble" ) );
+ }
+
+ function testFillPreparedEmpty() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT * FROM interwiki', array() );
+ $this->assertEquals(
+ "SELECT * FROM interwiki",
+ $sql );
+ }
+
+ function testFillPreparedQuestion() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?',
+ array( 4, "Snicker's_paradox" ) );
+
+ $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker''s_paradox'";
+ if ( $this->db->getType() === 'mysql' ) {
+ $check = "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker\'s_paradox'";
+ }
+ $this->assertEquals( $check, $sql );
+ }
+
+ function testFillPreparedBang() {
+ $sql = $this->db->fillPrepared(
+ 'SELECT user_id FROM ! WHERE user_name=?',
+ array( '"user"', "Slash's Dot" ) );
+
+ $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash''s Dot'";
+ if ( $this->db->getType() === 'mysql' ) {
+ $check = "SELECT user_id FROM \"user\" WHERE user_name='Slash\'s Dot'";
+ }
+ $this->assertEquals( $check, $sql );
+ }
+
+ function testFillPreparedRaw() {
+ $sql = $this->db->fillPrepared(
+ "SELECT * FROM cur WHERE cur_title='This_\\&_that,_WTF\\?\\!'",
+ array( '"user"', "Slash's Dot" ) );
+ $this->assertEquals(
+ "SELECT * FROM cur WHERE cur_title='This_&_that,_WTF?!'",
+ $sql );
+ }
+
+}
+
+
--- /dev/null
+<?php
+
+require_once( dirname( __FILE__ ) . '/ParserHelpers.php' );
+require_once( dirname(dirname(dirname( __FILE__ ))) . '/bootstrap.php' );
+
+class MediaWikiParserTest extends MediaWikiTestSetup {
+ public $count; // Number of tests in the suite.
+ public $backend; // ParserTestSuiteBackend instance
+ public $articles = array(); // Array of test articles defined by the tests
+
+ public function __construct() {
+ $suite = new PHPUnit_Framework_TestSuite('Parser Tests');
+ parent::__construct($suite);
+ $this->backend = new ParserTestSuiteBackend;
+ $this->setName( 'Parser tests' );
+ }
+
+ public static function suite() {
+ global $IP;
+
+ $tester = new self;
+
+ $iter = new TestFileIterator( "$IP/maintenance/tests/parser/parserTests.txt", $tester );
+ $tester->count = 0;
+
+ foreach ( $iter as $test ) {
+ $tester->suite->addTest( new ParserUnitTest( $tester, $test ), array( 'Parser', 'Destructive', 'Database', 'Broken' ) );
+ $tester->count++;
+ }
+
+ return $tester->suite;
+ }
+
+ public function count() {
+ return $this->count;
+ }
+
+ public function toString() {
+ return "MediaWiki Parser Tests";
+ }
+
+ public function getBackend() {
+ return $this->backend;
+ }
+
+ public function getIterator() {
+ return $this->iterator;
+ }
+
+ public function publishTestArticles() {
+ if ( empty( $this->articles ) ) {
+ return;
+ }
+
+ foreach ( $this->articles as $name => $text ) {
+ $title = Title::newFromText( $name );
+
+ if ( $title->getArticleID( Title::GAID_FOR_UPDATE ) == 0 ) {
+ ParserTest::addArticle( $name, $text );
+ }
+ }
+ $this->articles = array();
+ }
+
+ public function addArticle( $name, $text, $line ) {
+ $this->articles[$name] = $text;
+ }
+
+ public function showRunFile( $path ) {
+ /* Nothing shown when run from phpunit */
+ }
+}
+
--- /dev/null
+<?php
+
+class PHPUnitParserTest extends ParserTest {
+ function showTesting( $desc ) {
+ /* Do nothing since we don't want to show info during PHPUnit testing. */
+ }
+
+ public function showSuccess( $desc ) {
+ PHPUnit_Framework_Assert::assertTrue( true, $desc );
+ return true;
+ }
+
+ public function showFailure( $desc, $expected, $got ) {
+ PHPUnit_Framework_Assert::assertEquals( $expected, $got, $desc );
+ return false;
+ }
+}
+
+class ParserUnitTest extends PHPUnit_Framework_TestCase {
+ private $test = "";
+ private $suite;
+
+ public function __construct( $suite, $test = null ) {
+ $this->suite = $suite;
+ $this->test = $test;
+ }
+
+ function count() { return 1; }
+
+ public function run( PHPUnit_Framework_TestResult $result = null ) {
+ PHPUnit_Framework_Assert::resetCount();
+ if ( $result === NULL ) {
+ $result = new PHPUnit_Framework_TestResult;
+ }
+
+ $this->suite->publishTestArticles(); // Add articles needed by the tests.
+ $backend = new ParserTestSuiteBackend;
+ $result->startTest( $this );
+
+ // Support the transition to PHPUnit 3.5 where PHPUnit_Util_Timer is replaced with PHP_Timer
+ if ( class_exists( 'PHP_Timer' ) ) {
+ PHP_Timer::start();
+ } else {
+ PHPUnit_Util_Timer::start();
+ }
+
+ $r = false;
+ try {
+ # Run the test.
+ # On failure, the subclassed backend will throw an exception with
+ # the details.
+ $pt = new PHPUnitParserTest;
+ $r = $pt->runTest( $this->test['test'], $this->test['input'],
+ $this->test['result'], $this->test['options'], $this->test['config']
+ );
+ }
+ catch ( PHPUnit_Framework_AssertionFailedError $e ) {
+
+ // PHPUnit_Util_Timer -> PHP_Timer support (see above)
+ if ( class_exists( 'PHP_Timer' ) ) {
+ $result->addFailure( $this, $e, PHP_Timer::stop() );
+ } else {
+ $result->addFailure( $this, $e, PHPUnit_Util_Timer::stop() );
+ }
+ }
+ catch ( Exception $e ) {
+ // PHPUnit_Util_Timer -> PHP_Timer support (see above)
+ if ( class_exists( 'PHP_Timer' ) ) {
+ $result->addFailure( $this, $e, PHP_Timer::stop() );
+ } else {
+ $result->addFailure( $this, $e, PHPUnit_Util_Timer::stop() );
+ }
+ }
+
+ // PHPUnit_Util_Timer -> PHP_Timer support (see above)
+ if ( class_exists( 'PHP_Timer' ) ) {
+ $result->endTest( $this, PHP_Timer::stop() );
+ } else {
+ $result->endTest( $this, PHPUnit_Util_Timer::stop() );
+ }
+
+ $backend->recorder->record( $this->test['test'], $r );
+ $this->addToAssertionCount( PHPUnit_Framework_Assert::getCount() );
+
+ return $result;
+ }
+
+ public function toString() {
+ return $this->test['test'];
+ }
+
+}
+
+class ParserTestSuiteBackend extends PHPUnit_FrameWork_TestSuite {
+ public $recorder;
+ public $term;
+ static $usePHPUnit = false;
+
+ function __construct() {
+ parent::__construct();
+ $this->setupRecorder(null);
+ self::$usePHPUnit = method_exists('PHPUnit_Framework_Assert', 'assertEquals');
+ }
+
+ function showTesting( $desc ) {
+ }
+
+ function showRunFile( $path ) {
+ }
+
+ function showTestResult( $desc, $result, $out ) {
+ if ( $result === $out ) {
+ return self::showSuccess( $desc, $result, $out );
+ } else {
+ return self::showFailure( $desc, $result, $out );
+ }
+ }
+
+ public function setupRecorder( $options ) {
+ $this->recorder = new PHPUnitTestRecorder( $this );
+ }
+}
+
+class PHPUnitTestRecorder extends TestRecorder {
+ function record( $test, $result ) {
+ $this->total++;
+ $this->success += $result;
+
+ }
+
+ function reportPercentage( $success, $total ) { }
+}
--- /dev/null
+<?php
+
+require_once( dirname( __FILE__ ) . '/SearchEngineTest.php' );
+
+/**
+ * @group Search
+ * @group Destructive
+ */
+class SearchDbTest extends SearchEngineTest {
+ var $db;
+
+ function setUp() {
+ // Get a database connection or skip test
+ $this->db = wfGetDB( DB_MASTER );
+ if ( !$this->db ) {
+ $this->markTestIncomplete( "Can't find a database to test with." );
+ }
+
+ parent::setup();
+
+ // Initialize search database with data
+ $GLOBALS['wgContLang'] = new Language;
+ $this->insertSearchData();
+
+ $this->insertSearchData();
+ $searchType = preg_replace( "/Database/", "Search",
+ get_class( $this->db ) );
+ $this->search = new $searchType( $this->db );
+ }
+
+ function tearDown() {
+ $this->removeSearchData();
+ unset( $this->search );
+ }
+}
+
+
--- /dev/null
+<?php
+
+require_once dirname(dirname(dirname(__FILE__))). '/bootstrap.php';
+
+/**
+ * This class is not directly tested. Instead it is extended by SearchDbTest.
+ * @group Search
+ * @group Stub
+ * @group Destructive
+ */
+class SearchEngineTest extends MediaWikiTestSetup {
+ var $db, $search, $pageList;
+
+ /*
+ * Checks for database type & version.
+ * Will skip current test if DB does not support search.
+ */
+ function setUp() {
+ // Search tests require MySQL or SQLite with FTS
+ # Get database type and version
+ $dbType = $this->db->getType();
+ $dbSupported =
+ ($dbType === 'mysql')
+ || ( $dbType === 'sqlite' && $this->db->getFulltextSearchModule() == 'FTS3' );
+
+ if( !$dbSupported ) {
+ $this->markTestSkipped( "MySQL or SQLite with FTS3 only" );
+ }
+ }
+
+ function pageExists( $title ) {
+ return false;
+ }
+
+ function insertSearchData() {
+ if ( $this->pageExists( 'Not_Main_Page' ) ) {
+ return;
+ }
+ $this->insertPage( "Not_Main_Page", "This is not a main page", 0 );
+ $this->insertPage( 'Talk:Not_Main_Page', 'This is not a talk page to the main page, see [[smithee]]', 1 );
+ $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]', 0 );
+ $this->insertPage( 'Talk:Smithee', 'This article sucks.', 1 );
+ $this->insertPage( 'Unrelated_page', 'Nothing in this page is about the S word.', 0 );
+ $this->insertPage( 'Another_page', 'This page also is unrelated.', 0 );
+ $this->insertPage( 'Help:Help', 'Help me!', 4 );
+ $this->insertPage( 'Thppt', 'Blah blah', 0 );
+ $this->insertPage( 'Alan_Smithee', 'yum', 0 );
+ $this->insertPage( 'Pages', 'are\'food', 0 );
+ $this->insertPage( 'HalfOneUp', 'AZ', 0 );
+ $this->insertPage( 'FullOneUp', 'AZ', 0 );
+ $this->insertPage( 'HalfTwoLow', 'az', 0 );
+ $this->insertPage( 'FullTwoLow', 'az', 0 );
+ $this->insertPage( 'HalfNumbers', '1234567890', 0 );
+ $this->insertPage( 'FullNumbers', '1234567890', 0 );
+ $this->insertPage( 'DomainName', 'example.com', 0 );
+ }
+
+ function removeSearchData() {
+ return;
+ /*while ( count( $this->pageList ) ) {
+ list( $title, $id ) = array_pop( $this->pageList );
+ $article = new Article( $title, $id );
+ $article->doDeleteArticle( "Search Test" );
+ }*/
+ }
+
+ function fetchIds( $results ) {
+ $this->assertTrue( is_object( $results ) );
+
+ $matches = array();
+ while ( $row = $results->next() ) {
+ $matches[] = $row->getTitle()->getPrefixedText();
+ }
+ $results->free();
+ # Search is not guaranteed to return results in a certain order;
+ # sort them numerically so we will compare simply that we received
+ # the expected matches.
+ sort( $matches );
+ return $matches;
+ }
+
+ // Modified version of WikiRevision::importOldRevision()
+ function insertPage( $pageName, $text, $ns ) {
+ $dbw = $this->db;
+ $title = Title::newFromText( $pageName );
+
+ $userId = 0;
+ $userText = 'WikiSysop';
+ $comment = 'Search Test';
+
+ // avoid memory leak...?
+ $linkCache = LinkCache::singleton();
+ $linkCache->clear();
+
+ $article = new Article( $title );
+ $pageId = $article->getId();
+ $created = false;
+ if ( $pageId == 0 ) {
+ # must create the page...
+ $pageId = $article->insertOn( $dbw );
+ $created = true;
+ }
+
+ # FIXME: Use original rev_id optionally (better for backups)
+ # Insert the row
+ $revision = new Revision( array(
+ 'page' => $pageId,
+ 'text' => $text,
+ 'comment' => $comment,
+ 'user' => $userId,
+ 'user_text' => $userText,
+ 'timestamp' => 0,
+ 'minor_edit' => false,
+ ) );
+ $revId = $revision->insertOn( $dbw );
+ $changed = $article->updateIfNewerOn( $dbw, $revision );
+
+ $GLOBALS['wgTitle'] = $title;
+ if ( $created ) {
+ Article::onArticleCreate( $title );
+ $article->createUpdates( $revision );
+ } elseif ( $changed ) {
+ Article::onArticleEdit( $title );
+ $article->editUpdates(
+ $text, $comment, false, 0, $revId );
+ }
+
+ $su = new SearchUpdate( $article->getId(), $pageName, $text );
+ $su->doUpdate();
+
+ $this->pageList[] = array( $title, $article->getId() );
+
+ return true;
+ }
+
+ function testFullWidth() {
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'AZ' ) ),
+ "Search for normalized from Half-width Upper" );
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'az' ) ),
+ "Search for normalized from Half-width Lower" );
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'AZ' ) ),
+ "Search for normalized from Full-width Upper" );
+ $this->assertEquals(
+ array( 'FullOneUp', 'FullTwoLow', 'HalfOneUp', 'HalfTwoLow' ),
+ $this->fetchIds( $this->search->searchText( 'az' ) ),
+ "Search for normalized from Full-width Lower" );
+ }
+
+ function testTextSearch() {
+ $this->assertEquals(
+ array( 'Smithee' ),
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Plain search failed" );
+ }
+
+ function testTextPowerSearch() {
+ $this->search->setNamespaces( array( 0, 1, 4 ) );
+ $this->assertEquals(
+ array(
+ 'Smithee',
+ 'Talk:Not Main Page',
+ ),
+ $this->fetchIds( $this->search->searchText( 'smithee' ) ),
+ "Power search failed" );
+ }
+
+ function testTitleSearch() {
+ $this->assertEquals(
+ array(
+ 'Alan Smithee',
+ 'Smithee',
+ ),
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title search failed" );
+ }
+
+ function testTextTitlePowerSearch() {
+ $this->search->setNamespaces( array( 0, 1, 4 ) );
+ $this->assertEquals(
+ array(
+ 'Alan Smithee',
+ 'Smithee',
+ 'Talk:Smithee',
+ ),
+ $this->fetchIds( $this->search->searchTitle( 'smithee' ) ),
+ "Title power search failed" );
+ }
+
+}
--- /dev/null
+<?php
+
+class MockSearch extends SearchEngine {
+ public static $id;
+ public static $title;
+ public static $text;
+
+ public function __construct( $db ) {
+ }
+
+ public function update( $id, $title, $text ) {
+ self::$id = $id;
+ self::$title = $title;
+ self::$text = $text;
+ }
+}
+
+/**
+ * @group Search
+ */
+class SearchUpdateTest extends PHPUnit_Framework_TestCase {
+ static $searchType;
+
+ function update( $text, $title = 'Test', $id = 1 ) {
+ $u = new SearchUpdate( $id, $title, $text );
+ $u->doUpdate();
+ return array( MockSearch::$title, MockSearch::$text );
+ }
+
+ function updateText( $text ) {
+ list( , $resultText ) = $this->update( $text );
+ $resultText = trim( $resultText ); // abstract from some implementation details
+ return $resultText;
+ }
+
+ function setUp() {
+ global $wgSearchType;
+
+ self::$searchType = $wgSearchType;
+ $wgSearchType = 'MockSearch';
+ }
+
+ function tearDown() {
+ global $wgSearchType;
+
+ $wgSearchType = self::$searchType;
+ }
+
+ function testUpdateText() {
+ $this->assertEquals(
+ 'test',
+ $this->updateText( '<div>TeSt</div>' ),
+ 'HTML stripped, text lowercased'
+ );
+
+ $this->assertEquals(
+ 'foo bar boz quux',
+ $this->updateText( <<<EOT
+<table style="color:red; font-size:100px">
+ <tr class="scary"><td><div>foo</div></td><tr>bar</td></tr>
+ <tr><td>boz</td><tr>quux</td></tr>
+</table>
+EOT
+ ), 'Stripping HTML tables' );
+
+ $this->assertEquals(
+ 'a b',
+ $this->updateText( 'a > b' ),
+ 'Handle unclosed tags'
+ );
+
+ $text = str_pad( "foo <barbarbar \n", 10000, 'x' );
+
+ $this->assertNotEquals(
+ '',
+ $this->updateText( $text ),
+ 'Bug 18609'
+ );
+ }
+}
--- /dev/null
+#!/bin/sh
+
+if [ `id -u` -ne 0 ]; then
+ echo '*** ERROR' Must be root to run
+ exit 1
+fi
+
+if ( has_binary phpunit ); then
+ echo PHPUnit already installed
+else if ( has_binary pear ); then
+ echo Installing phpunit with pear
+ pear channel-discover pear.phpunit.de
+ pear install phpunit/PHPUnit
+else if ( has_binary apt-get ); then
+ echo Installing phpunit with apt-get
+ apt-get install phpunit
+else if ( has_binary yum ); then
+ echo Installing phpunit with yum
+ yum install phpunit
+fi
+fi
+fi
+fi
--- /dev/null
+<?php
+require_once dirname(dirname(__FILE__)). '/bootstrap.php';
+
+class LanguageBeTaraskTest extends MediaWikiTestSetup {
+ private $lang;
+
+ function setUp() {
+ $this->lang = Language::factory( 'Be_tarask' );
+ }
+ function tearDown() {
+ unset( $this->lang );
+ }
+
+ /** see bug 23156 & r64981 */
+ function testSearchRightSingleQuotationMarkAsApostroph() {
+ $this->assertEquals(
+ "'",
+ $this->lang->normalizeForSearch( '’' ),
+ 'bug 23156: U+2019 conversion to U+0027'
+ );
+ }
+ /** see bug 23156 & r64981 */
+ function testCommafy() {
+ $this->assertEquals( '1,234,567', $this->lang->commafy( '1234567' ) );
+ $this->assertEquals( '12,345', $this->lang->commafy( '12345' ) );
+ }
+ /** see bug 23156 & r64981 */
+ function testDoesNotCommafyFourDigitsNumber() {
+ $this->assertEquals( '1234', $this->lang->commafy( '1234' ) );
+ }
+}
--- /dev/null
+<?php
+require_once dirname(dirname(__FILE__)). '/bootstrap.php';
+
+class LanguageTest extends MediaWikiTestSetup {
+ private $lang;
+
+ function setUp() {
+ $this->lang = Language::factory( 'en' );
+ }
+ function tearDown() {
+ unset( $this->lang );
+ }
+
+ function testLanguageConvertDoubleWidthToSingleWidth() {
+ $this->assertEquals(
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+ $this->lang->normalizeForSearch(
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ ),
+ 'convertDoubleWidth() with the full alphabet and digits'
+ );
+ }
+
+ function testFormatTimePeriod() {
+ $this->assertEquals(
+ "9.5s",
+ $this->lang->formatTimePeriod( 9.45 ),
+ 'formatTimePeriod() rounding (<10s)'
+ );
+
+ $this->assertEquals(
+ "10s",
+ $this->lang->formatTimePeriod( 9.95 ),
+ 'formatTimePeriod() rounding (<10s)'
+ );
+
+ $this->assertEquals(
+ "1m 0s",
+ $this->lang->formatTimePeriod( 59.55 ),
+ 'formatTimePeriod() rounding (<60s)'
+ );
+
+ $this->assertEquals(
+ "2m 0s",
+ $this->lang->formatTimePeriod( 119.55 ),
+ 'formatTimePeriod() rounding (<1h)'
+ );
+
+ $this->assertEquals(
+ "1h 0m 0s",
+ $this->lang->formatTimePeriod( 3599.55 ),
+ 'formatTimePeriod() rounding (<1h)'
+ );
+
+ $this->assertEquals(
+ "2h 0m 0s",
+ $this->lang->formatTimePeriod( 7199.55 ),
+ 'formatTimePeriod() rounding (>=1h)'
+ );
+ }
+}
--- /dev/null
+#!/usr/bin/env php
+<?php
+/**
+ * Bootstrapping for MediaWiki PHPUnit tests
+ *
+ * @file
+ */
+
+/* Configuration */
+
+// Evaluate the include path relative to this file
+$IP = dirname( dirname( dirname( dirname( __FILE__ ) ) ) );
+
+// Set a flag which can be used to detect when other scripts have been entered through this entry point or not
+define( 'MW_PHPUNIT_TEST', true );
+
+// Start up MediaWiki in command-line mode
+require_once( "$IP/maintenance/commandLine.inc" );
+
+// Assume UTC for testing purposes
+$wgLocaltimezone = 'UTC';
+
+require_once( 'PHPUnit/Runner/Version.php' );
+if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '>=' ) ) {
+ # PHPUnit 3.5.0 introduced a nice autoloader based on class name
+ require_once( 'PHPUnit/Autoload.php' );
+} else {
+ # Keep the old pre PHPUnit 3.5.0 behaviour for compatibility
+ require_once( 'PHPUnit/TextUI/Command.php' );
+}
+PHPUnit_TextUI_Command::main();
--- /dev/null
+php phpunit.php --configuration suite.xml %*
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit bootstrap="./bootstrap.php"
+ colors="false"
+ backupGlobals="false"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ stopOnFailure="false"
+ strict="true">
+ <testsuites>
+ <testsuite name="includes">
+ <directory>./includes</directory>
+ </testsuite>
+ <testsuite name="languages">
+ <directory>./languages</directory>
+ </testsuite>
+ <testsuite name="skins">
+ <directory>./skins</directory>
+ </testsuite>
+ <testsuite name="uploadfromurl">
+ <file>./suites/UploadFromUrlTestSuite.php</file>
+ </testsuite>
+ <testsuite name="extensions">
+ <file>./suites/ExtensionsTestSuite.php</file>
+ </testsuite>
+ </testsuites>
+ <groups>
+ <exclude>
+ <group>Utility</group>
+ <group>Broken</group>
+ <group>Stub</group>
+ </exclude>
+ </groups>
+</phpunit>
--- /dev/null
+<?php
+/**
+ * This test suite runs unit tests registered by extensions.
+ * See http://www.mediawiki.org/wiki/Manual:Hooks/UnitTestsList for details of how to register your tests.
+ */
+
+class ExtensionsTestSuite extends PHPUnit_Framework_TestSuite {
+ public function __construct() {
+ parent::__construct();
+ $files = array();
+ wfRunHooks( 'UnitTestsList', array( &$files ) );
+ foreach ( $files as $file ) {
+ $this->addTestFile( $file );
+ }
+ if ( !count( $files ) ) {
+ $this->addTest( new DummyExtensionsTest( 'testNothing' ) );
+ }
+ }
+
+ public static function suite() {
+ return new self;
+ }
+}
+
+/**
+ * Needed to avoid warnings like 'No tests found in class "ExtensionsTestSuite".'
+ * when no extensions with tests are used.
+ */
+class DummyExtensionsTest extends PHPUnit_Framework_TestCase {
+ public function testNothing() {
+
+ }
+}
--- /dev/null
+<?php
+
+require_once( dirname( dirname( __FILE__ ) ) . '/includes/UploadFromUrlTest.php' );
+
+class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
+ public static function addTables( &$tables ) {
+ $tables[] = 'user_properties';
+ $tables[] = 'filearchive';
+ $tables[] = 'logging';
+ $tables[] = 'updatelog';
+ $tables[] = 'iwlinks';
+
+ return true;
+ }
+
+ function setUp() {
+ global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, $wgDeferredUpdateList,
+ $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory, $wgEnableParserCache,
+ $wgMessageCache, $wgUseDatabaseMessages, $wgMsgCacheExpiry, $parserMemc,
+ $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo,
+ $wgThumbnailScriptPath, $wgScriptPath,
+ $wgArticlePath, $wgStyleSheetPath, $wgScript, $wgStylePath;
+
+ $wgScript = '/index.php';
+ $wgScriptPath = '/';
+ $wgArticlePath = '/wiki/$1';
+ $wgStyleSheetPath = '/skins';
+ $wgStylePath = '/skins';
+ $wgThumbnailScriptPath = false;
+ $wgLocalFileRepo = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'directory' => wfTempDir() . '/test-repo',
+ 'url' => 'http://example.com/images',
+ 'deletedDir' => wfTempDir() . '/test-repo/delete',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ );
+ $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+
+ $wgEnableParserCache = false;
+ $wgDeferredUpdateList = array();
+ $wgMemc = &wfGetMainCache();
+ $messageMemc = &wfGetMessageCacheStorage();
+ $parserMemc = &wfGetParserCacheStorage();
+
+ // $wgContLang = new StubContLang;
+ $wgUser = new User;
+ $wgLang = new StubUserLang;
+ $wgOut = new StubObject( 'wgOut', 'OutputPage' );
+ $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) );
+ $wgRequest = new WebRequest;
+
+ $wgMessageCache = new StubObject( 'wgMessageCache', 'MessageCache',
+ array( $messageMemc, $wgUseDatabaseMessages,
+ $wgMsgCacheExpiry ) );
+ if ( $wgStyleDirectory === false ) {
+ $wgStyleDirectory = "$IP/skins";
+ }
+
+ }
+
+ public function tearDown() {
+ $this->teardownUploadDir( $this->uploadDir );
+ }
+
+ private $uploadDir;
+ private $keepUploads;
+
+ /**
+ * Remove the dummy uploads directory
+ */
+ private function teardownUploadDir( $dir ) {
+ if ( $this->keepUploads ) {
+ return;
+ }
+
+ // delete the files first, then the dirs.
+ self::deleteFiles(
+ array (
+ "$dir/3/3a/Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/200px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/640px-Foobar.jpg",
+ "$dir/thumb/3/3a/Foobar.jpg/120px-Foobar.jpg",
+
+ "$dir/0/09/Bad.jpg",
+ )
+ );
+
+ self::deleteDirs(
+ array (
+ "$dir/3/3a",
+ "$dir/3",
+ "$dir/thumb/6/65",
+ "$dir/thumb/6",
+ "$dir/thumb/3/3a/Foobar.jpg",
+ "$dir/thumb/3/3a",
+ "$dir/thumb/3",
+
+ "$dir/0/09/",
+ "$dir/0/",
+
+ "$dir/thumb",
+ "$dir",
+ )
+ );
+ }
+
+ /**
+ * Delete the specified files, if they exist.
+ *
+ * @param $files Array: full paths to files to delete.
+ */
+ private static function deleteFiles( $files ) {
+ foreach ( $files as $file ) {
+ if ( file_exists( $file ) ) {
+ unlink( $file );
+ }
+ }
+ }
+
+ /**
+ * Delete the specified directories, if they exist. Must be empty.
+ *
+ * @param $dirs Array: full paths to directories to delete.
+ */
+ private static function deleteDirs( $dirs ) {
+ foreach ( $dirs as $dir ) {
+ if ( is_dir( $dir ) ) {
+ rmdir( $dir );
+ }
+ }
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return String: the directory
+ */
+ private function setupUploadDir() {
+ global $IP;
+
+ if ( $this->keepUploads ) {
+ $dir = wfTempDir() . '/mwParser-images';
+
+ if ( is_dir( $dir ) ) {
+ return $dir;
+ }
+ } else {
+ $dir = wfTempDir() . "/mwParser-" . mt_rand() . "-images";
+ }
+
+ wfDebug( "Creating upload directory $dir\n" );
+
+ if ( file_exists( $dir ) ) {
+ wfDebug( "Already exists!\n" );
+ return $dir;
+ }
+
+ wfMkdirParents( $dir . '/3/3a' );
+ copy( "$IP/skins/monobook/headbg.jpg", "$dir/3/3a/Foobar.jpg" );
+
+ wfMkdirParents( $dir . '/0/09' );
+ copy( "$IP/skins/monobook/headbg.jpg", "$dir/0/09/Bad.jpg" );
+
+ return $dir;
+ }
+
+ public static function suite() {
+ // Hack to invoke the autoloader required to get phpunit to recognize
+ // the UploadFromUrlTest class
+ class_exists( 'UploadFromUrlTest' );
+ $suite = new UploadFromUrlTestSuite( 'UploadFromUrlTest' );
+ return $suite;
+ }
+}
--- /dev/null
+<?php
+/**
+ * Selenium connector
+ * This is implemented as a singleton.
+ */
+
+require( 'Testing/Selenium.php' );
+
+class Selenium {
+ protected static $_instance = null;
+
+ public $isStarted = false;
+ public $tester;
+
+ protected $port;
+ protected $host;
+ protected $browser;
+ protected $browsers;
+ protected $logger;
+ protected $user;
+ protected $pass;
+ protected $timeout = 30000;
+ protected $verbose;
+ protected $junitlogfile; //processed by phpUnderControl
+ protected $runagainstgrid = false;
+
+ /**
+ * @todo this shouldn't have to be static
+ */
+ static protected $url;
+
+ /**
+ * Override parent
+ */
+ public function __construct() {
+ /**
+ * @todo this is an ugly hack to make information available to
+ * other tests. It should be fixed.
+ */
+ if ( null === self::$_instance ) {
+ self::$_instance = $this;
+ } else {
+ throw new MWException( "Already have one Selenium instance." );
+ }
+ }
+
+ public function start() {
+ $this->tester = new Testing_Selenium( $this->browser, self::$url, $this->host,
+ $this->port, $this->timeout );
+ if ( method_exists( $this->tester, "setVerbose" ) ) $this->tester->setVerbose( $this->verbose );
+
+ $this->tester->start();
+ $this->isStarted = true;
+ }
+
+ public function stop() {
+ $this->tester->stop();
+ $this->tester = null;
+ $this->isStarted = false;
+ }
+
+ public function login() {
+ if ( strlen( $this->user ) == 0 ) {
+ return;
+ }
+ $this->open( self::$url . '/index.php?title=Special:Userlogin' );
+ $this->type( 'wpName1', $this->user );
+ $this->type( 'wpPassword1', $this->pass );
+ $this->click( "//input[@id='wpLoginAttempt']" );
+ $this->waitForPageToLoad( 10000 );
+
+ // after login we redirect to the main page. So check whether the "Prefernces" top menu item exists
+ $value = $this->isElementPresent( "//li[@id='pt-preferences']" );
+
+ if ( $value != true ) {
+ throw new Testing_Selenium_Exception( "Login Failed" );
+ }
+
+ }
+
+ public static function getInstance() {
+ if ( null === self::$_instance ) {
+ throw new MWException( "No instance set yet" );
+ }
+
+ return self::$_instance;
+ }
+
+ public function loadPage( $title, $action ) {
+ $this->open( self::$url . '/index.php?title=' . $title . '&action=' . $action );
+ }
+
+ public function setLogger( $logger ) {
+ $this->logger = $logger;
+ }
+
+ public function getLogger( ) {
+ return $this->logger;
+ }
+
+ public function log( $message ) {
+ $this->logger->write( $message );
+ }
+
+ public function setUrl( $url ) {
+ self::$url = $url;
+ }
+
+ static public function getUrl() {
+ return self::$url;
+ }
+
+ public function setPort( $port ) {
+ $this->port = $port;
+ }
+
+ public function getPort() {
+ return $this->port;
+ }
+
+ public function setUser( $user ) {
+ $this->user = $user;
+ }
+
+ // Function to get username
+ public function getUser() {
+ return $this->user;
+ }
+
+
+ public function setPass( $pass ) {
+ $this->pass = $pass;
+ }
+
+ //add function to get password
+ public function getPass( ) {
+ return $this->pass;
+ }
+
+
+ public function setHost( $host ) {
+ $this->host = $host;
+ }
+
+ public function setVerbose( $verbose ) {
+ $this->verbose = $verbose;
+ }
+
+ public function setAvailableBrowsers( $availableBrowsers ) {
+ $this->browsers = $availableBrowsers;
+ }
+
+ public function setJUnitLogfile( $junitlogfile ) {
+ $this->junitlogfile = $junitlogfile;
+ }
+
+ public function getJUnitLogfile( ) {
+ return $this->junitlogfile;
+ }
+
+ public function setRunAgainstGrid( $runagainstgrid ) {
+ $this->runagainstgrid = $runagainstgrid;
+ }
+
+ public function setBrowser( $b ) {
+ if ($this->runagainstgrid) {
+ $this->browser = $b;
+ return true;
+ }
+ if ( !isset( $this->browsers[$b] ) ) {
+ throw new MWException( "Invalid Browser: $b.\n" );
+ }
+
+ $this->browser = $this->browsers[$b];
+ }
+
+ public function getAvailableBrowsers() {
+ return $this->browsers;
+ }
+
+ public function __call( $name, $args ) {
+ $t = call_user_func_array( array( $this->tester, $name ), $args );
+ return $t;
+ }
+
+ // Prevent external cloning
+ protected function __clone() { }
+ // Prevent external construction
+ // protected function __construct() {}
+}
--- /dev/null
+<?php
+if ( !defined( 'SELENIUMTEST' ) ) {
+ die( 1 );
+}
+
+class SeleniumConfig {
+
+ /*
+ * Retreives the Selenium configuration values from an ini file.
+ * See sample config file in selenium_settings.ini.sample
+ *
+ */
+
+ public static function getSeleniumSettings ( &$seleniumSettings,
+ &$seleniumBrowsers,
+ &$seleniumTestSuites,
+ $seleniumConfigFile = null ) {
+ if ( strlen( $seleniumConfigFile ) == 0 ) {
+ global $wgSeleniumConfigFile;
+ if ( isset( $wgSeleniumConfigFile ) ) $seleniumConfigFile = $wgSeleniumConfigFile ;
+ }
+
+ if ( strlen( $seleniumConfigFile ) == 0 || !file_exists( $seleniumConfigFile ) ) {
+ throw new MWException( "Unable to read local Selenium Settings from " . $seleniumConfigFile . "\n" );
+ }
+
+ if ( !defined( 'PHP_VERSION_ID' ) ||
+ ( PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION < 3 ) ) {
+ $configArray = self::parse_5_2_ini_file( $seleniumConfigFile );
+ } else {
+ $configArray = parse_ini_file( $seleniumConfigFile, true );
+ }
+ if ( $configArray === false ) {
+ throw new MWException( "Error parsing " . $seleniumConfigFile . "\n" );
+ }
+
+ if ( array_key_exists( 'SeleniumSettings', $configArray) ) {
+ wfSuppressWarnings();
+ //we may need to change how this is set. But for now leave it in the ini file
+ $seleniumBrowsers = $configArray['SeleniumSettings']['browsers'];
+
+ $seleniumSettings['host'] = $configArray['SeleniumSettings']['host'];
+ $seleniumSettings['port'] = $configArray['SeleniumSettings']['port'];
+ $seleniumSettings['wikiUrl'] = $configArray['SeleniumSettings']['wikiUrl'];
+ $seleniumSettings['username'] = $configArray['SeleniumSettings']['username'];
+ $seleniumSettings['userPassword'] = $configArray['SeleniumSettings']['userPassword'];
+ $seleniumSettings['testBrowser'] = $configArray['SeleniumSettings']['testBrowser'];
+ $seleniumSettings['startserver'] = $configArray['SeleniumSettings']['startserver'];
+ $seleniumSettings['stopserver'] = $configArray['SeleniumSettings']['stopserver'];
+ $seleniumSettings['seleniumserverexecpath'] = $configArray['SeleniumSettings']['seleniumserverexecpath'];
+ $seleniumSettings['jUnitLogFile'] = $configArray['SeleniumSettings']['jUnitLogFile'];
+ $seleniumSettings['runAgainstGrid'] = $configArray['SeleniumSettings']['runAgainstGrid'];
+
+ wfRestoreWarnings();
+ }
+ if ( array_key_exists( 'SeleniumTests', $configArray) ) {
+ wfSuppressWarnings();
+ $seleniumTestSuites = $configArray['SeleniumTests']['testSuite'];
+ wfRestoreWarnings();
+ }
+ return true;
+ }
+
+ private static function parse_5_2_ini_file ( $ConfigFile ) {
+
+ $configArray = parse_ini_file( $ConfigFile, true );
+ if ( $configArray === false ) return false;
+
+ // PHP 5.2 ini files have [browsers] and [testSuite] sections
+ // to get around lack of support for array keys. It then
+ // inserts the section arrays into the appropriate places in
+ // the SeleniumSettings and SeleniumTests arrays.
+
+ if ( isset( $configArray['browsers'] ) ) {
+ $configArray['SeleniumSettings']['browsers'] = $configArray['browsers'];
+ unset ( $configArray['browsers'] );
+ }
+
+ if ( isset( $configArray['testSuite'] ) ) {
+ $configArray['SeleniumTests']['testSuite'] = $configArray['testSuite'];
+ unset ( $configArray['testSuite'] );
+ }
+
+ return $configArray;
+
+ }
+
+}
--- /dev/null
+<?php
+
+class SeleniumLoader {
+ static function load() {
+ require_once( 'Testing/Selenium.php' );
+ require_once( 'PHPUnit/Framework.php' );
+ require_once( 'PHPUnit/Extensions/SeleniumTestCase.php' );
+ }
+}
--- /dev/null
+<?php
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ */
+
+class SeleniumServerManager {
+ private $SeleniumStartServer = false;
+ private $OS = '';
+ private $SeleniumServerPid = 'NaN';
+ private $SeleniumServerPort = 4444;
+ private $SeleniumServerStartTimeout = 10; // 10 secs.
+ private $SeleniumServerExecPath;
+
+ public function __construct( $startServer,
+ $serverPort,
+ $serverExecPath ) {
+ $this->OS = (string) PHP_OS;
+ if ( isset( $startServer ) )
+ $this->SeleniumStartServer = $startServer;
+ if ( isset( $serverPort ) )
+ $this->SeleniumServerPort = $serverPort;
+ if ( isset( $serverExecPath ) )
+ $this->SeleniumServerExecPath = $serverExecPath;
+ return;
+ }
+
+ // Getters for certain private attributes. No setters, since they
+ // should not change after the manager object is created.
+
+ public function getSeleniumStartServer() {
+ return $this->SeleniumStartServer;
+ }
+
+ public function getSeleniumServerPort() {
+ return $this->SeleniumServerPort;
+ }
+
+ public function getSeleniumServerPid() {
+ return $this->SeleniumServerPid;
+ }
+
+ // Changing value of SeleniumStartServer allows starting server after
+ // creation of the class instance. Only allow setting SeleniumStartServer
+ // to true, since after server is started, it is shut down by stop().
+
+ public function setSeleniumStartServer( $startServer ) {
+ if ( $startServer == true ) $this->SeleniumStartServer = true;
+ }
+
+ // return values are: 1) started - server started, 2) failed -
+ // server not started, 3) running - instructed to start server, but
+ // server already running
+
+ public function start() {
+
+ if ( !$this->SeleniumStartServer ) return 'failed';
+
+ // commented out cases are untested
+
+ switch ( $this->OS ) {
+ case "Linux":
+# case' CYGWIN_NT-5.1':
+ case 'Darwin':
+# case 'FreeBSD':
+# case 'HP-UX':
+# case 'IRIX64':
+# case 'NetBSD':
+# case 'OpenBSD':
+# case 'SunOS':
+# case 'Unix':
+ // *nix based OS
+ return $this->startServerOnUnix();
+ break;
+ case "Windows":
+ case "WIN32":
+ case "WINNT":
+ // Windows
+ return $this->startServerOnWindows();
+ break;
+ default:
+ // An untested OS
+ return 'failed';
+ break;
+ }
+ }
+
+ public function stop() {
+
+ // commented out cases are untested
+
+ switch ( $this->OS ) {
+ case "Linux":
+# case' CYGWIN_NT-5.1':
+ case 'Darwin':
+# case 'FreeBSD':
+# case 'HP-UX':
+# case 'IRIX64':
+# case 'NetBSD':
+# case 'OpenBSD':
+# case 'SunOS':
+# case 'Unix':
+ // *nix based OS
+ return $this->stopServerOnUnix();
+ break;
+ case "Windows":
+ case "WIN32":
+ case "WINNT":
+ // Windows
+ return $this->stopServerOnWindows();
+ break;
+ default:
+ // An untested OS
+ return 'failed';
+ break;
+ }
+ }
+
+ private function startServerOnUnix() {
+
+ $output = array();
+ $user = $_ENV['USER'];
+ // @fixme this should be a little more generalized :)
+ if (PHP_OS == 'Darwin') {
+ // Mac OS X's ps barfs on the 'w' param, but doesn't need it.
+ $ps = "ps -U %s";
+ } else {
+ // Good on Linux
+ $ps = "ps -U %s w";
+ }
+ $psCommand = sprintf($ps, escapeshellarg($user));
+ exec($psCommand . " | grep -i selenium-server", $output);
+
+ // Start server. If there is already a server running,
+ // return running.
+
+ if ( isset( $this->SeleniumServerExecPath ) ) {
+ $found = 0;
+ foreach ( $output as $string ) {
+ $found += preg_match(
+ '~^(.*)java(.+)-jar(.+)selenium-server~',
+ $string );
+ }
+ if ( $found == 0 ) {
+
+ // Didn't find the selenium server. Start it up.
+ // First set up comamand line suffix.
+ // NB: $! is pid of last job run in background
+ // The echo guarentees it is put into $op when
+ // the exec command is run.
+
+ $commandSuffix = ' > /dev/null 2>&1'. ' & echo $!';
+ $portText = ' -port ' . $this->SeleniumServerPort;
+ $command = "java -jar " .
+ escapeshellarg($this->SeleniumServerExecPath) .
+ $portText . $commandSuffix;
+ exec($command ,$op);
+ $pid = (int)$op[0];
+ if ( $pid != "" )
+ $this->SeleniumServerPid = $pid;
+ else {
+ $this->SeleniumServerPid = 'NaN';
+ // Server start failed.
+ return 'failed';
+ }
+ // Wait for the server to startup and listen
+ // on its port. Note: this solution kinda
+ // stinks, since it uses a wait loop - dnessett
+
+ wfSuppressWarnings();
+ for ( $cnt = 1;
+ $cnt <= $this->SeleniumServerStartTimeout;
+ $cnt++ ) {
+ $fp = fsockopen ( 'localhost',
+ $this->SeleniumServerPort,
+ $errno, $errstr, 0 );
+ if ( !$fp ) {
+ sleep( 1 );
+ continue;
+ // Server start succeeded.
+ } else {
+ fclose ( $fp );
+ return 'started';
+ }
+ }
+ wfRestoreWarnings();
+ echo ( "Starting Selenium server timed out.\n" );
+ return 'failed';
+ }
+ // server already running.
+ else return 'running';
+
+ }
+ // No Server execution path defined.
+ return 'failed';
+ }
+
+ private function startServerOnWindows() {
+ // Unimplemented.
+ return 'failed';
+ }
+
+ private function stopServerOnUnix() {
+
+ if ( !empty( $this->SeleniumServerPid ) &&
+ $this->SeleniumServerPid != 'NaN' ) {
+ exec( "kill -9 " . $this->SeleniumServerPid );
+ return 'stopped';
+ }
+ else return 'failed';
+ }
+
+ private function stopServerOnWindows() {
+ // Unimplemented.
+ return 'failed';
+
+ }
+}
--- /dev/null
+<?php
+class SeleniumTestCase extends PHPUnit_Framework_TestCase { // PHPUnit_Extensions_SeleniumTestCase
+ protected $selenium;
+
+ public function setUp() {
+ set_time_limit( 60 );
+ $this->selenium = Selenium::getInstance();
+ }
+
+ public function tearDown() {
+
+ }
+
+ public function __call( $method, $args ) {
+ return call_user_func_array( array( $this->selenium, $method ), $args );
+ }
+
+ public function assertSeleniumAttributeEquals( $attribute, $value ) {
+ $attr = $this->getAttribute( $attribute );
+ $this->assertEquals( $attr, $value );
+ }
+
+ public function assertSeleniumHTMLContains( $element, $text ) {
+ $innerHTML = $this->getText( $element );
+ // or assertContains
+ $this->assertRegExp( "/$text/", $innerHTML );
+ }
+
+//Common Funtions Added for Selenium Tests
+
+ public function getExistingPage(){
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->type("searchInput", "new" );
+ $this->click("searchGoButton");
+ $this->waitForPageToLoad("30000");
+ }
+
+ public function getNewPage($pageName){
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->type("searchInput", $pageName );
+ $this->click("searchGoButton");
+ $this->waitForPageToLoad("30000");
+ $this->click("link=".$pageName);
+ $this->waitForPageToLoad("600000");
+
+
+ }
+ // Loading the mediawiki editor
+ public function loadWikiEditor(){
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ }
+
+ // Clear the content of the mediawiki editor
+ public function clearWikiEditor(){
+ $this->type("wpTextbox1", "");
+ }
+
+ // Click on the 'Show preview' button of the mediawiki editor
+ public function clickShowPreviewBtn(){
+ $this->click("wpPreview");
+ }
+
+ // Click on the 'Save Page' button of the mediawiki editor
+ public function clickSavePageBtn(){
+ $this->click("wpSave");
+ }
+
+ // Click on the 'Edit' link
+ public function clickEditLink(){
+ $this->click("link=Edit");
+ $this->waitForPageToLoad("30000");
+ }
+
+ public function deletePage($pageName){
+ $isLinkPresent = False;
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->click("link=Log out");
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "link=Log in / create account" );
+ $this->waitForPageToLoad( "30000" );
+ $this->type( "wpName1", "nadeesha" );
+ $this->type( "wpPassword1", "12345" );
+ $this->click( "wpLoginAttempt" );
+ $this->waitForPageToLoad( "30000" );
+ $this->type( "searchInput", $pageName );
+ $this->click( "searchGoButton");
+ $this->waitForPageToLoad( "30000" );
+
+ $this->click( "link=Delete" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "wpConfirmB" );
+ $this->waitForPageToLoad( "30000" );
+
+ }
+
+
+
+}
--- /dev/null
+<?php
+
+class SeleniumTestConsoleLogger {
+ public function __construct() {
+ // Prepare testsuite for immediate output
+ @ini_set( 'zlib.output_compression', 0 );
+ @ini_set( 'implicit_flush', 1 );
+ for ( $i = 0; $i < ob_get_level(); $i++ ) {
+ ob_end_flush();
+ }
+ ob_implicit_flush( 1 );
+ }
+
+ public function write( $message, $mode = false ) {
+ $out = '';
+ // if ( $mode == SeleniumTestSuite::RESULT_OK ) $out .= '<font color="green">';
+ $out .= htmlentities( $message );
+ // if ( $mode == SeleniumTestSuite::RESULT_OK ) $out .= '</font>';
+ if ( $mode != SeleniumTestSuite::CONTINUE_LINE ) {
+ $out .= "\n";
+ }
+
+ echo $out;
+ }
+}
--- /dev/null
+<?php
+
+class SeleniumTestHTMLLogger {
+ public function setHeaders() {
+ global $wgOut;
+ $wgOut->addHeadItem( 'selenium', '<style type="text/css">
+ .selenium pre {
+ overflow-x: auto; /* Use horizontal scroller if needed; for Firefox 2, not needed in Firefox 3 */
+ white-space: pre-wrap; /* css-3 */
+ white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ /* width: 99%; */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+ }
+ .selenium-success { color: green }
+ </style>' );
+ }
+
+ public function write( $message, $mode = false ) {
+ global $wgOut;
+ $out = '';
+ if ( $mode == SeleniumTestSuite::RESULT_OK ) {
+ $out .= '<span class="selenium-success">';
+ }
+ $out .= htmlspecialchars( $message );
+ if ( $mode == SeleniumTestSuite::RESULT_OK ) {
+ $out .= '</span>';
+ }
+ if ( $mode != SeleniumTestSuite::CONTINUE_LINE ) {
+ $out .= '<br />';
+ }
+
+ $wgOut->addHTML( $out );
+ }
+}
--- /dev/null
+<?php
+
+class SeleniumTestListener implements PHPUnit_Framework_TestListener {
+ private $logger;
+ private $tests_ok = 0;
+ private $tests_failed = 0;
+
+ public function __construct( $loggerInstance ) {
+ $this->logger = $loggerInstance;
+ }
+
+ public function addError( PHPUnit_Framework_Test $test, Exception $e, $time ) {
+ $this->logger->write( 'Error: ' . $e->getMessage() );
+ $this->tests_failed++;
+ }
+
+ public function addFailure( PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time )
+ {
+ $this->logger->write( 'Failed: ' . $e->getMessage() );
+ $this->tests_failed++;
+ }
+
+ public function addIncompleteTest( PHPUnit_Framework_Test $test, Exception $e, $time )
+ {
+ $this->logger->write( 'Incomplete.' );
+ $this->tests_failed++;
+ }
+
+ public function addSkippedTest( PHPUnit_Framework_Test $test, Exception $e, $time )
+ {
+ $this->logger->write( 'Skipped.' );
+ $this->tests_failed++;
+ }
+
+ public function startTest( PHPUnit_Framework_Test $test ) {
+ $this->logger->write(
+ 'Testing ' . $test->getName() . ' ... ',
+ SeleniumTestSuite::CONTINUE_LINE
+ );
+ }
+
+ public function endTest( PHPUnit_Framework_Test $test, $time ) {
+ if ( !$test->hasFailed() ) {
+ $this->logger->write( 'OK', SeleniumTestSuite::RESULT_OK );
+ $this->tests_ok++;
+ }
+ }
+
+ public function startTestSuite( PHPUnit_Framework_TestSuite $suite ) {
+ $this->logger->write( 'Testsuite ' . $suite->getName() . ' started.' );
+ $this->tests_ok = 0;
+ $this->tests_failed = 0;
+ }
+
+ public function endTestSuite( PHPUnit_Framework_TestSuite $suite ) {
+ $this->logger->write('Testsuite ' . $suite->getName() . ' ended.' );
+ if ( $this->tests_ok > 0 || $this->tests_failed > 0 ) {
+ $this->logger->write( ' OK: ' . $this->tests_ok . ' Failed: ' . $this->tests_failed );
+ }
+ $this->tests_ok = 0;
+ $this->tests_failed = 0;
+ }
+
+ public function statusMessage( $message ) {
+ $this->logger->write( $message );
+ }
+}
+
--- /dev/null
+<?php
+
+abstract class SeleniumTestSuite extends PHPUnit_Framework_TestSuite {
+ private $selenium;
+ private $isSetUp = false;
+ private $loginBeforeTests = true;
+
+ // Do not add line break after test output
+ const CONTINUE_LINE = 1;
+ const RESULT_OK = 2;
+ const RESULT_ERROR = 3;
+
+ public abstract function addTests();
+
+ public function setUp() {
+ // Hack because because PHPUnit version 3.0.6 which is on prototype does not
+ // run setUp as part of TestSuite::run
+ if ( $this->isSetUp ) {
+ return;
+ }
+ $this->isSetUp = true;
+ $this->selenium = Selenium::getInstance();
+ $this->selenium->start();
+ $this->selenium->open( $this->selenium->getUrl() . '/index.php?setupTestSuite=' . $this->getName() );
+ if ( $this->loginBeforeTests ) {
+ $this->login();
+ }
+ }
+
+ public function tearDown() {
+ $this->selenium->open( $this->selenium->getUrl() . '/index.php?clearTestSuite=' . $this->getName() );
+ $this->selenium->stop();
+ }
+
+ public function login() {
+ $this->selenium->login();
+ }
+
+ public function loadPage( $title, $action ) {
+ $this->selenium->loadPage( $title, $action );
+ }
+
+ protected function setLoginBeforeTests( $loginBeforeTests = true ) {
+ $this->loginBeforeTests = $loginBeforeTests;
+ }
+}
--- /dev/null
+[browsers]
+
+firefox = "*firefox"
+iexploreproxy = "*iexploreproxy"
+chrome = "*chrome"
+
+[SeleniumSettings]
+
+host = "localhost"
+port = "4444"
+wikiUrl = "http://localhost/mediawiki/latest_trunk/trunk/phase3"
+username = "Wikiadmin"
+userPassword = "Wikiadminpw"
+testBrowser = "firefox"
+startserver =
+stopserver =
+jUnitLogFile =
+runAgainstGrid = false
+
+[testSuite]
+
+SimpleSeleniumTestSuite = "maintenance/tests/selenium/suites/SimpleSeleniumTestSuite.php"
+WikiEditorTestSuite = "extensions/WikiEditor/selenium/WikiEditorTestSuite.php"
--- /dev/null
+[SeleniumSettings]
+
+; Set up the available browsers that Selenium can control.
+browsers[firefox] = "*firefox"
+browsers[iexplorer] = "*iexploreproxy"
+browsers[chrome] = "*chrome"
+
+; The simple configurations above usually work on Linux, but Windows and
+; Mac OS X hosts may need to specify a full path:
+;browsers[firefox] = "*firefox /Applications/Firefox.app/Contents/MacOS/firefox-bin"
+;browsers[firefox] = "*firefox C:\Program Files\Mozilla Firefox\firefox.exe"
+
+host = "localhost"
+port = "4444"
+wikiUrl = "http://localhost/deployment"
+username = "wikiuser"
+userPassword = "wikipass"
+testBrowser = "firefox"
+startserver =
+stopserver =
+jUnitLogFile =
+runAgainstGrid = false
+
+; To let the test runner start and stop the selenium server, it needs the full
+; path to selenium-server.jar from the selenium-remote-control package.
+seleniumserverexecpath = "/opt/local/selenium-remote-control-1.0.3/selenium-server-1.0.3/selenium-server.jar"
+
+[SeleniumTests]
+
+testSuite[SimpleSeleniumTestSuite] = "maintenance/tests/selenium/suites/SimpleSeleniumTestSuite.php"
+testSuite[WikiEditorTestSuite] = "extensions/WikiEditor/selenium/WikiEditorTestSuite.php"
+
--- /dev/null
+[SeleniumSettings]
+
+host = "grid.tesla.usability.wikimedia.org"
+port = "4444"
+wikiUrl = "http://208.80.152.253:5001"
+username = "wikiuser"
+userPassword = "wikipass"
+testBrowser = "Safari on OS X Snow Leopard"
+jUnitLogFile =
+runAgainstGrid = true
+
+[testSuite]
+
+SimpleSeleniumTestSuite = "maintenance/tests/selenium/suites/SimpleSeleniumTestSuite.php"
\ No newline at end of file
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+
+class AddContentToNewPageTestCase extends SeleniumTestCase {
+
+
+ // Add bold text and verify output
+ public function testAddBoldText() {
+
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "//*[@id='mw-editbutton-bold']" );
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify bold text displayed on mediawiki preview
+ $this->assertTrue($this->isElementPresent( "//div[@id='wikiPreview']/p/b" ));
+ $this->assertTrue($this->isTextPresent( "Bold text" ));
+ }
+
+ // Add italic text and verify output
+ public function testAddItalicText() {
+
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "//*[@id='mw-editbutton-italic']" );
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify italic text displayed on mediawiki preview
+ $this->assertTrue($this->isElementPresent("//div[@id='wikiPreview']/p/i"));
+ $this->assertTrue($this->isTextPresent( "Italic text" ));
+ }
+
+ // Add internal link for a new page and verify output in the preview
+ public function testAddInternalLinkNewPage() {
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "//*[@id='mw-editbutton-link']" );
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify internal link displayed on mediawiki preview
+ $source = $this->getText( "//*[@id='wikiPreview']/p/a" );
+ $correct = strstr( $source, "Link title" );
+ $this->assertEquals( $correct, true );
+
+ $this->click( "link=Link title" );
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify internal link open as a new page - editing mode
+ $source = $this->getText( "firstHeading" );
+ $correct = strstr( $source, "Editing Link title" );
+ $this->assertEquals( $correct, true );
+ }
+
+ // Add external link and verify output in the preview
+ public function testAddExternalLink() {
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "//*[@id='mw-editbutton-extlink']" );
+ $this->type( "wpTextbox1", "[http://www.google.com Google]" );
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify external links displayed on mediawiki preview
+ $source = $this->getText( "//*[@id='wikiPreview']/p/a" );
+ $correct = strstr( $source, "Google" );
+ $this->assertEquals( $correct, true );
+
+ $this->click( "link=Google" );
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify external link opens
+ $source = $this->getTitle();
+ $correct = strstr( $source, "Google" );
+ $this->assertEquals( $correct, true);
+ }
+
+ // Add level 2 headline and verify output in the preview
+ public function testAddLevel2HeadLine() {
+ $blnElementPresent = False;
+ $blnTextPresent = False;
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "mw-editbutton-headline" );
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+ $this->assertTrue($this->isElementPresent( "//div[@id='wikiPreview']/h2" ));
+
+ // Verify level 2 headline displayed on mediawiki preview
+ $source = $this->getText( "//*[@id='Headline_text']" );
+ $correct = strstr( $source, "Headline text" );
+ $this->assertEquals( $correct, true );
+ }
+
+ // Add text with ignore wiki format and verify output the preview
+ public function testAddNoWikiFormat() {
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "//*[@id='mw-editbutton-nowiki']" );
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify ignore wiki format text displayed on mediawiki preview
+ $source = $this->getText( "//div[@id='wikiPreview']/p" );
+ $correct = strstr( $source, "Insert non-formatted text here" );
+ $this->assertEquals( $correct, true );
+ }
+
+ // Add signature and verify output in the preview
+ public function testAddUserSignature() {
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "mw-editbutton-signature" );
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify signature displayed on mediawiki preview
+ $source = $this->getText( "//*[@id='wikiPreview']/p/a" );
+ $username = $this->getText( "//*[@id='pt-userpage']/a" );
+ $correct = strstr( $source, $username );
+ $this->assertEquals( $correct, true );
+ }
+
+ // Add horizontal line and verify output in the preview
+ public function testHorizontalLine() {
+ $this->getExistingPage();
+ $this->clickEditLink();
+ $this->loadWikiEditor();
+ $this->clearWikiEditor();
+ $this->click( "mw-editbutton-hr" );
+
+ $this->clickShowPreviewBtn();
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify horizontal line displayed on mediawiki preview
+ $this->assertTrue( $this->isElementPresent( "//div[@id='wikiPreview']/hr" ));
+ $this->deletePage( "new" );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+
+class AddNewPageTestCase extends SeleniumTestCase {
+
+ // Verify adding a new page
+ public function testAddNewPage() {
+ $newPage = "new";
+ $displayName = "New";
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->type( "searchInput", $newPage );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify 'Search results' text available
+ $source = $this->gettext( "firstHeading" );
+ $correct = strstr( $source, "Search results" );
+ $this->assertEquals( $correct, true);
+
+ // Verify 'Create the page "<page name>" on this wiki' text available
+ $source = $this->gettext( "//div[@id='bodyContent']/div[4]/p/b" );
+ $correct = strstr ( $source, "Create the page \"New\" on this wiki!" );
+ $this->assertEquals( $correct, true );
+
+ $this->click( "link=".$displayName );
+ $this->waitForPageToLoad( "600000" );
+
+ $this->assertTrue($this->isElementPresent( "link=Create" ));
+ $this->type( "wpTextbox1", "add new test page" );
+ $this->click( "wpSave" );
+
+ // Verify new page added
+ $source = $this->gettext( "firstHeading" );
+ $correct = strstr ( $source, $displayName );
+ $this->assertEquals( $correct, true );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+Class CreateAccountTestCase extends SeleniumTestCase {
+
+ // Change these values before run the test
+ private $userName = "yourname4000";
+ private $password = "yourpass4000";
+
+ // Verify 'Log in/create account' link existance in Main page.
+ public function testMainPageLink() {
+
+ $this->click( "link=Log out" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->open( $this->getUrl().'/index.php?title=Main_Page' );
+ $this->assertTrue($this->isElementPresent( "link=Log in / create account" ));
+ }
+
+ // Verify 'Create an account' link existance in 'Log in / create account' Page.
+ public function testCreateAccountPageLink() {
+
+ $this->click( "link=Log out" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->open( $this->getUrl().'/index.php?title=Main_Page' );
+
+ // click Log in / create account link to open Log in / create account' page
+ $this->click( "link=Log in / create account" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertTrue($this->isElementPresent( "link=Create an account" ));
+ }
+
+ // Verify Create account
+ public function testCreateAccount() {
+
+ $this->click( "link=Log out" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->open( $this->getUrl().'/index.php?title=Main_Page' );
+
+ $this->click( "link=Log in / create account" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->click( "link=Create an account" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify for blank user name
+ $this->type( "wpName2", "" );
+ $this->click( "wpCreateaccount" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "Login error\n You have not specified a valid user name.",
+ $this->getText( "//div[@id='bodyContent']/div[4]" ));
+
+ // Verify for invalid user name
+ $this->type( "wpName2", "@" );
+ $this->click("wpCreateaccount" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "Login error\n You have not specified a valid user name.",
+ $this->getText( "//div[@id='bodyContent']/div[4]" ));
+
+ // start of test for blank password
+ $this->type( "wpName2", $this->userName);
+ $this->type( "wpPassword2", "" );
+ $this->click( "wpCreateaccount" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "Login error\n Passwords must be at least 1 character.",
+ $this->getText("//div[@id='bodyContent']/div[4]" ));
+
+ $this->type( "wpName2", $this->userName );
+ $this->type( "wpPassword2", $this->password );
+ $this->click( "wpCreateaccount" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "Login error\n The passwords you entered do not match.",
+ $this->getText( "//div[@id='bodyContent']/div[4]" ));
+
+ $this->type( "wpName2", $this->userName );
+ $this->type( "wpPassword2", $this->password );
+ $this->type( "wpRetype", $this->password );
+ $this->click( "wpCreateaccount" );
+ $this->waitForPageToLoad( "30000 ");
+
+ // Verify successful account creation for valid combination of 'Username', 'Password', 'Retype password'
+ $this->assertEquals( "Welcome, ".ucfirst( $this->userName )."!",
+ $this->getText( "Welcome,_".ucfirst( $this->userName )."!" ));
+ }
+}
+
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+
+class DeletePageAdminTestCase extends SeleniumTestCase {
+
+ // Verify adding a new page
+ public function testDeletePage() {
+
+
+ $newPage = "new";
+ $displayName = "New";
+
+ $this->open( $this->getUrl().'/index.php?title=Main_Page' );
+
+ $this->type( "searchInput", $newPage );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "link=".$displayName );
+ $this->waitForPageToLoad( "60000" );
+ $this->type( "wpTextbox1", $newPage." text" );
+ $this->click( "wpSave" );
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->click( "link=Log out" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "link=Log in / create account" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->type( "wpName1", $this->selenium->getUser() );
+ $this->type( "wpPassword1", $this->selenium->getPass() );
+ $this->click( "wpLoginAttempt" );
+ $this->waitForPageToLoad( "30000" );
+ $this->type( "searchInput", "new" );
+ $this->click( "searchGoButton");
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify 'Delete' link displayed
+ $source = $this->gettext( "link=Delete" );
+ $correct = strstr ( $source, "Delete" );
+ $this->assertEquals($correct, true );
+
+ $this->click( "link=Delete" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify 'Delete' button available
+ $this->assertTrue($this->isElementPresent( "wpConfirmB" ));
+
+ $this->click( "wpConfirmB" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify 'Action complete' text displayed
+ $source = $this->gettext( "firstHeading" );
+ $correct = strstr ( $source, "Action complete" );
+ $this->assertEquals( $correct, true );
+
+ // Verify '<Page Name> has been deleted. See deletion log for a record of recent deletions.' text displayed
+ $source = $this->gettext( "//div[@id='bodyContent']/p[1]" );
+ $correct = strstr ( $source, "\"New\" has been deleted. See deletion log for a record of recent deletions." );
+ $this->assertEquals( $correct, true );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class EmailPasswordTestCase extends SeleniumTestCase {
+
+ // change user name for each and every test (with in 24 hours)
+ private $userName = "test1";
+
+ public function testEmailPasswordButton() {
+
+ $this->click( "link=Log out" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->open( $this->getUrl().'/index.php?title=Main_Page' );
+
+ // click Log in / create account link to open Log in / create account' page
+ $this->click( "link=Log in / create account" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertTrue($this->isElementPresent( "wpMailmypassword" ));
+ }
+
+ // Verify Email password functionality
+ public function testEmailPasswordMessages() {
+
+ $this->click( "link=Log out" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->open( $this->getUrl().'/index.php?title=Main_Page' );
+
+ // click Log in / create account link to open Log in / create account' page
+ $this->click( "link=Log in / create account" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->type( "wpName1", "" );
+ $this->click( "wpMailmypassword" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "Login error\n You have not specified a valid user name.",
+ $this->getText("//div[@id='bodyContent']/div[4]"));
+
+ $this->type( "wpName1", $this->userName );
+ $this->click( "wpMailmypassword" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Can not run on localhost
+ $this->assertEquals( "A new password has been sent to the e-mail address registered for ".ucfirst($this->userName).". Please log in again after you receive it.",
+ $this->getText("//div[@id='bodyContent']/div[4]" ));
+
+ $this->type( "wpName1", $this->userName );
+ $this->click( "wpMailmypassword" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "Login error\n A password reminder has already been sent, within the last 24 hours. To prevent abuse, only one password reminder will be sent per 24 hours.",
+ $this->getText( "//div[@id='bodyContent']/div[4]" ));
+ }
+}
+
--- /dev/null
+<?php
+
+class MediaWikExtraTestSuite extends SeleniumTestSuite {
+ public function setUp() {
+ $this->setLoginBeforeTests( true );
+ parent::setUp();
+ }
+ public function addTests() {
+ $testFiles = array(
+ 'maintenance/tests/selenium/suites/MyContributionsTestCase.php',
+ 'maintenance/tests/selenium/suites/MyWatchListTestCase.php',
+ 'maintenance/tests/selenium/suites/UserPreferencesTestCase.php',
+ 'maintenance/tests/selenium/suites/MovePageTestCase.php',
+ 'maintenance/tests/selenium/suites/PageSearchTestCase.php',
+ 'maintenance/tests/selenium/suites/EmailPasswordTestCase.php',
+ 'maintenance/tests/selenium/suites/CreateAccountTestCase.php'
+ );
+ parent::addTestFiles( $testFiles );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class MediaWikiEditorConfig {
+
+ public static function getSettings(&$includeFiles, &$globalConfigs) {
+ $includes = array(
+ //files that needed to be included would go here
+ 'maintenance/tests/selenium/suites/MediaWikiCommonFunction.php'
+ );
+ $configs = array(
+ 'wgPageLoadTime' => "600000"
+ );
+ $includeFiles = array_merge( $includeFiles, $includes );
+ $globalConfigs = array_merge( $globalConfigs, $configs);
+ return true;
+ }
+}
+
+
+
--- /dev/null
+<?php
+
+class MediaWikiEditorTestSuite extends SeleniumTestSuite {
+ public function setUp() {
+ $this->setLoginBeforeTests( true );
+ parent::setUp();
+ }
+ public function addTests() {
+ $testFiles = array(
+ 'maintenance/tests/selenium/suites/AddNewPageTestCase.php',
+ 'maintenance/tests/selenium/suites/AddContentToNewPageTestCase.php',
+ 'maintenance/tests/selenium/suites/PreviewPageTestCase.php',
+ 'maintenance/tests/selenium/suites/SavePageTestCase.php',
+ );
+ parent::addTestFiles( $testFiles );
+ }
+}
+
--- /dev/null
+<?php
+/*
+ * Stub of tests be need as part of the hack-a-ton
+ */
+class MediawikiCoreSmokeTestCase extends SeleniumTestCase {
+ public function testUserLogin() {
+
+ }
+
+ public function testChangeUserPreference() {
+
+ }
+
+ /*
+ * TODO: generalize this test to be reusable for different skins
+ */
+ public function testCreateNewPageVector() {
+
+ }
+
+ /*
+ * TODO: generalize this test to be reusable for different skins
+ */
+ public function testEditExistingPageVector() {
+
+ }
+
+ /*
+ * TODO: generalize this test to be reusable for different skins
+ */
+ public function testCreateNewPageMonobook() {
+
+ }
+
+ /*
+ * TODO: generalize this test to be reusable for different skins
+ */
+ public function testEditExistingPageMonobook() {
+
+ }
+
+ public function testImageUpload() {
+ $this->login();
+ $this->open( $this->getUrl() .
+ '/index.php?title=Special:Upload' );
+ $this->type( 'wpUploadFile', dirname( __FILE__ ) .
+ "\\..\\data\\Wikipedia-logo-v2-de.png" );
+ $this->check( 'wpIgnoreWarning' );
+ $this->click( 'wpUpload' );
+ $this->waitForPageToLoad( 30000 );
+
+ $this->assertSeleniumHTMLContains(
+ '//h1[@class="firstHeading"]', "Wikipedia-logo-v2-de.png" );
+
+ /*
+ $this->open( $this->getUrl() . '/index.php?title=Image:'
+ . ucfirst( $this->filename ) . '&action=delete' );
+ $this->type( 'wpReason', 'Remove test file' );
+ $this->click( 'mw-filedelete-submit' );
+ $this->waitForPageToLoad( 10000 );
+
+ // Todo: This message is localized
+ $this->assertSeleniumHTMLContains( '//div[@id="bodyContent"]/p',
+ ucfirst( $this->filename ) . '.*has been deleted.' );
+ */
+ }
+
+
+}
--- /dev/null
+<?php
+/*
+ * Stubs for now. We're going to start populating this test.
+ */
+class MediawikiCoreSmokeTestSuite extends SeleniumTestSuite
+{
+ public function setUp() {
+ $this->setLoginBeforeTests( false );
+ parent::setUp();
+ }
+ public function addTests() {
+ $testFiles = array(
+ 'maintenance/tests/selenium/suites/MediawikiCoreSmokeTestCase.php'
+ );
+ parent::addTestFiles( $testFiles );
+ }
+
+
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class MovePageTestCase extends SeleniumTestCase {
+
+ // Verify move(rename) wiki page
+ public function testMovePage() {
+
+ $newPage = "mypage99";
+ $displayName = "Mypage99";
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->type( "searchInput", $newPage );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "link=".$displayName );
+ $this->waitForPageToLoad( "60000" );
+ $this->type( "wpTextbox1", $newPage." text" );
+ $this->click( "wpSave" );
+ $this->waitForPageToLoad( "60000" );
+
+ // Verify link 'Move' available
+ $this->assertTrue($this->isElementPresent( "link=Move" ));
+
+ $this->click( "link=Move" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify correct page name displayed under 'Move Page' field
+ $this->assertEquals($displayName,
+ $this->getText("//table[@id='mw-movepage-table']/tbody/tr[1]/td[2]/strong/a"));
+ $movePageName = $this->getText( "//table[@id='mw-movepage-table']/tbody/tr[1]/td[2]/strong/a" );
+
+ // Verify 'To new title' field has current page name as the default name
+ $newTitle = $this->getValue( "wpNewTitle" );
+ $correct = strstr( $movePageName , $newTitle );
+ $this->assertEquals( $correct, true );
+
+ $this->type( "wpNewTitle", $displayName );
+ $this->click( "wpMove" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify warning message for the same source and destination titles
+ $this->assertEquals( "Source and destination titles are the same; cannot move a page over itself.",
+ $this->getText("//div[@id='bodyContent']/p[4]/strong" ));
+
+ // Verify warning message for the blank title
+ $this->type( "wpNewTitle", "" );
+ $this->click( "wpMove" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify warning message for the blank title
+ $this->assertEquals( "The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title. It may contain one or more characters which cannot be used in titles.",
+ $this->getText( "//div[@id='bodyContent']/p[4]/strong" ));
+
+ // Verify warning messages for the invalid titles
+ $this->type( "wpNewTitle", "# < > [ ] | { }" );
+ $this->click( "wpMove" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title. It may contain one or more characters which cannot be used in titles.",
+ $this->getText( "//div[@id='bodyContent']/p[4]/strong" ));
+
+ $this->type( "wpNewTitle", $displayName."move" );
+ $this->click( "wpMove" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify move success message displayed correctly
+ $this->assertEquals( "\"".$displayName."\" has been moved to \"".$displayName."move"."\"",
+ $this->getText( "//div[@id='bodyContent']/p[1]/b" ));
+
+ $this->type( "searchInput", $newPage."move" );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify search using new page name
+ $this->assertEquals( $displayName."move", $this->getText( "firstHeading" ));
+
+ $this->type( "searchInput", $newPage );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify search using old page name
+ $redirectPageName = $this->getText( "//*[@id='contentSub']" );
+ $this->assertEquals( "(Redirected from ".$displayName.")" , $redirectPageName );
+
+ // newpage delete
+ $this->deletePage( $newPage."move" );
+ $this->deletePage( $newPage );
+ }
+}
+
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class MyContributionsTestCase extends SeleniumTestCase {
+
+ // Verify user contributions
+ public function testRecentChangesAvailability() {
+
+ $newPage = "mypage999";
+ $displayName = "Mypage999";
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+
+ $this->type( "searchInput", $newPage);
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "60000" );
+ $this->click( "link=".$displayName );
+ $this->waitForPageToLoad( "600000" );
+ $this->type( "wpTextbox1", $newPage." text" );
+ $this->click( "wpSave" );
+ $this->waitForPageToLoad( "60000" );
+
+ // Verify My contributions Link available
+ $this->assertTrue($this->isElementPresent( "link=My contributions" ));
+
+ $this->click( "link=My contributions" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify recent page adding available on My Contributions list
+ $this->assertEquals( $displayName, $this->getText( "link=".$displayName ));
+
+ $this->type( "searchInput", $newPage );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->click( "link=Edit" );
+ $this->waitForPageToLoad( "30000" );
+ $this->type( "wpTextbox1", $newPage." text changed" );
+ $this->click( "wpSave" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "link=My contributions" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify recent page changes available on My Contributions
+ $this->assertTrue($this->isTextPresent($displayName." (top)"));
+ $this->deletePage($newPage);
+ }
+}
+
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+
+class MyWatchListTestCase extends SeleniumTestCase {
+
+ // Verify user watchlist
+ public function testMyWatchlist() {
+
+ $newPage = "mypage";
+ $displayName = "Mypage";
+ $wikiText = "watch page";
+
+ $this->open( $this->getUrl().'/index.php?title=Main_Page' );
+
+ $this->type( "searchInput", $newPage );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "link=".$displayName );
+ $this->waitForPageToLoad( "600000" );
+
+ $this->click( "wpWatchthis" );
+ $this->type( "wpTextbox1",$wikiText );
+ $this->click( "wpSave" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify link 'My Watchlist' available
+ $this->assertTrue( $this->isElementPresent( "link=My watchlist" ) );
+
+ $this->click( "link=My watchlist" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify newly added page to the watchlist is available
+ $watchList = $this->getText( "//*[@id='bodyContent']" );
+ $this->assertContains( $displayName, $watchList );
+
+ $this->type( "searchInput", $newPage );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click("link=Edit");
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "wpWatchthis" );
+ $this->click( "wpSave" );
+ $this->deletePage( $newPage );
+ }
+}
+
--- /dev/null
+<?php
+
+class PageDeleteTestSuite extends SeleniumTestSuite {
+ public function setUp() {
+ $this->setLoginBeforeTests( true );
+ parent::setUp();
+ }
+ public function addTests() {
+ $testFiles = array(
+ 'maintenance/tests/selenium/suites/DeletePageAdminTestCase.php'
+ );
+ parent::addTestFiles( $testFiles );
+ }
+
+
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class PageSearchTestCase extends SeleniumTestCase {
+
+ // Verify the functionality of the 'Go' button
+ public function testPageSearchBtnGo() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->type( "searchInput", "calcey qa" );
+ $this->click( "searchGoButton" );
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify no page matched with the entered search text
+ $source = $this->gettext( "//div[@id='bodyContent']/div[4]/p/b" );
+ $correct = strstr ( $source, "Create the page \"Calcey qa\" on this wiki!" );
+ $this->assertEquals( $correct, true );
+
+ $this->click( "link=Calcey qa" );
+ $this->waitForPageToLoad( "600000" );
+
+ $this->type( "wpTextbox1", "Calcey QA team" );
+ $this->click( "wpSave" );
+ $this->waitForPageToLoad( "600000" );
+
+ }
+
+ // Verify the functionality of the 'Search' button
+ public function testPageSearchBtnSearch() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->type( "searchInput", "Calcey web" );
+ $this->click( "mw-searchButton" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify no page is available as the search text
+ $source = $this->gettext( "//div[@id='bodyContent']/div[4]/p[2]/b" );
+ $correct = strstr ( $source, "Create the page \"Calcey web\" on this wiki!" );
+ $this->assertEquals( $correct, true );
+
+ $this->click( "link=Calcey web" );
+ $this->waitForPageToLoad( "600000" );
+
+ $this->type( "wpTextbox1", "Calcey web team" );
+ $this->click( "wpSave" );
+ $this->waitForPageToLoad( "600000" );
+
+ // Verify saved page is opened when the exact page name is given
+ $this->type( "searchInput", "Calcey web" );
+ $this->click( "mw-searchButton" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify exact page matched with the entered search text using 'Search' button
+ $source = $this->getText( "//*[@id='bodyContent']/div[4]/p/b" );
+ $correct = strstr( $source, "There is a page named \"Calcey web\" on this wiki." );
+ $this->assertEquals( $correct, true );
+
+ // Verify resutls available when partial page name is entered as the search text
+ $this->type( "searchInput", "Calcey" );
+ $this->click( "mw-searchButton" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify text avaialble in the search result under the page titles
+ if($this->isElementPresent( "Page_title_matches" )) {
+ $textPageTitle = $this->getText( "//*[@id='bodyContent']/div[4]/ul[1]/li[1]/div[1]/a" );
+ $this->assertContains( 'Calcey', $textPageTitle );
+ }
+
+ // Verify text avaialble in the search result under the page text
+ if($this->isElementPresent( "Page_text_matches" )) {
+ $textPageText = $this->getText( "//*[@id='bodyContent']/div[4]/ul[2]/li[2]/div[2]/span" );
+ $this->assertContains( 'Calcey', $textPageText );
+ }
+ $this->deletePage("Calcey QA");
+ $this->deletePage("Calcey web");
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class PreviewPageTestCase extends SeleniumTestCase {
+
+ // Verify adding a new page
+ public function testPreviewPage() {
+ $wikiText = "Adding this page to test the \n Preview button functionality";
+ $newPage = "Test Preview Page";
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->getNewPage( $newPage );
+ $this->type( "wpTextbox1", $wikiText."" );
+ $this->assertTrue($this->isElementPresent( "//*[@id='wpPreview']" ));
+
+ $this->click( "wpPreview" );
+
+ // Verify saved page available
+ $source = $this->gettext( "firstHeading" );
+ $correct = strstr( $source, "Test Preview Page" );
+ $this->assertEquals( $correct, true);
+
+ // Verify page content previewed succesfully
+ $contentOfPreviewPage = $this->getText( "//*[@id='content']" );
+ $this->assertContains( $wikiText, $contentOfPreviewPage );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class SavePageTestCase extends SeleniumTestCase {
+
+ // Verify adding a new page
+ public function testSavePage() {
+ $wikiText = "Adding this page to test the Save button functionality";
+ $newPage = "Test Save Page";
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->getNewPage($newPage);
+ $this->type("wpTextbox1", $wikiText);
+
+ // verify 'Save' button available
+ $this->assertTrue($this->isElementPresent( "wpSave" ));
+ $this->click( "wpSave" );
+
+ // Verify saved page available
+ $source = $this->gettext( "firstHeading" );
+ $correct = strstr( $source, "Test Save Page" );
+
+ // Verify Saved page name displayed correctly
+ $this->assertEquals( $correct, true );
+
+ // Verify page content saved succesfully
+ $contentOfSavedPage = $this->getText( "//*[@id='content']" );
+ $this->assertContains( $wikiText, $contentOfSavedPage );
+ $this->deletePage( $newPage );
+ }
+}
--- /dev/null
+<?php
+class SimpleSeleniumConfig {
+
+ public static function getSettings(&$includeFiles, &$globalConfigs) {
+ $includes = array(
+ //files that needed to be included would go here
+ );
+ $configs = array(
+ 'wgDefaultSkin' => 'chick'
+ );
+ $includeFiles = array_merge( $includeFiles, $includes );
+ $globalConfigs = array_merge( $globalConfigs, $configs);
+ return true;
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+/*
+ * This test case is part of the SimpleSeleniumTestSuite.
+ * Configuration for these tests are dosumented as part of SimpleSeleniumTestSuite.php
+ */
+class SimpleSeleniumTestCase extends SeleniumTestCase {
+ public function testBasic() {
+ $this->open( $this->getUrl() .
+ '/index.php?title=Selenium&action=edit' );
+ $this->type( "wpTextbox1", "This is a basic test" );
+ $this->click( "wpPreview" );
+ $this->waitForPageToLoad( 10000 );
+
+ // check result
+ $source = $this->getText( "//div[@id='wikiPreview']/p" );
+ $correct = strstr( $source, "This is a basic test" );
+ $this->assertEquals( $correct, true );
+ }
+
+ /*
+ * All this test really does is verify that a global var was set.
+ * It depends on $wgDefaultSkin = 'chick'; being set
+ */
+ public function testGlobalVariableForDefaultSkin() {
+ $this->open( $this->getUrl() . '/index.php?&action=purge' );
+ $bodyClass = $this->getAttribute( "//body/@class" );
+ $this-> assertContains('skin-chick', $bodyClass, 'Chick skin not set');
+ }
+
+}
--- /dev/null
+<?php
+/*
+ * Sample test suite.
+ * Two ways to configure MW for these tests
+ * 1) If you are running multiple test suites, add the following in LocalSettings.php
+ * require_once("maintenance/tests/selenium/SimpleSeleniumConfig.php");
+ * $wgSeleniumTestConfigs['SimpleSeleniumTestSuite'] = 'SimpleSeleniumConfig::getSettings';
+ * OR
+ * 2) Add the following to your Localsettings.php
+ * $wgDefaultSkin = 'chick';
+ */
+class SimpleSeleniumTestSuite extends SeleniumTestSuite
+{
+ public function setUp() {
+ $this->setLoginBeforeTests( false );
+ parent::setUp();
+ }
+ public function addTests() {
+ $testFiles = array(
+ 'maintenance/tests/selenium/suites/SimpleSeleniumTestCase.php'
+ );
+ parent::addTestFiles( $testFiles );
+ }
+
+
+}
--- /dev/null
+<?php
+
+/**
+ * Selenium server manager
+ *
+ * @file
+ * @ingroup Testing
+ * Copyright (C) 2010 Dan Nessett <dnessett@yahoo.com>
+ * http://citizendium.org/
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @addtogroup Testing
+ *
+ */
+
+class UserPreferencesTestCase extends SeleniumTestCase {
+
+ // Verify user information
+ public function testUserInfoDisplay() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->click( "link=My preferences" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify correct username displayed in User Preferences
+ $this->assertEquals( $this->getText( "//li[@id='pt-userpage']/a" ),
+ $this->getText( "//table[@id='mw-htmlform-info']/tbody/tr[1]/td[2]" ));
+
+ // Verify existing Signature Displayed correctly
+ $this->assertEquals( $this->selenium->getUser(),
+ $this->getTable( "mw-htmlform-signature.0.1" ) );
+ }
+
+ // Verify change password
+ public function testChangePassword() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->click( "link=My preferences" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->click( "link=Change password" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->type( "wpPassword", "12345" );
+ $this->type( "wpNewPassword", "54321" );
+ $this->type( "wpRetype", "54321" );
+ $this->click( "//input[@value='Change password']" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->assertEquals( "Preferences", $this->getText( "firstHeading" ));
+
+ $this->click( "link=Change password" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->type( "wpPassword", "54321" );
+ $this->type( "wpNewPassword", "12345" );
+ $this->type( "wpRetype", "12345" );
+ $this->click( "//input[@value='Change password']" );
+ $this->waitForPageToLoad( "30000" );
+ $this->assertEquals( "Preferences", $this->getText( "firstHeading" ));
+
+ $this->click( "link=Change password" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->type( "wpPassword", "54321" );
+ $this->type( "wpNewPassword", "12345" );
+ $this->type( "wpRetype", "12345" );
+ $this->click( "//input[@value='Change password']" );
+ $this->waitForPageToLoad( "30000" );
+ }
+
+ // Verify successful preferences save
+ public function testSuccessfullSave() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->click( "link=My preferences" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->type( "mw-input-realname", "Test User" );
+ $this->click( "prefcontrol" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify "Your preferences have been saved." message
+ $this->assertEquals( "Your preferences have been saved.",
+ $this->getText( "//div[@id='bodyContent']/div[4]/strong/p" ));
+ $this->type( "mw-input-realname", "" );
+ $this->click( "prefcontrol" );
+ $this->waitForPageToLoad( "30000" );
+
+ }
+
+ // Verify change signature
+ public function testChangeSignature() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->click( "link=My preferences" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->type( "mw-input-nickname", "TestSignature" );
+ $this->click( "prefcontrol" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify change user signature
+ $this->assertEquals( "TestSignature", $this->getText( "link=TestSignature" ));
+ $this->type( "mw-input-nickname", "Test" );
+ $this->click( "prefcontrol" );
+ $this->waitForPageToLoad("30000");
+ }
+
+ // Verify change date format
+ public function testChangeDateFormatTimeZone() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+
+ $this->click( "link=My preferences" );
+ $this->waitForPageToLoad( "30000" );
+ $this->click( "link=Date and time" );
+ $this->waitForPageToLoad( "30000" );
+
+ $this->click( "mw-input-date-dmy" );
+ $this->select( "mw-input-timecorrection", "label=Asia/Colombo" );
+ $this->click( "prefcontrol" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify Date format and time zome saved
+ $this->assertEquals( "Your preferences have been saved.",
+ $this->getText( "//div[@id='bodyContent']/div[4]/strong/p" ));
+ }
+
+ // Verify restoring all default settings
+ public function testSetAllDefault() {
+
+ $this->open( $this->getUrl() .
+ '/index.php?title=Main_Page&action=edit' );
+ $this->click( "link=My preferences" );
+ $this->waitForPageToLoad( "30000" );
+
+ // Verify restoring all default settings
+ $this->assertEquals( "Restore all default settings",
+ $this->getText( "link=Restore all default settings" ));
+
+ $this->click("//*[@id='preferences']/div/a");
+ $this->waitForPageToLoad("30000");
+
+ // Verify 'This can not be undone' warning message displayed
+ $this->assertTrue($this->isElementPresent("//input[@value='Restore all default settings']"));
+
+ // Verify 'Restore all default settings' button available
+ $this->assertEquals("You can use this page to reset your preferences to the site defaults. This cannot be undone.",
+ $this->getText("//div[@id='bodyContent']/p"));
+
+ $this->click("//input[@value='Restore all default settings']");
+ $this->waitForPageToLoad("30000");
+
+ // Verify preferences saved successfully
+ $this->assertEquals("Your preferences have been saved.",
+ $this->getText("//div[@id='bodyContent']/div[4]/strong/p"));
+ }
+}
+