summaryrefslogtreecommitdiff
path: root/tests/phpunit/includes/parser
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2011-12-03 13:29:22 +0100
committerPierre Schmitz <pierre@archlinux.de>2011-12-03 13:29:22 +0100
commitca32f08966f1b51fcb19460f0996bb0c4048e6fe (patch)
treeec04cc15b867bc21eedca904cea9af0254531a11 /tests/phpunit/includes/parser
parenta22fbfc60f36f5f7ee10d5ae6fe347340c2ee67c (diff)
Update to MediaWiki 1.18.0
* also update ArchLinux skin to chagnes in MonoBook * Use only css to hide our menu bar when printing
Diffstat (limited to 'tests/phpunit/includes/parser')
-rw-r--r--tests/phpunit/includes/parser/MagicVariableTest.php195
-rw-r--r--tests/phpunit/includes/parser/MediaWikiParserTest.php34
-rw-r--r--tests/phpunit/includes/parser/NewParserTest.php850
-rw-r--r--tests/phpunit/includes/parser/ParserHelpers.php136
-rw-r--r--tests/phpunit/includes/parser/PreprocessorTest.php195
-rw-r--r--tests/phpunit/includes/parser/TagHooks.php77
6 files changed, 1487 insertions, 0 deletions
diff --git a/tests/phpunit/includes/parser/MagicVariableTest.php b/tests/phpunit/includes/parser/MagicVariableTest.php
new file mode 100644
index 00000000..a47653e3
--- /dev/null
+++ b/tests/phpunit/includes/parser/MagicVariableTest.php
@@ -0,0 +1,195 @@
+<?php
+/**
+ * This file is intended to test magic variables in the parser
+ * It was inspired by Raymond & Matěj Grabovský commenting about r66200
+ *
+ * As of february 2011, it only tests some revisions and date related
+ * magic variables.
+ *
+ * @author Ashar Voultoiz
+ * @copyright Copyright © 2011, Ashar Voultoiz
+ * @file
+ */
+
+/** */
+class MagicVariableTest extends MediaWikiTestCase {
+ /** Will contains a parser object*/
+ private $testParser = null;
+
+ /**
+ * An array of magicword returned as type integer by the parser
+ * They are usually returned as a string for i18n since we support
+ * persan numbers for example, but some magic explicitly return
+ * them as integer.
+ * @see MagicVariableTest::assertMagic()
+ */
+ private $expectedAsInteger = array(
+ 'revisionday',
+ 'revisionmonth1',
+ );
+
+ /** setup a basic parser object */
+ function setUp() {
+ global $wgContLang;
+ $wgContLang = Language::factory( 'en' );
+
+ $this->testParser = new Parser();
+ $this->testParser->Options( new ParserOptions() );
+
+ # initialize parser output
+ $this->testParser->clearState();
+ }
+
+ /** destroy parser (TODO: is it really neded?)*/
+ function tearDown() {
+ unset( $this->testParser );
+ }
+
+ ############### TESTS #############################################
+ # @todo FIXME:
+ # - those got copy pasted, we can probably make them cleaner
+ # - tests are lacking useful messages
+
+ # day
+
+ /** @dataProvider MediaWikiProvide::Days */
+ function testCurrentdayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'currentday', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testCurrentdaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'currentday2', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testLocaldayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'localday', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testLocaldaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'localday2', $day );
+ }
+
+ # month
+
+ /** @dataProvider MediaWikiProvide::Months */
+ function testCurrentmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'currentmonth', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testCurrentmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'currentmonth1', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testLocalmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'localmonth', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testLocalmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'localmonth1', $month );
+ }
+
+
+ # revision day
+
+ /** @dataProvider MediaWikiProvide::Days */
+ function testRevisiondayIsUnPadded( $day ) {
+ $this->assertUnPadded( 'revisionday', $day );
+ }
+ /** @dataProvider MediaWikiProvide::Days */
+ function testRevisiondaytwoIsZeroPadded( $day ) {
+ $this->assertZeroPadded( 'revisionday2', $day );
+ }
+
+ # revision month
+
+ /** @dataProvider MediaWikiProvide::Months */
+ function testRevisionmonthIsZeroPadded( $month ) {
+ $this->assertZeroPadded( 'revisionmonth', $month );
+ }
+ /** @dataProvider MediaWikiProvide::Months */
+ function testRevisionmonthoneIsUnPadded( $month ) {
+ $this->assertUnPadded( 'revisionmonth1', $month );
+ }
+
+ /**
+ * Rough tests for {{SERVERNAME}} magic word
+ * Bug 31176
+ */
+ function testServernameFromDifferentProtocols() {
+ global $wgServer;
+ $saved_wgServer= $wgServer;
+
+ $wgServer = 'http://localhost/';
+ $this->assertMagic( 'localhost', 'servername' );
+ $wgServer = 'https://localhost/';
+ $this->assertMagic( 'localhost', 'servername' );
+ $wgServer = '//localhost/'; # bug 31176
+ $this->assertMagic( 'localhost', 'servername' );
+
+ $wgServer = $saved_wgServer;
+ }
+
+ ############### HELPERS ############################################
+
+ /** assertion helper expecting a magic output which is zero padded */
+ PUBLIC function assertZeroPadded( $magic, $value ) {
+ $this->assertMagicPadding( $magic, $value, '%02d' );
+ }
+
+ /** assertion helper expecting a magic output which is unpadded */
+ PUBLIC function assertUnPadded( $magic, $value ) {
+ $this->assertMagicPadding( $magic, $value, '%d' );
+ }
+
+ /**
+ * Main assertion helper for magic variables padding
+ * @param $magic string Magic variable name
+ * @param $value mixed Month or day
+ * @param $format string sprintf format for $value
+ */
+ private function assertMagicPadding( $magic, $value, $format ) {
+ # Initialize parser timestamp as year 2010 at 12h34 56s.
+ # month and day are given by the caller ($value). Month < 12!
+ if( $value > 12 ) { $month = $value % 12; }
+ else { $month = $value; }
+
+ $this->setParserTS(
+ sprintf( '2010%02d%02d123456', $month, $value )
+ );
+
+ # please keep the following commented line of code. It helps debugging.
+ //print "\nDEBUG (value $value):" . sprintf( '2010%02d%02d123456', $value, $value ) . "\n";
+
+ # format expectation and test it
+ $expected = sprintf( $format, $value );
+ $this->assertMagic( $expected, $magic );
+ }
+
+ /** helper to set the parser timestamp and revision timestamp */
+ private function setParserTS( $ts ) {
+ $this->testParser->Options()->setTimestamp( $ts );
+ $this->testParser->mRevisionTimestamp = $ts;
+ }
+
+ /**
+ * Assertion helper to test a magic variable output
+ */
+ private function assertMagic( $expected, $magic ) {
+ if( in_array( $magic, $this->expectedAsInteger ) ) {
+ $expected = (int) $expected;
+ }
+
+ # Generate a message for the assertion
+ $msg = sprintf( "Magic %s should be <%s:%s>",
+ $magic,
+ $expected,
+ gettype( $expected )
+ );
+
+ $this->assertSame(
+ $expected,
+ $this->testParser->getVariableValue( $magic ),
+ $msg
+ );
+ }
+}
diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php
new file mode 100644
index 00000000..18510d9a
--- /dev/null
+++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php
@@ -0,0 +1,34 @@
+<?php
+
+require_once( dirname( __FILE__ ) . '/ParserHelpers.php' );
+require_once( dirname( __FILE__ ) . '/NewParserTest.php' );
+
+/**
+ * The UnitTest must be either a class that inherits from PHPUnit_Framework_TestCase
+ * or a class that provides a public static suite() method which returns
+ * an PHPUnit_Framework_Test object
+ *
+ * @group Parser
+ * @group Database
+ */
+class MediaWikiParserTest {
+
+ public static function suite() {
+ global $wgParserTestFiles;
+
+ $suite = new PHPUnit_Framework_TestSuite;
+
+ foreach ( $wgParserTestFiles as $filename ) {
+ $testsName = basename( $filename, '.txt' );
+ $className = /*ucfirst( basename( dirname( $filename ) ) ) .*/ ucfirst( basename( $filename, '.txt' ) );
+
+ eval( "/** @group Database\n@group Parser\n*/ class $className extends NewParserTest { protected \$file = \"" . addslashes( $filename ) . "\"; } " );
+
+ $parserTester = new $className( $testsName );
+ $suite->addTestSuite( new ReflectionClass ( $parserTester ) );
+ }
+
+
+ return $suite;
+ }
+}
diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php
new file mode 100644
index 00000000..f4d5f757
--- /dev/null
+++ b/tests/phpunit/includes/parser/NewParserTest.php
@@ -0,0 +1,850 @@
+<?php
+
+/**
+ * Although marked as a stub, can work independently.
+ *
+ * @group Database
+ * @group Parser
+ * @group Stub
+ */
+class NewParserTest extends MediaWikiTestCase {
+
+ static protected $articles = array(); // Array of test articles defined by the tests
+ /* The dataProvider is run on a different instance than the test, so it must be static
+ * When running tests from several files, all tests will see all articles.
+ */
+
+ public $uploadDir;
+ public $keepUploads = false;
+ public $runDisabled = false;
+ public $regex = '';
+ public $showProgress = true;
+ public $savedInitialGlobals = array();
+ public $savedWeirdGlobals = array();
+ public $savedGlobals = array();
+ public $hooks = array();
+ public $functionHooks = array();
+
+ //Fuzz test
+ public $maxFuzzTestLength = 300;
+ public $fuzzSeed = 0;
+ public $memoryLimit = 50;
+
+ protected $file = false;
+
+ /*function __construct($a = null,$b = array(),$c = null ) {
+ parent::__construct($a,$b,$c);
+ }*/
+
+ function setUp() {
+ global $wgContLang, $wgNamespaceProtection, $wgNamespaceAliases;
+ global $wgHooks, $IP;
+ $wgContLang = Language::factory( 'en' );
+
+ //Setup CLI arguments
+ if ( $this->getCliArg( 'regex=' ) ) {
+ $this->regex = $this->getCliArg( 'regex=' );
+ } else {
+ # Matches anything
+ $this->regex = '';
+ }
+
+ $this->keepUploads = $this->getCliArg( 'keep-uploads' );
+
+ $tmpGlobals = array();
+
+ $tmpGlobals['wgScript'] = '/index.php';
+ $tmpGlobals['wgScriptPath'] = '/';
+ $tmpGlobals['wgArticlePath'] = '/wiki/$1';
+ $tmpGlobals['wgStyleSheetPath'] = '/skins';
+ $tmpGlobals['wgStylePath'] = '/skins';
+ $tmpGlobals['wgThumbnailScriptPath'] = false;
+ $tmpGlobals['wgLocalFileRepo'] = array(
+ 'class' => 'LocalRepo',
+ 'name' => 'local',
+ 'directory' => wfTempDir() . '/test-repo',
+ 'url' => 'http://example.com/images',
+ 'deletedDir' => wfTempDir() . '/test-repo/delete',
+ 'hashLevels' => 2,
+ 'transformVia404' => false,
+ );
+
+ $tmpGlobals['wgEnableParserCache'] = false;
+ $tmpGlobals['wgHooks'] = $wgHooks;
+ $tmpGlobals['wgDeferredUpdateList'] = array();
+ $tmpGlobals['wgMemc'] = wfGetMainCache();
+ $tmpGlobals['messageMemc'] = wfGetMessageCacheStorage();
+ $tmpGlobals['parserMemc'] = wfGetParserCacheStorage();
+
+ // $tmpGlobals['wgContLang'] = new StubContLang;
+ $tmpGlobals['wgUser'] = new User;
+ $context = new RequestContext();
+ $tmpGlobals['wgLang'] = $context->getLang();
+ $tmpGlobals['wgOut'] = $context->getOutput();
+ $tmpGlobals['wgParser'] = new StubObject( 'wgParser', $GLOBALS['wgParserConf']['class'], array( $GLOBALS['wgParserConf'] ) );
+ $tmpGlobals['wgRequest'] = new WebRequest;
+
+ if ( $GLOBALS['wgStyleDirectory'] === false ) {
+ $tmpGlobals['wgStyleDirectory'] = "$IP/skins";
+ }
+
+
+ foreach ( $tmpGlobals as $var => $val ) {
+ if ( array_key_exists( $var, $GLOBALS ) ) {
+ $this->savedInitialGlobals[$var] = $GLOBALS[$var];
+ }
+
+ $GLOBALS[$var] = $val;
+ }
+
+ $this->savedWeirdGlobals['mw_namespace_protection'] = $wgNamespaceProtection[NS_MEDIAWIKI];
+ $this->savedWeirdGlobals['image_alias'] = $wgNamespaceAliases['Image'];
+ $this->savedWeirdGlobals['image_talk_alias'] = $wgNamespaceAliases['Image_talk'];
+
+ $wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
+ $wgNamespaceAliases['Image'] = NS_FILE;
+ $wgNamespaceAliases['Image_talk'] = NS_FILE_TALK;
+
+ }
+
+ public function tearDown() {
+
+ foreach ( $this->savedInitialGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+
+ global $wgNamespaceProtection, $wgNamespaceAliases;
+
+ $wgNamespaceProtection[NS_MEDIAWIKI] = $this->savedWeirdGlobals['mw_namespace_protection'];
+ $wgNamespaceAliases['Image'] = $this->savedWeirdGlobals['image_alias'];
+ $wgNamespaceAliases['Image_talk'] = $this->savedWeirdGlobals['image_talk_alias'];
+ }
+
+ function addDBData() {
+ # 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 ),
+ /**
+ * @todo Fixme! Why are we inserting duplicate data here? Shouldn't
+ * need this IGNORE or shouldn't need the insert at all.
+ */
+ ), __METHOD__, array( 'IGNORE' ) );
+
+
+ # Update certain things in site_stats
+ $this->db->insert( 'site_stats',
+ array( 'ss_row_id' => 1, 'ss_images' => 2, 'ss_good_articles' => 1 ),
+ __METHOD__,
+ /**
+ * @todo Fixme! Same as above!
+ */
+ array( 'IGNORE' )
+ );
+
+ # Reinitialise the LocalisationCache to match the database state
+ Language::getLocalisationCache()->unloadAll();
+
+ # Clear the message cache
+ MessageCache::singleton()->clear();
+
+ $this->uploadDir = $this->setupUploadDir();
+
+ $user = User::newFromId( 0 );
+ LinkCache::singleton()->clear(); # Avoids the odd failure at creating the nullRevision
+
+ $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 );
+
+ }
+
+
+
+
+ //ParserTest setup/teardown functions
+
+ /**
+ * Set up the global variables for a consistent environment for each test.
+ * Ideally this should replace the global configuration entirely.
+ */
+ protected 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' ? 'unittest_' : 'ut_',
+ '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,
+ 'wgAdaptiveMessageCache' => true,
+ 'wgUseDatabaseMessages' => 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['wgContLang'] = $langObj;
+ $context = new RequestContext();
+ $GLOBALS['wgLang'] = $context->getLang();
+
+ $GLOBALS['wgMemc'] = new EmptyBagOStuff;
+ $GLOBALS['wgOut'] = $context->getOutput();
+
+ global $wgHooks;
+
+ $wgHooks['ParserTestParser'][] = 'ParserTestParserHook::setup';
+ $wgHooks['ParserTestParser'][] = 'ParserTestStaticParserHook::setup';
+ $wgHooks['ParserGetVariableValueTs'][] = 'ParserTest::getFakeTimestamp';
+
+ MagicWord::clearCache();
+
+ # Publish the articles after we have the final language set
+ $this->publishTestArticles();
+
+ # The entries saved into RepoGroup cache with previous globals will be wrong.
+ RepoGroup::destroySingleton();
+ MessageCache::singleton()->destroyInstance();
+
+ global $wgUser;
+ $wgUser = new User();
+ }
+
+ /**
+ * Create a dummy uploads directory which will contain a couple
+ * of files in order to pass existence tests.
+ *
+ * @return String: the directory
+ */
+ protected 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.
+ */
+ protected function teardownGlobals() {
+ RepoGroup::destroySingleton();
+ LinkCache::singleton()->clear();
+
+ foreach ( $this->savedGlobals as $var => $val ) {
+ $GLOBALS[$var] = $val;
+ }
+
+ $this->teardownUploadDir( $this->uploadDir );
+ }
+
+ /**
+ * 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 );
+ }
+ }
+ }
+
+ public function parserTestProvider() {
+ if ( $this->file === false ) {
+ global $wgParserTestFiles;
+ $this->file = $wgParserTestFiles[0];
+ }
+ return new TestFileIterator( $this->file, $this );
+ }
+
+ /**
+ * Set the file from whose tests will be run by this instance
+ */
+ public function setParserTestFile( $filename ) {
+ $this->file = $filename;
+ }
+
+ /** @dataProvider parserTestProvider */
+ public function testParserTest( $desc, $input, $result, $opts, $config ) {
+ if ( !preg_match( '/' . $this->regex . '/', $desc ) ) return; //$this->markTestSkipped( 'Filtered out by the user' );
+
+ wfDebug( "Running parser test: $desc\n" );
+
+ $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, $title );
+ } 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 = '';
+ }
+ }
+ $parser->mPreprocessor = null;
+
+ $result = $this->tidy( $result );
+ }
+
+ $this->teardownGlobals();
+
+ $this->assertEquals( $result, $out, $desc );
+ }
+
+ /**
+ * Run a fuzz test series
+ * Draw input from a set of test files
+ */
+ function testFuzzTests() {
+
+ $this->markTestIncomplete( "Somebody is serializing PDO objects, that's a no-no" );
+
+ global $wgParserTestFiles;
+
+ $files = $wgParserTestFiles;
+
+ if( $this->getCliArg( 'file=' ) ) {
+ $files = array( $this->getCliArg( 'file=' ) );
+ }
+
+ $dict = $this->getFuzzInput( $files );
+ $dictSize = strlen( $dict );
+ $logMaxLength = log( $this->maxFuzzTestLength );
+
+ $user = new User;
+ $opts = ParserOptions::newFromUser( $user );
+ $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+ $id = 1;
+
+ 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 );
+ $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" );
+ } catch ( Exception $exception ) {
+ $input_dump = sprintf( "string(%d) \"%s\"\n", strlen( $input ), $input );
+
+ $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\nInput: $input_dump\n\nError: {$exception->getMessage()}\n\nBacktrace: {$exception->getTraceAsString()}" );
+ }
+
+ $this->teardownGlobals();
+ $parser->__destruct();
+
+ if ( $id % 100 == 0 ) {
+ $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+ //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
+ if ( $usage > 90 ) {
+ $ret = "Out of memory:\n";
+ $memStats = $this->getMemoryBreakdown();
+
+ foreach ( $memStats as $name => $usage ) {
+ $ret .= "$name: $usage\n";
+ }
+
+ throw new MWException( $ret );
+ }
+ }
+
+ $id++;
+
+ }
+ }
+
+ //Various getter functions
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Get a Parser object
+ */
+ function getParser( $preprocessor = null ) {
+ global $wgParserConf;
+
+ $class = $wgParserConf['class'];
+ $parser = new $class( array( 'preprocessorClass' => $preprocessor ) + $wgParserConf );
+
+ wfRunHooks( 'ParserTestParser', array( &$parser ) );
+
+ return $parser;
+ }
+
+ //Various action functions
+
+ public function addArticle( $name, $text, $line ) {
+ self::$articles[$name] = $text;
+ }
+
+ public function publishTestArticles() {
+ if ( empty( self::$articles ) ) {
+ return;
+ }
+
+ foreach ( self::$articles as $name => $text ) {
+ $title = Title::newFromText( $name );
+
+ if ( $title->getArticleID( Title::GAID_FOR_UPDATE ) == 0 ) {
+ ParserTest::addArticle( $name, $text );
+ }
+ }
+ }
+
+ /**
+ * 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.
+ return isset( $wgParser->mTagHooks[$name] );
+ }
+
+ public function requireFunctionHook( $name ) {
+ global $wgParser;
+ $wgParser->firstCallInit( ); // make sure hooks are loaded.
+ return isset( $wgParser->mFunctionHooks[$name] );
+ }
+ //Various "cleanup" functions
+
+ /*
+ * Run the "tidy" command on text if the $wgUseTidy
+ * global is true
+ *
+ * @param $text String: the text to tidy
+ * @return String
+ */
+ protected function tidy( $text ) {
+ global $wgUseTidy;
+
+ if ( $wgUseTidy ) {
+ $text = MWTidy::tidy( $text );
+ }
+
+ return $text;
+ }
+
+ /**
+ * Remove last character if it is a newline
+ */
+ public function removeEndingNewline( $s ) {
+ if ( substr( $s, -1 ) === "\n" ) {
+ return substr( $s, 0, -1 );
+ }
+ else {
+ return $s;
+ }
+ }
+
+ public function showRunFile( $file ) {
+ /* NOP */
+ }
+
+ //Test options parser functions
+
+ protected 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;
+ }
+
+ protected 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;
+ }
+
+ /**
+ * 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
+ */
+ protected static function getOptionValue( $key, $opts, $default ) {
+ $key = strtolower( $key );
+
+ if ( isset( $opts[$key] ) ) {
+ return $opts[$key];
+ } else {
+ return $default;
+ }
+ }
+}
diff --git a/tests/phpunit/includes/parser/ParserHelpers.php b/tests/phpunit/includes/parser/ParserHelpers.php
new file mode 100644
index 00000000..4a6ce7c4
--- /dev/null
+++ b/tests/phpunit/includes/parser/ParserHelpers.php
@@ -0,0 +1,136 @@
+<?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;
+ }
+
+ public function setupRecorder( $options ) {
+ $this->recorder = new PHPUnitTestRecorder( $this );
+ }
+}
+
+class ParserUnitTest extends MediaWikiTestCase {
+ private $test = "";
+
+ public function __construct( $suite, $test = null ) {
+ parent::__construct();
+ $this->test = $test;
+ $this->suite = $suite;
+ }
+
+ 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 ) { }
+}
diff --git a/tests/phpunit/includes/parser/PreprocessorTest.php b/tests/phpunit/includes/parser/PreprocessorTest.php
new file mode 100644
index 00000000..7a5948d4
--- /dev/null
+++ b/tests/phpunit/includes/parser/PreprocessorTest.php
@@ -0,0 +1,195 @@
+<?php
+
+class PreprocessorTest extends MediaWikiTestCase {
+ var $mTitle = 'Page title';
+ var $mPPNodeCount = 0;
+ var $mOptions;
+
+ function setUp() {
+ global $wgParserConf;
+ $this->mOptions = new ParserOptions();
+ $name = isset( $wgParserConf['preprocessorClass'] ) ? $wgParserConf['preprocessorClass'] : 'Preprocessor_DOM';
+
+ $this->mPreprocessor = new $name( $this );
+ }
+
+ function getStripList() {
+ return array( 'gallery', 'display map' /* Used by Maps, see r80025 CR */, '/foo' );
+ }
+
+ function provideCases() {
+ return array(
+ array( "Foo", "<root>Foo</root>" ),
+ array( "<!-- Foo -->", "<root><comment>&lt;!-- Foo --&gt;</comment></root>" ),
+ array( "<!-- Foo --><!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment><comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> <comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> \n <!-- Bar -->", "<root><comment>&lt;!-- Foo --&gt;</comment> \n <comment>&lt;!-- Bar --&gt;</comment></root>" ),
+ array( "<!-- Foo --> \n <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> \n<comment> &lt;!-- Bar --&gt;\n</comment></root>" ),
+ array( "<!-- Foo --> <!-- Bar -->\n", "<root><comment>&lt;!-- Foo --&gt;</comment> <comment>&lt;!-- Bar --&gt;</comment>\n</root>" ),
+ array( "<!-->Bar", "<root><comment>&lt;!--&gt;Bar</comment></root>" ),
+ array( "<!-- Comment -- comment", "<root><comment>&lt;!-- Comment -- comment</comment></root>" ),
+ array( "== Foo ==\n <!-- Bar -->\n== Baz ==\n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<comment> &lt;!-- Bar --&gt;\n</comment><h level=\"2\" i=\"2\">== Baz ==</h>\n</root>" ),
+ array( "<gallery/>", "<root><ext><name>gallery</name><attr></attr></ext></root>" ),
+ array( "Foo <gallery/> Bar", "<root>Foo <ext><name>gallery</name><attr></attr></ext> Bar</root>" ),
+ array( "<gallery></gallery>", "<root><ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<foo> <gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner></inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<foo> <gallery><gallery></gallery>", "<root>&lt;foo&gt; <ext><name>gallery</name><attr></attr><inner>&lt;gallery&gt;</inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "<noinclude> Foo bar </noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore> Foo bar <ignore>&lt;/noinclude&gt;</ignore></root>" ),
+ array( "<noinclude>\n{{Foo}}\n</noinclude>", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore></root>" ),
+ array( "<noinclude>\n{{Foo}}\n</noinclude>\n", "<root><ignore>&lt;noinclude&gt;</ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore>&lt;/noinclude&gt;</ignore>\n</root>" ),
+ array( "<gallery>foo bar", "<root><ext><name>gallery</name><attr></attr><inner>foo bar</inner></ext></root>" ),
+ array( "<{{foo}}>", "<root>&lt;<template><title>foo</title></template>&gt;</root>" ),
+ array( "<{{{foo}}}>", "<root>&lt;<tplarg><title>foo</title></tplarg>&gt;</root>" ),
+ array( "<gallery></gallery</gallery>", "<root><ext><name>gallery</name><attr></attr><inner>&lt;/gallery</inner><close>&lt;/gallery&gt;</close></ext></root>" ),
+ array( "=== Foo === ", "<root><h level=\"3\" i=\"1\">=== Foo === </h></root>" ),
+ array( "==<!-- -->= Foo === ", "<root><h level=\"2\" i=\"1\">==<comment>&lt;!-- --&gt;</comment>= Foo === </h></root>" ),
+ array( "=== Foo ==<!-- -->= ", "<root><h level=\"1\" i=\"1\">=== Foo ==<comment>&lt;!-- --&gt;</comment>= </h></root>" ),
+ array( "=== Foo ===<!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment></h>\n</root>" ),
+ array( "=== Foo ===<!-- --> <!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment>&lt;!-- --&gt;</comment> <comment>&lt;!-- --&gt;</comment></h>\n</root>" ),
+ array( "== Foo ==\n== Bar == \n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<h level=\"2\" i=\"2\">== Bar == </h>\n</root>" ),
+ array( "===========", "<root><h level=\"5\" i=\"1\">===========</h></root>" ),
+ array( "Foo\n=\n==\n=\n", "<root>Foo\n=\n==\n=\n</root>" ),
+ array( "{{Foo}}", "<root><template lineStart=\"1\"><title>Foo</title></template></root>" ),
+ array( "\n{{Foo}}", "<root>\n<template lineStart=\"1\"><title>Foo</title></template></root>" ),
+ array( "{{Foo|bar}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar}}a", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part></template>a</root>" ),
+ array( "{{Foo|bar|baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>1</name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|=bar}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name></name>=<value>bar</value></part></template></root>" ),
+ array( "{{Foo|bar=baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>bar</name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar|baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name index=\"1\" /><value>baz</value></part></template></root>" ),
+ array( "{{Foo|1=bar|2=baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>1</name>=<value>bar</value></part><part><name>2</name>=<value>baz</value></part></template></root>" ),
+ array( "{{Foo|bar|foo=baz}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name>foo</name>=<value>baz</value></part></template></root>" ),
+ array( "{{{1}}}", "<root><tplarg lineStart=\"1\"><title>1</title></tplarg></root>" ),
+ array( "{{{1|}}}", "<root><tplarg lineStart=\"1\"><title>1</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo}}}", "<root><tplarg lineStart=\"1\"><title>Foo</title></tplarg></root>" ),
+ array( "{{{Foo|}}}", "<root><tplarg lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value></value></part></tplarg></root>" ),
+ array( "{{{Foo|bar|baz}}}", "<root><tplarg lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>bar</value></part><part><name index=\"2\" /><value>baz</value></part></tplarg></root>" ),
+ array( "{<!-- -->{Foo}}", "<root>{<comment>&lt;!-- --&gt;</comment>{Foo}}</root>" ),
+ array( "{{{{Foobar}}}}", "<root>{<tplarg><title>Foobar</title></tplarg>}</root>" ),
+ array( "{{{ {{Foo}} }}}", "<root><tplarg lineStart=\"1\"><title> <template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{ {{{Foo}}} }}", "<root><template lineStart=\"1\"><title> <tplarg><title>Foo</title></tplarg> </title></template></root>" ),
+ array( "{{{{{Foo}}}}}", "<root><template lineStart=\"1\"><title><tplarg><title>Foo</title></tplarg></title></template></root>" ),
+ array( "{{{{{Foo}} }}}", "<root><tplarg lineStart=\"1\"><title><template><title>Foo</title></template> </title></tplarg></root>" ),
+ array( "{{{{{{Foo}}}}}}", "<root><tplarg lineStart=\"1\"><title><tplarg><title>Foo</title></tplarg></title></tplarg></root>" ),
+ array( "{{{{{{Foo}}}}}", "<root>{<template><title><tplarg><title>Foo</title></tplarg></title></template></root>" ),
+ array( "[[[Foo]]", "<root>[[[Foo]]</root>" ),
+ array( "{{Foo|[[[[bar]]|baz]]}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name index=\"1\" /><value>[[[[bar]]|baz]]</value></part></template></root>" ), // This test is important, since it means the difference between having the [[ rule stacked or not
+ array( "{{Foo|[[[[bar]|baz]]}}", "<root>{{Foo|[[[[bar]|baz]]}}</root>" ),
+ array( "{{Foo|Foo [[[[bar]|baz]]}}", "<root>{{Foo|Foo [[[[bar]|baz]]}}</root>" ),
+ array( "Foo <display map>Bar</display map >Baz", "<root>Foo <ext><name>display map</name><attr></attr><inner>Bar</inner><close>&lt;/display map &gt;</close></ext>Baz</root>" ),
+ array( "Foo <display map foo>Bar</display map >Baz", "<root>Foo <ext><name>display map</name><attr> foo</attr><inner>Bar</inner><close>&lt;/display map &gt;</close></ext>Baz</root>" ),
+ array( "Foo <gallery bar=\"baz\" />", "<root>Foo <ext><name>gallery</name><attr> bar=&quot;baz&quot; </attr></ext></root>" ),
+ array( "</foo>Foo<//foo>", "<root><ext><name>/foo</name><attr></attr><inner>Foo</inner><close>&lt;//foo&gt;</close></ext></root>" ), # Worth blacklisting IMHO
+ array( "{{#ifexpr: ({{{1|1}}} = 2) | Foo | Bar }}", "<root><template lineStart=\"1\"><title>#ifexpr: (<tplarg><title>1</title><part><name index=\"1\" /><value>1</value></part></tplarg> = 2) </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | Foo | {{Bar}} }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> <template><title>Bar</title></template> </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | Foo | [[Bar]] }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> Foo </value></part><part><name index=\"2\" /><value> [[Bar]] </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | [[Foo]] | Bar }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> [[Foo]] </value></part><part><name index=\"2\" /><value> Bar </value></part></template></root>"),
+ array( "{{#if: {{{1|}}} | 1 | {{#if: {{{1|}}} | 2 | 3 }} }}", "<root><template lineStart=\"1\"><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 1 </value></part><part><name index=\"2\" /><value> <template><title>#if: <tplarg><title>1</title><part><name index=\"1\" /><value></value></part></tplarg> </title><part><name index=\"1\" /><value> 2 </value></part><part><name index=\"2\" /><value> 3 </value></part></template> </value></part></template></root>"),
+ array( "{{ {{Foo}}", "<root>{{ <template><title>Foo</title></template></root>"),
+ array( "{{Foobar {{Foo}} {{Bar}} {{Baz}} ", "<root>{{Foobar <template><title>Foo</title></template> <template><title>Bar</title></template> <template><title>Baz</title></template> </root>"),
+ array( "[[Foo]] |", "<root>[[Foo]] |</root>"),
+ array( "{{Foo|Bar|", "<root>{{Foo|Bar|</root>"),
+ array( "[[Foo]", "<root>[[Foo]</root>"),
+ array( "[[Foo|Bar]", "<root>[[Foo|Bar]</root>"),
+ array( "{{Foo| [[Bar] }}", "<root>{{Foo| [[Bar] }}</root>"),
+ array( "{{Foo| [[Bar|Baz] }}", "<root>{{Foo| [[Bar|Baz] }}</root>"),
+ array( "{{Foo|bar=[[baz]}}", "<root>{{Foo|bar=[[baz]}}</root>"),
+ array( "{{foo|", "<root>{{foo|</root>"),
+ array( "{{foo|}", "<root>{{foo|}</root>"),
+ array( "{{foo|} }}", "<root><template lineStart=\"1\"><title>foo</title><part><name index=\"1\" /><value>} </value></part></template></root>"),
+ array( "{{foo|bar=|}", "<root>{{foo|bar=|}</root>"),
+ array( "{{Foo|} Bar=", "<root>{{Foo|} Bar=</root>"),
+ array( "{{Foo|} Bar=}}", "<root><template lineStart=\"1\"><title>Foo</title><part><name>} Bar</name>=<value></value></part></template></root>"),
+ /* array( file_get_contents( dirname( __FILE__ ) . '/QuoteQuran.txt' ), file_get_contents( dirname( __FILE__ ) . '/QuoteQuranExpanded.txt' ) ), */
+ );
+ }
+
+ /**
+ * @dataProvider provideCases
+ */
+ function testPreprocessorOutput( $wikiText, $expectedXml ) {
+ $this->assertEquals( $expectedXml, $this->mPreprocessor->preprocessToXml( $wikiText ) );
+ }
+
+ /**
+ * These are more complex test cases taken out of wiki articles.
+ */
+ function provideFiles() {
+ return array(
+ array( "QuoteQuran" ), # http://en.wikipedia.org/w/index.php?title=Template:QuoteQuran/sandbox&oldid=237348988 GFDL + CC-BY-SA by Striver
+ array( "Factorial" ), # http://en.wikipedia.org/w/index.php?title=Template:Factorial&oldid=98548758 GFDL + CC-BY-SA by Polonium
+ array( "All_system_messages" ), # http://tl.wiktionary.org/w/index.php?title=Suleras:All_system_messages&oldid=2765 GPL text generated by MediaWiki
+ array( "Fundraising" ), # http://tl.wiktionary.org/w/index.php?title=MediaWiki:Sitenotice&oldid=5716 GFDL + CC-BY-SA, copied there by Sky Harbor.
+ );
+ }
+
+ /**
+ * @dataProvider provideFiles
+ */
+ function testPreprocessorOutputFiles( $filename ) {
+ $folder = dirname( __FILE__ ) . "/../../../parser/preprocess";
+ $wikiText = file_get_contents( "$folder/$filename.txt" );
+ $output = $this->mPreprocessor->preprocessToXml( $wikiText );
+
+ $expectedFilename = "$folder/$filename.expected";
+ if ( file_exists( $expectedFilename ) ) {
+ $this->assertStringEqualsFile( $expectedFilename, $output );
+ } else {
+ $tempFilename = tempnam( $folder, "$filename." );
+ file_put_contents( $tempFilename, $output );
+ $this->markTestIncomplete( "File $expectedFilename missing. Output stored as $tempFilename" );
+ }
+ }
+
+ /**
+ * Tests from Bug 28642 · https://bugzilla.wikimedia.org/28642
+ */
+ function provideHeadings() {
+ return array( /* These should become headings: */
+ array( "== h ==<!--c1-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment></h></root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+ array( "== h == <!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> <comment>&lt;!--c3--&gt;</comment> </h></root>" ),
+
+ /* These are not working: */
+ array( "== h ==<!--c1--> <!--c2-->", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></root>" ),
+ array( "== h == <!--c1--> <!--c2-->", "<root>== h == <comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment></root>" ),
+ array( "== h ==<!--c1--> <!--c2--> ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> <comment>&lt;!--c2--&gt;</comment> </root>" ),
+ array( "== h == x <!--c1--><!--c2--><!--c3--> ", "<root>== h == x <comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </root>" ),
+ array( "== h ==<!--c1--> x <!--c2--><!--c3--> ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment> x <comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> </root>" ),
+ array( "== h ==<!--c1--><!--c2--><!--c3--> x ", "<root>== h ==<comment>&lt;!--c1--&gt;</comment><comment>&lt;!--c2--&gt;</comment><comment>&lt;!--c3--&gt;</comment> x </root>" ),
+ );
+ }
+
+ /**
+ * @dataProvider provideHeadings
+ */
+ function testHeadings( $wikiText, $expectedXml ) {
+ $this->assertEquals( $expectedXml, $this->mPreprocessor->preprocessToXml( $wikiText ) );
+ }
+}
+
diff --git a/tests/phpunit/includes/parser/TagHooks.php b/tests/phpunit/includes/parser/TagHooks.php
new file mode 100644
index 00000000..713ce846
--- /dev/null
+++ b/tests/phpunit/includes/parser/TagHooks.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @group Parser
+ */
+class TagHookTest extends MediaWikiTestCase {
+
+ public static function provideValidNames() {
+ return array( array( 'foo' ), array( 'foo-bar' ), array( 'foo_bar' ), array( 'FOO-BAR' ), array( 'foo bar' ) );
+ }
+
+ public static function provideBadNames() {
+ return array( array( "foo<bar" ), array( "foo>bar" ), array( "foo\nbar" ), array( "foo\rbar" ) );
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ */
+ function testTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parserOutput = $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ */
+ function testBadTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setHook( $tag, array( $this, 'tagCallback' ) );
+ $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->fail('Exception not thrown.');
+ }
+
+ /**
+ * @dataProvider provideValidNames
+ */
+ function testFunctionTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), 0 );
+ $parserOutput = $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->assertEquals( "<p>FooOneBaz\n</p>", $parserOutput->getText() );
+
+ $parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
+ }
+
+ /**
+ * @dataProvider provideBadNames
+ * @expectedException MWException
+ */
+ function testBadFunctionTagHooks( $tag ) {
+ global $wgParserConf;
+ $parser = new Parser( $wgParserConf );
+
+ $parser->setFunctionTagHook( $tag, array( $this, 'functionTagCallback' ), SFH_OBJECT_ARGS );
+ $parser->parse( "Foo<$tag>Bar</$tag>Baz", Title::newFromText( 'Test' ), new ParserOptions );
+ $this->fail('Exception not thrown.');
+ }
+
+ function tagCallback( $text, $params, $parser ) {
+ return str_rot13( $text );
+ }
+
+ function functionTagCallback( &$parser, $frame, $code, $attribs ) {
+ return str_rot13( $code );
+ }
+}