diff options
Diffstat (limited to 'tests/phpunit')
199 files changed, 18719 insertions, 0 deletions
diff --git a/tests/phpunit/Makefile b/tests/phpunit/Makefile new file mode 100644 index 00000000..24536efc --- /dev/null +++ b/tests/phpunit/Makefile @@ -0,0 +1,82 @@ +.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: + @echo "Run 'make help' to get usage" + @echo "" + @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 tests are made non-destructive, you can run" + @echo "the destructive tests like so:" + @echo "" + @echo " make destructive" + @echo "" + @echo "Some tests are expected to be safe, you can run them with" + @echo "" + @echo " make safe" + @echo "" + @echo "You are recommended to run the tests with read-only credentials." + @echo "" + @echo "If you don't have a database running, you can still run" + @echo "" + @echo " make databaseless" + @echo "" + +destructive: phpunit + +phpunit: + ${PU} + +install: + ./install-phpunit.sh + +tap: + ${PU} --tap + +coverage: + ${PU} --coverage-html ../../docs/code-coverage + +parser: + ${PU} --group Parser +noparser: + ${PU} --exclude-group Parser,Broken,Stub + +safe: + ${PU} --exclude-group Broken,Destructive,Stub + +databaseless: + ${PU} --exclude-group Broken,Destructive,Database,Stub + +database: + ${PU} --exclude-group Broken,Destructive,Stub --group 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 diff --git a/tests/phpunit/MediaWikiLangTestCase.php b/tests/phpunit/MediaWikiLangTestCase.php new file mode 100644 index 00000000..1cd6a3ba --- /dev/null +++ b/tests/phpunit/MediaWikiLangTestCase.php @@ -0,0 +1,33 @@ +<?php + +/** + * Base class that store and restore the Language objects + */ +abstract class MediaWikiLangTestCase extends MediaWikiTestCase { + private static $oldLang; + private static $oldContLang; + + public function setUp() { + global $wgLanguageCode, $wgLang, $wgContLang; + + self::$oldLang = $wgLang; + self::$oldContLang = $wgContLang; + + if( $wgLanguageCode != $wgContLang->getCode() ) die("nooo!"); + + $wgLanguageCode = 'en'; # For mainpage to be 'Main Page' + + $wgContLang = $wgLang = Language::factory( $wgLanguageCode ); + MessageCache::singleton()->disable(); + } + + public function tearDown() { + global $wgContLang, $wgLang, $wgLanguageCode; + $wgLang = self::$oldLang; + + $wgContLang = self::$oldContLang; + $wgLanguageCode = $wgContLang->getCode(); + self::$oldContLang = self::$oldLang = null; + } + +} diff --git a/tests/phpunit/MediaWikiPHPUnitCommand.php b/tests/phpunit/MediaWikiPHPUnitCommand.php new file mode 100644 index 00000000..c0d9f363 --- /dev/null +++ b/tests/phpunit/MediaWikiPHPUnitCommand.php @@ -0,0 +1,46 @@ +<?php + +class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command { + + static $additionalOptions = array( + 'regex=' => false, + 'file=' => false, + 'keep-uploads' => false, + ); + + public function __construct() { + foreach( self::$additionalOptions as $option => $default ) { + $this->longOptions[$option] = $option . 'Handler'; + } + + } + + public static function main( $exit = true ) { + $command = new self; + $command->run($_SERVER['argv'], $exit); + } + + public function __call( $func, $args ) { + + if( substr( $func, -7 ) == 'Handler' ) { + if( is_null( $args[0] ) ) $args[0] = true; //Booleans + self::$additionalOptions[substr( $func, 0, -7 ) ] = $args[0]; + } + } + + public function showHelp() { + parent::showHelp(); + + print <<<EOT + +ParserTest-specific options: + + --regex="<regex>" Only run parser tests that match the given regex + --file="<filename>" Prints the version and exits. + --keep-uploads Re-use the same upload directory for each test, don't delete it + + +EOT; + } + +} diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php new file mode 100644 index 00000000..64cb486b --- /dev/null +++ b/tests/phpunit/MediaWikiTestCase.php @@ -0,0 +1,239 @@ +<?php + +abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { + public $suite; + public $regex = ''; + public $runDisabled = false; + + /** + * @var DatabaseBase + */ + protected $db; + protected $oldTablePrefix; + protected $useTemporaryTables = true; + private static $dbSetup = false; + + /** + * Table name prefixes. Oracle likes it shorter. + */ + const DB_PREFIX = 'unittest_'; + const ORA_DB_PREFIX = 'ut_'; + + protected $supportedDBs = array( + 'mysql', + 'sqlite', + 'oracle' + ); + + function __construct( $name = null, array $data = array(), $dataName = '' ) { + parent::__construct( $name, $data, $dataName ); + + $this->backupGlobals = false; + $this->backupStaticAttributes = false; + } + + function run( PHPUnit_Framework_TestResult $result = NULL ) { + /* Some functions require some kind of caching, and will end up using the db, + * which we can't allow, as that would open a new connection for mysql. + * Replace with a HashBag. They would not be going to persist anyway. + */ + ObjectCache::$instances[CACHE_DB] = new HashBagOStuff; + + if( $this->needsDB() ) { + + global $wgDBprefix; + + $this->db = wfGetDB( DB_MASTER ); + + $this->checkDbIsSupported(); + + $this->oldTablePrefix = $wgDBprefix; + + if( !self::$dbSetup ) { + $this->initDB(); + self::$dbSetup = true; + } + + $this->addCoreDBData(); + $this->addDBData(); + + parent::run( $result ); + + $this->resetDB(); + } else { + parent::run( $result ); + } + } + + function dbPrefix() { + return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX; + } + + function needsDB() { + $rc = new ReflectionClass( $this ); + return strpos( $rc->getDocComment(), '@group Database' ) !== false; + } + + /** + * Stub. If a test needs to add additional data to the database, it should + * implement this method and do so + */ + function addDBData() {} + + private function addCoreDBData() { + + User::resetIdByNameCache(); + + //Make sysop user + $user = User::newFromName( 'UTSysop' ); + + if ( $user->idForName() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTSysopPassword' ); + + $user->addGroup( 'sysop' ); + $user->addGroup( 'bureaucrat' ); + $user->saveSettings(); + } + + + //Make 1 page with 1 revision + $article = new Article( Title::newFromText( 'UTPage' ) ); + $article->doEdit( 'UTContent', + 'UTPageSummary', + EDIT_NEW, + false, + User::newFromName( 'UTSysop' ) ); + } + + private function initDB() { + global $wgDBprefix; + if ( $wgDBprefix === $this->dbPrefix() ) { + throw new MWException( 'Cannot run unit tests, the database prefix is already "unittest_"' ); + } + + $dbClone = new CloneDatabase( $this->db, $this->listTables(), $this->dbPrefix() ); + $dbClone->useTemporaryTables( $this->useTemporaryTables ); + $dbClone->cloneTableStructure(); + + if ( $this->db->getType() == 'oracle' ) { + $this->db->query( 'BEGIN FILL_WIKI_INFO; END;' ); + + # Insert 0 user to prevent FK violations + # Anonymous user + $this->db->insert( 'user', array( + 'user_id' => 0, + 'user_name' => 'Anonymous' ) ); + } + } + + /** + * Empty all tables so they can be repopulated for tests + */ + private function resetDB() { + if( $this->db ) { + foreach( $this->listTables() as $tbl ) { + if( $tbl == 'interwiki' || $tbl == 'user' ) continue; + $this->db->delete( $tbl, '*', __METHOD__ ); + } + } + } + + protected function destroyDB() { + if ( $this->useTemporaryTables || is_null( $this->db ) ) { + # Don't need to do anything + return; + } + + $tables = $this->db->listTables( $this->dbPrefix(), __METHOD__ ); + + foreach ( $tables as $table ) { + try { + $sql = $this->db->getType() == 'oracle' ? "DROP TABLE $table CASCADE CONSTRAINTS PURGE" : "DROP TABLE `$table`"; + $this->db->query( $sql, __METHOD__ ); + } catch( MWException $mwe ) {} + } + + if ( $this->db->getType() == 'oracle' ) + $this->db->query( 'BEGIN FILL_WIKI_INFO; END;', __METHOD__ ); + + CloneDatabase::changePrefix( $this->oldTablePrefix ); + } + + + function __call( $func, $args ) { + static $compatibility = array( + 'assertInternalType' => 'assertType', + 'assertNotInternalType' => 'assertNotType', + 'assertInstanceOf' => 'assertType', + 'assertEmpty' => 'assertEmpty2', + ); + + if ( method_exists( $this->suite, $func ) ) { + return call_user_func_array( array( $this->suite, $func ), $args); + } elseif ( isset( $compatibility[$func] ) ) { + return call_user_func_array( array( $this, $compatibility[$func] ), $args); + } else { + throw new MWException( "Called non-existant $func method on " + . get_class( $this ) ); + } + } + + private function assertEmpty2( $value, $msg ) { + return $this->assertTrue( $value == '', $msg ); + } + + static private function unprefixTable( $tableName ) { + global $wgDBprefix; + return substr( $tableName, strlen( $wgDBprefix ) ); + } + + static private function isNotUnittest( $table ) { + return strpos( $table, 'unittest_' ) !== 0; + } + + protected function listTables() { + global $wgDBprefix; + + $tables = $this->db->listTables( $wgDBprefix, __METHOD__ ); + $tables = array_map( array( __CLASS__, 'unprefixTable' ), $tables ); + + // Don't duplicate test tables from the previous fataled run + $tables = array_filter( $tables, array( __CLASS__, 'isNotUnittest' ) ); + + if ( $this->db->getType() == 'sqlite' ) { + $tables = array_flip( $tables ); + // these are subtables of searchindex and don't need to be duped/dropped separately + unset( $tables['searchindex_content'] ); + unset( $tables['searchindex_segdir'] ); + unset( $tables['searchindex_segments'] ); + $tables = array_flip( $tables ); + } + return $tables; + } + + protected function checkDbIsSupported() { + if( !in_array( $this->db->getType(), $this->supportedDBs ) ) { + throw new MWException( $this->db->getType() . " is not currently supported for unit testing." ); + } + } + + public function getCliArg( $offset ) { + + if( isset( MediaWikiPHPUnitCommand::$additionalOptions[$offset] ) ) { + return MediaWikiPHPUnitCommand::$additionalOptions[$offset]; + } + + } + + public function setCliArg( $offset, $value ) { + + MediaWikiPHPUnitCommand::$additionalOptions[$offset] = $value; + + } + + public static function disableInterwikis( $prefix, &$data ) { + return false; + } +} + diff --git a/tests/phpunit/README b/tests/phpunit/README new file mode 100644 index 00000000..0a32ba17 --- /dev/null +++ b/tests/phpunit/README @@ -0,0 +1,53 @@ +== MediaWiki PHPUnit Tests == + +The unit tests for MediaWiki are implemented using the PHPUnit testing +framework and require PHPUnit to run. + + +=== WARNING === + +Some of the unit tests are DESTRUCTIVE and WILL ALTER YOUR WIKI'S CONTENTS. + +DO NOT RUN THESE TESTS ON A PRODUCTION SYSTEM OR ON ANY SYSTEM WHERE YOU NEED +TO RETAIN YOUR DATA. + + +== Installation == + +If PHPUnit is not installed, follow the installation instructions in the +PHPUnit Manual at: + + http://www.phpunit.de/manual/current/en/installation.html + +- or - + +On Unix-like operating systems, run: + + make install + + +== Running tests == + +The tests are run from your operating system's command line. + +Ensure that you are in the tests/phpunit directory of your MediaWiki +installation. + + +On Unix-like operating systems, the tests runs are controlled with a makefile. +Run command: + + make help + +for a full list of options for running tests. + + +On Windows-family operating systems, run the 'run-tests.bat' batch file. + + +=== Writing tests === + +A guide to writing unit tests for MediaWiki can be found at: + + http://mediawiki.org/wiki/Unit_Testing + diff --git a/tests/phpunit/TODO b/tests/phpunit/TODO new file mode 100644 index 00000000..b2fa7fb6 --- /dev/null +++ b/tests/phpunit/TODO @@ -0,0 +1,10 @@ +== Things To Do == +* 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. diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php new file mode 100644 index 00000000..b023fdcf --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,32 @@ +<?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; +} + +/** @todo Check if this is really needed */ +MessageCache::destroyInstance(); diff --git a/tests/phpunit/data/media/1bit-png.png b/tests/phpunit/data/media/1bit-png.png Binary files differnew file mode 100644 index 00000000..254e403a --- /dev/null +++ b/tests/phpunit/data/media/1bit-png.png diff --git a/tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.png b/tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.png Binary files differnew file mode 100644 index 00000000..c2f45d90 --- /dev/null +++ b/tests/phpunit/data/media/Animated_PNG_example_bouncing_beach_ball.png diff --git a/tests/phpunit/data/media/Gtk-media-play-ltr.svg b/tests/phpunit/data/media/Gtk-media-play-ltr.svg new file mode 100644 index 00000000..fc22338a --- /dev/null +++ b/tests/phpunit/data/media/Gtk-media-play-ltr.svg @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd" [ + <!ATTLIST svg + xmlns:xlink CDATA #FIXED "http://www.w3.org/1999/xlink"> +]> +<!-- Created with Sodipodi ("http://www.sodipodi.com/") --> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" version="1" x="0.00000000" y="0.00000000" width="60.0000000" height="60.0000000" viewBox="0 0 256 256" id="svg548"> + <defs id="defs572"/> + <g style="font-size:12;stroke:#000000;" id="Layer_1"> + <path d="M 256 256 L 0 256 L 0 0 L 256 0 L 256 256 z " style="fill:none;stroke:none;" id="path550"/> + </g> + <g style="font-size:12;stroke:#000000;" id="Layer_2"> + <path d="M 35.159 8.29 C 32.18 10.01 30.329 13.216 30.329 16.656 L 30.329 245.539 C 30.329 248.978 32.179 252.184 35.158 253.902 C 38.138 255.623 41.839 255.623 44.817 253.904 L 243.037 139.463 C 246.016 137.742 247.867 134.537 247.867 131.098 C 247.867 127.658 246.016 124.452 243.037 122.731 L 44.818 8.29 C 41.839 6.57 38.138 6.57 35.159 8.29 z " style="opacity:0.2;stroke:none;" id="path552"/> + <path d="M 27.314 2.29 C 24.335 4.01 22.484 7.216 22.484 10.656 L 22.484 239.538 C 22.484 242.977 24.335 246.184 27.313 247.903 C 30.293 249.623 33.994 249.623 36.973 247.905 L 235.193 133.464 C 238.172 131.742 240.023 128.536 240.023 125.098 C 240.023 121.658 238.172 118.452 235.193 116.732 L 36.975 2.29 C 33.996 0.57 30.294 0.57 27.314 2.29 z " style="fill:#003399;stroke:none;" id="path553"/> + <path d="M 29.247 5.636 C 27.454 6.672 26.349 8.585 26.349 10.656 L 26.349 239.538 C 26.349 241.608 27.453 243.521 29.247 244.558 C 31.04 245.592 33.249 245.592 35.042 244.558 L 233.261 130.117 C 235.054 129.081 236.159 127.169 236.159 125.098 C 236.159 123.027 235.054 121.114 233.261 120.078 L 35.042 5.636 C 33.25 4.601 31.041 4.601 29.247 5.636 z " style="fill:#003399;stroke:none;" id="path554"/> + <path d="M 32.145 10.656 L 230.364 125.097 L 32.145 239.538 L 32.145 10.656 z " style="fill:#3399ff;stroke:none;" id="path555"/> + <linearGradient x1="109.971703" y1="8.70849991" x2="109.971703" y2="107.238800" id="XMLID_1_" gradientUnits="userSpaceOnUse" spreadMethod="pad"> + <stop style="stop-color:#ffffff;stop-opacity:1;" offset="0.00000000" id="stop557"/> + <stop style="stop-color:#3399ff;stop-opacity:1;" offset="1.00000000" id="stop558"/> + <a:midPointStop offset="0" style="stop-color:#FFFFFF" id="midPointStop559"/> + <a:midPointStop offset="0.5" style="stop-color:#FFFFFF" id="midPointStop560"/> + <a:midPointStop offset="1" style="stop-color:#3399FF" id="midPointStop561"/> + </linearGradient> + <path d="M 32.145 141.057 C 36.775 141.258 41.456 141.368 46.183 141.368 C 105.41 141.368 157.526 125.124 187.799 100.524 L 32.145 10.656 L 32.145 141.057 z " style="fill:url(#XMLID_1_);stroke:none;" id="path562"/> + <linearGradient x1="109.972198" y1="264.875000" x2="109.972198" y2="145.249298" id="XMLID_2_" gradientUnits="userSpaceOnUse" spreadMethod="pad"> + <stop style="stop-color:#ccffff;stop-opacity:1;" offset="0.00000000" id="stop564"/> + <stop style="stop-color:#3399ff;stop-opacity:1;" offset="1.00000000" id="stop565"/> + <a:midPointStop offset="0" style="stop-color:#CCFFFF" id="midPointStop566"/> + <a:midPointStop offset="0.5" style="stop-color:#CCFFFF" id="midPointStop567"/> + <a:midPointStop offset="1" style="stop-color:#3399FF" id="midPointStop568"/> + </linearGradient> + <path d="M 32.145 108.517 C 36.775 108.315 41.456 108.206 46.183 108.206 C 105.41 108.206 157.526 124.451 187.799 149.05 L 32.145 238.916 L 32.145 108.517 z " style="fill:url(#XMLID_2_);stroke:none;" id="path569"/> + <path d="M 37.145 19.316 C 36.526 19.673 36.145 20.334 36.145 21.048 L 36.145 162.69 C 36.145 163.768 36.999 198.629 38.077 198.667 C 39.154 198.703 40.41 48.03 48.066 40.375 C 55.722 32.72 212.492 122.951 213 122 C 213.507 121.049 186.703 104.509 185.77 103.97 L 39.145 19.316 C 38.526 18.959 37.764 18.959 37.145 19.316 z " style="opacity:0.5;fill:#ffffff;stroke:none;" id="path570"/> + </g> +</svg>
\ No newline at end of file diff --git a/tests/phpunit/data/media/Png-native-test.png b/tests/phpunit/data/media/Png-native-test.png Binary files differnew file mode 100644 index 00000000..a0b81ca9 --- /dev/null +++ b/tests/phpunit/data/media/Png-native-test.png diff --git a/tests/phpunit/data/media/QA_icon.svg b/tests/phpunit/data/media/QA_icon.svg new file mode 100644 index 00000000..6b5d86e4 --- /dev/null +++ b/tests/phpunit/data/media/QA_icon.svg @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg:svg xmlns:svg="http://www.w3.org/2000/svg" version="1.0" width="60" height="60" viewBox="0 0 128 128" id="svg548"> + <svg:defs id="defs601"> + <svg:linearGradient id="linearGradient2802"> + <svg:stop style="stop-color:#1d12aa;stop-opacity:1" offset="0" id="stop2804"/> + <svg:stop style="stop-color:#8b12aa;stop-opacity:0" offset="1" id="stop2806"/> + </svg:linearGradient> + <svg:linearGradient id="linearGradient2812"> + <svg:stop style="stop-color:#1d25aa;stop-opacity:1" offset="0" id="stop2814"/> + <svg:stop style="stop-color:#8b12aa;stop-opacity:0" offset="1" id="stop2816"/> + </svg:linearGradient> + <svg:marker refX="0" refY="0" orient="auto" style="overflow:visible" id="Arrow1Lstart"> + <svg:path d="M 0,0 L 5,-5 L -12.5,0 L 5,5 L 0,0 z " transform="scale(0.8)" style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;marker-start:none" id="path2991"/> + </svg:marker> + <svg:linearGradient id="linearGradient4766"> + <svg:stop style="stop-color:#0447ff;stop-opacity:1" offset="0" id="stop4768"/> + <svg:stop style="stop-color:#000000;stop-opacity:0" offset="1" id="stop4770"/> + </svg:linearGradient> + <svg:linearGradient x1="55.4272" y1="102.1953" x2="55.4272" y2="-7.1773" id="XMLID_1_" gradientUnits="userSpaceOnUse" gradientTransform="translate(0, -0.496766)" spreadMethod="pad"> + <svg:stop style="stop-color:#7c74ff;stop-opacity:1" offset="0" id="stop556"/> + <svg:stop style="stop-color:#b3caff;stop-opacity:1" offset="0.41010001" id="stop557"/> + <svg:stop style="stop-color:#dfeaff;stop-opacity:1" offset="0.8258" id="stop558"/> + <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop559"/> + <midPointStop offset="0" style="stop-color:#7C74FF" id="midPointStop560"/> + <midPointStop offset="0.5" style="stop-color:#7C74FF" id="midPointStop561"/> + <midPointStop offset="0.4101" style="stop-color:#B3CAFF" id="midPointStop562"/> + <midPointStop offset="0.5" style="stop-color:#B3CAFF" id="midPointStop563"/> + <midPointStop offset="0.8258" style="stop-color:#DFEAFF" id="midPointStop564"/> + <midPointStop offset="0.5" style="stop-color:#DFEAFF" id="midPointStop565"/> + <midPointStop offset="1" style="stop-color:#FFFFFF" id="midPointStop566"/> + </svg:linearGradient> + <svg:linearGradient x1="54.7607" y1="7.2758999" x2="54.7607" y2="57.487301" id="XMLID_2_" gradientUnits="userSpaceOnUse" spreadMethod="pad"> + <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="0" id="stop569"/> + <svg:stop style="stop-color:#b3caff;stop-opacity:1" offset="1" id="stop570"/> + <midPointStop offset="0" style="stop-color:#FFFFFF" id="midPointStop571"/> + <midPointStop offset="0.5" style="stop-color:#FFFFFF" id="midPointStop572"/> + <midPointStop offset="1" style="stop-color:#B3CAFF" id="midPointStop573"/> + </svg:linearGradient> + <svg:linearGradient x1="83.637703" y1="119.3457" x2="83.637703" y2="42.033901" id="XMLID_3_" gradientUnits="userSpaceOnUse" spreadMethod="pad"> + <svg:stop style="stop-color:#006dff;stop-opacity:1" offset="0" id="stop577"/> + <svg:stop style="stop-color:#94caff;stop-opacity:1" offset="0.41010001" id="stop578"/> + <svg:stop style="stop-color:#dcf0ff;stop-opacity:1" offset="0.8258" id="stop579"/> + <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop580"/> + <midPointStop offset="0" style="stop-color:#006DFF" id="midPointStop581"/> + <midPointStop offset="0.5" style="stop-color:#006DFF" id="midPointStop582"/> + <midPointStop offset="0.4101" style="stop-color:#94CAFF" id="midPointStop583"/> + <midPointStop offset="0.5" style="stop-color:#94CAFF" id="midPointStop584"/> + <midPointStop offset="0.8258" style="stop-color:#DCF0FF" id="midPointStop585"/> + <midPointStop offset="0.5" style="stop-color:#DCF0FF" id="midPointStop586"/> + <midPointStop offset="1" style="stop-color:#FFFFFF" id="midPointStop587"/> + </svg:linearGradient> + <svg:linearGradient x1="265.11331" y1="52.250999" x2="265.11331" y2="87.743599" id="XMLID_4_" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1, 0, 0, 1, 349, 0)" spreadMethod="pad"> + <svg:stop style="stop-color:#ffffff;stop-opacity:1" offset="0" id="stop590"/> + <svg:stop style="stop-color:#94caff;stop-opacity:1" offset="1" id="stop591"/> + <midPointStop offset="0" style="stop-color:#FFFFFF" id="midPointStop592"/> + <midPointStop offset="0.5" style="stop-color:#FFFFFF" id="midPointStop593"/> + <midPointStop offset="1" style="stop-color:#94CAFF" id="midPointStop594"/> + </svg:linearGradient> + </svg:defs> + <svg:g style="font-size:12px;stroke:#000000" id="Layer_2"> + <svg:path d="M 128,128 L 0,128 L 0,0 L 128,0 L 128,128 z " style="fill:none;stroke:none" id="path550"/> + </svg:g> + <svg:g style="font-size:12px;stroke:#000000" id="Layer_1"/> + <svg:path d="M 9.041,92.189 C 9.041,92.189 21.955,85.393 30.11,67.382 L 52.198,76.897 C 52.198,76.897 46.422,92.189 9.041,92.189 z " style="font-size:12px;fill:#00008d;stroke:none" id="path553"/> + <svg:path d="M 1.905,49.712 C 1.905,70.733 25.867,87.773 55.427,87.773 C 84.987,87.773 108.949,70.733 108.949,49.712 C 108.949,28.692 84.987,11.651 55.427,11.651 C 25.867,11.651 1.905,28.692 1.905,49.712 z " style="font-size:12px;fill:#00008d;stroke:none" id="path554"/> + <svg:path d="M 55.427,13.193234 C 27.039,13.193234 3.943,29.352234 3.943,49.214234 C 3.943,61.333234 12.55,72.067234 25.703,78.598234 C 22.202,83.521234 18.6,87.075234 15.722,89.464234 C 27.71,88.800234 35.664,86.388234 40.883,83.762234 C 45.498,84.716234 50.377,85.236234 55.427,85.236234 C 83.815,85.236234 106.91,69.077234 106.91,49.215234 C 106.91,29.353234 83.815,13.193234 55.427,13.193234 z " style="font-size:12px;fill:url(#XMLID_1_);stroke:none" id="path567"/> + <svg:path d="M 12.999,35.282 C 30.044,44.81 49.474,47.149 69.356,41.962 C 73.46,40.821 77.627,39.436 81.656,38.096 C 86.51,36.482 91.504,34.846 96.524,33.573 C 88.559,23.302 72.888,16.748 55.428,16.748 C 37.091,16.749 20.396,24.128 12.999,35.282 z " style="font-size:12px;fill:url(#XMLID_2_);stroke:none" id="path574"/> + <svg:text x="32.487015" y="68.006958" style="font-size:48px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Microsoft Sans Serif" id="text2303" xml:space="preserve"><svg:tspan x="32.487015" y="68.006958" style="font-size:64px;font-style:normal;font-weight:bold;fill:#000000;fill-opacity:1;font-family:sans" id="tspan2305">?</svg:tspan></svg:text> + <svg:path d="M 42.401,82.248 C 42.401,98.96 60.9,112.557 83.638,112.557 C 86.794,112.557 90.053,112.231 93.329,111.651 C 98.255,113.817 104.317,115.142 111.437,115.538 L 126.095,116.35 L 114.798,106.972 C 114.067,106.367 113.056,105.441 111.922,104.237 C 120.165,98.531 124.875,90.66 124.875,82.25 C 124.875,65.538 106.376,51.942 83.637,51.942 C 60.9,51.94 42.401,65.536 42.401,82.248 z " style="font-size:12px;fill:#0032a4;stroke:none" id="path575"/> + <svg:path d="M 44.823,82.248 C 44.823,97.624 62.236,110.133 83.637,110.133 C 87.009,110.133 90.357,109.784 93.616,109.163 C 98.327,111.368 104.334,112.717 111.571,113.118 L 118.9,113.524 L 113.251,108.835 C 111.96,107.763 110.162,106.071 108.268,103.754 C 117.176,98.487 122.454,90.629 122.454,82.248 C 122.454,66.871 105.042,54.363 83.639,54.363 C 62.236,54.363 44.823,66.871 44.823,82.248 z " style="font-size:12px;fill:url(#XMLID_3_);stroke:none" id="path588"/> + <svg:path d="M 83.638,57.505 C 98.257,57.505 110.759,63.777 115.655,72.576 C 102.935,80.147 88.183,82.013 73.429,78.165 C 66.228,76.163 59.247,73.276 52.12,71.719 C 57.332,63.374 69.498,57.505 83.638,57.505 z " style="font-size:12px;fill:url(#XMLID_4_);stroke:none" id="path595"/> + <svg:g transform="matrix(1.38561, 0, 0, 1.38561, -32.2514, -30.5491)" id="g4248"> + <svg:path d="M 103.21356 24.205935 A 24.311146 23.627199 0 1 1 54.591267,24.205935 A 24.311146 23.627199 0 1 1 103.21356 24.205935 z" transform="matrix(0.148134, 0, 0, 0.152972, 71.9504, 64.0705)" style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:6.64218044;stroke-linecap:square;marker-start:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" id="path3881"/> + <svg:path d="M 87.539971,77.627004 C 87.539971,78.31674 87.591107,91.916423 87.60756,94.355578 C 87.61771,95.860439 89.879004,95.050778 90.026509,95.980703 C 90.2785,97.569343 86.888685,97.025111 86.718511,97.01762 C 85.743882,96.974724 82.425764,97.036144 81.376943,97.036144 C 81.101002,97.036144 77.578516,97.69007 77.314172,96.309196 C 77.071189,95.039902 79.49446,95.29146 79.833236,94.380195 C 81.070282,91.052684 81.154686,84.029315 80.322646,79.891188 C 79.902772,77.802954 76.928763,78.363984 77.263297,76.859643 C 77.479369,75.888015 78.579837,75.778912 79.35102,75.513942 C 81.049574,74.930337 83.123826,75.068206 84.579101,74.012707 C 86.187481,72.846162 87.539971,75.631913 87.539971,77.627004 z " style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.99986994;stroke-linecap:square;marker-start:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" id="path4774"/> + </svg:g> +</svg:svg>
\ No newline at end of file diff --git a/tests/phpunit/data/media/README b/tests/phpunit/data/media/README new file mode 100644 index 00000000..fe3bc682 --- /dev/null +++ b/tests/phpunit/data/media/README @@ -0,0 +1,38 @@ +This directory contains media files for use with the +tests in includes/media directory. + +Image credits: + +QA_icon.svg: +http://es.wikipedia.org/wiki/Archivo:QA_icon.svg +GNU Lesser General Public License +~~helix84 (16.4.2007), Philverney (6.12.2005) David Vignoni + +Gtk-media-play-ltr.svg +http://commons.wikimedia.org/wiki/File:Gtk-media-play-ltr.svg +GNU Lesser General Public License +http://ftp.gnome.org/pub/GNOME/sources/gnome-themes-extras/0.9/gnome-themes-extras-0.9.0.tar.gz +David Vignoni + +US_states_by_total_state_tax_revenue.svg +http://commons.wikimedia.org/wiki/File:US_states_by_total_state_tax_revenue.svg +CC-BY 3.0 +TastyCakes on English Wikipedia + +greyscale-na-png.png, rgb-png.png, Xmp-exif-multilingual_test.jpg +greyscale-png.png, 1bit-png.png, Png-native-test.png, rgb-na-png.png, +test.tiff, test.jpg, jpeg-comment-multiple.jpg, jpeg-comment-utf.jpg, +jpeg-comment-iso8859-1.jpg, jpeg-comment-binary.jpg, jpeg-xmp-psir.jpg, +jpeg-xmp-alt.jpg, animated.gif, exif-user-comment.jpg, animated-xmp.gif, +iptc-timetest-invalid.jpg, jpeg-iptc-bad-hash.jpg, iptc-timetest.jpg, +xmp.png, nonanimated.gif, exif-gps.jpg, jpeg-xmp-psir.xmp, jpeg-iptc-good-hash.jpg, +jpeg-padding-even.jpg, jpeg-padding-odd.jpg +Are all by Bawolff. I don't think they contain enough originality to +claim copyright, but on the off chance they do, feel free to use them +however you feel fit, without restriction. + +Animated_PNG_example_bouncing_beach_ball.png +http://commons.wikimedia.org/wiki/File:Animated_PNG_example_bouncing_beach_ball.png (originally http://www.treebuilder.de/default.asp?file=89031.xml ) +Public Domain +Holger Will + diff --git a/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg b/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg new file mode 100644 index 00000000..9afea859 --- /dev/null +++ b/tests/phpunit/data/media/US_states_by_total_state_tax_revenue.svg @@ -0,0 +1,248 @@ +<ns0:svg height="592.78998" id="svg2275" version="1.0" width="958.69" ns1:docbase="C:\Users\Adam\Desktop" ns1:docname="Blank_US_Map_with_borders.svg" ns1:version="0.32" ns2:output_extension="org.inkscape.output.svg.inkscape" ns2:version="0.46" xmlns:ns0="http://www.w3.org/2000/svg" xmlns:ns1="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:ns2="http://www.inkscape.org/namespaces/inkscape"> + <ns0:metadata id="metadata2625"> + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <ns4:Work rdf:about="" xmlns:ns4="http://creativecommons.org/ns#"> + <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format> + <ns5:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" xmlns:ns5="http://purl.org/dc/elements/1.1/" /> + </ns4:Work> + </rdf:RDF> + </ns0:metadata> + <ns0:defs id="defs2623"> + <ns2:perspective id="perspective226" ns1:type="inkscape:persp3d" ns2:persp3d-origin="479.345 : 197.59666 : 1" ns2:vp_x="0 : 296.39499 : 1" ns2:vp_y="0 : 1000 : 0" ns2:vp_z="958.69 : 296.39499 : 1" /> + </ns0:defs> + <ns1:namedview bordercolor="#666666" borderopacity="1.0" gridtolerance="10.0" guidetolerance="10.0" id="base" objecttolerance="10.0" pagecolor="#ffffff" showgrid="false" ns2:current-layer="svg2275" ns2:cx="479.345" ns2:cy="299.1307" ns2:pageopacity="0.0" ns2:pageshadow="2" ns2:window-height="820" ns2:window-width="1440" ns2:window-x="-8" ns2:window-y="-8" ns2:zoom="0.99941554" /> + <ns0:path d="M 798.49579,591.98217 L 799.73403,593.07468 L 802.54072,590.88965 L 807.98899,586.51962 L 811.78627,582.48573 L 814.3453,575.59451 L 815.3359,573.82969 L 815.501,570.30004 L 814.75805,570.80427 L 813.76746,573.74564 L 812.28156,578.53588 L 808.97958,583.99844 L 804.52191,588.36847 L 801.05483,590.38542 L 798.49579,591.98217 z M 784.71002,597.19259 L 787.18651,596.52028 L 788.5073,596.26817 L 789.99319,593.83102 L 792.38713,592.15024 L 793.70792,592.65448 L 795.44146,592.99063 L 795.8542,594.08315 L 792.30458,595.34374 L 788.012,596.85644 L 785.61807,598.11703 L 784.71002,597.19259 z M 657.3254,482.07418 L 660.96585,481.47149 L 667.07449,479.28647 L 673.18314,478.78224 L 677.6408,478.10993 L 685.40042,479.95879 L 693.65535,483.99266 L 695.30633,485.50536 L 698.2781,486.6819 L 699.92909,488.69884 L 700.25929,491.55616 L 703.56126,490.21154 L 707.52362,490.21154 L 711.15578,488.1946 L 714.95305,484.49689 L 718.08992,484.66497 L 718.58522,483.48842 L 717.75972,482.47995 L 717.92482,480.46302 L 722.05228,479.62263 L 724.69386,479.62263 L 727.66563,481.13533 L 731.95819,482.64803 L 734.43467,486.51382 L 737.24134,487.52229 L 738.39703,491.05193 L 741.8641,492.73271 L 743.51508,495.42195 L 745.49627,496.09427 L 750.77942,497.43889 L 752.1002,500.63237 L 755.23708,504.49816 L 755.23708,514.41476 L 753.75119,519.28902 L 754.08139,522.14634 L 755.40217,527.18868 L 757.21826,531.39063 L 758.04375,530.8864 L 759.52964,526.18021 L 756.88806,525.17175 L 756.55786,524.49943 L 758.20885,523.82712 L 762.83161,524.83559 L 762.9967,526.51637 L 759.69473,532.23102 L 757.54845,534.75219 L 761.18062,538.61798 L 763.8222,541.81146 L 766.79397,547.35803 L 769.76574,551.3919 L 771.91202,556.60232 L 773.7281,556.93847 L 775.37909,554.75346 L 777.19517,555.93001 L 779.83675,560.13195 L 780.49714,563.82967 L 783.63401,568.36777 L 784.4595,567.02315 L 788.42187,567.3593 L 792.05403,569.7124 L 795.5211,575.09089 L 796.34659,578.62053 L 796.67679,581.64593 L 797.83248,582.6544 L 799.15327,583.15863 L 801.62975,582.15016 L 803.11563,580.46938 L 807.078,580.3013 L 810.21487,578.7886 L 813.02154,575.42704 L 812.52624,573.41011 L 812.19605,570.88894 L 812.85644,568.87201 L 812.52624,566.85507 L 815.00272,565.51045 L 815.33292,561.98081 L 814.67252,560.13195 L 814.17723,547.69419 L 812.85644,539.79453 L 808.23368,531.22255 L 804.60152,525.17175 L 801.95994,519.62517 L 798.98817,516.59977 L 796.0164,508.86819 L 796.84189,507.52356 L 797.99758,506.17894 L 796.34659,503.15354 L 792.21913,499.28775 L 787.26618,493.5731 L 783.46891,487.01806 L 778.02066,477.26954 L 774.21165,467.14054 L 771.56179,458.12552" id="FL_Gulf" style="fill:#cccccc;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 772.07835,458.61245 L 774.21165,467.14054 L 778.02066,477.26954 L 783.46891,487.01806 L 787.26618,493.5731 L 792.21913,499.28775 L 796.34659,503.15354 L 797.99758,506.17894 L 796.84189,507.52356 L 796.0164,508.86819 L 798.98817,516.59977 L 801.95994,519.62517 L 804.60152,525.17175 L 808.23368,531.22255 L 812.85644,539.79453 L 814.17723,547.69419 L 814.67252,560.13195 L 815.33292,561.98081 L 815.00272,565.51045 L 812.52624,566.85507 L 812.85644,568.87201 L 812.19605,570.88894 L 812.52624,573.41011 L 813.02154,575.42704 L 810.21487,578.7886 L 807.078,580.3013 L 803.11563,580.46938 L 801.62975,582.15016 L 799.15327,583.15863 L 797.83248,582.6544 L 796.67679,581.64593 L 796.34659,578.62053 L 795.5211,575.09089 L 792.05403,569.7124 L 788.42187,567.3593 L 784.4595,567.02315 L 783.63401,568.36777 L 780.49714,563.82967 L 779.83675,560.13195 L 777.19517,555.93001 L 775.37909,554.75346 L 773.7281,556.93847 L 771.91202,556.60232 L 769.76574,551.3919 L 766.79397,547.35803 L 763.8222,541.81146 L 761.18062,538.61798 L 757.54845,534.75219 L 759.69473,532.23102 L 762.9967,526.51637 L 762.83161,524.83559 L 758.20885,523.82712 L 756.55786,524.49943 L 756.88806,525.17175 L 759.52964,526.18021 L 758.04375,530.8864 L 757.21826,531.39063 L 755.40217,527.18868 L 754.08139,522.14634 L 753.75119,519.28902 L 755.23708,514.41476 L 755.23708,504.49816 L 752.1002,500.63237 L 750.77942,497.43889 L 745.49627,496.09427 L 743.51508,495.42195 L 741.8641,492.73271 L 738.39703,491.05193 L 737.24134,487.52229 L 734.43467,486.51382 L 731.95819,482.64803 L 727.66563,481.13533 L 724.69386,479.62263 L 722.05228,479.62263 L 717.92482,480.46302 L 717.75972,482.47995 L 718.58522,483.48842 L 718.08992,484.66497 L 714.95305,484.49689 L 711.15578,488.1946 L 707.52362,490.21154 L 703.56126,490.21154 L 700.25929,491.55616 L 699.92909,488.69884 L 698.2781,486.6819 L 695.30633,485.50536 L 693.65535,483.99266 L 685.40042,479.95879 L 677.6408,478.10993 L 673.18314,478.78224 L 667.07449,479.28647 L 660.96585,481.47149 L 657.41261,482.10878 L 657.16963,473.73948 L 654.52806,471.72255 L 652.71197,469.87369 L 653.04217,466.6802 L 663.44338,465.33558 L 689.52898,462.31017 L 696.46313,461.63786 L 702.73688,461.46977 L 705.37846,465.50365 L 706.86434,467.01635 L 714.95419,467.18443 L 726.00404,466.51212 L 747.97393,465.1675 L 753.53546,464.46636 L 758.21005,464.49519 L 758.37515,467.52059 L 761.01673,468.36098 L 761.34692,463.82287 L 759.69594,459.11668 L 760.85163,457.4359 L 766.79518,458.27629 L 772.07835,458.61245 z M 784.71002,597.19259 L 787.18651,596.52028 L 788.5073,596.26817 L 789.99319,593.83102 L 792.38713,592.15024 L 793.70792,592.65448 L 795.44146,592.99063 L 795.8542,594.08315 L 792.30458,595.34374 L 788.012,596.85644 L 785.61807,598.11703 L 784.71002,597.19259 z M 798.49579,591.98217 L 799.73403,593.07468 L 802.54072,590.88965 L 807.98899,586.51962 L 811.78627,582.48573 L 814.3453,575.59451 L 815.3359,573.82969 L 815.501,570.30004 L 814.75805,570.80427 L 813.76746,573.74564 L 812.28156,578.53588 L 808.97958,583.99844 L 804.52191,588.36847 L 801.05483,590.38542 L 798.49579,591.98217 z " id="FL" style="fill:#501616" /> + <ns0:path d="M 777.85557,425.66962 L 776.04071,426.6776 L 773.39913,425.33297 L 772.73874,423.14795 L 771.41795,419.45024 L 769.10656,417.26521 L 766.46499,416.5929 L 764.814,411.55057 L 762.00732,405.33167 L 757.71476,403.31473 L 755.56847,401.29779 L 754.24768,398.60854 L 752.1014,396.5916 L 749.79002,395.24697 L 747.47863,392.22157 L 744.34176,389.86848 L 739.71899,388.01961 L 739.2237,386.50691 L 736.74722,383.48151 L 736.25192,381.9688 L 732.78485,376.5903 L 729.31778,376.75838 L 725.19031,374.2372 L 723.86952,372.89258 L 723.53932,371.04372 L 724.36481,369.02679 L 726.67619,368.01831 L 726.346,365.8333 L 732.61975,363.14405 L 741.86527,358.43786 L 749.29471,357.59747 L 766.13479,357.09323 L 768.44617,359.11017 L 770.09715,362.47174 L 774.55482,361.9675 L 787.43252,360.45479 L 790.4043,361.29519 L 803.282,369.19486 L 813.60504,377.63896 L 808.06859,383.31398 L 805.42701,389.70094 L 804.93171,396.25598 L 803.28073,397.09637 L 802.12504,399.95369 L 799.64856,400.62601 L 797.50228,404.32372 L 794.69561,407.18104 L 792.38423,410.71068 L 790.73325,411.55107 L 787.10108,415.08071 L 784.12931,415.24879 L 785.1199,418.61034 L 780.00185,424.32499 L 777.85557,425.66962 z " id="SC" style="fill:#e9afaf" /> + <ns0:path d="M 704.71806,368.52255 L 699.7651,369.36294 L 691.17997,370.53949 L 682.42974,371.46391 L 682.42974,373.73297 L 682.59484,375.91799 L 683.25523,379.44763 L 686.72231,387.68346 L 689.19879,397.93623 L 690.68467,404.3232 L 692.33566,409.36554 L 693.82155,416.5929 L 695.96784,423.14795 L 698.60941,426.6776 L 699.10471,430.20724 L 701.0859,431.04763 L 701.251,433.23265 L 699.4349,438.27499 L 698.93961,441.63656 L 698.77451,443.65349 L 700.4255,448.19161 L 700.7557,453.73818 L 699.9302,456.25936 L 700.5906,457.09975 L 702.07649,457.94014 L 702.73688,461.46977 L 705.37846,465.50365 L 706.86434,467.01635 L 714.95419,467.18443 L 726.00404,466.51212 L 747.97393,465.1675 L 753.53546,464.46636 L 758.21005,464.49519 L 758.37515,467.52059 L 761.01673,468.36098 L 761.34692,463.82287 L 759.69594,459.11668 L 760.85163,457.4359 L 766.79518,458.27629 L 771.87844,458.60669 L 771.08653,452.05785 L 773.39791,441.63702 L 774.88379,437.26699 L 774.3885,434.57775 L 778.51596,427.3504 L 777.90454,425.66937 L 776.04071,426.6776 L 773.39913,425.33297 L 772.73874,423.14795 L 771.41795,419.45024 L 769.10656,417.26521 L 766.46499,416.5929 L 764.814,411.55057 L 762.00732,405.33167 L 757.71476,403.31473 L 755.56847,401.29779 L 754.24768,398.60854 L 752.1014,396.5916 L 749.79002,395.24697 L 747.47863,392.22157 L 744.34176,389.86848 L 739.71899,388.01961 L 739.2237,386.50691 L 736.74722,383.48151 L 736.25192,381.9688 L 732.78485,376.5903 L 729.31778,376.75838 L 725.19031,374.2372 L 723.86952,372.89258 L 723.53932,371.04372 L 724.36481,369.02679 L 726.67619,368.01831 L 726.51109,365.64481 L 724.69501,366.16945 L 718.75146,367.17792 L 711.65221,368.01831 L 704.71806,368.52255 z " id="GA" style="fill:#d35f5f" /> + <ns0:path d="M 639.33795,481.63956 L 637.68799,465.83981 L 634.88131,446.34274 L 635.04641,431.71994 L 635.8719,399.44893 L 635.7068,382.13688 L 635.87539,375.46299 L 643.79664,375.07759 L 672.19362,372.38834 L 682.58068,371.46391 L 682.42974,373.73297 L 682.59484,375.91799 L 683.25523,379.44763 L 686.72231,387.68346 L 689.19879,397.93623 L 690.68467,404.3232 L 692.33566,409.36554 L 693.82155,416.5929 L 695.96784,423.14795 L 698.60941,426.6776 L 699.10471,430.20724 L 701.0859,431.04763 L 701.251,433.23265 L 699.4349,438.27499 L 698.93961,441.63656 L 698.77451,443.65349 L 700.4255,448.19161 L 700.7557,453.73818 L 699.9302,456.25936 L 700.5906,457.09975 L 702.07649,457.94014 L 702.90198,461.63786 L 696.46313,461.63786 L 689.52898,462.31017 L 663.44338,465.33558 L 653.04217,466.6802 L 652.71197,469.87369 L 654.52806,471.72255 L 657.16963,473.73948 L 657.76284,481.98993 L 651.05994,484.66497 L 648.25327,484.32881 L 651.05994,482.31188 L 651.05994,481.30341 L 647.92307,475.08453 L 645.61169,474.41221 L 644.12581,478.95032 L 642.80502,481.80764 L 642.14462,481.63956 L 639.33795,481.63956 z " id="AL" style="fill:#e9afaf" /> + <ns0:path d="M 850.23842,306.65958 L 851.98478,311.54471 L 855.61694,318.26782 L 858.09342,320.78899 L 858.75382,323.14208 L 856.27734,323.31016 L 857.10283,323.98247 L 856.77263,328.3525 L 854.13106,329.69712 L 853.47066,331.88214 L 852.14988,334.90754 L 848.35261,336.58832 L 845.87614,336.25216 L 844.39025,336.08408 L 842.73926,334.73946 L 843.06946,336.08408 L 843.06946,337.09255 L 845.05064,337.09255 L 845.87614,338.43717 L 843.89495,344.99221 L 848.18751,344.99221 L 848.84791,346.67299 L 851.15929,344.3199 L 852.48007,343.81567 L 850.49889,347.51338 L 847.36202,352.55572 L 846.04123,352.55572 L 844.88554,352.05149 L 842.07887,352.7238 L 836.79572,355.24497 L 830.19178,360.79154 L 826.72471,365.6658 L 824.74353,372.38892 L 824.24824,374.91008 L 819.46038,375.41432 L 813.43993,377.723 L 803.282,369.19486 L 790.4043,361.29519 L 787.43252,360.45479 L 774.55482,361.9675 L 770.09715,362.47174 L 768.44617,359.11017 L 766.13479,357.09323 L 749.29471,357.59747 L 741.86527,358.43786 L 732.61975,363.14405 L 726.346,365.8333 L 724.69501,366.16945 L 718.75146,367.17792 L 711.65221,368.01831 L 704.71806,368.52255 L 705.04826,363.4802 L 706.86434,361.9675 L 709.67103,361.29519 L 710.33142,357.42939 L 714.62399,354.57206 L 718.58636,353.05935 L 722.87893,349.36164 L 727.33659,347.17662 L 727.99698,343.98313 L 731.95935,339.94926 L 732.61975,339.78119 C 732.61975,339.78119 732.61975,340.95773 733.44524,340.95773 C 734.27073,340.95773 735.42643,341.29389 735.42643,341.29389 L 737.73781,337.59616 L 739.88409,336.92385 L 742.19547,337.26001 L 743.84646,333.56229 L 746.81824,330.87303 L 747.31353,328.68802 L 747.31353,324.57011 L 751.9363,325.32646 L 759.22415,323.98183 L 775.38031,321.96489 L 792.88078,319.27565 L 813.92151,315.35219 L 833.49506,311.37597 L 845.21707,308.35056 L 850.23842,306.65958 z M 854.21672,340.95692 L 856.85831,338.3517 L 860.07773,335.66244 L 861.64617,334.99013 L 861.81127,332.88915 L 861.15088,326.50217 L 859.66499,324.06503 L 859.00459,322.13213 L 859.74753,321.88001 L 862.55422,327.59468 L 862.96697,332.21684 L 862.80187,335.74649 L 859.33479,337.34323 L 856.44555,339.86441 L 855.28987,341.125 L 854.21672,340.95692 z " id="NC" style="fill:#c83737" /> + <ns0:path d="M 712.3126,329.69649 L 659.31592,334.90691 L 643.2212,336.75577 L 638.50172,337.28883 L 634.55111,337.26001 L 634.55111,341.29389 L 625.96598,341.79812 L 618.86673,342.47043 L 607.53473,342.52544 L 607.26436,348.59252 L 605.08072,355.11718 L 604.06449,358.25292 L 602.68706,362.80789 L 602.35687,365.49714 L 598.22939,367.85023 L 599.71528,371.54796 L 598.72469,376.08606 L 597.15628,377.85089 L 605.49374,377.76685 L 630.09345,375.74991 L 635.54175,375.58184 L 643.79664,375.07759 L 672.19362,372.38834 L 682.58068,371.54796 L 691.17997,370.53949 L 699.7651,369.36294 L 704.71806,368.52255 L 705.04826,363.4802 L 706.86434,361.9675 L 709.67103,361.29519 L 710.33142,357.42939 L 714.62399,354.57206 L 718.58636,353.05935 L 722.87893,349.36164 L 727.33659,347.17662 L 727.99698,343.98313 L 731.95935,339.94926 L 732.61975,339.78119 C 732.61975,339.78119 732.61975,340.95773 733.44524,340.95773 C 734.27073,340.95773 735.42643,341.29389 735.42643,341.29389 L 737.73781,337.59616 L 739.88409,336.92385 L 742.19547,337.26001 L 743.84646,333.56229 L 746.81824,330.87303 L 747.31353,328.68802 L 747.49366,324.59981 L 745.16725,324.65414 L 742.69078,326.67109 L 734.60093,326.83916 L 722.3505,328.8153 L 712.3126,329.69649 z " id="TN" style="fill:#de8787" /> + <ns0:path d="M 893.09433,183.30123 L 892.6011,178.92994 L 891.77561,174.39182 L 890.04208,168.25697 L 895.90308,166.66023 L 897.55407,167.83677 L 901.02115,172.37489 L 903.99187,176.99768 L 901.01902,178.59507 L 899.69824,178.42699 L 898.54255,180.27585 L 896.06607,182.29279 L 893.09433,183.30123 z " id="RI" style="fill:#f4d7d7" /> + <ns0:path d="M 893.58963,183.30123 L 892.6011,178.92994 L 891.77561,174.39182 L 890.12463,168.17293 L 884.84146,169.34947 L 862.55312,174.30778 L 863.21351,177.75339 L 864.6994,185.31692 L 864.6994,193.72083 L 863.54371,196.07393 L 865.41508,198.26677 L 870.47581,194.73055 L 874.10797,191.36899 L 876.08916,189.18398 L 876.91465,189.85629 L 879.72132,188.34359 L 885.00447,187.16705 L 893.58963,183.30123 z " id="CT" style="fill:#de8787" /> + <ns0:path d="M 919.55232,177.09192 L 921.77043,176.37882 L 922.23741,174.59609 L 923.28809,174.71493 L 924.33877,177.09192 L 923.0546,177.56732 L 919.08535,177.68617 L 919.55232,177.09192 z M 909.97943,177.92387 L 912.31427,175.19033 L 913.94868,175.19033 L 915.81656,176.73537 L 913.36497,177.80501 L 911.14686,178.87466 L 909.97943,177.92387 z M 874.44023,155.06282 L 892.27091,150.69278 L 894.5823,150.02047 L 896.72858,146.65891 L 900.54482,144.92957 L 903.4955,149.51759 L 901.01902,154.89608 L 900.68883,156.40879 L 902.67001,159.09803 L 903.8257,158.25764 L 905.64178,158.25764 L 907.95316,160.94689 L 911.91552,167.16577 L 915.54769,167.67001 L 917.85907,166.66154 L 919.67515,164.81268 L 918.84966,161.95536 L 916.70338,160.27458 L 915.21749,161.11497 L 914.2269,159.77034 L 914.7222,159.26611 L 916.86848,159.09803 L 918.68456,159.93842 L 920.66574,162.45959 L 921.65633,165.48499 L 921.98653,168.00616 L 917.69397,169.51886 L 913.73161,171.5358 L 909.76924,176.24198 L 907.78806,177.75468 L 907.78806,176.74621 L 910.26454,175.23351 L 910.75983,173.38466 L 909.93434,170.19118 L 906.96257,171.70388 L 906.13708,173.21658 L 906.63237,175.56967 L 903.82678,177.08172 L 901.02115,172.37489 L 897.55407,167.83677 L 895.90308,166.66023 L 890.04208,168.25697 L 884.84146,169.34947 L 862.55312,174.30778 L 861.56253,168.34101 L 862.22292,157.33189 L 867.50608,156.40745 L 874.44023,155.06282" id="MA" style="fill:#c83737" /> + <ns0:path d="M 943.28423,76.73985 L 945.26541,78.924863 L 947.57679,82.790656 L 947.57679,84.807591 L 945.43051,89.68185 L 943.44933,90.354162 L 939.98226,93.547643 L 935.02931,99.262292 C 935.02931,99.262292 934.36891,99.262292 933.70852,99.262292 C 933.04813,99.262292 932.71793,97.077279 932.71793,97.077279 L 930.90185,97.245357 L 929.91126,98.758058 L 927.43478,100.27076 L 926.44419,101.78346 L 928.09517,103.29616 L 927.59988,103.96847 L 927.10458,106.8258 L 925.1234,106.65772 L 925.1234,104.97694 L 924.7932,103.63232 L 923.30732,103.96847 L 921.49123,100.60692 L 919.34495,101.95154 L 920.66574,103.46424 L 920.99594,104.64079 L 920.17045,105.98541 L 920.50064,109.17889 L 920.66574,110.85967 L 919.01476,113.54892 L 916.04298,114.05315 L 915.71279,117.07855 L 910.26454,120.27203 L 908.94375,120.77627 L 907.29277,119.26356 L 904.15589,122.96128 L 905.14649,126.32284 L 903.6606,127.66746 L 903.4955,132.20556 L 901.88477,140.12915 L 899.37016,138.9273 L 898.87486,135.73381 L 894.91249,134.55727 L 894.5823,131.69993 L 887.15284,107.32858 L 882.28553,91.967581 L 884.77927,91.608771 L 886.32526,92.034941 L 886.32526,89.345695 L 887.15075,83.631045 L 889.79233,78.756786 L 891.27821,74.554837 L 889.29703,72.033669 L 889.29703,65.814786 L 890.12252,64.806318 L 890.94802,61.948993 L 890.78292,60.436292 L 890.61782,55.393954 L 892.4339,50.351617 L 895.40568,41.107331 L 897.55196,36.737305 L 898.87274,36.737305 L 900.19353,36.905383 L 900.19353,38.081928 L 901.51432,40.435019 L 904.32099,41.107331 L 905.14649,40.266941 L 905.14649,39.258474 L 909.27395,36.233071 L 911.09003,34.384214 L 912.57592,34.552292 L 918.68456,37.073461 L 920.66574,38.081928 L 929.91126,69.176344 L 936.0199,69.176344 L 936.84539,71.193279 L 937.01049,76.235617 L 939.98226,78.588708 L 940.80775,78.588708 L 940.97285,78.084474 L 940.47756,76.907928 L 943.28423,76.73985 z M 921.90732,108.08415 L 923.47577,106.48741 L 924.87911,107.57992 L 925.45696,110.1011 L 923.72342,111.02553 L 921.90732,108.08415 z M 928.75894,101.94929 L 930.57502,103.88219 C 930.57502,103.88219 931.89582,103.96623 931.89582,103.63007 C 931.89582,103.29391 932.14346,101.52909 932.14346,101.52909 L 933.05151,100.6887 L 932.22602,98.839833 L 930.16228,99.596189 L 928.75894,101.94929 z " id="ME" style="fill:#f4d7d7" /> + <ns0:path d="M 900.54588,144.88986 L 900.85393,143.29871 L 901.96733,139.87704 L 899.37016,138.9273 L 898.87486,135.73381 L 894.91249,134.55727 L 894.5823,131.69993 L 887.15284,107.32858 L 882.45357,92.208279 L 881.5374,92.203019 L 880.87701,93.883799 L 880.21662,93.379565 L 879.22603,92.371097 L 877.74014,94.388032 L 876.76354,100.09176 L 877.08182,105.98396 L 879.063,108.84129 L 879.063,113.04325 L 875.26572,117.2452 L 872.62415,118.42176 L 872.62415,119.5983 L 873.77984,121.44716 L 873.77984,130.35531 L 872.95434,139.93577 L 872.78925,144.97812 L 873.77984,146.32275 L 873.61474,151.02894 L 873.11944,152.8778 L 874.60533,154.97877 L 892.27091,150.69278 L 894.5823,150.02047 L 896.72858,146.65891 L 900.54588,144.88986 z " id="NH" style="fill:#f4d7d7" /> + <ns0:path d="M 862.38802,157.584 L 861.56253,151.70126 L 858.42565,140.27193 L 857.76525,139.93577 L 854.79347,138.59115 L 855.61896,135.56574 L 854.79347,133.38072 L 852.1519,128.67453 L 853.14249,124.64065 L 852.31699,119.26214 L 849.84051,112.53901 L 849.0178,107.42109 L 876.75058,99.933872 L 877.08182,105.98396 L 879.063,108.84129 L 879.063,113.04325 L 875.26572,117.2452 L 872.62415,118.42176 L 872.62415,119.5983 L 873.77984,121.44716 L 873.77984,130.35531 L 872.95434,139.93577 L 872.78925,144.97812 L 873.77984,146.32275 L 873.61474,151.02894 L 873.11944,152.8778 L 874.60533,154.97877 L 867.50608,156.40745 L 862.38802,157.584 z " id="VT" style="fill:#f4d7d7" /> + <ns0:path d="M 846.20833,194.22506 L 845.05264,193.21659 L 842.41105,193.04851 L 840.09968,191.03158 L 837.62319,185.485 L 834.55471,184.51732 L 832.17493,182.29151 L 813.18856,186.49346 L 769.27227,195.56969 L 760.19184,197.08239 L 759.43798,189.88537 L 762.17121,188.00743 L 763.492,186.83089 L 764.48259,185.15011 L 766.29867,183.97356 L 768.27985,182.12471 L 768.77515,180.44393 L 770.92143,177.5866 L 772.07712,176.57814 L 771.91202,175.56967 L 770.59123,172.37619 L 768.77515,172.20811 L 766.79397,165.82115 L 769.76574,163.97229 L 774.2234,162.45959 L 778.35086,161.11497 L 781.65283,160.61073 L 788.09167,160.44266 L 790.07285,161.78728 L 791.72384,161.95536 L 793.87012,160.61073 L 796.51169,159.43419 L 801.79484,158.92995 L 803.94112,157.0811 L 805.75721,153.71954 L 807.40819,151.7026 L 809.55447,151.7026 L 811.53565,150.52606 L 811.70075,148.17297 L 810.21487,145.98795 L 809.88467,144.47525 L 811.04036,142.29024 L 811.04036,140.77754 L 809.22428,140.77754 L 807.40819,139.93715 L 806.5827,138.7606 L 806.4176,136.07136 L 812.36115,130.35671 L 813.02154,129.51632 L 814.50743,126.49092 L 817.4792,121.78473 L 820.28587,117.91894 L 822.43215,115.39777 L 824.89861,113.49969 L 828.0455,112.20429 L 833.65885,110.85967 L 836.96082,111.02775 L 841.58358,109.51505 L 849.30966,107.36166 L 849.84051,112.53901 L 852.31699,119.26214 L 853.14249,124.64065 L 852.1519,128.67453 L 854.79347,133.38072 L 855.61896,135.56574 L 854.79347,138.59115 L 857.76525,139.93577 L 858.42565,140.27193 L 861.56253,151.70126 L 862.05782,157.07976 L 861.56253,168.34101 L 862.38802,174.05567 L 863.21351,177.75339 L 864.6994,185.31692 L 864.6994,193.72083 L 863.54371,196.07393 L 865.42216,198.14582 L 865.19266,199.77289 L 863.21147,201.62175 L 863.54167,202.96637 L 864.86246,202.63021 L 866.34835,201.28559 L 868.65972,198.59634 L 869.81541,197.92403 L 871.4664,198.59634 L 873.77778,198.76442 L 881.8676,194.73055 L 884.83937,191.87323 L 886.16016,190.36053 L 890.45272,192.0413 L 886.98565,195.73902 L 883.02329,198.76442 L 875.75896,204.31099 L 873.11738,205.31946 L 867.17384,207.3364 L 863.04638,208.51294 L 861.49899,207.95886 L 860.90212,204.47784 L 861.39742,201.62051 L 861.23232,199.4355 L 858.59075,198.25894 L 853.96798,197.25047 L 850.0056,196.07393 L 846.20833,194.22506 z " id="NY" style="fill:#280b0b" /> + <ns0:path d="M 846.20833,194.22506 L 844.06205,196.74624 L 844.06205,199.93973 L 842.08086,203.13321 L 841.91576,204.814 L 843.23656,206.15862 L 843.07146,208.6798 L 840.76007,209.85635 L 841.58556,212.71367 L 841.75066,213.89023 L 844.55734,214.22639 L 845.54794,216.91563 L 849.18011,219.43681 L 851.65659,221.11759 L 851.65659,221.95798 L 848.35462,225.15147 L 846.70362,227.50456 L 845.21774,230.3619 L 842.90636,231.70652 L 841.66812,232.46288 L 841.42046,233.72347 L 840.79828,236.43369 L 841.91377,238.76697 L 845.21574,241.79237 L 850.1687,244.14546 L 854.29616,244.81777 L 854.46126,246.33047 L 853.63576,247.33894 L 853.96596,250.19627 L 854.79145,250.19627 L 856.93773,247.6751 L 857.76322,242.63276 L 860.5699,238.43081 L 863.70677,231.70769 L 864.86246,225.99305 L 864.20207,224.8165 L 864.03697,215.06798 L 862.38598,211.53834 L 861.23029,212.37873 L 858.42362,212.71489 L 857.92832,212.21066 L 859.08401,211.20219 L 861.23029,209.18525 L 861.29469,208.048 L 860.90212,204.47784 L 861.39742,201.62051 L 861.23232,199.4355 L 858.59075,198.25894 L 853.96798,197.25047 L 850.0056,196.07393 L 846.20833,194.22506 z " id="NJ" style="fill:#a02c2c" /> + <ns0:path d="M 841.75066,232.37883 L 842.90636,231.70652 L 845.21774,230.3619 L 846.70362,227.50456 L 848.35462,225.15147 L 851.65659,221.95798 L 851.65659,221.11759 L 849.18011,219.43681 L 845.54794,216.91563 L 844.55734,214.22639 L 841.75066,213.89023 L 841.58556,212.71367 L 840.76007,209.85635 L 843.07146,208.6798 L 843.23656,206.15862 L 841.91576,204.814 L 842.08086,203.13321 L 844.06205,199.93973 L 844.06205,196.74624 L 846.45598,194.22507 L 845.05264,193.21659 L 842.41105,193.04851 L 840.09968,191.03158 L 837.62319,185.485 L 834.55471,184.51732 L 832.17493,182.29151 L 813.18856,186.49346 L 769.27227,195.56969 L 760.19184,197.08239 L 759.68563,189.71729 L 754.08139,195.57094 L 752.7606,196.07518 L 748.46894,199.20351 L 751.4416,219.10066 L 753.16482,230.27806 L 756.81257,250.30417 L 761.54207,249.5232 L 773.73965,247.96108 L 812.47286,239.9916 L 827.66544,237.0562 L 836.14231,235.36944 L 837.45809,234.05962 L 839.60438,232.37883 L 841.75066,232.37883 z " id="PA" style="fill:#782121" /> + <ns0:path d="M 840.59298,235.90964 L 841.42046,233.72347 L 841.66812,232.37883 L 839.60438,232.37883 L 837.45809,234.05962 L 835.9722,235.57232 L 837.45809,239.94236 L 839.76948,245.8251 L 841.91576,255.9098 L 843.56675,262.46486 L 848.68482,262.29678 L 854.95755,261.03674 L 852.64517,253.38975 L 851.65458,253.89398 L 848.02242,251.37281 L 846.20633,246.49855 L 844.22515,242.80084 L 841.91377,241.79237 L 839.76749,238.09466 L 840.59298,235.90964 z " id="DE" style="fill:#f4d7d7" /> + <ns0:path d="M 854.95655,260.95325 L 848.68482,262.29678 L 843.56675,262.46486 L 841.91576,255.9098 L 839.76948,245.8251 L 837.45809,239.94236 L 836.14231,235.36944 L 827.66544,237.0562 L 812.47286,239.9916 L 774.22495,247.84224 L 775.38031,253.05285 L 776.37091,258.93558 L 776.7011,258.59942 L 778.84739,256.07825 L 781.15877,252.88476 L 783.63525,252.71668 L 785.12115,251.20398 L 786.93723,248.51473 L 788.25802,249.18705 L 791.22979,248.85089 L 793.87137,246.66588 L 795.92094,245.15492 L 797.80542,244.65068 L 799.48473,245.82549 L 802.45651,247.33819 L 804.43769,249.18705 L 805.67593,250.78379 L 809.88595,252.5486 L 809.88595,255.57402 L 815.49931,256.91864 L 817.48049,258.26326 L 818.47108,256.24633 L 820.78247,257.92711 L 819.29657,261.28868 L 818.96637,264.146 L 817.15029,266.83525 L 817.15029,269.02027 L 817.81068,270.86913 L 822.98233,272.27864 L 827.38511,272.21447 L 830.52198,273.22294 L 832.66826,273.5591 L 833.65885,271.37408 L 832.17296,269.18907 L 832.17296,267.34021 L 829.69649,265.1552 L 827.55021,259.44055 L 828.87099,253.89398 L 828.70589,251.70897 L 827.38511,250.36434 C 827.38511,250.36434 828.87099,248.68356 828.87099,248.01125 C 828.87099,247.33894 829.36629,245.82624 829.36629,245.82624 L 831.34747,244.48162 L 833.32865,242.80084 L 833.82395,243.8093 L 832.33806,245.49008 L 831.01727,249.35588 L 831.34747,250.53242 L 833.16355,250.86858 L 833.65885,256.58323 L 831.51257,257.59169 L 831.84277,261.28941 L 832.33806,261.12133 L 833.49375,259.1044 L 835.14473,260.95325 L 833.49375,262.29788 L 833.16355,265.82751 L 835.80513,269.35715 L 839.76749,269.86138 L 841.41848,269.02099 L 844.72045,274.39949 L 846.53653,274.90372 L 846.53653,278.60143 L 844.22515,283.64377 L 843.72986,290.87112 L 845.21574,294.40076 L 846.70163,294.56884 L 848.68281,290.19881 L 849.5083,286.5011 L 849.6734,279.10567 L 852.81027,274.06333 L 854.95655,266.83598 L 854.95655,260.95325 z M 838.20212,271.12031 L 839.3578,273.72552 L 839.5229,275.57439 L 840.67859,277.50729 C 840.67859,277.50729 841.58664,276.58285 841.58664,276.2467 C 841.58664,275.91054 840.8437,273.05321 840.8437,273.05321 L 840.10075,270.61606 L 838.20212,271.12031 z " id="MD" style="fill:#d35f5f" /> + <ns0:path d="M 822.59725,272.21447 L 827.38511,272.21447 L 830.52198,273.22294 L 832.66826,273.5591 L 833.65885,271.37408 L 832.17296,269.18907 L 832.17296,267.34021 L 829.69649,265.1552 L 827.55021,259.44055 L 828.87099,253.89398 L 828.70589,251.70897 L 827.38511,250.36434 C 827.38511,250.36434 828.87099,248.68356 828.87099,248.01125 C 828.87099,247.33894 829.36629,245.82624 829.36629,245.82624 L 831.34747,244.48162 L 833.32865,242.80084 L 833.82395,243.8093 L 832.33806,245.49008 L 831.01727,249.35588 L 831.34747,250.53242 L 833.16355,250.86858 L 833.65885,256.58323 L 831.51257,257.59169 L 831.84277,261.28941 L 832.33806,261.12133 L 833.49375,259.1044 L 835.14473,260.95325 L 833.49375,262.29788 L 833.16355,265.82751 L 835.80513,269.35715 L 839.76749,269.86138 L 841.41848,269.02099 L 844.72045,274.39949 L 846.53653,274.90372 L 846.53653,278.60143 L 844.22515,283.64377 L 843.72986,290.87112 L 845.21574,294.40076 L 846.70163,294.56884 L 848.68281,290.19881 L 849.5083,286.5011 L 849.6734,279.10567 L 852.81027,274.06333 L 854.95655,266.83598 L 854.95655,260.95325 M 838.20212,271.12031 L 839.3578,273.72552 L 839.5229,275.57439 L 840.67859,277.50729 C 840.67859,277.50729 841.58664,276.58285 841.58664,276.2467 C 841.58664,275.91054 840.8437,273.05321 840.8437,273.05321 L 840.10075,270.61606 L 838.20212,271.12031 z " id="MD_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 774.24466,247.91204 L 775.38031,253.05285 L 776.37091,258.93558 L 776.7011,258.59942 L 778.84739,256.07825 L 781.15877,252.88476 L 783.63525,252.71668 L 785.12115,251.20398 L 786.93723,248.51473 L 788.25802,249.18705 L 791.22979,248.85089 L 793.87137,246.66588 L 795.92094,245.15492 L 797.80542,244.65068 L 799.48473,245.82549 L 802.45651,247.33819 L 804.43769,249.18705 L 805.84103,250.53167 L 804.7679,255.7421 L 798.98943,252.5486 L 794.36667,250.69975 L 794.20157,256.24633 L 793.70628,258.43134 L 792.05529,261.28868 L 791.39489,262.96946 L 788.25802,265.49063 L 787.76272,267.84373 L 784.29564,268.17988 L 783.96545,271.37336 L 782.80976,277.08802 L 780.16818,277.08802 L 778.84739,276.24763 L 777.1964,273.3903 L 775.38031,273.55838 L 775.05012,278.09649 L 772.90384,284.9877 L 767.78577,296.24894 L 768.61127,297.59356 L 768.44617,300.45089 L 766.29989,302.46782 L 764.814,302.13167 L 761.51202,304.65285 L 758.87045,303.64438 L 757.05436,308.51864 C 757.05436,308.51864 753.25709,309.35903 752.59669,309.52711 C 751.9363,309.69518 750.12022,308.18248 750.12022,308.18248 L 747.64373,310.53557 L 745.00215,311.2079 L 742.03037,310.36749 L 740.70958,309.02287 L 738.47074,305.8795 L 735.26132,303.81246 L 732.61975,300.95512 L 729.64798,297.08933 L 728.98758,294.73623 L 726.346,293.22353 L 725.5205,291.54275 L 725.27285,286.0802 L 727.50168,285.99616 L 729.48288,285.15577 L 729.64798,282.29845 L 731.29896,280.78574 L 731.46406,275.57532 L 732.45465,271.54144 L 733.77544,270.86913 L 735.09623,272.04567 L 735.59153,273.89453 L 737.40761,272.88606 L 737.90291,271.20529 L 736.74722,269.35643 L 736.74722,266.83525 L 737.73781,265.49063 L 740.04919,261.96099 L 741.36998,260.44829 L 743.51627,260.95252 L 745.82765,259.27173 L 748.96452,255.7421 L 751.27591,251.70821 L 751.6061,245.82549 L 752.1014,240.61506 L 752.1014,235.74079 L 750.94571,232.54731 L 751.9363,231.0346 L 753.24707,229.68997 L 756.81257,250.30417 L 761.54207,249.5232 L 774.24466,247.91204 z " id="WV" style="fill:#f4d7d7" /> + <ns0:path d="M 738.61165,306.09768 L 735.42643,309.35903 L 731.13386,313.05675 L 726.51109,318.60333 L 724.69501,320.45219 L 724.69501,322.6372 L 720.73264,324.82222 L 714.95419,328.35186 L 712.28688,329.81359 L 659.31592,334.90691 L 643.2212,336.75577 L 638.50172,337.28883 L 634.55111,337.26001 L 634.55111,341.29389 L 625.96598,341.79812 L 618.86673,342.47043 L 608.21432,342.68419 L 609.24067,341.39196 L 611.46709,339.55999 L 613.56845,338.3715 L 613.80194,335.04372 L 614.73588,333.14213 L 613.09488,330.50244 L 613.91377,328.51994 L 616.22516,326.67109 L 618.37144,325.99877 L 621.17811,327.3434 L 624.81029,328.68802 L 625.96598,328.35186 L 626.13108,325.99877 L 624.81029,323.47759 L 625.14049,321.1245 L 627.12167,319.6118 L 629.76325,318.93949 L 631.41424,318.26718 L 630.58875,316.41831 L 629.92835,314.40138 L 631.08404,313.56099 L 632.15718,310.11537 L 635.2115,308.35056 L 641.15506,307.34209 L 644.78724,306.83786 L 646.27312,308.85479 L 648.08921,309.69518 L 649.90529,306.33362 L 652.87707,304.82092 L 654.85825,306.5017 L 655.68375,307.67825 L 657.83004,307.17401 L 657.66493,303.64438 L 660.63671,301.96359 L 661.7924,301.1232 L 662.94809,302.80398 L 667.73595,302.80398 L 668.56145,300.61896 L 668.23125,298.26587 L 671.20302,294.56815 L 675.99089,290.53428 L 676.48618,285.82809 L 679.29287,285.49193 L 683.25523,283.64307 L 686.06192,281.62613 L 685.73171,279.60919 L 684.24582,278.09649 L 684.82367,275.82744 L 689.03369,275.57532 L 691.51017,274.73493 L 694.48195,276.41571 L 696.13293,280.95382 L 702.07649,281.28997 L 703.89257,283.13884 L 706.03885,283.30692 L 708.51534,281.79422 L 711.65221,282.29845 L 712.973,283.81115 L 715.77968,281.12189 L 717.59577,279.77727 L 719.24675,279.77727 L 719.90714,282.63461 L 721.72324,283.64307 L 725.3554,285.82809 L 725.5205,291.54275 L 726.346,293.22353 L 728.98758,294.73623 L 729.64798,297.08933 L 732.61975,300.95512 L 735.26132,303.81246 L 738.61165,306.09768 z " id="KY" style="fill:#e9afaf" /> + <ns0:path d="M 748.46982,198.97029 L 741.20371,203.30253 L 737.24134,205.65562 L 733.77427,209.52141 L 729.64681,213.55528 L 726.34484,214.39567 L 723.37307,214.8999 L 717.75972,217.58915 L 715.61344,217.75723 L 712.14638,214.56375 L 706.86322,215.23606 L 704.22165,213.72336 L 701.78994,212.3189 L 696.79333,213.05024 L 686.39211,214.73102 L 678.46737,215.99161 L 679.78816,231.20268 L 681.60425,245.48932 L 684.24582,269.86066 L 684.82367,275.82744 L 689.03369,275.57532 L 691.51017,274.73493 L 694.48195,276.41571 L 696.13293,280.95382 L 702.07649,281.28997 L 703.89257,283.13884 L 706.03885,283.30692 L 708.51534,281.79422 L 711.65221,282.29845 L 712.973,283.81115 L 715.77968,281.12189 L 717.59577,279.77727 L 719.24675,279.77727 L 719.90714,282.63461 L 721.72324,283.64307 L 725.27285,286.0802 L 727.50168,285.99616 L 729.48288,285.15577 L 729.64798,282.29845 L 731.29896,280.78574 L 731.46406,275.57532 L 732.45465,271.54144 L 733.77544,270.86913 L 735.09623,272.04567 L 735.59153,273.89453 L 737.40761,272.88606 L 737.90291,271.20529 L 736.74722,269.35643 L 736.74722,266.83525 L 737.73781,265.49063 L 740.04919,261.96099 L 741.36998,260.44829 L 743.51627,260.95252 L 745.82765,259.27173 L 748.96452,255.7421 L 751.27591,251.70821 L 751.6061,245.82549 L 752.1014,240.61506 L 752.1014,235.74079 L 750.94571,232.54731 L 751.9363,231.0346 L 753.33994,230.27806 L 751.4416,219.10066 L 748.46982,198.97029 z " id="OH" style="fill:#c83737" /> + <ns0:path d="M 594.42414,81.655837 L 596.29202,79.516552 L 598.51013,78.684606 L 603.99703,74.643722 L 606.33188,74.049474 L 606.79885,74.524879 L 601.54544,79.873103 L 598.1599,81.893534 L 596.05854,82.844333 L 594.42414,81.655837 z M 682.43117,115.05941 L 683.09156,117.66462 L 686.39354,117.8327 L 687.71434,116.57211 C 687.71434,116.57211 687.63178,115.05941 687.30159,114.89133 C 686.97139,114.72326 685.6506,112.95843 685.6506,112.95843 L 683.42177,113.21054 L 681.77077,113.37862 L 681.44058,114.55518 L 682.43117,115.05941 z M 713.13697,180.61201 L 709.835,172.04003 L 707.52362,162.62767 L 705.04714,159.26611 L 702.40557,157.41725 L 700.75458,158.5938 L 696.79222,160.44266 L 694.81104,165.65307 L 692.00436,169.51886 L 690.84867,170.19118 L 689.36279,169.51886 C 689.36279,169.51886 686.72121,168.00616 686.88631,167.33385 C 687.05141,166.66154 687.38161,162.12343 687.38161,162.12343 L 690.84867,160.77881 L 691.67417,157.24918 L 692.33456,154.55993 L 694.81104,152.87915 L 694.48084,142.45832 L 692.82985,140.10523 L 691.50907,139.26484 L 690.68357,137.07982 L 691.50907,136.23943 L 693.16005,136.57559 L 693.32515,134.89481 L 690.84867,132.54172 L 689.52789,129.85247 L 686.88631,129.85247 L 682.26355,128.33977 L 676.6502,124.81014 L 673.84353,124.81014 L 673.18314,125.48245 L 672.19255,124.97821 L 669.05568,122.62512 L 666.0839,124.47398 L 663.11213,126.82707 L 663.44233,130.52479 L 664.43292,130.86094 L 666.5792,131.36518 L 667.07449,132.20556 L 664.43292,133.04595 L 661.79134,133.38211 L 660.30546,135.23097 L 659.97526,137.41598 L 660.30546,139.09676 L 660.63565,144.81141 L 657.00349,146.99642 L 656.34309,146.82834 L 656.34309,142.45832 L 657.66388,139.93715 L 658.32427,137.41598 L 657.49878,136.57559 L 655.5176,137.41598 L 654.52701,141.78601 L 651.72034,142.96255 L 649.90425,144.97949 L 649.73915,145.98795 L 650.39955,146.82834 L 649.73915,149.51759 L 647.42778,150.02182 L 647.42778,151.19837 L 648.25327,153.71954 L 647.09758,160.1065 L 645.44659,164.30845 L 646.10699,169.18271 L 646.60228,170.35925 L 645.77679,172.88042 L 645.44659,173.72081 L 645.1164,176.57814 L 648.74856,182.79702 L 651.72034,189.52014 L 653.20622,194.56247 L 652.38073,199.43673 L 651.39014,205.65562 L 648.91366,211.03411 L 648.58347,213.89143 L 645.43483,217.12572 L 644.67586,217.92491 L 649.40999,217.75643 L 671.86343,215.40333 L 678.13717,214.73102 L 678.46737,215.99161 L 686.39211,214.73102 L 696.79333,213.05024 L 702.12014,212.57103 L 700.75458,211.37027 L 700.91968,209.85756 L 703.06596,205.99177 L 705.10906,204.18493 L 704.88204,198.9325 L 706.51299,197.27212 L 707.62681,196.91556 L 707.85382,193.21785 L 709.42225,190.0664 L 710.49539,190.69668 L 710.66049,191.36899 L 711.48598,191.53707 L 713.46716,190.5286 L 713.13697,180.61201 z M 578.8376,112.43927 L 580.72799,111.3639 L 583.53467,110.52351 L 587.16683,108.17042 L 587.16683,107.16195 L 587.82723,106.48964 L 593.93587,105.48118 L 596.41235,103.46424 L 600.87001,101.27923 L 601.03511,99.934604 L 603.01629,96.909201 L 604.83237,96.068811 L 606.15316,94.219954 L 608.46454,91.866863 L 612.9222,89.345695 L 617.71005,88.841461 L 618.86574,90.018006 L 618.53554,91.026474 L 614.73828,92.034941 L 613.25239,95.228422 L 610.94101,96.068811 L 610.44572,98.58998 L 607.96924,101.95154 L 607.63904,104.64079 L 608.46454,105.14502 L 609.45513,103.96847 L 613.08729,100.94307 L 614.40808,102.28769 L 616.71946,102.28769 L 620.02143,103.29616 L 621.50732,104.47271 L 622.9932,107.66619 L 625.79988,110.52351 L 629.76224,110.35543 L 631.24813,109.34697 L 632.89911,110.69159 L 634.5501,111.19582 L 635.87088,110.35543 L 637.02657,110.35543 L 638.67756,109.34697 L 642.80502,105.64925 L 646.27209,104.47271 L 653.04112,104.13655 L 657.66388,102.11962 L 660.30546,100.77499 L 661.79134,100.94307 L 661.79134,106.8258 L 662.28664,107.16195 L 665.25841,108.00234 L 667.23959,107.49811 L 673.51333,105.81733 L 674.66902,104.64079 L 676.15491,105.14502 L 676.15491,112.37237 L 679.45688,115.56585 L 680.77767,116.23816 L 682.09845,117.24663 L 680.77767,117.58279 L 679.95217,117.24663 L 676.15491,116.7424 L 674.00863,117.41471 L 671.69725,117.24663 L 668.39528,118.75933 L 666.5792,118.75933 L 660.63565,117.41471 L 655.3525,117.58279 L 653.37132,120.27203 L 646.27209,120.94434 L 643.79561,121.78473 L 642.63992,124.97821 L 641.31913,126.15476 L 640.82384,125.98668 L 639.33795,124.3059 L 634.71519,126.82707 L 634.0548,126.82707 L 632.89911,125.14629 L 632.07362,125.31437 L 630.09244,129.85247 L 629.10185,134.05442 L 625.27363,142.51293 L 623.6083,141.31962 L 622.20739,139.89342 L 620.57299,129.197 L 616.83724,128.00851 L 615.43633,125.63153 L 602.59467,122.77914 L 600.02634,121.59066 L 591.62089,119.21367 L 583.21544,118.02518 L 578.8376,112.43927 z " id="MI" style="fill:#c83737" /> + <ns0:path d="M 363.20447,145.98954 L 351.44763,144.98362 L 318.67723,141.55738 L 302.09981,139.41809 L 273.14771,135.13952 L 252.83454,132.04945 L 251.38528,143.66906 L 247.4644,168.89268 L 242.09425,200.50655 L 240.53069,211.44019 L 238.82546,223.80098 L 245.48808,224.7662 L 262.73518,227.14318 L 271.75391,228.36645 L 292.76047,230.93187 L 330.81813,235.21041 L 355.80134,237.34972 L 360.23755,191.23625 L 361.87193,164.85174 L 363.20447,145.98954 z " id="WY" style="fill:#f4d7d7" /> + <ns0:path d="M 365.51098,123.96764 L 366.33647,111.87386 L 368.64299,85.935913 L 370.0439,70.247815 L 371.33142,55.452236 L 338.69364,52.032396 L 308.81082,48.334682 L 278.92799,44.132734 L 245.9083,38.586162 L 227.08707,35.056526 L 193.6675,27.848581 L 189.09322,50.043547 L 192.59549,57.887585 L 191.19458,62.64155 L 193.06246,67.395514 L 196.33125,68.821708 L 200.067,79.518133 L 203.80276,83.321298 L 204.26973,84.509794 L 207.772,85.698291 L 208.23897,87.837565 L 201.00095,106.14034 L 201.00095,108.75502 L 203.56928,112.08279 L 204.50321,112.08279 L 209.40639,108.99272 L 210.10685,107.80422 L 211.74124,108.51732 L 211.50775,113.98438 L 214.30957,127.05779 L 217.34487,129.67247 L 218.2788,130.38556 L 220.14668,132.76254 L 219.67972,136.32802 L 220.38017,139.89349 L 221.5476,140.84429 L 223.88244,138.4673 L 226.68426,138.4673 L 229.95305,140.13119 L 232.52138,139.1804 L 236.7241,139.1804 L 240.45985,140.84429 L 243.26167,140.36889 L 243.72864,137.27881 L 246.76394,136.56572 L 248.16485,137.99191 L 248.63182,141.31968 L 251.26853,143.90677 L 252.83454,132.04945 L 273.14771,135.13952 L 302.09981,139.41809 L 318.67723,141.55738 L 351.44763,144.98362 L 363.16317,146.23449 L 364.89307,129.69427 L 365.51098,123.96764 z " id="MT" style="fill:#f4d7d7" /> + <ns0:path d="M 144.08485,180.96023 L 148.93381,161.76187 L 153.37002,143.34026 L 154.77093,138.94284 L 157.33926,132.76269 L 156.0551,130.38571 L 153.48676,130.50455 L 152.66957,129.43491 L 153.13654,128.24642 L 153.48676,125.0375 L 158.03971,119.33273 L 159.90759,118.85734 L 161.07501,117.66885 L 161.65873,114.34107 L 162.59266,113.62798 L 166.5619,107.56668 L 170.53114,103.05041 L 170.76463,99.128389 L 167.26235,96.394856 L 165.91982,91.819167 L 166.32842,81.776422 L 170.06418,64.662142 L 174.61712,43.031605 L 178.46962,29.00742 L 179.24775,25.053259 L 193.6675,27.848581 L 189.09322,50.043547 L 192.59549,57.887585 L 191.19458,62.64155 L 193.06246,67.395514 L 196.33125,68.821708 L 200.067,79.518133 L 203.80276,83.321298 L 204.26973,84.509794 L 207.772,85.698291 L 208.23897,87.837565 L 201.00095,106.14034 L 201.00095,108.75502 L 203.56928,112.08279 L 204.50321,112.08279 L 209.40639,108.99272 L 210.10685,107.80422 L 211.74124,108.51732 L 211.50775,113.98438 L 214.30957,127.05779 L 217.34487,129.67247 L 218.2788,130.38556 L 220.14668,132.76254 L 219.67972,136.32802 L 220.38017,139.89349 L 221.5476,140.84429 L 223.88244,138.4673 L 226.68426,138.4673 L 229.95305,140.13119 L 232.52138,139.1804 L 236.7241,139.1804 L 240.45985,140.84429 L 243.26167,140.36889 L 243.72864,137.27881 L 246.76394,136.56572 L 248.16485,137.99191 L 248.63182,141.31968 L 251.31689,143.45897 L 247.4644,168.89268 L 242.21095,200.38777 L 237.30774,199.55589 L 228.78555,198.12969 L 218.27875,196.22811 L 206.02081,194.08882 L 193.0624,191.65242 L 184.89044,189.57255 L 175.43432,187.67098 L 165.51122,185.65054 L 144.08485,180.96023 z " id="ID" style="fill:#f4d7d7" /> + <ns0:path d="M 95.99889,2.9536428 L 100.45655,4.4663441 L 110.36246,7.3236687 L 119.11268,9.3406038 L 139.58489,15.223331 L 163.02887,21.106058 L 179.49525,24.969183 L 178.46962,29.00742 L 174.61712,43.031605 L 170.06418,64.662142 L 166.32842,81.776422 L 166.13328,91.861195 L 151.85237,88.313121 L 136.44238,84.628799 L 120.68217,84.747642 L 120.21521,83.321459 L 114.61157,85.460744 L 110.05862,84.866496 L 107.60703,83.202605 L 106.32286,83.915707 L 101.53644,83.678 L 99.785303,82.251817 L 94.415149,80.112532 L 93.597952,80.231386 L 89.161743,78.686338 L 87.177124,80.587926 L 80.873036,80.231386 L 74.802439,75.952816 L 75.50289,75.12087 L 75.736374,67.039124 L 73.401527,62.998262 L 69.198802,62.404014 L 68.498351,59.789335 L 66.094359,59.304248 L 64.13488,57.747045 L 62.318797,58.755513 L 60.007419,55.73011 L 60.337616,52.704708 L 63.14429,52.368552 L 64.795274,48.166604 L 62.153699,46.990058 L 62.318797,43.124266 L 66.776456,42.451954 L 63.969782,39.59463 L 62.483896,32.199201 L 63.14429,29.173799 L 63.14429,20.93798 L 61.328206,17.576422 L 63.639585,7.8279025 L 65.785865,8.3321363 L 68.262342,11.357539 L 71.069016,14.046786 L 74.370985,16.063721 L 78.993743,18.248734 L 82.130616,18.921045 L 85.102388,20.433747 L 88.569459,21.442214 L 90.880838,21.274136 L 90.880838,18.752967 L 92.201625,17.576422 L 94.347905,16.231799 L 94.678102,17.408344 L 95.008299,19.257201 L 92.696921,19.761435 L 92.366724,21.946448 L 94.182807,23.459149 L 95.338496,25.980318 L 95.99889,27.997253 L 97.484776,27.829175 L 97.649875,26.484552 L 96.659284,25.139928 L 96.163989,21.77837 L 96.989481,19.929513 L 96.329087,18.416812 L 96.329087,16.063721 L 98.14517,12.366006 L 96.989481,9.6767597 L 94.513004,4.634422 L 94.843201,3.7940324 L 95.99889,2.9536428 z M 86.341086,9.169955 L 88.404826,9.001877 L 88.900121,10.430545 L 90.468562,8.7497548 L 92.862495,8.7497548 L 93.687987,10.3465 L 92.119546,12.111324 L 92.779951,12.951724 L 92.037002,15.052704 L 90.63366,15.472893 C 90.63366,15.472893 89.725613,15.556938 89.725613,15.220782 C 89.725613,14.884626 91.21151,12.531524 91.21151,12.531524 L 89.477971,11.943246 L 89.147774,13.455958 L 88.404826,14.12827 L 86.836382,11.775168 L 86.341086,9.169955 z " id="WA" style="fill:#d35f5f" /> + <ns0:path d="M 224.65378,521.59843 L 226.52879,518.16091 L 228.7163,517.84841 L 229.0288,518.62966 L 226.99754,521.59843 L 224.65378,521.59843 z M 234.49758,518.00466 L 240.43511,520.50467 L 242.46637,520.19217 L 244.02887,516.44215 L 243.40387,513.16089 L 239.34135,512.69214 L 235.43508,514.41089 L 234.49758,518.00466 z M 264.18522,527.69221 L 267.77898,533.00473 L 270.12274,532.69223 L 271.2165,532.22348 L 272.62275,533.47348 L 276.21652,533.31723 L 277.15403,531.91098 L 274.34151,530.19222 L 272.4665,526.59845 L 270.43524,523.16094 L 264.81022,525.97345 L 264.18522,527.69221 z M 283.71656,536.286 L 284.96656,534.41099 L 289.49783,535.34849 L 290.12284,534.87974 L 296.06036,535.50474 L 295.74786,536.75475 L 293.24785,538.161 L 289.02908,537.8485 L 283.71656,536.286 z M 288.87283,541.28602 L 290.74784,545.03604 L 293.7166,543.94228 L 294.0291,542.37977 L 292.4666,540.34851 L 288.87283,540.03601 L 288.87283,541.28602 z M 295.59161,540.19226 L 297.77912,537.37975 L 302.31039,539.72351 L 306.52916,540.81727 L 310.74793,543.47353 L 310.74793,545.34854 L 307.31042,547.0673 L 302.62289,548.0048 L 300.27913,546.59854 L 295.59161,540.19226 z M 311.68544,555.19233 L 313.24794,553.94233 L 316.52921,555.50484 L 323.87299,558.94235 L 327.15426,560.97361 L 328.71676,563.31737 L 330.59177,567.53614 L 334.49804,570.03615 L 334.18554,571.28616 L 330.43552,574.41117 L 326.373,575.81743 L 324.96675,575.19243 L 321.99798,576.91118 L 319.65422,580.0362 L 317.46671,582.84871 L 315.74795,582.69246 L 312.31044,580.19245 L 311.99794,575.81743 L 312.62294,573.47367 L 311.06043,568.00489 L 309.02917,566.28613 L 308.87292,563.78612 L 311.06043,562.84862 L 313.09169,559.87986 L 313.56044,558.94235 L 311.99794,557.22359 L 311.68544,555.19233 z " id="HI_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 224.65378,521.59843 L 226.52879,518.16091 L 228.7163,517.84841 L 229.0288,518.62966 L 226.99754,521.59843 L 224.65378,521.59843 z M 234.49758,518.00466 L 240.43511,520.50467 L 242.46637,520.19217 L 244.02887,516.44215 L 243.40387,513.16089 L 239.34135,512.69214 L 235.43508,514.41089 L 234.49758,518.00466 z M 264.18522,527.69221 L 267.77898,533.00473 L 270.12274,532.69223 L 271.2165,532.22348 L 272.62275,533.47348 L 276.21652,533.31723 L 277.15403,531.91098 L 274.34151,530.19222 L 272.4665,526.59845 L 270.43524,523.16094 L 264.81022,525.97345 L 264.18522,527.69221 z M 283.71656,536.286 L 284.96656,534.41099 L 289.49783,535.34849 L 290.12284,534.87974 L 296.06036,535.50474 L 295.74786,536.75475 L 293.24785,538.161 L 289.02908,537.8485 L 283.71656,536.286 z M 288.87283,541.28602 L 290.74784,545.03604 L 293.7166,543.94228 L 294.0291,542.37977 L 292.4666,540.34851 L 288.87283,540.03601 L 288.87283,541.28602 z M 295.59161,540.19226 L 297.77912,537.37975 L 302.31039,539.72351 L 306.52916,540.81727 L 310.74793,543.47353 L 310.74793,545.34854 L 307.31042,547.0673 L 302.62289,548.0048 L 300.27913,546.59854 L 295.59161,540.19226 z M 311.68544,555.19233 L 313.24794,553.94233 L 316.52921,555.50484 L 323.87299,558.94235 L 327.15426,560.97361 L 328.71676,563.31737 L 330.59177,567.53614 L 334.49804,570.03615 L 334.18554,571.28616 L 330.43552,574.41117 L 326.373,575.81743 L 324.96675,575.19243 L 321.99798,576.91118 L 319.65422,580.0362 L 317.46671,582.84871 L 315.74795,582.69246 L 312.31044,580.19245 L 311.99794,575.81743 L 312.62294,573.47367 L 311.06043,568.00489 L 309.02917,566.28613 L 308.87292,563.78612 L 311.06043,562.84862 L 313.09169,559.87986 L 313.56044,558.94235 L 311.99794,557.22359 L 311.68544,555.19233 z " id="HI" style="fill:#e9afaf" /> + <ns0:path d="M 365.08234,342.9472 L 388.2557,344.07626 L 420.00963,345.26475 L 418.60872,369.98537 L 418.14175,388.52584 L 418.37523,390.18973 L 422.81144,393.9929 L 424.9128,395.18139 L 425.61327,394.94369 L 426.31372,392.80441 L 427.71463,394.70599 L 429.81599,394.70599 L 429.81599,393.2798 L 432.6178,394.70599 L 432.15084,398.74687 L 436.35356,398.98457 L 438.92189,400.17306 L 443.12462,400.88615 L 445.69295,402.78774 L 448.02779,400.64846 L 451.53007,401.36155 L 454.0984,404.92703 L 455.03233,404.92703 L 455.03233,407.30401 L 457.36718,408.0171 L 459.70203,405.64012 L 461.5699,406.35321 L 464.13823,406.35321 L 465.07218,408.9679 L 469.97536,410.86948 L 471.37627,410.15638 L 473.24415,405.87781 L 474.41156,405.87781 L 475.57899,408.0171 L 479.78172,408.73019 L 483.51747,410.15638 L 486.55277,411.10718 L 488.42065,410.15638 L 489.1211,407.54171 L 493.55731,407.54171 L 495.65868,408.49249 L 498.46049,406.35321 L 499.62792,406.35321 L 500.32837,408.0171 L 504.53109,408.0171 L 506.16549,405.87781 L 508.03337,406.35321 L 510.13473,408.9679 L 513.40351,410.86948 L 516.6723,411.82028 L 519.47412,413.12761 L 521.80896,415.14805 L 524.84426,413.72186 L 527.64608,414.91035 L 528.33194,426.45357 L 528.34653,436.5409 L 529.04699,446.28652 L 529.74745,450.3274 L 532.31578,454.60597 L 533.24971,459.83533 L 537.68592,465.54009 L 537.9194,468.86787 L 538.61987,469.58096 L 537.9194,478.3758 L 534.88411,483.60516 L 536.5185,485.74444 L 535.81804,488.35912 L 535.11759,495.96547 L 533.71668,499.29324 L 534.00598,503.01936 L 527.40119,504.83432 L 517.33018,509.5405 L 516.33959,511.55743 L 513.69802,513.57437 L 511.55174,515.08707 L 510.23095,515.92746 L 504.4525,521.47403 L 501.64583,523.65904 L 496.19758,527.0206 L 490.41913,529.54177 L 483.98029,533.07141 L 482.16421,534.58411 L 476.22066,538.28182 L 472.7536,538.95414 L 468.79123,544.66878 L 464.66377,545.00494 L 463.67318,547.02188 L 465.98456,549.03881 L 464.49867,554.75346 L 463.17788,559.45964 L 462.0222,563.49351 L 461.1967,568.19969 L 462.0222,570.72086 L 463.83828,577.94821 L 464.82887,584.33517 L 466.64495,587.1925 L 465.65436,588.7052 L 462.51749,590.72214 L 456.73904,586.68827 L 451.1257,585.51172 L 449.80491,586.01595 L 446.50294,585.34364 L 442.21038,582.15016 L 436.92723,580.97362 L 429.1676,577.44398 L 427.02132,573.41011 L 425.70053,566.68699 L 422.39856,564.67006 L 421.73817,562.31697 L 422.39856,561.64466 L 422.72876,558.11502 L 421.40797,557.44271 L 420.74758,556.43424 L 422.06837,551.89614 L 420.41738,549.54304 L 417.11541,548.19842 L 413.64834,543.66032 L 410.01618,536.76912 L 405.72362,534.07988 L 405.88872,532.06294 L 400.44047,519.28902 L 399.61497,514.91899 L 397.79889,512.90206 L 397.63379,511.38936 L 391.52515,505.84279 L 388.88357,502.6493 L 388.88357,501.47276 L 386.242,499.28775 L 379.30786,498.1112 L 371.71333,497.43889 L 368.57646,495.0858 L 363.9537,496.93466 L 360.32154,498.44736 L 358.01016,501.80891 L 357.01957,505.67471 L 352.56191,512.06167 L 350.08543,514.58284 L 347.44386,513.57437 L 345.62777,512.39782 L 343.64659,511.72551 L 339.68423,509.37242 L 339.68423,508.70011 L 337.86815,506.68317 L 332.585,504.49816 L 324.99047,496.43042 L 322.67909,491.55616 L 322.67909,483.15227 L 319.37712,476.42915 L 318.88182,473.57182 L 317.23084,472.56336 L 316.07515,470.37834 L 310.9571,468.19333 L 309.63631,466.51255 L 302.37198,458.27673 L 301.05119,454.91517 L 296.26332,452.56207 L 294.77744,448.02394 L 292.13584,444.99855 L 290.15467,444.49434 L 289.49163,439.63101 L 297.66367,440.34413 L 327.31615,443.19648 L 356.96871,444.86037 L 359.30356,420.13975 L 363.27277,362.37936 L 364.90719,342.88781 L 366.3081,342.91754 M 467.38967,586.18345 L 466.81183,578.788 L 464.00514,571.30851 L 463.42729,563.99709 L 464.99573,555.42509 L 468.38027,548.28176 L 471.92989,542.65112 L 475.14933,538.95339 L 475.80972,539.20552 L 470.9393,546.09673 L 466.48163,552.90391 L 464.41788,559.79513 L 464.08769,565.17365 L 464.99573,571.56063 L 467.63732,579.04012 L 468.13261,584.41863 L 468.29771,585.93134 L 467.38967,586.18345 z " id="TX" style="fill:#280b0b" /> + <ns0:path d="M 140.74058,399.1133 L 144.96457,398.27159 L 146.48222,396.01346 L 147.06593,393.16108 L 143.33019,392.56684 L 142.86321,391.73489 L 143.33019,389.95216 L 143.44692,383.89086 L 145.54828,383.17775 L 148.46685,380.32538 L 149.05056,375.21486 L 150.56821,371.4117 L 152.55282,369.39125 L 156.0551,367.60852 L 157.68948,366.18234 L 157.80623,363.80536 L 156.75555,363.21111 L 155.93835,362.14147 L 154.65419,356.08016 L 151.85237,350.96965 L 152.44317,347.57226 L 149.86775,344.19525 L 135.04148,320.54427 L 115.19528,290.35661 L 91.963567,255.17727 L 79.318752,235.85757 L 80.989783,229.03046 L 88.111072,202.05171 L 96.399776,169.3682 L 82.624178,165.56503 L 68.848582,161.99955 L 56.006928,157.72098 L 48.301935,155.58169 L 36.627704,152.49162 L 29.426941,149.98419 L 27.813217,154.89608 L 27.648119,162.62767 L 22.364968,174.89736 L 19.228097,177.5866 L 18.8979,178.76315 L 17.081817,179.60354 L 15.595931,183.97356 L 14.770438,187.33512 L 17.577112,191.70515 L 19.228097,196.07518 L 20.383786,199.77289 L 20.053589,206.49601 L 18.237506,209.68949 L 17.577112,215.74029 L 16.586521,219.60608 L 18.402605,223.63995 L 21.209279,228.34614 L 23.520657,233.38847 L 24.841445,237.59042 L 24.511248,240.95198 L 24.181051,241.45621 L 24.181051,243.64123 L 29.959497,250.19627 L 29.464202,252.71743 L 28.803808,255.07053 L 28.143414,257.08746 L 28.308513,265.65943 L 30.454793,269.52523 L 32.435974,272.21447 L 35.242648,272.71871 L 36.233239,275.57603 L 35.07755,279.27375 L 32.93127,280.95453 L 31.775581,280.95453 L 30.950088,284.9884 L 31.445384,288.0138 L 34.747353,292.5519 L 36.398338,298.09847 L 37.884224,302.97273 L 39.205012,306.16621 L 42.672079,312.21702 L 44.157966,314.90627 L 44.653261,317.93167 L 46.304246,318.94014 L 46.304246,321.4613 L 45.478753,323.47824 L 43.66267,330.87367 L 43.167375,332.8906 L 45.643852,335.74793 L 49.936412,336.25216 L 54.559169,338.10102 L 58.521532,340.28603 L 61.493305,340.28603 L 64.465077,343.47951 L 67.106653,348.52185 L 68.262342,350.87494 L 72.224705,353.05995 L 77.177659,353.90034 L 78.663546,356.08536 L 79.32394,359.44692 L 77.838053,360.11923 L 78.16825,361.12769 L 81.470222,361.96808 L 84.276896,362.13616 L 87.248668,367.01042 L 91.211035,371.38045 L 92.036527,373.73354 L 94.678102,378.10356 L 95.008299,381.46512 L 95.008299,391.21364 L 95.503595,393.0625 L 105.7397,394.5752 L 125.88171,397.43253 L 140.74058,399.1133 z M 50.26694,346.75563 L 51.587732,348.35237 L 51.422633,349.697 L 48.120652,349.61296 L 47.542806,348.35237 L 46.88241,346.83966 L 50.26694,346.75563 z M 52.248128,346.75563 L 53.48637,346.08332 L 57.118549,348.26833 L 60.255432,349.52892 L 59.347387,350.20124 L 54.724613,349.94912 L 53.073623,348.26833 L 52.248128,346.75563 z M 73.380807,367.34524 L 75.19689,369.78238 L 76.022393,370.79086 L 77.590834,371.37912 L 78.168673,369.86642 L 77.178082,368.01756 L 74.453952,365.91658 L 73.380807,366.08465 L 73.380807,367.34524 z M 71.89491,376.33744 L 73.711004,379.61497 L 74.949248,381.63192 L 73.463351,381.88403 L 72.142563,380.62344 C 72.142563,380.62344 71.399615,379.11074 71.399615,378.69054 C 71.399615,378.27035 71.399615,376.42148 71.399615,376.42148 L 71.89491,376.33744 z " id="CA" style="fill:#280b0b" /> + <ns0:path d="M 141.11208,399.22238 L 138.4292,401.4664 L 138.099,402.9791 L 138.5943,403.98756 L 157.91082,415.08071 L 170.2932,422.98037 L 185.31716,431.8885 L 202.4874,442.30933 L 215.03489,444.8305 L 242.33612,448.47703 L 244.42909,434.63935 L 248.26156,406.64864 L 255.37454,351.33455 L 259.72234,319.30647 L 233.45531,315.31482 L 205.67064,310.56085 L 171.53056,303.99238 L 168.54652,322.80241 L 168.07955,323.2778 L 166.32842,326.01134 L 163.76009,325.89248 L 162.47592,323.04011 L 159.67411,322.68356 L 158.74017,321.49507 L 157.80623,321.49507 L 156.87229,322.08932 L 154.88767,323.15896 L 154.77093,330.40875 L 154.53744,332.19149 L 153.95374,345.26489 L 152.43609,347.52302 L 151.85237,350.96965 L 154.65419,356.08016 L 155.93835,362.14147 L 156.75555,363.21111 L 157.80623,363.80536 L 157.68948,366.18234 L 156.0551,367.60852 L 152.55282,369.39125 L 150.56821,371.4117 L 149.05056,375.21486 L 148.46685,380.32538 L 145.54828,383.17775 L 143.44692,383.89086 L 143.33019,389.95216 L 142.86321,391.73489 L 143.33019,392.56684 L 147.06593,393.16108 L 146.48222,396.01346 L 144.96457,398.27159 L 141.11208,399.22238 z " id="AZ" style="fill:#de8787" /> + <ns0:path d="M 144.08485,180.96023 L 165.51122,185.65054 L 175.43432,187.67098 L 184.89044,189.57255 L 193.12078,191.77126 L 191.89498,197.53545 L 188.27597,215.71936 L 184.42347,236.99336 L 182.43886,246.26359 L 180.22075,260.40663 L 176.83522,277.63975 L 173.56644,293.44668 L 171.55619,304.36486 L 168.54652,322.80241 L 168.07955,323.2778 L 166.32842,326.01134 L 163.76009,325.89248 L 162.47592,323.04011 L 159.67411,322.68356 L 158.74017,321.49507 L 157.80623,321.49507 L 156.87229,322.08932 L 154.88767,323.15896 L 154.77093,330.40875 L 154.53744,332.19149 L 153.95374,345.26489 L 152.43963,347.54764 L 149.86775,344.19525 L 135.04148,320.54427 L 115.19528,290.35661 L 91.963567,255.17727 L 79.318752,235.85757 L 80.989783,229.03046 L 88.111072,202.05171 L 96.166292,169.45944 L 130.48854,177.92533 L 144.49761,181.0154" id="NV" style="fill:#e9afaf" /> + <ns0:path d="M 259.6056,319.59339 L 233.45531,315.31482 L 205.67064,310.56085 L 171.45228,304.13508 L 173.56644,293.44668 L 176.83522,277.63975 L 180.22075,260.40663 L 182.43886,246.26359 L 184.42347,236.99336 L 188.27597,215.71936 L 191.89498,197.53545 L 193.03322,191.74155 L 206.02081,194.08882 L 218.27875,196.22811 L 228.78555,198.12969 L 237.30774,199.55589 L 242.21095,200.38777 L 240.53069,211.44019 L 238.82546,223.80098 L 245.48808,224.7662 L 262.73518,227.14318 L 272.10412,228.36648 L 268.94498,251.37398 L 265.6762,274.66841 L 261.84373,303.76606 L 260.30605,315.31482 L 259.6056,319.59339 z " id="UT" style="fill:#e9afaf" /> + <ns0:path d="M 384.05299,331.95365 L 388.2557,263.49654 L 389.8901,240.2021 L 355.80134,237.34972 L 330.81813,235.21041 L 292.76047,230.93187 L 271.63008,228.31722 L 268.94498,251.37398 L 265.6762,274.66841 L 261.84373,303.76606 L 260.30605,315.31482 L 259.72234,319.35569 L 294.86179,323.63426 L 332.58567,328.23704 L 366.04543,330.52751 L 372.84567,331.2406 L 384.5199,331.83485" id="CO" style="fill:#e9afaf" /> + <ns0:path d="M 290.31977,444.66242 L 289.49163,439.63101 L 297.66367,440.34413 L 327.31615,443.19648 L 356.96871,444.86037 L 359.30356,420.13975 L 363.27277,362.37936 L 364.90719,342.88781 L 366.3081,342.91754 L 366.29351,330.73549 L 332.58567,328.23704 L 294.86179,323.63426 L 259.66398,319.35569 L 255.37454,351.33455 L 248.26156,406.64864 L 244.42909,434.63935 L 242.33612,448.47703 L 258.12559,450.54515 L 259.44637,440.12432 L 276.45152,442.81356 L 290.31977,444.66242 z " id="NM" style="fill:#e9afaf" /> + <ns0:path d="M 144.38087,180.54003 L 148.93381,161.76187 L 153.37002,143.34026 L 154.77093,138.94284 L 157.33926,132.76269 L 156.0551,130.38571 L 153.48676,130.50455 L 152.66957,129.43491 L 153.13654,128.24642 L 153.48676,125.0375 L 158.03971,119.33273 L 159.90759,118.85734 L 161.07501,117.66885 L 161.65873,114.34107 L 162.59266,113.62798 L 166.5619,107.56668 L 170.53114,103.05041 L 170.76463,99.128389 L 167.26235,96.394856 L 166.19165,92.03947 L 151.85237,88.313121 L 136.44238,84.628799 L 120.68217,84.747642 L 120.21521,83.321459 L 114.61157,85.460744 L 110.05862,84.866496 L 107.60703,83.202605 L 106.32286,83.915707 L 101.53644,83.678 L 99.785303,82.251817 L 94.415149,80.112532 L 93.597952,80.231386 L 89.161743,78.686338 L 87.177124,80.587926 L 80.873036,80.231386 L 74.802439,75.952816 L 75.50289,75.12087 L 75.736374,67.039124 L 73.401527,62.998262 L 69.198802,62.404014 L 68.498351,59.789335 L 66.094359,59.304248 L 60.172517,61.44476 L 57.861139,68.167876 L 54.559169,78.588708 L 51.2572,85.311824 L 46.139147,99.934604 L 39.535209,114.05315 L 31.280285,127.16323 L 29.299104,130.18863 L 28.473611,139.09676 L 27.152823,145.31564 L 29.426941,149.98419 L 36.627704,152.49162 L 48.301935,155.58169 L 56.006928,157.72098 L 68.848582,161.99955 L 82.624178,165.56503 L 96.399776,169.60589 M 144.08485,180.96023 L 96.166292,169.45944 L 130.48854,177.92533 L 144.49761,181.0154" id="OR" style="fill:#e9afaf" /> + <ns0:path d="M 482.58353,129.91009 L 481.88308,121.11525 L 480.0152,113.50891 L 478.14732,99.484714 L 477.68036,89.263683 L 475.81248,85.698205 L 474.17808,80.468846 L 474.17808,69.772421 L 474.87853,65.731548 L 472.88209,60.014242 L 442.87077,59.427825 L 423.88445,58.755513 L 396.8083,57.410889 L 371.33142,55.452236 L 370.0439,70.247815 L 368.64299,85.935913 L 366.33647,111.87386 L 365.67607,124.49947 L 423.04492,128.24621 L 482.58353,129.91009 z " id="ND" style="fill:#f4d7d7" /> + <ns0:path d="M 484.10703,208.42015 L 483.13305,206.29529 L 481.4161,203.35887 L 483.28398,198.8426 L 484.6849,192.90014 L 481.88308,190.76086 L 481.4161,187.90848 L 482.35005,185.29379 L 484.21793,185.29379 L 484.6849,178.16284 L 484.45141,146.54897 L 483.98444,143.4589 L 479.78172,139.89342 L 478.61429,137.99183 L 478.61429,136.32794 L 480.71565,134.66406 L 482.11657,133.23787 L 482.4668,129.91009 L 423.04492,128.24621 L 365.67608,124.20534 L 364.89307,129.69427 L 363.24575,146.19243 L 361.87193,164.85174 L 360.23755,191.5928 L 376.1145,192.66245 L 396.66115,193.85093 L 414.87297,195.03943 L 439.15538,196.22791 L 450.12916,195.75252 L 452.23052,198.1295 L 457.1337,201.21959 L 458.30112,202.17037 L 462.73733,200.74418 L 466.70658,200.26879 L 469.50839,200.03109 L 471.37627,201.45728 L 476.51293,203.12117 L 479.54822,204.78505 L 480.0152,206.44894 L 480.94914,208.58823 L 482.81702,208.58823 L 484.10703,208.42015 z " id="SD" style="fill:#f4d7d7" /> + <ns0:path d="M 496.12564,252.80011 L 497.52656,255.41479 L 497.29307,257.79177 L 499.8614,261.83265 L 503.13018,266.11122 L 496.82609,266.11122 L 451.76325,265.63581 L 410.43676,264.20963 L 388.13897,263.37769 L 389.8901,240.2021 L 355.80134,237.34972 L 360.23755,191.5928 L 376.1145,192.66245 L 396.66115,193.85093 L 414.87297,195.03943 L 439.15538,196.22791 L 450.12916,195.75252 L 452.23052,198.1295 L 457.1337,201.21959 L 458.30112,202.17037 L 462.73733,200.74418 L 466.70658,200.26879 L 469.50839,200.03109 L 471.37627,201.45728 L 476.51293,203.12117 L 479.54822,204.78505 L 480.0152,206.44894 L 480.94914,208.58823 L 482.81702,208.58823 L 484.45141,208.46938 L 485.61883,214.05529 L 488.42065,221.89933 L 489.35459,226.891 L 491.68943,230.69417 L 492.38988,236.16123 L 494.02428,240.4398 L 494.25776,247.33306 L 496.23653,253.05224" id="NE" style="fill:#f4d7d7" /> + <ns0:path d="M 580.12177,205.73586 L 580.18014,207.63744 L 582.51499,208.35053 L 583.44892,209.53902 L 583.91589,211.44061 L 587.88513,215.00608 L 588.58559,217.38307 L 587.88513,220.94855 L 586.01725,224.75171 L 585.3168,227.36639 L 582.98196,229.26798 L 581.11408,229.98108 L 575.74393,231.40726 L 575.04347,233.30885 L 574.34302,235.44814 L 575.04347,236.87433 L 576.91135,238.53822 L 576.67787,242.81678 L 574.80999,244.48067 L 574.10954,246.14456 L 574.10954,248.99694 L 572.24166,249.47233 L 570.60726,250.66083 L 570.37378,252.08702 L 570.60726,254.22631 L 568.85613,256.06846 L 565.4706,252.56242 L 564.30317,250.18543 L 556.3647,250.89852 L 546.32485,251.37392 L 520.40805,252.32472 L 506.63246,252.56242 L 497.05959,252.80011 L 495.9476,252.92617 L 494.25776,247.33306 L 494.02428,240.4398 L 492.38988,236.16123 L 491.68943,230.69417 L 489.35459,226.891 L 488.42065,221.89933 L 485.61883,214.05529 L 484.45141,208.46938 L 483.0505,206.21125 L 481.4161,203.35887 L 483.28398,198.8426 L 484.6849,192.90014 L 481.88308,190.76086 L 481.4161,187.90848 L 482.35005,185.29379 L 484.10119,185.29379 L 495.89216,185.29379 L 546.55834,184.5807 L 565.00364,183.86761 L 569.20636,183.74877 L 569.90681,187.19538 L 572.24166,188.85927 L 572.47514,190.28546 L 570.37378,193.85093 L 570.60726,197.17871 L 573.1756,201.21959 L 575.74393,202.40807 L 578.77923,202.88347 L 580.12177,205.73586 z " id="IA" style="fill:#e9afaf" /> + <ns0:path d="M 639.20393,481.84625 L 638.01716,483.15227 L 632.73401,483.15227 L 631.24813,482.31188 L 629.10185,481.97572 L 622.16771,483.99266 L 620.35163,483.15227 L 617.71005,487.52229 L 616.58406,488.3312 L 615.43633,485.74444 L 614.2689,481.70357 L 610.76664,478.3758 L 611.93406,470.53175 L 611.23361,469.58096 L 609.36573,469.81866 L 600.96028,470.53175 L 576.2109,471.24485 L 575.74393,469.58096 L 576.44438,461.26152 L 579.94666,454.84366 L 585.3168,445.33573 L 584.38287,443.19645 L 585.55029,443.19645 L 586.25075,439.86868 L 583.91589,437.96709 L 584.14938,436.0655 L 582.04802,431.31154 L 581.75616,425.75534 L 583.15706,422.99209 L 582.74847,418.47583 L 581.34756,415.38574 L 582.74847,413.95956 L 581.34756,411.82028 L 581.81454,409.91869 L 582.74847,403.50083 L 585.78377,400.64846 L 585.08332,398.50917 L 588.81908,393.0421 L 591.62089,392.09132 L 591.62089,389.47664 L 590.92044,388.05044 L 593.72225,382.58339 L 596.52407,381.39489 L 596.63379,377.84737 L 605.49374,377.76685 L 630.09345,375.74991 L 635.69855,375.51222 L 635.7068,382.13688 L 635.8719,399.44893 L 635.04641,431.71994 L 634.88131,446.34274 L 637.68799,465.83981 L 639.20393,481.84625 z " id="MS" style="fill:#e9afaf" /> + <ns0:path d="M 632.23973,310.19942 L 632.07463,306.16555 L 632.56993,301.45935 L 634.88131,298.43395 L 636.6974,294.40007 L 639.33898,290.03004 L 638.84368,283.97923 L 637.0276,281.12189 L 636.6974,277.76034 L 637.52289,272.04567 L 637.0276,264.81831 L 635.7068,248.17858 L 634.38601,232.21115 L 633.3949,220.02589 L 636.53128,220.95071 L 638.01716,221.95917 L 639.17285,221.62302 L 641.31913,219.60608 L 644.20888,217.92491 L 649.40999,217.75643 L 671.86343,215.40333 L 678.13717,214.73102 L 678.30227,215.90756 L 679.78816,231.20268 L 681.60425,245.48932 L 684.24582,269.86066 L 684.74112,275.7434 L 684.24582,278.09649 L 685.73171,279.60919 L 686.06192,281.62613 L 683.25523,283.64307 L 679.29287,285.49193 L 676.48618,285.82809 L 675.99089,290.53428 L 671.20302,294.56815 L 668.23125,298.26587 L 668.56145,300.61896 L 667.73595,302.80398 L 662.94809,302.80398 L 661.7924,301.1232 L 660.63671,301.96359 L 657.66493,303.64438 L 657.83004,307.17401 L 655.68375,307.67825 L 654.85825,306.5017 L 652.87707,304.82092 L 649.90529,306.33362 L 648.08921,309.69518 L 646.27312,308.85479 L 644.78724,306.83786 L 641.15506,307.34209 L 635.2115,308.35056 L 632.23973,310.19942 z " id="IN" style="fill:#de8787" /> + <ns0:path d="M 632.07463,310.03134 L 632.07463,306.16555 L 632.56993,301.45935 L 634.88131,298.43395 L 636.6974,294.40007 L 639.33898,290.03004 L 638.84368,283.97923 L 637.0276,281.12189 L 636.6974,277.76034 L 637.52289,272.04567 L 637.0276,264.81831 L 635.7068,248.17858 L 634.38601,232.21115 L 633.56001,220.10992 L 632.23872,219.26993 L 631.41322,216.58068 L 630.09244,212.71489 L 628.44145,210.86603 L 626.95557,208.17679 L 626.71703,202.46993 L 616.60376,203.83427 L 588.81909,205.617 L 579.94666,205.17133 L 580.18014,207.63744 L 582.51499,208.35053 L 583.44892,209.53902 L 583.91589,211.44061 L 587.88513,215.00608 L 588.58559,217.38307 L 587.88513,220.94855 L 586.01725,224.75171 L 585.3168,227.36639 L 582.98196,229.26798 L 581.11408,229.98108 L 575.74393,231.40726 L 575.04347,233.30885 L 574.34302,235.44814 L 575.04347,236.87433 L 576.91135,238.53822 L 576.67787,242.81678 L 574.80999,244.48067 L 574.10954,246.14456 L 574.10954,248.99694 L 572.24166,249.47233 L 570.60726,250.66083 L 570.37378,252.08702 L 570.60726,254.22631 L 568.85613,255.59306 L 567.80545,258.50488 L 568.27242,262.30804 L 570.60726,269.91439 L 578.07878,277.75843 L 583.68241,281.56161 L 583.44892,286.07787 L 584.38287,287.50407 L 590.92044,287.97946 L 593.72225,289.40566 L 593.0218,293.20882 L 590.68696,299.38898 L 589.98649,302.71676 L 592.32134,306.75762 L 598.85891,312.22469 L 603.52862,312.93778 L 605.62997,318.16715 L 607.73133,321.49492 L 606.7974,324.58499 L 608.43179,328.86356 L 610.29967,331.00285 L 613.32836,330.65102 L 613.91377,328.51994 L 616.22516,326.67109 L 618.37144,325.99877 L 621.17811,327.3434 L 624.81029,328.68802 L 625.96598,328.35186 L 626.13108,325.99877 L 624.81029,323.47759 L 625.14049,321.1245 L 627.12167,319.6118 L 629.76325,318.93949 L 631.41424,318.26718 L 630.58875,316.41831 L 629.92835,314.40138 L 631.08404,313.56099 L 632.07463,310.03134 z " id="IL" style="fill:#782121" /> + <ns0:path d="M 482.35005,129.91009 L 481.88308,121.11525 L 480.0152,113.50891 L 478.14732,99.484714 L 477.68036,89.263683 L 475.81248,85.698205 L 474.17808,80.468846 L 474.17808,69.772421 L 474.87853,65.731548 L 473.01887,60.063466 L 503.79211,60.100136 L 504.1223,51.528162 L 504.7827,51.360084 L 507.09408,51.864318 L 509.07526,52.704708 L 509.90075,58.419357 L 511.38664,64.806318 L 513.03762,66.487097 L 517.99058,66.487097 L 518.32077,67.999799 L 524.75961,68.335954 L 524.75961,70.520967 L 529.71257,70.520967 L 530.04276,69.176344 L 531.19845,67.999799 L 533.50983,67.327487 L 534.83062,68.335954 L 537.80239,68.335954 L 541.76476,71.025201 L 547.21301,73.54637 L 549.68948,74.050604 L 550.18478,73.042136 L 551.67066,72.537902 L 552.16596,75.563305 L 554.80753,76.907928 L 555.30283,76.403695 L 556.62362,76.571773 L 556.62362,78.756786 L 559.26519,79.765253 L 562.40206,79.765253 L 564.05305,78.924863 L 567.35502,75.563305 L 569.99659,75.059071 L 570.82209,76.907928 L 571.31738,78.252552 L 572.30797,78.252552 L 573.29856,77.412162 L 582.37898,77.076006 L 584.19506,80.269487 L 584.85546,80.269487 L 585.58425,79.142165 L 590.11857,78.756786 L 589.49346,81.126733 L 585.47097,83.036786 L 576.02857,87.25913 L 571.15228,89.345695 L 568.01541,92.034941 L 565.53894,95.732656 L 563.22756,99.766526 L 561.41147,100.60692 L 556.78872,105.81733 L 555.46793,105.98541 L 552.00086,109.17889 L 552.70347,109.74365 L 549.82713,112.55812 L 549.59365,115.4105 L 549.59365,124.20534 L 548.42622,125.86923 L 543.05607,129.91009 L 540.72123,136.09025 L 541.18819,136.32794 L 543.75652,138.46723 L 544.45698,141.79501 L 542.5891,145.12278 L 542.5891,149.16365 L 543.05607,156.0569 L 546.09137,159.14699 L 549.59365,159.14699 L 551.46153,162.47476 L 554.96379,162.95015 L 558.93303,168.89261 L 566.17105,173.17118 L 568.27242,176.02356 L 569.20636,183.86761 L 565.00364,183.86761 L 546.55834,184.5807 L 495.89216,185.29379 L 484.10119,185.29379 L 484.6849,178.16284 L 484.45141,146.54897 L 483.98444,143.4589 L 479.78172,139.89342 L 478.61429,137.99183 L 478.61429,136.32794 L 480.71565,134.66406 L 482.11657,133.23787 L 482.35005,129.91009 z " id="MN" style="fill:#d35f5f" /> + <ns0:path d="M 626.6436,202.64577 L 626.79047,198.26019 L 625.13948,193.55401 L 624.47909,187.16705 L 623.3234,184.64588 L 624.31399,181.4524 L 625.13948,178.42699 L 626.62537,175.73775 L 625.96497,172.20811 L 625.30458,168.5104 L 625.79988,166.66154 L 627.78106,164.14037 L 627.94616,161.28305 L 627.12066,159.93842 L 627.78106,157.24918 L 628.27635,153.88762 L 631.08303,148.00489 L 634.0548,140.94562 L 634.2199,138.59253 L 633.8897,137.58406 L 633.06421,138.08829 L 628.77165,144.64333 L 625.96497,148.84528 L 623.98379,150.69414 L 623.1583,153.04723 L 621.67241,153.88762 L 620.51673,155.90455 L 619.03084,155.5684 L 618.86574,153.71954 L 620.18653,151.19837 L 622.33281,146.32411 L 624.14889,144.64333 L 625.27363,142.26081 L 623.6083,141.31962 L 622.20739,139.89342 L 620.57299,129.197 L 616.83724,128.00851 L 615.43633,125.63153 L 602.59467,122.77914 L 600.02634,121.59066 L 591.62089,119.21367 L 583.21544,118.02518 L 578.9573,112.40581 L 578.41662,113.71699 L 577.26093,113.54892 L 576.60053,112.37237 L 573.79386,111.53198 L 572.63817,111.70006 L 570.82209,112.70853 L 569.8315,112.03621 L 570.49189,110.01928 L 572.47307,106.8258 L 573.62876,105.64925 L 571.64758,104.13655 L 569.5013,104.97694 L 566.52953,106.99388 L 558.935,110.35543 L 555.96322,111.02775 L 552.99145,110.52351 L 551.98885,109.6104 L 549.82713,112.55812 L 549.59365,115.4105 L 549.59365,124.20534 L 548.42622,125.86923 L 543.05607,129.91009 L 540.72123,136.09025 L 541.18819,136.32794 L 543.75652,138.46723 L 544.45698,141.79501 L 542.5891,145.12278 L 542.5891,149.16365 L 543.05607,156.0569 L 546.09137,159.14699 L 549.59365,159.14699 L 551.46153,162.47476 L 554.96379,162.95015 L 558.93303,168.89261 L 566.17105,173.17118 L 568.27242,176.02356 L 569.20636,183.74877 L 569.90681,187.19538 L 572.24166,188.85927 L 572.47514,190.28546 L 570.37378,193.85093 L 570.60726,197.17871 L 573.1756,201.21959 L 575.74393,202.40807 L 578.77923,202.88347 L 580.03422,205.49817 L 589.40281,205.49815 L 616.60376,203.83427 L 626.6436,202.64577 z " id="WI" style="fill:#de8787" /> + <ns0:path d="M 568.73938,255.89021 L 565.4706,252.56242 L 564.30317,250.18543 L 556.3647,250.89852 L 546.32485,251.37392 L 520.40805,252.32472 L 506.63246,252.56242 L 498.57727,252.68126 L 496.24239,252.80011 L 497.52656,255.41479 L 497.29307,257.79177 L 499.8614,261.83265 L 503.01343,266.11122 L 506.16549,268.96359 L 508.50034,269.20129 L 509.90124,270.15209 L 509.90124,273.24216 L 508.03337,274.90605 L 507.56639,277.28304 L 509.66775,280.84852 L 512.23609,283.93859 L 514.80442,285.84018 L 516.20533,297.96278 L 515.50487,334.68718 L 515.73836,339.55999 L 516.20533,346.69094 L 540.02077,346.21554 L 563.83621,345.50245 L 585.08332,344.55165 L 596.29058,344.07626 L 598.15846,347.16634 L 597.69149,349.54332 L 594.4227,352.3957 L 593.72225,355.48577 L 600.02634,355.96118 L 605.163,355.24808 L 607.26436,348.59252 L 607.11843,342.73921 L 610.06619,341.22388 L 611.46709,339.55999 L 613.56845,338.3715 L 613.80194,335.04372 L 614.73588,333.14213 L 613.33497,330.61659 L 610.29967,331.00285 L 608.43179,328.86356 L 606.7974,324.58499 L 607.73133,321.49492 L 605.62997,318.16715 L 603.52862,312.93778 L 598.85891,312.22469 L 592.32134,306.75762 L 589.98649,302.71676 L 590.68696,299.38898 L 593.0218,293.20882 L 593.72225,289.40566 L 590.92044,287.97946 L 584.38287,287.50407 L 583.44892,286.07787 L 583.68241,281.56161 L 578.07878,277.75843 L 570.60726,269.91439 L 568.27242,262.30804 L 567.80545,258.50488 L 568.73938,255.89021 z " id="MO" style="fill:#de8787" /> + <ns0:path d="M 604.99844,354.98628 L 600.02634,355.96118 L 593.72225,355.48577 L 594.4227,352.3957 L 597.69149,349.54332 L 598.15846,347.16634 L 596.29058,344.07626 L 585.08332,344.55165 L 563.83621,345.50245 L 540.02077,346.21554 L 516.20533,346.69094 L 517.83972,353.82189 L 517.83971,362.37904 L 519.24063,373.78867 L 519.47412,413.12761 L 521.80896,415.14805 L 524.84426,413.72186 L 527.64608,414.91035 L 528.31735,426.58728 L 550.99455,426.55757 L 570.60726,425.60677 L 581.63941,425.75534 L 583.15706,422.99209 L 582.74847,418.47583 L 581.34756,415.38574 L 582.74847,413.95956 L 581.34756,411.82028 L 581.81454,409.91869 L 582.74847,403.50083 L 585.78377,400.64846 L 585.08332,398.50917 L 588.81908,393.0421 L 591.62089,392.09132 L 591.62089,389.47664 L 590.92044,388.05044 L 593.72225,382.58339 L 596.52407,381.39489 L 596.22104,377.59524 L 598.72469,376.42223 L 599.71528,371.54796 L 598.22939,367.85023 L 602.35687,365.49714 L 602.68706,362.80789 L 604.06449,358.25292 L 604.99844,354.98628 z " id="AR" style="fill:#e9afaf" /> + <ns0:path d="M 383.76113,331.71594 L 372.84567,331.2406 L 366.27891,330.73549 L 366.54158,330.94347 L 365.98708,342.94722 L 388.2557,344.07626 L 420.00963,345.26475 L 418.60872,369.98537 L 418.14175,388.52584 L 418.37523,390.18973 L 422.81144,393.9929 L 424.9128,395.18139 L 425.61327,394.94369 L 426.31372,392.80441 L 427.71463,394.70599 L 429.81599,394.70599 L 429.81599,393.2798 L 432.6178,394.70599 L 432.15084,398.74687 L 436.35356,398.98457 L 438.92189,400.17306 L 443.12462,400.88615 L 445.69295,402.78774 L 448.02779,400.64846 L 451.53007,401.36155 L 454.0984,404.92703 L 455.03233,404.92703 L 455.03233,407.30401 L 457.36718,408.0171 L 459.70203,405.64012 L 461.5699,406.35321 L 464.13823,406.35321 L 465.07218,408.9679 L 469.97536,410.86948 L 471.37627,410.15638 L 473.24415,405.87781 L 474.41156,405.87781 L 475.57899,408.0171 L 479.78172,408.73019 L 483.51747,410.15638 L 486.55277,411.10718 L 488.42065,410.15638 L 489.1211,407.54171 L 493.55731,407.54171 L 495.65868,408.49249 L 498.46049,406.35321 L 499.62792,406.35321 L 500.32837,408.0171 L 504.53109,408.0171 L 506.16549,405.87781 L 508.03337,406.35321 L 510.13473,408.9679 L 513.40351,410.86948 L 516.6723,411.82028 L 519.47412,413.48417 L 519.24063,373.78867 L 517.83971,362.37904 L 517.83972,353.82189 L 516.20533,346.69094 L 515.73836,339.55999 L 515.50487,334.92488 L 501.9627,335.75681 L 454.56566,335.28143 L 408.56892,333.14212 L 383.76113,331.71594 z " id="OK" style="fill:#e9afaf" /> + <ns0:path d="M 515.50487,335.04372 L 501.9627,335.75681 L 454.56566,335.28143 L 408.56892,333.14212 L 383.90706,331.83479 L 388.13897,263.37769 L 410.43676,264.20963 L 451.76325,265.63581 L 496.82609,266.11122 L 503.01343,266.11122 L 506.16549,268.96359 L 508.50034,269.20129 L 509.90124,270.15209 L 509.90124,273.24216 L 508.03337,274.90605 L 507.56639,277.28304 L 509.66775,280.84852 L 512.23609,283.93859 L 514.80442,285.84018 L 516.20533,297.96278 L 515.50487,335.04372 z " id="KS" style="fill:#e9afaf" /> + <ns0:path d="M 616.71945,488.11058 L 615.43633,485.74444 L 614.2689,481.70357 L 610.76664,478.3758 L 611.93406,470.53175 L 611.23361,469.58096 L 609.36573,469.81866 L 600.96028,470.53175 L 576.2109,471.24485 L 575.74393,469.58096 L 576.44438,461.26152 L 579.94666,454.84366 L 585.3168,445.33573 L 584.38287,443.19645 L 585.55029,443.19645 L 586.25075,439.86868 L 583.91589,437.96709 L 584.14938,436.0655 L 582.04802,431.31154 L 581.69779,425.60677 L 570.60726,425.60677 L 550.99455,426.55757 L 528.31735,426.58728 L 528.34653,436.5409 L 529.04699,446.28652 L 529.74745,450.3274 L 532.31578,454.60597 L 533.24971,459.83533 L 537.68592,465.54009 L 537.9194,468.86787 L 538.61987,469.58096 L 537.9194,478.3758 L 534.88411,483.60516 L 536.5185,485.74444 L 535.81804,488.35912 L 535.11759,495.96547 L 533.71668,499.29324 L 533.84174,503.05325 L 538.62788,501.47276 L 546.88281,501.1366 L 557.44911,504.83432 L 564.05305,506.01086 L 567.85031,504.49816 L 571.15228,505.67471 L 574.45425,506.68317 L 575.27974,504.49816 L 571.97778,503.32162 L 569.3362,503.82585 L 566.52953,502.14507 C 566.52953,502.14507 566.69462,500.80045 567.35502,500.63237 C 568.01541,500.46429 570.49189,499.6239 570.49189,499.6239 L 572.30797,501.1366 L 574.12406,500.12814 L 577.42603,500.80045 L 578.91191,503.32162 L 579.24211,505.67471 L 583.86487,506.01086 L 585.68095,507.85972 L 584.85546,509.5405 L 583.53467,510.38089 L 585.18565,512.06167 L 593.77077,515.75938 L 597.40294,514.41476 L 598.39353,511.89359 L 601.03511,511.22128 L 602.85119,509.70858 L 604.17198,510.71704 L 604.99747,513.74245 L 602.68609,514.58284 L 603.34648,515.25515 L 606.81355,513.91053 L 609.12493,510.38089 L 609.95042,509.87666 L 607.80414,509.5405 L 608.62964,507.85972 L 608.46454,506.34702 L 610.61082,505.84279 L 611.76651,504.49816 L 612.4269,505.33855 C 612.4269,505.33855 612.2618,508.53203 613.08729,508.53203 C 613.91279,508.53203 617.37985,509.20434 617.37985,509.20434 L 621.50732,511.22128 L 622.49791,512.73398 L 625.46968,512.73398 L 626.62537,513.74245 L 628.93675,510.54897 L 628.93675,509.03627 L 627.61596,509.03627 L 624.14889,506.17894 L 618.20535,505.33855 L 614.90338,502.98546 L 616.05907,500.12814 L 618.37045,500.46429 L 618.53554,499.79198 L 616.71946,498.78351 L 616.71946,498.27928 L 620.02143,498.27928 L 621.83751,495.0858 L 620.51673,493.06886 L 620.18653,490.21154 L 618.70064,490.37962 L 616.71946,492.56463 L 616.05907,495.25388 L 612.9222,494.58156 L 611.93161,492.73271 L 613.74769,490.71577 L 615.81141,488.86693 L 616.71945,488.11058 z " id="LA" style="fill:#de8787" /> + <ns0:path d="M 817.62464,258.28441 L 818.55858,256.38283 L 820.84673,258.09426 L 819.7727,260.66139 L 818.09161,259.04505 L 817.62464,258.28441 z " id="path6656" style="fill:#0000ff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <ns0:g id="g8180" style="fill:#cccccc"> + <ns0:path d="M 738.7284,305.32516 L 740.70958,309.02287 L 742.03037,310.36749 L 745.00215,311.2079 L 747.64373,310.53557 L 750.12022,308.18248 C 750.12022,308.18248 751.9363,309.69518 752.59669,309.52711 C 753.25709,309.35903 757.05436,308.51864 757.05436,308.51864 L 758.87045,303.64438 L 761.51202,304.65285 L 764.814,302.13167 L 766.29989,302.46782 L 768.44617,300.45089 L 768.61127,297.59356 L 767.78577,296.24894 L 772.90384,284.9877 L 775.05012,278.09649 L 775.38031,273.55838 L 777.1964,273.3903 L 778.84739,276.24763 L 780.16818,277.08802 L 782.80976,277.08802 L 783.96545,271.37336 L 784.29564,268.17988 L 787.76272,267.84373 L 788.25802,265.49063 L 791.39489,262.96946 L 792.05529,261.28868 L 793.70628,258.43134 L 794.20157,256.24633 L 794.36667,250.69975 L 798.98943,252.5486 L 804.7679,255.7421 L 805.59338,250.44764 L 809.88595,252.5486 L 809.88595,255.57402 L 815.49931,256.91864 L 817.48049,258.26326 L 818.47108,256.24633 L 820.78247,257.92711 L 819.29657,261.28868 L 818.96637,264.146 L 817.15029,266.83525 L 817.15029,269.02027 L 817.81068,270.86913 L 822.98233,272.27864 L 824.90863,273.89525 L 830.19178,274.23141 L 832.83336,276.5845 L 836.13533,277.25681 L 837.45611,278.60143 L 836.96082,283.30762 L 837.95141,284.31608 L 838.11651,286.66917 L 839.43729,288.85419 L 839.2722,290.70305 L 835.97023,289.5265 L 835.97023,290.53497 L 837.95141,292.21575 L 837.95141,293.39229 L 839.43729,294.56884 L 840.75808,296.24962 L 840.92318,298.60271 L 838.6118,300.11541 L 838.942,300.61964 L 841.58358,300.11541 L 844.88554,299.4431 L 846.04123,299.27502 L 850.20356,306.58097 L 845.21707,308.35056 L 833.49506,311.37597 L 813.92151,315.35219 L 792.88078,319.27565 L 775.38031,321.96489 L 759.22415,323.98183 L 751.9363,325.32646 L 747.32856,324.68385 L 745.16725,324.65414 L 742.69078,326.67109 L 734.60093,326.83916 L 722.3505,328.8153 L 712.13922,329.78475 L 714.95419,328.35186 L 720.73264,324.82222 L 724.69501,322.6372 L 724.69501,320.45219 L 726.51109,318.60333 L 731.13386,313.05675 L 735.42643,309.35903 L 738.7284,305.32516 z " id="VA" style="fill:#d35f5f" /> + <ns0:path d="M 845.69487,293.77543 L 844.35758,290.76684 L 844.75381,282.95122 L 847.26331,278.51396 L 847.13123,274.21116 L 853.07478,271.9253 L 852.41438,274.21116 L 849.24449,278.3795 L 848.92256,286.54808 L 848.17135,289.94326 L 846.33876,293.85948 L 845.69487,293.77543 z " id="path3106" style="fill:#d35f5f" ns1:nodetypes="cccccccccccc" /> + </ns0:g> + <ns0:path d="M 467.38967,586.18345 L 466.81183,578.788 L 464.00514,571.30851 L 463.42729,563.99709 L 464.99573,555.42509 L 468.38027,548.28176 L 471.92989,542.65112 L 475.14933,538.95339 L 475.80972,539.20552 L 470.9393,546.09673 L 466.48163,552.90391 L 464.41788,559.79513 L 464.08769,565.17365 L 464.99573,571.56063 L 467.63732,579.04012 L 468.13261,584.41863 L 468.29771,585.93134 L 467.38967,586.18345 z M 465.65436,588.7052 L 466.64495,587.1925 L 464.82887,584.33517 L 463.83828,577.94821 L 462.0222,570.72086 L 461.1967,568.19969 L 462.0222,563.49351 L 463.17788,559.45964 L 464.49867,554.75346 L 465.98456,549.03881 L 463.67318,547.02188 L 464.66377,545.00494 L 468.79123,544.66878 L 472.7536,538.95414 L 476.22066,538.28182 L 482.16421,534.58411 L 483.98029,533.07141 L 490.41913,529.54177 L 496.19758,527.0206 L 501.64583,523.65904 L 504.4525,521.47403 L 510.23095,515.92746 L 511.55174,515.08707 L 513.69802,513.57437 L 516.33959,511.55743 L 517.33018,509.5405 L 527.40119,504.83432 L 534.17024,502.98546" id="TX_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 900.52373,145.31564 L 900.85393,143.29871 L 902.00961,139.60099" id="NH_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 919.55232,177.09192 L 921.77043,176.37882 L 922.23741,174.59609 L 923.28809,174.71493 L 924.33877,177.09192 L 923.0546,177.56732 L 919.08535,177.68617 L 919.55232,177.09192 z M 909.97943,177.92387 L 912.31427,175.19033 L 913.94868,175.19033 L 915.81656,176.73537 L 913.36497,177.80501 L 911.14686,178.87466 L 909.97943,177.92387 z M 903.66061,177.08236 L 906.63237,175.56967 L 906.13708,173.21658 L 906.96257,171.70388 L 909.93434,170.19118 L 910.75983,173.38466 L 910.26454,175.23351 L 907.78806,176.74621 L 907.78806,177.75468 L 909.76924,176.24198 L 913.73161,171.5358 L 917.69397,169.51886 L 921.98653,168.00616 L 921.65633,165.48499 L 920.66574,162.45959 L 918.68456,159.93842 L 916.86848,159.09803 L 914.7222,159.26611 L 914.2269,159.77034 L 915.21749,161.11497 L 916.70338,160.27458 L 918.84966,161.95536 L 919.67515,164.81268 L 917.85907,166.66154 L 915.54769,167.67001 L 911.91552,167.16577 L 907.95316,160.94689 L 905.64178,158.25764 L 903.8257,158.25764 L 902.67001,159.09803 L 900.68883,156.40879 L 901.01902,154.89608 L 903.4955,149.51759 L 900.35862,144.81139" id="MA_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 771.67854,458.60093 L 771.08653,452.05785 L 773.39791,441.63702 L 774.88379,437.26699 L 774.3885,434.57775 L 778.51596,427.3504 L 777.85557,425.66962" id="GA_Atlantic" style="fill:#6666e6;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 777.85557,425.66962 L 780.00185,424.32499 L 785.1199,418.61034 L 784.12931,415.24879 L 787.10108,415.08071 L 790.73325,411.55107 L 792.38423,410.71068 L 794.69561,407.18104 L 797.50228,404.32372 L 799.64856,400.62601 L 802.12504,399.95369 L 803.28073,397.09637 L 804.93171,396.25598 L 805.42701,389.70094 L 808.06859,383.31398 L 813.51684,377.43125" id="SC_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 813.18663,377.59933 L 819.46038,375.41432 L 824.24824,374.91008 L 824.74353,372.38892 L 826.72471,365.6658 L 830.19178,360.79154 L 836.79572,355.24497 L 842.07887,352.7238 L 844.88554,352.05149 L 846.04123,352.55572 L 847.36202,352.55572 L 850.49889,347.51338 L 852.48007,343.81567 L 851.15929,344.3199 L 848.84791,346.67299 L 848.18751,344.99221 L 843.89495,344.99221 L 845.87614,338.43717 L 845.05064,337.09255 L 843.06946,337.09255 L 843.06946,336.08408 L 842.73926,334.73946 L 844.39025,336.08408 L 845.87614,336.25216 L 848.35261,336.58832 L 852.14988,334.90754 L 853.47066,331.88214 L 854.13106,329.69712 L 856.77263,328.3525 L 857.10283,323.98247 L 856.27734,323.31016 L 858.75382,323.14208 L 858.09342,320.78899 L 855.61694,318.26782 L 851.98478,311.54471 L 850.1687,306.50237 M 854.21672,340.95692 L 856.85831,338.3517 L 860.07773,335.66244 L 861.64617,334.99013 L 861.81127,332.88915 L 861.15088,326.50217 L 859.66499,324.06503 L 859.00459,322.13213 L 859.74753,321.88001 L 862.55422,327.59468 L 862.96697,332.21684 L 862.80187,335.74649 L 859.33479,337.34323 L 856.44555,339.86441 L 855.28987,341.125 L 854.21672,340.95692 z " id="NC_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 850.1687,306.50237 L 846.04123,299.27502 L 844.88554,299.4431 L 841.58358,300.11541 L 838.942,300.61964 L 838.6118,300.11541 L 840.92318,298.60271 L 840.75808,296.24962 L 839.43729,294.56884 L 837.95141,293.39229 L 837.95141,292.21575 L 835.97023,290.53497 L 835.97023,289.5265 L 839.2722,290.70305 L 839.43729,288.85419 L 838.11651,286.66917 L 837.95141,284.31608 L 836.96082,283.30762 L 837.45611,278.60143 L 836.13533,277.25681 L 832.83336,276.5845 L 830.19178,274.23141 L 824.90863,273.89525 L 822.59725,272.21447" id="VA_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 893.09433,183.30123 L 896.06607,182.29279 L 898.54255,180.27585 L 899.69824,178.42699 L 901.01902,178.59507 L 903.99082,176.91428" id="RI_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 865.2752,198.17615 L 870.47581,194.73055 L 874.10797,191.36899 L 876.08916,189.18398 L 876.91465,189.85629 L 879.72132,188.34359 L 885.00447,187.16705 L 893.58963,183.30123" id="CT_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 840.75808,236.41389 L 841.91377,238.76697 L 845.21574,241.79237 L 850.1687,244.14546 L 854.29616,244.81777 L 854.46126,246.33047 L 853.63576,247.33894 L 853.96596,250.19627 L 854.79145,250.19627 L 856.93773,247.6751 L 857.76322,242.63276 L 860.5699,238.43081 L 863.70677,231.70769 L 864.86246,225.99305 L 864.20207,224.8165 L 864.03697,215.06798 L 862.38598,211.53834 L 861.23029,212.37873 L 858.42362,212.71489 L 857.92832,212.21066 L 859.08401,211.20219 L 861.23029,209.18525 L 861.06519,207.84063" id="NJ_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 861.06519,207.84063 L 863.04638,208.51294 L 867.17384,207.3364 L 873.11738,205.31946 L 875.75896,204.31099 L 883.02329,198.76442 L 886.98565,195.73902 L 890.45272,192.0413 L 886.16016,190.36053 L 884.83937,191.87323 L 881.8676,194.73055 L 873.77778,198.76442 L 871.4664,198.59634 L 869.81541,197.92403 L 868.65972,198.59634 L 866.34835,201.28559 L 864.86246,202.63021 L 863.54167,202.96637 L 863.21147,201.62175 L 865.19266,199.77289 L 865.52285,197.75595" id="NY_Atlantic" style="fill:#0000ff;fill-opacity:0;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 854.95655,260.95325 L 852.64517,253.38975 L 851.65458,253.89398 L 848.02242,251.37281 L 846.20633,246.49855 L 844.22515,242.80084 L 841.91377,241.79237 L 839.76749,238.09466 L 840.59298,235.90964" id="DE_Atlantic" style="fill:#cccccc;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 29.464207,149.85375 L 27.813223,154.89608 L 27.648124,162.62767 L 22.364973,174.89736 L 19.228102,177.5866 L 18.897905,178.76315 L 17.081822,179.60354 L 15.595936,183.97356 L 14.770444,187.33512 L 17.577118,191.70515 L 19.228102,196.07518 L 20.383791,199.77289 L 20.053595,206.49601 L 18.237511,209.68949 L 17.577118,215.74029 L 16.586527,219.60608 L 18.40261,223.63995 L 21.209284,228.34614 L 23.520662,233.38847 L 24.84145,237.59042 L 24.511253,240.95198 L 24.181056,241.45621 L 24.181056,243.64123 L 29.959503,250.19627 L 29.464207,252.71743 L 28.803813,255.07053 L 28.14342,257.08746 L 28.308518,265.65943 L 30.454798,269.52523 L 32.43598,272.21447 L 35.242654,272.71871 L 36.233244,275.57603 L 35.077555,279.27375 L 32.931275,280.95453 L 31.775586,280.95453 L 30.950093,284.9884 L 31.445389,288.0138 L 34.747358,292.5519 L 36.398343,298.09847 L 37.884229,302.97273 L 39.205017,306.16621 L 42.672085,312.21702 L 44.157971,314.90627 L 44.653266,317.93167 L 46.304251,318.94014 L 46.304251,321.4613 L 45.478759,323.47824 L 43.662676,330.87367 L 43.16738,332.8906 L 45.643857,335.74793 L 49.936417,336.25216 L 54.559175,338.10102 L 58.521538,340.28603 L 61.49331,340.28603 L 64.465083,343.47951 L 67.106658,348.52185 L 68.262347,350.87494 L 72.224711,353.05995 L 77.177665,353.90034 L 78.663551,356.08536 L 79.323945,359.44692 L 77.838059,360.11923 L 78.168256,361.12769 L 81.470225,361.96808 L 84.276899,362.13616 L 87.248671,367.01042 L 91.211035,371.38045 L 92.036527,373.73354 L 94.678102,378.10356 L 95.008299,381.46512 L 95.008299,391.21364 L 95.503595,393.0625 M 50.266945,346.75563 L 51.587737,348.35237 L 51.422639,349.697 L 48.120658,349.61296 L 47.542811,348.35237 L 46.882415,346.83966 L 50.266945,346.75563 z M 52.248133,346.75563 L 53.486376,346.08332 L 57.118555,348.26833 L 60.255437,349.52892 L 59.347393,350.20124 L 54.724619,349.94912 L 53.073629,348.26833 L 52.248133,346.75563 z M 73.380812,367.34524 L 75.196895,369.78238 L 76.022398,370.79086 L 77.590839,371.37912 L 78.168678,369.86642 L 77.178087,368.01756 L 74.453957,365.91658 L 73.380812,366.08465 L 73.380812,367.34524 z M 71.894915,376.33744 L 73.711009,379.61497 L 74.949253,381.63192 L 73.463356,381.88403 L 72.142568,380.62344 C 72.142568,380.62344 71.39962,379.11074 71.39962,378.69054 C 71.39962,378.27035 71.39962,376.42148 71.39962,376.42148 L 71.894915,376.33744 z " id="CA_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 95.99889,2.9536428 L 94.843201,3.7940324 L 94.513004,4.634422 L 96.989481,9.6767597 L 98.14517,12.366006 L 96.329087,16.063721 L 96.329087,18.416812 L 96.989481,19.929513 L 96.163989,21.77837 L 96.659284,25.139928 L 97.649875,26.484552 L 97.484776,27.829175 L 95.99889,27.997253 L 95.338496,25.980318 L 94.182807,23.459149 L 92.366724,21.946448 L 92.696921,19.761435 L 95.008299,19.257201 L 94.678102,17.408344 L 94.347905,16.231799 L 92.201625,17.576422 L 90.880838,18.752967 L 90.880838,21.274136 L 88.569459,21.442214 L 85.102391,20.433747 L 82.130619,18.921045 L 78.993748,18.248734 L 74.370991,16.063721 L 71.069021,14.046786 L 68.262347,11.357539 L 65.78587,8.3321363 L 63.63959,7.8279025 L 61.328212,17.576422 L 63.144295,20.93798 L 63.144295,29.173799 L 62.483901,32.199201 L 63.969787,39.59463 L 66.776461,42.451954 L 62.318803,43.124266 L 62.153704,46.990058 L 64.79528,48.166604 L 63.144295,52.368552 L 60.337621,52.704708 L 60.007424,55.73011 L 62.318803,58.755513 L 64.134886,57.747045 L 66.446264,59.427825 M 86.341089,9.169955 L 88.404826,9.001877 L 88.900121,10.430545 L 90.468562,8.7497548 L 92.862495,8.7497548 L 93.687987,10.3465 L 92.119546,12.111324 L 92.779951,12.951724 L 92.037002,15.052704 L 90.63366,15.472893 C 90.63366,15.472893 89.725613,15.556938 89.725613,15.220782 C 89.725613,14.884626 91.21151,12.531524 91.21151,12.531524 L 89.477971,11.943246 L 89.147774,13.455958 L 88.404826,14.12827 L 86.836385,11.775168 L 86.341089,9.169955 z " id="WA_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 66.446264,59.427825 L 60.172522,61.44476 L 57.861144,68.167876 L 54.559175,78.588708 L 51.257205,85.311824 L 46.139153,99.934604 L 39.535214,114.05315 L 31.28029,127.16323 L 29.299109,130.18863 L 28.473616,139.09676 L 27.152829,145.31564 L 29.464207,149.85375" id="OR_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 639.33795,481.63956 L 642.14462,481.63956 L 642.80502,481.80764 L 644.12581,478.95032 L 645.61169,474.41221 L 647.92307,475.08453 L 651.05994,481.30341 L 651.05994,482.31188 L 648.25327,484.32881 L 651.05994,484.66497 L 658.02586,481.83647" id="AL_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 616.38926,488.36268 L 617.71005,487.52229 L 620.35163,483.15227 L 622.16771,483.99266 L 629.10185,481.97572 L 631.24813,482.31188 L 632.73401,483.15227 L 638.01716,483.15227 L 639.33795,481.63956" id="MS_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:path d="M 533.67494,503.06949 L 538.62788,501.47276 L 546.88281,501.1366 L 557.44911,504.83432 L 564.05305,506.01086 L 567.85031,504.49816 L 571.15228,505.67471 L 574.45425,506.68317 L 575.27974,504.49816 L 571.97778,503.32162 L 569.3362,503.82585 L 566.52953,502.14507 C 566.52953,502.14507 566.69462,500.80045 567.35502,500.63237 C 568.01541,500.46429 570.49189,499.6239 570.49189,499.6239 L 572.30797,501.1366 L 574.12406,500.12814 L 577.42603,500.80045 L 578.91191,503.32162 L 579.24211,505.67471 L 583.86487,506.01086 L 585.68095,507.85972 L 584.85546,509.5405 L 583.53467,510.38089 L 585.18565,512.06167 L 593.77077,515.75938 L 597.40294,514.41476 L 598.39353,511.89359 L 601.03511,511.22128 L 602.85119,509.70858 L 604.17198,510.71704 L 604.99747,513.74245 L 602.68609,514.58284 L 603.34648,515.25515 L 606.81355,513.91053 L 609.12493,510.38089 L 609.95042,509.87666 L 607.80414,509.5405 L 608.62964,507.85972 L 608.46454,506.34702 L 610.61082,505.84279 L 611.76651,504.49816 L 612.4269,505.33855 C 612.4269,505.33855 612.2618,508.53203 613.08729,508.53203 C 613.91279,508.53203 617.37985,509.20434 617.37985,509.20434 L 621.50732,511.22128 L 622.49791,512.73398 L 625.46968,512.73398 L 626.62537,513.74245 L 628.93675,510.54897 L 628.93675,509.03627 L 627.61596,509.03627 L 624.14889,506.17894 L 618.20535,505.33855 L 614.90338,502.98546 L 616.05907,500.12814 L 618.37045,500.46429 L 618.53554,499.79198 L 616.71946,498.78351 L 616.71946,498.27928 L 620.02143,498.27928 L 621.83751,495.0858 L 620.51673,493.06886 L 620.18653,490.21154 L 618.70064,490.37962 L 616.71946,492.56463 L 616.05907,495.25388 L 612.9222,494.58156 L 611.93161,492.73271 L 613.74769,490.71577 L 616.38926,488.36268 L 617.2973,487.77441" id="LA_Gulf" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1.06612186pt" /> + <ns0:g id="Great_Lakes_borders" style="fill:none;stroke:#80b0f0" transform="matrix(1.0566302,0,0,1.0756987,-45.325399,-166.80506)"> + <ns0:path d="M 652.1875,357.8125 L 649.84375,359.21875 L 647.8125,361.09375 L 646.71875,361.40625 L 645.3125,360.46875 L 642.18749,359.53125" id="IN_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 712.53906,338.43751 L 712.8125,334.6875 L 714.29687,331.75782 L 715.50782,330.39062 M 717.85156,323.16407 L 714.6875,315 L 712.5,306.25 L 710.15625,303.125 L 707.65625,301.40625 L 706.09375,302.5 L 702.34375,304.21875 L 700.46875,309.0625 L 697.8125,312.65625 L 696.71875,313.28125 L 695.3125,312.65625 C 695.3125,312.65625 692.8125,311.25 692.96875,310.625 C 693.125,310 693.4375,305.78125 693.4375,305.78125 L 696.71875,304.53125 L 697.5,301.25 L 698.125,298.75 L 700.46875,297.1875 L 700.15625,287.5 L 698.59375,285.3125 L 697.34375,284.53125 L 696.5625,282.5 L 697.34375,281.71875 L 698.90625,282.03125 L 699.0625,280.46875 L 696.71875,278.28125 L 695.46875,275.78125 L 692.96875,275.78125 L 688.59375,274.375 L 683.28125,271.09375 L 680.625,271.09375 L 680,271.71875 L 679.0625,271.25 L 676.09375,269.0625 L 673.28125,270.78125 L 670.46875,272.96875 L 670.78125,276.40625 L 671.71875,276.71875 L 673.75,277.1875 L 674.21875,277.96875 L 671.71875,278.75 L 669.21875,279.0625 L 667.8125,280.78125 L 667.5,282.8125 L 667.8125,284.375 L 668.125,289.6875 L 664.6875,291.71875 L 664.0625,291.5625 L 664.0625,287.5 L 665.3125,285.15625 L 665.9375,282.8125 L 665.15625,282.03125 L 663.28125,282.8125 L 662.34375,286.875 L 659.6875,287.96875 L 657.96875,289.84375 L 657.8125,290.78125 L 658.4375,291.5625 L 657.8125,294.0625 L 655.625,294.53125 L 655.625,295.625 L 656.40625,297.96875 L 655.3125,303.90625 L 653.75,307.8125 L 654.375,312.34375 L 654.84375,313.4375 L 654.0625,315.78125 L 653.75,316.5625 L 653.4375,319.21875 L 656.875,325 L 659.6875,331.25 L 661.09375,335.9375 L 660.3125,340.46875 L 659.375,346.25 L 657.03125,351.25 L 656.71875,353.90625 L 654.84375,356.25 L 652.1875,357.8125 M 605.4621,230.97629 L 607.22987,228.98755 L 609.3291,228.21415 L 614.52193,224.45763 L 616.73164,223.9052 L 617.17359,224.34715 L 612.20173,229.31901 L 608.99764,231.19726 L 607.0089,232.08115 L 605.4621,230.97629 z M 634.68749,287.50003 L 638.28125,279.6875 L 639.21875,275.78125 L 641.09375,271.5625 L 641.875,271.40625 L 642.96875,272.96875 L 643.59375,272.96875 L 647.96875,270.625 L 649.375,272.1875 L 649.84375,272.34375 L 651.09375,271.25 L 652.1875,268.28125 L 654.53125,267.5 L 661.25,266.875 L 663.125,264.375 L 668.125,264.21875 L 673.75,265.46875 L 675.46875,265.46875 L 678.59375,264.0625 L 680.78125,264.21875 L 682.8125,263.59375 L 686.40625,264.0625 L 687.1875,264.375 L 688.4375,264.0625 L 687.1875,263.125 L 685.9375,262.5 L 682.8125,259.53125 L 682.8125,252.8125 L 681.40625,252.34375 L 680.3125,253.4375 L 674.375,255 L 672.5,255.46875 L 669.6875,254.6875 L 669.21875,254.375 L 669.21875,248.90625 L 667.8125,248.75 L 665.3125,250 L 660.9375,251.875 L 654.53125,252.1875 L 651.25,253.28125 L 647.34375,256.71875 L 645.78125,257.65625 L 644.6875,257.65625 L 643.4375,258.4375 L 641.875,257.96875 L 640.3125,256.71875 L 638.90625,257.65625 L 635.15625,257.8125 L 632.5,255.15625 L 631.09375,252.1875 L 629.6875,251.09375 L 626.5625,250.15625 L 624.375,250.15625 L 623.125,248.90625 L 619.6875,251.71875 L 618.75,252.8125 L 617.96875,252.34375 L 618.28125,249.84375 L 620.625,246.71875 L 621.09375,244.375 L 623.28125,243.59375 L 624.6875,240.625 L 628.28125,239.6875 L 628.59375,238.75 L 627.5,237.65625 L 622.96875,238.125 L 618.75,240.46875 L 616.5625,242.65625 L 615.3125,244.375 L 613.59375,245.15625 L 611.71875,247.96875 L 611.5625,249.21875 L 607.34375,251.25 L 605,253.125 L 599.21875,254.0625 L 598.59375,254.6875 L 598.59375,255.625 L 595.15625,257.8125 L 592.5,258.59375 L 590.9375,259.53125 M 688.75238,262.0292 L 689.37738,264.45108 L 692.50239,264.60733 L 693.7524,263.43545 C 693.7524,263.43545 693.67427,262.0292 693.36177,261.87295 C 693.04927,261.7167 691.79927,260.07607 691.79927,260.07607 L 689.68989,260.31044 L 688.12738,260.46669 L 687.81488,261.56045 L 688.75238,262.0292 z M 707.34375,352.8125 L 706.09375,351.5625 L 706.25,350.15625 L 708.28125,346.5625 L 710.42969,344.60937" id="MI_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 762.03126,331.40624 L 756.5625,336.875 L 755.3125,337.34375 L 751.25001,340.46875" id="PA_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 773.4375,318.28125 L 773.59375,319.21875 L 772.5,320.15625 L 770.46875,322.8125 L 770,324.375 L 768.125,326.09375 L 766.40625,327.1875 L 765.46875,328.75 L 764.21875,329.84375 L 761.56251,331.71874 M 823.75,260.46875 L 821.25,262.34375 L 819.21875,264.6875 L 816.5625,268.28125 L 813.75,272.65625 L 812.34375,275.46875 L 811.71875,276.25 L 806.09375,281.5625 L 806.25,284.0625 L 807.03125,285.15625 L 808.75,285.9375 L 810.46875,285.9375 L 810.46875,287.34375 L 809.375,289.375 L 809.6875,290.78125 L 811.09375,292.8125 L 810.9375,295 L 809.0625,296.09375 L 807.03125,296.09375 L 805.46875,297.96875 L 803.75,301.09375 L 801.71875,302.8125 L 796.71875,303.28125 L 794.21875,304.375 L 792.1875,305.625 L 790.625,305.46875 L 788.75,304.21875 L 782.65625,304.375 L 779.53125,304.84375 L 775.625,306.09375 L 771.40625,307.5 L 768.59375,309.21875" id="NY_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 751.40626,340.46875 L 744.375,344.0625 L 740.625,346.25 L 737.34375,349.84375 L 733.4375,353.59375 L 730.3125,354.375 L 727.5,354.84375 L 722.1875,357.34375 L 720.15625,357.5 L 716.875,354.53125 L 711.875,355.15625 L 709.375,353.75 L 706.71874,352.34374" id="OH_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 642.5,359.6875 L 641.25,358.90625 L 640.46875,356.40625 L 639.21875,352.8125 L 637.65625,351.09375 L 636.25,348.59375 L 636.09375,343.12504" id="IL_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 590.9375,259.53125 L 590.3125,260.78125 L 589.21875,260.625 L 588.59375,259.53125 L 585.9375,258.75 L 584.84375,258.90625 L 583.125,259.84375 L 582.1875,259.21875 L 582.8125,257.34375 L 584.6875,254.375 L 585.78125,253.28125 L 583.90625,251.875 L 581.875,252.65625 L 579.0625,254.53125 L 571.875,257.65625 L 569.0625,258.28125 L 566.25,257.8125 L 565.46875,256.875 M 636.25,343.43744 L 636.09375,339.375 L 634.53125,335 L 633.90625,329.0625 L 632.8125,326.71875 L 633.75,323.75 L 634.53125,320.9375 L 635.9375,318.4375 L 635.3125,315.15625 L 634.6875,311.71875 L 635.15625,310 L 637.03125,307.65625 L 637.1875,305 L 636.40625,303.75 L 637.03125,301.25 L 637.5,298.125 L 640.15625,292.65625 L 642.96875,286.09375 L 643.125,283.90625 L 642.8125,282.96875 L 642.03125,283.4375 L 637.96875,289.53125 L 635.3125,293.4375 L 633.4375,295.15625 L 632.65625,297.34375 L 631.25,298.125 L 630.15625,300 L 628.75,299.6875 L 628.59375,297.96875 L 629.84375,295.625 L 631.875,291.09375 L 633.59375,289.53125 L 634.68749,287.03127" id="WI_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:path d="M 565.9375,257.34374 L 565.3125,256.5625 L 568.59375,253.59375 L 569.84375,253.4375 L 574.21875,248.59375 L 575.9375,247.8125 L 578.125,244.0625 L 580.46875,240.625 L 583.4375,238.125 L 587.5,236.40625 L 595,232.8125 L 597.8125,232.03125 C 597.8125,232.03125 600.78125,230.3125 601.09375,229.6875 C 601.40625,229.0625 601.71875,228.28125 601.71875,228.28125" id="MN_Great_Lakes" style="fill:none;stroke:#80b0f0;stroke-width:1pt" /> + </ns0:g> + <ns0:path d="M 152.15345,458.16063 L 151.84095,540.66102 L 153.40345,541.59852 L 156.37222,541.75477 L 157.77847,540.66102 L 160.27848,540.66102 L 160.43474,543.47353 L 167.15352,550.03606 L 167.62227,552.53607 L 170.90353,550.66106 L 171.52854,550.50481 L 171.84104,547.53605 L 173.24729,545.97354 L 174.34105,545.81729 L 176.21606,544.41103 L 179.18482,546.44229 L 179.80983,549.25481 L 181.68483,550.34856 L 182.77859,552.69232 L 186.52861,554.41108 L 189.80987,560.19236 L 192.46613,563.94237 L 194.65364,566.59864 L 196.0599,570.1924 L 200.90367,571.91116 L 205.9037,573.94242 L 206.8412,578.16119 L 207.30995,581.12995 L 206.37245,584.41122 L 204.65369,586.59873 L 203.09118,585.81748 L 201.68493,582.84871 L 199.02866,581.44246 L 197.30991,580.3487 L 196.52865,581.12995 L 197.93491,583.78622 L 198.09116,587.37998 L 196.9974,587.84873 L 195.1224,585.97373 L 193.09114,584.72372 L 193.55989,586.28623 L 194.80989,588.00499 L 194.02864,588.78624 C 194.02864,588.78624 193.24739,588.47374 192.77864,587.84873 C 192.30988,587.22373 190.74738,584.56747 190.74738,584.56747 L 189.80987,582.37996 C 189.80987,582.37996 189.49737,583.62997 188.87237,583.31746 C 188.24736,583.00496 187.62236,581.91121 187.62236,581.91121 L 189.34112,580.0362 L 187.93486,578.62994 L 187.93486,573.78617 L 187.15361,573.78617 L 186.37236,577.06743 L 185.2786,577.53619 L 184.3411,573.94242 L 183.71609,570.34865 L 182.93484,569.8799 L 183.24734,575.34868 L 183.24734,576.44243 L 181.84108,575.19243 L 178.40357,569.41115 L 176.37231,568.9424 L 175.74731,565.34863 L 174.1848,562.53612 L 172.62229,561.44236 L 172.62229,559.25485 L 174.65355,558.00485 L 174.1848,557.69235 L 171.68479,558.31735 L 168.40352,555.97359 L 165.90351,553.16107 L 161.21599,550.66106 L 157.30972,548.16105 L 158.55973,545.03604 L 158.55973,543.47353 L 156.84097,545.03604 L 154.02846,546.12979 L 150.43469,545.03604 L 144.96591,542.69228 L 139.65339,542.69228 L 139.02839,543.16103 L 132.77836,539.41101 L 130.7471,539.09851 L 128.09084,533.47348 L 124.65332,533.78598 L 121.2158,535.19224 L 121.68456,539.56726 L 122.77831,536.75475 L 123.71582,537.06725 L 122.30956,541.28602 L 125.43457,538.62976 L 126.05958,540.19226 L 122.30956,544.41103 L 121.05955,544.09853 L 120.5908,542.22352 L 119.3408,541.44227 L 118.09079,542.53603 L 115.43453,540.81727 L 112.46576,542.84853 L 110.74701,544.87979 L 107.46574,546.91105 L 102.93447,546.75479 L 102.46572,544.72354 L 106.05948,544.09853 L 106.05948,542.84853 L 103.87197,542.22352 L 104.80948,539.87976 L 106.99699,536.12975 L 106.99699,534.41099 L 107.15324,533.62973 L 111.37201,531.44222 L 112.30951,532.69223 L 114.96578,532.69223 L 113.71577,530.19222 L 110.122,529.87972 L 105.27823,532.53598 L 102.93447,535.81724 L 101.21571,538.31726 L 100.12196,540.50477 L 96.059441,541.91102 L 93.090671,544.41103 L 92.778171,545.97354 L 94.965681,546.91105 L 95.746941,548.9423 L 93.090671,552.06732 L 86.840651,556.12984 L 79.340608,560.19236 L 77.309348,561.28611 L 72.153076,562.37987 L 66.996796,564.56738 L 68.715556,565.81738 L 67.309296,567.22364 L 66.840546,568.31739 L 64.184286,567.37989 L 61.059276,567.53614 L 60.278016,569.72365 L 59.340516,569.72365 L 59.653016,567.37989 L 56.215496,568.6299 L 53.402986,569.5674 L 50.121721,568.31739 L 47.309208,570.1924 L 44.184194,570.1924 L 42.152934,571.44241 L 40.590427,572.22366 L 38.559168,571.91116 L 36.059156,570.81741 L 33.871646,571.44241 L 32.934142,572.37991 L 31.371634,571.28616 L 31.371634,569.41115 L 34.340398,568.16114 L 40.434176,568.78615 L 44.652946,567.22364 L 46.684205,565.19238 L 49.496718,564.56738 L 51.215476,563.78612 L 53.871739,563.94237 L 55.434246,565.19238 L 56.371746,564.87988 L 58.559256,562.22362 L 61.528026,561.28611 L 64.809286,560.66111 L 66.059296,560.34861 L 66.684296,560.81736 L 67.465556,560.81736 L 68.715556,557.22359 L 72.621826,555.81734 L 74.496836,552.22357 L 76.684348,547.84855 L 78.246858,546.44229 L 78.559358,543.94228 L 76.996848,545.19229 L 73.715576,545.81729 L 73.090576,543.47353 L 71.840576,543.16103 L 70.903066,544.09853 L 70.746816,546.91105 L 69.340556,546.75479 L 67.934306,541.12977 L 66.684296,542.37977 L 65.590546,541.91102 L 65.278046,540.03601 L 61.371776,540.19226 L 59.340516,541.28602 L 56.840506,540.97352 L 58.246756,539.56726 L 58.715506,537.06725 L 58.090506,535.19224 L 59.496766,534.25474 L 60.746766,534.09849 L 60.121766,532.37973 L 60.121766,528.16096 L 59.184266,527.22345 L 58.403006,528.62971 L 52.465482,528.62971 L 51.059226,527.3797 L 50.434223,523.62969 L 48.402963,520.19217 L 48.402963,519.25467 L 50.434223,518.47341 L 50.590473,516.44215 L 51.684228,515.3484 L 50.902975,514.87965 L 49.652969,515.3484 L 48.559214,512.69214 L 49.496718,507.84836 L 53.871739,504.72335 L 56.371746,503.16084 L 58.246756,499.56708 L 60.903026,498.31707 L 63.403036,499.41083 L 63.715536,501.75459 L 66.059296,501.44208 L 69.184306,499.09832 L 70.746816,499.72333 L 71.684316,500.34833 L 73.246826,500.34833 L 75.434338,499.09832 L 76.215598,494.87955 C 76.215598,494.87955 76.528098,492.06704 77.153098,491.59829 C 77.778098,491.12954 78.090598,490.66079 78.090598,490.66079 L 76.996848,488.78578 L 74.496836,489.56703 L 71.371816,490.34828 L 69.496806,489.87953 L 66.059296,488.16077 L 61.215526,488.00452 L 57.778006,484.41076 L 58.246756,480.66074 L 58.871766,478.31698 L 56.840506,476.59822 L 54.965494,473.00445 L 55.434246,472.2232 L 61.996776,471.75445 L 64.028036,471.75445 L 64.965536,472.69195 L 65.590546,472.69195 L 65.434296,471.12944 L 69.184306,470.50444 L 71.684316,470.81694 L 73.090576,471.9107 L 71.684316,473.94196 L 71.215566,475.34821 L 73.871836,476.91072 L 78.715608,478.62948 L 80.434368,477.69198 L 78.246858,473.47321 L 77.309348,470.34819 L 78.246858,469.56694 L 74.965588,467.69193 L 74.496836,466.59817 L 74.965588,465.03567 L 74.184336,461.28565 L 71.371816,456.75438 L 69.028056,452.69186 L 71.840576,450.81685 L 74.965588,450.81685 L 76.684348,451.44185 L 80.746868,451.2856 L 84.340631,447.84809 L 85.434391,444.87932 L 89.028161,442.53556 L 90.590661,443.47307 L 93.246921,442.84806 L 96.840691,440.8168 L 97.934451,440.66055 L 98.871951,441.44181 L 103.24697,441.28556 L 105.90323,438.31679 L 106.99699,438.31679 L 110.4345,440.66055 L 112.30951,442.69181 L 111.84076,443.78557 L 112.46576,444.87932 L 114.02827,443.31682 L 117.77829,443.62932 L 118.09079,447.22308 L 119.9658,448.62934 L 126.84083,449.25434 L 132.93461,453.31686 L 134.34086,452.37936 L 139.34089,454.87937 L 141.37215,454.25437 L 143.24716,453.47311 L 147.93468,455.34812 L 152.15345,458.16063 z M 40.902929,486.12951 L 42.934188,491.28579 L 42.777937,492.22329 L 39.965424,491.91079 L 38.246666,488.00452 L 36.527908,486.59827 L 34.184147,486.59827 L 34.027897,484.09825 L 35.746655,481.75449 L 36.84041,484.09825 L 38.246666,485.50451 L 40.902929,486.12951 z M 38.402917,518.47341 L 41.996684,519.25467 L 45.59045,520.19217 L 46.371704,521.12968 L 44.809197,524.72344 L 41.840433,524.56719 L 38.559168,521.12968 L 38.402917,518.47341 z M 18.402824,504.8796 L 19.49658,507.37961 L 20.590335,508.94212 L 19.49658,509.72337 L 17.46532,506.75461 L 17.46532,504.8796 L 18.402824,504.8796 z M 5.1215129,575.50493 L 8.4027779,573.31742 L 11.684043,572.37991 L 14.184055,572.69241 L 14.652807,574.25492 L 16.527816,574.72367 L 18.402824,572.84867 L 18.090323,571.28616 L 20.746585,570.66116 L 23.559098,573.16117 L 22.465343,574.87992 L 18.246574,575.97368 L 15.590311,575.50493 L 11.996545,574.41117 L 7.7777749,575.81743 L 6.2152679,576.12993 L 5.1215129,575.50493 z M 52.465482,571.12991 L 54.027989,573.00492 L 56.059246,571.44241 L 54.652992,570.1924 L 52.465482,571.12991 z M 55.277995,574.09867 L 56.371746,571.91116 L 58.403006,572.22366 L 57.621756,574.09867 L 55.277995,574.09867 z M 78.090598,572.22366 L 79.496858,573.94242 L 80.434368,572.84867 L 79.653108,570.97366 L 78.090598,572.22366 z M 86.528141,560.19236 L 87.621901,565.81738 L 90.434411,566.59864 L 95.278181,563.78612 L 99.496951,561.28611 L 97.934451,558.94235 L 98.403201,556.59859 L 96.371941,557.8486 L 93.559431,557.06734 L 95.121931,555.97359 L 96.996941,556.75484 L 100.74696,555.03608 L 101.21571,553.62983 L 98.871951,552.84857 L 99.653201,550.97356 L 96.996941,552.84857 L 92.465671,556.28609 L 87.778151,559.0986 L 86.528141,560.19236 z M 127.46583,540.97352 L 129.80959,539.56726 L 128.87209,537.8485 L 127.15333,538.78601 L 127.46583,540.97352 z " id="AK" style="fill:#f4d7d7" /> + <ns0:g id="g16325" style="stroke:#000000;stroke-opacity:1"> + <ns0:g id="g5778" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:path d="M 816.92419,258.09425 C 817.71859,258.70718 818.14466,259.56702 819.77271,260.56631 L 819.77271,260.61385" id="path6654" style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#000000;stroke-width:1.33265233;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" ns1:nodetypes="ccc" /> + <ns0:g id="g4679" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:g id="g3580" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:g id="State_borders_old" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1" transform="matrix(1.0566302,0,0,1.0756987,-45.325399,-166.80506)"> + <ns0:path d="M 389.29574,462.33445 L 395.75915,462.99736 L 406.8077,463.54979" id="CO_OK" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 389.79293,462.72114 L 389.2405,473.88019" id="NM_OK" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 389.90342,473.82495 L 388.24613,473.82494 L 386.69931,491.94483 L 382.94283,545.64053 L 380.73312,568.62152 L 352.66979,567.07472 L 324.60654,564.42309 L 316.87248,563.76016 L 317.5354,568.62152" id="NM_TX" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 299.63678,367.31684 L 319.96612,369.74752 L 355.98408,373.72497 L 379.62831,375.71374" id="WY_CO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 379.62831,375.71374 L 411.89008,378.36539 L 410.34328,400.02056" id="NE_CO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 410.34328,400.02056 L 406.36581,463.66023" id="CO_KS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 379.62831,375.71374 L 383.82676,332.84535" id="WY_NE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 383.82676,332.84535 L 385.37355,308.31756 L 386.63467,290.78272" id="WY_SD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 386.7128,291.15996 L 388.23277,275.63418 L 388.81756,270.31054" id="MT_SD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 388.81755,270.85742 L 389.59881,259.06782 L 391.78171,234.95517 L 393.10754,220.37107 L 394.43337,206.67087" id="MT_ND" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 389.13006,270.75248 L 443.26797,274.28802 L 499.6156,275.83481" id="ND_SD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 490.55578,211.09029 L 492.32355,216.17262 L 491.66064,219.92913 L 491.66064,229.87283 L 493.20744,234.73419 L 494.97521,238.04876 L 495.41715,247.55052 L 497.18492,260.58781 L 498.95269,267.65888 L 499.39463,275.83481" id="ND_MN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 499.39463,275.83481 L 499.17366,278.92841 L 497.84783,280.25424 L 495.85909,281.80103 L 495.85909,283.34783 L 496.96395,285.1156 L 500.94143,288.43017 L 501.38337,291.30279 L 501.60434,320.69194 L 501.1624,327.32107" id="SD_MN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 501.1624,327.32107 L 499.39463,327.32107 L 498.51074,329.75176 L 498.95269,332.40341 L 501.60434,334.39215 L 500.27851,339.91643 L 498.51074,344.11488 L 500.05754,346.76653 L 501.60434,348.97624" id="SD_IA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 501.60434,348.97624 L 499.83657,348.97624 L 498.0688,348.97624 L 497.18492,346.9875 L 496.74297,345.4407 L 493.87035,343.89391 L 489.00899,342.34711 L 487.24122,341.02128 L 484.58957,341.24225 L 480.83306,341.68419 L 476.63461,343.01002 L 475.52975,342.12614 L 470.88936,339.25351 L 468.90062,337.0438 L 458.51498,337.48574 L 435.53399,336.38089 L 418.29824,335.27603 L 398.85279,334.17118 L 383.82676,333.50826" id="SD_NE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 500.94143,327.32107 L 512.21095,327.32107 L 560.16167,326.65816 L 577.61839,325.99525 L 581.59587,325.99525" id="MN_IA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 581.59587,325.99525 L 580.71198,318.7032 L 578.72324,316.05155 L 571.87314,312.07407 L 568.11663,306.54979 L 564.80207,306.10785 L 563.0343,303.01426 L 559.71973,303.01426 L 556.84711,300.14163 L 556.40516,293.73347 L 556.40516,289.97696 L 558.17293,286.88337 L 557.51002,283.78977 L 555.07934,281.80103 L 554.6374,281.58006 L 556.84711,275.83481 L 561.92944,272.07831 L 563.0343,270.53151 L 563.0343,262.35558 L 563.25527,259.70393 L 566.01741,256.8313" id="MN_WI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 590.71092,259.59344 L 594.85413,264.78626 L 602.80909,265.89112 L 610.76405,268.10083 L 613.19473,269.20568 L 625.34814,271.85734 L 626.67397,274.06705 L 630.2095,275.1719 L 631.7563,285.1156 L 633.08213,286.44143 L 634.62893,287.60152" id="WI_MI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 581.59587,325.77428 L 582.25878,329.08884 L 584.46849,330.63564 L 584.68946,331.96147 L 582.70072,335.27603 L 582.92169,338.36963 L 585.35238,342.12614 L 587.78306,343.23099 L 590.65568,343.67293 L 591.92627,346.3246" id="IA_WI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 591.76054,345.88265 L 601.04136,346.10361 L 626.453,344.55682 L 635.95475,343.45196" id="WI_IL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 591.76054,345.71692 L 591.98151,348.09236 L 594.19122,348.75527 L 595.0751,349.86012 L 595.51704,351.62789 L 599.27355,354.94246 L 599.93647,357.15217 L 599.27355,360.46674 L 597.50578,364.00227 L 596.84287,366.43295 L 594.63316,368.20072 L 592.86539,368.86364 L 587.78306,370.18946 L 587.12014,371.95723 L 586.45723,373.94597 L 587.12014,375.2718 L 588.88791,376.8186 L 588.66694,380.79607 L 586.89917,382.34287 L 586.23626,383.88967 L 586.23626,386.54132 L 584.46849,386.98326 L 582.92169,388.08812 L 582.70072,389.41395 L 582.92169,391.40269 L 581.15393,393.05996" id="IA_IL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 581.3749,393.17045 L 578.06033,389.85589 L 576.95547,387.64618 L 569.44246,388.30909 L 559.9407,388.75103 L 535.41291,389.63492 L 522.37562,389.85589 L 513.31581,390.07686 L 511.98998,390.07686" id="IA_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 512.53686,390.31124 L 510.66415,384.99453 L 510.44318,378.58636 L 508.89638,374.60888 L 508.23347,369.52655 L 506.02376,365.99101 L 505.13988,361.35062 L 502.48822,354.05857 L 501.1624,348.75527" id="NE_IA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 410.12231,399.79959 L 431.33554,400.68347 L 470.44713,402.00929 L 513.09483,402.45124 L 519.06105,402.45124" id="NE_KS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 519.06105,402.45124 L 515.96746,398.47376 L 513.53678,394.71725 L 513.75775,392.50754 L 512.43192,390.07686" id="NE_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 518.84008,402.45124 L 521.93368,405.10289 L 524.14339,405.32386 L 525.46921,406.20775 L 525.46921,409.08037 L 523.70145,410.62717 L 523.2595,412.83688 L 525.24824,416.15145 L 527.67893,419.02407 L 530.10961,420.79184 L 531.43543,432.06136 L 530.77252,466.53285" id="KS_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 530.77252,466.53285 L 517.95614,467.19576 L 473.09935,466.75383 L 429.56781,464.76507 L 406.08959,463.43925" id="KS_OK" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 388.4119,473.88015 L 410.34328,474.92975 L 440.39535,476.03461 L 439.06952,499.0156 L 438.62758,516.25134 L 438.84855,517.79814 L 443.047,521.33368 L 445.03574,522.43853 L 445.69866,522.21756 L 446.36157,520.22882 L 447.6874,521.99659 L 449.67614,521.99659 L 449.67614,520.67076 L 452.32779,521.99659 L 451.88585,525.7531 L 455.86333,525.97407 L 458.29401,527.07893 L 462.27149,527.74184 L 464.70217,529.50961 L 466.91188,527.52087 L 470.22645,528.18378 L 472.65713,531.49835 L 473.54101,531.49835 L 473.54101,533.70806 L 475.75072,534.37097 L 477.96043,532.16126 L 479.7282,532.82417 L 482.15888,532.82417 L 483.04277,535.25486 L 487.68316,537.02262 L 489.00899,536.35971 L 490.77676,532.38223 L 491.88161,532.38223 L 492.98647,534.37097 L 496.96395,535.03388 L 500.49948,536.35971 L 503.37211,537.2436 L 505.13988,536.35971 L 505.80279,533.92903 L 510.00124,533.92903 L 511.98998,534.81291 L 514.64163,532.82417 L 515.74649,532.82417 L 516.4094,534.37097 L 520.38688,534.37097 L 521.93368,532.38223 L 523.70145,532.82417 L 525.69019,535.25486 L 528.78378,537.02262 L 531.87738,537.90651 L 534.52903,539.45331" id="OK_TX" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 534.52903,539.45331 L 534.30806,502.55125 L 532.98222,491.94454 L 532.98223,483.98957 L 531.43543,477.36043" id="OK_AR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 531.43543,477.36043 L 530.99349,470.7313 L 530.77252,465.86994" id="OK_MO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 531.43543,477.36043 L 553.97448,476.91849 L 576.51353,476.25558 L 596.6219,475.37169 L 607.22851,474.92975 L 608.99628,477.80238 L 608.55434,480.01209 L 605.46074,482.66374 L 604.79783,485.53636 L 610.76405,485.97831 L 615.62541,485.31539" id="MO_AR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 615.62541,485.31539 L 617.61415,479.1282 L 617.61415,473.38295" id="MO_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 617.33793,473.99063 L 620.26581,472.2781 L 621.59163,470.7313 L 623.58037,469.62645 L 623.80134,466.53285 L 624.68523,464.76508 L 623.13842,462.27915" id="MO_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 623.58037,462.55537 L 620.48678,462.77634 L 618.71901,460.7876 L 617.17221,456.81012 L 618.05609,453.9375 L 616.06735,450.84391 L 614.07862,445.98254 L 609.65919,445.31963 L 603.472,440.23729 L 601.26229,436.48079 L 601.92521,433.38719 L 604.13492,427.64194 L 604.79783,424.10641 L 602.14618,422.78058 L 595.95899,422.33864 L 595.0751,421.01281 L 595.29607,416.81436 L 589.99277,413.27882 L 582.92169,405.98678 L 580.71198,398.9157 L 580.27004,395.38017 L 581.3749,392.28657" id="MO_IL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 534.52903,538.79039 L 536.73874,541.0001 L 539.61136,539.67428 L 542.26302,540.77913 L 542.92593,551.38574" id="TX_AR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 542.92593,551.38574 L 542.92593,560.8875 L 543.58884,569.94731 L 544.25176,573.70382 L 546.68244,577.6813 L 547.56632,582.54267 L 551.76477,587.84597 L 551.98574,590.93957 L 552.64866,591.60248 L 551.98574,599.77841 L 549.11312,604.63977 L 550.65992,606.62851 L 549.997,609.05919 L 549.33409,616.13027 L 548.00826,619.22386 L 548.28448,622.70416" id="TX_LA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 542.87069,551.88293 L 564.36012,551.60672 L 582.92169,550.72283 L 593.30733,550.72283" id="AR_LA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 593.52831,550.72283 L 593.74928,556.02614 L 595.73802,560.44556 L 595.51704,562.21333 L 597.72676,563.9811 L 597.06384,567.07469 L 595.95899,567.07469 L 596.84287,569.06343 L 591.76054,577.90227 L 588.44597,583.86849 L 587.78306,591.60248 L 588.225,593.14928 L 611.64793,592.48636 L 619.60289,591.82345 L 621.37066,591.60248 L 622.03357,592.48636 L 620.92872,599.77841 L 624.24328,602.872 L 625.34814,606.62851 L 626.61872,609.00395" id="LA_MS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 593.41781,550.99905 L 594.79888,548.29215 L 594.41219,544.0937 L 593.08636,541.22107 L 594.41219,539.89525 L 593.08636,537.90651 L 593.52831,536.13874 L 594.41219,530.17252 L 597.28481,527.52087 L 596.6219,525.53213 L 600.15744,520.44979 L 602.80909,519.56591 L 602.80909,517.13523 L 602.14618,515.8094 L 604.79783,510.72707 L 607.44948,509.62221 L 607.44948,506.08667" id="AR_MS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 642.50096,359.68676 L 643.28221,370.93678 L 644.53221,385.78055 L 645.78222,401.24932 L 646.25097,407.96808 L 645.46972,413.28059 L 645.78222,416.40559 L 647.50097,419.06185 L 647.96972,424.68686 L 645.46972,428.74936 L 643.75096,432.49937 L 641.56346,435.31187 L 641.09471,439.68688 L 641.09471,443.28063" id="IL_IN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 641.09471,443.28063 L 640.15721,446.56189 L 639.06346,447.34314 L 639.68846,449.21814 L 640.46971,450.9369 L 638.90721,451.5619 L 636.4072,452.1869 L 634.5322,453.59315 L 634.2197,455.78065 L 635.4697,458.12441 L 635.31345,460.31191 L 634.2197,460.62441 L 630.78219,459.37441 L 628.12594,458.12441 L 626.09469,458.74941 L 623.90718,460.46816 L 623.12593,462.34316" id="IL_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 641.25096,443.43688 L 644.06346,441.71813 L 649.68847,440.78063 L 653.12598,440.31188 L 654.53223,442.18688 L 656.25098,442.96813 L 657.96973,439.84313 L 660.78224,438.43688 L 662.65724,439.99938 L 663.43849,441.09313 L 665.46975,440.62438 L 665.31349,437.34313 L 668.126,435.78062 L 669.21975,434.99937 L 670.3135,436.56187 L 674.84476,436.56187 L 675.62601,434.53062 L 675.31351,432.34312 L 678.12601,428.90561 L 682.65727,425.15561 L 683.12602,420.7806 L 685.78228,420.4681 L 689.53228,418.74935 L 692.18854,416.87434 L 691.87603,414.99934 L 690.46978,413.59309 L 690.93853,411.40559" id="IN_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 690.93853,411.40559 L 690.46978,405.93683 L 687.96978,383.28054 L 686.25103,369.99927 L 684.84477,355.7805" id="IN_OH" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 684.84477,355.7805 L 684.68852,354.68675 L 678.75102,355.31175 L 657.50098,357.49926 L 652.96973,357.49926" id="IN_MI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 615.46967,485.07202 L 614.58577,488.10878 L 613.28217,492.34321 L 612.96967,494.84321 L 609.06341,497.03071 L 610.46966,500.46822 L 609.53216,504.99948 L 606.87591,506.09323" id="AR_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 607.65716,506.56198 L 615.93842,506.24948 L 639.21971,504.37448 L 642.96971,504.21823" id="TN_MS" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 644.51654,503.77628 L 644.53221,510.31198 L 644.68846,526.40576 L 643.90721,556.4058 L 643.75096,569.99957 L 646.40722,588.1246 L 647.9524,603.33611" id="MS_AL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 642.65721,504.21823 L 652.18848,503.74947 L 679.06352,501.24947 L 689.03673,500.46822" id="TN_AL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 199.86827,240.25868 L 200.31021,231.08839 L 203.84575,215.17847 L 208.15468,195.07011 L 211.8007,182.03283 L 212.5741,178.27632" id="WA_ID" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 105.11516,210.08273 L 107.72336,210.64857 L 108.38627,213.07925 L 112.36375,213.63168 L 114.57346,217.38818 L 114.35249,224.9012 L 113.68958,225.6746 L 119.43482,229.65208 L 125.40104,229.98353 L 127.27929,228.21576 L 131.47774,229.65208 L 132.25114,229.54159 L 137.33348,231.53033 L 138.99076,232.85615 L 143.52066,233.07713 L 144.736,232.41421 L 147.0562,233.96101 L 151.36513,234.51344 L 156.66844,232.5247 L 157.11038,233.85052 L 172.02592,233.74004 L 186.61001,237.16509 L 200.38279,240.66827" id="WA_OR" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 199.97875,240.59014 L 201.19409,244.6781 L 204.50866,247.21927 L 204.28769,250.86529 L 200.53118,255.06374 L 196.77467,260.6985 L 195.89079,261.36141 L 195.33836,264.45501 L 194.23351,265.55986 L 192.46574,266.0018 L 188.1568,271.30511 L 187.82535,274.28822 L 187.38341,275.39307 L 188.1568,276.38744 L 190.58749,276.27696 L 191.80283,278.48667 L 189.37215,284.23191 L 188.04632,288.31988 L 183.84787,305.44513 L 179.53894,322.90184" id="OR_ID" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 134.1294,312.29523 L 126.28493,342.89971 L 119.54531,367.97992 L 117.96384,374.32659 L 129.93095,392.28673 L 151.91756,424.99044 L 170.7001,453.05375 L 184.73175,475.04037 L 187.28633,478.33598" id="CA_NV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 179.25879,323.29247 L 199.53681,327.65271 L 208.92808,329.53097 L 217.8774,331.29873 L 225.72187,333.17699" id="ID_NV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 225.5009,333.28747 L 237.87528,335.49718 L 249.47626,337.48592 L 259.41995,339.25369 L 267.48539,340.57952 L 272.23627,341.24243" id="ID_UT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 187.05195,478.02348 L 186.61001,481.33804 L 189.26166,486.08892 L 190.477,491.72368 L 191.2504,492.71805 L 192.24477,493.27048 L 192.13428,495.48019 L 190.58749,496.80601 L 187.27292,498.46329 L 185.39467,500.34155 L 183.95836,503.87708 L 183.40593,508.62796 L 180.64379,511.27961 L 178.65505,511.94253 L 178.54457,517.57729 L 178.10262,519.23457 L 178.54457,520.00797 L 182.0801,520.56039 L 181.52767,523.21205 L 180.09136,525.31127 L 176.44534,526.19515" id="CA_AZ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 187.27292,478.24445 L 188.59875,476.03474 L 189.15117,463.88133 L 189.37215,462.22405 L 189.48263,455.48444 L 191.36088,454.49007 L 192.24477,453.93764 L 193.12865,453.93764 L 194.01254,455.04249 L 196.66419,455.37395 L 197.87953,458.0256 L 200.31021,458.13609 L 201.96749,455.59492 L 202.40943,455.15298 L 205.40595,437.74758" id="NV_AZ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 205.28206,438.35918 L 207.16031,427.86306 L 210.2539,413.16849 L 213.45798,397.14809 L 215.55721,384.00032 L 217.43546,375.38245 L 221.08148,355.60554 L 224.50653,338.70126 L 225.61139,333.50844" id="NV_UT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 70.710722,294.61753 L 77.560823,296.82724 L 88.609373,299.69986 L 95.901416,301.6886 L 108.05482,305.66608 L 121.09211,308.98065 L 134.1294,312.73715" id="OR_CA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 133.68746,312.68584 L 166.39117,320.47114 L 179.64943,323.34376" id="OR_NV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 226.27435,180.81733 L 221.85493,201.58861 L 225.1695,208.88065 L 223.84367,213.30007 L 225.61144,217.71949 L 228.70504,219.04532 L 232.24057,228.98902 L 235.77611,232.52455 L 236.21805,233.62941 L 239.53262,234.73427 L 239.97456,236.723 L 233.12446,253.73778 L 233.12446,256.16846 L 235.55514,259.26205 L 236.43902,259.26205 L 241.07941,256.38943 L 241.74233,255.28457 L 243.28913,255.94749 L 243.06815,261.02982 L 245.71981,273.18323 L 248.59243,275.61391 L 249.47631,276.27682 L 251.24408,278.48653 L 250.80214,281.8011 L 251.46505,285.11566 L 252.56991,285.99955 L 254.77962,283.78984 L 257.43127,283.78984 L 260.52487,285.33664 L 262.95555,284.45275 L 266.93303,284.45275 L 270.46856,285.99955 L 273.12022,285.55761 L 273.56216,282.68498 L 276.43478,282.02207 L 277.76061,283.3479 L 278.20255,286.44149 L 280.63323,288.6512" id="ID_MT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 280.76267,289.04183 L 282.18003,277.82362 L 301.40451,280.69624 L 328.80492,284.67372 L 344.49387,286.66246 L 375.50794,289.84759 L 386.47836,290.86091" id="MT_WY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 280.8542,288.20926 L 277.0977,312.07413 L 272.01536,341.46328" id="ID_WY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 272.01536,341.46328 L 270.5356,351.6275 L 268.92177,363.11844 L 275.2273,364.01574 L 291.55004,366.22545 L 300.53404,367.40842" id="UT_WY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 300.29966,367.31689 L 297.42703,388.75109 L 294.33344,410.40625 L 290.70637,437.45625 L 289.2511,448.1923 L 288.58819,452.16978" id="UT_CO" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 205.06113,437.58569 L 237.54388,443.77288 L 263.83943,448.1923 L 288.80916,451.85728" id="UT_AZ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 288.58819,451.94881 L 284.5839,481.67727 L 277.85214,533.09881 L 274.22507,559.11977 L 272.4573,571.93609" id="AZ_NM" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 288.80916,451.72784 L 321.95482,455.92629 L 357.65689,460.20517 L 389.35099,462.33445" id="CO_NM" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 685.15727,355.93675 L 692.50104,354.68675 L 702.3448,353.12425 L 707.42849,352.54501" id="MI_OH" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 691.09478,411.56184 L 695.00104,411.24934 L 697.34479,410.46809 L 700.1573,412.03059 L 701.7198,416.24934 L 707.34481,416.56184 L 709.06356,418.2806 L 711.09481,418.43685 L 713.43857,417.0306 L 716.40732,417.49935 L 717.65732,418.9056 L 720.31358,416.40559 L 722.03233,415.15559 L 723.59483,415.15559 L 724.21983,417.81185 L 725.93859,418.74935 L 729.37609,420.7806" id="OH_KY" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 729.37609,420.7806 L 729.53234,426.09311 L 730.31359,427.65561 L 732.8136,429.06186 L 733.4386,431.24937 L 736.2511,434.84312 L 738.7511,437.49938 L 741.92187,439.62379" id="KY_WV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 742.03236,438.90563 L 738.90736,442.65563 L 734.84485,446.09314 L 730.46984,451.2494 L 728.75109,452.96815 L 728.75109,454.9994 L 725.00108,457.03065 L 719.53233,460.31191 L 716.70413,461.72601" id="KY_VA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 717.31181,461.61552 L 666.876,466.40567 L 651.64388,468.12442 L 647.17734,468.61997 L 643.43846,468.59317 L 643.43846,472.34318 L 635.31345,472.81193 L 628.59469,473.43693 L 618.12592,473.59318" id="KY_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 665.15724,603.28087 L 664.84474,595.46836 L 662.34474,593.59336 L 660.62599,591.87461 L 660.93849,588.90585 L 670.78225,587.65585 L 695.46979,584.84335 L 702.0323,584.21835 L 708.12606,584.21835" id="AL_FL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 708.12606,584.21835 L 707.34481,580.78084 L 705.93856,579.99959 L 705.31355,579.21834 L 706.09481,576.87458 L 705.78231,571.71833 L 704.2198,567.49957 L 704.37605,565.62457 L 704.8448,562.49956 L 706.56356,557.81206 L 706.40731,555.7808 L 704.5323,554.99955 L 704.06355,551.7183 L 701.56355,548.43704 L 699.5323,542.34328 L 698.12604,535.62452 L 696.56354,530.93702 L 695.15729,524.99951 L 692.81354,515.46824 L 689.53228,507.81198 L 688.90728,504.53073 L 688.75103,502.49947 L 688.75103,500.31197" id="AL_GA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 707.81356,583.90584 L 710.46981,587.8121 L 711.87606,589.21835 L 719.53233,589.3746 L 729.98996,588.7496 L 750.78237,587.4996 L 756.04583,586.8478 L 760.46989,586.8746 L 760.62614,589.6871 L 763.12614,590.46835 L 763.43864,586.2496 L 761.87614,581.87459 L 762.96989,580.31209 L 768.5949,581.09334 L 773.59491,581.40584" id="GA_FL" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 688.75103,500.46822 L 697.03229,499.53072 L 705.1573,498.43697 L 709.84481,497.65572" id="TN_GA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 709.84481,497.65572 L 716.40732,497.18696 L 723.12608,496.40571 L 728.75109,495.46821 L 730.46984,495.31196" id="NC_GA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 730.46984,495.31196 L 730.62609,497.18696 L 728.43859,498.12447 L 727.65734,499.99947 L 727.96984,501.71822 L 729.21984,502.96822 L 733.1261,505.31198 L 736.40735,505.15573 L 739.68861,510.15573 L 740.15736,511.56199 L 742.50111,514.37449 L 742.96986,515.78074 L 747.34487,517.4995 L 750.31362,519.687 L 752.50113,522.4995 L 754.68863,523.7495 L 756.71988,525.62451 L 757.96988,528.12451 L 760.00114,529.99951 L 764.06364,531.87452 L 766.7199,537.65578 L 768.2824,542.34328 L 770.7824,542.96828 L 772.96991,544.99954 L 774.21991,548.43704 L 774.84491,550.46829 L 777.34491,551.7183 L 779.37617,550.7808" id="GA_SC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 730.15734,494.99946 L 736.2511,492.65571 L 745.00111,488.2807 L 752.03237,487.49945 L 767.9699,487.0307 L 770.1574,488.9057 L 771.7199,492.03071 L 775.93866,491.56196 L 788.12618,490.1557 L 790.93868,490.93696 L 803.1262,498.28072 L 812.97945,506.32368" id="NC_SC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 709.84481,497.65572 L 710.15731,492.96821 L 711.87606,491.56196 L 714.53232,490.93696 L 715.15732,487.3432 L 719.21983,484.68695 L 722.96983,483.28069 L 727.03234,479.84319 L 731.25109,477.81194 L 731.87609,474.84318 L 735.6261,471.09318 L 736.2511,470.93693 C 736.2511,470.93693 736.2511,472.03068 737.03235,472.03068 C 737.8136,472.03068 738.90736,472.34318 738.90736,472.34318 L 741.09486,468.90567 L 743.12611,468.28067 L 745.31361,468.59317 L 746.87612,465.15567 L 749.68862,462.65566 L 750.15737,460.62441 L 750.31362,456.71815" id="TN_NC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 717.03232,461.56191 L 726.53223,460.74273 L 738.1261,458.90566 L 745.78237,458.74941 L 748.12612,456.8744 L 750.34206,456.92964" id="VA_TN" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 750.00112,456.8744 L 754.53238,457.49941 L 761.42964,456.2494 L 776.71991,454.3744 L 793.28244,451.8744 L 813.19549,448.22704 L 831.71999,444.53064 L 842.81376,441.71813 L 847.56599,440.14615" id="VA_NC" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 741.65514,439.21813 L 743.90736,442.34313 L 745.15736,443.59313 L 747.96987,444.37439 L 750.46987,443.74938 L 752.81363,441.56188 C 752.81363,441.56188 754.53238,442.96813 755.15738,442.81188 C 755.78238,442.65563 759.37614,441.87438 759.37614,441.87438 L 761.09489,437.34313 L 763.59489,438.28063 L 766.7199,435.93687 L 768.12615,436.24937 L 770.1574,434.37437 L 770.31365,431.71812 L 769.5324,430.46812 L 774.37616,419.99935 L 776.40741,413.59309 L 776.71991,409.37433 L 778.43866,409.21808 L 780.00117,411.87434 L 781.25117,412.65559 L 783.75117,412.65559 L 784.84492,407.34308 L 785.15742,404.37433 L 788.43868,404.06183 L 788.90743,401.87432 L 791.87618,399.53057 L 792.50119,397.96807 L 794.06369,395.31181 L 794.53244,393.28056 L 794.68869,388.1243 L 799.06369,389.84305 L 804.53246,392.81181 L 805.46995,387.65555" id="WV_VA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 805.15745,388.1243 L 809.37621,389.84305 L 809.37621,392.65556 L 814.68872,393.90556 L 816.56372,395.15556 L 817.50122,393.28056 L 819.68873,394.84306 L 818.28247,397.96807 L 817.96997,400.62432 L 816.25122,403.12432 L 816.25122,405.15558 L 816.87622,406.87433 L 822.13512,408.2443" id="VA_MD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 805.6262,388.28055 L 804.21995,386.71805 L 802.34495,384.9993 L 799.53245,383.59305 L 797.94314,382.50092 L 796.15966,382.96967 L 794.21994,384.3743 L 791.71993,386.40555 L 788.90743,386.71805 L 787.65743,386.09305 L 785.93868,388.59305 L 784.53242,389.9993 L 782.18867,390.15555 L 780.00117,393.12431 L 777.96991,395.46806 L 777.65741,395.78056 L 776.71991,390.31181 L 775.31366,385.3118" id="WV_MD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 775.64512,385.53277 L 763.62333,387.03055 L 759.14731,387.75656 L 755.60717,368.59302" id="PA_WV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 755.93863,368.59302 L 754.53238,369.84302 L 753.59488,371.24928 L 754.68863,374.21803 L 754.68863,378.74929 L 754.21988,383.59305 L 753.90738,389.0618 L 751.71987,392.81181 L 748.75112,396.09306 L 746.56362,397.65557 L 744.53236,397.18682 L 743.28236,398.59307 L 741.09486,401.87432 L 740.15736,403.12432 L 740.15736,405.46808 L 741.25111,407.18683 L 740.78236,408.74933 L 739.06361,409.68683 L 738.59485,407.96808 L 737.34485,406.87433 L 736.09485,407.49933 L 735.15735,411.24934 L 735.0011,416.09309 L 733.4386,417.49935 L 733.28235,420.1556 L 731.40734,420.93685 L 729.21984,421.24935" id="OH_WV" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 751.25169,340.03513 L 754.06419,358.74889 L 755.78295,369.68641" id="OH_PA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 761.7015,331.46032 L 762.34546,338.28009 L 770.93923,336.87384 L 812.50182,328.43632 L 830.47061,324.53006 L 832.72285,326.59924 L 835.62687,327.49882 L 837.97063,332.65508 L 840.15813,334.53008 L 842.65814,334.68633 L 843.90814,335.78009" id="NY_PA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 843.75189,335.62383 L 847.34565,337.34259 L 851.09566,338.43634 L 855.47067,339.37384 L 857.97067,340.4676 L 858.12692,342.49885 L 857.65817,345.15511 L 858.24689,348.66681" id="NY_NJ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 844.06439,335.46758 L 841.72064,337.96759 L 841.72064,340.93635 L 839.84563,343.9051 L 839.68938,345.46761 L 840.93939,346.71761 L 840.78314,349.06137 L 838.59563,350.15512 L 839.37688,352.81137 L 839.53313,353.90513 L 842.18939,354.21763 L 843.12689,356.71763 L 846.5644,359.06139 L 848.90815,360.62389 L 848.90815,361.40514 L 845.78315,364.3739 L 844.22064,366.5614 L 842.81439,369.21766 L 840.62689,370.46766 L 839.53313,371.24891" id="PA_NJ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 839.37688,371.09266 L 839.22063,372.34267 L 838.66983,374.88059" id="DE_NJ" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 839.53313,371.09266 L 837.50188,371.09266 L 835.47062,372.65517 L 834.06437,374.06142" id="NY_DE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 834.06437,374.06142 L 835.47062,378.12393 L 837.65813,383.59269 L 839.68938,392.96771 L 841.25189,399.06148 L 846.09565,398.90523 L 852.03316,397.81147" id="MD_DE" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 834.38635,373.6842 L 826.20281,375.44087 L 811.82448,378.1697 L 774.68924,385.62395" id="PA_MD" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 862.06162,339.46537 L 860.15818,337.34259 L 861.25193,335.15508 L 861.25193,327.34257 L 859.84568,320.3113 L 859.06443,316.87379" id="NY_CT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 859.06443,316.87379 L 858.28318,311.56128 L 858.75193,301.09251" id="NY_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 858.75193,301.09251 L 858.28318,296.0925 L 855.31442,285.46747 L 854.68942,285.15497 L 851.87691,283.90497 L 852.65816,281.09246 L 851.87691,279.06121 L 849.37691,274.6862 L 850.31441,270.93619 L 849.53316,265.93618 L 847.1894,259.68616 L 846.5644,254.8424" id="NY_VT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 872.65821,247.81114 L 872.97071,253.5924 L 874.84571,256.24866 L 874.84571,260.15492 L 871.25195,264.06117 L 868.75195,265.15493 L 868.75195,266.24868 L 869.8457,267.96743 L 869.8457,276.2487 L 869.06445,285.15497 L 868.9082,289.84248 L 869.8457,291.09249 L 869.68945,295.46749 L 869.2207,297.18625 L 870.7832,299.06125" id="VT_NH" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 859.06443,301.56126 L 863.90819,300.46751 L 870.4707,299.2175" id="VT_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 870.4707,299.2175 L 887.34574,295.15499 L 889.53325,294.52999 L 891.5645,291.40499 L 895.50868,289.5947" id="NH_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 896.48246,285.35698 L 894.06451,284.21747 L 893.59575,281.24871 L 889.84575,280.15496 L 889.53325,277.4987 L 882.50198,254.8424 L 877.81447,240.46737" id="NH_ME" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 859.37693,317.34254 L 880.31447,312.49878 L 885.31449,311.40503" id="MA_CT" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 885.31449,311.40503 L 886.87699,317.18629 L 887.65824,321.40505 L 888.28324,325.46756" id="CT_RI" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + <ns0:path d="M 885.15824,311.56128 L 890.78325,309.99878 L 892.34575,311.09253 L 895.62701,315.31129 L 898.43952,319.6863" id="RI_MA" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:1pt;stroke-opacity:1" /> + </ns0:g> + <ns0:g id="g3561" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:path d="M 242.11104,448.52821 L 258.12559,450.54515 L 259.44637,440.12432 L 276.45152,442.81356 L 290.31977,444.66242" id="NM_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 289.98957,444.32625 L 292.13584,444.99855 L 294.77744,448.02394 L 296.26332,452.56207 L 301.05119,454.91517 L 302.37198,458.27673 L 309.63631,466.51255 L 310.9571,468.19333 L 316.07515,470.37834 L 317.23084,472.56336 L 318.88182,473.57182 L 319.37712,476.42915 L 322.67909,483.15227 L 322.67909,491.55616 L 324.99047,496.43042 L 332.585,504.49816 L 337.86815,506.68317 L 339.68423,508.70011 L 339.68423,509.37242 L 343.64659,511.72551 L 345.62777,512.39782 L 347.44386,513.57437 L 350.08543,514.58284 L 352.56191,512.06167 L 357.01957,505.67471 L 358.01016,501.80891 L 360.32154,498.44736 L 363.9537,496.93466 L 368.57646,495.0858 L 371.71333,497.43889 L 379.30786,498.1112 L 386.242,499.28775 L 388.88357,501.47276 L 388.88357,502.6493 L 391.52515,505.84279 L 397.63379,511.38936 L 397.79889,512.90206 L 399.61497,514.91899 L 400.44047,519.28902 L 405.88872,532.06294 L 405.72362,534.07988 L 410.01618,536.76912 L 413.64834,543.66032 L 417.11541,548.19842 L 420.41738,549.54304 L 422.06837,551.89614 L 420.74758,556.43424 L 421.40797,557.44271 L 422.72876,558.11502 L 422.39856,561.64466 L 421.73817,562.31697 L 422.39856,564.67006 L 425.70053,566.68699 L 427.02132,573.41011 L 429.1676,577.44398 L 436.92723,580.97362 L 442.21038,582.15016 L 446.50294,585.34364 L 449.80491,586.01595 L 451.1257,585.51172 L 456.73904,586.68827 L 462.51749,590.72214 L 465.65436,588.7052" id="TX_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 95.503595,393.0625 L 105.7397,394.5752 L 125.88171,397.43253 L 140.74058,399.1133" id="CA_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 140.74058,399.1133 L 138.4292,401.4664 L 138.099,402.9791 L 138.5943,403.98756 L 157.91082,415.08071 L 170.2932,422.98037 L 185.31716,431.8885 L 202.4874,442.30933 L 215.03489,444.8305 L 242.11104,448.52821" id="AZ_Mexico" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:g id="g3547" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:path d="M 876.74955,100.10268 L 848.85548,107.51359" id="VT_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 943.28423,76.73985 L 940.47756,76.907928 L 940.97285,78.084474 L 940.80775,78.588708 L 939.98226,78.588708 L 937.01049,76.235617 L 936.84539,71.193279 L 936.0199,69.176344 L 929.91126,69.176344 L 920.66574,38.081928 L 918.68456,37.073461 L 912.57592,34.552292 L 911.09003,34.384214 L 909.27395,36.233071 L 905.14649,39.258474 L 905.14649,40.266941 L 904.32099,41.107331 L 901.51432,40.435019 L 900.19353,38.081928 L 900.19353,36.905383 L 898.87274,36.737305 L 897.55196,36.737305 L 895.40568,41.107331 L 892.4339,50.351617 L 890.61782,55.393954 L 890.78292,60.436292 L 890.94802,61.948993 L 890.12252,64.806318 L 889.29703,65.814786 L 889.29703,72.033669 L 891.27821,74.554837 L 889.79233,78.756786 L 887.15075,83.631045 L 886.32526,89.345695 L 886.32526,92.034941 L 884.77927,91.608771 L 881.71077,91.733617" id="ME_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 882.59051,91.36264 L 881.5374,92.203019 L 880.87701,93.883799 L 880.21662,93.379565 L 879.22603,92.371097 L 877.74014,94.388032 L 875.84148,100.77501" id="NH_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:g id="g3537" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:path d="M 710.49539,188.67975 L 711.48598,189.52014 L 710.49539,190.69668 L 710.66049,191.36899 L 711.48598,191.53707 L 713.46716,190.5286 L 713.13697,180.61201 M 704.88204,204.47907 L 704.88204,198.9325 L 706.86322,196.91556 L 707.68872,196.57941" id="MI_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 766.79397,165.82115 L 768.77515,172.20811 L 770.59123,172.37619 L 771.91202,175.56967 M 849.4392,107.39474 L 841.58358,109.51505 L 836.96082,111.02775 L 833.65885,110.85967 L 828.0455,112.20429 L 824.7235,113.61855" id="NY_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-opacity:1" /> + <ns0:g id="g3530" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:path d="M 179.70368,24.971818 L 163.02887,21.106058 L 139.58489,15.223331 L 119.11268,9.3406038 L 110.36246,7.3236687 L 100.45655,4.4663441 L 95.99889,2.9536428" id="WA_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 193.57209,27.997253 L 179.20868,25.139971" id="ID_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 371.21804,55.393954 L 338.69364,52.032396 L 308.81082,48.334682 L 278.92799,44.132734 L 245.9083,38.586162 L 227.08707,35.056526 L 193.57209,27.997253" id="MT_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 472.75352,59.76398 L 442.87077,59.427825 L 423.88445,58.755513 L 396.8083,57.410889 L 371.21804,55.393954" id="ND_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + <ns0:path d="M 590.4688,78.756786 L 585.35075,79.261019 L 584.85546,80.269487 L 584.19506,80.269487 L 582.37898,77.076006 L 573.29856,77.412162 L 572.30797,78.252552 L 571.31738,78.252552 L 570.82209,76.907928 L 569.99659,75.059071 L 567.35502,75.563305 L 564.05305,78.924863 L 562.40206,79.765253 L 559.26519,79.765253 L 556.62362,78.756786 L 556.62362,76.571773 L 555.30283,76.403695 L 554.80753,76.907928 L 552.16596,75.563305 L 551.67066,72.537902 L 550.18478,73.042136 L 549.68948,74.050604 L 547.21301,73.54637 L 541.76476,71.025201 L 537.80239,68.335954 L 534.83062,68.335954 L 533.50983,67.327487 L 531.19845,67.999799 L 530.04276,69.176344 L 529.71257,70.520967 L 524.75961,70.520967 L 524.75961,68.335954 L 518.32077,67.999799 L 517.99058,66.487097 L 513.03762,66.487097 L 511.38664,64.806318 L 509.90075,58.419357 L 509.07526,52.704708 L 507.09408,51.864318 L 504.7827,51.360084 L 504.1223,51.528162 L 503.79211,60.100136 L 472.09313,60.100136" id="MN_Canada" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:2.66530466;stroke-dasharray:none;stroke-opacity:1" /> + </ns0:g> + </ns0:g> + </ns0:g> + </ns0:g> + </ns0:g> + <ns0:g id="g4675" style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-opacity:1"> + <ns0:path d="M 846.52085,274.72594 L 847.80814,273.69012 L 850.29359,272.77823 L 852.1404,272.10064 L 852.89317,271.82446 L 853.40684,271.87199" id="path3995" style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#000000;stroke-width:1.33265233;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" ns1:nodetypes="cccccc" /> + <ns0:path d="M 817.25107,257.90409 C 818.2317,258.85489 818.79206,259.61552 818.79206,259.61552 L 819.39913,260.85156 L 819.53922,260.70894" id="path5767" style="fill:#000000;fill-opacity:0;fill-rule:evenodd;stroke:#000000;stroke-width:0.41898587;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" ns1:nodetypes="cccc" /> + </ns0:g> + </ns0:g> + </ns0:g> + <ns0:path d="M 152.15345,458.16063 L 151.84095,540.66102 L 153.40345,541.59852 L 156.37222,541.75477 L 157.77847,540.66102 L 160.27848,540.66102 L 160.43474,543.47353 L 167.15352,550.03606 L 167.62227,552.53607 L 170.90353,550.66106 L 171.52854,550.50481 L 171.84104,547.53605 L 173.24729,545.97354 L 174.34105,545.81729 L 176.21606,544.41103 L 179.18482,546.44229 L 179.80983,549.25481 L 181.68483,550.34856 L 182.77859,552.69232 L 186.52861,554.41108 L 189.80987,560.19236 L 192.46613,563.94237 L 194.65364,566.59864 L 196.0599,570.1924 L 200.90367,571.91116 L 205.9037,573.94242 L 206.8412,578.16119 L 207.30995,581.12995 L 206.37245,584.41122 L 204.34118,587.14562" id="AK_Canada" style="fill:none;stroke:#000000;stroke-width:2.5;stroke-dasharray:none;stroke-opacity:1" /> + </ns0:g> + <ns0:path d="M 127.46583,540.97352 L 129.80959,539.56726 L 128.87209,537.8485 L 127.15333,538.78601 L 127.46583,540.97352 z M 86.528141,560.19236 L 87.621901,565.81738 L 90.434411,566.59864 L 95.278181,563.78612 L 99.496951,561.28611 L 97.934451,558.94235 L 98.403201,556.59859 L 96.371941,557.8486 L 93.559431,557.06734 L 95.121931,555.97359 L 96.996941,556.75484 L 100.74696,555.03608 L 101.21571,553.62983 L 98.871951,552.84857 L 99.653201,550.97356 L 96.996941,552.84857 L 92.465671,556.28609 L 87.778151,559.0986 L 86.528141,560.19236 z M 78.090601,572.22366 L 79.496861,573.94242 L 80.434371,572.84867 L 79.653111,570.97366 L 78.090601,572.22366 z M 55.278,574.09867 L 56.371751,571.91116 L 58.403011,572.22366 L 57.621761,574.09867 L 55.278,574.09867 z M 52.465487,571.12991 L 54.027994,573.00492 L 56.059251,571.44241 L 54.652997,570.1924 L 52.465487,571.12991 z M 5.1215179,575.50493 L 8.4027829,573.31742 L 11.684048,572.37991 L 14.18406,572.69241 L 14.652812,574.25492 L 16.527821,574.72367 L 18.402829,572.84867 L 18.090328,571.28616 L 20.74659,570.66116 L 23.559103,573.16117 L 22.465348,574.87992 L 18.246579,575.97368 L 15.590316,575.50493 L 11.99655,574.41117 L 7.7777799,575.81743 L 6.2152729,576.12993 L 5.1215179,575.50493 z M 18.402829,504.8796 L 19.496585,507.37961 L 20.59034,508.94212 L 19.496585,509.72337 L 17.465325,506.75461 L 17.465325,504.8796 L 18.402829,504.8796 z M 38.402922,518.47341 L 41.996689,519.25467 L 45.590455,520.19217 L 46.371709,521.12968 L 44.809202,524.72344 L 41.840438,524.56719 L 38.559173,521.12968 L 38.402922,518.47341 z M 40.902934,486.12951 L 42.934193,491.28579 L 42.777942,492.22329 L 39.965429,491.91079 L 38.246671,488.00452 L 36.527913,486.59827 L 34.184152,486.59827 L 34.027902,484.09825 L 35.74666,481.75449 L 36.840415,484.09825 L 38.246671,485.50451 L 40.902934,486.12951 z M 204.65369,586.59873 L 203.09118,585.81748 L 201.68493,582.84871 L 199.02866,581.44246 L 197.30991,580.3487 L 196.52865,581.12995 L 197.93491,583.78622 L 198.09116,587.37998 L 196.9974,587.84873 L 195.1224,585.97373 L 193.09114,584.72372 L 193.55989,586.28623 L 194.80989,588.00499 L 194.02864,588.78624 C 194.02864,588.78624 193.24739,588.47374 192.77864,587.84873 C 192.30988,587.22373 190.74738,584.56747 190.74738,584.56747 L 189.80987,582.37996 C 189.80987,582.37996 189.49737,583.62997 188.87237,583.31746 C 188.24736,583.00496 187.62236,581.91121 187.62236,581.91121 L 189.34112,580.0362 L 187.93486,578.62994 L 187.93486,573.78617 L 187.15361,573.78617 L 186.37236,577.06743 L 185.2786,577.53619 L 184.3411,573.94242 L 183.71609,570.34865 L 182.93484,569.8799 L 183.24734,575.34868 L 183.24734,576.44243 L 181.84108,575.19243 L 178.40357,569.41115 L 176.37231,568.9424 L 175.74731,565.34863 L 174.1848,562.53612 L 172.62229,561.44236 L 172.62229,559.25485 L 174.65355,558.00485 L 174.1848,557.69235 L 171.68479,558.31735 L 168.40352,555.97359 L 165.90351,553.16107 L 161.21599,550.66106 L 157.30972,548.16105 L 158.55973,545.03604 L 158.55973,543.47353 L 156.84097,545.03604 L 154.02846,546.12979 L 150.43469,545.03604 L 144.96591,542.69228 L 139.65339,542.69228 L 139.02839,543.16103 L 132.77836,539.41101 L 130.7471,539.09851 L 128.09084,533.47348 L 124.65332,533.78598 L 121.2158,535.19224 L 121.68456,539.56726 L 122.77831,536.75475 L 123.71582,537.06725 L 122.30956,541.28602 L 125.43457,538.62976 L 126.05958,540.19226 L 122.30956,544.41103 L 121.05955,544.09853 L 120.5908,542.22352 L 119.3408,541.44227 L 118.09079,542.53603 L 115.43453,540.81727 L 112.46576,542.84853 L 110.74701,544.87979 L 107.46574,546.91105 L 102.93447,546.75479 L 102.46572,544.72354 L 106.05948,544.09853 L 106.05948,542.84853 L 103.87197,542.22352 L 104.80948,539.87976 L 106.99699,536.12975 L 106.99699,534.41099 L 107.15324,533.62973 L 111.37201,531.44222 L 112.30951,532.69223 L 114.96578,532.69223 L 113.71577,530.19222 L 110.122,529.87972 L 105.27823,532.53598 L 102.93447,535.81724 L 101.21571,538.31726 L 100.12196,540.50477 L 96.059441,541.91102 L 93.090671,544.41103 L 92.778171,545.97354 L 94.965681,546.91105 L 95.746941,548.9423 L 93.090671,552.06732 L 86.840651,556.12984 L 79.340611,560.19236 L 77.309351,561.28611 L 72.153081,562.37987 L 66.996801,564.56738 L 68.715561,565.81738 L 67.309301,567.22364 L 66.840551,568.31739 L 64.184291,567.37989 L 61.059281,567.53614 L 60.278021,569.72365 L 59.340521,569.72365 L 59.653021,567.37989 L 56.215501,568.6299 L 53.402991,569.5674 L 50.121726,568.31739 L 47.309213,570.1924 L 44.184199,570.1924 L 42.152939,571.44241 L 40.590432,572.22366 L 38.559173,571.91116 L 36.059161,570.81741 L 33.871651,571.44241 L 32.934147,572.37991 L 31.371639,571.28616 L 31.371639,569.41115 L 34.340403,568.16114 L 40.434181,568.78615 L 44.652951,567.22364 L 46.68421,565.19238 L 49.496723,564.56738 L 51.215481,563.78612 L 53.871744,563.94237 L 55.434251,565.19238 L 56.371751,564.87988 L 58.559261,562.22362 L 61.528031,561.28611 L 64.809291,560.66111 L 66.059301,560.34861 L 66.684301,560.81736 L 67.465561,560.81736 L 68.715561,557.22359 L 72.621831,555.81734 L 74.496841,552.22357 L 76.684351,547.84855 L 78.246861,546.44229 L 78.559361,543.94228 L 76.996851,545.19229 L 73.715581,545.81729 L 73.090581,543.47353 L 71.840581,543.16103 L 70.903071,544.09853 L 70.746821,546.91105 L 69.340561,546.75479 L 67.934311,541.12977 L 66.684301,542.37977 L 65.590551,541.91102 L 65.278051,540.03601 L 61.371781,540.19226 L 59.340521,541.28602 L 56.840511,540.97352 L 58.246761,539.56726 L 58.715511,537.06725 L 58.090511,535.19224 L 59.496771,534.25474 L 60.746771,534.09849 L 60.121771,532.37973 L 60.121771,528.16096 L 59.184271,527.22345 L 58.403011,528.62971 L 52.465487,528.62971 L 51.059231,527.3797 L 50.434228,523.62969 L 48.402968,520.19217 L 48.402968,519.25467 L 50.434228,518.47341 L 50.590478,516.44215 L 51.684233,515.3484 L 50.90298,514.87965 L 49.652974,515.3484 L 48.559219,512.69214 L 49.496723,507.84836 L 53.871744,504.72335 L 56.371751,503.16084 L 58.246761,499.56708 L 60.903031,498.31707 L 63.403041,499.41083 L 63.715541,501.75459 L 66.059301,501.44208 L 69.184311,499.09832 L 70.746821,499.72333 L 71.684321,500.34833 L 73.246831,500.34833 L 75.434341,499.09832 L 76.215601,494.87955 C 76.215601,494.87955 76.528101,492.06704 77.153101,491.59829 C 77.778101,491.12954 78.090601,490.66079 78.090601,490.66079 L 76.996851,488.78578 L 74.496841,489.56703 L 71.371821,490.34828 L 69.496811,489.87953 L 66.059301,488.16077 L 61.215531,488.00452 L 57.778011,484.41076 L 58.246761,480.66074 L 58.871771,478.31698 L 56.840511,476.59822 L 54.965499,473.00445 L 55.434251,472.2232 L 61.996781,471.75445 L 64.028041,471.75445 L 64.965541,472.69195 L 65.590551,472.69195 L 65.434301,471.12944 L 69.184311,470.50444 L 71.684321,470.81694 L 73.090581,471.9107 L 71.684321,473.94196 L 71.215571,475.34821 L 73.871841,476.91072 L 78.715611,478.62948 L 80.434371,477.69198 L 78.246861,473.47321 L 77.309351,470.34819 L 78.246861,469.56694 L 74.965591,467.69193 L 74.496841,466.59817 L 74.965591,465.03567 L 74.184341,461.28565 L 71.371821,456.75438 L 69.028061,452.69186 L 71.840581,450.81685 L 74.965591,450.81685 L 76.684351,451.44185 L 80.746871,451.2856 L 84.340631,447.84809 L 85.434391,444.87932 L 89.028161,442.53556 L 90.590661,443.47307 L 93.246921,442.84806 L 96.840691,440.8168 L 97.934451,440.66055 L 98.871951,441.44181 L 103.24697,441.28556 L 105.90323,438.31679 L 106.99699,438.31679 L 110.4345,440.66055 L 112.30951,442.69181 L 111.84076,443.78557 L 112.46576,444.87932 L 114.02827,443.31682 L 117.77829,443.62932 L 118.09079,447.22308 L 119.9658,448.62934 L 126.84083,449.25434 L 132.93461,453.31686 L 134.34086,452.37936 L 139.34089,454.87937 L 141.37215,454.25437 L 143.24716,453.47311 L 147.93468,455.34812 L 152.15345,458.16063" id="AK_Pacific" style="fill:none;fill-opacity:0.26666697;stroke:#80b0f0;stroke-width:1pt" /> + <ns0:g id="Frames" transform="translate(-18.307669,-131.99439)"> + <ns0:path d="M 229.21212,631.12334 L 229.68087,688.43625 L 264.68114,723.74902 M 18.429285,562.99783 L 161.86786,563.31033 L 229.52462,631.59209 L 314.68151,631.74834 L 370.43191,685.18624 L 370.11941,723.43652" id="Inset_border" style="fill:none;fill-opacity:0.75;stroke:#000000;stroke-width:1.875;stroke-dasharray:none" ns1:nodetypes="ccccccccc" /> + <ns0:rect height="590.28674" id="Outer_border" style="fill:none;stroke:#000000;stroke-width:2.5;stroke-dasharray:none" width="955.48639" x="19.444839" y="133.89751" /> + </ns0:g> + <ns0:path d="M 822.91849,258.28198 A 4.1274123,3.62712 0 1 1 814.66366,258.28198 A 4.1274123,3.62712 0 1 1 822.91849,258.28198 z" id="DC" style="opacity:1;fill:#a02c2c;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" transform="matrix(0.9707988,0,0,1.0018987,24.345102,-0.4278704)" ns1:cx="818.79108" ns1:cy="258.28198" ns1:rx="4.1274123" ns1:ry="3.62712" ns1:type="arc" ns2:label="#DC" /> +</ns0:svg>
\ No newline at end of file diff --git a/tests/phpunit/data/media/Wikimedia-logo.svg b/tests/phpunit/data/media/Wikimedia-logo.svg new file mode 100644 index 00000000..1e17acbe --- /dev/null +++ b/tests/phpunit/data/media/Wikimedia-logo.svg @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Wikimedia logo" viewBox="-599 -599 1198 1198" width="1024" height="1024"> +<defs> + <clipPath id="mask"> + <path d="M 47.5,-87.5 v 425 h -95 v -425 l -552,-552 v 1250 h 1199 v -1250 z"/> + </clipPath> +</defs> +<g clip-path="url(#mask)"> + <circle id="green parts" fill="#396" r="336.5"/> + <circle id="blue arc" fill="none" stroke="#069" r="480.25" stroke-width="135.5"/> +</g> +<circle fill="#900" cy="-379.5" r="184.5" id="red circle"/> +</svg>
\ No newline at end of file diff --git a/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg b/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg Binary files differnew file mode 100644 index 00000000..f7b23025 --- /dev/null +++ b/tests/phpunit/data/media/Xmp-exif-multilingual_test.jpg diff --git a/tests/phpunit/data/media/animated-xmp.gif b/tests/phpunit/data/media/animated-xmp.gif Binary files differnew file mode 100644 index 00000000..fcba079d --- /dev/null +++ b/tests/phpunit/data/media/animated-xmp.gif diff --git a/tests/phpunit/data/media/animated.gif b/tests/phpunit/data/media/animated.gif Binary files differnew file mode 100644 index 00000000..a8f248b3 --- /dev/null +++ b/tests/phpunit/data/media/animated.gif diff --git a/tests/phpunit/data/media/broken_exif_date.jpg b/tests/phpunit/data/media/broken_exif_date.jpg Binary files differnew file mode 100644 index 00000000..82f62f57 --- /dev/null +++ b/tests/phpunit/data/media/broken_exif_date.jpg diff --git a/tests/phpunit/data/media/exif-gps.jpg b/tests/phpunit/data/media/exif-gps.jpg Binary files differnew file mode 100644 index 00000000..f99b484d --- /dev/null +++ b/tests/phpunit/data/media/exif-gps.jpg diff --git a/tests/phpunit/data/media/exif-user-comment.jpg b/tests/phpunit/data/media/exif-user-comment.jpg Binary files differnew file mode 100644 index 00000000..9f23966a --- /dev/null +++ b/tests/phpunit/data/media/exif-user-comment.jpg diff --git a/tests/phpunit/data/media/greyscale-na-png.png b/tests/phpunit/data/media/greyscale-na-png.png Binary files differnew file mode 100644 index 00000000..4a4b7452 --- /dev/null +++ b/tests/phpunit/data/media/greyscale-na-png.png diff --git a/tests/phpunit/data/media/greyscale-png.png b/tests/phpunit/data/media/greyscale-png.png Binary files differnew file mode 100644 index 00000000..340a67b4 --- /dev/null +++ b/tests/phpunit/data/media/greyscale-png.png diff --git a/tests/phpunit/data/media/iptc-timetest-invalid.jpg b/tests/phpunit/data/media/iptc-timetest-invalid.jpg Binary files differnew file mode 100644 index 00000000..b03e192a --- /dev/null +++ b/tests/phpunit/data/media/iptc-timetest-invalid.jpg diff --git a/tests/phpunit/data/media/iptc-timetest.jpg b/tests/phpunit/data/media/iptc-timetest.jpg Binary files differnew file mode 100644 index 00000000..db9932ba --- /dev/null +++ b/tests/phpunit/data/media/iptc-timetest.jpg diff --git a/tests/phpunit/data/media/jpeg-comment-binary.jpg b/tests/phpunit/data/media/jpeg-comment-binary.jpg Binary files differnew file mode 100644 index 00000000..b467fe43 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-comment-binary.jpg diff --git a/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg b/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg Binary files differnew file mode 100644 index 00000000..d9ffbac1 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-comment-iso8859-1.jpg diff --git a/tests/phpunit/data/media/jpeg-comment-multiple.jpg b/tests/phpunit/data/media/jpeg-comment-multiple.jpg Binary files differnew file mode 100644 index 00000000..363c7385 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-comment-multiple.jpg diff --git a/tests/phpunit/data/media/jpeg-comment-utf.jpg b/tests/phpunit/data/media/jpeg-comment-utf.jpg Binary files differnew file mode 100644 index 00000000..d6d35b4b --- /dev/null +++ b/tests/phpunit/data/media/jpeg-comment-utf.jpg diff --git a/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg b/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg Binary files differnew file mode 100644 index 00000000..6464c5b8 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-iptc-bad-hash.jpg diff --git a/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg b/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg Binary files differnew file mode 100644 index 00000000..ef970854 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-iptc-good-hash.jpg diff --git a/tests/phpunit/data/media/jpeg-padding-even.jpg b/tests/phpunit/data/media/jpeg-padding-even.jpg Binary files differnew file mode 100644 index 00000000..c83c66bd --- /dev/null +++ b/tests/phpunit/data/media/jpeg-padding-even.jpg diff --git a/tests/phpunit/data/media/jpeg-padding-odd.jpg b/tests/phpunit/data/media/jpeg-padding-odd.jpg Binary files differnew file mode 100644 index 00000000..25b93308 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-padding-odd.jpg diff --git a/tests/phpunit/data/media/jpeg-xmp-alt.jpg b/tests/phpunit/data/media/jpeg-xmp-alt.jpg Binary files differnew file mode 100644 index 00000000..0e2c3f63 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-xmp-alt.jpg diff --git a/tests/phpunit/data/media/jpeg-xmp-psir.jpg b/tests/phpunit/data/media/jpeg-xmp-psir.jpg Binary files differnew file mode 100644 index 00000000..4d19fcbe --- /dev/null +++ b/tests/phpunit/data/media/jpeg-xmp-psir.jpg diff --git a/tests/phpunit/data/media/jpeg-xmp-psir.xmp b/tests/phpunit/data/media/jpeg-xmp-psir.xmp new file mode 100644 index 00000000..fee6ee18 --- /dev/null +++ b/tests/phpunit/data/media/jpeg-xmp-psir.xmp @@ -0,0 +1,35 @@ +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> +<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'> +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> + + <rdf:Description rdf:about='' + xmlns:dc='http://purl.org/dc/elements/1.1/'> + <dc:identifier>jpeg-xmp-psir.jpg</dc:identifier> + </rdf:Description> +</rdf:RDF> +</x:xmpmeta> + + + + + + + + + + + + + + + + + + + + + + + + +<?xpacket end='w'?>
\ No newline at end of file diff --git a/tests/phpunit/data/media/landscape-plain.jpg b/tests/phpunit/data/media/landscape-plain.jpg Binary files differnew file mode 100644 index 00000000..cf296555 --- /dev/null +++ b/tests/phpunit/data/media/landscape-plain.jpg diff --git a/tests/phpunit/data/media/nonanimated.gif b/tests/phpunit/data/media/nonanimated.gif Binary files differnew file mode 100644 index 00000000..9e52a7f0 --- /dev/null +++ b/tests/phpunit/data/media/nonanimated.gif diff --git a/tests/phpunit/data/media/portrait-rotated.jpg b/tests/phpunit/data/media/portrait-rotated.jpg Binary files differnew file mode 100644 index 00000000..445feaed --- /dev/null +++ b/tests/phpunit/data/media/portrait-rotated.jpg diff --git a/tests/phpunit/data/media/rgb-na-png.png b/tests/phpunit/data/media/rgb-na-png.png Binary files differnew file mode 100644 index 00000000..2f2a5ca0 --- /dev/null +++ b/tests/phpunit/data/media/rgb-na-png.png diff --git a/tests/phpunit/data/media/rgb-png.png b/tests/phpunit/data/media/rgb-png.png Binary files differnew file mode 100644 index 00000000..6f40cc92 --- /dev/null +++ b/tests/phpunit/data/media/rgb-png.png diff --git a/tests/phpunit/data/media/test.jpg b/tests/phpunit/data/media/test.jpg Binary files differnew file mode 100644 index 00000000..cb084253 --- /dev/null +++ b/tests/phpunit/data/media/test.jpg diff --git a/tests/phpunit/data/media/test.tiff b/tests/phpunit/data/media/test.tiff Binary files differnew file mode 100644 index 00000000..6a36f760 --- /dev/null +++ b/tests/phpunit/data/media/test.tiff diff --git a/tests/phpunit/data/media/xmp.png b/tests/phpunit/data/media/xmp.png Binary files differnew file mode 100644 index 00000000..6b9f7a87 --- /dev/null +++ b/tests/phpunit/data/media/xmp.png diff --git a/tests/phpunit/data/xmp/1.result.php b/tests/phpunit/data/xmp/1.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/1.result.php @@ -0,0 +1,8 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/1.xmp b/tests/phpunit/data/xmp/1.xmp new file mode 100644 index 00000000..66e15427 --- /dev/null +++ b/tests/phpunit/data/xmp/1.xmp @@ -0,0 +1,11 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + exif:DigitalZoomRatio="0/10"> +<exif:Flash rdf:parseType='Resource'> +<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/2.result.php b/tests/phpunit/data/xmp/2.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/2.result.php @@ -0,0 +1,8 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/2.xmp b/tests/phpunit/data/xmp/2.xmp new file mode 100644 index 00000000..0fa6a894 --- /dev/null +++ b/tests/phpunit/data/xmp/2.xmp @@ -0,0 +1,12 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + exif:DigitalZoomRatio="0/10"> +<exif:Flash> +<rdf:Description exif:Return="0"> +<exif:Fired>True</exif:Fired> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></rdf:Description></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/3-invalid.result.php b/tests/phpunit/data/xmp/3-invalid.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/3-invalid.result.php @@ -0,0 +1,7 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/3-invalid.xmp b/tests/phpunit/data/xmp/3-invalid.xmp new file mode 100644 index 00000000..2425e254 --- /dev/null +++ b/tests/phpunit/data/xmp/3-invalid.xmp @@ -0,0 +1,31 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<!-- +This file has an invalid flash compoenent (one of the values are a qualifier) +--> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" +> +<exif:DigitalZoomRatio> + +<rdf:Description> +<rdf:value> +0/10 +</rdf:value> +<exif:foobarbaz>fred</exif:foobarbaz> + +</rdf:Description> + +</exif:DigitalZoomRatio> + +<exif:Flash> +<rdf:Description exif:Return="0"> +<exif:Mode><rdf:Description> +<rdf:value>1</rdf:value> +<exif:Fired>False</exif:Fired> <!-- qualifier. should be ignored--> +</rdf:Description> +</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></rdf:Description></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/3.result.php b/tests/phpunit/data/xmp/3.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/3.result.php @@ -0,0 +1,8 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/3.xmp b/tests/phpunit/data/xmp/3.xmp new file mode 100644 index 00000000..2cf19883 --- /dev/null +++ b/tests/phpunit/data/xmp/3.xmp @@ -0,0 +1,29 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" +> +<exif:DigitalZoomRatio> + +<rdf:Description> +<rdf:value> +0/10 +</rdf:value> +<exif:foobarbaz>fred</exif:foobarbaz> + +</rdf:Description> + +</exif:DigitalZoomRatio> + +<exif:Flash> +<rdf:Description exif:Return="0"> +<exif:Fired>True</exif:Fired> +<exif:Mode><rdf:Description> +<rdf:value>1</rdf:value> +<exif:Fired>False</exif:Fired> <!-- qualifier. should be ignored--> +</rdf:Description> +</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></rdf:Description></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/4.result.php b/tests/phpunit/data/xmp/4.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/4.result.php @@ -0,0 +1,7 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/4.xmp b/tests/phpunit/data/xmp/4.xmp new file mode 100644 index 00000000..29eb614b --- /dev/null +++ b/tests/phpunit/data/xmp/4.xmp @@ -0,0 +1,22 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<!-- Valid output is just the DigitalZoomRatio +as the flash is a qualifier +--> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/"> + <exif:DigitalZoomRatio> +<rdf:Description> +<rdf:value> +0/10 +</rdf:value> +<exif:Flash rdf:parseType='Resource'> +<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> +</rdf:Description> +</exif:DigitalZoomRatio> +</rdf:Description> </rdf:RDF> </x:xmpmeta> + + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/5.result.php b/tests/phpunit/data/xmp/5.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/5.result.php @@ -0,0 +1,7 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/5.xmp b/tests/phpunit/data/xmp/5.xmp new file mode 100644 index 00000000..3cc61d68 --- /dev/null +++ b/tests/phpunit/data/xmp/5.xmp @@ -0,0 +1,16 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/"> + <exif:DigitalZoomRatio> +<rdf:Description rdf:value="0/10"> +<exif:Flash rdf:parseType='Resource'> +<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> +</rdf:Description> +</exif:DigitalZoomRatio> +</rdf:Description> </rdf:RDF> </x:xmpmeta> + + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/6.result.php b/tests/phpunit/data/xmp/6.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/6.result.php @@ -0,0 +1,8 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/6.xmp b/tests/phpunit/data/xmp/6.xmp new file mode 100644 index 00000000..f435ab23 --- /dev/null +++ b/tests/phpunit/data/xmp/6.xmp @@ -0,0 +1,18 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/"> +<exif:DigitalZoomRatio> +0/10 +</exif:DigitalZoomRatio> +</rdf:Description> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/"> + +<exif:Flash rdf:parseType='Resource'> +<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/7.result.php b/tests/phpunit/data/xmp/7.result.php new file mode 100644 index 00000000..0efcfa36 --- /dev/null +++ b/tests/phpunit/data/xmp/7.result.php @@ -0,0 +1,52 @@ +<?php +$result = array ( + 'xmp-exif' => + array ( + 'CameraOwnerName' => 'Me!', + ), + 'xmp-general' => + array ( + 'LicenseUrl' => 'http://creativecommons.com/cc-by-2.9', + 'ImageDescription' => + array ( + 'x-default' => 'Test image for the cc: xmp: xmpRights: namespaces in xmp', + '_type' => 'lang', + ), + 'ObjectName' => + array ( + 'x-default' => 'xmp core/xmp rights/cc ns test', + '_type' => 'lang', + ), + 'DateTimeDigitized' => '2005:04:03', + 'Software' => 'The one true editor: Vi (ok i used gimp)', + 'Identifier' => + array ( + 0 => 'http://example.com/identifierurl', + 1 => 'urn:sha1:342524abcdef', + '_type' => 'ul', + ), + 'Label' => 'Test image', + 'DateTimeMetadata' => '2011:05:12', + 'DateTime' => '2007:03:04 06:34:10', + 'Nickname' => 'My little xmp test image', + 'Rating' => '5', + 'RightsCertificate' => 'http://example.com/rights-certificate/', + 'Copyrighted' => 'True', + 'CopyrightOwner' => + array ( + 0 => 'Bawolff is copyright owner', + '_type' => 'ul', + ), + 'UsageTerms' => + array ( + 'x-default' => 'do whatever you want', + 'en-gb' => 'Do whatever you want in british english', + '_type' => 'lang', + ), + 'WebStatement' => 'http://example.com/web_statement', + ), + 'xmp-deprecated' => + array ( + 'Identifier' => 'http://example.com/identifierurl/wrong', + ), +); diff --git a/tests/phpunit/data/xmp/7.xmp b/tests/phpunit/data/xmp/7.xmp new file mode 100644 index 00000000..e18e13d9 --- /dev/null +++ b/tests/phpunit/data/xmp/7.xmp @@ -0,0 +1,67 @@ +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> +<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'> +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> + + <rdf:Description rdf:about='' + xmlns:aux='http://ns.adobe.com/exif/1.0/aux/'> + <aux:OwnerName>Me!</aux:OwnerName> + </rdf:Description> + + <rdf:Description rdf:about='' + xmlns:cc='http://creativecommons.org/ns#'> + <cc:license>http://creativecommons.com/cc-by-2.9</cc:license> + </rdf:Description> + + <rdf:Description rdf:about='' + xmlns:dc='http://purl.org/dc/elements/1.1/'> + <dc:description> + <rdf:Alt> + <rdf:li xml:lang='x-default'>Test image for the cc: xmp: xmpRights: namespaces in xmp</rdf:li> + </rdf:Alt> + </dc:description> + <dc:identifier>http://example.com/identifierurl/wrong</dc:identifier> + <dc:title> + <rdf:Alt> + <rdf:li xml:lang='x-default'>xmp core/xmp rights/cc ns test</rdf:li> + </rdf:Alt> + </dc:title> + </rdf:Description> + + <rdf:Description rdf:about='' + xmlns:xmp='http://ns.adobe.com/xap/1.0/'> + <xmp:CreateDate>2005-04-03</xmp:CreateDate> + <xmp:CreatorTool>The one true editor: Vi (ok i used gimp)</xmp:CreatorTool> + <xmp:Identifier> + <rdf:Bag> + <rdf:li>http://example.com/identifierurl +</rdf:li> + <rdf:li>urn:sha1:342524abcdef</rdf:li> + </rdf:Bag> + </xmp:Identifier> + <xmp:Label>Test image</xmp:Label> + <xmp:MetadataDate>2011-05-12</xmp:MetadataDate> + <xmp:ModifyDate>2007-03-04T12:34:10-06:00</xmp:ModifyDate> + <xmp:Nickname>My little xmp test image</xmp:Nickname> + <xmp:Rating>7</xmp:Rating> + </rdf:Description> + + <rdf:Description rdf:about='' + xmlns:xmpRights='http://ns.adobe.com/xap/1.0/rights/'> + <xmpRights:Certificate>http://example.com/rights-certificate/</xmpRights:Certificate> + <xmpRights:Marked>True</xmpRights:Marked> + <xmpRights:Owner> + <rdf:Bag> + <rdf:li>Bawolff is copyright owner</rdf:li> + </rdf:Bag> + </xmpRights:Owner> + <xmpRights:UsageTerms> + <rdf:Alt> + <rdf:li xml:lang='x-default'>do whatever you want</rdf:li> + <rdf:li xml:lang='en-GB'>Do whatever you want in british english</rdf:li> + </rdf:Alt> + </xmpRights:UsageTerms> + <xmpRights:WebStatement>http://example.com/web_statement</xmpRights:WebStatement> + </rdf:Description> +</rdf:RDF> +</x:xmpmeta> +<?xpacket end='r'?> diff --git a/tests/phpunit/data/xmp/README b/tests/phpunit/data/xmp/README new file mode 100644 index 00000000..bd949176 --- /dev/null +++ b/tests/phpunit/data/xmp/README @@ -0,0 +1,3 @@ +This directory contains a bunch of XMP files +as well as a bunch of php files containing what the +parsed version of the XMP looks like. diff --git a/tests/phpunit/data/xmp/bag-for-seq.result.php b/tests/phpunit/data/xmp/bag-for-seq.result.php new file mode 100644 index 00000000..b5244f88 --- /dev/null +++ b/tests/phpunit/data/xmp/bag-for-seq.result.php @@ -0,0 +1,10 @@ +<?php + +$result = array( + 'xmp-general' => array( + 'Artist' => array( + '_type' => 'ul', + 0 => 'The author', + ) + ) +); diff --git a/tests/phpunit/data/xmp/bag-for-seq.xmp b/tests/phpunit/data/xmp/bag-for-seq.xmp new file mode 100644 index 00000000..c6ed5b7c --- /dev/null +++ b/tests/phpunit/data/xmp/bag-for-seq.xmp @@ -0,0 +1 @@ +<?xpacket begin=""?> <x:xmpmeta xmlns:x="adobe:ns:meta/"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/"> <dc:creator> <rdf:Bag> <rdf:li>The author</rdf:li> </rdf:Bag> </dc:creator> </rdf:Description> </rdf:RDF> </x:xmpmeta> diff --git a/tests/phpunit/data/xmp/flash.result.php b/tests/phpunit/data/xmp/flash.result.php new file mode 100644 index 00000000..018c0ac1 --- /dev/null +++ b/tests/phpunit/data/xmp/flash.result.php @@ -0,0 +1,8 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '127' + ) +); diff --git a/tests/phpunit/data/xmp/flash.xmp b/tests/phpunit/data/xmp/flash.xmp new file mode 100644 index 00000000..b1373cc2 --- /dev/null +++ b/tests/phpunit/data/xmp/flash.xmp @@ -0,0 +1,11 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + exif:DigitalZoomRatio="0/10"> +<exif:Flash rdf:parseType='Resource'> +<exif:Fired>True</exif:Fired> <exif:Return>3</exif:Return> <exif:Mode>3</exif:Mode> <exif:Function>True</exif:Function> <exif:RedEyeMode>True</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/invalid-child-not-struct.result.php b/tests/phpunit/data/xmp/invalid-child-not-struct.result.php new file mode 100644 index 00000000..5741b2c9 --- /dev/null +++ b/tests/phpunit/data/xmp/invalid-child-not-struct.result.php @@ -0,0 +1,7 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ) +); diff --git a/tests/phpunit/data/xmp/invalid-child-not-struct.xmp b/tests/phpunit/data/xmp/invalid-child-not-struct.xmp new file mode 100644 index 00000000..6aa0c10b --- /dev/null +++ b/tests/phpunit/data/xmp/invalid-child-not-struct.xmp @@ -0,0 +1,12 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + exif:DigitalZoomRatio="0/10"> +<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode> + + </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/no-namespace.result.php b/tests/phpunit/data/xmp/no-namespace.result.php new file mode 100644 index 00000000..3ff69201 --- /dev/null +++ b/tests/phpunit/data/xmp/no-namespace.result.php @@ -0,0 +1,7 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'FNumber' => '28/10', + ) +); diff --git a/tests/phpunit/data/xmp/no-namespace.xmp b/tests/phpunit/data/xmp/no-namespace.xmp new file mode 100644 index 00000000..7d6cdb2f --- /dev/null +++ b/tests/phpunit/data/xmp/no-namespace.xmp @@ -0,0 +1,11 @@ +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<!-- Testing it handles random non-namespaced properties in files ok. + Some older photoshop's did not include the rdf: prefix on about. --> +<rdf:Description + about="" + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + exif:FNumber="28/10"> +</rdf:Description> +</rdf:RDF> +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/no-recognized-props.result.php b/tests/phpunit/data/xmp/no-recognized-props.result.php new file mode 100644 index 00000000..b3ca9f5a --- /dev/null +++ b/tests/phpunit/data/xmp/no-recognized-props.result.php @@ -0,0 +1,2 @@ +<?php +$result = array(); diff --git a/tests/phpunit/data/xmp/no-recognized-props.xmp b/tests/phpunit/data/xmp/no-recognized-props.xmp new file mode 100644 index 00000000..54e80901 --- /dev/null +++ b/tests/phpunit/data/xmp/no-recognized-props.xmp @@ -0,0 +1,8 @@ +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/not-exif-namespace" + exif:FNumber="2/10"> +</rdf:Description> +</rdf:RDF> +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/utf16BE.result.php b/tests/phpunit/data/xmp/utf16BE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf16BE.result.php @@ -0,0 +1,12 @@ +<?php + +$result = array( + 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '' + ), +); diff --git a/tests/phpunit/data/xmp/utf16BE.xmp b/tests/phpunit/data/xmp/utf16BE.xmp Binary files differnew file mode 100644 index 00000000..0cf60d60 --- /dev/null +++ b/tests/phpunit/data/xmp/utf16BE.xmp diff --git a/tests/phpunit/data/xmp/utf16LE.result.php b/tests/phpunit/data/xmp/utf16LE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf16LE.result.php @@ -0,0 +1,12 @@ +<?php + +$result = array( + 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '' + ), +); diff --git a/tests/phpunit/data/xmp/utf16LE.xmp b/tests/phpunit/data/xmp/utf16LE.xmp Binary files differnew file mode 100644 index 00000000..66d71f4c --- /dev/null +++ b/tests/phpunit/data/xmp/utf16LE.xmp diff --git a/tests/phpunit/data/xmp/utf32BE.result.php b/tests/phpunit/data/xmp/utf32BE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf32BE.result.php @@ -0,0 +1,12 @@ +<?php + +$result = array( + 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '' + ), +); diff --git a/tests/phpunit/data/xmp/utf32BE.xmp b/tests/phpunit/data/xmp/utf32BE.xmp Binary files differnew file mode 100644 index 00000000..06afdf92 --- /dev/null +++ b/tests/phpunit/data/xmp/utf32BE.xmp diff --git a/tests/phpunit/data/xmp/utf32LE.result.php b/tests/phpunit/data/xmp/utf32LE.result.php new file mode 100644 index 00000000..ac7ea506 --- /dev/null +++ b/tests/phpunit/data/xmp/utf32LE.result.php @@ -0,0 +1,12 @@ +<?php + +$result = array( + 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + ), + 'xmp-general' => + array( + 'Label' => '' + ), +); diff --git a/tests/phpunit/data/xmp/utf32LE.xmp b/tests/phpunit/data/xmp/utf32LE.xmp Binary files differnew file mode 100644 index 00000000..bf2097fe --- /dev/null +++ b/tests/phpunit/data/xmp/utf32LE.xmp diff --git a/tests/phpunit/data/xmp/xmpExt.result.php b/tests/phpunit/data/xmp/xmpExt.result.php new file mode 100644 index 00000000..beead1bd --- /dev/null +++ b/tests/phpunit/data/xmp/xmpExt.result.php @@ -0,0 +1,8 @@ +<?php + +$result = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => '9' + ) +); diff --git a/tests/phpunit/data/xmp/xmpExt.xmp b/tests/phpunit/data/xmp/xmpExt.xmp new file mode 100644 index 00000000..da0383f8 --- /dev/null +++ b/tests/phpunit/data/xmp/xmpExt.xmp @@ -0,0 +1,13 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core + 4.1.3-c001 49.282696, Mon Apr 02 2007 21:16:10 "> +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + xmlns:xmpNote="http://ns.adobe.com/xmp/note/" + exif:DigitalZoomRatio="0/10" + xmpNote:HasExtendedXMP="28C74E0AC2D796886759006FBE2E57B7"> +<exif:Flash rdf:parseType='Resource'> +<exif:Fired>True</exif:Fired> <exif:Return>0</exif:Return> <exif:Mode>1</exif:Mode> <exif:Function>False</exif:Function> <exif:RedEyeMode>False</exif:RedEyeMode></exif:Flash> </rdf:Description> </rdf:RDF> </x:xmpmeta> + +<?xpacket end="w"?> diff --git a/tests/phpunit/data/xmp/xmpExt2.xmp b/tests/phpunit/data/xmp/xmpExt2.xmp new file mode 100644 index 00000000..060abb2c --- /dev/null +++ b/tests/phpunit/data/xmp/xmpExt2.xmp @@ -0,0 +1,8 @@ +<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> +<rdf:Description + rdf:about="" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + exif:FNumber="2/10"> +</rdf:Description> +</rdf:RDF> +<?xpacket end="w"?> diff --git a/tests/phpunit/data/zip/cd-gap.zip b/tests/phpunit/data/zip/cd-gap.zip Binary files differnew file mode 100644 index 00000000..b5ae6ccd --- /dev/null +++ b/tests/phpunit/data/zip/cd-gap.zip diff --git a/tests/phpunit/data/zip/cd-truncated.zip b/tests/phpunit/data/zip/cd-truncated.zip Binary files differnew file mode 100644 index 00000000..4d40d7d4 --- /dev/null +++ b/tests/phpunit/data/zip/cd-truncated.zip diff --git a/tests/phpunit/data/zip/class-trailing-null.zip b/tests/phpunit/data/zip/class-trailing-null.zip Binary files differnew file mode 100644 index 00000000..31dcf3d8 --- /dev/null +++ b/tests/phpunit/data/zip/class-trailing-null.zip diff --git a/tests/phpunit/data/zip/class-trailing-slash.zip b/tests/phpunit/data/zip/class-trailing-slash.zip Binary files differnew file mode 100644 index 00000000..9eb1f037 --- /dev/null +++ b/tests/phpunit/data/zip/class-trailing-slash.zip diff --git a/tests/phpunit/data/zip/class.zip b/tests/phpunit/data/zip/class.zip Binary files differnew file mode 100644 index 00000000..98a625b7 --- /dev/null +++ b/tests/phpunit/data/zip/class.zip diff --git a/tests/phpunit/data/zip/empty.zip b/tests/phpunit/data/zip/empty.zip Binary files differnew file mode 100644 index 00000000..15cb0ecb --- /dev/null +++ b/tests/phpunit/data/zip/empty.zip diff --git a/tests/phpunit/data/zip/looks-like-zip64.zip b/tests/phpunit/data/zip/looks-like-zip64.zip Binary files differnew file mode 100644 index 00000000..7428cddd --- /dev/null +++ b/tests/phpunit/data/zip/looks-like-zip64.zip diff --git a/tests/phpunit/data/zip/nosig.zip b/tests/phpunit/data/zip/nosig.zip Binary files differnew file mode 100644 index 00000000..a22c73a4 --- /dev/null +++ b/tests/phpunit/data/zip/nosig.zip diff --git a/tests/phpunit/data/zip/split.zip b/tests/phpunit/data/zip/split.zip Binary files differnew file mode 100644 index 00000000..6984ae6d --- /dev/null +++ b/tests/phpunit/data/zip/split.zip diff --git a/tests/phpunit/data/zip/trail.zip b/tests/phpunit/data/zip/trail.zip Binary files differnew file mode 100644 index 00000000..50bcea12 --- /dev/null +++ b/tests/phpunit/data/zip/trail.zip diff --git a/tests/phpunit/data/zip/wrong-cd-start-disk.zip b/tests/phpunit/data/zip/wrong-cd-start-disk.zip Binary files differnew file mode 100644 index 00000000..59b45938 --- /dev/null +++ b/tests/phpunit/data/zip/wrong-cd-start-disk.zip diff --git a/tests/phpunit/data/zip/wrong-central-entry-sig.zip b/tests/phpunit/data/zip/wrong-central-entry-sig.zip Binary files differnew file mode 100644 index 00000000..05329b43 --- /dev/null +++ b/tests/phpunit/data/zip/wrong-central-entry-sig.zip diff --git a/tests/phpunit/includes/ArticleTablesTest.php b/tests/phpunit/includes/ArticleTablesTest.php new file mode 100644 index 00000000..01776c95 --- /dev/null +++ b/tests/phpunit/includes/ArticleTablesTest.php @@ -0,0 +1,34 @@ +<?php + +/** + * @group Database + */ +class ArticleTablesTest extends MediaWikiLangTestCase { + + function testbug14404() { + global $wgUser, $wgContLang, $wgLanguageCode, $wgLang; + + $title = Title::newFromText("Bug 14404"); + $article = new Article( $title ); + $wgUser = new User(); + $wgUser->mRights = array( 'createpage', 'edit', 'purge' ); + $wgLanguageCode = 'es'; + $wgContLang = Language::factory( 'es' ); + + $wgLang = Language::factory( 'fr' ); + $status = $article->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', 0 ); + $templates1 = $article->getUsedTemplates(); + + $wgLang = Language::factory( 'de' ); + $article->mParserOptions = null; // Let it pick the new user language + $article->mPreparedEdit = false; // In order to force the rerendering of the same wikitext + + // We need an edit, a purge is not enough to regenerate the tables + $status = $article->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', EDIT_UPDATE ); + $templates2 = $article->getUsedTemplates(); + + $this->assertEquals( $templates1, $templates2 ); + $this->assertEquals( $templates1[0]->getFullText(), 'Historial' ); + } + +} diff --git a/tests/phpunit/includes/ArticleTest.php b/tests/phpunit/includes/ArticleTest.php new file mode 100644 index 00000000..285efee9 --- /dev/null +++ b/tests/phpunit/includes/ArticleTest.php @@ -0,0 +1,82 @@ +<?php + +class ArticleTest extends MediaWikiTestCase { + + private $title; // holds a Title object + private $article; // holds an article + + /** creates a title object and its article object */ + function setUp() { + $this->title = Title::makeTitle( NS_MAIN, 'SomePage' ); + $this->article = new Article( $this->title ); + + } + + /** cleanup title object and its article object */ + function tearDown() { + $this->title = null; + $this->article = null; + + } + + function testImplementsGetMagic() { + $this->assertEquals( -1, $this->article->mCounter, "Article __get magic" ); + } + + /** + * @depends testImplementsGetMagic + */ + function testImplementsSetMagic() { + + $this->article->mCounter = 2; + $this->assertEquals( 2, $this->article->mCounter, "Article __set magic" ); + } + + /** + * @depends testImplementsSetMagic + */ + function testImplementsCallMagic() { + $this->article->mCounter = 33; + $this->assertEquals( 33, $this->article->getCount(), "Article __call magic" ); + } + + function testGetOrSetOnNewProperty() { + $this->article->ext_someNewProperty = 12; + $this->assertEquals( 12, $this->article->ext_someNewProperty, + "Article get/set magic on new field" ); + + $this->article->ext_someNewProperty = -8; + $this->assertEquals( -8, $this->article->ext_someNewProperty, + "Article get/set magic on update to new field" ); + } + + /** + * Checks for the existence of the backwards compatibility static functions (forwarders to WikiPage class) + */ + function testStaticFunctions() { + $this->assertEquals( WikiPage::selectFields(), Article::selectFields(), + "Article static functions" ); + $this->assertEquals( true, is_callable( "Article::onArticleCreate" ), + "Article static functions" ); + $this->assertEquals( true, is_callable( "Article::onArticleDelete" ), + "Article static functions" ); + $this->assertEquals( true, is_callable( "ImagePage::onArticleEdit" ), + "Article static functions" ); + $this->assertTrue( is_string( CategoryPage::getAutosummary( '', '', 0 ) ), + "Article static functions" ); + } + + function testWikiPageFactory() { + $title = Title::makeTitle( NS_FILE, 'Someimage.png' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( 'WikiFilePage', get_class( $page ) ); + + $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( 'WikiCategoryPage', get_class( $page ) ); + + $title = Title::makeTitle( NS_MAIN, 'SomePage' ); + $page = WikiPage::factory( $title ); + $this->assertEquals( 'WikiPage', get_class( $page ) ); + } +} diff --git a/tests/phpunit/includes/BlockTest.php b/tests/phpunit/includes/BlockTest.php new file mode 100644 index 00000000..2f224ba8 --- /dev/null +++ b/tests/phpunit/includes/BlockTest.php @@ -0,0 +1,124 @@ +<?php + +/** + * @group Database + */ +class BlockTest extends MediaWikiLangTestCase { + + const REASON = "Some reason"; + + private $block, $madeAt; + + /* variable used to save up the blockID we insert in this test suite */ + private $blockId; + + function setUp() { + global $wgContLang; + parent::setUp(); + $wgContLang = Language::factory( 'en' ); + } + + function addDBData() { + //$this->dumpBlocks(); + + $user = User::newFromName( 'UTBlockee' ); + if( $user->getID() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTBlockeePassword' ); + + $user->saveSettings(); + } + + // Delete the last round's block if it's still there + $oldBlock = Block::newFromTarget( 'UTBlockee' ); + if ( $oldBlock ) { + // An old block will prevent our new one from saving. + $oldBlock->delete(); + } + + $this->block = new Block( 'UTBlockee', 1, 0, + self::REASON + ); + $this->madeAt = wfTimestamp( TS_MW ); + + $this->block->insert(); + // save up ID for use in assertion. Since ID is an autoincrement, + // its value might change depending on the order the tests are run. + // ApiBlockTest insert its own blocks! + $newBlockId = $this->block->getId(); + if ($newBlockId) { + $this->blockId = $newBlockId; + } else { + throw new MWException( "Failed to insert block for BlockTest; old leftover block remaining?" ); + } + } + + /** + * debug function : dump the ipblocks table + */ + function dumpBlocks() { + $v = $this->db->query( 'SELECT * FROM unittest_ipblocks' ); + print "Got " . $v->numRows() . " rows. Full dump follow:\n"; + foreach( $v as $row ) { + print_r( $row ); + } + } + + function testInitializerFunctionsReturnCorrectBlock() { + // $this->dumpBlocks(); + + $this->assertTrue( $this->block->equals( Block::newFromTarget('UTBlockee') ), "newFromTarget() returns the same block as the one that was made"); + + $this->assertTrue( $this->block->equals( Block::newFromID( $this->blockId ) ), "newFromID() returns the same block as the one that was made"); + + } + + /** + * per bug 26425 + */ + function testBug26425BlockTimestampDefaultsToTime() { + + $this->assertEquals( $this->madeAt, $this->block->mTimestamp, "If no timestamp is specified, the block is recorded as time()"); + + } + + /** + * This is the method previously used to load block info in CheckUser etc + * passing an empty value (empty string, null, etc) as the ip parameter bypasses IP lookup checks. + * + * This stopped working with r84475 and friends: regression being fixed for bug 29116. + * + * @dataProvider dataBug29116 + */ + function testBug29116LoadWithEmptyIp( $vagueTarget ) { + $uid = User::idFromName( 'UTBlockee' ); + $this->assertTrue( ($uid > 0), 'Must be able to look up the target user during tests' ); + + $block = new Block(); + $ok = $block->load( $vagueTarget, $uid ); + $this->assertTrue( $ok, "Block->load() with empty IP and user ID '$uid' should return a block" ); + + $this->assertTrue( $this->block->equals( $block ), "Block->load() returns the same block as the one that was made when given empty ip param " . var_export( $vagueTarget, true ) ); + } + + /** + * CheckUser since being changed to use Block::newFromTarget started failing + * because the new function didn't accept empty strings like Block::load() + * had. Regression bug 29116. + * + * @dataProvider dataBug29116 + */ + function testBug29116NewFromTargetWithEmptyIp( $vagueTarget ) { + $block = Block::newFromTarget('UTBlockee', $vagueTarget); + $this->assertTrue( $this->block->equals( $block ), "newFromTarget() returns the same block as the one that was made when given empty vagueTarget param " . var_export( $vagueTarget, true ) ); + } + + function dataBug29116() { + return array( + array( null ), + array( '' ), + array( false ) + ); + } +} + diff --git a/tests/phpunit/includes/CdbTest.php b/tests/phpunit/includes/CdbTest.php new file mode 100644 index 00000000..6c3e6664 --- /dev/null +++ b/tests/phpunit/includes/CdbTest.php @@ -0,0 +1,84 @@ +<?php + +/** + * Test the CDB reader/writer + */ + +class CdbTest extends MediaWikiTestCase { + + 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 ) + ); + } +} diff --git a/tests/phpunit/includes/ExternalStoreTest.php b/tests/phpunit/includes/ExternalStoreTest.php new file mode 100644 index 00000000..92ec7344 --- /dev/null +++ b/tests/phpunit/includes/ExternalStoreTest.php @@ -0,0 +1,32 @@ +<?php +/** + * External Store tests + */ + +class ExternalStoreTest extends MediaWikiTestCase { + 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' ) ); + } +} + diff --git a/tests/phpunit/includes/ExtraParserTest.php b/tests/phpunit/includes/ExtraParserTest.php new file mode 100644 index 00000000..5b0aa98b --- /dev/null +++ b/tests/phpunit/includes/ExtraParserTest.php @@ -0,0 +1,113 @@ +<?php + +/** + * Parser-related tests that don't suit for parserTests.txt + */ +class ExtraParserTest extends MediaWikiTestCase { + + function setUp() { + global $wgMemc; + global $wgContLang; + global $wgShowDBErrorBacktrace; + global $wgLanguageCode; + + $wgShowDBErrorBacktrace = true; + $wgLanguageCode = 'en'; + $wgContLang = new Language( 'en' ); + $wgMemc = new EmptyBagOStuff; + + $this->options = new ParserOptions; + $this->options->setTemplateCallback( array( __CLASS__, 'statelessFetchTemplate' ) ); + $this->parser = new Parser; + } + + // 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; + + $t = Title::newFromText( 'Unit test' ); + $options = ParserOptions::newFromUser( $wgUser ); + $this->assertEquals( "<p>$longLine</p>", + $this->parser->parse( $longLine, $t, $options )->getText() ); + } + + /* Test the parser entry points */ + function testParse() { + $title = Title::newFromText( __FUNCTION__ ); + $parserOutput = $this->parser->parse( "Test\n{{Foo}}\n{{Bar}}" , $title, $this->options ); + $this->assertEquals( "<p>Test\nContent of <i>Template:Foo</i>\nContent of <i>Template:Bar</i>\n</p>", $parserOutput->getText() ); + } + + function testPreSaveTransform() { + global $wgUser; + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->preSaveTransform( "Test\r\n{{subst:Foo}}\n{{Bar}}", $title, $wgUser, $this->options ); + + $this->assertEquals( "Test\nContent of ''Template:Foo''\n{{Bar}}", $outputText ); + } + + function testPreprocess() { + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->preprocess( "Test\n{{Foo}}\n{{Bar}}" , $title, $this->options ); + + $this->assertEquals( "Test\nContent of ''Template:Foo''\nContent of ''Template:Bar''", $outputText ); + } + + /** + * cleanSig() makes all templates substs and removes tildes + */ + function testCleanSig() { + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->cleanSig( "{{Foo}} ~~~~" ); + + $this->assertEquals( "{{SUBST:Foo}} ", $outputText ); + } + + /** + * cleanSigInSig() just removes tildes + */ + function testCleanSigInSig() { + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->cleanSigInSig( "{{Foo}} ~~~~" ); + + $this->assertEquals( "{{Foo}} ", $outputText ); + } + + function testGetSection() { + $outputText2 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 2 ); + $outputText1 = $this->parser->getSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1 ); + + $this->assertEquals( "=== Heading 2 ===\nSection 2", $outputText2 ); + $this->assertEquals( "== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2", $outputText1 ); + } + + function testReplaceSection() { + $outputText = $this->parser->replaceSection( "Section 0\n== Heading 1 ==\nSection 1\n=== Heading 2 ===\nSection 2\n== Heading 3 ==\nSection 3\n", 1, "New section 1" ); + + $this->assertEquals( "Section 0\nNew section 1\n\n== Heading 3 ==\nSection 3", $outputText ); + } + + /** + * Templates and comments are not affected, but noinclude/onlyinclude is. + */ + function testGetPreloadText() { + $title = Title::newFromText( __FUNCTION__ ); + $outputText = $this->parser->getPreloadText( "{{Foo}}<noinclude> censored</noinclude> information <!-- is very secret -->", $title, $this->options ); + + $this->assertEquals( "{{Foo}} information <!-- is very secret -->", $outputText ); + } + + static function statelessFetchTemplate( $title, $parser=false ) { + $text = "Content of ''" . $title->getFullText() . "''"; + $deps = array(); + + return array( + 'text' => $text, + 'finalTitle' => $title, + 'deps' => $deps ); + } + } diff --git a/tests/phpunit/includes/FauxResponseTest.php b/tests/phpunit/includes/FauxResponseTest.php new file mode 100644 index 00000000..c0420049 --- /dev/null +++ b/tests/phpunit/includes/FauxResponseTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Tests for the FauxResponse class + * + * Copyright @ 2011 Alexandre Emsenhuber + * + * 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 + */ + +class FauxResponseTest extends MediaWikiTestCase { + var $response; + + function setUp() { + $this->response = new FauxResponse; + } + + function testCookie() { + $this->assertEquals( null, $this->response->getcookie( 'key' ), 'Non-existing cookie' ); + $this->response->setcookie( 'key', 'val' ); + $this->assertEquals( 'val', $this->response->getcookie( 'key' ), 'Existing cookie' ); + } + + function testHeader() { + $this->assertEquals( null, $this->response->getheader( 'Location' ), 'Non-existing header' ); + + $this->response->header( 'Location: http://localhost/' ); + $this->assertEquals( 'http://localhost/', $this->response->getheader( 'Location' ), 'Set header' ); + + $this->response->header( 'Location: http://127.0.0.1/' ); + $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header' ); + + $this->response->header( 'Location: http://127.0.0.2/', false ); + $this->assertEquals( 'http://127.0.0.1/', $this->response->getheader( 'Location' ), 'Same header with override disabled' ); + } + + function testResponseCode() { + $this->response->header( 'HTTP/1.1 200' ); + $this->assertEquals( 200, $this->response->getStatusCode(), 'Header with no message' ); + + $this->response->header( 'HTTP/1.x 201' ); + $this->assertEquals( 201, $this->response->getStatusCode(), 'Header with no message and protocol 1.x' ); + + $this->response->header( 'HTTP/1.1 202 OK' ); + $this->assertEquals( 202, $this->response->getStatusCode(), 'Normal header' ); + + $this->response->header( 'HTTP/1.x 203 OK' ); + $this->assertEquals( 203, $this->response->getStatusCode(), 'Normal header with no message and protocol 1.x' ); + + $this->response->header( 'HTTP/1.x 204 OK', false, 205 ); + $this->assertEquals( 205, $this->response->getStatusCode(), 'Third parameter overrides the HTTP/... header' ); + + $this->response->header( 'Location: http://localhost/', false, 206 ); + $this->assertEquals( 206, $this->response->getStatusCode(), 'Third parameter with another header' ); + } +} diff --git a/tests/phpunit/includes/FormOptionsInitializationTest.php b/tests/phpunit/includes/FormOptionsInitializationTest.php new file mode 100644 index 00000000..85d76271 --- /dev/null +++ b/tests/phpunit/includes/FormOptionsInitializationTest.php @@ -0,0 +1,85 @@ +<?php +/** + * This file host two test case classes for the MediaWiki FormOptions class: + * - FormOptionsInitializationTest : tests initialization of the class. + * - FormOptionsTest : tests methods an on instance + * + * The split let us take advantage of setting up a fixture for the methods + * tests. + */ + +/** + * Dummy class to makes FormOptions::$options public. + * Used by FormOptionsInitializationTest which need to verify the $options + * array is correctly set through the FormOptions::add() function. + */ +class FormOptionsExposed extends FormOptions { + public function getOptions() { + return $this->options; + } +} + +/** + * Test class for FormOptions initialization + * Ensure the FormOptions::add() does what we want it to do. + * + * Generated by PHPUnit on 2011-02-28 at 20:46:27. + * + * Copyright © 2011, Ashar Voultoiz + * + * @author Ashar Voultoiz + */ +class FormOptionsInitializationTest extends MediaWikiTestCase { + /** + * @var FormOptions + */ + protected $object; + + + /** + * A new fresh and empty FormOptions object to test initialization + * with. + */ + protected function setUp() { + $this->object = new FormOptionsExposed(); + + } + + public function testAddStringOption() { + $this->object->add( 'foo', 'string value' ); + $this->assertEquals( + array( + 'foo' => array( + 'default' => 'string value', + 'consumed' => false, + 'type' => FormOptions::STRING, + 'value' => null, + ) + ), + $this->object->getOptions() + ); + } + + public function testAddIntegers() { + $this->object->add( 'one', 1 ); + $this->object->add( 'negone', -1 ); + $this->assertEquals( + array( + 'negone' => array( + 'default' => -1, + 'value' => null, + 'consumed' => false, + 'type' => FormOptions::INT, + ), + 'one' => array( + 'default' => 1, + 'value' => null, + 'consumed' => false, + 'type' => FormOptions::INT, + ) + ), + $this->object->getOptions() + ); + } + +} diff --git a/tests/phpunit/includes/FormOptionsTest.php b/tests/phpunit/includes/FormOptionsTest.php new file mode 100644 index 00000000..86618d93 --- /dev/null +++ b/tests/phpunit/includes/FormOptionsTest.php @@ -0,0 +1,90 @@ +<?php +/** + * This file host two test case classes for the MediaWiki FormOptions class: + * - FormOptionsInitializationTest : tests initialization of the class. + * - FormOptionsTest : tests methods an on instance + * + * The split let us take advantage of setting up a fixture for the methods + * tests. + */ + +/** + * Test class for FormOptions methods. + * Generated by PHPUnit on 2011-02-28 at 20:46:27. + * + * Copyright © 2011, Ashar Voultoiz + * + * @author Ashar Voultoiz + */ +class FormOptionsTest extends MediaWikiTestCase { + /** + * @var FormOptions + */ + protected $object; + + /** + * Instanciates a FormOptions object to play with. + * FormOptions::add() is tested by the class FormOptionsInitializationTest + * so we assume the function is well tested already an use it to create + * the fixture. + */ + protected function setUp() { + $this->object = new FormOptions; + $this->object->add( 'string1', 'string one' ); + $this->object->add( 'string2', 'string two' ); + $this->object->add( 'integer', 0 ); + $this->object->add( 'intnull', 0, FormOptions::INTNULL ); + } + + /** Helpers for testGuessType() */ + /* @{ */ + private function assertGuessBoolean( $data ) { + $this->guess( FormOptions::BOOL, $data ); + } + private function assertGuessInt( $data ) { + $this->guess( FormOptions::INT, $data ); + } + private function assertGuessString( $data ) { + $this->guess( FormOptions::STRING, $data ); + } + + /** Generic helper */ + private function guess( $expected, $data ) { + $this->assertEquals( + $expected, + FormOptions::guessType( $data ) + ); + } + /* @} */ + + /** + * Reuse helpers above assertGuessBoolean assertGuessInt assertGuessString + */ + public function testGuessTypeDetection() { + $this->assertGuessBoolean( true ); + $this->assertGuessBoolean( false ); + + $this->assertGuessInt( 0 ); + $this->assertGuessInt( -5 ); + $this->assertGuessInt( 5 ); + $this->assertGuessInt( 0x0F ); + + $this->assertGuessString( 'true' ); + $this->assertGuessString( 'false' ); + $this->assertGuessString( '5' ); + $this->assertGuessString( '0' ); + } + + /** + * @expectedException MWException + */ + public function testGuessTypeOnArrayThrowException() { + $this->object->guessType( array( 'foo' ) ); + } + /** + * @expectedException MWException + */ + public function testGuessTypeOnNullThrowException() { + $this->object->guessType( null ); + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/GlobalTest.php b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php new file mode 100644 index 00000000..3d157d0a --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/GlobalTest.php @@ -0,0 +1,902 @@ +<?php + +class GlobalTest extends MediaWikiTestCase { + function setUp() { + global $wgReadOnlyFile, $wgUrlProtocols; + $this->originals['wgReadOnlyFile'] = $wgReadOnlyFile; + $this->originals['wgUrlProtocols'] = $wgUrlProtocols; + $wgReadOnlyFile = tempnam( wfTempDir(), "mwtest_readonly" ); + $wgUrlProtocols[] = 'file://'; + unlink( $wgReadOnlyFile ); + } + + function tearDown() { + global $wgReadOnlyFile, $wgUrlProtocols; + if ( file_exists( $wgReadOnlyFile ) ) { + unlink( $wgReadOnlyFile ); + } + $wgReadOnlyFile = $this->originals['wgReadOnlyFile']; + $wgUrlProtocols = $this->originals['wgUrlProtocols']; + } + + /** @dataProvider provideForWfArrayDiff2 */ + public function testWfArrayDiff2( $a, $b, $expected ) { + $this->assertEquals( + wfArrayDiff2( $a, $b), $expected + ); + } + + // @todo Provide more tests + public function provideForWfArrayDiff2() { + // $a $b $expected + return array( + array( + array( 'a', 'b'), + array( 'a', 'b'), + array(), + ), + array( + array( array( 'a'), array( 'a', 'b', 'c' )), + array( array( 'a'), array( 'a', 'b' )), + array( 1 => array( 'a', 'b', 'c' ) ), + ), + ); + } + + 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; # Check on $wgReadOnlyFile + + $this->assertTrue( wfReadOnly() ); + $this->assertTrue( wfReadOnly() ); # Check cached + + unlink( $wgReadOnlyFile ); + $wgReadOnly = null; # Clean cache + + $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->assertInternalType( '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' ) ) ); + $this->assertEquals( + "path%5B0%5D=wiki&path%5B1%5D=test&cfg%5Bservers%5D%5Bhttp%5D=localhost", + wfArrayToCGI( array( + 'path' => array( 'wiki', 'test' ), + 'cfg' => array( 'servers' => array( 'http' => 'localhost' ) ) ) ) ); + } + + function testCgiToArray() { + $this->assertEquals( + array( 'path' => array( 'wiki', 'test' ), + 'cfg' => array( 'servers' => array( 'http' => 'localhost' ) ) ), + wfCgiToArray( 'path%5B0%5D=wiki&path%5B1%5D=test&cfg%5Bservers%5D%5Bhttp%5D=localhost' ) ); + } + + 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]]'); + + /* @todo 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 testTimestampParameter() { + // There are a number of assumptions in our codebase where wfTimestamp() should give + // the current date but it is not given a 0 there. See r71751 CR + + $now = wfTimestamp( TS_UNIX ); + // We check that wfTimestamp doesn't return false (error) and use a LessThan assert + // for the cases where the test is run in a second boundary. + + $zero = wfTimestamp( TS_UNIX, 0 ); + $this->assertNotEquals( false, $zero ); + $this->assertLessThan( 5, $zero - $now ); + + $empty = wfTimestamp( TS_UNIX, '' ); + $this->assertNotEquals( false, $empty ); + $this->assertLessThan( 5, $empty - $now ); + + $null = wfTimestamp( TS_UNIX, null ); + $this->assertNotEquals( false, $null ); + $this->assertLessThan( 5, $null - $now ); + } + + 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'" ); + } + } + + + function testFallbackMbstringFunctions() { + + if( !extension_loaded( 'mbstring' ) ) { + $this->markTestSkipped( "The mb_string functions must be installed to test the fallback functions" ); + } + + $sampleUTF = "Östergötland_coat_of_arms.png"; + + + //mb_substr + $substr_params = array( + array( 0, 0 ), + array( 5, -4 ), + array( 33 ), + array( 100, -5 ), + array( -8, 10 ), + array( 1, 1 ), + array( 2, -1 ) + ); + + foreach( $substr_params as $param_set ) { + $old_param_set = $param_set; + array_unshift( $param_set, $sampleUTF ); + + $this->assertEquals( + MWFunction::callArray( 'mb_substr', $param_set ), + MWFunction::callArray( 'Fallback::mb_substr', $param_set ), + 'Fallback mb_substr with params ' . implode( ', ', $old_param_set ) + ); + } + + + //mb_strlen + $this->assertEquals( + mb_strlen( $sampleUTF ), + Fallback::mb_strlen( $sampleUTF ), + 'Fallback mb_strlen' + ); + + + //mb_str(r?)pos + $strpos_params = array( + //array( 'ter' ), + //array( 'Ö' ), + //array( 'Ö', 3 ), + //array( 'oat_', 100 ), + //array( 'c', -10 ), + //Broken for now + ); + + foreach( $strpos_params as $param_set ) { + $old_param_set = $param_set; + array_unshift( $param_set, $sampleUTF ); + + $this->assertEquals( + MWFunction::callArray( 'mb_strpos', $param_set ), + MWFunction::callArray( 'Fallback::mb_strpos', $param_set ), + 'Fallback mb_strpos with params ' . implode( ', ', $old_param_set ) + ); + + $this->assertEquals( + MWFunction::callArray( 'mb_strrpos', $param_set ), + MWFunction::callArray( 'Fallback::mb_strrpos', $param_set ), + 'Fallback mb_strrpos with params ' . implode( ', ', $old_param_set ) + ); + } + + } + + + function testDebugFunctionTest() { + + global $wgDebugLogFile, $wgOut, $wgShowDebug, $wgDebugTimestamps; + + $old_log_file = $wgDebugLogFile; + $wgDebugLogFile = tempnam( wfTempDir(), 'mw-' ); + # @todo FIXME: This setting should be tested + $wgDebugTimestamps = false; + + + + wfDebug( "This is a normal string" ); + $this->assertEquals( "This is a normal string", file_get_contents( $wgDebugLogFile ) ); + unlink( $wgDebugLogFile ); + + + wfDebug( "This is nöt an ASCII string" ); + $this->assertEquals( "This is nöt an ASCII string", file_get_contents( $wgDebugLogFile ) ); + unlink( $wgDebugLogFile ); + + + wfDebug( "\00305This has böth UTF and control chars\003" ); + $this->assertEquals( " 05This has böth UTF and control chars ", file_get_contents( $wgDebugLogFile ) ); + unlink( $wgDebugLogFile ); + + + + $old_wgOut = $wgOut; + $old_wgShowDebug = $wgShowDebug; + + $wgOut = new MockOutputPage; + + $wgShowDebug = true; + + $message = "\00305This has böth UTF and control chars\003"; + + wfDebug( $message ); + + if( $wgOut->message == "JAJA is a stupid error message. Anyway, here's your message: $message" ) { + $this->assertTrue( true, 'MockOutputPage called, set the proper message.' ); + } + else { + $this->assertTrue( false, 'MockOutputPage was not called.' ); + } + + $wgOut = $old_wgOut; + $wgShowDebug = $old_wgShowDebug; + unlink( $wgDebugLogFile ); + + + + wfDebugMem(); + $this->assertGreaterThan( 5000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) ); + unlink( $wgDebugLogFile ); + + wfDebugMem(true); + $this->assertGreaterThan( 5000000, preg_replace( '/\D/', '', file_get_contents( $wgDebugLogFile ) ) ); + unlink( $wgDebugLogFile ); + + + + $wgDebugLogFile = $old_log_file; + + } + + function testClientAcceptsGzipTest() { + + $settings = array( + 'gzip' => true, + 'bzip' => false, + '*' => false, + 'compress, gzip' => true, + 'gzip;q=1.0' => true, + 'foozip' => false, + 'foo*zip' => false, + 'gzip;q=abcde' => true, //is this REALLY valid? + 'gzip;q=12345678.9' => true, + ' gzip' => true, + ); + + if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) $old_server_setting = $_SERVER['HTTP_ACCEPT_ENCODING']; + + foreach ( $settings as $encoding => $expect ) { + $_SERVER['HTTP_ACCEPT_ENCODING'] = $encoding; + + $this->assertEquals( $expect, wfClientAcceptsGzip( true ), + "'$encoding' => " . wfBoolToStr( $expect ) ); + } + + if( isset( $old_server_setting ) ) $_SERVER['HTTP_ACCEPT_ENCODING'] = $old_server_setting; + + } + + + + function testSwapVarsTest() { + + + $var1 = 1; + $var2 = 2; + + $this->assertEquals( $var1, 1, 'var1 is set originally' ); + $this->assertEquals( $var2, 2, 'var1 is set originally' ); + + swap( $var1, $var2 ); + + $this->assertEquals( $var1, 2, 'var1 is swapped' ); + $this->assertEquals( $var2, 1, 'var2 is swapped' ); + + } + + + function testWfPercentTest() { + + $pcts = array( + array( 6/7, '0.86%', 2, false ), + array( 3/3, '1%' ), + array( 22/7, '3.14286%', 5 ), + array( 3/6, '0.5%' ), + array( 1/3, '0%', 0 ), + array( 10/3, '0%', -1 ), + array( 3/4/5, '0.1%', 1 ), + array( 6/7*8, '6.8571428571%', 10 ), + ); + + foreach( $pcts as $pct ) { + if( !isset( $pct[2] ) ) $pct[2] = 2; + if( !isset( $pct[3] ) ) $pct[3] = true; + + $this->assertEquals( wfPercent( $pct[0], $pct[2], $pct[3] ), $pct[1], $pct[1] ); + } + + } + + + function testInStringTest() { + + $this->assertTrue( in_string( 'foo', 'foobar' ), 'foo is in foobar' ); + $this->assertFalse( in_string( 'Bar', 'foobar' ), 'Case-sensitive by default' ); + $this->assertTrue( in_string( 'Foo', 'foobar', true ), 'Case-insensitive when asked' ); + + } + + /** + * test @see wfShorthandToInteger() + * @dataProvider provideShorthand + */ + public function testWfShorthandToInteger( $shorthand, $expected ) { + $this->assertEquals( $expected, + wfShorthandToInteger( $shorthand ) + ); + } + + /** array( shorthand, expected integer ) */ + public function provideShorthand() { + return array( + # Null, empty ... + array( '', -1), + array( ' ', -1), + array( null, -1), + + # Failures returns 0 :( + array( 'ABCDEFG', 0 ), + array( 'Ak', 0 ), + + # Int, strings with spaces + array( 1, 1 ), + array( ' 1 ', 1 ), + array( 1023, 1023 ), + array( ' 1023 ', 1023 ), + + # kilo, Mega, Giga + array( '1k', 1024 ), + array( '1K', 1024 ), + array( '1m', 1024 * 1024 ), + array( '1M', 1024 * 1024 ), + array( '1g', 1024 * 1024 * 1024 ), + array( '1G', 1024 * 1024 * 1024 ), + + # Negatives + array( -1, -1 ), + array( -500, -500 ), + array( '-500', -500 ), + array( '-1k', -1024 ), + + # Zeroes + array( '0', 0 ), + array( '0k', 0 ), + array( '0M', 0 ), + array( '0G', 0 ), + array( '-0', 0 ), + array( '-0k', 0 ), + array( '-0M', 0 ), + array( '-0G', 0 ), + ); + } + + + /** + * test @see wfBCP47(). + * Please note the BCP explicitly state that language codes are case + * insensitive, there are some exceptions to the rule :) + * This test is used to verify our formatting against all lower and + * all upper cases language code. + * + * @see http://tools.ietf.org/html/bcp47 + * @dataProvider provideLanguageCodes() + */ + function testBCP47( $code, $expected ) { + $code = strtolower( $code ); + $this->assertEquals( $expected, wfBCP47($code), + "Applying BCP47 standard to lower case '$code'" + ); + + $code = strtoupper( $code ); + $this->assertEquals( $expected, wfBCP47($code), + "Applying BCP47 standard to upper case '$code'" + ); + } + + /** + * Array format is ($code, $expected) + */ + function provideLanguageCodes() { + return array( + // Extracted from BCP47 (list not exhaustive) + # 2.1.1 + array( 'en-ca-x-ca' , 'en-CA-x-ca' ), + array( 'sgn-be-fr' , 'sgn-BE-FR' ), + array( 'az-latn-x-latn', 'az-Latn-x-latn' ), + # 2.2 + array( 'sr-Latn-RS', 'sr-Latn-RS' ), + array( 'az-arab-ir', 'az-Arab-IR' ), + + # 2.2.5 + array( 'sl-nedis' , 'sl-nedis' ), + array( 'de-ch-1996', 'de-CH-1996' ), + + # 2.2.6 + array( + 'en-latn-gb-boont-r-extended-sequence-x-private', + 'en-Latn-GB-boont-r-extended-sequence-x-private' + ), + + // Examples from BCP47 Appendix A + # Simple language subtag: + array( 'DE', 'de' ), + array( 'fR', 'fr' ), + array( 'ja', 'ja' ), + + # Language subtag plus script subtag: + array( 'zh-hans', 'zh-Hans'), + array( 'sr-cyrl', 'sr-Cyrl'), + array( 'sr-latn', 'sr-Latn'), + + # Extended language subtags and their primary language subtag + # counterparts: + array( 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ), + array( 'cmn-hans-cn' , 'cmn-Hans-CN' ), + array( 'zh-yue-hk' , 'zh-yue-HK' ), + array( 'yue-hk' , 'yue-HK' ), + + # Language-Script-Region: + array( 'zh-hans-cn', 'zh-Hans-CN' ), + array( 'sr-latn-RS', 'sr-Latn-RS' ), + + # Language-Variant: + array( 'sl-rozaj' , 'sl-rozaj' ), + array( 'sl-rozaj-biske', 'sl-rozaj-biske' ), + array( 'sl-nedis' , 'sl-nedis' ), + + # Language-Region-Variant: + array( 'de-ch-1901' , 'de-CH-1901' ), + array( 'sl-it-nedis' , 'sl-IT-nedis' ), + + # Language-Script-Region-Variant: + array( 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ), + + # Language-Region: + array( 'de-de' , 'de-DE' ), + array( 'en-us' , 'en-US' ), + array( 'es-419', 'es-419'), + + # Private use subtags: + array( 'de-ch-x-phonebk' , 'de-CH-x-phonebk' ), + array( 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ), + /** + * Previous test does not reflect the BCP which states: + * az-Arab-x-AZE-derbend + * AZE being private, it should be lower case, hence the test above + * should probably be: + #array( 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ), + */ + + # Private use registry values: + array( 'x-whatever', 'x-whatever' ), + array( 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ), + array( 'de-qaaa' , 'de-Qaaa' ), + array( 'sr-latn-qm', 'sr-Latn-QM' ), + array( 'sr-qaaa-rs', 'sr-Qaaa-RS' ), + + # Tags that use extensions + array( 'en-us-u-islamcal', 'en-US-u-islamcal' ), + array( 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ), + array( 'en-a-myext-b-another', 'en-a-myext-b-another' ), + + # Invalid: + // de-419-DE + // a-DE + // ar-a-aaa-b-bbb-a-ccc + + /* + // ISO 15924 : + array( 'sr-Cyrl', 'sr-Cyrl' ), + # @todo FIXME: Fix our function? + array( 'SR-lATN', 'sr-Latn' ), + array( 'fr-latn', 'fr-Latn' ), + // Use lowercase for single segment + // ISO 3166-1-alpha-2 code + array( 'US', 'us' ), # USA + array( 'uS', 'us' ), # USA + array( 'Fr', 'fr' ), # France + array( 'va', 'va' ), # Holy See (Vatican City State) + */); + } + + /** + * @dataProvider provideMakeUrlIndexes() + */ + function testMakeUrlIndexes( $url, $expected ) { + $index = wfMakeUrlIndexes( $url ); + $this->assertEquals( $expected, $index, "wfMakeUrlIndexes(\"$url\")" ); + } + + function provideMakeUrlIndexes() { + return array( + array( + // just a regular :) + 'https://bugzilla.wikimedia.org/show_bug.cgi?id=28627', + array( 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627' ) + ), + array( + // mailtos are handled special + // is this really right though? that final . probably belongs earlier? + 'mailto:wiki@wikimedia.org', + array( 'mailto:org.wikimedia@wiki.' ) + ), + + // file URL cases per bug 28627... + array( + // three slashes: local filesystem path Unix-style + 'file:///whatever/you/like.txt', + array( 'file://./whatever/you/like.txt' ) + ), + array( + // three slashes: local filesystem path Windows-style + 'file:///c:/whatever/you/like.txt', + array( 'file://./c:/whatever/you/like.txt' ) + ), + array( + // two slashes: UNC filesystem path Windows-style + 'file://intranet/whatever/you/like.txt', + array( 'file://intranet./whatever/you/like.txt' ) + ), + // Multiple-slash cases that can sorta work on Mozilla + // if you hack it just right are kinda pathological, + // and unreliable cross-platform or on IE which means they're + // unlikely to appear on intranets. + // + // Those will survive the algorithm but with results that + // are less consistent. + + // protocol-relative URL cases per bug 29854... + array( + '//bugzilla.wikimedia.org/show_bug.cgi?id=28627', + array( + 'http://org.wikimedia.bugzilla./show_bug.cgi?id=28627', + 'https://org.wikimedia.bugzilla./show_bug.cgi?id=28627' + ) + ), + ); + } + + /* TODO: many more! */ +} + + +class MockOutputPage { + + public $message; + + function debug( $message ) { + $this->message = "JAJA is a stupid error message. Anyway, here's your message: $message"; + } +} + diff --git a/tests/phpunit/includes/GlobalFunctions/README b/tests/phpunit/includes/GlobalFunctions/README new file mode 100644 index 00000000..0042bdac --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/README @@ -0,0 +1,2 @@ +This directory hold tests for includes/GlobalFunctions.php file +which is a pile of functions. diff --git a/tests/phpunit/includes/GlobalFunctions/wfExpandUrl.php b/tests/phpunit/includes/GlobalFunctions/wfExpandUrl.php new file mode 100644 index 00000000..b388b266 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfExpandUrl.php @@ -0,0 +1,78 @@ +<?php +/* + * Unit tests for wfExpandUrl() + */ + +class wfExpandUrl extends MediaWikiTestCase { + /** @dataProvider provideExpandableUrls */ + public function testWfExpandUrl( $fullUrl, $shortUrl, $defaultProto, $server, $canServer, $httpsMode, $message ) { + // Fake $wgServer and $wgCanonicalServer + global $wgServer, $wgCanonicalServer; + $oldServer = $wgServer; + $oldCanServer = $wgCanonicalServer; + $wgServer = $server; + $wgCanonicalServer = $canServer; + + // Fake $_SERVER['HTTPS'] if needed + if ( $httpsMode ) { + $_SERVER['HTTPS'] = 'on'; + } else { + unset( $_SERVER['HTTPS'] ); + } + + $this->assertEquals( $fullUrl, wfExpandUrl( $shortUrl, $defaultProto ), $message ); + + // Restore $wgServer and $wgCanonicalServer + $wgServer = $oldServer; + $wgCanonicalServer = $oldCanServer; + } + + /** + * Provider of URL examples for testing wfExpandUrl() + */ + public function provideExpandableUrls() { + $modes = array( 'http', 'https' ); + $servers = array( 'http' => 'http://example.com', 'https' => 'https://example.com', 'protocol-relative' => '//example.com' ); + $defaultProtos = array( 'http' => PROTO_HTTP, 'https' => PROTO_HTTPS, 'protocol-relative' => PROTO_RELATIVE, 'current' => PROTO_CURRENT, 'canonical' => PROTO_CANONICAL ); + + $retval = array(); + foreach ( $modes as $mode ) { + $httpsMode = $mode == 'https'; + foreach ( $servers as $serverDesc => $server ) { + foreach ( $modes as $canServerMode ) { + $canServer = "$canServerMode://example2.com"; + foreach ( $defaultProtos as $protoDesc => $defaultProto ) { + $retval[] = array( 'http://example.com', 'http://example.com', $defaultProto, $server, $canServer, $httpsMode, "Testing fully qualified http URLs (no need to expand) (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" ); + $retval[] = array( 'https://example.com', 'https://example.com', $defaultProto, $server, $canServer, $httpsMode, "Testing fully qualified https URLs (no need to expand) (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" ); + # Would be nice to support this, see fixme on wfExpandUrl() + $retval[] = array( "wiki/FooBar", 'wiki/FooBar', $defaultProto, $server, $canServer, $httpsMode, "Test non-expandable relative URLs (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" ); + + // Determine expected protocol + $p = $protoDesc . ':'; // default case + if ( $protoDesc == 'protocol-relative' ) { + $p = ''; + } else if ( $protoDesc == 'current' ) { + $p = "$mode:"; + } else if ( $protoDesc == 'canonical' ) { + $p = "$canServerMode:"; + } else { + $p = $protoDesc . ':'; + } + // Determine expected server name + if ( $protoDesc == 'canonical' ) { + $srv = $canServer; + } else if ( $serverDesc == 'protocol-relative' ) { + $srv = $p . $server; + } else { + $srv = $server; + } + + $retval[] = array( "$p//wikipedia.org", '//wikipedia.org', $defaultProto, $server, $canServer, $httpsMode, "Test protocol-relative URL (defaultProto: $protoDesc, wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" ); + $retval[] = array( "$srv/wiki/FooBar", '/wiki/FooBar', $defaultProto, $server, $canServer, $httpsMode, "Testing expanding URL beginning with / (defaultProto: $protoDesc , wgServer: $server, wgCanonicalServer: $canServer, current request protocol: $mode )" ); + } + } + } + } + return $retval; + } +} diff --git a/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php new file mode 100644 index 00000000..cd1a8dbd --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfUrlencodeTest.php @@ -0,0 +1,120 @@ +<?php +/** + * Tests for includes/GlobalFunctions.php -> wfUrlencode() + * + * The function only need a string parameter and might react to IIS7.0 + */ + +class wfUrlencodeTest extends MediaWikiTestCase { + + #### TESTS ############################################################## + + /** @dataProvider provideURLS */ + public function testEncodingUrlWith( $input, $expected ) { + $this->verifyEncodingFor( 'Apache', $input, $expected ); + } + + /** @dataProvider provideURLS */ + public function testEncodingUrlWithMicrosoftIis7( $input, $expected ) { + $this->verifyEncodingFor( 'Microsoft-IIS/7', $input, $expected ); + } + + #### HELPERS ############################################################# + + /** + * Internal helper that actually run the test. + * Called by the public methods testEncodingUrlWith...() + * + */ + private function verifyEncodingFor( $server, $input, $expectations ) { + $expected = $this->extractExpect( $server, $expectations ); + + // save up global + $old = isset($_SERVER['SERVER_SOFTWARE']) + ? $_SERVER['SERVER_SOFTWARE'] + : null + ; + $_SERVER['SERVER_SOFTWARE'] = $server; + wfUrlencode( null ); + + // do the requested test + $this->assertEquals( + $expected, + wfUrlencode( $input ), + "Encoding '$input' for server '$server' should be '$expected'" + ); + + // restore global + if( $old === null ) { + unset( $_SERVER['SERVER_SOFTWARE'] ); + } else { + $_SERVER['SERVER_SOFTWARE'] = $old; + } + wfUrlencode( null ); + } + + /** + * Interprets the provider array. Return expected value depending + * the HTTP server name. + */ + private function extractExpect( $server, $expectations ) { + if( is_string( $expectations ) ) { + return $expectations; + } elseif( is_array( $expectations ) ) { + if( !array_key_exists( $server, $expectations ) ) { + throw new MWException( __METHOD__ . " expectation does not have any value for server name $server. Check the provider array.\n" ); + } else { + return $expectations[$server]; + } + } else { + throw new MWException( __METHOD__ . " given invalid expectation for '$server'. Should be a string or an array( <http server name> => <string> ).\n" ); + } + } + + + #### PROVIDERS ########################################################### + + /** + * Format is either: + * array( 'input', 'expected' ); + * Or: + * array( 'input', + * array( 'Apache', 'expected' ), + * array( 'Microsoft-IIS/7', 'expected' ), + * ), + * If you want to add other HTTP server name, you will have to add a new + * testing method much like the testEncodingUrlWith() method above. + */ + public function provideURLS() { + return array( + ### RFC 1738 chars + // + is not safe + array( '+', '%2B' ), + // & and = not safe in queries + array( '&', '%26' ), + array( '=', '%3D' ), + + array( ':', array( + 'Apache' => ':', + 'Microsoft-IIS/7' => '%3A', + ) ), + + // remaining chars do not need encoding + array( + ';@$-_.!*', + ';@$-_.!*', + ), + + ### Other tests + // slash remain unchanged. %2F seems to break things + array( '/', '/' ), + + // Other 'funnies' chars + array( '[]', '%5B%5D' ), + array( '<>', '%3C%3E' ), + + // Apostrophe is encoded + array( '\'', '%27' ), + ); + } +} diff --git a/tests/phpunit/includes/HooksTest.php b/tests/phpunit/includes/HooksTest.php new file mode 100644 index 00000000..2f9d9f8d --- /dev/null +++ b/tests/phpunit/includes/HooksTest.php @@ -0,0 +1,102 @@ +<?php + +class HooksTest extends MediaWikiTestCase { + + public function testOldStyleHooks() { + $foo = 'Foo'; + $bar = 'Bar'; + + $i = new NothingClass(); + + global $wgHooks; + + $wgHooks['MediaWikiHooksTest001'][] = array( $i, 'someNonStatic' ); + + wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'fOO', $foo, 'Standard method' ); + $foo = 'Foo'; + + $wgHooks['MediaWikiHooksTest001'][] = $i; + + wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'foo', $foo, 'onEventName style' ); + $foo = 'Foo'; + + $wgHooks['MediaWikiHooksTest001'][] = array( $i, 'someNonStaticWithData', 'baz' ); + + wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'baz', $foo, 'Data included' ); + $foo = 'Foo'; + + $wgHooks['MediaWikiHooksTest001'][] = array( $i, 'someStatic' ); + + wfRunHooks( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'bah', $foo, 'Standard static method' ); + //$foo = 'Foo'; + + unset( $wgHooks['MediaWikiHooksTest001'] ); + + } + + public function testNewStyleHooks() { + $foo = 'Foo'; + $bar = 'Bar'; + + $i = new NothingClass(); + + Hooks::register( 'MediaWikiHooksTest001', array( $i, 'someNonStatic' ) ); + + Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'fOO', $foo, 'Standard method' ); + $foo = 'Foo'; + + Hooks::register( 'MediaWikiHooksTest001', $i ); + + Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'foo', $foo, 'onEventName style' ); + $foo = 'Foo'; + + Hooks::register( 'MediaWikiHooksTest001', array( $i, 'someNonStaticWithData', 'baz' ) ); + + Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'baz', $foo, 'Data included' ); + $foo = 'Foo'; + + Hooks::register( 'MediaWikiHooksTest001', array( $i, 'someStatic' ) ); + + Hooks::run( 'MediaWikiHooksTest001', array( &$foo, &$bar ) ); + + $this->assertEquals( 'bah', $foo, 'Standard static method' ); + $foo = 'Foo'; + } +} + +class NothingClass { + static public function someStatic( &$foo, &$bar ) { + $foo = 'bah'; + return true; + } + + public function someNonStatic( &$foo, &$bar ) { + $foo = 'fOO'; + $bar = 'bAR'; + return true; + } + + public function onMediaWikiHooksTest001( &$foo, &$bar ) { + $foo = 'foo'; + return true; + } + + public function someNonStaticWithData( $foo, &$bar ) { + $bar = $foo; + return true; + } +} diff --git a/tests/phpunit/includes/HtmlTest.php b/tests/phpunit/includes/HtmlTest.php new file mode 100644 index 00000000..96bb1803 --- /dev/null +++ b/tests/phpunit/includes/HtmlTest.php @@ -0,0 +1,90 @@ +<?php +/** tests for includes/Html.php */ + +class HtmlTest extends MediaWikiTestCase { + private static $oldLang; + + public function setUp() { + global $wgLang, $wgLanguageCode; + + self::$oldLang = $wgLang; + $wgLanguageCode = 'en'; + $wgLang = Language::factory( $wgLanguageCode ); + } + + public function tearDown() { + global $wgLang, $wgLanguageCode; + $wgLang = self::$oldLang; + $wgLanguageCode = $wgLang->getCode(); + } + + public function testExpandAttributesSkipsNullAndFalse() { + + ### EMPTY ######## + $this->AssertEmpty( + Html::expandAttributes( array( 'foo'=>null) ), + 'skip keys with null value' + ); + $this->AssertEmpty( + Html::expandAttributes( array( 'foo'=>false) ), + 'skip keys with false value' + ); + $this->AssertNotEmpty( + Html::expandAttributes( array( 'foo'=>'') ), + 'keep keys with an empty string' + ); + } + + public function testExpandAttributesForBooleans() { + $this->AssertEquals( + '', + Html::expandAttributes( array( 'selected'=>false) ), + 'Boolean attributes do not generates output when value is false' + ); + $this->AssertEquals( + '', + Html::expandAttributes( array( 'selected'=>null) ), + 'Boolean attributes do not generates output when value is null' + ); + + ### FIXME: maybe they should just output 'selected' + $this->AssertEquals( + ' selected=""', + Html::expandAttributes( array( 'selected'=>true ) ), + 'Boolean attributes skip value output' + ); + $this->AssertEquals( + ' selected=""', + Html::expandAttributes( array( 'selected' ) ), + 'Boolean attributes (ex: selected) do not need a value' + ); + } + + /** + * Test for Html::expandAttributes() + * Please note it output a string prefixed with a space! + */ + public function testExpandAttributesVariousExpansions() { + ### NOT EMPTY #### + $this->AssertEquals( + ' empty_string=""', + Html::expandAttributes( array( 'empty_string'=>'') ), + 'Value with an empty string' + ); + $this->AssertEquals( + ' key="value"', + Html::expandAttributes( array( 'key'=>'value') ), + 'Value is a string' + ); + $this->AssertEquals( + ' one="1"', + Html::expandAttributes( array( 'one'=>1) ), + 'Value is a numeric one' + ); + $this->AssertEquals( + ' zero="0"', + Html::expandAttributes( array( 'zero'=>0) ), + 'Value is a numeric zero' + ); + } +} diff --git a/tests/phpunit/includes/HttpTest.php b/tests/phpunit/includes/HttpTest.php new file mode 100644 index 00000000..1a99af7d --- /dev/null +++ b/tests/phpunit/includes/HttpTest.php @@ -0,0 +1,618 @@ +<?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 MediaWikiTestCase { + static $content; + static $headers; + static $has_curl; + static $has_fopen; + static $has_proxy = false; + static $proxy = "http://hulk:8080/"; + var $test_geturl = array( + "http://en.wikipedia.org/robots.txt", + "https://secure.wikimedia.org/", + "http://pecl.php.net/feeds/pkg_apc.rss", + "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.mediawiki.org/xml/made-up-url", $timeout ); + $this->assertFalse( $r, "False on 404s" ); + + + $r = MWHttpRequest::factory( "http://www.mediawiki.org/xml/made-up-url" ); + $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(); + } + + 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() { + } + + /** + * @dataProvider cookieDomains + */ + function testValidateCookieDomain( $expected, $domain, $origin=null ) { + if ( $origin ) { + $ok = Cookie::validateCookieDomain( $domain, $origin ); + $msg = "$domain against origin $origin"; + } else { + $ok = Cookie::validateCookieDomain( $domain ); + $msg = "$domain"; + } + $this->assertEquals( $expected, $ok, $msg ); + } + + function cookieDomains() { + return array( + array( false, "org"), + array( false, ".org"), + array( true, "wikipedia.org"), + array( true, ".wikipedia.org"), + array( false, "co.uk" ), + array( false, ".co.uk" ), + array( false, "gov.uk" ), + array( false, ".gov.uk" ), + array( true, "supermarket.uk" ), + array( false, "uk" ), + array( false, ".uk" ), + array( false, "127.0.0." ), + array( false, "127." ), + array( false, "127.0.0.1." ), + array( true, "127.0.0.1" ), + array( false, "333.0.0.1" ), + array( true, "example.com" ), + array( false, "example.com." ), + array( true, ".example.com" ), + + array( true, ".example.com", "www.example.com" ), + array( false, "example.com", "www.example.com" ), + array( true, "127.0.0.1", "127.0.0.1" ), + array( false, "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' ) ); + + $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(); + } + + /** + * Test Http::isValidURI() + * @bug 27854 : Http::isValidURI is to lax + *@dataProvider provideURI */ + function testIsValidUri( $expect, $URI, $message = '' ) { + $this->assertEquals( + $expect, + (bool) Http::isValidURI( $URI ), + $message + ); + } + + /** + * Feeds URI to test a long regular expression in Http::isValidURI + */ + function provideURI() { + /** Format: 'boolean expectation', 'URI to test', 'Optional message' */ + return array( + array( false, '¿non sens before!! http://a', 'Allow anything before URI' ), + + # (http|https) - only two schemes allowed + array( true, 'http://www.example.org/' ), + array( true, 'https://www.example.org/' ), + array( true, 'http://www.example.org', 'URI without directory' ), + array( true, 'http://a', 'Short name' ), + array( true, 'http://étoile', 'Allow UTF-8 in hostname' ), # 'étoile' is french for 'star' + array( false, '\\host\directory', 'CIFS share' ), + array( false, 'gopher://host/dir', 'Reject gopher scheme' ), + array( false, 'telnet://host', 'Reject telnet scheme' ), + + # :\/\/ - double slashes + array( false, 'http//example.org', 'Reject missing colon in protocol' ), + array( false, 'http:/example.org', 'Reject missing slash in protocol' ), + array( false, 'http:example.org', 'Must have two slashes' ), + # Following fail since hostname can be made of anything + array( false, 'http:///example.org', 'Must have exactly two slashes, not three' ), + + # (\w+:{0,1}\w*@)? - optional user:pass + array( true, 'http://user@host', 'Username provided' ), + array( true, 'http://user:@host', 'Username provided, no password' ), + array( true, 'http://user:pass@host', 'Username and password provided' ), + + # (\S+) - host part is made of anything not whitespaces + array( false, 'http://!"èèè¿¿¿~~\'', 'hostname is made of any non whitespace' ), + array( false, 'http://exam:ple.org/', 'hostname can not use colons!' ), + + # (:[0-9]+)? - port number + array( true, 'http://example.org:80/' ), + array( true, 'https://example.org:80/' ), + array( true, 'http://example.org:443/' ), + array( true, 'https://example.org:443/' ), + + # Part after the hostname is / or / with something else + array( true, 'http://example/#' ), + array( true, 'http://example/!' ), + array( true, 'http://example/:' ), + array( true, 'http://example/.' ), + array( true, 'http://example/?' ), + array( true, 'http://example/+' ), + array( true, 'http://example/=' ), + array( true, 'http://example/&' ), + array( true, 'http://example/%' ), + array( true, 'http://example/@' ), + array( true, 'http://example/-' ), + array( true, 'http://example//' ), + array( true, 'http://example/&' ), + + # Fragment + array( true, 'http://exam#ple.org', ), # This one is valid, really! + array( true, 'http://example.org:80#anchor' ), + array( true, 'http://example.org/?id#anchor' ), + array( true, 'http://example.org/?#anchor' ), + + array( false, 'http://a ¿non !!sens after', 'Allow anything after URI' ), + ); + } + +} diff --git a/tests/phpunit/includes/IPTest.php b/tests/phpunit/includes/IPTest.php new file mode 100644 index 00000000..c77dd852 --- /dev/null +++ b/tests/phpunit/includes/IPTest.php @@ -0,0 +1,508 @@ +<?php +/* + * Tests for IP validity functions. Ported from /t/inc/IP.t by avar. + */ + +class IPTest extends MediaWikiTestCase { + /** + * not sure it should be tested with boolean false. hashar 20100924 + * @covers IP::isIPAddress + */ + 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" ); + } + } + + /** + * @covers IP::isIPv6 + */ + 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->assertTrue( IP::isIPv6( '2001::df'), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df'), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isIPv6( '2001:5c0:1400:a::df:2'), 'IPv6 with "::" and 6 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' ) ); + } + + /** + * @covers IP::isIPv4 + */ + 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' ) ); + } + + /** + * @covers IP::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, 0xF ) 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" ); + } + } + // test with some abbreviations + $this->assertFalse( IP::isValid( ':fc:100::' ), 'IPv6 starting with lone ":"' ); + $this->assertFalse( IP::isValid( 'fc:100:::' ), 'IPv6 ending with a ":::"' ); + $this->assertFalse( IP::isValid( 'fc:300' ), 'IPv6 with only 2 words' ); + $this->assertFalse( IP::isValid( 'fc:100:300' ), 'IPv6 with only 3 words' ); + + $this->assertTrue( IP::isValid( 'fc:100::' ) ); + $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e::' ) ); + $this->assertTrue( IP::isValid( 'fc:100:a:d:1:e:ac::' ) ); + + $this->assertTrue( IP::isValid( 'fc::100' ), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a' ), 'IPv6 with "::" and 3 words' ); + $this->assertTrue( IP::isValid( '2001::df'), 'IPv6 with "::" and 2 words' ); + $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df'), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isValid( '2001:5c0:1400:a::df:2'), 'IPv6 with "::" and 6 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a:d:1' ), 'IPv6 with "::" and 5 words' ); + $this->assertTrue( IP::isValid( 'fc::100:a:d:1:e:ac' ), 'IPv6 with "::" and 7 words' ); + + $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0::' ), 'IPv6 with 8 words ending with "::"' ); + $this->assertFalse( IP::isValid( 'fc:100:a:d:1:e:ac:0:1::' ), 'IPv6 with 9 words ending with "::"' ); + } + + /** + * @covers IP::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" ); + } + } + + /** + * @covers IP::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" ); + } + } + + /** + * @covers IP::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" ); + } + } + + /** + * Improve IP::sanitizeIP() code coverage + * @todo Most probably incomplete + */ + public function testSanitizeIP() { + $this->assertNull( IP::sanitizeIP('') ); + $this->assertNull( IP::sanitizeIP(' ') ); + } + + /** + * test wrapper around ip2long which might return -1 or false depending on PHP version + * @covers IP::toUnsigned + */ + public function testip2longWrapper() { + // @todo 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 ) ); + } + + /** + * @covers IP::isPublic + */ + public function testPrivateIPs() { + $private = array( 'fc00::3', 'fc00::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" ); + } + $public = array( '2001:5c0:1000:a::133', 'fc::3' ); + foreach ( $public as $p ) { + $this->assertTrue( IP::isPublic( $p ), "$p is 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" ); + } + + /** + * @covers IP::hexToQuad + */ + 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' ) ); + } + + /** + * @covers IP::hexToOctet + */ + 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. + * @covers IP::parseCIDR + */ + 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') ); + + // @todo 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' ); + } + + + /** + * @covers IP::canonicalize + */ + public function testIPCanonicalizeOnValidIp() { + $this->assertEquals( '192.0.2.152', IP::canonicalize( '192.0.2.152' ), + 'Canonicalization of a valid IP returns it unchanged' ); + } + + /** + * @covers IP::canonicalize + */ + public function testIPCanonicalizeMappedAddress() { + $this->assertEquals( + '192.0.2.152', + IP::canonicalize( '::ffff:192.0.2.152' ) + ); + $this->assertEquals( + '192.0.2.152', + IP::canonicalize( '::192.0.2.152' ) + ); + } + + /** + * Issues there are most probably from IP::toHex() or IP::parseRange() + * @covers IP::isInRange + * @dataProvider provideIPsAndRanges + */ + public function testIPIsInRange( $expected, $addr, $range, $message = '' ) { + $this->assertEquals( + $expected, + IP::isInRange( $addr, $range ), + $message + ); + } + + /** Provider for testIPIsInRange() */ + function provideIPsAndRanges() { + # Format: (expected boolean, address, range, optional message) + return array( + # IPv4 + array( true , '192.0.2.0' , '192.0.2.0/24', 'Network address' ), + array( true , '192.0.2.77' , '192.0.2.0/24', 'Simple address' ), + array( true , '192.0.2.255' , '192.0.2.0/24', 'Broadcast address' ), + + array( false, '0.0.0.0' , '192.0.2.0/24' ), + array( false, '255.255.255' , '192.0.2.0/24' ), + + # IPv6 + array( false, '::1' , '2001:DB8::/32' ), + array( false, '::' , '2001:DB8::/32' ), + array( false, 'FE80::1', '2001:DB8::/32' ), + + array( true , '2001:DB8::' , '2001:DB8::/32' ), + array( true , '2001:0DB8::' , '2001:DB8::/32' ), + array( true , '2001:DB8::1' , '2001:DB8::/32' ), + array( true , '2001:0DB8::1', '2001:DB8::/32' ), + array( true , '2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF', + '2001:DB8::/32' ), + + array( false, '2001:0DB8:F::', '2001:DB8::/96' ), + ); + } + + /** + * Test for IP::splitHostAndPort(). + * @dataProvider provideSplitHostAndPort + */ + function testSplitHostAndPort( $expected, $input, $description ) { + $this->assertEquals( $expected, IP::splitHostAndPort( $input ), $description ); + } + + /** + * Provider for IP::splitHostAndPort() + */ + function provideSplitHostAndPort() { + return array( + array( false, '[', 'Unclosed square bracket' ), + array( false, '[::', 'Unclosed square bracket 2' ), + array( array( '::', false ), '::', 'Bare IPv6 0' ), + array( array( '::1', false ), '::1', 'Bare IPv6 1' ), + array( array( '::', false ), '[::]', 'Bracketed IPv6 0' ), + array( array( '::1', false ), '[::1]', 'Bracketed IPv6 1' ), + array( array( '::1', 80 ), '[::1]:80', 'Bracketed IPv6 with port' ), + array( false, '::x', 'Double colon but no IPv6' ), + array( array( 'x', 80 ), 'x:80', 'Hostname and port' ), + array( false, 'x:x', 'Hostname and invalid port' ), + array( array( 'x', false ), 'x', 'Plain hostname' ) + ); + } + + /** + * Test for IP::combineHostAndPort() + * @dataProvider provideCombineHostAndPort + */ + function testCombineHostAndPort( $expected, $input, $description ) { + list( $host, $port, $defaultPort ) = $input; + $this->assertEquals( + $expected, + IP::combineHostAndPort( $host, $port, $defaultPort ), + $description ); + } + + /** + * Provider for IP::combineHostAndPort() + */ + function provideCombineHostAndPort() { + return array( + array( '[::1]', array( '::1', 2, 2 ), 'IPv6 default port' ), + array( '[::1]:2', array( '::1', 2, 3 ), 'IPv6 non-default port' ), + array( 'x', array( 'x', 2, 2 ), 'Normal default port' ), + array( 'x:2', array( 'x', 2, 3 ), 'Normal non-default port' ), + ); + } + + /** + * Test for IP::sanitizeRange() + * @dataProvider provideIPCIDRs + */ + function testSanitizeRange( $input, $expected, $description ) { + $this->assertEquals( $expected, IP::sanitizeRange( $input ), $description ); + } + + /** + * Provider for IP::testSanitizeRange() + */ + function provideIPCIDRs() { + return array( + array( '35.56.31.252/16', '35.56.0.0/16', 'IPv4 range' ), + array( '135.16.21.252/24', '135.16.21.0/24', 'IPv4 range' ), + array( '5.36.71.252/32', '5.36.71.252/32', 'IPv4 silly range' ), + array( '5.36.71.252', '5.36.71.252', 'IPv4 non-range' ), + array( '0:1:2:3:4:c5:f6:7/96', '0:1:2:3:4:C5:0:0/96', 'IPv6 range' ), + array( '0:1:2:3:4:5:6:7/120', '0:1:2:3:4:5:6:0/120', 'IPv6 range' ), + array( '0:e1:2:3:4:5:e6:7/128', '0:E1:2:3:4:5:E6:7/128', 'IPv6 silly range' ), + array( '0:c1:A2:3:4:5:c6:7', '0:C1:A2:3:4:5:C6:7', 'IPv6 non range' ), + ); + } +} diff --git a/tests/phpunit/includes/ImageFunctionsTest.php b/tests/phpunit/includes/ImageFunctionsTest.php new file mode 100644 index 00000000..cb7e67f3 --- /dev/null +++ b/tests/phpunit/includes/ImageFunctionsTest.php @@ -0,0 +1,48 @@ +<?php + +class ImageFunctionsTest extends MediaWikiTestCase { + 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" ); + } + } + } +} + + diff --git a/tests/phpunit/includes/JsonTest.php b/tests/phpunit/includes/JsonTest.php new file mode 100644 index 00000000..75dd18d5 --- /dev/null +++ b/tests/phpunit/includes/JsonTest.php @@ -0,0 +1,33 @@ +<?php + +class JsonTest extends MediaWikiTestCase { + + function testPhpBug46944Test() { + + $this->assertNotEquals( + '\ud840\udc00', + strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ), + 'Test encoding an broken json_encode character (U+20000)' + ); + + + } + + function testDecodeVarTypes() { + + $this->assertInternalType( + 'object', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ), + 'Default to object' + ); + + $this->assertInternalType( + 'array', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ), + 'Optional array' + ); + + } + +} + diff --git a/tests/phpunit/includes/LanguageConverterTest.php b/tests/phpunit/includes/LanguageConverterTest.php new file mode 100644 index 00000000..baf28b07 --- /dev/null +++ b/tests/phpunit/includes/LanguageConverterTest.php @@ -0,0 +1,130 @@ +<?php + +class LanguageConverterTest extends MediaWikiLangTestCase { + protected $lang = null; + protected $lc = null; + + function setUp() { + parent::setUp(); + global $wgMemc, $wgRequest, $wgUser, $wgContLang; + + $wgUser = new User; + $wgRequest = new FauxRequest( array() ); + $wgMemc = new EmptyBagOStuff; + $wgContLang = Language::factory( 'tg' ); + $this->lang = new LanguageToTest(); + $this->lc = new TestConverter( $this->lang, 'tg', + array( 'tg', 'tg-latn' ) ); + } + + function tearDown() { + global $wgMemc; + unset( $wgMemc ); + unset( $this->lc ); + unset( $this->lang ); + parent::tearDown(); + } + + 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->load(); // from 'defaults' + $wgUser->mId = 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->mFrom = 'defaults'; + $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 ); + } +} diff --git a/tests/phpunit/includes/LicensesTest.php b/tests/phpunit/includes/LicensesTest.php new file mode 100644 index 00000000..e467f3cd --- /dev/null +++ b/tests/phpunit/includes/LicensesTest.php @@ -0,0 +1,22 @@ +<?php + +class LicensesTest extends MediaWikiTestCase { + + function testLicenses() { + $str = " +* Free licenses: +** GFDL|Debian disagrees +"; + + $lc = new Licenses( array( + 'fieldname' => 'FooField', + 'type' => 'select', + 'section' => 'description', + 'id' => 'wpLicense', + 'label' => 'A label text', # Note can't test label-message because $wgOut is not defined + 'name' => 'AnotherName', + 'licenses' => $str, + ) ); + $this->assertThat( $lc, $this->isInstanceOf( 'Licenses' ) ); + } +} diff --git a/tests/phpunit/includes/LocalFileTest.php b/tests/phpunit/includes/LocalFileTest.php new file mode 100644 index 00000000..e08d4d7e --- /dev/null +++ b/tests/phpunit/includes/LocalFileTest.php @@ -0,0 +1,99 @@ +<?php + +/** + * These tests should work regardless of $wgCapitalLinks + * @group Database + */ + +class LocalFileTest extends MediaWikiTestCase { + function setUp() { + global $wgCapitalLinks; + + $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' ); + } +} + + diff --git a/tests/phpunit/includes/MWFunctionTest.php b/tests/phpunit/includes/MWFunctionTest.php new file mode 100644 index 00000000..ed5e7602 --- /dev/null +++ b/tests/phpunit/includes/MWFunctionTest.php @@ -0,0 +1,86 @@ +<?php + +class MWFunctionTest extends MediaWikiTestCase { + + function testCallUserFuncWorkarounds() { + + $this->assertEquals( + call_user_func( array( 'MWFunctionTest', 'someMethod' ) ), + MWFunction::call( 'MWFunctionTest::someMethod' ) + ); + $this->assertEquals( + call_user_func( array( 'MWFunctionTest', 'someMethod' ), 'foo', 'bar', 'baz' ), + MWFunction::call( 'MWFunctionTest::someMethod', 'foo', 'bar', 'baz' ) + ); + + + + $this->assertEquals( + call_user_func_array( array( 'MWFunctionTest', 'someMethod' ), array() ), + MWFunction::callArray( 'MWFunctionTest::someMethod', array() ) + ); + $this->assertEquals( + call_user_func_array( array( 'MWFunctionTest', 'someMethod' ), array( 'foo', 'bar', 'baz' ) ), + MWFunction::callArray( 'MWFunctionTest::someMethod', array( 'foo', 'bar', 'baz' ) ) + ); + + } + + function testNewObjFunction() { + + $arg1 = 'Foo'; + $arg2 = 'Bar'; + $arg3 = array( 'Baz' ); + $arg4 = new ExampleObject; + + $args = array( $arg1, $arg2, $arg3, $arg4 ); + + $newObject = new MWBlankClass( $arg1, $arg2, $arg3, $arg4 ); + + $this->assertEquals( + MWFunction::newObj( 'MWBlankClass', $args )->args, + $newObject->args + ); + + $this->assertEquals( + MWFunction::newObj( 'MWBlankClass', $args, true )->args, + $newObject->args, + 'Works even with PHP version < 5.1.3' + ); + + } + + /** + * @expectedException MWException + */ + function testCallingParentFails() { + + MWFunction::call( 'parent::foo' ); + } + + /** + * @expectedException MWException + */ + function testCallingSelfFails() { + + MWFunction::call( 'self::foo' ); + } + + public static function someMethod() { + return func_get_args(); + } + +} + +class MWBlankClass { + + public $args = array(); + + function __construct( $arg1, $arg2, $arg3, $arg4 ) { + $this->args = array( $arg1, $arg2, $arg3, $arg4 ); + } + +} + +class ExampleObject { +} diff --git a/tests/phpunit/includes/MWNamespaceTest.php b/tests/phpunit/includes/MWNamespaceTest.php new file mode 100644 index 00000000..462afc24 --- /dev/null +++ b/tests/phpunit/includes/MWNamespaceTest.php @@ -0,0 +1,460 @@ +<?php +/** + * @author Ashar Voultoiz + * @copyright Copyright © 2011, Ashar Voultoiz + * @file + */ + +/** + * Test class for MWNamespace. + * Generated by PHPUnit on 2011-02-20 at 21:01:55. + * + */ +class MWNamespaceTest extends MediaWikiTestCase { + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + protected function setUp() { + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + protected function tearDown() { + } + + +#### START OF TESTS ######################################################### + + /** + * @todo Write more texts, handle $wgAllowImageMoving setting + */ + public function testIsMovable() { + $this->assertFalse( MWNamespace::isMovable( NS_CATEGORY ) ); + # @todo FIXME: Write more tests!! + } + + /** + * Please make sure to change testIsTalk() if you change the assertions below + */ + public function testIsMain() { + // Special namespaces + $this->assertTrue( MWNamespace::isMain( NS_MEDIA ) ); + $this->assertTrue( MWNamespace::isMain( NS_SPECIAL ) ); + + // Subject pages + $this->assertTrue( MWNamespace::isMain( NS_MAIN ) ); + $this->assertTrue( MWNamespace::isMain( NS_USER ) ); + $this->assertTrue( MWNamespace::isMain( 100 ) ); # user defined + + // Talk pages + $this->assertFalse( MWNamespace::isMain( NS_TALK ) ); + $this->assertFalse( MWNamespace::isMain( NS_USER_TALK ) ); + $this->assertFalse( MWNamespace::isMain( 101 ) ); # user defined + } + + /** + * Reverse of testIsMain(). + * Please update testIsMain() if you change assertions below + */ + public function testIsTalk() { + // Special namespaces + $this->assertFalse( MWNamespace::isTalk( NS_MEDIA ) ); + $this->assertFalse( MWNamespace::isTalk( NS_SPECIAL ) ); + + // Subject pages + $this->assertFalse( MWNamespace::isTalk( NS_MAIN ) ); + $this->assertFalse( MWNamespace::isTalk( NS_USER ) ); + $this->assertFalse( MWNamespace::isTalk( 100 ) ); # user defined + + // Talk pages + $this->assertTrue( MWNamespace::isTalk( NS_TALK ) ); + $this->assertTrue( MWNamespace::isTalk( NS_USER_TALK ) ); + $this->assertTrue( MWNamespace::isTalk( 101 ) ); # user defined + } + + /** + * Regular getTalk() calls + * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in + * the function testGetTalkExceptions() + */ + public function testGetTalk() { + $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_MAIN ) ); + } + + /** + * Exceptions with getTalk() + * NS_MEDIA does not have talk pages. MediaWiki raise an exception for them. + * @expectedException MWException + */ + public function testGetTalkExceptionsForNsMedia() { + $this->assertNull( MWNamespace::getTalk( NS_MEDIA ) ); + } + + /** + * Exceptions with getTalk() + * NS_SPECIAL does not have talk pages. MediaWiki raise an exception for them. + * @expectedException MWException + */ + public function testGetTalkExceptionsForNsSpecial() { + $this->assertNull( MWNamespace::getTalk( NS_SPECIAL ) ); + } + + /** + * Regular getAssociated() calls + * Namespaces without an associated page (NS_MEDIA, NS_SPECIAL) are tested in + * the function testGetAssociatedExceptions() + */ + public function testGetAssociated() { + $this->assertEquals( NS_TALK, MWNamespace::getAssociated( NS_MAIN ) ); + $this->assertEquals( NS_MAIN, MWNamespace::getAssociated( NS_TALK ) ); + + } + + ### Exceptions with getAssociated() + ### NS_MEDIA and NS_SPECIAL do not have talk pages. MediaWiki raises + ### an exception for them. + /** + * @expectedException MWException + */ + public function testGetAssociatedExceptionsForNsMedia() { + $this->assertNull( MWNamespace::getAssociated( NS_MEDIA ) ); + } + + /** + * @expectedException MWException + */ + public function testGetAssociatedExceptionsForNsSpecial() { + $this->assertNull( MWNamespace::getAssociated( NS_SPECIAL ) ); + } + + /** + */ + public function testGetSubject() { + // Special namespaces are their own subjects + $this->assertEquals( NS_MEDIA, MWNamespace::getSubject( NS_MEDIA ) ); + $this->assertEquals( NS_SPECIAL, MWNamespace::getSubject( NS_SPECIAL ) ); + + $this->assertEquals( NS_MAIN, MWNamespace::getSubject( NS_TALK ) ); + $this->assertEquals( NS_USER, MWNamespace::getSubject( NS_USER_TALK ) ); + } + + /** + * @todo Implement testExists(). + */ + public function testExists() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + + /** + * @todo Implement testGetCanonicalNamespaces(). + */ + public function testGetCanonicalNamespaces() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + + /** + * @todo Implement testGetCanonicalName(). + */ + public function testGetCanonicalName() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + + /** + * @todo Implement testGetCanonicalIndex(). + */ + public function testGetCanonicalIndex() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + + /** + * @todo Implement testGetValidNamespaces(). + */ + public function testGetValidNamespaces() { + // Remove the following lines when you implement this test. + $this->markTestIncomplete( + 'This test has not been implemented yet. Rely on $wgCanonicalNamespaces.' + ); + } + + /** + */ + public function testCanTalk() { + $this->assertFalse( MWNamespace::canTalk( NS_MEDIA ) ); + $this->assertFalse( MWNamespace::canTalk( NS_SPECIAL ) ); + + $this->assertTrue( MWNamespace::canTalk( NS_MAIN ) ); + $this->assertTrue( MWNamespace::canTalk( NS_TALK ) ); + $this->assertTrue( MWNamespace::canTalk( NS_USER ) ); + $this->assertTrue( MWNamespace::canTalk( NS_USER_TALK ) ); + + // User defined namespaces + $this->assertTrue( MWNamespace::canTalk( 100 ) ); + $this->assertTrue( MWNamespace::canTalk( 101 ) ); + } + + /** + */ + public function testIsContent() { + // NS_MAIN is a content namespace per DefaultSettings.php + // and per function definition. + $this->assertTrue( MWNamespace::isContent( NS_MAIN ) ); + + // Other namespaces which are not expected to be content + $this->assertFalse( MWNamespace::isContent( NS_MEDIA ) ); + $this->assertFalse( MWNamespace::isContent( NS_SPECIAL ) ); + $this->assertFalse( MWNamespace::isContent( NS_TALK ) ); + $this->assertFalse( MWNamespace::isContent( NS_USER ) ); + $this->assertFalse( MWNamespace::isContent( NS_CATEGORY ) ); + // User defined namespace: + $this->assertFalse( MWNamespace::isContent( 100 ) ); + } + + /** + * Similar to testIsContent() but alters the $wgContentNamespaces + * global variable. + */ + public function testIsContentWithAdditionsInWgContentNamespaces() { + // NS_MAIN is a content namespace per DefaultSettings.php + // and per function definition. + $this->assertTrue( MWNamespace::isContent( NS_MAIN ) ); + + // Tests that user defined namespace #252 is not content: + $this->assertFalse( MWNamespace::isContent( 252 ) ); + + # @todo FIXME: Is global saving really required for PHPUnit? + // Bless namespace # 252 as a content namespace + global $wgContentNamespaces; + $savedGlobal = $wgContentNamespaces; + $wgContentNamespaces[] = 252; + $this->assertTrue( MWNamespace::isContent( 252 ) ); + + // Makes sure NS_MAIN was not impacted + $this->assertTrue( MWNamespace::isContent( NS_MAIN ) ); + + // Restore global + $wgContentNamespaces = $savedGlobal; + + // Verify namespaces after global restauration + $this->assertTrue( MWNamespace::isContent( NS_MAIN ) ); + $this->assertFalse( MWNamespace::isContent( 252 ) ); + } + + public function testIsWatchable() { + // Specials namespaces are not watchable + $this->assertFalse( MWNamespace::isWatchable( NS_MEDIA ) ); + $this->assertFalse( MWNamespace::isWatchable( NS_SPECIAL ) ); + + // Core defined namespaces are watchables + $this->assertTrue( MWNamespace::isWatchable( NS_MAIN ) ); + $this->assertTrue( MWNamespace::isWatchable( NS_TALK ) ); + + // Additional, user defined namespaces are watchables + $this->assertTrue( MWNamespace::isWatchable( 100 ) ); + $this->assertTrue( MWNamespace::isWatchable( 101 ) ); + } + + public function testHasSubpages() { + // Special namespaces: + $this->assertFalse( MWNamespace::hasSubpages( NS_MEDIA ) ); + $this->assertFalse( MWNamespace::hasSubpages( NS_SPECIAL ) ); + + // namespaces without subpages + # save up global + global $wgNamespacesWithSubpages; + $saved = null; + if( array_key_exists( NS_MAIN, $wgNamespacesWithSubpages ) ) { + $saved = $wgNamespacesWithSubpages[NS_MAIN]; + unset( $wgNamespacesWithSubpages[NS_MAIN] ); + } + + $this->assertFalse( MWNamespace::hasSubpages( NS_MAIN ) ); + + $wgNamespacesWithSubpages[NS_MAIN] = true; + $this->assertTrue( MWNamespace::hasSubpages( NS_MAIN ) ); + $wgNamespacesWithSubpages[NS_MAIN] = false; + $this->assertFalse( MWNamespace::hasSubpages( NS_MAIN ) ); + + # restore global + if( $saved !== null ) { + $wgNamespacesWithSubpages[NS_MAIN] = $saved; + } + + // Some namespaces with subpages + $this->assertTrue( MWNamespace::hasSubpages( NS_TALK ) ); + $this->assertTrue( MWNamespace::hasSubpages( NS_USER ) ); + $this->assertTrue( MWNamespace::hasSubpages( NS_USER_TALK ) ); + } + + /** + */ + public function testGetContentNamespaces() { + $this->assertEquals( + array( NS_MAIN ), + MWNamespace::getcontentNamespaces(), + '$wgContentNamespaces is an array with only NS_MAIN by default' + ); + + global $wgContentNamespaces; + + # test !is_array( $wgcontentNamespaces ) + $wgContentNamespaces = ''; + $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() ); + $wgContentNamespaces = false; + $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() ); + $wgContentNamespaces = null; + $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() ); + $wgContentNamespaces = 5; + $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() ); + + # test $wgContentNamespaces === array() + $wgContentNamespaces = array(); + $this->assertEquals( NS_MAIN, MWNamespace::getcontentNamespaces() ); + + # test !in_array( NS_MAIN, $wgContentNamespaces ) + $wgContentNamespaces = array( NS_USER, NS_CATEGORY ); + $this->assertEquals( + array( NS_MAIN, NS_USER, NS_CATEGORY ), + MWNamespace::getcontentNamespaces(), + 'NS_MAIN is forced in wgContentNamespaces even if unwanted' + ); + + # test other cases, return $wgcontentNamespaces as is + $wgContentNamespaces = array( NS_MAIN ); + $this->assertEquals( + array( NS_MAIN ), + MWNamespace::getcontentNamespaces() + ); + + $wgContentNamespaces = array( NS_MAIN, NS_USER, NS_CATEGORY ); + $this->assertEquals( + array( NS_MAIN, NS_USER, NS_CATEGORY ), + MWNamespace::getcontentNamespaces() + ); + + } + + /** + * Some namespaces are always capitalized per code definition + * in MWNamespace::$alwaysCapitalizedNamespaces + */ + public function testIsCapitalizedHardcodedAssertions() { + // NS_MEDIA and NS_FILE are treated the same + $this->assertEquals( + MWNamespace::isCapitalized( NS_MEDIA ), + MWNamespace::isCapitalized( NS_FILE ), + 'NS_MEDIA and NS_FILE have same capitalization rendering' + ); + + // Boths are capitalized by default + $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIA ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_FILE ) ); + + // Always capitalized namespaces + // @see MWNamespace::$alwaysCapitalizedNamespaces + $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) ); + } + + /** + * Follows up for testIsCapitalizedHardcodedAssertions() but alter the + * global $wgCapitalLink setting to have extended coverage. + * + * MWNamespace::isCapitalized() rely on two global settings: + * $wgCapitalLinkOverrides = array(); by default + * $wgCapitalLinks = true; by default + * This function test $wgCapitalLinks + * + * Global setting correctness is tested against the NS_PROJECT and + * NS_PROJECT_TALK namespaces since they are not hardcoded nor specials + */ + public function testIsCapitalizedWithWgCapitalLinks() { + global $wgCapitalLinks; + // Save the global to easily reset to MediaWiki default settings + $savedGlobal = $wgCapitalLinks; + + $wgCapitalLinks = true; + $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT_TALK ) ); + + $wgCapitalLinks = false; + // hardcoded namespaces (see above function) are still capitalized: + $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) ); + // setting is correctly applied + $this->assertFalse( MWNamespace::isCapitalized( NS_PROJECT ) ); + $this->assertFalse( MWNamespace::isCapitalized( NS_PROJECT_TALK ) ); + + // reset global state: + $wgCapitalLinks = $savedGlobal; + } + + /** + * Counter part for MWNamespace::testIsCapitalizedWithWgCapitalLinks() now + * testing the $wgCapitalLinkOverrides global. + * + * @todo split groups of assertions in autonomous testing functions + */ + public function testIsCapitalizedWithWgCapitalLinkOverrides() { + global $wgCapitalLinkOverrides; + // Save the global to easily reset to MediaWiki default settings + $savedGlobal = $wgCapitalLinkOverrides; + + // Test default settings + $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT_TALK ) ); + // hardcoded namespaces (see above function) are capitalized: + $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) ); + + // Hardcoded namespaces remains capitalized + $wgCapitalLinkOverrides[NS_SPECIAL] = false; + $wgCapitalLinkOverrides[NS_USER] = false; + $wgCapitalLinkOverrides[NS_MEDIAWIKI] = false; + $this->assertTrue( MWNamespace::isCapitalized( NS_SPECIAL ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_USER ) ); + $this->assertTrue( MWNamespace::isCapitalized( NS_MEDIAWIKI ) ); + + $wgCapitalLinkOverrides = $savedGlobal; + $wgCapitalLinkOverrides[NS_PROJECT] = false; + $this->assertFalse( MWNamespace::isCapitalized( NS_PROJECT ) ); + $wgCapitalLinkOverrides[NS_PROJECT] = true ; + $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) ); + unset( $wgCapitalLinkOverrides[NS_PROJECT] ); + $this->assertTrue( MWNamespace::isCapitalized( NS_PROJECT ) ); + + // reset global state: + $wgCapitalLinkOverrides = $savedGlobal; + } + + public function testHasGenderDistinction() { + // Namespaces with gender distinctions + $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER ) ); + $this->assertTrue( MWNamespace::hasGenderDistinction( NS_USER_TALK ) ); + + // Other ones, "genderless" + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MEDIA ) ); + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_SPECIAL ) ); + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_MAIN ) ); + $this->assertFalse( MWNamespace::hasGenderDistinction( NS_TALK ) ); + + } +} + diff --git a/tests/phpunit/includes/MessageTest.php b/tests/phpunit/includes/MessageTest.php new file mode 100644 index 00000000..45c02bbe --- /dev/null +++ b/tests/phpunit/includes/MessageTest.php @@ -0,0 +1,62 @@ +<?php + +class MessageTest extends MediaWikiLangTestCase { + + 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->assertInstanceOf( 'Message', wfMessage( 'mainpage' ) ); + $this->assertInstanceOf( '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 testMessageParams() { + $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() ); + } + + function testMessageParamSubstitution() { + $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses', 'Заглавная страница' )->plain() ); + $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses', 'Заглавная страница $1' )->plain() ); + $this->assertEquals( '(Заглавная страница)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница' )->plain() ); + $this->assertEquals( '(Заглавная страница $1)', wfMessage( 'parentheses' )->rawParams( 'Заглавная страница $1' )->plain() ); + } + + function testInContentLanguage() { + global $wgLang, $wgForceUIMsgAsContentMsg; + $oldLang = $wgLang; + $wgLang = Language::factory( 'fr' ); + + $this->assertEquals( 'Main Page', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg disabled' ); + $wgForceUIMsgAsContentMsg['testInContentLanguage'] = 'mainpage'; + $this->assertEquals( 'Accueil', wfMessage( 'mainpage' )->inContentLanguage()->plain(), 'ForceUIMsg enabled' ); + + /* Restore globals */ + $wgLang = $oldLang; + unset( $wgForceUIMsgAsContentMsg['testInContentLanguage'] ); + } + + /** + * @expectedException MWException + */ + function testInLanguageThrows() { + wfMessage( 'foo' )->inLanguage( 123 ); + } +} diff --git a/tests/phpunit/includes/ParserOptionsTest.php b/tests/phpunit/includes/ParserOptionsTest.php new file mode 100644 index 00000000..58c89146 --- /dev/null +++ b/tests/phpunit/includes/ParserOptionsTest.php @@ -0,0 +1,36 @@ +<?php + +class ParserOptionsTest extends MediaWikiTestCase { + + private $popts; + private $pcache; + + function setUp() { + ParserTest::setUp(); //reuse setup from parser tests + global $wgContLang, $wgUser, $wgLanguageCode; + $wgContLang = Language::factory( $wgLanguageCode ); + $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 ); + } +} diff --git a/tests/phpunit/includes/Providers.php b/tests/phpunit/includes/Providers.php new file mode 100644 index 00000000..02898673 --- /dev/null +++ b/tests/phpunit/includes/Providers.php @@ -0,0 +1,44 @@ +<?php +/** + * Generic providers for the MediaWiki PHPUnit test suite + * + * @author Ashar Voultoiz + * @copyright Copyright © 2011, Ashar Voultoiz + * @file + */ + +/** */ +class MediaWikiProvide { + + /* provide an array of numbers from 1 up to @param $num */ + private static function createProviderUpTo( $num ) { + $ret = array(); + for( $i=1; $i<=$num;$i++ ) { + $ret[] = array( $i ); + } + return $ret; + } + + /* array of months numbers (as an integer) */ + public static function Months() { + return self::createProviderUpTo( 12 ); + } + + /* array of days numbers (as an integer) */ + public static function Days() { + return self::createProviderUpTo( 31 ); + } + + public static function DaysMonths() { + $ret = array(); + + $months = self::Months(); + $days = self::Days(); + foreach( $months as $month) { + foreach( $days as $day ) { + $ret[] = array( $day[0], $month[0] ); + } + } + return $ret; + } +} diff --git a/tests/phpunit/includes/ResourceLoaderTest.php b/tests/phpunit/includes/ResourceLoaderTest.php new file mode 100644 index 00000000..30a69c5e --- /dev/null +++ b/tests/phpunit/includes/ResourceLoaderTest.php @@ -0,0 +1,91 @@ +<?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 ) ); + } + + /** + * @dataProvider providePackedModules + */ + public function testMakePackedModulesString( $desc, $modules, $packed ) { + $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc ); + } + + /** + * @dataProvider providePackedModules + */ + public function testexpandModuleNames( $desc, $modules, $packed ) { + $this->assertEquals( $modules, ResourceLoaderContext::expandModuleNames( $packed ), $desc ); + } + + public function providePackedModules() { + return array( + array( + 'Example from makePackedModulesString doc comment', + array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ), + 'foo.bar,baz|bar.baz,quux', + ), + array( + 'Example from expandModuleNames doc comment', + array( 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ), + 'jquery.foo,bar|jquery.ui.baz,quux', + ), + array( + 'Regression fixed in r88706 with dotless names', + array( 'foo', 'bar', 'baz' ), + 'foo,bar,baz', + ) + ); + } +} + +/* Stubs */ + +class ResourceLoaderTestModule extends ResourceLoaderModule { } + +/* Hooks */ +global $wgHooks; +$wgHooks['ResourceLoaderRegisterModules'][] = 'ResourceLoaderTest::resourceLoaderRegisterModules'; diff --git a/tests/phpunit/includes/RevisionTest.php b/tests/phpunit/includes/RevisionTest.php new file mode 100644 index 00000000..d7654db9 --- /dev/null +++ b/tests/phpunit/includes/RevisionTest.php @@ -0,0 +1,125 @@ +<?php + +class RevisionTest extends MediaWikiTestCase { + var $saveGlobals = array(); + + function setUp() { + global $wgContLang; + $wgContLang = Language::factory( 'en' ); + $globalSet = array( + 'wgLegacyEncoding' => false, + 'wgCompressRevisions' => false, + ); + 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() { + if ( !function_exists( 'gzdeflate' ) ) { + $this->markTestSkipped( 'Gzip compression is not enabled (requires zlib).' ); + } else { + $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() { + if ( !function_exists( 'gzdeflate' ) ) { + $this->markTestSkipped( 'Gzip compression is not enabled (requires zlib).' ); + } else { + $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() { + if ( !function_exists( 'gzdeflate' ) ) { + $this->markTestSkipped( 'Gzip compression is not enabled (requires zlib).' ); + } else { + $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" ); + } +} + + diff --git a/tests/phpunit/includes/SampleTest.php b/tests/phpunit/includes/SampleTest.php new file mode 100644 index 00000000..77a371d5 --- /dev/null +++ b/tests/phpunit/includes/SampleTest.php @@ -0,0 +1,98 @@ +<?php + +class TestSample extends MediaWikiLangTestCase { + + /** + * Anything that needs to happen before your tests should go here. + */ + function setUp() { + global $wgContLang; + parent::setUp(); + + /* 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() { + parent::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( Title::newFromText( "test" ) ); + $this->assertEquals( "Test", $title->isLocal() ); + } +} + diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php new file mode 100644 index 00000000..40d6cf77 --- /dev/null +++ b/tests/phpunit/includes/SanitizerTest.php @@ -0,0 +1,113 @@ +<?php + +class SanitizerTest extends MediaWikiTestCase { + + 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' + ); + } + + function testDecodeTagAttributes() { + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=bar' ), array( 'foo' => 'bar' ), 'Unquoted attribute' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( ' foo = bar ' ), array( 'foo' => 'bar' ), 'Spaced attribute' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo="bar"' ), array( 'foo' => 'bar' ), 'Double-quoted attribute' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\'' ), array( 'foo' => 'bar' ), 'Single-quoted attribute' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\' baz="foo"' ), array( 'foo' => 'bar', 'baz' => 'foo' ), 'Several attributes' ); + + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\' baz="foo"' ), array( 'foo' => 'bar', 'baz' => 'foo' ), 'Several attributes' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'bar\' baz="foo"' ), array( 'foo' => 'bar', 'baz' => 'foo' ), 'Several attributes' ); + + $this->assertEquals( Sanitizer::decodeTagAttributes( ':foo=\'bar\'' ), array( ':foo' => 'bar' ), 'Leading :' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( '_foo=\'bar\'' ), array( '_foo' => 'bar' ), 'Leading _' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'Foo=\'bar\'' ), array( 'foo' => 'bar' ), 'Leading capital' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'FOO=BAR' ), array( 'foo' => 'BAR' ), 'Attribute keys are normalized to lowercase' ); + + # Invalid beginning + $this->assertEquals( Sanitizer::decodeTagAttributes( '-foo=bar' ), array(), 'Leading - is forbidden' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( '.foo=bar' ), array(), 'Leading . is forbidden' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo-bar=bar' ), array( 'foo-bar' => 'bar' ), 'A - is allowed inside the attribute' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo-=bar' ), array( 'foo-' => 'bar' ), 'A - is allowed inside the attribute' ); + + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo.bar=baz' ), array( 'foo.bar' => 'baz' ), 'A . is allowed inside the attribute' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo.=baz' ), array( 'foo.' => 'baz' ), 'A . is allowed as last character' ); + + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo6=baz' ), array( 'foo6' => 'baz' ), 'Numbers are allowed' ); + + # This bit is more relaxed than XML rules, but some extensions use it, like ProofreadPage (see bug 27539) + $this->assertEquals( Sanitizer::decodeTagAttributes( '1foo=baz' ), array( '1foo' => 'baz' ), 'Leading numbers are allowed' ); + + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo$=baz' ), array(), 'Symbols are not allowed' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo@=baz' ), array(), 'Symbols are not allowed' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo~=baz' ), array(), 'Symbols are not allowed' ); + + + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=1[#^`*%w/(' ), array( 'foo' => '1[#^`*%w/(' ), 'All kind of characters are allowed as values' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo="1[#^`*%\'w/("' ), array( 'foo' => '1[#^`*%\'w/(' ), 'Double quotes are allowed if quoted by single quotes' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=\'1[#^`*%"w/(\'' ), array( 'foo' => '1[#^`*%"w/(' ), 'Single quotes are allowed if quoted by double quotes' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=&"' ), array( 'foo' => '&"' ), 'Special chars can be provided as entities' ); + $this->assertEquals( Sanitizer::decodeTagAttributes( 'foo=&foobar;' ), array( 'foo' => '&foobar;' ), 'Entity-like items are accepted' ); + } +} + diff --git a/tests/phpunit/includes/SeleniumConfigurationTest.php b/tests/phpunit/includes/SeleniumConfigurationTest.php new file mode 100644 index 00000000..750524eb --- /dev/null +++ b/tests/phpunit/includes/SeleniumConfigurationTest.php @@ -0,0 +1,228 @@ +<?php + +class SeleniumConfigurationTest extends MediaWikiTestCase { + + /* + * 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] = "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' => '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 = array(); + $seleniumBrowsers = array(); + $seleniumTestSuites = array(); + + SeleniumConfig::getSeleniumSettings($seleniumSettings, + $seleniumBrowsers, + $seleniumTestSuites, + "Some_fake_settings_file.ini" ); + + } + + /** + * @expectedException MWException + * @group SeleniumFramework + */ + public function testErrorOnMissingConfigFile() { + $seleniumSettings = array(); + $seleniumBrowsers = array(); + $seleniumTestSuites = array(); + global $wgSeleniumConfigFile; + $wgSeleniumConfigFile = ''; + SeleniumConfig::getSeleniumSettings($seleniumSettings, + $seleniumBrowsers, + $seleniumTestSuites); + } + + /** + * @group SeleniumFramework + */ + public function testUsesGlobalVarForConfigFile() { + $seleniumSettings = array(); + $seleniumBrowsers = array(); + $seleniumTestSuites = array(); + 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 = array(); + $seleniumBrowsers = array(); + $seleniumTestSuites = null; + + 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 ) + ); + } + + +} diff --git a/tests/phpunit/includes/SiteConfigurationTest.php b/tests/phpunit/includes/SiteConfigurationTest.php new file mode 100644 index 00000000..57d3532a --- /dev/null +++ b/tests/phpunit/includes/SiteConfigurationTest.php @@ -0,0 +1,311 @@ +<?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 MediaWikiTestCase { + 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 testGetConfVariables() { + $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 testParameterReplacement() { + $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 testGetAllGlobals() { + $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' ); + } +} diff --git a/tests/phpunit/includes/TimeAdjustTest.php b/tests/phpunit/includes/TimeAdjustTest.php new file mode 100644 index 00000000..cd027c5b --- /dev/null +++ b/tests/phpunit/includes/TimeAdjustTest.php @@ -0,0 +1,51 @@ +<?php + +class TimeAdjustTest extends MediaWikiLangTestCase { + static $offset; + + public function setUp() { + parent::setUp(); + global $wgLocalTZoffset; + self::$offset = $wgLocalTZoffset; + + $this->iniSet( 'precision', 15 ); + } + + public function tearDown() { + global $wgLocalTZoffset; + $wgLocalTZoffset = self::$offset; + parent::tearDown(); + } + + # 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]}" + ); + } + } +} diff --git a/tests/phpunit/includes/TitlePermissionTest.php b/tests/phpunit/includes/TitlePermissionTest.php new file mode 100644 index 00000000..1b179686 --- /dev/null +++ b/tests/phpunit/includes/TitlePermissionTest.php @@ -0,0 +1,654 @@ +<?php + +/** + * @group Database + */ +class TitlePermissionTest extends MediaWikiLangTestCase { + protected $title; + protected $user; + protected $anonUser; + protected $userUser; + protected $altUser; + protected $userName; + protected $altUserName; + + function setUp() { + global $wgLocaltimezone, $wgLocalTZoffset, $wgMemc, $wgContLang, $wgLang; + parent::setUp(); + + if(!$wgMemc) { + $wgMemc = new EmptyBagOStuff; + } + $wgContLang = $wgLang = Language::factory( 'en' ); + + $this->userName = "Useruser"; + $this->altUserName = "Altuseruser"; + date_default_timezone_set( $wgLocaltimezone ); + $wgLocalTZoffset = date( "Z" ) / 60; + + $this->title = Title::makeTitle( NS_MAIN, "Main Page" ); + if ( !isset( $this->userUser ) || !( $this->userUser instanceOf User ) ) { + $this->userUser = User::newFromName( $this->userName ); + + if ( !$this->userUser->getID() ) { + $this->userUser = User::createNew( $this->userName, array( + "email" => "test@example.com", + "real_name" => "Test User" ) ); + $this->userUser->load(); + } + + $this->altUser = User::newFromName( $this->altUserName ); + if ( !$this->altUser->getID() ) { + $this->altUser = User::createNew( $this->altUserName, array( + "email" => "alttest@example.com", + "real_name" => "Test User Alt" ) ); + $this->altUser->load(); + } + + $this->anonUser = User::newFromId( 0 ); + + $this->user = $this->userUser; + } + } + + function tearDown() { + parent::tearDown(); + } + + function setUserPerm( $perm ) { + if ( is_array( $perm ) ) { + $this->user->mRights = $perm; + } else { + $this->user->mRights = array( $perm ); + } + } + + function setTitle( $ns, $title = "Main_Page" ) { + $this->title = Title::makeTitle( $ns, $title ); + } + + function setUser( $userName = null ) { + if ( $userName === 'anon' ) { + $this->user = $this->anonUser; + } elseif ( $userName === null || $userName === $this->userName ) { + $this->user = $this->userUser; + } else { + $this->user = $this->altUser; + } + + global $wgUser; + $wgUser = $this->user; + } + + function testQuickPermissions() { + global $wgContLang; + $prefix = $wgContLang->getFormattedNsText( NS_PROJECT ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array(), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( "nocreatetext" ) ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreatetext' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreatetext' ) ), $res ); + + $this->setUser( $this->userName ); + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_TALK ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createpage" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "createtalk" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'create', $this->user ); + $this->assertEquals( array( array( 'nocreate-loggedin' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'cant-move-user-page' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_USER, $this->userName . '/subpage' ); + $this->setUserPerm( "move-rootuserpages" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setUser( $this->userName ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenotallowed' ) ), $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenotallowed' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenotallowedfile' ), array( 'movenologintext' ) ), $res ); + + $this->setTitle( NS_FILE, "img.png" ); + $this->setUserPerm( "movefile" ); + $res = $this->title->getUserPermissionsErrors( 'move', $this->user ); + $this->assertEquals( array( array( 'movenologintext' ) ), $res ); + + $this->setUser( $this->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( $this->userName ); + $this->setUserPerm( "" ); + $this->runGroupPermissions( 'move', array( array( 'movenotallowed' ) ) ); + + $this->setUserPerm( "move" ); + $this->runGroupPermissions( 'move', array( ) ); + + $this->setUser( 'anon' ); + $this->setUserPerm( 'move' ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( ), $res ); + + $this->setUserPerm( '' ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( array( 'movenotallowed' ) ), $res ); + + $this->setTitle( NS_USER ); + $this->setUser( $this->userName ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( ), $res ); + + $this->setUserPerm( "move" ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( array( 'cant-move-to-user-page' ) ), $res ); + + $this->setUser( 'anon' ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( ), $res ); + + $this->setTitle( NS_USER, "User/subpage" ); + $this->setUserPerm( array( "move", "move-rootuserpages" ) ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->user ); + $this->assertEquals( array( ), $res ); + + $this->setUserPerm( "move" ); + $res = $this->title->getUserPermissionsErrors( 'move-target', $this->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 = $this->user; + foreach ( array( "edit", "protect", "" ) as $action ) { + $this->setUserPerm( null ); + $this->assertEquals( $check[$action][0], + $this->title->getUserPermissionsErrors( $action, $this->user, true ) ); + + global $wgGroupPermissions; + $old = $wgGroupPermissions; + $wgGroupPermissions = array(); + + $this->assertEquals( $check[$action][1], + $this->title->getUserPermissionsErrors( $action, $this->user, true ) ); + $wgGroupPermissions = $old; + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][2], + $this->title->getUserPermissionsErrors( $action, $this->user, true ) ); + + $this->setUserPerm( $action ); + $this->assertEquals( $check[$action][3], + $this->title->userCan( $action, true ) ); + $this->assertEquals( $check[$action][3], + $this->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 = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = false; + $res = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = true; + $wgGroupPermissions['user']['move'] = true; + $res = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result2, $res ); + + $wgGroupPermissions['autoconfirmed']['move'] = false; + $wgGroupPermissions['user']['move'] = true; + $res = $this->title->getUserPermissionsErrors( $action, $this->user ); + $this->assertEquals( $result2, $res ); + } + + function testSpecialsAndNSPermissions() { + $this->setUser( $this->userName ); + global $wgUser; + $wgUser = $this->user; + + $this->setTitle( NS_SPECIAL ); + + $this->assertEquals( array( array( 'badaccess-group0' ), array( 'ns-specialprotected' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'execute', $this->user ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->setTitle( NS_MAIN ); + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + global $wgNamespaceProtection; + $wgNamespaceProtection[NS_USER] = array ( 'bogus' ); + $this->setTitle( NS_USER ); + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ), array( 'namespaceprotected', 'User' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( array( 'protectedinterface' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->setTitle( NS_MEDIAWIKI ); + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( array( 'protectedinterface' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $wgNamespaceProtection = null; + $this->setUserPerm( 'bogus' ); + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'bogus' ) ); + + $this->setUserPerm( '' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'bogus' ) ); + } + + function testCssAndJavascriptPermissions() { + $this->setUser( $this->userName ); + global $wgUser; + $wgUser = $this->user; + + $this->setTitle( NS_USER, $this->altUserName . '/test.js' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ), + array( array( 'badaccess-group0' ), array( 'customjsprotected' ) ), + array( array( 'badaccess-group0' ) ) ); + + $this->setTitle( NS_USER, $this->altUserName . '/test.css' ); + $this->runCSSandJSPermissions( + array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ), + array( array( 'badaccess-group0' ) ), + array( array( 'badaccess-group0' ), array( 'customcssprotected' ) ) ); + + $this->setTitle( NS_USER, $this->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, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'editusercss' ); + $this->assertEquals( $result1, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'edituserjs' ); + $this->assertEquals( $result2, + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( 'editusercssjs' ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + + $this->setUserPerm( array( 'edituserjs', 'editusercss' ) ); + $this->assertEquals( array( array( 'badaccess-group0' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + } + + function testPageRestrictions() { + global $wgUser, $wgContLang; + + $prefix = $wgContLang->getFormattedNsText( NS_PROJECT ); + + $wgUser = $this->user; + $this->setTitle( NS_MAIN ); + $this->title->mRestrictionsLoaded = true; + $this->setUserPerm( "edit" ); + $this->title->mRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + + $this->assertEquals( true, + $this->title->quickUserCan( 'edit', false ) ); + $this->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' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + $this->setUserPerm( "" ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( array( 'badaccess-groups', "*, [[$prefix:Users|Users]]", 2 ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + $this->setUserPerm( array( "edit", "editprotected" ) ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + $this->title->mCascadeRestriction = true; + $this->assertEquals( false, + $this->title->quickUserCan( 'bogus', false ) ); + $this->assertEquals( false, + $this->title->quickUserCan( 'edit', false ) ); + $this->assertEquals( array( array( 'badaccess-group0' ), + array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'bogus', + $this->user ) ); + $this->assertEquals( array( array( 'protectedpagetext', 'bogus' ), + array( 'protectedpagetext', 'protect' ), + array( 'protectedpagetext', 'protect' ) ), + $this->title->getUserPermissionsErrors( 'edit', + $this->user ) ); + } + + function testCascadingSourcesRestrictions() { + global $wgUser; + $wgUser = $this->user; + $this->setTitle( NS_MAIN, "test page" ); + $this->setUserPerm( array( "edit", "bogus" ) ); + + $this->title->mCascadeSources = array( Title::makeTitle( NS_MAIN, "Bogus" ), Title::makeTitle( NS_MAIN, "UnBogus" ) ); + $this->title->mCascadingRestrictions = array( "bogus" => array( 'bogus', "sysop", "protect", "" ) ); + + $this->assertEquals( false, + $this->title->userCan( 'bogus' ) ); + $this->assertEquals( array( array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ), + array( "cascadeprotected", 2, "* [[:Bogus]]\n* [[:UnBogus]]\n" ) ), + $this->title->getUserPermissionsErrors( 'bogus', $this->user ) ); + + $this->assertEquals( true, + $this->title->userCan( 'edit' ) ); + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'edit', $this->user ) ); + + } + + function testActionPermissions() { + global $wgUser; + $wgUser = $this->user; + + $this->setUserPerm( array( "createpage" ) ); + $this->setTitle( NS_MAIN, "test page" ); + $this->title->mTitleProtection['pt_create_perm'] = ''; + $this->title->mTitleProtection['pt_user'] = $this->user->getID(); + $this->title->mTitleProtection['pt_expiry'] = Block::infinity(); + $this->title->mTitleProtection['pt_reason'] = 'test'; + $this->title->mCascadeRestriction = false; + + $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ), + $this->title->getUserPermissionsErrors( 'create', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'create' ) ); + + $this->title->mTitleProtection['pt_create_perm'] = 'sysop'; + $this->setUserPerm( array( 'createpage', 'protect' ) ); + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'create', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'create' ) ); + + + $this->setUserPerm( array( 'createpage' ) ); + $this->assertEquals( array( array( 'titleprotected', 'Useruser', 'test' ) ), + $this->title->getUserPermissionsErrors( 'create', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'create' ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->setUserPerm( array( "move" ) ); + $this->assertEquals( false, + $this->title->userCan( 'move' ) ); + $this->assertEquals( array( array( 'immobile-source-namespace', 'Media' ) ), + $this->title->getUserPermissionsErrors( 'move', $this->user ) ); + + $this->setTitle( NS_MAIN, "test page" ); + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'move', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'move' ) ); + + $this->title->mInterwiki = "no"; + $this->assertEquals( array( array( 'immobile-page' ) ), + $this->title->getUserPermissionsErrors( 'move', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'move' ) ); + + $this->setTitle( NS_MEDIA, "test page" ); + $this->assertEquals( false, + $this->title->userCan( 'move-target' ) ); + $this->assertEquals( array( array( 'immobile-target-namespace', 'Media' ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + + $this->setTitle( NS_MAIN, "test page" ); + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + $this->assertEquals( true, + $this->title->userCan( 'move-target' ) ); + + $this->title->mInterwiki = "no"; + $this->assertEquals( array( array( 'immobile-target-page' ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + $this->assertEquals( false, + $this->title->userCan( 'move-target' ) ); + + } + + function testUserBlock() { + global $wgUser, $wgEmailConfirmToEdit, $wgEmailAuthentication; + $wgEmailConfirmToEdit = true; + $wgEmailAuthentication = true; + $wgUser = $this->user; + + $this->setUserPerm( array( "createpage", "move" ) ); + $this->setTitle( NS_MAIN, "test page" ); + + # $short + $this->assertEquals( array( array( 'confirmedittext' ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + $wgEmailConfirmToEdit = false; + $this->assertEquals( true, $this->title->userCan( 'move-target' ) ); + + # $wgEmailConfirmToEdit && !$user->isEmailConfirmed() && $action != 'createaccount' + $this->assertEquals( array( ), + $this->title->getUserPermissionsErrors( 'move-target', + $this->user ) ); + + global $wgLang; + $prev = time(); + $now = time() + 120; + $this->user->mBlockedby = $this->user->getId(); + $this->user->mBlock = new Block( '127.0.8.1', $this->user->getId(), $this->user->getId(), + 'no reason given', $prev + 3600, 1, 0 ); + $this->user->mBlock->mTimestamp = 0; + $this->assertEquals( array( array( 'autoblockedtext', + '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1', + 'Useruser', null, 'infinite', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $prev ), true ) ) ), + $this->title->getUserPermissionsErrors( 'move-target', + $this->user ) ); + + $this->assertEquals( false, $this->title->userCan( 'move-target' ) ); + // quickUserCan should ignore user blocks + $this->assertEquals( true, $this->title->quickUserCan( 'move-target' ) ); + + global $wgLocalTZoffset; + $wgLocalTZoffset = -60; + $this->user->mBlockedby = $this->user->getName(); + $this->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', null, '23:00, 31 December 1969', '127.0.8.1', + $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ) ), + $this->title->getUserPermissionsErrors( 'move-target', $this->user ) ); + + # $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) + # $user->blockedFor() == '' + # $user->mBlock->mExpiry == 'infinity' + } +} diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php new file mode 100644 index 00000000..51b36160 --- /dev/null +++ b/tests/phpunit/includes/TitleTest.php @@ -0,0 +1,79 @@ +<?php + +class TitleTest extends MediaWikiTestCase { + + 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" ); + } + } + } + + /** + * @dataProvider dataBug31100 + */ + function testBug31100FixSpecialName( $text, $expectedParam ) { + $title = Title::newFromText( $text ); + $fixed = $title->fixSpecialName(); + $stuff = explode( '/', $fixed->getDbKey(), 2 ); + if ( count( $stuff ) == 2 ) { + $par = $stuff[1]; + } else { + $par = null; + } + $this->assertEquals( $expectedParam, $par, "Bug 31100 regression check: Title->fixSpecialName() should preserve parameter" ); + } + + function dataBug31100() { + return array( + array( 'Special:Version', null ), + array( 'Special:Version/', '' ), + array( 'Special:Version/param', 'param' ), + ); + } + + /** + * Auth-less test of Title::isValidMoveOperation + * + * @param string $source + * @param string $target + * @param array|string|true $requiredErrors + * @dataProvider dataTestIsValidMoveOperation + */ + function testIsValidMoveOperation( $source, $target, $expected ) { + $title = Title::newFromText( $source ); + $nt = Title::newFromText( $target ); + $errors = $title->isValidMoveOperation( $nt, false ); + if ( $expected === true ) { + $this->assertTrue( $errors ); + } else { + $errors = $this->flattenErrorsArray( $errors ); + foreach ( (array)$expected as $error ) { + $this->assertContains( $error, $errors ); + } + } + } + + function flattenErrorsArray( $errors ) { + $result = array(); + foreach ( $errors as $error ) { + $result[] = $error[0]; + } + return $result; + } + + function dataTestIsValidMoveOperation() { + return array( + array( 'Test', 'Test', 'selfmove' ), + array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ) + ); + } + + +} diff --git a/tests/phpunit/includes/UserIsValidEmailAddrTest.php b/tests/phpunit/includes/UserIsValidEmailAddrTest.php new file mode 100644 index 00000000..99bf718e --- /dev/null +++ b/tests/phpunit/includes/UserIsValidEmailAddrTest.php @@ -0,0 +1,79 @@ +<?php + +class UserIsValidEmailAddrTest extends MediaWikiTestCase { + + private function checkEmail( $addr, $expected = true, $msg = '') { + if( $msg == '' ) { $msg = "Testing $addr"; } + $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 testEmailDoesNotNeedATopLevelDomain() { + $this->valid( "user@localhost" ); + $this->valid( "FooBar@localdomain" ); + $this->valid( "nobody@mycompany" ); + } + function testEmailWithWhiteSpacesBeforeOrAfterAreInvalids() { + $this->invalid( " user@host.com" ); + $this->invalid( "user@host.com " ); + $this->invalid( "\tuser@host.com" ); + $this->invalid( "user@host.com\t" ); + } + function testEmailWithWhiteSpacesAreInvalids() { + $this->invalid( "User user@host" ); + $this->invalid( "first last@mycompany" ); + $this->invalid( "firstlast@my company" ); + } + // bug 26948 : comma were matched by an incorrect regexp range + function testEmailWithCommasAreInvalids() { + $this->invalid( "user,foo@example.org" ); + $this->invalid( "userfoo@ex,ample.org" ); + } + function testEmailWithHyphens() { + $this->valid( "user-foo@example.org" ); + $this->valid( "userfoo@ex-ample.org" ); + } + function testEmailDomainCanNotBeginWithDot() { + $this->invalid( "user@." ); + $this->invalid( "user@.localdomain" ); + $this->invalid( "user@localdomain." ); + $this->valid( "user.@localdomain" ); + $this->valid( ".@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 testEmailWithOneCharacterDomainIsValid() { + $this->valid( 'user@a' ); + } +} diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/UserTest.php new file mode 100644 index 00000000..df91aca8 --- /dev/null +++ b/tests/phpunit/includes/UserTest.php @@ -0,0 +1,58 @@ +<?php + +class UserTest extends MediaWikiTestCase { + protected $savedGroupPermissions, $savedRevokedPermissions; + + public function setUp() { + parent::setUp(); + + $this->savedGroupPermissions = $GLOBALS['wgGroupPermissions']; + $this->savedRevokedPermissions = $GLOBALS['wgRevokePermissions']; + + $this->setUpPermissionGlobals(); + } + private function setUpPermissionGlobals() { + global $wgGroupPermissions, $wgRevokePermissions; + + $wgGroupPermissions['unittesters'] = array( + 'runtest' => true, + 'writetest' => false, + 'nukeworld' => false, + ); + $wgGroupPermissions['testwriters'] = array( + 'writetest' => true, + 'modifytest' => true, + ); + + $wgRevokePermissions['formertesters'] = array( + 'runtest' => true, + ); + } + public function tearDown() { + parent::tearDown(); + + $GLOBALS['wgGroupPermissions'] = $this->savedGroupPermissions; + $GLOBALS['wgRevokePermissions'] = $this->savedRevokedPermissions; + } + + public function testGroupPermissions() { + $rights = User::getGroupPermissions( array( 'unittesters' ) ); + $this->assertContains( 'runtest', $rights ); + $this->assertNotContains( 'writetest', $rights ); + $this->assertNotContains( 'modifytest', $rights ); + $this->assertNotContains( 'nukeworld', $rights ); + + $rights = User::getGroupPermissions( array( 'unittesters', 'testwriters' ) ); + $this->assertContains( 'runtest', $rights ); + $this->assertContains( 'writetest', $rights ); + $this->assertContains( 'modifytest', $rights ); + $this->assertNotContains( 'nukeworld', $rights ); + } + public function testRevokePermissions() { + $rights = User::getGroupPermissions( array( 'unittesters', 'formertesters' ) ); + $this->assertNotContains( 'runtest', $rights ); + $this->assertNotContains( 'writetest', $rights ); + $this->assertNotContains( 'modifytest', $rights ); + $this->assertNotContains( 'nukeworld', $rights ); + } +}
\ No newline at end of file diff --git a/tests/phpunit/includes/WebRequestTest.php b/tests/phpunit/includes/WebRequestTest.php new file mode 100644 index 00000000..1cfbd3fc --- /dev/null +++ b/tests/phpunit/includes/WebRequestTest.php @@ -0,0 +1,88 @@ +<?php + +class WebRequestTest extends MediaWikiTestCase { + /** + * @dataProvider provideDetectServer + */ + function testDetectServer( $expected, $input, $description ) { + $oldServer = $_SERVER; + $_SERVER = $input; + $result = WebRequest::detectServer(); + $_SERVER = $oldServer; + $this->assertEquals( $expected, $result, $description ); + } + + function provideDetectServer() { + return array( + array( + 'http://x', + array( + 'HTTP_HOST' => 'x' + ), + 'Host header' + ), + array( + 'https://x', + array( + 'HTTP_HOST' => 'x', + 'HTTPS' => 'on', + ), + 'Host header with secure' + ), + array( + 'http://x', + array( + 'HTTP_HOST' => 'x', + 'SERVER_PORT' => 80, + ), + 'Default SERVER_PORT', + ), + array( + 'http://x', + array( + 'HTTP_HOST' => 'x', + 'HTTPS' => 'off', + ), + 'Secure off' + ), + array( + 'http://y', + array( + 'SERVER_NAME' => 'y', + ), + 'Server name' + ), + array( + 'http://x', + array( + 'HTTP_HOST' => 'x', + 'SERVER_NAME' => 'y', + ), + 'Host server name precedence' + ), + array( + 'http://[::1]:81', + array( + 'HTTP_HOST' => '[::1]', + 'SERVER_NAME' => '::1', + 'SERVER_PORT' => '81', + ), + 'Apache bug 26005' + ), + array( + 'http://localhost', + array( + 'SERVER_NAME' => '[2001' + ), + 'Kind of like lighttpd per commit message in MW r83847', + ), + array( + 'http://[2a01:e35:2eb4:1::2]:777', + array( + 'SERVER_NAME' => '[2a01:e35:2eb4:1::2]:777' + ), + 'Possible lighttpd environment per bug 14977 comment 13', + ), + ); + } +} diff --git a/tests/phpunit/includes/XmlJsTest.php b/tests/phpunit/includes/XmlJsTest.php new file mode 100644 index 00000000..c5b411fb --- /dev/null +++ b/tests/phpunit/includes/XmlJsTest.php @@ -0,0 +1,9 @@ +<?php +class XmlJs extends MediaWikiTestCase { + public function testConstruction() { + $obj = new XmlJsCode( null ); + $this->assertNull( $obj->value ); + $obj = new XmlJsCode( '' ); + $this->assertSame( $obj->value, '' ); + } +} diff --git a/tests/phpunit/includes/XmlSelectTest.php b/tests/phpunit/includes/XmlSelectTest.php new file mode 100644 index 00000000..bf761e3d --- /dev/null +++ b/tests/phpunit/includes/XmlSelectTest.php @@ -0,0 +1,139 @@ +<?php + +// TODO +class XmlSelectTest extends MediaWikiTestCase { + protected $select; + + protected function setUp() { + $this->select = new XmlSelect(); + } + protected function tearDown() { + $this->select = null; + } + + ### START OF TESTS ### + + public function testConstructWithoutParameters() { + $this->assertEquals( '<select></select>', $this->select->getHTML() ); + } + + /** + * Parameters are $name (false), $id (false), $default (false) + * @dataProvider provideConstructionParameters + */ + public function testConstructParameters( $name, $id, $default, $expected ) { + $this->select = new XmlSelect( $name, $id, $default ); + $this->assertEquals( $expected, $this->select->getHTML() ); + } + + /** + * Provide parameters for testConstructParameters() which use three + * parameters: + * - $name (default: false) + * - $id (default: false) + * - $default (default: false) + * Provides a fourth parameters representing the expected HTML output + * + */ + public function provideConstructionParameters() { + return array( + /** + * Values are set following a 3-bit Gray code where two successive + * values differ by only one value. + * See http://en.wikipedia.org/wiki/Gray_code + */ + # $name $id $default + array( false , false, false, '<select></select>' ), + array( false , false, 'foo', '<select></select>' ), + array( false , 'id' , 'foo', '<select id="id"></select>' ), + array( false , 'id' , false, '<select id="id"></select>' ), + array( 'name', 'id' , false, '<select name="name" id="id"></select>' ), + array( 'name', 'id' , 'foo', '<select name="name" id="id"></select>' ), + array( 'name', false, 'foo', '<select name="name"></select>' ), + array( 'name', false, false, '<select name="name"></select>' ), + ); + } + + # Begin XmlSelect::addOption() similar to Xml::option + public function testAddOption() { + $this->select->addOption( 'foo' ); + $this->assertEquals( '<select><option value="foo">foo</option></select>', $this->select->getHTML() ); + } + public function testAddOptionWithDefault() { + $this->select->addOption( 'foo', true ); + $this->assertEquals( '<select><option value="1">foo</option></select>', $this->select->getHTML() ); + } + public function testAddOptionWithFalse() { + $this->select->addOption( 'foo', false ); + $this->assertEquals( '<select><option value="foo">foo</option></select>', $this->select->getHTML() ); + } + public function testAddOptionWithValueZero() { + $this->select->addOption( 'foo', 0 ); + $this->assertEquals( '<select><option value="0">foo</option></select>', $this->select->getHTML() ); + } + # End XmlSelect::addOption() similar to Xml::option + + public function testSetDefault() { + $this->select->setDefault( 'bar1' ); + $this->select->addOption( 'foo1' ); + $this->select->addOption( 'bar1' ); + $this->select->addOption( 'foo2' ); + $this->assertEquals( +'<select><option value="foo1">foo1</option>' . "\n" . +'<option value="bar1" selected="selected">bar1</option>' . "\n" . +'<option value="foo2">foo2</option></select>', $this->select->getHTML() ); + } + + /** + * Adding default later on should set the correct selection or + * raise an exception. + * To handle this, we need to render the options in getHtml() + */ + public function testSetDefaultAfterAddingOptions() { + $this->select->addOption( 'foo1' ); + $this->select->addOption( 'bar1' ); + $this->select->addOption( 'foo2' ); + $this->select->setDefault( 'bar1' ); # setting default after adding options + $this->assertEquals( +'<select><option value="foo1">foo1</option>' . "\n" . +'<option value="bar1" selected="selected">bar1</option>' . "\n" . +'<option value="foo2">foo2</option></select>', $this->select->getHTML() ); + } + + public function testGetAttributes() { + # create some attributes + $this->select->setAttribute( 'dummy', 0x777 ); + $this->select->setAttribute( 'string', 'euro €' ); + $this->select->setAttribute( 1911, 'razor' ); + + # verify we can retrieve them + $this->assertEquals( + $this->select->getAttribute( 'dummy' ), + 0x777 + ); + $this->assertEquals( + $this->select->getAttribute( 'string' ), + 'euro €' + ); + $this->assertEquals( + $this->select->getAttribute( 1911 ), + 'razor' + ); + + # inexistant keys should give us 'null' + $this->assertEquals( + $this->select->getAttribute( 'I DO NOT EXIT' ), + null + ); + + # verify string / integer + $this->assertEquals( + $this->select->getAttribute( '1911' ), + 'razor' + ); + $this->assertEquals( + $this->select->getAttribute( 'dummy' ), + 0x777 + ); + } +} diff --git a/tests/phpunit/includes/XmlTest.php b/tests/phpunit/includes/XmlTest.php new file mode 100644 index 00000000..fbb498d8 --- /dev/null +++ b/tests/phpunit/includes/XmlTest.php @@ -0,0 +1,304 @@ +<?php + +class XmlTest extends MediaWikiTestCase { + private static $oldLang; + + public function setUp() { + global $wgLang, $wgLanguageCode; + + self::$oldLang = $wgLang; + $wgLanguageCode = 'en'; + $wgLang = Language::factory( $wgLanguageCode ); + } + + public function tearDown() { + global $wgLang, $wgLanguageCode; + $wgLang = self::$oldLang; + $wgLanguageCode = $wgLang->getCode(); + } + + 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' ); + } + + public function testDateMenu( ) { + $curYear = intval(gmdate('Y')); + $prevYear = $curYear - 1; + + $curMonth = intval(gmdate('n')); + $prevMonth = $curMonth - 1; + if( $prevMonth == 0 ) { $prevMonth = 12; } + $nextMonth = $curMonth + 1; + if( $nextMonth == 13 ) { $nextMonth = 1; } + + + $this->assertEquals( + '<label for="year">From year (and earlier):</label> <input name="year" size="4" value="2011" id="year" maxlength="4" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" . +'<option value="1">January</option>' . "\n" . +'<option value="2" selected="selected">February</option>' . "\n" . +'<option value="3">March</option>' . "\n" . +'<option value="4">April</option>' . "\n" . +'<option value="5">May</option>' . "\n" . +'<option value="6">June</option>' . "\n" . +'<option value="7">July</option>' . "\n" . +'<option value="8">August</option>' . "\n" . +'<option value="9">September</option>' . "\n" . +'<option value="10">October</option>' . "\n" . +'<option value="11">November</option>' . "\n" . +'<option value="12">December</option></select>', + Xml::dateMenu( 2011, 02 ), + "Date menu for february 2011" + ); + $this->assertEquals( + '<label for="year">From year (and earlier):</label> <input name="year" size="4" value="2011" id="year" maxlength="4" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" . +'<option value="1">January</option>' . "\n" . +'<option value="2">February</option>' . "\n" . +'<option value="3">March</option>' . "\n" . +'<option value="4">April</option>' . "\n" . +'<option value="5">May</option>' . "\n" . +'<option value="6">June</option>' . "\n" . +'<option value="7">July</option>' . "\n" . +'<option value="8">August</option>' . "\n" . +'<option value="9">September</option>' . "\n" . +'<option value="10">October</option>' . "\n" . +'<option value="11">November</option>' . "\n" . +'<option value="12">December</option></select>', + Xml::dateMenu( 2011, -1), + "Date menu with negative month for 'All'" + ); + $this->assertEquals( + Xml::dateMenu( $curYear, $curMonth ), + Xml::dateMenu( '' , $curMonth ), + "Date menu year is the current one when not specified" + ); + $this->assertEquals( + Xml::dateMenu( $prevYear, $nextMonth ), + Xml::dateMenu( '', $nextMonth ), + "Date menu next month is 11 months ago" + ); + + # @todo FIXME: Please note there is no year there! + $this->assertEquals( + '<label for="year">From year (and earlier):</label> <input name="year" size="4" value="" id="year" maxlength="4" /> <label for="month">From month (and earlier):</label> <select id="month" name="month" class="mw-month-selector"><option value="-1">all</option>' . "\n" . +'<option value="1">January</option>' . "\n" . +'<option value="2">February</option>' . "\n" . +'<option value="3">March</option>' . "\n" . +'<option value="4">April</option>' . "\n" . +'<option value="5">May</option>' . "\n" . +'<option value="6">June</option>' . "\n" . +'<option value="7">July</option>' . "\n" . +'<option value="8">August</option>' . "\n" . +'<option value="9">September</option>' . "\n" . +'<option value="10">October</option>' . "\n" . +'<option value="11">November</option>' . "\n" . +'<option value="12">December</option></select>', + Xml::dateMenu( '', ''), + "Date menu with neither year or month" + ); + } + + # + # 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 testLabelAttributeCanOnlyBeClassOrTitle() { + $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" title="nice tooltip">name</label>', + Xml::label( 'name', 'id', array( 'title' => 'nice tooltip' ) ), + 'label() can get a title attribute' + ); + $this->assertEquals( + '<label for="id" class="nice" title="nice tooltip">name</label>', + Xml::label( 'name', 'id', array( + 'generated' => true, + 'class' => 'nice', + 'title' => 'nice tooltip', + 'anotherattr' => 'value', + ) + ), + 'label() skip all attributes but "class" and "title"' + ); + } + + # + # 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' + ); + } + + function testEncodeJsVarInt() { + $this->assertEquals( + '123456', + Xml::encodeJsVar( 123456 ), + 'encodeJsVar() with int' + ); + } + + function testEncodeJsVarFloat() { + $this->assertEquals( + '1.23456', + Xml::encodeJsVar( 1.23456 ), + 'encodeJsVar() with float' + ); + } + + function testEncodeJsVarIntString() { + $this->assertEquals( + '"123456"', + Xml::encodeJsVar( '123456' ), + 'encodeJsVar() with int-like string' + ); + } + + function testEncodeJsVarFloatString() { + $this->assertEquals( + '"1.23456"', + Xml::encodeJsVar( '1.23456' ), + 'encodeJsVar() with float-like string' + ); + } +} diff --git a/tests/phpunit/includes/ZipDirectoryReaderTest.php b/tests/phpunit/includes/ZipDirectoryReaderTest.php new file mode 100644 index 00000000..f7ca59e2 --- /dev/null +++ b/tests/phpunit/includes/ZipDirectoryReaderTest.php @@ -0,0 +1,79 @@ +<?php + +class ZipDirectoryReaderTest extends MediaWikiTestCase { + var $zipDir, $entries; + + function setUp() { + $this->zipDir = dirname( __FILE__ ) . '/../data/zip'; + } + + function zipCallback( $entry ) { + $this->entries[] = $entry; + } + + function readZipAssertError( $file, $error, $assertMessage ) { + $this->entries = array(); + $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) ); + $this->assertTrue( $status->hasMessage( $error ), $assertMessage ); + } + + function readZipAssertSuccess( $file, $assertMessage ) { + $this->entries = array(); + $status = ZipDirectoryReader::read( "{$this->zipDir}/$file", array( $this, 'zipCallback' ) ); + $this->assertTrue( $status->isOK(), $assertMessage ); + } + + function testEmpty() { + $this->readZipAssertSuccess( 'empty.zip', 'Empty zip' ); + } + + function testMultiDisk0() { + $this->readZipAssertError( 'split.zip', 'zip-unsupported', + 'Split zip error' ); + } + + function testNoSignature() { + $this->readZipAssertError( 'nosig.zip', 'zip-wrong-format', + 'No signature should give "wrong format" error' ); + } + + function testSimple() { + $this->readZipAssertSuccess( 'class.zip', 'Simple ZIP' ); + $this->assertEquals( $this->entries, array( array( + 'name' => 'Class.class', + 'mtime' => '20010115000000', + 'size' => 1, + ) ) ); + } + + function testBadCentralEntrySignature() { + $this->readZipAssertError( 'wrong-central-entry-sig.zip', 'zip-bad', + 'Bad central entry error' ); + } + + function testTrailingBytes() { + $this->readZipAssertError( 'trail.zip', 'zip-bad', + 'Trailing bytes error' ); + } + + function testWrongCDStart() { + $this->readZipAssertError( 'wrong-cd-start-disk.zip', 'zip-unsupported', + 'Wrong CD start disk error' ); + } + + + function testCentralDirectoryGap() { + $this->readZipAssertError( 'cd-gap.zip', 'zip-bad', + 'CD gap error' ); + } + + function testCentralDirectoryTruncated() { + $this->readZipAssertError( 'cd-truncated.zip', 'zip-bad', + 'CD truncated error (should hit unpack() overrun)' ); + } + + function testLooksLikeZip64() { + $this->readZipAssertError( 'looks-like-zip64.zip', 'zip-unsupported', + 'A file which looks like ZIP64 but isn\'t, should give error' ); + } +} diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php new file mode 100644 index 00000000..227555eb --- /dev/null +++ b/tests/phpunit/includes/api/ApiBlockTest.php @@ -0,0 +1,62 @@ +<?php + +/** + * @group Database + */ +class ApiBlockTest extends ApiTestCase { + + function setUp() { + parent::setUp(); + $this->doLogin(); + } + + function getTokens() { + return $this->getTokenList( self::$users['sysop'] ); + } + + function addDBData() { + $user = User::newFromName( 'UTBlockee' ); + + if ( $user->getId() == 0 ) { + $user->addToDatabase(); + $user->setPassword( 'UTBlockeePassword' ); + + $user->saveSettings(); + } + } + + function testMakeNormalBlock() { + + $data = $this->getTokens(); + + $user = User::newFromName( 'UTBlockee' ); + + if ( !$user->getId() ) { + $this->markTestIncomplete( "The user UTBlockee does not exist" ); + } + + if( !isset( $data[0]['query']['pages'] ) ) { + $this->markTestIncomplete( "No block token found" ); + } + + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + + $data = $this->doApiRequest( array( + 'action' => 'block', + 'user' => 'UTBlockee', + 'reason' => BlockTest::REASON, + 'token' => $pageinfo['blocktoken'] ), $data ); + + $block = Block::newFromTarget('UTBlockee'); + + $this->assertTrue( !is_null( $block ), 'Block is valid' ); + + $this->assertEquals( 'UTBlockee', (string)$block->getTarget() ); + $this->assertEquals( 'Some reason', $block->mReason ); + $this->assertEquals( 'infinity', $block->mExpiry ); + + } + +} diff --git a/tests/phpunit/includes/api/ApiPurgeTest.php b/tests/phpunit/includes/api/ApiPurgeTest.php new file mode 100644 index 00000000..db1563e9 --- /dev/null +++ b/tests/phpunit/includes/api/ApiPurgeTest.php @@ -0,0 +1,41 @@ +<?php + +/** + * @group Database + */ +class ApiPurgeTest extends ApiTestCase { + + function setUp() { + parent::setUp(); + $this->doLogin(); + } + + function testPurgeMainPage() { + + if ( !Title::newFromText( 'UTPage' )->exists() ) { + $this->markTestIncomplete( "The article [[UTPage]] does not exist" ); + } + + $somePage = mt_rand(); + + $data = $this->doApiRequest( array( + 'action' => 'purge', + 'titles' => 'UTPage|' . $somePage . '|%5D' ) ); + + $this->assertArrayHasKey( 'purge', $data[0] ); + + $this->assertArrayHasKey( 0, $data[0]['purge'] ); + $this->assertArrayHasKey( 'purged', $data[0]['purge'][0] ); + $this->assertEquals( 'UTPage', $data[0]['purge'][0]['title'] ); + + $this->assertArrayHasKey( 1, $data[0]['purge'] ); + $this->assertArrayHasKey( 'missing', $data[0]['purge'][1] ); + $this->assertEquals( $somePage, $data[0]['purge'][1]['title'] ); + + $this->assertArrayHasKey( 2, $data[0]['purge'] ); + $this->assertArrayHasKey( 'invalid', $data[0]['purge'][2] ); + $this->assertEquals( '%5D', $data[0]['purge'][2]['title'] ); + + } + +} diff --git a/tests/phpunit/includes/api/ApiQueryTest.php b/tests/phpunit/includes/api/ApiQueryTest.php new file mode 100644 index 00000000..114eadf3 --- /dev/null +++ b/tests/phpunit/includes/api/ApiQueryTest.php @@ -0,0 +1,67 @@ +<?php + +/** + * @group Database + */ +class ApiQueryTest extends ApiTestCase { + + function setUp() { + parent::setUp(); + $this->doLogin(); + } + + function testTitlesGetNormalized() { + + global $wgMetaNamespace; + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'titles' => 'Project:articleA|article_B' ) ); + + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'normalized', $data[0]['query'] ); + + $this->assertEquals( + array( + 'from' => 'Project:articleA', + 'to' => $wgMetaNamespace . ':ArticleA' + ), + $data[0]['query']['normalized'][0] + ); + + $this->assertEquals( + array( + 'from' => 'article_B', + 'to' => 'Article B' + ), + $data[0]['query']['normalized'][1] + ); + + } + + function testTitlesAreRejectedIfInvalid() { + $title = false; + while( !$title || Title::newFromText( $title )->exists() ) { + $title = md5( mt_rand( 0, 10000 ) + rand( 0, 999000 ) ); + } + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'titles' => $title . '|Talk:' ) ); + + + $this->assertArrayHasKey( 'query', $data[0] ); + $this->assertArrayHasKey( 'pages', $data[0]['query'] ); + $this->assertEquals( 2, count( $data[0]['query']['pages'] ) ); + + $this->assertArrayHasKey( -2, $data[0]['query']['pages'] ); + $this->assertArrayHasKey( -1, $data[0]['query']['pages'] ); + + $this->assertArrayHasKey( 'missing', $data[0]['query']['pages'][-2] ); + $this->assertArrayHasKey( 'invalid', $data[0]['query']['pages'][-1] ); + + + } + +} diff --git a/tests/phpunit/includes/api/ApiTest.php b/tests/phpunit/includes/api/ApiTest.php new file mode 100644 index 00000000..a587e6b1 --- /dev/null +++ b/tests/phpunit/includes/api/ApiTest.php @@ -0,0 +1,277 @@ +<?php + +/** + * @group Database + */ +class ApiTest extends ApiTestCase { + + 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->assertNotInternalType( "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::$users['sysop']->password, + ) ); + $this->assertEquals( 'NoName', $data[0]['login']['result'] ); + } + + function testApiLoginBadPass() { + global $wgServer; + + $user = self::$users['sysop']; + $user->user->logOut(); + + 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->assertNotInternalType( "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" => "badnowayinhell", + ) + ); + + $result = $ret[0]; + + $this->assertNotInternalType( "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::$users['sysop']; + $user->user->logOut(); + + $ret = $this->doApiRequest( array( + "action" => "login", + "lgname" => $user->username, + "lgpassword" => $user->password, + ) + ); + + $result = $ret[0]; + $this->assertNotInternalType( "bool", $result ); + $this->assertNotInternalType( "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->assertNotInternalType( "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' ); + } + $user = self::$users['sysop']; + + $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml", + array( "method" => "POST", + "postData" => array( + "lgname" => $user->username, + "lgpassword" => $user->password ) ) ); + $req->execute(); + + libxml_use_internal_errors( true ); + $sxe = simplexml_load_string( $req->getContent() ); + $this->assertNotInternalType( "bool", $sxe ); + $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + $this->assertNotInternalType( "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" => $user->username, + "lgpassword" => $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=' . $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->assertNotInternalType( "bool", $sxe ); + $this->assertThat( $sxe, $this->isInstanceOf( "SimpleXMLElement" ) ); + $a = $sxe->query[0]->pages[0]->page[0]->attributes(); + } + + function testRunLogin() { + $sysopUser = self::$users['sysop']; + $data = $this->doApiRequest( array( + 'action' => 'login', + 'lgname' => $sysopUser->username, + 'lgpassword' => $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" => $sysopUser->username, + "lgpassword" => $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 ( self::$users as $user ) { + $this->runTokenTest( $user ); + } + } + + function runTokenTest( $user ) { + + $data = $this->getTokenList( $user ); + + $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; + } +} diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php new file mode 100644 index 00000000..2917c880 --- /dev/null +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -0,0 +1,139 @@ +<?php + +abstract class ApiTestCase extends MediaWikiLangTestCase { + /** + * @var Array of ApiTestUser + */ + public static $users; + protected static $apiUrl; + + function setUp() { + global $wgContLang, $wgAuth, $wgMemc, $wgRequest, $wgUser, $wgServer; + + parent::setUp(); + self::$apiUrl = $wgServer . wfScript( 'api' ); + $wgMemc = new EmptyBagOStuff(); + $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, $appendModule = false ) { + if ( is_null( $session ) ) { + $session = array(); + } + + $request = new FauxRequest( $params, true, $session ); + $module = new ApiMain( $request, true ); + $module->execute(); + + $results = array( $module->getResultData(), $request, $request->getSessionArray() ); + if( $appendModule ) { + $results[] = $module; + } + + return $results; + } + + /** + * 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'] ) . User::EDIT_TOKEN_SUFFIX; + return $this->doApiRequest( $params, $session ); + } else { + throw new Exception( "request data not in right format" ); + } + } + + protected function doLogin() { + $data = $this->doApiRequest( array( + 'action' => 'login', + 'lgname' => self::$users['sysop']->username, + 'lgpassword' => self::$users['sysop']->password ) ); + + $token = $data[0]['login']['token']; + + $data = $this->doApiRequest( array( + 'action' => 'login', + 'lgtoken' => $token, + 'lgname' => self::$users['sysop']->username, + 'lgpassword' => self::$users['sysop']->password + ), $data ); + + return $data; + } + + protected function getTokenList( $user ) { + $GLOBALS['wgUser'] = $user->user; + $data = $this->doApiRequest( array( + 'action' => 'query', + 'titles' => 'Main Page', + 'intoken' => 'edit|delete|protect|move|block|unblock', + 'prop' => 'info' ) ); + return $data; + } +} + +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(); + } +} + +class MockApi extends ApiBase { + public function execute() { } + public function getVersion() { } + + public function __construct() { } + + public function getAllowedParams() { + return array( + 'filename' => null, + 'enablechunks' => false, + 'sessionkey' => null, + ); + } +} diff --git a/tests/phpunit/includes/api/ApiTestCaseUpload.php b/tests/phpunit/includes/api/ApiTestCaseUpload.php new file mode 100644 index 00000000..e51e7214 --- /dev/null +++ b/tests/phpunit/includes/api/ApiTestCaseUpload.php @@ -0,0 +1,114 @@ +<?php + +/** + * * Abstract class to support upload tests + */ + +abstract class ApiTestCaseUpload extends ApiTestCase { + /** + * Fixture -- run before every test + */ + public function setUp() { + global $wgEnableUploads, $wgEnableAPI; + parent::setUp(); + + $wgEnableUploads = true; + $wgEnableAPI = true; + wfSetupSession(); + + $this->clearFakeUploads(); + } + + /** + * 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( $title->getText(), 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(); + } + + + + +} diff --git a/tests/phpunit/includes/api/ApiTestUser.php b/tests/phpunit/includes/api/ApiTestUser.php new file mode 100644 index 00000000..df60682f --- /dev/null +++ b/tests/phpunit/includes/api/ApiTestUser.php @@ -0,0 +1,59 @@ +<?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(); + + } + +} diff --git a/tests/phpunit/includes/api/ApiUploadTest.php b/tests/phpunit/includes/api/ApiUploadTest.php new file mode 100644 index 00000000..5c929784 --- /dev/null +++ b/tests/phpunit/includes/api/ApiUploadTest.php @@ -0,0 +1,433 @@ +<?php + +/** + * @group Database + */ + +/** + * 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: port the other Upload tests, and other API tests to this framework + +require_once( 'ApiTestCaseUpload.php' ); + +/** + * @group Database + * + * This is pretty sucky... needs to be prettified. + */ +class ApiUploadTest extends ApiTestCaseUpload { + + /** + * 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, , $session ) = $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, $session ); + $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 filekey, 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']['filekey'] ) ); + $this->assertEquals( $result['upload']['sessionkey'], $result['upload']['filekey'] ); + $filekey = $result['upload']['filekey']; + + // 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', + 'filekey' => $filekey, + '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 ); + } +} + diff --git a/tests/phpunit/includes/api/ApiWatchTest.php b/tests/phpunit/includes/api/ApiWatchTest.php new file mode 100644 index 00000000..3c7ff304 --- /dev/null +++ b/tests/phpunit/includes/api/ApiWatchTest.php @@ -0,0 +1,179 @@ +<?php + +/** + * @group Database + * @todo This test suite is severly broken and need a full review + */ +class ApiWatchTest extends ApiTestCase { + + function setUp() { + parent::setUp(); + $this->doLogin(); + } + + function getTokens() { + return $this->getTokenList( self::$users['sysop'] ); + } + + /** + * @group Broken + */ + function testWatchEdit() { + + $data = $this->getTokens(); + + $keys = array_keys( $data[0]['query']['pages'] ); + $key = array_pop( $keys ); + $pageinfo = $data[0]['query']['pages'][$key]; + + $data = $this->doApiRequest( array( + 'action' => 'edit', + 'title' => 'UTPage', + '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 = $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; + } + + /** + * @group Broken + */ + function testWatchProtect() { + + $data = $this->getTokens(); + + $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' => 'UTPage', + '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] ); + } + + + function testGetRollbackToken() { + + $data = $this->getTokens(); + + if ( !Title::newFromText( 'UTPage' )->exists() ) { + $this->markTestIncomplete( "The article [[UTPage]] does not exist" ); + } + + $data = $this->doApiRequest( array( + 'action' => 'query', + 'prop' => 'revisions', + 'titles' => 'UTPage', + '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 (UTPage) 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 + * @group Broken + */ + 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' => 'UTPage', + 'user' => $pageinfo['user'], + 'token' => $pageinfo['rollbacktoken'], + 'watchlist' => 'watch' ), $data ); + } catch( UsageException $ue ) { + if( $ue->getCodeString() == 'onlyauthor' ) { + $this->markTestIncomplete( "Only one author to 'UTPage', cannot test rollback" ); + } else { + $this->fail( "Received error '" . $ue->getCodeString() . "'" ); + } + } + + $this->assertArrayHasKey( 'rollback', $data[0] ); + $this->assertArrayHasKey( 'title', $data[0]['rollback'] ); + } + + /** + * @group Broken + */ + function testWatchDelete() { + + $data = $this->getTokens(); + + $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' => 'UTPage' ), $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' ); + } +} diff --git a/tests/phpunit/includes/api/RandomImageGenerator.php b/tests/phpunit/includes/api/RandomImageGenerator.php new file mode 100644 index 00000000..ae349978 --- /dev/null +++ b/tests/phpunit/includes/api/RandomImageGenerator.php @@ -0,0 +1,473 @@ +<?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 triangles) + * + * 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 $shapesToDraw = 5; + private $imageWriteMethod; + + /** + * Orientations: 0th row, 0th column, EXIF orientation code, rotation 2x2 matrix that is opposite of orientation + * n.b. we do not handle the 'flipped' orientations, which is why there is no entry for 2, 4, 5, or 7. Those + * seem to be rare in real images anyway + * (we also would need a non-symmetric shape for the images to test those, like a letter F) + */ + private static $orientations = array( + array( + '0thRow' => 'top', + '0thCol' => 'left', + 'exifCode' => 1, + 'counterRotation' => array( array( 1, 0 ), array( 0, 1 ) ) + ), + array( + '0thRow' => 'bottom', + '0thCol' => 'right', + 'exifCode' => 3, + 'counterRotation' => array( array( -1, 0 ), array( 0, -1 ) ) + ), + array( + '0thRow' => 'right', + '0thCol' => 'top', + 'exifCode' => 6, + 'counterRotation' => array( array( 0, 1 ), array( 1, 0 ) ) + ), + array( + '0thRow' => 'left', + '0thCol' => 'bottom', + 'exifCode' => 8, + 'counterRotation' => array( array( 0, -1 ), array( -1, 0 ) ) + ) + ); + + + public function __construct( $options = array() ) { + foreach ( array( 'dictionaryFile', 'minWidth', 'minHeight', 'maxHeight', 'shapesToDraw' ) 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', + dirname( __FILE__ ) . '/words.txt' ) + 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" ); + } + + if ( !class_exists( 'Imagick' ) ) { + throw new Exception( 'No Imagick extension' ); + } + global $wgExiv2Command; + if ( !$wgExiv2Command || !is_executable( $wgExiv2Command ) ) { + throw new Exception( 'exiv2 not executable or $wgExiv2Command not set' ); + } + } + + /** + * 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 ); + $imageWriteMethod = $this->getImageWriteMethod( $format ); + foreach( $filenames as $filename ) { + $this->{$imageWriteMethod}( $this->getImageSpec(), $format, $filename ); + } + return $filenames; + } + + + /** + * Figure out how we write images. This is a factor of both format and the local system + * @param $format (a typical extension like 'svg', 'jpg', etc.) + */ + function getImageWriteMethod( $format ) { + global $wgUseImageMagick, $wgImageMagickConvertCommand; + if ( $format === 'svg' ) { + return 'writeSvg'; + } else { + // figure out how to write images + if ( class_exists( 'Imagick' ) ) { + return 'writeImageWithApi'; + } elseif ( $wgUseImageMagick && $wgImageMagickConvertCommand && is_executable( $wgImageMagickConvertCommand ) ) { + return 'writeImageWithCommandLine'; + } + } + throw new Exception( "RandomImageGenerator: could not find a suitable method to write images in '$format' format" ); + } + + /** + * 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 upward pointing triangles 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->shapesToDraw; $i++ ) { + $radius = mt_rand( 0, $diagonalLength / 4 ); + if ( $radius == 0 ) { + continue; + } + $originX = mt_rand( -1 * $radius, $spec['width'] + $radius ); + $originY = mt_rand( -1 * $radius, $spec['height'] + $radius ); + $angle = mt_rand( 0, ( 3.141592/2 ) * $radius ) / $radius; + $legDeltaX = round( $radius * sin( $angle ) ); + $legDeltaY = round( $radius * cos( $angle ) ); + + $draw = array(); + $draw['fill'] = $this->getRandomColor(); + $draw['shape'] = array( + array( 'x' => $originX, 'y' => $originY - $radius ), + array( 'x' => $originX + $legDeltaX, 'y' => $originY + $legDeltaY ), + array( 'x' => $originX - $legDeltaX, 'y' => $originY + $legDeltaY ), + array( 'x' => $originX, 'y' => $originY - $radius ) + ); + $draws[] = $draw; + + } + + $spec['draws'] = $draws; + + return $spec; + } + + /** + * Given array( array('x' => 10, 'y' => 20), array( 'x' => 30, y=> 5 ) ) + * returns "10,20 30,5" + * Useful for SVG and imagemagick command line arguments + * @param $shape: Array of arrays, each array containing x & y keys mapped to numeric values + * @return string + */ + static function shapePointsToString( $shape ) { + $points = array(); + foreach ( $shape as $point ) { + $points[] = $point['x'] . ',' . $point['y']; + } + return join( " ", $points ); + } + + /** + * Based on image specification, write a very simple SVG file to disk. + * Ignores the background spec because transparency is cool. :) + * @param $spec: spec describing background and shapes to draw + * @param $format: file format to write (which is obviously always svg here) + * @param $filename: filename to write to + */ + public function writeSvg( $spec, $format, $filename ) { + $svg = new SimpleXmlElement( '<svg/>' ); + $svg->addAttribute( 'xmlns', 'http://www.w3.org/2000/svg' ); + $svg->addAttribute( 'version', '1.1' ); + $svg->addAttribute( 'width', $spec['width'] ); + $svg->addAttribute( 'height', $spec['height'] ); + $g = $svg->addChild( 'g' ); + foreach ( $spec['draws'] as $drawSpec ) { + $shape = $g->addChild( 'polygon' ); + $shape->addAttribute( 'fill', $drawSpec['fill'] ); + $shape->addAttribute( 'points', self::shapePointsToString( $drawSpec['shape'] ) ); + }; + if ( ! $fh = fopen( $filename, 'w' ) ) { + throw new Exception( "couldn't open $filename for writing" ); + } + fwrite( $fh, $svg->asXML() ); + if ( !fclose($fh) ) { + throw new Exception( "couldn't close $filename" ); + } + } + + /** + * 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 ) { + // this is a hack because I can't get setImageOrientation() to work. See below. + global $wgExiv2Command; + + $image = new Imagick(); + /** + * If the format is 'jpg', will also add a random orientation -- the image will be drawn rotated with triangle points + * facing in some direction (0, 90, 180 or 270 degrees) and a countering rotation should turn the triangle points upward again + */ + $orientation = self::$orientations[0]; // default is normal orientation + if ( $format == 'jpg' ) { + $orientation = self::$orientations[ array_rand( self::$orientations ) ]; + $spec = self::rotateImageSpec( $spec, $orientation['counterRotation'] ); + } + + $image->newImage( $spec['width'], $spec['height'], new ImagickPixel( $spec['fill'] ) ); + + foreach ( $spec['draws'] as $drawSpec ) { + $draw = new ImagickDraw(); + $draw->setFillColor( $drawSpec['fill'] ); + $draw->polygon( $drawSpec['shape'] ); + $image->drawImage( $draw ); + } + + $image->setImageFormat( $format ); + + // this doesn't work, even though it's documented to do so... + // $image->setImageOrientation( $orientation['exifCode'] ); + + $image->writeImage( $filename ); + + // because the above setImageOrientation call doesn't work... nor can I get an external imagemagick binary to do this either... + // hacking this for now (only works if you have exiv2 installed, a program to read and manipulate exif) + if ( $wgExiv2Command ) { + $cmd = wfEscapeShellArg( $wgExiv2Command ) + . " -M " + . wfEscapeShellArg( "set Exif.Image.Orientation " . $orientation['exifCode'] ) + . " " + . wfEscapeShellArg( $filename ); + + $retval = 0; + $err = wfShellExec( $cmd, $retval ); + if ( $retval !== 0 ) { + print "Error with $cmd: $retval, $err\n"; + } + } + + + } + + /** + * Given an image specification, produce rotated version + * This is used when simulating a rotated image capture with EXIF orientation + * @param $spec Object returned by getImageSpec + * @param $matrix 2x2 transformation matrix + * @return transformed Spec + */ + private static function rotateImageSpec( &$spec, $matrix ) { + $tSpec = array(); + $dims = self::matrixMultiply2x2( $matrix, $spec['width'], $spec['height'] ); + $correctionX = 0; + $correctionY = 0; + if ( $dims['x'] < 0 ) { + $correctionX = abs( $dims['x'] ); + } + if ( $dims['y'] < 0 ) { + $correctionY = abs( $dims['y'] ); + } + $tSpec['width'] = abs( $dims['x'] ); + $tSpec['height'] = abs( $dims['y'] ); + $tSpec['fill'] = $spec['fill']; + $tSpec['draws'] = array(); + foreach( $spec['draws'] as $draw ) { + $tDraw = array( + 'fill' => $draw['fill'], + 'shape' => array() + ); + foreach( $draw['shape'] as $point ) { + $tPoint = self::matrixMultiply2x2( $matrix, $point['x'], $point['y'] ); + $tPoint['x'] += $correctionX; + $tPoint['y'] += $correctionY; + $tDraw['shape'][] = $tPoint; + } + $tSpec['draws'][] = $tDraw; + } + return $tSpec; + } + + /** + * Given a matrix and a pair of images, return new position + * @param $matrix: 2x2 rotation matrix + * @param $x: x-coordinate number + * @param $y: y-coordinate number + * @return Array transformed with properties x, y + */ + private static function matrixMultiply2x2( $matrix, $x, $y ) { + return array( + 'x' => $x * $matrix[0][0] + $y * $matrix[0][1], + 'y' => $x * $matrix[1][0] + $y * $matrix[1][1] + ); + } + + + /** + * 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) polygon 41,39 44,57 50,57 41,39' \ + * -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 shapes 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']; + $polygon = self::shapePointsToString( $draw['shape'] ); + $drawCommand = "fill $fill polygon $polygon"; + $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 or SVG, 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; + } + +} diff --git a/tests/phpunit/includes/api/format/ApiFormatPhpTest.php b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php new file mode 100644 index 00000000..8209f591 --- /dev/null +++ b/tests/phpunit/includes/api/format/ApiFormatPhpTest.php @@ -0,0 +1,19 @@ +<?php + +/** + * @group API + * @group Database + */ +class ApiFormatPhpTest extends ApiFormatTestBase { + + function testValidPhpSyntax() { + + $data = $this->apiRequest( 'php', array( 'action' => 'query', 'meta' => 'siteinfo' ) ); + + $this->assertInternalType( 'array', unserialize( $data ) ); + $this->assertGreaterThan( 0, count( (array) $data ) ); + + + } + +} diff --git a/tests/phpunit/includes/api/format/ApiFormatTestBase.php b/tests/phpunit/includes/api/format/ApiFormatTestBase.php new file mode 100644 index 00000000..a0b7b020 --- /dev/null +++ b/tests/phpunit/includes/api/format/ApiFormatTestBase.php @@ -0,0 +1,22 @@ +<?php + +abstract class ApiFormatTestBase extends ApiTestCase { + protected function apiRequest( $format, $params, $data = null ) { + $data = parent::doApiRequest( $params, $data, true ); + + $module = $data[3]; + + $printer = $module->createPrinterByName( $format ); + $printer->setUnescapeAmps( false ); + + $printer->initPrinter( false ); + + ob_start(); + $printer->execute(); + $out = ob_get_clean(); + + $printer->closePrinter(); + + return $out; + } +} diff --git a/tests/phpunit/includes/api/generateRandomImages.php b/tests/phpunit/includes/api/generateRandomImages.php new file mode 100644 index 00000000..f3a14e5b --- /dev/null +++ b/tests/phpunit/includes/api/generateRandomImages.php @@ -0,0 +1,47 @@ +<?php +/** + * Bootstrapping for test image file generation + * + * @file + */ + +// Evaluate the include path relative to this file +$IP = dirname( dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) ); + +// Start up MediaWiki in command-line mode +require_once( "$IP/maintenance/Maintenance.php" ); +require("RandomImageGenerator.php"); + +class GenerateRandomImages extends Maintenance { + + public function execute() { + + $getOptSpec = array( + 'dictionaryFile::', + 'minWidth::', + 'maxWidth::', + 'minHeight::', + 'maxHeight::', + 'shapesToDraw::', + 'shape::', + + '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 ); + } +} + +$maintClass = 'GenerateRandomImages'; +require( RUN_MAINTENANCE_IF_MAIN ); + + diff --git a/tests/phpunit/includes/api/words.txt b/tests/phpunit/includes/api/words.txt new file mode 100644 index 00000000..7ce23ee3 --- /dev/null +++ b/tests/phpunit/includes/api/words.txt @@ -0,0 +1,1000 @@ +Andaquian +Anoplanthus +Araquaju +Astrophyton +Avarish +Batonga +Bdellidae +Betoyan +Bismarck +Britishness +Carmen +Chatillon +Clement +Coryphaena +Croton +Cyrillianism +Dagomba +Decimus +Dichorisandra +Duculinae +Empusa +Escallonia +Fathometer +Fon +Fundulinae +Gadswoons +Gederathite +Gemini +Gerbera +Gregarinida +Gyracanthus +Halopsychidae +Hasidim +Hemerobius +Ichthyosauridae +Iscariot +Jeames +Jesuitry +Jovian +Judaization +Katie +Ladin +Langhian +Lapithaean +Lisette +Macrochira +Malaxis +Malvastrum +Maranhao +Marxian +Maurist +Metrosideros +Micky +Microsporon +Odacidae +Ophiuchid +Osmorhiza +Paguma +Palesman +Papayaceae +Pastinaca +Philoxenian +Pleurostigma +Rarotongan +Rhodoraceae +Rong +Saho +Sanyakoan +Sardanapalian +Sauropoda +Sedentaria +Shambu +Shukulumbwe +Solonian +Spaniardization +Spirochaetaceae +Stomatopoda +Stratiotes +Taiwanhemp +Titanically +Venetianed +Victrola +Yuman +abatis +abaton +abjoint +acanthoma +acari +acceptance +actinography +acuteness +addiment +adelite +adelomorphic +adelphogamy +adipocele +aelurophobia +affined +aflaunt +agathokakological +aischrolatreia +alarmedly +alebench +aleurone +allelotropic +allerion +alloplastic +allowable +alternacy +alternariose +altricial +ambitionist +amendment +amiableness +amicableness +ammo +amortizable +anchorate +anemometrically +angelocracy +angelological +anodal +anomalure +antedate +antiagglutinin +antirationalist +antiscorbutic +antisplasher +antithesize +antiunionist +antoecian +apolegamic +appropriation +archididascalian +archival +arteriophlebotomy +articulable +asseveration +assignation +atelo +atrienses +atrophy +atterminement +atypic +automower +aveloz +awrist +azteca +bairnteam +balsamweed +bannerman +beardy +becry +beek +beggarwise +bescab +bestness +bethel +bewildering +bibliophilism +bitterblain +blakeberyed +boccarella +bocedization +boobyalla +bourbon +bowbent +bowerbird +brachygnathous +brail +branchiferous +brelaw +brew +brideweed +bridgeable +brombenzamide +buddler +burbankian +burr +buskin +cacochymical +calefactory +caliper +canaliculus +candidature +canellaceous +canniness +canning +cantilene +carbonatation +carthamic +caseum +caudated +causationist +ceruleite +chalder +chalta +charmel +chekan +chillness +chirogymnast +chirpling +chlorinous +cholanthrene +chondroblast +chromatography +chromophilous +chronical +cicatrice +cinchonine +city +clubbing +coastal +coaxially +coercible +coeternity +coff +coinventor +collyba +combinator +complanation +comprehensibility +conchuela +congenital +context +contranatural +corallum +cordately +cornupete +corolliferous +coroneted +corticosterone +coseat +cottage +crocetin +crossleted +crottels +curvedness +cycadeous +cyclism +cylindrically +cynanche +cyrtoceratitic +cystospasm +danceress +dancette +dawny +daydreamy +debar +decarburization +decorousness +decrepitness +delirious +deozonizer +dermatosis +desma +deutencephalic +diacetate +diarthrodial +diathermy +dicolic +dimastigate +dimidiation +dipetto +disavowable +disintrench +disman +dismay +disorder +disoxygenation +dithionous +dogman +dragonfly +dramatical +drawspan +drubbly +drunk +duskly +ecderonic +ectocuniform +ectocyst +ehrwaldite +electrocute +elemicin +embracing +emotionality +enactment +enamor +enclave +endameba +endochylous +endocrinologist +endolymph +endothecal +entasia +epigeous +episcopicide +epitrichial +erminee +erraticalness +eruptivity +erythrocytoschisis +esperance +estuous +eucrystalline +eugeny +evacuant +everbloomer +evocation +exarchateship +exasperate +excorticate +excrementary +exile +expandedly +exponency +expressionist +expulsion +extemporary +extollation +extortive +extrabulbar +extraprostatic +facticide +fairer +fakery +fasibitikite +fatiscent +fearless +febrifuge +ferie +fibrousness +fingered +fisheye +flagpole +flagrantness +fleche +fluidism +folliculin +footbreadth +forceps +forecontrive +forthbring +foveated +fuchsin +fungicidal +funori +gamelang +gametically +garvanzo +gasoliner +gastrophile +germproof +gerontism +gigantical +glaciology +godmotherhood +gooseherd +gordunite +gove +gracilis +greathead +grieveship +guidable +gyromancy +gyrostat +habitus +hailweed +handhole +hangalai +haznadar +heliced +hemihypertrophy +hemimorphic +hemistrumectomy +heptavalent +heptite +herbalist +herpetology +hesperid +hexacarbon +hieromnemon +hobbyless +holodactylic +homoeoarchy +hopperings +hospitable +houseboat +huh +huntedly +hydroponics +hydrosomal +hyperdactylia +hyperperistalsis +hypogeocarpous +ideogram +idiopathical +illegitimate +imambarah +impotently +improvise +impuberal +inaccurately +incarnant +inchoation +incliner +incredulous +indiscriminateness +indulgenced +inebriation +inexpressiveness +infibulate +inflectedness +iniome +ink +inquietly +insaturable +insinuative +instiller +institutive +insultproof +interactionist +intercensal +interpenetrable +intertranspicuous +intrinsicality +inwards +iridiocyte +iridoparalysis +irreportable +isoprene +isosmotic +izard +jacuaru +jaculative +jerkined +joe +joyous +julienne +justicehood +kali +kalidium +katha +kathal +keelage +keratomycosis +khaki +khedival +kinkily +knife +kolo +kraken +kwarta +labba +labber +laboress +lacunar +latch +lauric +lawter +lectotype +leeches +legible +lepidosteoid +leucobasalt +leverer +libellate +limnimeter +lithography +lithotypic +locomotor +logarithmetically +logistician +lyncine +lysogenesis +machan +macromyelon +maharana +mandibulate +manganapatite +marchpane +mas +masochistic +mastaba +matching +meditatively +megalopolitan +melaniline +mentum +mercaptides +mestome +metasomatism +meterless +micronuclear +micropetalous +microreaction +microsporophore +mileway +milliarium +millisecond +misbind +miscollocation +misreader +modernicide +modification +modulant +monkfish +monoamino +monocarbide +monographical +morphinomaniac +mullein +munge +mutilate +mycophagist +myelosarcoma +myospasm +myriadly +nagaika +naphthionate +natant +naviculaeform +nayward +neallotype +necrophilia +nectared +neigher +neogamous +neurodynia +neurorthopteran +nidation +nieceship +nitrobacteria +nitrosification +nogheaded +nonassertive +noneuphonious +nonextant +nonincrease +nonintermittent +nonmetallic +nonprehensile +nonremunerative +nonsocial +nonvesting +noontime +noreaster +nounal +nub +nucleoplasm +nullisome +numero +numerous +oblongatal +observe +obtusilingual +obvert +occipitoatlantal +oceanside +ochlophobist +odontiasis +opalescence +opticon +oraculousness +orarium +organically +orthopedically +ostosis +overadvance +overbuilt +overdiscouragement +overdoer +overhardy +overjocular +overmagnify +overofficered +overpotent +overprizer +overrunner +overshrink +oversimply +oversplash +ovology +oxskin +oxychloride +oxygenant +ozokerite +pactional +palaeoanthropography +palaeographical +palaeopsychology +palliasse +palpebral +pandaric +pantelegraph +papicolist +papulate +parakinetic +parasitism +parochialic +parochialize +passionlike +patch +paucidentate +pawnbrokeress +pecite +pecky +pedipulation +pellitory +perfilograph +periblast +perigemmal +periost +periplus +perishable +periwig +permansive +persistingly +persymmetrical +phantom +phasmatrope +philocaly +philogyny +philosophister +philotherianism +phorology +phototrophic +phrator +phratral +phthisipneumony +physogastry +phytologic +phytoptid +pianograph +picqueter +piculet +pigeoner +pimaric +pinesap +pist +planometer +platano +playful +plea +pleuropneumonic +plowwoman +plump +pluviographical +pneumocele +podophthalmate +polyad +polythalamian +poppyhead +portamento +portmanteau +portraitlike +possible +potassamide +powderer +praepubis +preanesthetic +prebarbaric +predealer +predomination +prefactory +preirrigational +prelector +presbytership +presecure +preservable +prespecialist +preventionism +prewound +princely +priorship +proannexationist +proanthropos +probeable +probouleutic +profitless +proplasma +prosectorial +protecting +protochemistry +protosulphate +pseudoataxia +psilology +psychoneurotic +pterygial +publicist +purgation +purplishness +putatively +pyracene +pyrenomycete +pyromancy +pyrophone +quadroon +quailhead +qualifier +quaternal +rabblelike +rambunctious +rapidness +ratably +rationalism +razor +reannoy +recultivation +regulable +reimplant +reimposition +reimprison +reinjure +reinspiration +reintroduce +remantle +reprehensibility +reptant +require +resteal +restful +returnability +revisableness +rewash +rewhirl +reyield +rhizotomy +rhodamine +rigwiddie +rimester +ripper +rippet +rockish +rockwards +rollicky +roosters +rooted +rosal +rozum +saccharated +sagamore +sagy +salesmanship +salivous +sallet +salta +saprostomous +satiation +sauropsid +sawarra +sawback +scabish +scabrate +scampavia +scientificophilosophical +scirrosity +scoliometer +scolopendrelloid +secantly +seignioral +semibull +semic +seminarianism +semiped +semiprivate +semispherical +semispontaneous +seneschal +septendecimal +serotherapist +servation +sesquisulphuret +severish +sextipartite +sextubercular +shipyard +shuckpen +siderosis +silex +sillyhow +silverbelly +silverbelly +simulacrum +sisham +sixte +skeiner +skiapod +slopped +slubby +smalts +sockmaker +solute +somethingness +somnify +southwester +spathilla +spectrochemical +sphagnology +spinales +spiriting +spirling +spirochetemia +spreadboard +spurflower +squawdom +squeezing +staircase +staker +stamphead +statolith +stekan +stellulate +stinker +stomodaea +streamingly +strikingness +strouthocamelian +stuprum +subacutely +subboreal +subcontractor +subendorsement +subprofitable +subserviate +subsneer +subungual +sucuruju +sugan +sulphocarbolate +summerwood +superficialist +superinference +superregenerative +supplicate +suspendible +synchronizer +syntectic +tachyglossate +tailless +taintment +takingly +taletelling +tarpon +tasteful +taxeater +taxy +teache +teachless +teg +tegmen +teletyper +temperable +ten +tenent +teskere +testes +thallogen +thapsia +thewness +thickety +thiobacteria +thorniness +throwing +thyroprivic +tinnitus +tocalote +tolerationist +tonalamatl +torvous +totality +tottering +toug +tracheopathia +tragedical +translucent +trifoveolate +trilaurin +trophoplasmatic +trunkless +turbanless +turnpiker +twangle +twitterboned +ultraornate +umbilication +unabatingly +unabjured +unadequateness +unaffectedness +unarriving +unassorted +unattacked +unbenumbed +unboasted +unburning +uncensorious +uncongested +uncontemnedly +uncontemporary +uncrook +uncrystallizability +uncurb +uncustomariness +underbillow +undercanopy +underestimation +underhanging +underpetticoated +underpropped +undersole +understocking +underworld +undevout +undisappointing +undistinctive +unfiscal +unfluted +unfreckled +ungentilize +unglobe +unhelped +unhomogeneously +unifoliate +uninflammable +uninterrogated +unisonal +unkindled +unlikeableness +unlisty +unlocked +unmoving +unmultipliable +unnestled +unnoticed +unobservable +unobviated +unoffensively +unofficerlike +unpoetic +unpractically +unquestionableness +unrehearsed +unrevised +unrhetorical +unsadden +unsaluting +unscriptural +unseeking +unshowed +unsolicitous +unsprouted +unsubjective +unsubsidized +unsymbolic +untenant +unterrified +untranquil +untraversed +untrusty +untying +unwillful +unwinding +upspring +uptwist +urachovesical +uropygial +vagabondism +varicoid +varletess +vasal +ventrocaudal +verisimilitude +vermigerous +vibrometer +viminal +virus +vocationalism +voguey +vulnerability +waggle +wamblingly +warmus +waxer +waying +wedgeable +wellmaker +whomever +wigged +witchlike +wokas +woodrowel +woodsman +woolding +xanthelasmic +xiphosternum +yachtman +yachtsmanlike +yelp +zoophytal
\ No newline at end of file diff --git a/tests/phpunit/includes/db/DatabaseSqliteTest.php b/tests/phpunit/includes/db/DatabaseSqliteTest.php new file mode 100644 index 00000000..914ab27c --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseSqliteTest.php @@ -0,0 +1,312 @@ +<?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 MediaWikiTestCase { + var $db; + + public function setUp() { + if ( !Sqlite::isPresent() ) { + $this->markTestSkipped( 'No SQLite support detected' ); + } + $this->db = new MockDatabaseSqlite(); + if ( version_compare( $this->db->getServerVersion(), '3.6.0', '<' ) ) { + $this->markTestSkipped( "SQLite at least 3.6 required, {$this->db->getServerVersion()} found" ); + } + } + + private function replaceVars( $sql ) { + // normalize spacing to hide implementation details + return preg_replace( '/\s+/', ' ', $this->db->replaceVars( $sql ) ); + } + + private function assertResultIs( $expected, $res ) { + $this->assertNotNull( $res ); + $i = 0; + foreach( $res as $row ) { + foreach( $expected[$i] as $key => $value ) { + $this->assertTrue( isset( $row->$key ) ); + $this->assertEquals( $value, $row->$key ); + } + $i++; + } + $this->assertEquals( count( $expected ), $i, 'Unexpected number of rows' ); + } + + 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 foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );", + $this->replaceVars("CREATE TABLE foo ( foobar INT PRIMARY KEY NOT NULL AUTO_INCREMENT );" ) + ); + $this->assertEquals( "CREATE TABLE foo ( foobar INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL );", + $this->replaceVars("CREATE TABLE foo ( foobar INT PRIMARY KEY AUTO_INCREMENT NOT NULL );" ) + ); + + $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' ) ); + } + + public function testDuplicateTableStructure() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + $db->query( 'CREATE TABLE foo(foo, barfoo)' ); + + $db->duplicateTableStructure( 'foo', 'bar' ); + $this->assertEquals( 'CREATE TABLE "bar"(foo, barfoo)', + $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ), + 'Normal table duplication' + ); + + $db->duplicateTableStructure( 'foo', 'baz', true ); + $this->assertEquals( 'CREATE TABLE "baz"(foo, barfoo)', + $db->selectField( 'sqlite_temp_master', 'sql', array( 'name' => 'baz' ) ), + 'Creation of temporary duplicate' + ); + $this->assertEquals( 0, + $db->selectField( 'sqlite_master', 'COUNT(*)', array( 'name' => 'baz' ) ), + 'Create a temporary duplicate only' + ); + } + + public function testDuplicateTableStructureVirtual() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + if ( $db->getFulltextSearchModule() != 'FTS3' ) { + $this->markTestSkipped( 'FTS3 not supported, cannot create virtual tables' ); + } + $db->query( 'CREATE VIRTUAL TABLE "foo" USING FTS3(foobar)' ); + + $db->duplicateTableStructure( 'foo', 'bar' ); + $this->assertEquals( 'CREATE VIRTUAL TABLE "bar" USING FTS3(foobar)', + $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'bar' ) ), + 'Duplication of virtual tables' + ); + + $db->duplicateTableStructure( 'foo', 'baz', true ); + $this->assertEquals( 'CREATE VIRTUAL TABLE "baz" USING FTS3(foobar)', + $db->selectField( 'sqlite_master', 'sql', array( 'name' => 'baz' ) ), + "Can't create temporary virtual tables, should fall back to non-temporary duplication" + ); + } + + public function testDeleteJoin() { + $db = new DatabaseSqliteStandalone( ':memory:' ); + $db->query( 'CREATE TABLE a (a_1)', __METHOD__ ); + $db->query( 'CREATE TABLE b (b_1, b_2)', __METHOD__ ); + $db->insert( 'a', array( + array( 'a_1' => 1 ), + array( 'a_1' => 2 ), + array( 'a_1' => 3 ), + ), + __METHOD__ + ); + $db->insert( 'b', array( + array( 'b_1' => 2, 'b_2' => 'a' ), + array( 'b_1' => 3, 'b_2' => 'b' ), + ), + __METHOD__ + ); + $db->deleteJoin( 'a', 'b', 'a_1', 'b_1', array( 'b_2' => 'a' ), __METHOD__ ); + $res = $db->query( "SELECT * FROM a", __METHOD__ ); + $this->assertResultIs( array( + array( 'a_1' => 1 ), + array( 'a_1' => 3 ), + ), + $res + ); + } + + public function testEntireSchema() { + global $IP; + + $result = Sqlite::checkSqlSyntax( "$IP/maintenance/tables.sql" ); + if ( $result !== true ) { + $this->fail( $result ); + } + $this->assertTrue( true ); // avoid test being marked as incomplete due to lack of assertions + } + + /** + * Runs upgrades of older databases and compares results with current schema + * @todo: currently only checks list of tables + */ + public function testUpgrades() { + global $IP, $wgVersion; + + // Versions tested + $versions = array( + //'1.13', disabled for now, was totally screwed up + // SQLite wasn't included in 1.14 + '1.15', + '1.16', + '1.17', + ); + + // Mismatches for these columns we can safely ignore + $ignoredColumns = array( + 'user_newtalk.user_last_timestamp', // r84185 + ); + + $currentDB = new DatabaseSqliteStandalone( ':memory:' ); + $currentDB->sourceFile( "$IP/maintenance/tables.sql" ); + $currentTables = $this->getTables( $currentDB ); + sort( $currentTables ); + + foreach ( $versions as $version ) { + $versions = "upgrading from $version to $wgVersion"; + $db = $this->prepareDB( $version ); + $tables = $this->getTables( $db ); + $this->assertEquals( $currentTables, $tables, "Different tables $versions" ); + foreach ( $tables as $table ) { + $currentCols = $this->getColumns( $currentDB, $table ); + $cols = $this->getColumns( $db, $table ); + $this->assertEquals( + array_keys( $currentCols ), + array_keys( $cols ), + "Mismatching columns for table \"$table\" $versions" + ); + foreach ( $currentCols as $name => $column ) { + $fullName = "$table.$name"; + $this->assertEquals( + (bool)$column->pk, + (bool)$cols[$name]->pk, + "PRIMARY KEY status does not match for column $fullName $versions" + ); + if ( !in_array( $fullName, $ignoredColumns ) ) { + $this->assertEquals( + (bool)$column->notnull, + (bool)$cols[$name]->notnull, + "NOT NULL status does not match for column $fullName $versions" + ); + $this->assertEquals( + $column->dflt_value, + $cols[$name]->dflt_value, + "Default values does not match for column $fullName $versions" + ); + } + } + $currentIndexes = $this->getIndexes( $currentDB, $table ); + $indexes = $this->getIndexes( $db, $table ); + $this->assertEquals( + array_keys( $currentIndexes ), + array_keys( $indexes ), + "mismatching indexes for table \"$table\" $versions" + ); + } + $db->close(); + } + } + + private function prepareDB( $version ) { + static $maint = null; + if ( $maint === null ) { + $maint = new FakeMaintenance(); + $maint->loadParamsAndArgs( null, array( 'quiet' => 1 ) ); + } + + $db = new DatabaseSqliteStandalone( ':memory:' ); + $db->sourceFile( dirname( __FILE__ ) . "/sqlite/tables-$version.sql" ); + $updater = DatabaseUpdater::newForDB( $db, false, $maint ); + $updater->doUpdates( array( 'core' ) ); + return $db; + } + + private function getTables( $db ) { + $list = array_flip( $db->listTables() ); + $excluded = array( + 'math', // moved out of core in 1.18 + 'searchindex', + 'searchindex_content', + 'searchindex_segments', + 'searchindex_segdir', + // FTS4 ready!!1 + 'searchindex_docsize', + 'searchindex_stat', + ); + foreach ( $excluded as $t ) { + unset( $list[$t] ); + } + $list = array_flip( $list ); + sort( $list ); + return $list; + } + + private function getColumns( $db, $table ) { + $cols = array(); + $res = $db->query( "PRAGMA table_info($table)" ); + $this->assertNotNull( $res ); + foreach ( $res as $col ) { + $cols[$col->name] = $col; + } + ksort( $cols ); + return $cols; + } + + private function getIndexes( $db, $table ) { + $indexes = array(); + $res = $db->query( "PRAGMA index_list($table)" ); + $this->assertNotNull( $res ); + foreach ( $res as $index ) { + $res2 = $db->query( "PRAGMA index_info({$index->name})" ); + $this->assertNotNull( $res2 ); + $index->columns = array(); + foreach ( $res2 as $col ) { + $index->columns[] = $col; + } + $indexes[$index->name] = $index; + } + ksort( $indexes ); + return $indexes; + } +} diff --git a/tests/phpunit/includes/db/DatabaseTest.php b/tests/phpunit/includes/db/DatabaseTest.php new file mode 100644 index 00000000..d480ac6e --- /dev/null +++ b/tests/phpunit/includes/db/DatabaseTest.php @@ -0,0 +1,95 @@ +<?php + +/** + * @group Database + */ +class DatabaseTest extends MediaWikiTestCase { + var $db; + + function setUp() { + $this->db = wfGetDB( DB_SLAVE ); + } + + function testAddQuotesNull() { + $check = "NULL"; + if ( $this->db->getType() === 'sqlite' || $this->db->getType() === 'oracle' ) { + $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 ); + } + +} + + diff --git a/tests/phpunit/includes/db/sqlite/tables-1.13.sql b/tests/phpunit/includes/db/sqlite/tables-1.13.sql new file mode 100644 index 00000000..a0dcb553 --- /dev/null +++ b/tests/phpunit/includes/db/sqlite/tables-1.13.sql @@ -0,0 +1,342 @@ +-- This is a copy of SQLite schema from MediaWiki 1.13 used for updater testing + +CREATE TABLE /*$wgDBprefix*/user ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_name varchar(255) default '', + user_real_name varchar(255) default '', + user_password tinyblob , + user_newpassword tinyblob , + user_newpass_time BLOB, + user_email tinytext , + user_options blob , + user_touched BLOB default '', + user_token BLOB default '', + user_email_authenticated BLOB, + user_email_token BLOB, + user_email_token_expires BLOB, + user_registration BLOB, + user_editcount int) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/user_groups ( + ug_user INTEGER default '0', + ug_group varBLOB default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/user_newtalk ( + user_id INTEGER default '0', + user_ip varBLOB default '', + user_last_timestamp BLOB default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/page ( + page_id INTEGER PRIMARY KEY AUTOINCREMENT, + page_namespace INTEGER , + page_title varchar(255) , + page_restrictions tinyblob , + page_counter bigint default '0', + page_is_redirect tinyint default '0', + page_is_new tinyint default '0', + page_random real , + page_touched BLOB default '', + page_latest INTEGER , + page_len INTEGER ) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/revision ( + rev_id INTEGER PRIMARY KEY AUTOINCREMENT, + rev_page INTEGER , + rev_text_id INTEGER , + rev_comment tinyblob , + rev_user INTEGER default '0', + rev_user_text varchar(255) default '', + rev_timestamp BLOB default '', + rev_minor_edit tinyint default '0', + rev_deleted tinyint default '0', + rev_len int, + rev_parent_id INTEGER default NULL) /*$wgDBTableOptions*/ ; + +CREATE TABLE /*$wgDBprefix*/text ( + old_id INTEGER PRIMARY KEY AUTOINCREMENT, + old_text mediumblob , + old_flags tinyblob ) /*$wgDBTableOptions*/ ; + +CREATE TABLE /*$wgDBprefix*/archive ( + ar_namespace INTEGER default '0', + ar_title varchar(255) default '', + ar_text mediumblob , + ar_comment tinyblob , + ar_user INTEGER default '0', + ar_user_text varchar(255) , + ar_timestamp BLOB default '', + ar_minor_edit tinyint default '0', + ar_flags tinyblob , + ar_rev_id int, + ar_text_id int, + ar_deleted tinyint default '0', + ar_len int, + ar_page_id int, + ar_parent_id INTEGER default NULL) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/pagelinks ( + pl_from INTEGER default '0', + pl_namespace INTEGER default '0', + pl_title varchar(255) default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/templatelinks ( + tl_from INTEGER default '0', + tl_namespace INTEGER default '0', + tl_title varchar(255) default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/imagelinks ( + il_from INTEGER default '0', + il_to varchar(255) default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/categorylinks ( + cl_from INTEGER default '0', + cl_to varchar(255) default '', + cl_sortkey varchar(70) default '', + cl_timestamp timestamp ) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/category ( + cat_id INTEGER PRIMARY KEY AUTOINCREMENT, + cat_title varchar(255) , + cat_pages INTEGER signed default 0, + cat_subcats INTEGER signed default 0, + cat_files INTEGER signed default 0, + cat_hidden tinyint default 0) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/externallinks ( + el_from INTEGER default '0', + el_to blob , + el_index blob ) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/langlinks ( + ll_from INTEGER default '0', + ll_lang varBLOB default '', + ll_title varchar(255) default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/site_stats ( + ss_row_id INTEGER , + ss_total_views bigint default '0', + ss_total_edits bigint default '0', + ss_good_articles bigint default '0', + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_admins INTEGER default '-1', + ss_images INTEGER default '0') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/hitcounter ( + hc_id INTEGER +) ; + +CREATE TABLE /*$wgDBprefix*/ipblocks ( + ipb_id INTEGER PRIMARY KEY AUTOINCREMENT, + ipb_address tinyblob , + ipb_user INTEGER default '0', + ipb_by INTEGER default '0', + ipb_by_text varchar(255) default '', + ipb_reason tinyblob , + ipb_timestamp BLOB default '', + ipb_auto bool default 0, + ipb_anon_only bool default 0, + ipb_create_account bool default 1, + ipb_enable_autoblock bool default '1', + ipb_expiry varBLOB default '', + ipb_range_start tinyblob , + ipb_range_end tinyblob , + ipb_deleted bool default 0, + ipb_block_email bool default 0) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/image ( + img_name varchar(255) default '', + img_size INTEGER default '0', + img_width INTEGER default '0', + img_height INTEGER default '0', + img_metadata mediumblob , + img_bits INTEGER default '0', + img_media_type TEXT default NULL, + img_major_mime TEXT default "unknown", + img_minor_mime varBLOB default "unknown", + img_description tinyblob , + img_user INTEGER default '0', + img_user_text varchar(255) , + img_timestamp varBLOB default '', + img_sha1 varBLOB default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/oldimage ( + oi_name varchar(255) default '', + oi_archive_name varchar(255) default '', + oi_size INTEGER default 0, + oi_width INTEGER default 0, + oi_height INTEGER default 0, + oi_bits INTEGER default 0, + oi_description tinyblob , + oi_user INTEGER default '0', + oi_user_text varchar(255) , + oi_timestamp BLOB default '', + oi_metadata mediumblob , + oi_media_type TEXT default NULL, + oi_major_mime TEXT default "unknown", + oi_minor_mime varBLOB default "unknown", + oi_deleted tinyint default '0', + oi_sha1 varBLOB default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/filearchive ( + fa_id INTEGER PRIMARY KEY AUTOINCREMENT, + fa_name varchar(255) default '', + fa_archive_name varchar(255) default '', + fa_storage_group varBLOB, + fa_storage_key varBLOB default '', + fa_deleted_user int, + fa_deleted_timestamp BLOB default '', + fa_deleted_reason text, + fa_size INTEGER default '0', + fa_width INTEGER default '0', + fa_height INTEGER default '0', + fa_metadata mediumblob, + fa_bits INTEGER default '0', + fa_media_type TEXT default NULL, + fa_major_mime TEXT default "unknown", + fa_minor_mime varBLOB default "unknown", + fa_description tinyblob, + fa_user INTEGER default '0', + fa_user_text varchar(255) , + fa_timestamp BLOB default '', + fa_deleted tinyint default '0') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/recentchanges ( + rc_id INTEGER PRIMARY KEY AUTOINCREMENT, + rc_timestamp varBLOB default '', + rc_cur_time varBLOB default '', + rc_user INTEGER default '0', + rc_user_text varchar(255) , + rc_namespace INTEGER default '0', + rc_title varchar(255) default '', + rc_comment varchar(255) default '', + rc_minor tinyint default '0', + rc_bot tinyint default '0', + rc_new tinyint default '0', + rc_cur_id INTEGER default '0', + rc_this_oldid INTEGER default '0', + rc_last_oldid INTEGER default '0', + rc_type tinyint default '0', + rc_moved_to_ns tinyint default '0', + rc_moved_to_title varchar(255) default '', + rc_patrolled tinyint default '0', + rc_ip varBLOB default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint default '0', + rc_logid INTEGER default '0', + rc_log_type varBLOB NULL default NULL, + rc_log_action varBLOB NULL default NULL, + rc_params blob NULL) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/watchlist ( + wl_user INTEGER , + wl_namespace INTEGER default '0', + wl_title varchar(255) default '', + wl_notificationtimestamp varBLOB) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/math ( + math_inputhash varBLOB , + math_outputhash varBLOB , + math_html_conservativeness tinyint , + math_html text, + math_mathml text) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/searchindex ( + si_page INTEGER , + si_title varchar(255) default '', + si_text mediumtext ) ; + +CREATE TABLE /*$wgDBprefix*/interwiki ( + iw_prefix varchar(32) , + iw_url blob , + iw_local bool , + iw_trans tinyint default 0) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/querycache ( + qc_type varBLOB , + qc_value INTEGER default '0', + qc_namespace INTEGER default '0', + qc_title varchar(255) default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/objectcache ( + keyname varBLOB default '', + value mediumblob, + exptime datetime) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/transcache ( + tc_url varBLOB , + tc_contents text, + tc_time INTEGER ) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/logging ( + log_id INTEGER PRIMARY KEY AUTOINCREMENT, + log_type varBLOB default '', + log_action varBLOB default '', + log_timestamp BLOB default '19700101000000', + log_user INTEGER default 0, + log_namespace INTEGER default 0, + log_title varchar(255) default '', + log_comment varchar(255) default '', + log_params blob , + log_deleted tinyint default '0') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/trackbacks ( + tb_id INTEGER PRIMARY KEY AUTOINCREMENT, + tb_page INTEGER REFERENCES /*$wgDBprefix*/page(page_id) ON DELETE CASCADE, + tb_title varchar(255) , + tb_url blob , + tb_ex text, + tb_name varchar(255)) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/job ( + job_id INTEGER PRIMARY KEY AUTOINCREMENT, + job_cmd varBLOB default '', + job_namespace INTEGER , + job_title varchar(255) , + job_params blob ) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/querycache_info ( + qci_type varBLOB default '', + qci_timestamp BLOB default '19700101000000') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/redirect ( + rd_from INTEGER default '0', + rd_namespace INTEGER default '0', + rd_title varchar(255) default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/querycachetwo ( + qcc_type varBLOB , + qcc_value INTEGER default '0', + qcc_namespace INTEGER default '0', + qcc_title varchar(255) default '', + qcc_namespacetwo INTEGER default '0', + qcc_titletwo varchar(255) default '') /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/page_restrictions ( + pr_page INTEGER , + pr_type varBLOB , + pr_level varBLOB , + pr_cascade tinyint , + pr_user INTEGER NULL, + pr_expiry varBLOB NULL, + pr_id INTEGER PRIMARY KEY AUTOINCREMENT) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/protected_titles ( + pt_namespace INTEGER , + pt_title varchar(255) , + pt_user INTEGER , + pt_reason tinyblob, + pt_timestamp BLOB , + pt_expiry varBLOB default '', + pt_create_perm varBLOB ) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/page_props ( + pp_page INTEGER , + pp_propname varBLOB , + pp_value blob ) /*$wgDBTableOptions*/; + +CREATE TABLE /*$wgDBprefix*/updatelog ( + ul_key varchar(255) ) /*$wgDBTableOptions*/; + + diff --git a/tests/phpunit/includes/db/sqlite/tables-1.15.sql b/tests/phpunit/includes/db/sqlite/tables-1.15.sql new file mode 100644 index 00000000..901bac52 --- /dev/null +++ b/tests/phpunit/includes/db/sqlite/tables-1.15.sql @@ -0,0 +1,454 @@ +-- This is a copy of MediaWiki 1.15 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_options blob NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(16) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp binary(14) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varchar(70) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0, + cat_hidden tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_admins int default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0 +) /*$wgDBTableOptions*/; + +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(32) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(32) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(32) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_moved_to_ns tinyint unsigned NOT NULL default 0, + rc_moved_to_title varchar(255) binary NOT NULL default '', + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/math ( + math_inputhash varbinary(16) NOT NULL, + math_outputhash varbinary(16) NOT NULL, + math_html_conservativeness tinyint NOT NULL, + math_html text, + math_mathml text +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/math_inputhash ON /*_*/math (math_inputhash); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time int NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(10) NOT NULL default '', + log_action varbinary(10) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE TABLE /*_*/trackbacks ( + tb_id int PRIMARY KEY AUTO_INCREMENT, + tb_page int REFERENCES /*_*/page(page_id) ON DELETE CASCADE, + tb_title varchar(255) NOT NULL, + tb_url blob NOT NULL, + tb_ex text, + tb_name varchar(255) +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/tb_page ON /*_*/trackbacks (tb_page); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_params blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL, + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; diff --git a/tests/phpunit/includes/db/sqlite/tables-1.16.sql b/tests/phpunit/includes/db/sqlite/tables-1.16.sql new file mode 100644 index 00000000..6e56add2 --- /dev/null +++ b/tests/phpunit/includes/db/sqlite/tables-1.16.sql @@ -0,0 +1,483 @@ +-- This is a copy of MediaWiki 1.16 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_options blob NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(16) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp binary(14) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/user_properties ( + up_user int NOT NULL, + up_property varbinary(32) NOT NULL, + up_value blob +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varchar(70) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0, + cat_hidden tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/external_user ( + eu_local_id int unsigned NOT NULL PRIMARY KEY, + eu_external_id varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_admins int default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(100) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(100) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(100) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_moved_to_ns tinyint unsigned NOT NULL default 0, + rc_moved_to_title varchar(255) binary NOT NULL default '', + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/math ( + math_inputhash varbinary(16) NOT NULL, + math_outputhash varbinary(16) NOT NULL, + math_html_conservativeness tinyint NOT NULL, + math_html text, + math_mathml text +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/math_inputhash ON /*_*/math (math_inputhash); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(32) NOT NULL default '', + log_action varbinary(32) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_user_text varchar(255) binary NOT NULL default '', + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_page int unsigned NULL, + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); +CREATE TABLE /*_*/log_search ( + ls_field varbinary(32) NOT NULL, + ls_value varchar(255) NOT NULL, + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); +CREATE TABLE /*_*/trackbacks ( + tb_id int PRIMARY KEY AUTO_INCREMENT, + tb_page int REFERENCES /*_*/page(page_id) ON DELETE CASCADE, + tb_title varchar(255) NOT NULL, + tb_url blob NOT NULL, + tb_ex text, + tb_name varchar(255) +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/tb_page ON /*_*/trackbacks (tb_page); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_params blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '', + rd_interwiki varchar(32) default NULL, + rd_fragment varchar(255) binary default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL, + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); diff --git a/tests/phpunit/includes/db/sqlite/tables-1.17.sql b/tests/phpunit/includes/db/sqlite/tables-1.17.sql new file mode 100644 index 00000000..69ae3764 --- /dev/null +++ b/tests/phpunit/includes/db/sqlite/tables-1.17.sql @@ -0,0 +1,516 @@ +-- This is a copy of MediaWiki 1.17 schema shared by MySQL and SQLite. +-- It is used for updater testing. Comments are stripped to decrease +-- file size, as we don't need to maintain it. + +CREATE TABLE /*_*/user ( + user_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + user_name varchar(255) binary NOT NULL default '', + user_real_name varchar(255) binary NOT NULL default '', + user_password tinyblob NOT NULL, + user_newpassword tinyblob NOT NULL, + user_newpass_time binary(14), + user_email tinytext NOT NULL, + user_options blob NOT NULL, + user_touched binary(14) NOT NULL default '', + user_token binary(32) NOT NULL default '', + user_email_authenticated binary(14), + user_email_token binary(32), + user_email_token_expires binary(14), + user_registration binary(14), + user_editcount int +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_name ON /*_*/user (user_name); +CREATE INDEX /*i*/user_email_token ON /*_*/user (user_email_token); +CREATE TABLE /*_*/user_groups ( + ug_user int unsigned NOT NULL default 0, + ug_group varbinary(16) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ug_user_group ON /*_*/user_groups (ug_user,ug_group); +CREATE INDEX /*i*/ug_group ON /*_*/user_groups (ug_group); +CREATE TABLE /*_*/user_newtalk ( + user_id int NOT NULL default 0, + user_ip varbinary(40) NOT NULL default '', + user_last_timestamp binary(14) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/un_user_id ON /*_*/user_newtalk (user_id); +CREATE INDEX /*i*/un_user_ip ON /*_*/user_newtalk (user_ip); +CREATE TABLE /*_*/user_properties ( + up_user int NOT NULL, + up_property varbinary(32) NOT NULL, + up_value blob +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/user_properties_user_property ON /*_*/user_properties (up_user,up_property); +CREATE INDEX /*i*/user_properties_property ON /*_*/user_properties (up_property); +CREATE TABLE /*_*/page ( + page_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + page_namespace int NOT NULL, + page_title varchar(255) binary NOT NULL, + page_restrictions tinyblob NOT NULL, + page_counter bigint unsigned NOT NULL default 0, + page_is_redirect tinyint unsigned NOT NULL default 0, + page_is_new tinyint unsigned NOT NULL default 0, + page_random real unsigned NOT NULL, + page_touched binary(14) NOT NULL default '', + page_latest int unsigned NOT NULL, + page_len int unsigned NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title); +CREATE INDEX /*i*/page_random ON /*_*/page (page_random); +CREATE INDEX /*i*/page_len ON /*_*/page (page_len); +CREATE TABLE /*_*/revision ( + rev_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + rev_page int unsigned NOT NULL, + rev_text_id int unsigned NOT NULL, + rev_comment tinyblob NOT NULL, + rev_user int unsigned NOT NULL default 0, + rev_user_text varchar(255) binary NOT NULL default '', + rev_timestamp binary(14) NOT NULL default '', + rev_minor_edit tinyint unsigned NOT NULL default 0, + rev_deleted tinyint unsigned NOT NULL default 0, + rev_len int unsigned, + rev_parent_id int unsigned default NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024; +CREATE UNIQUE INDEX /*i*/rev_page_id ON /*_*/revision (rev_page, rev_id); +CREATE INDEX /*i*/rev_timestamp ON /*_*/revision (rev_timestamp); +CREATE INDEX /*i*/page_timestamp ON /*_*/revision (rev_page,rev_timestamp); +CREATE INDEX /*i*/user_timestamp ON /*_*/revision (rev_user,rev_timestamp); +CREATE INDEX /*i*/usertext_timestamp ON /*_*/revision (rev_user_text,rev_timestamp); +CREATE TABLE /*_*/text ( + old_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + old_text mediumblob NOT NULL, + old_flags tinyblob NOT NULL +) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=10240; +CREATE TABLE /*_*/archive ( + ar_namespace int NOT NULL default 0, + ar_title varchar(255) binary NOT NULL default '', + ar_text mediumblob NOT NULL, + ar_comment tinyblob NOT NULL, + ar_user int unsigned NOT NULL default 0, + ar_user_text varchar(255) binary NOT NULL, + ar_timestamp binary(14) NOT NULL default '', + ar_minor_edit tinyint NOT NULL default 0, + ar_flags tinyblob NOT NULL, + ar_rev_id int unsigned, + ar_text_id int unsigned, + ar_deleted tinyint unsigned NOT NULL default 0, + ar_len int unsigned, + ar_page_id int unsigned, + ar_parent_id int unsigned default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp); +CREATE INDEX /*i*/ar_usertext_timestamp ON /*_*/archive (ar_user_text,ar_timestamp); +CREATE INDEX /*i*/ar_revid ON /*_*/archive (ar_rev_id); +CREATE TABLE /*_*/pagelinks ( + pl_from int unsigned NOT NULL default 0, + pl_namespace int NOT NULL default 0, + pl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pl_from ON /*_*/pagelinks (pl_from,pl_namespace,pl_title); +CREATE UNIQUE INDEX /*i*/pl_namespace ON /*_*/pagelinks (pl_namespace,pl_title,pl_from); +CREATE TABLE /*_*/templatelinks ( + tl_from int unsigned NOT NULL default 0, + tl_namespace int NOT NULL default 0, + tl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tl_from ON /*_*/templatelinks (tl_from,tl_namespace,tl_title); +CREATE UNIQUE INDEX /*i*/tl_namespace ON /*_*/templatelinks (tl_namespace,tl_title,tl_from); +CREATE TABLE /*_*/imagelinks ( + il_from int unsigned NOT NULL default 0, + il_to varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/il_from ON /*_*/imagelinks (il_from,il_to); +CREATE UNIQUE INDEX /*i*/il_to ON /*_*/imagelinks (il_to,il_from); +CREATE TABLE /*_*/categorylinks ( + cl_from int unsigned NOT NULL default 0, + cl_to varchar(255) binary NOT NULL default '', + cl_sortkey varbinary(230) NOT NULL default '', + cl_sortkey_prefix varchar(255) binary NOT NULL default '', + cl_timestamp timestamp NOT NULL, + cl_collation varbinary(32) NOT NULL default '', + cl_type ENUM('page', 'subcat', 'file') NOT NULL default 'page' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cl_from ON /*_*/categorylinks (cl_from,cl_to); +CREATE INDEX /*i*/cl_sortkey ON /*_*/categorylinks (cl_to,cl_type,cl_sortkey,cl_from); +CREATE INDEX /*i*/cl_timestamp ON /*_*/categorylinks (cl_to,cl_timestamp); +CREATE INDEX /*i*/cl_collation ON /*_*/categorylinks (cl_collation); +CREATE TABLE /*_*/category ( + cat_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + cat_title varchar(255) binary NOT NULL, + cat_pages int signed NOT NULL default 0, + cat_subcats int signed NOT NULL default 0, + cat_files int signed NOT NULL default 0, + cat_hidden tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/cat_title ON /*_*/category (cat_title); +CREATE INDEX /*i*/cat_pages ON /*_*/category (cat_pages); +CREATE TABLE /*_*/externallinks ( + el_from int unsigned NOT NULL default 0, + el_to blob NOT NULL, + el_index blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/el_from ON /*_*/externallinks (el_from, el_to(40)); +CREATE INDEX /*i*/el_to ON /*_*/externallinks (el_to(60), el_from); +CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60)); +CREATE TABLE /*_*/external_user ( + eu_local_id int unsigned NOT NULL PRIMARY KEY, + eu_external_id varchar(255) binary NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id); +CREATE TABLE /*_*/langlinks ( + ll_from int unsigned NOT NULL default 0, + ll_lang varbinary(20) NOT NULL default '', + ll_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ll_from ON /*_*/langlinks (ll_from, ll_lang); +CREATE INDEX /*i*/ll_lang ON /*_*/langlinks (ll_lang, ll_title); +CREATE TABLE /*_*/iwlinks ( + iwl_from int unsigned NOT NULL default 0, + iwl_prefix varbinary(20) NOT NULL default '', + iwl_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iwl_from ON /*_*/iwlinks (iwl_from, iwl_prefix, iwl_title); +CREATE UNIQUE INDEX /*i*/iwl_prefix_title_from ON /*_*/iwlinks (iwl_prefix, iwl_title, iwl_from); +CREATE TABLE /*_*/site_stats ( + ss_row_id int unsigned NOT NULL, + ss_total_views bigint unsigned default 0, + ss_total_edits bigint unsigned default 0, + ss_good_articles bigint unsigned default 0, + ss_total_pages bigint default '-1', + ss_users bigint default '-1', + ss_active_users bigint default '-1', + ss_admins int default '-1', + ss_images int default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ss_row_id ON /*_*/site_stats (ss_row_id); +CREATE TABLE /*_*/hitcounter ( + hc_id int unsigned NOT NULL +) ENGINE=HEAP MAX_ROWS=25000; +CREATE TABLE /*_*/ipblocks ( + ipb_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + ipb_address tinyblob NOT NULL, + ipb_user int unsigned NOT NULL default 0, + ipb_by int unsigned NOT NULL default 0, + ipb_by_text varchar(255) binary NOT NULL default '', + ipb_reason tinyblob NOT NULL, + ipb_timestamp binary(14) NOT NULL default '', + ipb_auto bool NOT NULL default 0, + ipb_anon_only bool NOT NULL default 0, + ipb_create_account bool NOT NULL default 1, + ipb_enable_autoblock bool NOT NULL default '1', + ipb_expiry varbinary(14) NOT NULL default '', + ipb_range_start tinyblob NOT NULL, + ipb_range_end tinyblob NOT NULL, + ipb_deleted bool NOT NULL default 0, + ipb_block_email bool NOT NULL default 0, + ipb_allow_usertalk bool NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ipb_address ON /*_*/ipblocks (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only); +CREATE INDEX /*i*/ipb_user ON /*_*/ipblocks (ipb_user); +CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8)); +CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp); +CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry); +CREATE TABLE /*_*/image ( + img_name varchar(255) binary NOT NULL default '' PRIMARY KEY, + img_size int unsigned NOT NULL default 0, + img_width int NOT NULL default 0, + img_height int NOT NULL default 0, + img_metadata mediumblob NOT NULL, + img_bits int NOT NULL default 0, + img_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + img_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + img_minor_mime varbinary(100) NOT NULL default "unknown", + img_description tinyblob NOT NULL, + img_user int unsigned NOT NULL default 0, + img_user_text varchar(255) binary NOT NULL, + img_timestamp varbinary(14) NOT NULL default '', + img_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/img_usertext_timestamp ON /*_*/image (img_user_text,img_timestamp); +CREATE INDEX /*i*/img_size ON /*_*/image (img_size); +CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp); +CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1); +CREATE TABLE /*_*/oldimage ( + oi_name varchar(255) binary NOT NULL default '', + oi_archive_name varchar(255) binary NOT NULL default '', + oi_size int unsigned NOT NULL default 0, + oi_width int NOT NULL default 0, + oi_height int NOT NULL default 0, + oi_bits int NOT NULL default 0, + oi_description tinyblob NOT NULL, + oi_user int unsigned NOT NULL default 0, + oi_user_text varchar(255) binary NOT NULL, + oi_timestamp binary(14) NOT NULL default '', + oi_metadata mediumblob NOT NULL, + oi_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + oi_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") NOT NULL default "unknown", + oi_minor_mime varbinary(100) NOT NULL default "unknown", + oi_deleted tinyint unsigned NOT NULL default 0, + oi_sha1 varbinary(32) NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/oi_usertext_timestamp ON /*_*/oldimage (oi_user_text,oi_timestamp); +CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp); +CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14)); +CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1); +CREATE TABLE /*_*/filearchive ( + fa_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + fa_name varchar(255) binary NOT NULL default '', + fa_archive_name varchar(255) binary default '', + fa_storage_group varbinary(16), + fa_storage_key varbinary(64) default '', + fa_deleted_user int, + fa_deleted_timestamp binary(14) default '', + fa_deleted_reason text, + fa_size int unsigned default 0, + fa_width int default 0, + fa_height int default 0, + fa_metadata mediumblob, + fa_bits int default 0, + fa_media_type ENUM("UNKNOWN", "BITMAP", "DRAWING", "AUDIO", "VIDEO", "MULTIMEDIA", "OFFICE", "TEXT", "EXECUTABLE", "ARCHIVE") default NULL, + fa_major_mime ENUM("unknown", "application", "audio", "image", "text", "video", "message", "model", "multipart") default "unknown", + fa_minor_mime varbinary(100) default "unknown", + fa_description tinyblob, + fa_user int unsigned default 0, + fa_user_text varchar(255) binary, + fa_timestamp binary(14) default '', + fa_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/fa_name ON /*_*/filearchive (fa_name, fa_timestamp); +CREATE INDEX /*i*/fa_storage_group ON /*_*/filearchive (fa_storage_group, fa_storage_key); +CREATE INDEX /*i*/fa_deleted_timestamp ON /*_*/filearchive (fa_deleted_timestamp); +CREATE INDEX /*i*/fa_user_timestamp ON /*_*/filearchive (fa_user_text,fa_timestamp); +CREATE TABLE /*_*/recentchanges ( + rc_id int NOT NULL PRIMARY KEY AUTO_INCREMENT, + rc_timestamp varbinary(14) NOT NULL default '', + rc_cur_time varbinary(14) NOT NULL default '', + rc_user int unsigned NOT NULL default 0, + rc_user_text varchar(255) binary NOT NULL, + rc_namespace int NOT NULL default 0, + rc_title varchar(255) binary NOT NULL default '', + rc_comment varchar(255) binary NOT NULL default '', + rc_minor tinyint unsigned NOT NULL default 0, + rc_bot tinyint unsigned NOT NULL default 0, + rc_new tinyint unsigned NOT NULL default 0, + rc_cur_id int unsigned NOT NULL default 0, + rc_this_oldid int unsigned NOT NULL default 0, + rc_last_oldid int unsigned NOT NULL default 0, + rc_type tinyint unsigned NOT NULL default 0, + rc_moved_to_ns tinyint unsigned NOT NULL default 0, + rc_moved_to_title varchar(255) binary NOT NULL default '', + rc_patrolled tinyint unsigned NOT NULL default 0, + rc_ip varbinary(40) NOT NULL default '', + rc_old_len int, + rc_new_len int, + rc_deleted tinyint unsigned NOT NULL default 0, + rc_logid int unsigned NOT NULL default 0, + rc_log_type varbinary(255) NULL default NULL, + rc_log_action varbinary(255) NULL default NULL, + rc_params blob NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rc_timestamp ON /*_*/recentchanges (rc_timestamp); +CREATE INDEX /*i*/rc_namespace_title ON /*_*/recentchanges (rc_namespace, rc_title); +CREATE INDEX /*i*/rc_cur_id ON /*_*/recentchanges (rc_cur_id); +CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,rc_timestamp); +CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip); +CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text); +CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp); +CREATE TABLE /*_*/watchlist ( + wl_user int unsigned NOT NULL, + wl_namespace int NOT NULL default 0, + wl_title varchar(255) binary NOT NULL default '', + wl_notificationtimestamp varbinary(14) +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/wl_user ON /*_*/watchlist (wl_user, wl_namespace, wl_title); +CREATE INDEX /*i*/namespace_title ON /*_*/watchlist (wl_namespace, wl_title); +CREATE TABLE /*_*/math ( + math_inputhash varbinary(16) NOT NULL, + math_outputhash varbinary(16) NOT NULL, + math_html_conservativeness tinyint NOT NULL, + math_html text, + math_mathml text +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/math_inputhash ON /*_*/math (math_inputhash); +CREATE TABLE /*_*/searchindex ( + si_page int unsigned NOT NULL, + si_title varchar(255) NOT NULL default '', + si_text mediumtext NOT NULL +) ENGINE=MyISAM; +CREATE UNIQUE INDEX /*i*/si_page ON /*_*/searchindex (si_page); +CREATE FULLTEXT INDEX /*i*/si_title ON /*_*/searchindex (si_title); +CREATE FULLTEXT INDEX /*i*/si_text ON /*_*/searchindex (si_text); +CREATE TABLE /*_*/interwiki ( + iw_prefix varchar(32) NOT NULL, + iw_url blob NOT NULL, + iw_api blob NOT NULL, + iw_wikiid varchar(64) NOT NULL, + iw_local bool NOT NULL, + iw_trans tinyint NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/iw_prefix ON /*_*/interwiki (iw_prefix); +CREATE TABLE /*_*/querycache ( + qc_type varbinary(32) NOT NULL, + qc_value int unsigned NOT NULL default 0, + qc_namespace int NOT NULL default 0, + qc_title varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qc_type ON /*_*/querycache (qc_type,qc_value); +CREATE TABLE /*_*/objectcache ( + keyname varbinary(255) NOT NULL default '' PRIMARY KEY, + value mediumblob, + exptime datetime +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/exptime ON /*_*/objectcache (exptime); +CREATE TABLE /*_*/transcache ( + tc_url varbinary(255) NOT NULL, + tc_contents text, + tc_time binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tc_url_idx ON /*_*/transcache (tc_url); +CREATE TABLE /*_*/logging ( + log_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + log_type varbinary(32) NOT NULL default '', + log_action varbinary(32) NOT NULL default '', + log_timestamp binary(14) NOT NULL default '19700101000000', + log_user int unsigned NOT NULL default 0, + log_user_text varchar(255) binary NOT NULL default '', + log_namespace int NOT NULL default 0, + log_title varchar(255) binary NOT NULL default '', + log_page int unsigned NULL, + log_comment varchar(255) NOT NULL default '', + log_params blob NOT NULL, + log_deleted tinyint unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/type_time ON /*_*/logging (log_type, log_timestamp); +CREATE INDEX /*i*/user_time ON /*_*/logging (log_user, log_timestamp); +CREATE INDEX /*i*/page_time ON /*_*/logging (log_namespace, log_title, log_timestamp); +CREATE INDEX /*i*/times ON /*_*/logging (log_timestamp); +CREATE INDEX /*i*/log_user_type_time ON /*_*/logging (log_user, log_type, log_timestamp); +CREATE INDEX /*i*/log_page_id_time ON /*_*/logging (log_page,log_timestamp); +CREATE TABLE /*_*/log_search ( + ls_field varbinary(32) NOT NULL, + ls_value varchar(255) NOT NULL, + ls_log_id int unsigned NOT NULL default 0 +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/ls_field_val ON /*_*/log_search (ls_field,ls_value,ls_log_id); +CREATE INDEX /*i*/ls_log_id ON /*_*/log_search (ls_log_id); +CREATE TABLE /*_*/trackbacks ( + tb_id int PRIMARY KEY AUTO_INCREMENT, + tb_page int REFERENCES /*_*/page(page_id) ON DELETE CASCADE, + tb_title varchar(255) NOT NULL, + tb_url blob NOT NULL, + tb_ex text, + tb_name varchar(255) +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/tb_page ON /*_*/trackbacks (tb_page); +CREATE TABLE /*_*/job ( + job_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, + job_cmd varbinary(60) NOT NULL default '', + job_namespace int NOT NULL, + job_title varchar(255) binary NOT NULL, + job_params blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/job_cmd ON /*_*/job (job_cmd, job_namespace, job_title, job_params(128)); +CREATE TABLE /*_*/querycache_info ( + qci_type varbinary(32) NOT NULL default '', + qci_timestamp binary(14) NOT NULL default '19700101000000' +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/qci_type ON /*_*/querycache_info (qci_type); +CREATE TABLE /*_*/redirect ( + rd_from int unsigned NOT NULL default 0 PRIMARY KEY, + rd_namespace int NOT NULL default 0, + rd_title varchar(255) binary NOT NULL default '', + rd_interwiki varchar(32) default NULL, + rd_fragment varchar(255) binary default NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/rd_ns_title ON /*_*/redirect (rd_namespace,rd_title,rd_from); +CREATE TABLE /*_*/querycachetwo ( + qcc_type varbinary(32) NOT NULL, + qcc_value int unsigned NOT NULL default 0, + qcc_namespace int NOT NULL default 0, + qcc_title varchar(255) binary NOT NULL default '', + qcc_namespacetwo int NOT NULL default 0, + qcc_titletwo varchar(255) binary NOT NULL default '' +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/qcc_type ON /*_*/querycachetwo (qcc_type,qcc_value); +CREATE INDEX /*i*/qcc_title ON /*_*/querycachetwo (qcc_type,qcc_namespace,qcc_title); +CREATE INDEX /*i*/qcc_titletwo ON /*_*/querycachetwo (qcc_type,qcc_namespacetwo,qcc_titletwo); +CREATE TABLE /*_*/page_restrictions ( + pr_page int NOT NULL, + pr_type varbinary(60) NOT NULL, + pr_level varbinary(60) NOT NULL, + pr_cascade tinyint NOT NULL, + pr_user int NULL, + pr_expiry varbinary(14) NULL, + pr_id int unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pr_pagetype ON /*_*/page_restrictions (pr_page,pr_type); +CREATE INDEX /*i*/pr_typelevel ON /*_*/page_restrictions (pr_type,pr_level); +CREATE INDEX /*i*/pr_level ON /*_*/page_restrictions (pr_level); +CREATE INDEX /*i*/pr_cascade ON /*_*/page_restrictions (pr_cascade); +CREATE TABLE /*_*/protected_titles ( + pt_namespace int NOT NULL, + pt_title varchar(255) binary NOT NULL, + pt_user int unsigned NOT NULL, + pt_reason tinyblob, + pt_timestamp binary(14) NOT NULL, + pt_expiry varbinary(14) NOT NULL default '', + pt_create_perm varbinary(60) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pt_namespace_title ON /*_*/protected_titles (pt_namespace,pt_title); +CREATE INDEX /*i*/pt_timestamp ON /*_*/protected_titles (pt_timestamp); +CREATE TABLE /*_*/page_props ( + pp_page int NOT NULL, + pp_propname varbinary(60) NOT NULL, + pp_value blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/pp_page_propname ON /*_*/page_props (pp_page,pp_propname); +CREATE TABLE /*_*/updatelog ( + ul_key varchar(255) NOT NULL PRIMARY KEY, + ul_value blob +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/change_tag ( + ct_rc_id int NULL, + ct_log_id int NULL, + ct_rev_id int NULL, + ct_tag varchar(255) NOT NULL, + ct_params blob NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/change_tag_rc_tag ON /*_*/change_tag (ct_rc_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_log_tag ON /*_*/change_tag (ct_log_id,ct_tag); +CREATE UNIQUE INDEX /*i*/change_tag_rev_tag ON /*_*/change_tag (ct_rev_id,ct_tag); +CREATE INDEX /*i*/change_tag_tag_id ON /*_*/change_tag (ct_tag,ct_rc_id,ct_rev_id,ct_log_id); +CREATE TABLE /*_*/tag_summary ( + ts_rc_id int NULL, + ts_log_id int NULL, + ts_rev_id int NULL, + ts_tags blob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/tag_summary_rc_id ON /*_*/tag_summary (ts_rc_id); +CREATE UNIQUE INDEX /*i*/tag_summary_log_id ON /*_*/tag_summary (ts_log_id); +CREATE UNIQUE INDEX /*i*/tag_summary_rev_id ON /*_*/tag_summary (ts_rev_id); +CREATE TABLE /*_*/valid_tag ( + vt_tag varchar(255) NOT NULL PRIMARY KEY +) /*$wgDBTableOptions*/; +CREATE TABLE /*_*/l10n_cache ( + lc_lang varbinary(32) NOT NULL, + lc_key varchar(255) NOT NULL, + lc_value mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE INDEX /*i*/lc_lang_key ON /*_*/l10n_cache (lc_lang, lc_key); +CREATE TABLE /*_*/msg_resource ( + mr_resource varbinary(255) NOT NULL, + mr_lang varbinary(32) NOT NULL, + mr_blob mediumblob NOT NULL, + mr_timestamp binary(14) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mr_resource_lang ON /*_*/msg_resource (mr_resource, mr_lang); +CREATE TABLE /*_*/msg_resource_links ( + mrl_resource varbinary(255) NOT NULL, + mrl_message varbinary(255) NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/mrl_message_resource ON /*_*/msg_resource_links (mrl_message, mrl_resource); +CREATE TABLE /*_*/module_deps ( + md_module varbinary(255) NOT NULL, + md_skin varbinary(32) NOT NULL, + md_deps mediumblob NOT NULL +) /*$wgDBTableOptions*/; +CREATE UNIQUE INDEX /*i*/md_module_skin ON /*_*/module_deps (md_module, md_skin); diff --git a/tests/phpunit/includes/installer/InstallDocFormatterTest.php b/tests/phpunit/includes/installer/InstallDocFormatterTest.php new file mode 100644 index 00000000..56485d3e --- /dev/null +++ b/tests/phpunit/includes/installer/InstallDocFormatterTest.php @@ -0,0 +1,64 @@ +<?php +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +class InstallDocFormatterTest extends MediaWikiTestCase { + /** + * @covers InstallDocFormatter::format + * @dataProvider provideDocFormattingTests + */ + function testFormat( $expected, $unformattedText, $message = '' ) { + $this->assertEquals( + $expected, + InstallDocFormatter::format( $unformattedText ), + $message + ); + } + + /** + * Provider for testFormat() + */ + function provideDocFormattingTests() { + # Format: (expected string, unformattedText string, optional message) + return array( + # Escape some wikitext + array( 'Install <tag>' , 'Install <tag>', 'Escaping <' ), + array( 'Install {{template}}' , 'Install {{template}}', 'Escaping [[' ), + array( 'Install [[page]]' , 'Install [[page]]', 'Escaping {{' ), + array( 'Install ' , "Install \r", 'Removing \r' ), + + # Transform \t{1,2} into :{1,2} + array( ':One indentation', "\tOne indentation", 'Replacing a single \t' ), + array( '::Two indentations', "\t\tTwo indentations", 'Replacing 2 x \t' ), + + # Transform 'bug 123' links + array( + '<span class="config-plainlink">[https://bugzilla.wikimedia.org/123 bug 123]</span>', + 'bug 123', 'Testing bug 123 links' ), + array( + '(<span class="config-plainlink">[https://bugzilla.wikimedia.org/987654 bug 987654]</span>)', + '(bug 987654)', 'Testing (bug 987654) links' ), + + # "bug abc" shouldn't work + array( 'bug foobar', 'bug foobar', "Don't match bug followed by non-digits" ), + array( 'bug !!fakefake!!', 'bug !!fakefake!!', "Don't match bug followed by non-digits" ), + + # Transform '$wgFooBar' links + array( + '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFooBar $wgFooBar]</span>', + '$wgFooBar', 'Testing basic $wgFooBar' ), + array( + '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFooBar45 $wgFooBar45]</span>', + '$wgFooBar45', 'Testing $wgFooBar45 (with numbers)' ), + array( + '<span class="config-plainlink">[http://www.mediawiki.org/wiki/Manual:$wgFoo_Bar $wgFoo_Bar]</span>', + '$wgFoo_Bar', 'Testing $wgFoo_Bar (with underscore)' ), + + # Icky variables that shouldn't link + array( '$myAwesomeVariable', '$myAwesomeVariable', 'Testing $myAwesomeVariable (not starting with $wg)' ), + array( '$()not!a&Var', '$()not!a&Var', 'Testing $()not!a&Var (obviously not a variable)' ), + ); + } +} diff --git a/tests/phpunit/includes/libs/IEUrlExtensionTest.php b/tests/phpunit/includes/libs/IEUrlExtensionTest.php new file mode 100644 index 00000000..c6270e90 --- /dev/null +++ b/tests/phpunit/includes/libs/IEUrlExtensionTest.php @@ -0,0 +1,118 @@ +<?php + +/** + * Tests for IEUrlExtension::findIE6Extension + */ +class IEUrlExtensionTest extends MediaWikiTestCase { + function testSimple() { + $this->assertEquals( + 'y', + IEUrlExtension::findIE6Extension( 'x.y' ), + 'Simple extension' + ); + } + + function testSimpleNoExt() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'x' ), + 'No extension' + ); + } + + function testEmpty() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '' ), + 'Empty string' + ); + } + + function testQuestionMark() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '?' ), + 'Question mark only' + ); + } + + function testExtQuestionMark() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '.x?' ), + 'Extension then question mark' + ); + } + + function testQuestionMarkExt() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '?.x' ), + 'Question mark then extension' + ); + } + + function testInvalidChar() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '.x*' ), + 'Extension with invalid character' + ); + } + + function testInvalidCharThenExtension() { + $this->assertEquals( + 'x', + IEUrlExtension::findIE6Extension( '*.x' ), + 'Invalid character followed by an extension' + ); + } + + function testMultipleQuestionMarks() { + $this->assertEquals( + 'c', + IEUrlExtension::findIE6Extension( 'a?b?.c?.d?e?f' ), + 'Multiple question marks' + ); + } + + function testExeException() { + $this->assertEquals( + 'd', + IEUrlExtension::findIE6Extension( 'a?b?.exe?.d?.e' ), + '.exe exception' + ); + } + + function testExeException2() { + $this->assertEquals( + 'exe', + IEUrlExtension::findIE6Extension( 'a?b?.exe' ), + '.exe exception 2' + ); + } + + function testHash() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'a#b.c' ), + 'Hash character preceding extension' + ); + } + + function testHash2() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( 'a?#b.c' ), + 'Hash character preceding extension 2' + ); + } + + function testDotAtEnd() { + $this->assertEquals( + '', + IEUrlExtension::findIE6Extension( '.' ), + 'Dot at end of string' + ); + } +} diff --git a/tests/phpunit/includes/libs/JavaScriptMinifierTest.php b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php new file mode 100644 index 00000000..aa05500e --- /dev/null +++ b/tests/phpunit/includes/libs/JavaScriptMinifierTest.php @@ -0,0 +1,105 @@ +<?php + +class JavaScriptMinifierTest extends MediaWikiTestCase { + + function provideCases() { + return array( + // Basic tokens + array( "\r\t\f \v\n\r", "" ), + array( "/* Foo *\n*bar\n*/", "" ), + /** + * ' Foo \' bar \ + * baz \' quox ' . + */ + array( "' Foo \\' bar \\\n baz \\' quox ' .length", "' Foo \\' bar \\\n baz \\' quox '.length" ), + array( "\" Foo \\\" bar \\\n baz \\\" quox \" .length", "\" Foo \\\" bar \\\n baz \\\" quox \".length" ), + array( "// Foo b/ar baz", "" ), + array( "/ Foo \\/ bar [ / \\] / ] baz / .length", "/ Foo \\/ bar [ / \\] / ] baz /.length" ), + // HTML comments + array( "<!-- Foo bar", "" ), + array( "<!-- Foo --> bar", "" ), + array( "--> Foo", "" ), + array( "x --> y", "x-->y" ), + // Semicolon insertion + array( "(function(){return\nx;})", "(function(){return\nx;})" ), + array( "throw\nx;", "throw\nx;" ), + array( "while(p){continue\nx;}", "while(p){continue\nx;}" ), + array( "while(p){break\nx;}", "while(p){break\nx;}" ), + array( "var\nx;", "var x;" ), + array( "x\ny;", "x\ny;" ), + array( "x\n++y;", "x\n++y;" ), + array( "x\n!y;", "x\n!y;" ), + array( "x\n{y}", "x\n{y}" ), + array( "x\n+y;", "x+y;" ), + array( "x\n(y);", "x(y);" ), + array( "5.\nx;", "5.\nx;" ), + array( "0xFF.\nx;", "0xFF.x;" ), + array( "5.3.\nx;", "5.3.x;" ), + // Token separation + array( "x in y", "x in y" ), + array( "/x/g in y", "/x/g in y" ), + array( "x in 30", "x in 30" ), + array( "x + ++ y", "x+ ++y" ), + array( "x / /y/.exec(z)", "x/ /y/.exec(z)" ), + // State machine + array( "/ x/g", "/ x/g" ), + array( "(function(){return/ x/g})", "(function(){return/ x/g})" ), + array( "+/ x/g", "+/ x/g" ), + array( "++/ x/g", "++/ x/g" ), + array( "x/ x/g", "x/x/g" ), + array( "(/ x/g)", "(/ x/g)" ), + array( "if(/ x/g);", "if(/ x/g);" ), + array( "(x/ x/g)", "(x/x/g)" ), + array( "([/ x/g])", "([/ x/g])" ), + array( "+x/ x/g", "+x/x/g" ), + array( "{}/ x/g", "{}/ x/g" ), + array( "+{}/ x/g", "+{}/x/g" ), + array( "(x)/ x/g", "(x)/x/g" ), + array( "if(x)/ x/g", "if(x)/ x/g" ), + array( "for(x;x;{}/ x/g);", "for(x;x;{}/x/g);" ), + array( "x;x;{}/ x/g", "x;x;{}/ x/g" ), + array( "x:{}/ x/g", "x:{}/ x/g" ), + array( "switch(x){case y?z:{}/ x/g:{}/ x/g;}", "switch(x){case y?z:{}/x/g:{}/ x/g;}" ), + array( "function x(){}/ x/g", "function x(){}/ x/g" ), + array( "+function x(){}/ x/g", "+function x(){}/x/g" ), + + // Tests for things that broke in the past + // Multiline quoted string + array( "var foo=\"\\\nblah\\\n\";", "var foo=\"\\\nblah\\\n\";" ), + // Multiline quoted string followed by string with spaces + array( "var foo=\"\\\nblah\\\n\";\nvar baz = \" foo \";\n", "var foo=\"\\\nblah\\\n\";var baz=\" foo \";" ), + // URL in quoted string ( // is not a comment) + array( "aNode.setAttribute('href','http://foo.bar.org/baz');", "aNode.setAttribute('href','http://foo.bar.org/baz');" ), + // URL in quoted string after multiline quoted string + array( "var foo=\"\\\nblah\\\n\";\naNode.setAttribute('href','http://foo.bar.org/baz');", "var foo=\"\\\nblah\\\n\";aNode.setAttribute('href','http://foo.bar.org/baz');" ), + // Division vs. regex nastiness + array( "alert( (10+10) / '/'.charCodeAt( 0 ) + '//' );", "alert((10+10)/'/'.charCodeAt(0)+'//');" ), + array( "if(1)/a /g.exec('Pa ss');", "if(1)/a /g.exec('Pa ss');" ), + + // newline insertion after 1000 chars: break after the "++", not before + array( str_repeat( ';', 996 ) . "if(x++);", str_repeat( ';', 996 ) . "if(x++\n);" ), + + // Unicode letter characters should pass through ok in identifiers (bug 31187) + array( "var KaŝSkatolVal = {}", 'var KaŝSkatolVal={}'), + // And also per spec unicode char escape values should work in identifiers, + // as long as it's a valid char. In future it might get normalized. + array( "var Ka\\u015dSkatolVal = {}", 'var Ka\\u015dSkatolVal={}'), + ); + } + + /** + * @dataProvider provideCases + */ + function testJavaScriptMinifierOutput( $code, $expectedOutput ) { + $minified = JavaScriptMinifier::minify( $code ); + + // JSMin+'s parser will throw an exception if output is not valid JS. + // suppression of warnings needed for stupid crap + wfSuppressWarnings(); + $parser = new JSParser(); + wfRestoreWarnings(); + $parser->parse( $minified, 'minify-test.js', 1 ); + + $this->assertEquals( $expectedOutput, $minified, "Minified output should be in the form expected." ); + } +} diff --git a/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php new file mode 100644 index 00000000..a0d5cd86 --- /dev/null +++ b/tests/phpunit/includes/media/BitmapMetadataHandlerTest.php @@ -0,0 +1,125 @@ +<?php +class BitmapMetadataHandlerTest extends MediaWikiTestCase { + + public function setUp() { + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + } + + /** + * Test if having conflicting metadata values from different + * types of metadata, that the right one takes precedence. + * + * Basically the file has IPTC and XMP metadata, the + * IPTC should override the XMP, except for the multilingual + * translation (to en) where XMP should win. + */ + public function testMultilingualCascade() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + '/Xmp-exif-multilingual_test.jpg' ); + + $expected = array( + 'x-default' => 'right(iptc)', + 'en' => 'right translation', + '_type' => 'lang' + ); + + $this->assertArrayHasKey( 'ImageDescription', $meta, + 'Did not extract any ImageDescription info?!' ); + + $this->assertEquals( $expected, $meta['ImageDescription'] ); + } + + /** + * Test for jpeg comments are being handled by + * BitmapMetadataHandler correctly. + * + * There's more extensive tests of comment extraction in + * JpegMetadataExtractorTests.php + */ + public function testJpegComment() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'jpeg-comment-utf.jpg' ); + + $this->assertEquals( 'UTF-8 JPEG Comment — ¼', + $meta['JPEGFileComment'][0] ); + } + + public function testIPTCDates() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-timetest.jpg' ); + + $this->assertEquals( '2020:07:14 01:36:05', $meta['DateTimeDigitized'] ); + $this->assertEquals( '1997:03:02 00:01:02', $meta['DateTimeOriginal'] ); + } + /* File has an invalid time (+ one valid but really weird time) + * that shouldn't be included + */ + public function testIPTCDatesInvalid() { + $meta = BitmapMetadataHandler::Jpeg( $this->filePath . + 'iptc-timetest-invalid.jpg' ); + + $this->assertEquals( '1845:03:02 00:01:02', $meta['DateTimeOriginal'] ); + $this->assertFalse( isset( $meta['DateTimeDigitized'] ) ); + } + + /** + * XMP data should take priority over iptc data + * when hash has been updated, but not when + * the hash is wrong. + */ + public function testMerging() { + $merger = new BitmapMetadataHandler(); + $merger->addMetadata( array( 'foo' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'bar' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'baz' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'fred' => 'xmp' ), 'xmp-general' ); + $merger->addMetadata( array( 'foo' => 'iptc (hash)' ), 'iptc-good-hash' ); + $merger->addMetadata( array( 'bar' => 'iptc (bad hash)' ), 'iptc-bad-hash' ); + $merger->addMetadata( array( 'baz' => 'iptc (bad hash)' ), 'iptc-bad-hash' ); + $merger->addMetadata( array( 'fred' => 'iptc (no hash)' ), 'iptc-no-hash' ); + $merger->addMetadata( array( 'baz' => 'exif' ), 'exif' ); + + $actual = $merger->getMetadataArray(); + $expected = array( + 'foo' => 'xmp', + 'bar' => 'iptc (bad hash)', + 'baz' => 'exif', + 'fred' => 'xmp', + ); + $this->assertEquals( $expected, $actual ); + } + + public function testPNGXMP() { + $handler = new BitmapMetadataHandler(); + $result = $handler->png( $this->filePath . 'xmp.png' ); + $expected = array ( + 'frameCount' => 0, + 'loopCount' => 1, + 'duration' => 0, + 'bitDepth' => 1, + 'colorType' => 'index-coloured', + 'metadata' => array ( + 'SerialNumber' => '123456789', + '_MW_PNG_VERSION' => 1, + ), + ); + $this->assertEquals( $expected, $result ); + } + public function testPNGNative() { + $handler = new BitmapMetadataHandler(); + $result = $handler->png( $this->filePath . 'Png-native-test.png' ); + $expected = 'http://example.com/url'; + $this->assertEquals( $expected, $result['metadata']['Identifier']['x-default'] ); + } + public function testTiffByteOrder() { + $handler = new BitmapMetadataHandler(); + $res = $handler->getTiffByteOrder( $this->filePath . 'test.tiff' ); + $this->assertEquals( 'LE', $res ); + } + +} diff --git a/tests/phpunit/includes/media/BitmapScalingTest.php b/tests/phpunit/includes/media/BitmapScalingTest.php new file mode 100644 index 00000000..5bcd3232 --- /dev/null +++ b/tests/phpunit/includes/media/BitmapScalingTest.php @@ -0,0 +1,136 @@ +<?php + +class BitmapScalingTest extends MediaWikiTestCase { + + function setUp() { + global $wgMaxImageArea; + $this->oldMaxImageArea = $wgMaxImageArea; + $wgMaxImageArea = 1.25e7; // 3500x3500 + } + function tearDown() { + global $wgMaxImageArea; + $wgMaxImageArea = $this->oldMaxImageArea; + } + /** + * @dataProvider provideNormaliseParams + */ + function testNormaliseParams( $fileDimensions, $expectedParams, $params, $msg ) { + $file = new FakeDimensionFile( $fileDimensions ); + $handler = new BitmapHandler; + $valid = $handler->normaliseParams( $file, $params ); + $this->assertTrue( $valid ); + $this->assertEquals( $expectedParams, $params, $msg ); + } + + function provideNormaliseParams() { + return array( + /* Regular resize operations */ + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 512 ), + 'Resizing with width set', + ), + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 512, 'height' => 768 ), + 'Resizing with height set too high', + ), + array( + array( 1024, 768 ), + array( + 'width' => 512, 'height' => 384, + 'physicalWidth' => 512, 'physicalHeight' => 384, + 'page' => 1, + ), + array( 'width' => 1024, 'height' => 384 ), + 'Resizing with height set', + ), + + /* Very tall images */ + array( + array( 1000, 100 ), + array( + 'width' => 5, 'height' => 1, + 'physicalWidth' => 5, 'physicalHeight' => 1, + 'page' => 1, + ), + array( 'width' => 5 ), + 'Very wide image', + ), + + array( + array( 100, 1000 ), + array( + 'width' => 1, 'height' => 10, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, + ), + array( 'width' => 1 ), + 'Very high image', + ), + array( + array( 100, 1000 ), + array( + 'width' => 1, 'height' => 5, + 'physicalWidth' => 1, 'physicalHeight' => 10, + 'page' => 1, + ), + array( 'width' => 10, 'height' => 5 ), + 'Very high image with height set', + ), + /* Max image area */ + array( + array( 4000, 4000 ), + array( + 'width' => 5000, 'height' => 5000, + 'physicalWidth' => 4000, 'physicalHeight' => 4000, + 'page' => 1, + ), + array( 'width' => 5000 ), + 'Bigger than max image size but doesn\'t need scaling', + ), + ); + } + function testTooBigImage() { + $file = new FakeDimensionFile( array( 4000, 4000 ) ); + $handler = new BitmapHandler; + $params = array( 'width' => '3700' ); // Still bigger than max size. + $this->assertFalse( $handler->normaliseParams( $file, $params ) ); + } + function testTooBigMustRenderImage() { + $file = new FakeDimensionFile( array( 4000, 4000 ) ); + $file->mustRender = true; + $handler = new BitmapHandler; + $params = array( 'width' => '5000' ); // Still bigger than max size. + $this->assertFalse( $handler->normaliseParams( $file, $params ) ); + } +} + +class FakeDimensionFile extends File { + public $mustRender = false; + + public function __construct( $dimensions ) { + parent::__construct( Title::makeTitle( NS_FILE, 'Test' ), null ); + + $this->dimensions = $dimensions; + } + public function getWidth( $page = 1 ) { + return $this->dimensions[0]; + } + public function getHeight( $page = 1 ) { + return $this->dimensions[1]; + } + public function mustRender() { + return $this->mustRender; + } +} diff --git a/tests/phpunit/includes/media/ExifBitmapTest.php b/tests/phpunit/includes/media/ExifBitmapTest.php new file mode 100644 index 00000000..4282d3c8 --- /dev/null +++ b/tests/phpunit/includes/media/ExifBitmapTest.php @@ -0,0 +1,122 @@ +<?php +class ExifBitmapTest extends MediaWikiTestCase { + + public function setUp() { + global $wgShowEXIF; + $this->showExif = $wgShowEXIF; + $wgShowEXIF = true; + $this->handler = new ExifBitmapHandler; + if ( !wfDl( 'exif' ) ) { + $this->markTestSkipped( "This test needs the exif extension." ); + } + } + + public function tearDown() { + global $wgShowEXIF; + $wgShowEXIF = $this->showExif; + } + + public function testIsOldBroken() { + if ( !wfDl( 'exif' ) ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::OLD_BROKEN_FILE ); + $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res ); + } + public function testIsBrokenFile() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $res = $this->handler->isMetadataValid( null, ExifBitmapHandler::BROKEN_FILE ); + $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); + } + public function testIsInvalid() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $res = $this->handler->isMetadataValid( null, 'Something Invalid Here.' ); + $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); + } + public function testGoodMetadata() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_GOOD, $res ); + } + public function testIsOldGood() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $meta = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_COMPATIBLE, $res ); + } + // Handle metadata from paged tiff handler (gotten via instant commons) + // gracefully. + public function testPagedTiffHandledGracefully() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $meta = 'a:6:{s:9:"page_data";a:1:{i:1;a:5:{s:5:"width";i:643;s:6:"height";i:448;s:5:"alpha";s:4:"true";s:4:"page";i:1;s:6:"pixels";i:288064;}}s:10:"page_count";i:1;s:10:"first_page";i:1;s:9:"last_page";i:1;s:4:"exif";a:9:{s:10:"ImageWidth";i:643;s:11:"ImageLength";i:448;s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:4;s:12:"RowsPerStrip";i:50;s:19:"PlanarConfiguration";i:1;s:22:"MEDIAWIKI_EXIF_VERSION";i:1;}s:21:"TIFF_METADATA_VERSION";s:3:"1.4";}'; + $res = $this->handler->isMetadataValid( null, $meta ); + $this->assertEquals( ExifBitmapHandler::METADATA_BAD, $res ); + } + + function testConvertMetadataLatest() { + $metadata = array( + 'foo' => array( 'First', 'Second', '_type' => 'ol' ), + 'MEDIAWIKI_EXIF_VERSION' => 2 + ); + $res = $this->handler->convertMetadataVersion( $metadata, 2 ); + $this->assertEquals( $metadata, $res ); + } + function testConvertMetadataToOld() { + $metadata = array( + 'foo' => array( 'First', 'Second', '_type' => 'ol' ), + 'bar' => array( 'First', 'Second', '_type' => 'ul' ), + 'baz' => array( 'First', 'Second' ), + 'fred' => 'Single', + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'foo' => "\n#First\n#Second", + 'bar' => "\n*First\n*Second", + 'baz' => "\n*First\n*Second", + 'fred' => 'Single', + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } + function testConvertMetadataSoftware() { + $metadata = array( + 'Software' => array( array('GIMP', '1.1' ) ), + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'Software' => 'GIMP (Version 1.1)', + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } + function testConvertMetadataSoftwareNormal() { + $metadata = array( + 'Software' => array( "GIMP 1.2", "vim" ), + 'MEDIAWIKI_EXIF_VERSION' => 2, + ); + $expected = array( + 'Software' => "\n*GIMP 1.2\n*vim", + 'MEDIAWIKI_EXIF_VERSION' => 1, + ); + $res = $this->handler->convertMetadataVersion( $metadata, 1 ); + $this->assertEquals( $expected, $res ); + } +} diff --git a/tests/phpunit/includes/media/ExifRotationTest.php b/tests/phpunit/includes/media/ExifRotationTest.php new file mode 100644 index 00000000..639091d0 --- /dev/null +++ b/tests/phpunit/includes/media/ExifRotationTest.php @@ -0,0 +1,249 @@ +<?php + +/** + * Tests related to auto rotation + */ +class ExifRotationTest extends MediaWikiTestCase { + + function setUp() { + parent::setUp(); + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->handler = new BitmapHandler(); + $this->repo = new FSRepo(array( + 'name' => 'temp', + 'directory' => wfTempDir() . '/exif-test-' . time() . '-' . mt_rand(), + 'url' => 'http://localhost/thumbtest' + )); + if ( !wfDl( 'exif' ) ) { + $this->markTestSkipped( "This test needs the exif extension." ); + } + global $wgShowEXIF; + $this->show = $wgShowEXIF; + $wgShowEXIF = true; + + global $wgEnableAutoRotation; + $this->oldAuto = $wgEnableAutoRotation; + $wgEnableAutoRotation = true; + } + public function tearDown() { + global $wgShowEXIF, $wgEnableAutoRotation; + $wgShowEXIF = $this->show; + $wgEnableAutoRotation = $this->oldAuto; + } + + /** + * + * @dataProvider providerFiles + */ + function testMetadata( $name, $type, $info ) { + if ( !BitmapHandler::canRotate() ) { + $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." ); + } + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + } + + /** + * + * @dataProvider providerFiles + */ + function testRotationRendering( $name, $type, $info, $thumbs ) { + if ( !BitmapHandler::canRotate() ) { + $this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." ); + } + foreach( $thumbs as $size => $out ) { + if( preg_match('/^(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + ); + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + 'height' => $matches[2] + ); + } else { + throw new MWException('bogus test data format ' . $size); + } + + $file = $this->localFile( $name, $type ); + $thumb = $file->transform( $params, File::RENDER_NOW ); + + $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" ); + $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" ); + + $gis = getimagesize( $thumb->getPath() ); + if ($out[0] > $info['width']) { + // Physical image won't be scaled bigger than the original. + $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size"); + $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size"); + } else { + $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size"); + $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size"); + } + } + } + + private function localFile( $name, $type ) { + return new UnregisteredLocalFile( false, $this->repo, $this->filePath . $name, $type ); + } + + function providerFiles() { + return array( + array( + 'landscape-plain.jpg', + 'image/jpeg', + array( + 'width' => 1024, + 'height' => 768, + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ), + array( + 'portrait-rotated.jpg', + 'image/jpeg', + array( + 'width' => 768, // as rotated + 'height' => 1024, // as rotated + ), + array( + '800x600px' => array( 450, 600 ), + '9999x800px' => array( 600, 800 ), + '800px' => array( 800, 1067 ), + '600px' => array( 600, 800 ), + ) + ) + ); + } + + /** + * Same as before, but with auto-rotation disabled. + * @dataProvider providerFilesNoAutoRotate + */ + function testMetadataNoAutoRotate( $name, $type, $info ) { + global $wgEnableAutoRotation; + $wgEnableAutoRotation = false; + + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $name, $type ); + $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" ); + $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" ); + + $wgEnableAutoRotation = true; + } + + /** + * + * @dataProvider providerFilesNoAutoRotate + */ + function testRotationRenderingNoAutoRotate( $name, $type, $info, $thumbs ) { + global $wgEnableAutoRotation; + $wgEnableAutoRotation = false; + + foreach( $thumbs as $size => $out ) { + if( preg_match('/^(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + ); + } elseif ( preg_match( '/^(\d+)x(\d+)px$/', $size, $matches ) ) { + $params = array( + 'width' => $matches[1], + 'height' => $matches[2] + ); + } else { + throw new MWException('bogus test data format ' . $size); + } + + $file = $this->localFile( $name, $type ); + $thumb = $file->transform( $params, File::RENDER_NOW ); + + $this->assertEquals( $out[0], $thumb->getWidth(), "$name: thumb reported width check for $size" ); + $this->assertEquals( $out[1], $thumb->getHeight(), "$name: thumb reported height check for $size" ); + + $gis = getimagesize( $thumb->getPath() ); + if ($out[0] > $info['width']) { + // Physical image won't be scaled bigger than the original. + $this->assertEquals( $info['width'], $gis[0], "$name: thumb actual width check for $size"); + $this->assertEquals( $info['height'], $gis[1], "$name: thumb actual height check for $size"); + } else { + $this->assertEquals( $out[0], $gis[0], "$name: thumb actual width check for $size"); + $this->assertEquals( $out[1], $gis[1], "$name: thumb actual height check for $size"); + } + } + $wgEnableAutoRotation = true; + } + + function providerFilesNoAutoRotate() { + return array( + array( + 'landscape-plain.jpg', + 'image/jpeg', + array( + 'width' => 1024, + 'height' => 768, + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ), + array( + 'portrait-rotated.jpg', + 'image/jpeg', + array( + 'width' => 1024, // since not rotated + 'height' => 768, // since not rotated + ), + array( + '800x600px' => array( 800, 600 ), + '9999x800px' => array( 1067, 800 ), + '800px' => array( 800, 600 ), + '600px' => array( 600, 450 ), + ) + ) + ); + } + + + const TEST_WIDTH = 100; + const TEST_HEIGHT = 200; + + /** + * @dataProvider provideBitmapExtractPreRotationDimensions + */ + function testBitmapExtractPreRotationDimensions( $rotation, $expected ) { + $result = $this->handler->extractPreRotationDimensions( array( + 'physicalWidth' => self::TEST_WIDTH, + 'physicalHeight' => self::TEST_HEIGHT, + ), $rotation ); + $this->assertEquals( $expected, $result ); + } + + function provideBitmapExtractPreRotationDimensions() { + return array( + array( + 0, + array( self::TEST_WIDTH, self::TEST_HEIGHT ) + ), + array( + 90, + array( self::TEST_HEIGHT, self::TEST_WIDTH ) + ), + array( + 180, + array( self::TEST_WIDTH, self::TEST_HEIGHT ) + ), + array( + 270, + array( self::TEST_HEIGHT, self::TEST_WIDTH ) + ), + ); + } +} + diff --git a/tests/phpunit/includes/media/ExifTest.php b/tests/phpunit/includes/media/ExifTest.php new file mode 100644 index 00000000..9b490e92 --- /dev/null +++ b/tests/phpunit/includes/media/ExifTest.php @@ -0,0 +1,51 @@ +<?php +class ExifTest extends MediaWikiTestCase { + + public function setUp() { + $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/'; + + global $wgShowEXIF; + $this->showExif = $wgShowEXIF; + $wgShowEXIF = true; + } + public function tearDown() { + global $wgShowEXIF; + $wgShowEXIF = $this->showExif; + } + + public function testGPSExtraction() { + if ( !wfDl( 'exif' ) ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + + $filename = $this->mediaPath . 'exif-gps.jpg'; + $seg = JpegMetadataExtractor::segmentSplitter( $filename ); + $exif = new Exif( $filename, $seg['byteOrder'] ); + $data = $exif->getFilteredData(); + $expected = array( + 'GPSLatitude' => 88.5180555556, + 'GPSLongitude' => -21.12357, + 'GPSAltitude' => -200, + 'GPSDOP' => '5/1', + 'GPSVersionID' => '2.2.0.0', + ); + $this->assertEquals( $expected, $data, '', 0.0000000001 ); + } + public function testUnicodeUserComment() { + if ( !wfDl( 'exif' ) ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + + $filename = $this->mediaPath . 'exif-user-comment.jpg'; + $seg = JpegMetadataExtractor::segmentSplitter( $filename ); + $exif = new Exif( $filename, $seg['byteOrder'] ); + $data = $exif->getFilteredData(); + + $expected = array( + 'UserComment' => 'test⁔comment' + ); + $this->assertEquals( $expected, $data ); + } + + +} diff --git a/tests/phpunit/includes/media/FormatMetadataTest.php b/tests/phpunit/includes/media/FormatMetadataTest.php new file mode 100644 index 00000000..db36dea3 --- /dev/null +++ b/tests/phpunit/includes/media/FormatMetadataTest.php @@ -0,0 +1,29 @@ +<?php +class FormatMetadataTest extends MediaWikiTestCase { + public function testInvalidDate() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + + $file = UnregisteredLocalFile::newFromPath( dirname( __FILE__ ) . + '/../../data/media/broken_exif_date.jpg', 'image/jpeg' ); + + // Throws an error if bug hit + $meta = $file->formatMetadata(); + $this->assertNotEquals( false, $meta, 'Valid metadata extracted' ); + + // Find date exif entry + $this->assertArrayHasKey( 'visible', $meta ); + $dateIndex = null; + foreach ( $meta['visible'] as $i => $data ) { + if ( $data['id'] == 'exif-datetimeoriginal' ) { + $dateIndex = $i; + } + } + $this->assertNotNull( $dateIndex, 'Date entry exists in metadata' ); + $this->assertEquals( '0000:01:00 00:02:27', + $meta['visible'][$dateIndex]['value'], + 'File with invalid date metadata (bug 29471)' ); + } +}
\ No newline at end of file diff --git a/tests/phpunit/includes/media/GIFMetadataExtractorTest.php b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php new file mode 100644 index 00000000..59b30441 --- /dev/null +++ b/tests/phpunit/includes/media/GIFMetadataExtractorTest.php @@ -0,0 +1,95 @@ +<?php +class GIFMetadataExtractorTest extends MediaWikiTestCase { + + public function setUp() { + $this->mediaPath = dirname( __FILE__ ) . '/../../data/media/'; + } + /** + * Put in a file, and see if the metadata coming out is as expected. + * @param $filename String + * @param $expected Array The extracted metadata. + * @dataProvider dataGetMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $actual = GIFMetadataExtractor::getMetadata( $this->mediaPath . $filename ); + $this->assertEquals( $expected, $actual ); + } + public function dataGetMetadata() { + + $xmpNugget = <<<EOF +<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?> +<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 7.30'> +<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'> + + <rdf:Description rdf:about='' + xmlns:Iptc4xmpCore='http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/'> + <Iptc4xmpCore:Location>The interwebs</Iptc4xmpCore:Location> + </rdf:Description> + + <rdf:Description rdf:about='' + xmlns:tiff='http://ns.adobe.com/tiff/1.0/'> + <tiff:Artist>Bawolff</tiff:Artist> + <tiff:ImageDescription> + <rdf:Alt> + <rdf:li xml:lang='x-default'>A file to test GIF</rdf:li> + </rdf:Alt> + </tiff:ImageDescription> + </rdf:Description> +</rdf:RDF> +</x:xmpmeta> + + + + + + + + + + + + + + + + + + + + + + + + +<?xpacket end='w'?> +EOF; + + return array( + array( 'nonanimated.gif', array( + 'comment' => array( 'GIF test file ⁕ Created with GIMP' ), + 'duration' => 0.1, + 'frameCount' => 1, + 'looped' => false, + 'xmp' => '', + ) + ), + array( 'animated.gif', array( + 'comment' => array( 'GIF test file . Created with GIMP' ), + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'xmp' => '', + ) + ), + + array( 'animated-xmp.gif', array( + 'xmp' => $xmpNugget, + 'duration' => 2.4, + 'frameCount' => 4, + 'looped' => true, + 'comment' => array( 'GIƒ·test·file' ), + ) + ), + ); + } +} diff --git a/tests/phpunit/includes/media/GIFTest.php b/tests/phpunit/includes/media/GIFTest.php new file mode 100644 index 00000000..42c25ca5 --- /dev/null +++ b/tests/phpunit/includes/media/GIFTest.php @@ -0,0 +1,85 @@ +<?php +class GIFHandlerTest extends MediaWikiTestCase { + + public function setUp() { + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->handler = new GIFHandler(); + } + + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . 'README' ); + $this->assertEquals( GIFHandler::BROKEN_FILE, $res ); + } + /** + * @param $filename String basename of the file to check + * @param $expected boolean Expected result. + * @dataProvider dataIsAnimated + */ + public function testIsAnimanted( $filename, $expected ) { + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename, + 'image/gif' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + public function dataIsAnimated() { + return array( + array( 'animated.gif', true ), + array( 'nonanimated.gif', false ), + ); + } + + /** + * @param $filename String + * @param $expected Integer Total image area + * @dataProvider dataGetImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename, + 'image/gif' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + public function dataGetImageArea() { + return array( + array( 'animated.gif', 5400 ), + array( 'nonanimated.gif', 1350 ), + ); + } + + /** + * @param $metadata String Serialized metadata + * @param $expected Integer One of the class constants of GIFHandler + * @dataProvider dataIsMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + public function dataIsMetadataValid() { + return array( + array( GIFHandler::BROKEN_FILE, GIFHandler::METADATA_GOOD ), + array( '', GIFHandler::METADATA_BAD ), + array( null, GIFHandler::METADATA_BAD ), + array( 'Something invalid!', GIFHandler::METADATA_BAD ), + array( 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}', GIFHandler::METADATA_GOOD ), + ); + } + + /** + * @param $filename String + * @param $expected String Serialized array + * @dataProvider dataGetMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename, + 'image/gif' ); + $actual = $this->handler->getMetadata( $file, $this->filePath . $filename ); + $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + } + public function dataGetMetadata() { + return array( + array( 'nonanimated.gif', 'a:4:{s:10:"frameCount";i:1;s:6:"looped";b:0;s:8:"duration";d:0.1000000000000000055511151231257827021181583404541015625;s:8:"metadata";a:2:{s:14:"GIFFileComment";a:1:{i:0;s:35:"GIF test file ⁕ Created with GIMP";}s:15:"_MW_GIF_VERSION";i:1;}}' ), + array( 'animated-xmp.gif', 'a:4:{s:10:"frameCount";i:4;s:6:"looped";b:1;s:8:"duration";d:2.399999999999999911182158029987476766109466552734375;s:8:"metadata";a:5:{s:6:"Artist";s:7:"Bawolff";s:16:"ImageDescription";a:2:{s:9:"x-default";s:18:"A file to test GIF";s:5:"_type";s:4:"lang";}s:15:"SublocationDest";s:13:"The interwebs";s:14:"GIFFileComment";a:1:{i:0;s:16:"GIƒ·test·file";}s:15:"_MW_GIF_VERSION";i:1;}}' ), + ); + } +} diff --git a/tests/phpunit/includes/media/IPTCTest.php b/tests/phpunit/includes/media/IPTCTest.php new file mode 100644 index 00000000..ec6deeb8 --- /dev/null +++ b/tests/phpunit/includes/media/IPTCTest.php @@ -0,0 +1,55 @@ +<?php +class IPTCTest extends MediaWikiTestCase { + public function testRecognizeUtf8() { + // utf-8 is the only one used in practise. + $res = IPTC::getCharset( "\x1b%G" ); + $this->assertEquals( 'UTF-8', $res ); + } + + public function testIPTCParseNoCharset88591() { + // basically IPTC for keyword with value of 0xBC which is 1/4 in iso-8859-1 + // This data doesn't specify a charset. We're supposed to guess + // (which basically means utf-8 if valid, windows 1252 (iso 8859-1) if not) + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x06\x1c\x02\x19\x00\x01\xBC"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } + /* This one contains a sequence that's valid iso 8859-1 but not valid utf8 */ + /* \xC3 = Ã, \xB8 = ¸ */ + public function testIPTCParseNoCharset88591b() { + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x09\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( 'ÃÃø' ), $res['Keywords'] ); + } + /* Same as testIPTCParseNoCharset88591b, but forcing the charset to utf-8. + * What should happen is the first "\xC3\xC3" should be dropped as invalid, + * leaving \xC3\xB8, which is ø + */ + public function testIPTCParseForcedUTFButInvalid() { + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x11\x1c\x02\x19\x00\x04\xC3\xC3\xC3\xB8" + . "\x1c\x01\x5A\x00\x03\x1B\x25\x47"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( 'ø' ), $res['Keywords'] ); + } + public function testIPTCParseNoCharsetUTF8() { + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x07\x1c\x02\x19\x00\x02¼"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } + // Testing something that has 2 values for keyword + public function testIPTCParseMulti() { + $iptcData = /* identifier */ "Photoshop 3.0\08BIM\4\4" + /* length */ . "\0\0\0\0\0\x0D" + . "\x1c\x02\x19" . "\x00\x01" . "\xBC" + . "\x1c\x02\x19" . "\x00\x02" . "\xBC\xBD"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼', '¼½' ), $res['Keywords'] ); + } + public function testIPTCParseUTF8() { + // This has the magic "\x1c\x01\x5A\x00\x03\x1B\x25\x47" which marks content as UTF8. + $iptcData = "Photoshop 3.0\08BIM\4\4\0\0\0\0\0\x0F\x1c\x02\x19\x00\x02¼\x1c\x01\x5A\x00\x03\x1B\x25\x47"; + $res = IPTC::Parse( $iptcData ); + $this->assertEquals( array( '¼' ), $res['Keywords'] ); + } + +} diff --git a/tests/phpunit/includes/media/JpegMetadataExtractorTest.php b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php new file mode 100644 index 00000000..61fc9c81 --- /dev/null +++ b/tests/phpunit/includes/media/JpegMetadataExtractorTest.php @@ -0,0 +1,94 @@ +<?php +/* + * @todo Could use a test of extended XMP segments. Hard to find programs that + * create example files, and creating my own in vim propbably wouldn't + * serve as a very good "test". (Adobe photoshop probably creates such files + * but it costs money). The implementation of it currently in MediaWiki is based + * solely on reading the standard, without any real world test files. + */ +class JpegMetadataExtractorTest extends MediaWikiTestCase { + + public function setUp() { + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + } + + /** + * We also use this test to test padding bytes don't + * screw stuff up + * + * @param $file filename + * + * @dataProvider dataUtf8Comment + */ + public function testUtf8Comment( $file ) { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . $file ); + $this->assertEquals( array( 'UTF-8 JPEG Comment — ¼' ), $res['COM'] ); + } + public function dataUtf8Comment() { + return array( + array( 'jpeg-comment-utf.jpg' ), + array( 'jpeg-padding-even.jpg' ), + array( 'jpeg-padding-odd.jpg' ), + ); + } + /** The file is iso-8859-1, but it should get auto converted */ + public function testIso88591Comment() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-iso8859-1.jpg' ); + $this->assertEquals( array( 'ISO-8859-1 JPEG Comment - ¼' ), $res['COM'] ); + } + /** Comment values that are non-textual (random binary junk) should not be shown. + * The example test file has a comment with a 0x5 byte in it which is a control character + * and considered binary junk for our purposes. + */ + public function testBinaryCommentStripped() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-binary.jpg' ); + $this->assertEmpty( $res['COM'] ); + } + /* Very rarely a file can have multiple comments. + * Order of comments is based on order inside the file. + */ + public function testMultipleComment() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-comment-multiple.jpg' ); + $this->assertEquals( array( 'foo', 'bar' ), $res['COM'] ); + } + public function testXMPExtraction() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' ); + $this->assertEquals( $expected, $res['XMP'] ); + } + public function testPSIRExtraction() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $expected = '50686f746f73686f7020332e30003842494d04040000000000181c02190004746573741c02190003666f6f1c020000020004'; + $this->assertEquals( $expected, bin2hex( $res['PSIR'] ) ); + } + public function testXMPExtractionAltAppId() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-alt.jpg' ); + $expected = file_get_contents( $this->filePath . 'jpeg-xmp-psir.xmp' ); + $this->assertEquals( $expected, $res['XMP'] ); + } + + + public function testIPTCHashComparisionNoHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-psir.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'] ); + + $this->assertEquals( 'iptc-no-hash', $res ); + } + public function testIPTCHashComparisionBadHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-bad-hash.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'] ); + + $this->assertEquals( 'iptc-bad-hash', $res ); + } + public function testIPTCHashComparisionGoodHash() { + $segments = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-iptc-good-hash.jpg' ); + $res = JpegMetadataExtractor::doPSIR( $segments['PSIR'] ); + + $this->assertEquals( 'iptc-good-hash', $res ); + } + public function testExifByteOrder() { + $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'exif-user-comment.jpg' ); + $expected = 'BE'; + $this->assertEquals( $expected, $res['byteOrder'] ); + } +} diff --git a/tests/phpunit/includes/media/JpegTest.php b/tests/phpunit/includes/media/JpegTest.php new file mode 100644 index 00000000..713a3410 --- /dev/null +++ b/tests/phpunit/includes/media/JpegTest.php @@ -0,0 +1,29 @@ +<?php +class JpegTest extends MediaWikiTestCase { + + public function setUp() { + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + } + + public function testInvalidFile() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $jpeg = new JpegHandler; + $res = $jpeg->getMetadata( null, $this->filePath . 'README' ); + $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res ); + } + public function testJpegMetadataExtraction() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $h = new JpegHandler; + $res = $h->getMetadata( null, $this->filePath . 'test.jpg' ); + $expected = 'a:7:{s:16:"ImageDescription";s:9:"Test file";s:11:"XResolution";s:4:"72/1";s:11:"YResolution";s:4:"72/1";s:14:"ResolutionUnit";i:2;s:16:"YCbCrPositioning";i:1;s:15:"JPEGFileComment";a:1:{i:0;s:17:"Created with GIMP";}s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + + // Unserialize in case serialization format ever changes. + $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); + } +} diff --git a/tests/phpunit/includes/media/PNGMetadataExtractorTest.php b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php new file mode 100644 index 00000000..9f702c50 --- /dev/null +++ b/tests/phpunit/includes/media/PNGMetadataExtractorTest.php @@ -0,0 +1,141 @@ +<?php +class PNGMetadataExtractorTest extends MediaWikiTestCase { + + function setUp() { + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + } + /** + * Tests zTXt tag (compressed textual metadata) + */ + function testPngNativetZtxt() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + $expected = "foo bar baz foo foo foo foof foo foo foo foo"; + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'Make', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['Make'] ); + + $this->assertEquals( $expected, $meta['Make']['x-default'] ); + } + + /** + * Test tEXt tag (Uncompressed textual metadata) + */ + function testPngNativeText() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + $expected = "Some long image desc"; + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'ImageDescription', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['ImageDescription'] ); + $this->assertArrayHasKey( '_type', $meta['ImageDescription'] ); + + $this->assertEquals( $expected, $meta['ImageDescription']['x-default'] ); + } + + /** + * tEXt tags must be encoded iso-8859-1 (vs iTXt which are utf-8) + * Make sure non-ascii characters get converted properly + */ + function testPngNativeTextNonAscii() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + // Note the Copyright symbol here is a utf-8 one + // (aka \xC2\xA9) where in the file its iso-8859-1 + // encoded as just \xA9. + $expected = "© 2010 Bawolff"; + + + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + $this->assertArrayHasKey( 'Copyright', $meta ); + $this->assertArrayHasKey( 'x-default', $meta['Copyright'] ); + + $this->assertEquals( $expected, $meta['Copyright']['x-default'] ); + } + + /** + * Test extraction of pHYs tags, which can tell what the + * actual resolution of the image is (aka in dots per meter). + function testPngPhysTag () { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertArrayHasKey( 'text', $meta ); + $meta = $meta['text']; + + $this->assertEquals( '2835/100', $meta['XResolution'] ); + $this->assertEquals( '2835/100', $meta['YResolution'] ); + $this->assertEquals( 3, $meta['ResolutionUnit'] ); // 3 = cm + } + + /** + * Given a normal static PNG, check the animation metadata returned. + */ + function testStaticPngAnimationMetadata() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 0, $meta['frameCount'] ); + $this->assertEquals( 1, $meta['loopCount'] ); + $this->assertEquals( 0, $meta['duration'] ); + } + + /** + * Given an animated APNG image file + * check it gets animated metadata right. + */ + function testApngAnimationMetadata() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Animated_PNG_example_bouncing_beach_ball.png' ); + + $this->assertEquals( 20, $meta['frameCount'] ); + // Note loop count of 0 = infinity + $this->assertEquals( 0, $meta['loopCount'] ); + $this->assertEquals( 1.5, $meta['duration'], '', 0.00001 ); + } + + function testPngBitDepth8() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 8, $meta['bitDepth'] ); + } + function testPngBitDepth1() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + '1bit-png.png' ); + $this->assertEquals( 1, $meta['bitDepth'] ); + } + + + function testPngIndexColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'Png-native-test.png' ); + + $this->assertEquals( 'index-coloured', $meta['colorType'] ); + } + function testPngRgbColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'rgb-png.png' ); + $this->assertEquals( 'truecolour-alpha', $meta['colorType'] ); + } + function testPngRgbNoAlphaColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'rgb-na-png.png' ); + $this->assertEquals( 'truecolour', $meta['colorType'] ); + } + function testPngGreyscaleColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'greyscale-png.png' ); + $this->assertEquals( 'greyscale-alpha', $meta['colorType'] ); + } + function testPngGreyscaleNoAlphaColour() { + $meta = PNGMetadataExtractor::getMetadata( $this->filePath . + 'greyscale-na-png.png' ); + $this->assertEquals( 'greyscale', $meta['colorType'] ); + } + +} diff --git a/tests/phpunit/includes/media/PNGTest.php b/tests/phpunit/includes/media/PNGTest.php new file mode 100644 index 00000000..b782918c --- /dev/null +++ b/tests/phpunit/includes/media/PNGTest.php @@ -0,0 +1,88 @@ +<?php +class PNGHandlerTest extends MediaWikiTestCase { + + public function setUp() { + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->handler = new PNGHandler(); + } + + public function testInvalidFile() { + $res = $this->handler->getMetadata( null, $this->filePath . 'README' ); + $this->assertEquals( PNGHandler::BROKEN_FILE, $res ); + } + /** + * @param $filename String basename of the file to check + * @param $expected boolean Expected result. + * @dataProvider dataIsAnimated + */ + public function testIsAnimanted( $filename, $expected ) { + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename, + 'image/png' ); + $actual = $this->handler->isAnimatedImage( $file ); + $this->assertEquals( $expected, $actual ); + } + public function dataIsAnimated() { + return array( + array( 'Animated_PNG_example_bouncing_beach_ball.png', true ), + array( '1bit-png.png', false ), + ); + } + + /** + * @param $filename String + * @param $expected Integer Total image area + * @dataProvider dataGetImageArea + */ + public function testGetImageArea( $filename, $expected ) { + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename, + 'image/png' ); + $actual = $this->handler->getImageArea( $file, $file->getWidth(), $file->getHeight() ); + $this->assertEquals( $expected, $actual ); + } + public function dataGetImageArea() { + return array( + array( '1bit-png.png', 2500 ), + array( 'greyscale-png.png', 2500 ), + array( 'Png-native-test.png', 126000 ), + array( 'Animated_PNG_example_bouncing_beach_ball.png', 10000 ), + ); + } + + /** + * @param $metadata String Serialized metadata + * @param $expected Integer One of the class constants of PNGHandler + * @dataProvider dataIsMetadataValid + */ + public function testIsMetadataValid( $metadata, $expected ) { + $actual = $this->handler->isMetadataValid( null, $metadata ); + $this->assertEquals( $expected, $actual ); + } + public function dataIsMetadataValid() { + return array( + array( PNGHandler::BROKEN_FILE, PNGHandler::METADATA_GOOD ), + array( '', PNGHandler::METADATA_BAD ), + array( null, PNGHandler::METADATA_BAD ), + array( 'Something invalid!', PNGHandler::METADATA_BAD ), + array( 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}', PNGHandler::METADATA_GOOD ), + ); + } + + /** + * @param $filename String + * @param $expected String Serialized array + * @dataProvider dataGetMetadata + */ + public function testGetMetadata( $filename, $expected ) { + $file = UnregisteredLocalFile::newFromPath( $this->filePath . $filename, + 'image/png' ); + $actual = $this->handler->getMetadata( $file, $this->filePath . $filename ); +// $this->assertEquals( unserialize( $expected ), unserialize( $actual ) ); + $this->assertEquals( ( $expected ), ( $actual ) ); + } + public function dataGetMetadata() { + return array( + array( 'rgb-na-png.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:8;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:1;}}' ), + array( 'xmp.png', 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:1;s:9:"colorType";s:14:"index-coloured";s:8:"metadata";a:2:{s:12:"SerialNumber";s:9:"123456789";s:15:"_MW_PNG_VERSION";i:1;}}' ), + ); + } +} diff --git a/tests/phpunit/includes/media/SVGMetadataExtractorTest.php b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php new file mode 100644 index 00000000..c2c81b98 --- /dev/null +++ b/tests/phpunit/includes/media/SVGMetadataExtractorTest.php @@ -0,0 +1,88 @@ +<?php + +class SVGMetadataExtractorTest extends MediaWikiTestCase { + + function setUp() { + AutoLoader::loadClass( 'SVGMetadataExtractorTest' ); + } + + /** + * @dataProvider providerSvgFiles + */ + function testGetMetadata( $infile, $expected ) { + $this->assertMetadata( $infile, $expected ); + } + + /** + * @dataProvider providerSvgFilesWithXMLMetadata + */ + function testGetXMLMetadata( $infile, $expected ) { + $r = new XMLReader(); + if( !method_exists( $r, 'readInnerXML' ) ) { + $this->markTestSkipped( 'XMLReader::readInnerXML() does not exist (libxml >2.6.20 needed).' ); + return; + } + $this->assertMetadata( $infile, $expected ); + } + + function assertMetadata( $infile, $expected ) { + try { + $data = SVGMetadataExtractor::getMetadata( $infile ); + $this->assertEquals( $expected, $data, 'SVG metadata extraction test' ); + } catch ( MWException $e ) { + if ( $expected === false ) { + $this->assertTrue( true, 'SVG metadata extracted test (expected failure)' ); + } else { + throw $e; + } + } + } + + function providerSvgFiles() { + $base = dirname( __FILE__ ) . '/../../data/media'; + return array( + array( + "$base/Wikimedia-logo.svg", + array( + 'width' => 1024, + 'height' => 1024 + ) + ), + array( + "$base/QA_icon.svg", + array( + 'width' => 60, + 'height' => 60 + ) + ), + array( + "$base/Gtk-media-play-ltr.svg", + array( + 'width' => 60, + 'height' => 60 + ) + ), + ); + } + + function providerSvgFilesWithXMLMetadata() { + $base = dirname( __FILE__ ) . '/../../data/media'; + return array( + array( + "$base/US_states_by_total_state_tax_revenue.svg", + array( + 'height' => 593, + 'metadata' => + '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <ns4:Work xmlns:ns4="http://creativecommons.org/ns#" rdf:about=""> + <ns5:format xmlns:ns5="http://purl.org/dc/elements/1.1/">image/svg+xml</ns5:format> + <ns5:type xmlns:ns5="http://purl.org/dc/elements/1.1/" rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + </ns4:Work> + </rdf:RDF>', + 'width' => 959 + ) + ), + ); + } +} + diff --git a/tests/phpunit/includes/media/TiffTest.php b/tests/phpunit/includes/media/TiffTest.php new file mode 100644 index 00000000..0a7e8e8c --- /dev/null +++ b/tests/phpunit/includes/media/TiffTest.php @@ -0,0 +1,36 @@ +<?php +class TiffTest extends MediaWikiTestCase { + + public function setUp() { + global $wgShowEXIF; + $this->showExif = $wgShowEXIF; + $wgShowEXIF = true; + $this->filePath = dirname( __FILE__ ) . '/../../data/media/'; + $this->handler = new TiffHandler; + } + + public function tearDown() { + global $wgShowEXIF; + $wgShowEXIF = $this->showExif; + } + + public function testInvalidFile() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $res = $this->handler->getMetadata( null, $this->filePath . 'README' ); + $this->assertEquals( ExifBitmapHandler::BROKEN_FILE, $res ); + } + public function testTiffMetadataExtraction() { + global $wgShowEXIF; + if ( !$wgShowEXIF ) { + $this->markTestIncomplete( "This test needs the exif extension." ); + } + $res = $this->handler->getMetadata( null, $this->filePath . 'test.tiff' ); + $expected = 'a:16:{s:10:"ImageWidth";i:20;s:11:"ImageLength";i:20;s:13:"BitsPerSample";a:3:{i:0;i:8;i:1;i:8;i:2;i:8;}s:11:"Compression";i:5;s:25:"PhotometricInterpretation";i:2;s:16:"ImageDescription";s:17:"Created with GIMP";s:12:"StripOffsets";i:8;s:11:"Orientation";i:1;s:15:"SamplesPerPixel";i:3;s:12:"RowsPerStrip";i:64;s:15:"StripByteCounts";i:238;s:11:"XResolution";s:19:"1207959552/16777216";s:11:"YResolution";s:19:"1207959552/16777216";s:19:"PlanarConfiguration";i:1;s:14:"ResolutionUnit";i:2;s:22:"MEDIAWIKI_EXIF_VERSION";i:2;}'; + // Re-unserialize in case there are subtle differences between how versions + // of php serialize stuff. + $this->assertEquals( unserialize( $expected ), unserialize( $res ) ); + } +} diff --git a/tests/phpunit/includes/media/XMPTest.php b/tests/phpunit/includes/media/XMPTest.php new file mode 100644 index 00000000..d1ec4767 --- /dev/null +++ b/tests/phpunit/includes/media/XMPTest.php @@ -0,0 +1,154 @@ +<?php +class XMPTest extends MediaWikiTestCase { + + /** + * Put XMP in, compare what comes out... + * + * @param $xmp String the actual xml data. + * @param $expected Array expected result of parsing the xmp. + * @param $info String Short sentence on what's being tested. + * + * @dataProvider dataXMPParse + */ + public function testXMPParse( $xmp, $expected, $info ) { + if ( !function_exists( 'xml_parser_create_ns' ) ) { + $this->markIncomplete( 'Requires libxml to do XMP parsing' ); + } + if ( !is_string( $xmp ) || !is_array( $expected ) ) { + throw new Exception( "Invalid data provided to " . __METHOD__ ); + } + $reader = new XMPReader; + $reader->parse( $xmp ); + $this->assertEquals( $expected, $reader->getResults(), $info ); + } + + public function dataXMPParse() { + $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/' ; + $data = array(); + + // $xmpFiles format: array of arrays with first arg file base name, + // with the actual file having .xmp on the end for the xmp + // and .result.php on the end for a php file containing the result + // array. Second argument is some info on what's being tested. + $xmpFiles = array( + array( '1', 'parseType=Resource test' ), + array( '2', 'Structure with mixed attribute and element props' ), + array( '3', 'Extra qualifiers (that should be ignored)' ), + array( '3-invalid', 'Test ignoring qualifiers that look like normal props' ), + array( '4', 'Flash as qualifier' ), + array( '5', 'Flash as qualifier 2' ), + array( '6', 'Multiple rdf:Description' ), + array( '7', 'Generic test of several property types' ), + array( 'flash', 'Test of Flash property' ), + array( 'invalid-child-not-struct', 'Test child props not in struct or ignored' ), + array( 'no-recognized-props', 'Test namespace and no recognized props' ), + array( 'no-namespace', 'Test non-namespaced attributes are ignored' ), + array( 'bag-for-seq', "Allow bag's instead of seq's. (bug 27105)" ), + array( 'utf16BE', 'UTF-16BE encoding' ), + array( 'utf16LE', 'UTF-16LE encoding' ), + array( 'utf32BE', 'UTF-32BE encoding' ), + array( 'utf32LE', 'UTF-32LE encoding' ), + array( 'xmpExt', 'Extended XMP missing second part' ), + ); + foreach( $xmpFiles as $file ) { + $xmp = file_get_contents( $xmpPath . $file[0] . '.xmp' ); + // I'm not sure if this is the best way to handle getting the + // result array, but it seems kind of big to put directly in the test + // file. + $result = null; + include( $xmpPath . $file[0] . '.result.php' ); + $data[] = array( $xmp, $result, '[' . $file[0] . '.xmp] ' . $file[1] ); + } + return $data; + } + + /** Test ExtendedXMP block support. (Used when the XMP has to be split + * over multiple jpeg segments, due to 64k size limit on jpeg segments. + * + * @todo This is based on what the standard says. Need to find a real + * world example file to double check the support for this is right. + */ + function testExtendedXMP() { + $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 0 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + 'FNumber' => '2/10', + ) + ); + + $this->assertEquals( $expected, $actual ); + } + + /** + * This test has an extended XMP block with a wrong guid (md5sum) + * and thus should only return the StandardXMP, not the ExtendedXMP. + */ + function testExtendedXMPWithWrongGUID() { + $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B9'; // Note last digit. + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 0 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + ) + ); + + $this->assertEquals( $expected, $actual ); + } + /** + * Have a high offset to simulate a missing packet, + * which should cause it to ignore the ExtendedXMP packet. + */ + function testExtendedXMPMissingPacket() { + $xmpPath = dirname( __FILE__ ) . '/../../data/xmp/'; + $standardXMP = file_get_contents( $xmpPath . 'xmpExt.xmp' ); + $extendedXMP = file_get_contents( $xmpPath . 'xmpExt2.xmp' ); + + $md5sum = '28C74E0AC2D796886759006FBE2E57B7'; // of xmpExt2.xmp + $length = pack( 'N', strlen( $extendedXMP ) ); + $offset = pack( 'N', 2048 ); + $extendedPacket = $md5sum . $length . $offset . $extendedXMP; + + $reader = new XMPReader(); + $reader->parse( $standardXMP ); + $reader->parseExtended( $extendedPacket ); + $actual = $reader->getResults(); + + $expected = array( 'xmp-exif' => + array( + 'DigitalZoomRatio' => '0/10', + 'Flash' => 9, + ) + ); + + $this->assertEquals( $expected, $actual ); + } + +} diff --git a/tests/phpunit/includes/normal/CleanUpTest.php b/tests/phpunit/includes/normal/CleanUpTest.php new file mode 100644 index 00000000..d5ad18d8 --- /dev/null +++ b/tests/phpunit/includes/normal/CleanUpTest.php @@ -0,0 +1,382 @@ +<?php +/** + * Tests for UtfNormal::cleanUp() function. + * + * Copyright © 2004 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 + * + * @file + */ + +/** + * Additional tests for UtfNormal::cleanUp() function, inclusion + * regression checks for known problems. + * Requires PHPUnit. + * + * @ingroup UtfNormal + */ +class CleanUpTest extends MediaWikiTestCase { + /** @todo document */ + function testAscii() { + $text = 'This is plain ASCII text.'; + $this->assertEquals( $text, UtfNormal::cleanUp( $text ) ); + } + + /** @todo document */ + function testNull() { + $text = "a \x00 null"; + $expect = "a \xef\xbf\xbd null"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + function testLatin() { + $text = "L'\xc3\xa9cole"; + $this->assertEquals( $text, UtfNormal::cleanUp( $text ) ); + } + + /** @todo document */ + function testLatinNormal() { + $text = "L'e\xcc\x81cole"; + $expect = "L'\xc3\xa9cole"; + $this->assertEquals( $expect, UtfNormal::cleanUp( $text ) ); + } + + /** + * This test is *very* expensive! + * @todo document + */ + function XtestAllChars() { + $rep = UTF8_REPLACEMENT; + for( $i = 0x0; $i < UNICODE_MAX; $i++ ) { + $char = codepointToUtf8( $i ); + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%04X", $i ); + if( $i % 0x1000 == 0 ) echo "U+$x\n"; + if( $i == 0x0009 || + $i == 0x000a || + $i == 0x000d || + ($i > 0x001f && $i < UNICODE_SURROGATE_FIRST) || + ($i > UNICODE_SURROGATE_LAST && $i < 0xfffe ) || + ($i > 0xffff && $i <= UNICODE_MAX ) ) { + if( isset( UtfNormal::$utfCanonicalComp[$char] ) || isset( UtfNormal::$utfCanonicalDecomp[$char] ) ) { + $comp = UtfNormal::NFC( $char ); + $this->assertEquals( + bin2hex( $comp ), + bin2hex( $clean ), + "U+$x should be decomposed" ); + } else { + $this->assertEquals( + bin2hex( $char ), + bin2hex( $clean ), + "U+$x should be intact" ); + } + } else { + $this->assertEquals( bin2hex( $rep ), bin2hex( $clean ), $x ); + } + } + } + + /** @todo document */ + function testAllBytes() { + $this->doTestBytes( '', '' ); + $this->doTestBytes( 'x', '' ); + $this->doTestBytes( '', 'x' ); + $this->doTestBytes( 'x', 'x' ); + } + + /** @todo document */ + function doTestBytes( $head, $tail ) { + for( $i = 0x0; $i < 256; $i++ ) { + $char = $head . chr( $i ) . $tail; + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%02X", $i ); + if( $i == 0x0009 || + $i == 0x000a || + $i == 0x000d || + ($i > 0x001f && $i < 0x80) ) { + $this->assertEquals( + bin2hex( $char ), + bin2hex( $clean ), + "ASCII byte $x should be intact" ); + if( $char != $clean ) return; + } else { + $norm = $head . UTF8_REPLACEMENT . $tail; + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Forbidden byte $x should be rejected" ); + if( $norm != $clean ) return; + } + } + } + + /** @todo document */ + function testDoubleBytes() { + $this->doTestDoubleBytes( '', '' ); + $this->doTestDoubleBytes( 'x', '' ); + $this->doTestDoubleBytes( '', 'x' ); + $this->doTestDoubleBytes( 'x', 'x' ); + } + + /** + * @todo document + */ + function doTestDoubleBytes( $head, $tail ) { + for( $first = 0xc0; $first < 0x100; $first+=2 ) { + for( $second = 0x80; $second < 0x100; $second+=2 ) { + $char = $head . chr( $first ) . chr( $second ) . $tail; + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%02X,%02X", $first, $second ); + if( $first > 0xc1 && + $first < 0xe0 && + $second < 0xc0 ) { + $norm = UtfNormal::NFC( $char ); + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Pair $x should be intact" ); + if( $norm != $clean ) return; + } elseif( $first > 0xfd || $second > 0xbf ) { + # fe and ff are not legal head bytes -- expect two replacement chars + $norm = $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail; + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Forbidden pair $x should be rejected" ); + if( $norm != $clean ) return; + } else { + $norm = $head . UTF8_REPLACEMENT . $tail; + $this->assertEquals( + bin2hex( $norm ), + bin2hex( $clean ), + "Forbidden pair $x should be rejected" ); + if( $norm != $clean ) return; + } + } + } + } + + /** @todo document */ + function testTripleBytes() { + $this->doTestTripleBytes( '', '' ); + $this->doTestTripleBytes( 'x', '' ); + $this->doTestTripleBytes( '', 'x' ); + $this->doTestTripleBytes( 'x', 'x' ); + } + + /** @todo document */ + function doTestTripleBytes( $head, $tail ) { + for( $first = 0xc0; $first < 0x100; $first+=2 ) { + for( $second = 0x80; $second < 0x100; $second+=2 ) { + #for( $third = 0x80; $third < 0x100; $third++ ) { + for( $third = 0x80; $third < 0x81; $third++ ) { + $char = $head . chr( $first ) . chr( $second ) . chr( $third ) . $tail; + $clean = UtfNormal::cleanUp( $char ); + $x = sprintf( "%02X,%02X,%02X", $first, $second, $third ); + if( $first >= 0xe0 && + $first < 0xf0 && + $second < 0xc0 && + $third < 0xc0 ) { + if( $first == 0xe0 && $second < 0xa0 ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Overlong triplet $x should be rejected" ); + } elseif( $first == 0xed && + ( chr( $first ) . chr( $second ) . chr( $third )) >= UTF8_SURROGATE_FIRST ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Surrogate triplet $x should be rejected" ); + } else { + $this->assertEquals( + bin2hex( UtfNormal::NFC( $char ) ), + bin2hex( $clean ), + "Triplet $x should be intact" ); + } + } elseif( $first > 0xc1 && $first < 0xe0 && $second < 0xc0 ) { + $this->assertEquals( + bin2hex( UtfNormal::NFC( $head . chr( $first ) . chr( $second ) ) . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Valid 2-byte $x + broken tail" ); + } elseif( $second > 0xc1 && $second < 0xe0 && $third < 0xc0 ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . UtfNormal::NFC( chr( $second ) . chr( $third ) . $tail ) ), + bin2hex( $clean ), + "Broken head + valid 2-byte $x" ); + } elseif( ( $first > 0xfd || $second > 0xfd ) && + ( ( $second > 0xbf && $third > 0xbf ) || + ( $second < 0xc0 && $third < 0xc0 ) || + ( $second > 0xfd ) || + ( $third > 0xfd ) ) ) { + # fe and ff are not legal head bytes -- expect three replacement chars + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Forbidden triplet $x should be rejected" ); + } elseif( $first > 0xc2 && $second < 0xc0 && $third < 0xc0 ) { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Forbidden triplet $x should be rejected" ); + } else { + $this->assertEquals( + bin2hex( $head . UTF8_REPLACEMENT . UTF8_REPLACEMENT . $tail ), + bin2hex( $clean ), + "Forbidden triplet $x should be rejected" ); + } + } + } + } + } + + /** @todo document */ + function testChunkRegression() { + # Check for regression against a chunking bug + $text = "\x46\x55\xb8" . + "\xdc\x96" . + "\xee" . + "\xe7" . + "\x44" . + "\xaa" . + "\x2f\x25"; + $expect = "\x46\x55\xef\xbf\xbd" . + "\xdc\x96" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x44" . + "\xef\xbf\xbd" . + "\x2f\x25"; + + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + function testInterposeRegression() { + $text = "\x4e\x30" . + "\xb1" . # bad tail + "\x3a" . + "\x92" . # bad tail + "\x62\x3a" . + "\x84" . # bad tail + "\x43" . + "\xc6" . # bad head + "\x3f" . + "\x92" . # bad tail + "\xad" . # bad tail + "\x7d" . + "\xd9\x95"; + + $expect = "\x4e\x30" . + "\xef\xbf\xbd" . + "\x3a" . + "\xef\xbf\xbd" . + "\x62\x3a" . + "\xef\xbf\xbd" . + "\x43" . + "\xef\xbf\xbd" . + "\x3f" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x7d" . + "\xd9\x95"; + + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + function testOverlongRegression() { + $text = "\x67" . + "\x1a" . # forbidden ascii + "\xea" . # bad head + "\xc1\xa6" . # overlong sequence + "\xad" . # bad tail + "\x1c" . # forbidden ascii + "\xb0" . # bad tail + "\x3c" . + "\x9e"; # bad tail + $expect = "\x67" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x3c" . + "\xef\xbf\xbd"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + function testSurrogateRegression() { + $text = "\xed\xb4\x96" . # surrogate 0xDD16 + "\x83" . # bad tail + "\xb4" . # bad tail + "\xac"; # bad head + $expect = "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + function testBomRegression() { + $text = "\xef\xbf\xbe" . # U+FFFE, illegal char + "\xb2" . # bad tail + "\xef" . # bad head + "\x59"; + $expect = "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\xef\xbf\xbd" . + "\x59"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + function testForbiddenRegression() { + $text = "\xef\xbf\xbf"; # U+FFFF, illegal char + $expect = "\xef\xbf\xbd"; + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } + + /** @todo document */ + function testHangulRegression() { + $text = "\xed\x9c\xaf" . # Hangul char + "\xe1\x87\x81"; # followed by another final jamo + $expect = $text; # Should *not* change. + $this->assertEquals( + bin2hex( $expect ), + bin2hex( UtfNormal::cleanUp( $text ) ) ); + } +} 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><!-- Foo --></comment></root>" ), + array( "<!-- Foo --><!-- Bar -->", "<root><comment><!-- Foo --></comment><comment><!-- Bar --></comment></root>" ), + array( "<!-- Foo --> <!-- Bar -->", "<root><comment><!-- Foo --></comment> <comment><!-- Bar --></comment></root>" ), + array( "<!-- Foo --> \n <!-- Bar -->", "<root><comment><!-- Foo --></comment> \n <comment><!-- Bar --></comment></root>" ), + array( "<!-- Foo --> \n <!-- Bar -->\n", "<root><comment><!-- Foo --></comment> \n<comment> <!-- Bar -->\n</comment></root>" ), + array( "<!-- Foo --> <!-- Bar -->\n", "<root><comment><!-- Foo --></comment> <comment><!-- Bar --></comment>\n</root>" ), + array( "<!-->Bar", "<root><comment><!-->Bar</comment></root>" ), + array( "<!-- Comment -- comment", "<root><comment><!-- Comment -- comment</comment></root>" ), + array( "== Foo ==\n <!-- Bar -->\n== Baz ==\n", "<root><h level=\"2\" i=\"1\">== Foo ==</h>\n<comment> <!-- Bar -->\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></gallery></close></ext></root>" ), + array( "<foo> <gallery></gallery>", "<root><foo> <ext><name>gallery</name><attr></attr><inner></inner><close></gallery></close></ext></root>" ), + array( "<foo> <gallery><gallery></gallery>", "<root><foo> <ext><name>gallery</name><attr></attr><inner><gallery></inner><close></gallery></close></ext></root>" ), + array( "<noinclude> Foo bar </noinclude>", "<root><ignore><noinclude></ignore> Foo bar <ignore></noinclude></ignore></root>" ), + array( "<noinclude>\n{{Foo}}\n</noinclude>", "<root><ignore><noinclude></ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore></noinclude></ignore></root>" ), + array( "<noinclude>\n{{Foo}}\n</noinclude>\n", "<root><ignore><noinclude></ignore>\n<template lineStart=\"1\"><title>Foo</title></template>\n<ignore></noinclude></ignore>\n</root>" ), + array( "<gallery>foo bar", "<root><ext><name>gallery</name><attr></attr><inner>foo bar</inner></ext></root>" ), + array( "<{{foo}}>", "<root><<template><title>foo</title></template>></root>" ), + array( "<{{{foo}}}>", "<root><<tplarg><title>foo</title></tplarg>></root>" ), + array( "<gallery></gallery</gallery>", "<root><ext><name>gallery</name><attr></attr><inner></gallery</inner><close></gallery></close></ext></root>" ), + array( "=== Foo === ", "<root><h level=\"3\" i=\"1\">=== Foo === </h></root>" ), + array( "==<!-- -->= Foo === ", "<root><h level=\"2\" i=\"1\">==<comment><!-- --></comment>= Foo === </h></root>" ), + array( "=== Foo ==<!-- -->= ", "<root><h level=\"1\" i=\"1\">=== Foo ==<comment><!-- --></comment>= </h></root>" ), + array( "=== Foo ===<!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment><!-- --></comment></h>\n</root>" ), + array( "=== Foo ===<!-- --> <!-- -->\n", "<root><h level=\"3\" i=\"1\">=== Foo ===<comment><!-- --></comment> <comment><!-- --></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><!-- --></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></display map ></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></display map ></close></ext>Baz</root>" ), + array( "Foo <gallery bar=\"baz\" />", "<root>Foo <ext><name>gallery</name><attr> bar="baz" </attr></ext></root>" ), + array( "</foo>Foo<//foo>", "<root><ext><name>/foo</name><attr></attr><inner>Foo</inner><close><//foo></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><!--c1--></comment></h></root>" ), + array( "== h == <!--c1-->", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment></h></root>" ), + array( "== h ==<!--c1--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment> </h></root>" ), + array( "== h == <!--c1--> ", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment> </h></root>" ), + array( "== h ==<!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment><comment><!--c2--></comment></h></root>" ), + array( "== h == <!--c1--><!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment><comment><!--c2--></comment></h></root>" ), + array( "== h ==<!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment><comment><!--c2--></comment> </h></root>" ), + array( "== h == <!--c1--><!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment><comment><!--c2--></comment> </h></root>" ), + array( "== h == <!--c1--> <!--c2-->", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment> <comment><!--c2--></comment></h></root>" ), + array( "== h ==<!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment> <comment><!--c2--></comment> </h></root>" ), + array( "== h == <!--c1--> <!--c2--> ", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment> <comment><!--c2--></comment> </h></root>" ), + array( "== h ==<!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment><comment><!--c2--></comment><comment><!--c3--></comment></h></root>" ), + array( "== h ==<!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment> <comment><!--c2--></comment><comment><!--c3--></comment></h></root>" ), + array( "== h ==<!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment><comment><!--c2--></comment> <comment><!--c3--></comment></h></root>" ), + array( "== h ==<!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment> <comment><!--c2--></comment> <comment><!--c3--></comment></h></root>" ), + array( "== h == <!--c1--><!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment><comment><!--c2--></comment><comment><!--c3--></comment></h></root>" ), + array( "== h == <!--c1--> <!--c2--><!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment> <comment><!--c2--></comment><comment><!--c3--></comment></h></root>" ), + array( "== h == <!--c1--><!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment><comment><!--c2--></comment> <comment><!--c3--></comment></h></root>" ), + array( "== h == <!--c1--> <!--c2--> <!--c3-->", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment> <comment><!--c2--></comment> <comment><!--c3--></comment></h></root>" ), + array( "== h ==<!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment><comment><!--c2--></comment><comment><!--c3--></comment> </h></root>" ), + array( "== h ==<!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment> <comment><!--c2--></comment><comment><!--c3--></comment> </h></root>" ), + array( "== h ==<!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment><comment><!--c2--></comment> <comment><!--c3--></comment> </h></root>" ), + array( "== h ==<!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h ==<comment><!--c1--></comment> <comment><!--c2--></comment> <comment><!--c3--></comment> </h></root>" ), + array( "== h == <!--c1--><!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment><comment><!--c2--></comment><comment><!--c3--></comment> </h></root>" ), + array( "== h == <!--c1--> <!--c2--><!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment> <comment><!--c2--></comment><comment><!--c3--></comment> </h></root>" ), + array( "== h == <!--c1--><!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment><comment><!--c2--></comment> <comment><!--c3--></comment> </h></root>" ), + array( "== h == <!--c1--> <!--c2--> <!--c3--> ", "<root><h level=\"2\" i=\"1\">== h == <comment><!--c1--></comment> <comment><!--c2--></comment> <comment><!--c3--></comment> </h></root>" ), + + /* These are not working: */ + array( "== h ==<!--c1--> <!--c2-->", "<root>== h ==<comment><!--c1--></comment> <comment><!--c2--></comment></root>" ), + array( "== h == <!--c1--> <!--c2-->", "<root>== h == <comment><!--c1--></comment> <comment><!--c2--></comment></root>" ), + array( "== h ==<!--c1--> <!--c2--> ", "<root>== h ==<comment><!--c1--></comment> <comment><!--c2--></comment> </root>" ), + array( "== h == x <!--c1--><!--c2--><!--c3--> ", "<root>== h == x <comment><!--c1--></comment><comment><!--c2--></comment><comment><!--c3--></comment> </root>" ), + array( "== h ==<!--c1--> x <!--c2--><!--c3--> ", "<root>== h ==<comment><!--c1--></comment> x <comment><!--c2--></comment><comment><!--c3--></comment> </root>" ), + array( "== h ==<!--c1--><!--c2--><!--c3--> x ", "<root>== h ==<comment><!--c1--></comment><comment><!--c2--></comment><comment><!--c3--></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 ); + } +} diff --git a/tests/phpunit/includes/search/SearchEngineTest.php b/tests/phpunit/includes/search/SearchEngineTest.php new file mode 100644 index 00000000..1a0fcd31 --- /dev/null +++ b/tests/phpunit/includes/search/SearchEngineTest.php @@ -0,0 +1,163 @@ +<?php + +/** + * This class is not directly tested. Instead it is extended by SearchDbTest. + * @group Search + * @group Database + */ +class SearchEngineTest extends MediaWikiTestCase { + protected $search, $pageList; + + function tearDown() { + unset( $this->search ); + } + + /* + * Checks for database type & version. + * Will skip current test if DB does not support search. + */ + function setUp() { + parent::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" ); + } + + $searchType = $this->db->getSearchEngine(); + $this->search = new $searchType( $this->db ); + } + + function pageExists( $title ) { + return false; + } + + function addDBData() { + 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 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; + } + + /** + * Insert a new page + * + * @param $pageName String: page name + * @param $text String: page's content + * @param $n Integer: unused + */ + function insertPage( $pageName, $text, $ns ) { + $dbw = $this->db; + $title = Title::newFromText( $pageName ); + + $user = User::newFromName( 'WikiSysop' ); + $comment = 'Search Test'; + + // avoid memory leak...? + $linkCache = LinkCache::singleton(); + $linkCache->clear(); + + $article = new Article( $title ); + $article->doEdit( $text, $comment, 0, false, $user ); + + $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" ); + } + +} diff --git a/tests/phpunit/includes/search/SearchUpdateTest.php b/tests/phpunit/includes/search/SearchUpdateTest.php new file mode 100644 index 00000000..935425a6 --- /dev/null +++ b/tests/phpunit/includes/search/SearchUpdateTest.php @@ -0,0 +1,80 @@ +<?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 MediaWikiTestCase { + 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' + ); + } +} diff --git a/tests/phpunit/includes/specials/SpecialRecentchanges.php b/tests/phpunit/includes/specials/SpecialRecentchanges.php new file mode 100644 index 00000000..a98e7c1a --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialRecentchanges.php @@ -0,0 +1,134 @@ +<?php +/** + * Test class for SpecialRecentchanges class + * + * Copyright © 2011, Ashar Voultoiz + * + * @author Ashar Voultoiz + */ +class SpecialRecentchangesTest extends MediaWikiTestCase { + + /** + * @var SpecialRecentChanges + */ + protected $rc; + + function setUp() { + } + + /** helper to test SpecialRecentchanges::buildMainQueryConds() */ + private function assertConditions( $expected, $requestOptions = null, $message = '' ) { + global $wgRequest; + $savedGlobal = $wgRequest; + + # Initialize a WebRequest object ... + $wgRequest = new FauxRequest( $requestOptions ); + # ... then setup the rc object (which use wgRequest internally) + $this->rc = new SpecialRecentChanges(); + $formOptions = $this->rc->setup( null ); + + # Filter out rc_timestamp conditions which depends on the test runtime + # This condition is not needed as of march 2, 2011 -- hashar + # @todo FIXME: Find a way to generate the correct rc_timestamp + $queryConditions = array_filter( + $this->rc->buildMainQueryConds( $formOptions ), + 'SpecialRecentchangesTest::filterOutRcTimestampCondition' + ); + + $this->assertEquals( + $expected, + $queryConditions, + $message + ); + + $wgRequest = $savedGlobal; + } + + /** return false if condition begin with 'rc_timestamp ' */ + private static function filterOutRcTimestampCondition( $var ) { + return (false === strpos( $var, 'rc_timestamp ' )); + + } + + public function testRcNsFilter() { + $this->assertConditions( + array( # expected + 'rc_bot' => 0, + #0 => "rc_timestamp >= '20110223000000'", + 1 => "rc_namespace = '0'", + ), + array( + 'namespace' => NS_MAIN, + ), + "rc conditions with no options (aka default setting)" + ); + } + + public function testRcNsFilterInversion() { + $this->assertConditions( + array( # expected + #0 => "rc_timestamp >= '20110223000000'", + 'rc_bot' => 0, + 1 => sprintf( "rc_namespace != '%s'", NS_MAIN ), + ), + array( + 'namespace' => NS_MAIN, + 'invert' => 1, + ), + "rc conditions with namespace inverted" + ); + } + + /** + * @bug 2429 + * @dataProvider provideNamespacesAssociations + */ + public function testRcNsFilterAssociation( $ns1, $ns2 ) { + $this->assertConditions( + array( # expected + #0 => "rc_timestamp >= '20110223000000'", + 'rc_bot' => 0, + 1 => sprintf( "(rc_namespace = '%s' OR rc_namespace = '%s')", $ns1, $ns2 ), + ), + array( + 'namespace' => $ns1, + 'associated' => 1, + ), + "rc conditions with namespace inverted" + ); + } + + /** + * @bug 2429 + * @dataProvider provideNamespacesAssociations + */ + public function testRcNsFilterAssociationWithInversion( $ns1, $ns2 ) { + $this->assertConditions( + array( # expected + #0 => "rc_timestamp >= '20110223000000'", + 'rc_bot' => 0, + 1 => sprintf( "(rc_namespace != '%s' AND rc_namespace != '%s')", $ns1, $ns2 ), + ), + array( + 'namespace' => $ns1, + 'associated' => 1, + 'invert' => 1, + ), + "rc conditions with namespace inverted" + ); + } + + /** + * Provides associated namespaces to test recent changes + * namespaces association filtering. + */ + public function provideNamespacesAssociations() { + return array( # (NS => Associated_NS) + array( NS_MAIN, NS_TALK), + array( NS_TALK, NS_MAIN), + ); + } + +} + + diff --git a/tests/phpunit/includes/upload/UploadFromUrlTest.php b/tests/phpunit/includes/upload/UploadFromUrlTest.php new file mode 100644 index 00000000..4722d408 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadFromUrlTest.php @@ -0,0 +1,348 @@ +<?php + +/** + * @group Broken + * @group Upload + */ +class UploadFromUrlTest extends ApiTestCase { + + public function setUp() { + global $wgEnableUploads, $wgAllowCopyUploads, $wgAllowAsyncCopyUploads; + parent::setUp(); + + $wgEnableUploads = true; + $wgAllowCopyUploads = true; + $wgAllowAsyncCopyUploads = true; + wfSetupSession(); + + if ( wfLocalFile( 'UploadFromUrlTest.png' )->exists() ) { + $this->deleteFile( 'UploadFromUrlTest.png' ); + } + } + + protected function doApiRequest( $params, $unused = null, $appendModule = false ) { + $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' => $this->user->userName, + 'lgpassword' => $this->user->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' => $this->user->userName, + 'lgpassword' => $this->user->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 = $this->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" ); + + $this->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" ); + + $this->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 = $this->user->editToken(); + + $this->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 = $this->user->editToken(); + + $this->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 = $this->user->editToken(); + + $job = Job::pop(); + $this->assertFalse( $job, 'Starting with an empty jobqueue' ); + + $this->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 = $this->user->user->editToken(); + + $talk = $this->user->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"); + } + } diff --git a/tests/phpunit/includes/upload/UploadStashTest.php b/tests/phpunit/includes/upload/UploadStashTest.php new file mode 100644 index 00000000..9c39bc61 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadStashTest.php @@ -0,0 +1,53 @@ +<?php + +class UploadStashTest extends MediaWikiTestCase { + /** + * @var Array of UploadStashTestUser + */ + public static $users; + + public function setUp() { + parent::setUp(); + + // Setup a file for bug 29408 + $this->bug29408File = dirname( __FILE__ ) . '/bug29408'; + file_put_contents( $this->bug29408File, "\x00" ); + + self::$users = array( + 'sysop' => new ApiTestUser( + 'Uploadstashtestsysop', + 'Upload Stash Test Sysop', + 'upload_stash_test_sysop@sample.com', + array( 'sysop' ) + ), + 'uploader' => new ApiTestUser( + 'Uploadstashtestuser', + 'Upload Stash Test User', + 'upload_stash_test_user@sample.com', + array() + ) + ); + } + + public function testBug29408() { + global $wgUser; + $wgUser = self::$users['uploader']->user; + + $repo = RepoGroup::singleton()->getLocalRepo(); + $stash = new UploadStash( $repo ); + + // Throws exception caught by PHPUnit on failure + $file = $stash->stashFile( $this->bug29408File ); + // We'll never reach this point if we hit bug 29408 + $this->assertTrue( true, 'Unrecognized file without extension' ); + + $stash->removeFile( $file->getFileKey() ); + } + + public function tearDown() { + parent::tearDown(); + + unlink( $this->bug29408File . "." ); + + } +} diff --git a/tests/phpunit/includes/upload/UploadTest.php b/tests/phpunit/includes/upload/UploadTest.php new file mode 100644 index 00000000..69c29032 --- /dev/null +++ b/tests/phpunit/includes/upload/UploadTest.php @@ -0,0 +1,142 @@ +<?php +/** + * @group Upload + */ +class UploadTest extends MediaWikiTestCase { + protected $upload; + + + function setUp() { + global $wgHooks; + parent::setUp(); + + $this->upload = new UploadTestHandler; + $this->hooks = $wgHooks; + $wgHooks['InterwikiLoadPrefix'][] = 'MediaWikiTestCase::disableInterwikis'; + } + + function tearDown() { + global $wgHooks; + $wgHooks = $this->hooks; + } + + /** + * 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', UploadBase::OK, + 'upload valid title' ); + + /* A title with a slash */ + $this->assertUploadTitleAndCode( 'A/B.jpg', + 'B.jpg', UploadBase::OK, + 'upload title with slash' ); + + /* A title with illegal char */ + $this->assertUploadTitleAndCode( 'A:B.jpg', + 'A-B.jpg', UploadBase::OK, + 'upload title with colon' ); + + /* Stripping leading File: prefix */ + $this->assertUploadTitleAndCode( 'File:C.jpg', + 'C.jpg', UploadBase::OK, + 'upload title with File prefix' ); + + /* Test illegal suggested title (r94601) */ + $this->assertUploadTitleAndCode( '%281%29.JPG', + null, UploadBase::ILLEGAL_FILENAME, + 'illegal title for upload' ); + + /* A title without extension */ + $this->assertUploadTitleAndCode( 'A', + null, UploadBase::FILETYPE_MISSING, + 'upload title without extension' ); + + /* A title with no basename */ + $this->assertUploadTitleAndCode( '.jpg', + null, UploadBase::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 == UploadBase::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( UploadBase::EMPTY_FILE, + $result['status'], + 'upload empty file' ); + } + + // Helper used to create an empty file of size $size. + private function createFileOfSize( $size ) { + $filename = tempnam( wfTempDir(), "mwuploadtest" ); + + $fh = fopen( $filename, 'w' ); + ftruncate( $fh, $size ); + fclose( $fh ); + + return $filename; + } + + /** + * test uploading a 100 bytes file with wgMaxUploadSize = 100 + * + * This method should be abstracted so we can test different settings. + */ + + public function testMaxUploadSize() { + global $wgMaxUploadSize; + $savedGlobal = $wgMaxUploadSize; // save global + global $wgFileExtensions; + $wgFileExtensions[] = 'txt'; + + $wgMaxUploadSize = 100; + + $filename = $this->createFileOfSize( $wgMaxUploadSize ); + $this->upload->initializePathInfo( basename($filename) . '.txt', $filename, 100 ); + $result = $this->upload->verifyUpload(); + unlink( $filename ); + + $this->assertEquals( + array( 'status' => UploadBase::OK ), $result ); + + $wgMaxUploadSize = $savedGlobal; // restore global + } +} + +class UploadTestHandler extends UploadBase { + public function initializeFromRequest( &$request ) { } + public function testTitleValidation( $name ) { + $this->mTitle = false; + $this->mDesiredDestName = $name; + $this->getTitle(); + return $this->mTitleError; + } + + +} diff --git a/tests/phpunit/install-phpunit.sh b/tests/phpunit/install-phpunit.sh new file mode 100644 index 00000000..2d2b53ab --- /dev/null +++ b/tests/phpunit/install-phpunit.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +has_binary () { + if [ -z `which $1` ]; then + return 1 + fi + return 0 +} + +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 channel-discover components.ez.no + pear channel-discover pear.symfony-project.com + 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 +else if ( has_binary port ); then + echo Installing phpunit with macports + port install php5-unit +fi +fi +fi +fi +fi diff --git a/tests/phpunit/languages/LanguageBe_taraskTest.php b/tests/phpunit/languages/LanguageBe_taraskTest.php new file mode 100644 index 00000000..e7fdb7ca --- /dev/null +++ b/tests/phpunit/languages/LanguageBe_taraskTest.php @@ -0,0 +1,30 @@ +<?php + +class LanguageBeTaraskTest extends MediaWikiTestCase { + 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' ) ); + } +} diff --git a/tests/phpunit/languages/LanguageTest.php b/tests/phpunit/languages/LanguageTest.php new file mode 100644 index 00000000..aaad9c31 --- /dev/null +++ b/tests/phpunit/languages/LanguageTest.php @@ -0,0 +1,246 @@ +<?php + +class LanguageTest extends MediaWikiTestCase { + 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)' + ); + + $this->assertEquals( + "2h 0m", + $this->lang->formatTimePeriod( 7199.55, 'avoidseconds' ), + 'formatTimePeriod() rounding (>=1h), avoidseconds' + ); + + $this->assertEquals( + "2h 0m", + $this->lang->formatTimePeriod( 7199.55, 'avoidminutes' ), + 'formatTimePeriod() rounding (>=1h), avoidminutes' + ); + + $this->assertEquals( + "48h 0m", + $this->lang->formatTimePeriod( 172799.55, 'avoidseconds' ), + 'formatTimePeriod() rounding (=48h), avoidseconds' + ); + + $this->assertEquals( + "3d 0h", + $this->lang->formatTimePeriod( 259199.55, 'avoidminutes' ), + 'formatTimePeriod() rounding (>48h), avoidminutes' + ); + + $this->assertEquals( + "2d 1h 0m", + $this->lang->formatTimePeriod( 176399.55, 'avoidseconds' ), + 'formatTimePeriod() rounding (>48h), avoidseconds' + ); + + $this->assertEquals( + "2d 1h", + $this->lang->formatTimePeriod( 176399.55, 'avoidminutes' ), + 'formatTimePeriod() rounding (>48h), avoidminutes' + ); + + $this->assertEquals( + "3d 0h 0m", + $this->lang->formatTimePeriod( 259199.55, 'avoidseconds' ), + 'formatTimePeriod() rounding (>48h), avoidminutes' + ); + + $this->assertEquals( + "2d 0h 0m", + $this->lang->formatTimePeriod( 172801.55, 'avoidseconds' ), + 'formatTimePeriod() rounding, (>48h), avoidseconds' + ); + + $this->assertEquals( + "2d 1h 1m 1s", + $this->lang->formatTimePeriod( 176460.55 ), + 'formatTimePeriod() rounding, recursion, (>48h)' + ); + } + + function testTruncate() { + $this->assertEquals( + "XXX", + $this->lang->truncate( "1234567890", 0, 'XXX' ), + 'truncate prefix, len 0, small ellipsis' + ); + + $this->assertEquals( + "12345XXX", + $this->lang->truncate( "1234567890", 8, 'XXX' ), + 'truncate prefix, small ellipsis' + ); + + $this->assertEquals( + "123456789", + $this->lang->truncate( "123456789", 5, 'XXXXXXXXXXXXXXX' ), + 'truncate prefix, large ellipsis' + ); + + $this->assertEquals( + "XXX67890", + $this->lang->truncate( "1234567890", -8, 'XXX' ), + 'truncate suffix, small ellipsis' + ); + + $this->assertEquals( + "123456789", + $this->lang->truncate( "123456789", -5, 'XXXXXXXXXXXXXXX' ), + 'truncate suffix, large ellipsis' + ); + } + + /** + * @dataProvider provideHTMLTruncateData() + */ + function testTruncateHtml( $len, $ellipsis, $input, $expected ) { + // Actual HTML... + $this->assertEquals( + $expected, + $this->lang->truncateHTML( $input, $len, $ellipsis ) + ); + } + + /** + * Array format is ($len, $ellipsis, $input, $expected) + */ + function provideHTMLTruncateData() { + return array( + array( 0, 'XXX', "1234567890", "XXX" ), + array( 8, 'XXX', "1234567890", "12345XXX" ), + array( 5, 'XXXXXXXXXXXXXXX', '1234567890', "1234567890" ), + array( 2, '***', + '<p><span style="font-weight:bold;"></span></p>', + '<p><span style="font-weight:bold;"></span></p>', + ), + array( 2, '***', + '<p><span style="font-weight:bold;">123456789</span></p>', + '<p><span style="font-weight:bold;">***</span></p>', + ), + array( 2, '***', + '<p><span style="font-weight:bold;"> 23456789</span></p>', + '<p><span style="font-weight:bold;">***</span></p>', + ), + array( 3, '***', + '<p><span style="font-weight:bold;">123456789</span></p>', + '<p><span style="font-weight:bold;">***</span></p>', + ), + array( 4, '***', + '<p><span style="font-weight:bold;">123456789</span></p>', + '<p><span style="font-weight:bold;">1***</span></p>', + ), + array( 5, '***', + '<tt><span style="font-weight:bold;">123456789</span></tt>', + '<tt><span style="font-weight:bold;">12***</span></tt>', + ), + array( 6, '***', + '<p><a href="www.mediawiki.org">123456789</a></p>', + '<p><a href="www.mediawiki.org">123***</a></p>', + ), + array( 6, '***', + '<p><a href="www.mediawiki.org">12 456789</a></p>', + '<p><a href="www.mediawiki.org">12 ***</a></p>', + ), + array( 7, '***', + '<small><span style="font-weight:bold;">123<p id="#moo">456</p>789</span></small>', + '<small><span style="font-weight:bold;">123<p id="#moo">4***</p></span></small>', + ), + array( 8, '***', + '<div><span style="font-weight:bold;">123<span>4</span>56789</span></div>', + '<div><span style="font-weight:bold;">123<span>4</span>5***</span></div>', + ), + array( 9, '***', + '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>', + '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>', + ), + array( 10, '***', + '<p><font style="font-weight:bold;">123456789</font></p>', + '<p><font style="font-weight:bold;">123456789</font></p>', + ), + ); + } + + /** + * Test Language::isValidBuiltInCode() + * @dataProvider provideLanguageCodes + */ + function testBuiltInCodeValidation( $code, $message = '' ) { + $this->assertTrue( + (bool) Language::isValidBuiltInCode( $code ), + "validating code $code $message" + ); + } + + function testBuiltInCodeValidationRejectUnderscore() { + $this->assertFalse( + (bool) Language::isValidBuiltInCode( 'be_tarask' ), + "reject underscore in language code" + ); + } + + function provideLanguageCodes() { + return array( + array( 'fr' , 'Two letters, minor case' ), + array( 'EN' , 'Two letters, upper case' ), + array( 'tyv' , 'Three letters' ), + array( 'tokipona' , 'long language code' ), + array( 'be-tarask', 'With dash' ), + array( 'Zh-classical', 'Begin with upper case, dash' ), + array( 'Be-x-old', 'With extension (two dashes)' ), + ); + } +} diff --git a/tests/phpunit/languages/LanguageTrTest.php b/tests/phpunit/languages/LanguageTrTest.php new file mode 100644 index 00000000..d2a5ff36 --- /dev/null +++ b/tests/phpunit/languages/LanguageTrTest.php @@ -0,0 +1,65 @@ +<?php +/** + * @author Ashar Voultoiz + * @copyright Copyright © 2011, Ashar Voultoiz + * @file + */ + +/** Tests for MediaWiki languages/LanguageTr.php */ +class LanguageTrTest extends MediaWikiTestCase { + private $lang; + + function setUp() { + $this->lang = Language::factory( 'Tr' ); + } + function tearDown() { + unset( $this->lang ); + } + + /** + * See @bug 28040 + * Credits to #wikipedia-tr users berm, []LuCkY[] and Emperyan + * @see http://en.wikipedia.org/wiki/Dotted_and_dotless_I + * @dataProvider provideDottedAndDotlessI + */ + function testDottedAndDotlessI( $func, $input, $inputCase, $expected ) { + if( $func == 'ucfirst' ) { + $res = $this->lang->ucfirst( $input ); + } elseif( $func == 'lcfirst' ) { + $res = $this->lang->lcfirst( $input ); + } else { + throw new MWException( __METHOD__ . " given an invalid function name '$func'" ); + } + + $msg = "Converting $inputCase case '$input' with $func should give '$expected'"; + + $this->assertEquals( $expected, $res, $msg ); + } + + function provideDottedAndDotlessI() { + return array( + # function, input, input case, expected + # Case changed: + array( 'ucfirst', 'ı', 'lower', 'I' ), + array( 'ucfirst', 'i', 'lower', 'İ' ), + array( 'lcfirst', 'I', 'upper', 'ı' ), + array( 'lcfirst', 'İ', 'upper', 'i' ), + + # Already using the correct case + array( 'ucfirst', 'I', 'upper', 'I' ), + array( 'ucfirst', 'İ', 'upper', 'İ' ), + array( 'lcfirst', 'ı', 'lower', 'ı' ), + array( 'lcfirst', 'i', 'lower', 'i' ), + + # A real example taken from bug 28040 using + # http://tr.wikipedia.org/wiki/%C4%B0Phone + array( 'lcfirst', 'iPhone', 'lower', 'iPhone' ), + + # next case is valid in Turkish but are different words if we + # consider IPhone is English! + array( 'lcfirst', 'IPhone', 'upper', 'ıPhone' ), + + ); + } + +} diff --git a/tests/phpunit/phpunit.php b/tests/phpunit/phpunit.php new file mode 100644 index 00000000..39cccf80 --- /dev/null +++ b/tests/phpunit/phpunit.php @@ -0,0 +1,61 @@ +#!/usr/bin/env php +<?php +/** + * Bootstrapping for MediaWiki PHPUnit tests + * + * @file + */ + +/* Configuration */ + +// Evaluate the include path relative to this file +$IP = 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/Maintenance.php" ); + +class PHPUnitMaintClass extends Maintenance { + public function finalSetup() { + parent::finalSetup(); + + global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType, $wgUseDatabaseMessages; + global $wgLocaltimezone, $wgLocalisationCacheConf; + + $wgMainCacheType = CACHE_NONE; + $wgMessageCacheType = CACHE_NONE; + $wgParserCacheType = CACHE_NONE; + + $wgUseDatabaseMessages = false; # Set for future resets + + // Assume UTC for testing purposes + $wgLocaltimezone = 'UTC'; + + $wgLocalisationCacheConf['storeClass'] = 'LCStore_Null'; + } + public function execute() { } + public function getDbType() { + return Maintenance::DB_ADMIN; + } +} + +$maintClass = 'PHPUnitMaintClass'; +require( RUN_MAINTENANCE_IF_MAIN ); + +if( !in_array( '--configuration', $_SERVER['argv'] ) ) { + //Hack to eliminate the need to use the Makefile (which sucks ATM) + $_SERVER['argv'][] = '--configuration'; + $_SERVER['argv'][] = $IP . '/tests/phpunit/suite.xml'; +} + +require_once( 'PHPUnit/Runner/Version.php' ); +if( version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) { + die( 'PHPUnit 3.5 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" ); +} +require_once( 'PHPUnit/Autoload.php' ); + +require_once( "$IP/tests/TestsAutoLoader.php" ); +MediaWikiPHPUnitCommand::main(); + diff --git a/tests/phpunit/run-tests.bat b/tests/phpunit/run-tests.bat new file mode 100644 index 00000000..e6eb3e0c --- /dev/null +++ b/tests/phpunit/run-tests.bat @@ -0,0 +1 @@ +php phpunit.php --configuration suite.xml %* diff --git a/tests/phpunit/skins/SideBarTest.php b/tests/phpunit/skins/SideBarTest.php new file mode 100644 index 00000000..47182a71 --- /dev/null +++ b/tests/phpunit/skins/SideBarTest.php @@ -0,0 +1,175 @@ +<?php + +/** + * @group Skin + */ +class SideBarTest extends MediaWikiLangTestCase { + + /** A skin template, reinitialized before each test */ + private $skin; + /** Local cache for sidebar messages */ + private $messages; + + function __construct() { + parent::__construct(); + } + + /** Build $this->messages array */ + private function initMessagesHref() { + # List of default messages for the sidebar: + $URL_messages = array( + 'mainpage', + 'portal-url', + 'currentevents-url', + 'recentchanges-url', + 'randompage-url', + 'helppage', + ); + + foreach( $URL_messages as $m ) { + $titleName = MessageCache::singleton()->get($m); + $title = Title::newFromText( $titleName ); + $this->messages[$m]['href'] = $title->getLocalURL(); + } + } + + function setUp() { + parent::setUp(); + $this->initMessagesHref(); + $this->skin = new SkinTemplate(); + } + function tearDown() { + parent::tearDown(); + $this->skin = null; + } + + /** + * Internal helper to test the sidebar + * @param $expected + * @param $text + * @param $message (Default: '') + */ + private function assertSideBar( $expected, $text, $message = '' ) { + $bar = array(); + $this->skin->addToSidebarPlain( $bar, $text ); + $this->assertEquals( $expected, $bar, $message ); + } + + function testSidebarWithOnlyTwoTitles() { + $this->assertSideBar( + array( + 'Title1' => array(), + 'Title2' => array(), + ), +'* Title1 +* Title2 +' + ); + } + + function testExpandMessages() { + $this->assertSidebar( + array( 'Title' => array( + array( + 'text' => 'Help', + 'href' => $this->messages['helppage']['href'], + 'id' => 'n-help', + 'active' => null + ) + )), +'* Title +** helppage|help +' + ); + } + + function testExternalUrlsRequireADescription() { + $this->assertSidebar( + array( 'Title' => array( + # ** http://www.mediawiki.org/| Home + array( + 'text' => 'Home', + 'href' => 'http://www.mediawiki.org/', + 'id' => 'n-Home', + 'active' => null, + 'rel' => 'nofollow', + ), + # ** http://valid.no.desc.org/ + # ... skipped since it is missing a pipe with a description + )), +'* Title +** http://www.mediawiki.org/| Home +** http://valid.no.desc.org/ +' + + ); + + } + + + #### Attributes for external links ########################## + private function getAttribs( ) { + # Sidebar text we will use everytime + $text = '* Title +** http://www.mediawiki.org/| Home'; + + $bar = array(); + $this->skin->addToSideBarPlain( $bar, $text ); + + return $bar['Title'][0]; + } + + /** + * Simple test to verify our helper assertAttribs() is functional + * Please note this assume MediaWiki default settings: + * $wgNoFollowLinks = true + * $wgExternalLinkTarget = false + */ + function testTestAttributesAssertionHelper() { + $attribs = $this->getAttribs(); + + $this->assertArrayHasKey( 'rel', $attribs ); + $this->assertEquals( 'nofollow', $attribs['rel'] ); + + $this->assertArrayNotHasKey( 'target', $attribs ); + } + + /** + * Test wgNoFollowLinks in sidebar + */ + function testRespectWgnofollowlinks() { + global $wgNoFollowLinks; + $saved = $wgNoFollowLinks; + $wgNoFollowLinks = false; + + $attribs = $this->getAttribs(); + $this->assertArrayNotHasKey( 'rel', $attribs, + 'External URL in sidebar do not have rel=nofollow when wgNoFollowLinks = false' + ); + + // Restore global + $wgNoFollowLinks = $saved; + } + + /** + * Test wgExternaLinkTarget in sidebar + */ + function testRespectExternallinktarget() { + global $wgExternalLinkTarget; + $saved = $wgExternalLinkTarget; + + $wgExternalLinkTarget = '_blank'; + $attribs = $this->getAttribs(); + $this->assertArrayHasKey( 'target', $attribs ); + $this->assertEquals( $attribs['target'], '_blank' ); + + $wgExternalLinkTarget = '_self'; + $attribs = $this->getAttribs(); + $this->assertArrayHasKey( 'target', $attribs ); + $this->assertEquals( $attribs['target'], '_self' ); + + // Restore global + $wgExternalLinkTarget = $saved; + } + +} diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml new file mode 100644 index 00000000..e6649beb --- /dev/null +++ b/tests/phpunit/suite.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- colors don't work on Windows! --> +<phpunit bootstrap="./bootstrap.php" + colors="false" + backupGlobals="false" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + stopOnFailure="false" + strict="true" + verbose="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> diff --git a/tests/phpunit/suites/ExtensionsTestSuite.php b/tests/phpunit/suites/ExtensionsTestSuite.php new file mode 100644 index 00000000..d728807f --- /dev/null +++ b/tests/phpunit/suites/ExtensionsTestSuite.php @@ -0,0 +1,33 @@ +<?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 MediaWikiTestCase { + public function testNothing() { + $this->assertTrue( true ); + } +} diff --git a/tests/phpunit/suites/UploadFromUrlTestSuite.php b/tests/phpunit/suites/UploadFromUrlTestSuite.php new file mode 100644 index 00000000..9b666825 --- /dev/null +++ b/tests/phpunit/suites/UploadFromUrlTestSuite.php @@ -0,0 +1,178 @@ +<?php + +require_once( dirname( dirname( __FILE__ ) ) . '/includes/upload/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, + $wgNamespaceAliases, $wgNamespaceProtection, $wgLocalFileRepo, + $parserMemc, $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; + $context = new RequestContext; + $wgLang = $context->getLang(); + $wgOut = $context->getOutput(); + $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], array( $wgParserConf ) ); + $wgRequest = new WebRequest; + + 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; + } +} |